Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions .claude/skills/write-tests/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
---
name: write-tests
description: docs/testing-guide.md 기준으로 지정한 경로의 코드에 맞는 테스트를 작성한다. FSD 레이어를 판단해 Jest+RTL / Storybook play 중 적합한 도구를 선택하고, 테스트 실행까지 확인한다.
arguments: [path]
---

# write-tests

테스트 전략 가이드와 대상 소스를 읽고, 누락된 테스트를 작성한다.

**참조 문서:**

- @docs/testing-guide.md — 도구 선택 기준 (Jest+RTL / Storybook play / Chromatic)
- @docs/architecture.md — FSD 레이어별 책임 범위
- @docs/conventions.md — 파일 네이밍 규칙

---

## 실행 절차

### 1단계 — 대상 파일 파악

`$path` 경로 아래의 모든 소스 파일을 읽는다.
이미 존재하는 `*.test.ts(x)`, `*.stories.tsx`가 있으면 함께 읽어 커버된 케이스를 파악한다.

테스트 대상 파일의 import 경로를 확인해, mock 경로도 **반드시 현재 실제 import 경로와 동일하게** 맞춘다.
(예: `@/components/...` 구경로가 아니라 `@/widgets/...` 등 현재 경로 사용)

### 2단계 — FSD 레이어 판단 및 도구 선택

경로에서 레이어를 확인하고 `docs/testing-guide.md`의 기준으로 도구를 결정한다.

| 경로 패턴 | 도구 |
| ------------------------------------- | -------------------------------- |
| `shared/lib`, `shared/utils` | Jest + RTL (필수) |
| `shared/hooks`, `shared/store` | Jest + RTL (`renderHook`) |
| `shared/ui` | Jest + RTL + Storybook play 검토 |
| `entities/model` | Jest + RTL |
| `entities/api` | Jest + RTL (apiClient 모킹) |
| `entities/query` | **테스트 작성 금지** |
| `features/mutation`, `features/hooks` | Jest + RTL (MSW 모킹) |
| `widgets/` | Jest + RTL (핵심 인터랙션만) |

Chromatic은 CI 설정이므로 코드로 작성하지 않는다. 해당 컴포넌트라면 주석으로 언급만 한다.

### 3단계 — 테스트 작성

**파일 위치:** 소스 파일과 같은 디렉터리에 `{SourceFile}.test.tsx` (또는 `.test.ts`)

**공통 보일러플레이트 (React Query가 필요한 경우):**

```tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});

function Wrapper({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}

return Wrapper;
};
```

**Next.js 라우터 모킹:**

```ts
jest.mock("next/navigation", () => ({
useRouter: () => ({ push: jest.fn(), replace: jest.fn(), back: jest.fn() }),
useParams: () => ({}),
}));
```

`useParams`, `useRouter`를 테스트 내에서 케이스별로 바꾸어야 하면 `jest.fn()`으로 모킹하고 `mockReturnValue`로 제어한다.

```ts
const mockUseParams = useParams as jest.MockedFunction<typeof useParams>;
mockUseParams.mockReturnValue({ teamId: "1" } as ReturnType<typeof useParams>);
```

**커스텀 훅 테스트 (renderHook) 기본 패턴:**

```ts
import { renderHook } from "@testing-library/react";

describe("useSomething", () => {
test("조건 A에서 기대값을 반환한다", () => {
const { result } = renderHook(() => useSomething());
expect(result.current).toBe(...);
});
});
```

**테스트 케이스 선정 기준:**

- 정상 동작 (happy path)
- 에러 / 빈 상태 / 로딩 상태
- 경계값 (빈 문자열, null, 최댓값)
- 사용자 인터랙션 후 상태 변화
- 이미 커버된 케이스는 작성하지 않는다

**describe / test 네이밍:**

```tsx
describe("{컴포넌트 또는 훅 이름}", () => {
describe("{기능 또는 상황}", () => {
test("{기대 동작을 한국어로}", () => { ... });
});
});
```

### 4단계 — 실행 및 검증

작성 후 아래 명령으로 실행한다.

```bash
pnpm test -- {작성한 테스트 파일 경로}
```

실패하면 에러를 읽고 수정한다. 모든 케이스가 통과할 때까지 반복한다.
테스트 통과 후, 수정 파일에 대한 lint 에러도 확인한다.

### 5단계 — 결과 보고

- 작성한 파일 목록
- 파일별 커버한 시나리오 요약
- 의도적으로 제외한 케이스가 있으면 이유 명시
- Storybook play가 추가로 필요하다 판단되면 언급
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,3 @@ next-env.d.ts

*storybook.log
storybook-static

CLAUDE.md
141 changes: 141 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# CLAUDE.md

This file provides guidance to Claude Code when working with this repository.
Always read [@docs/architecture.md](@docs/architecture.md) and [@docs/conventions.md](@docs/conventions.md) before generating or modifying code.

---

## Commands

```bash
pnpm dev # Next.js dev server (localhost:3000)
pnpm dev:full # Next.js + json-server mock (port 4000) concurrently
pnpm build # Production build
pnpm lint # ESLint (Flat Config)
pnpm test # Jest (unit/integration)
pnpm test:watch # Jest in watch mode
pnpm mock # json-server only (port 4000, watches db.json)
pnpm storybook # Storybook on port 6006
pnpm steiger # FSD architecture lint
pnpm steiger:watch # FSD architecture lint in watch mode
```

Run a single Jest test file:

```bash
pnpm test -- src/shared/hooks/useOverlay/index.test.tsx
```

---

## Architecture

### Feature-Sliced Design (FSD)

The `src/` directory follows [FSD](https://feature-sliced.design/) with strict top-to-bottom dependency flow:

```
app → pages, layouts, providers, Next.js route handlers
widgets → composite UI blocks composed from features/entities
features → business actions (mutations, forms, hooks per domain)
entities → domain models: api functions, query options, types
shared → reusable primitives: ui, hooks, lib, utils, store
```

Upper layers may import from lower layers only. Cross-slice imports at the same layer are forbidden.
See [@docs/architecture.md](@docs/architecture.md) for detailed layer rules and examples.

### ❌ NEVER DO (Critical Rules)

- **DO NOT** call `fetch` or `axios` directly in `features` or `widgets` — always use `entities/{domain}/query/` options
- **DO NOT** put business logic (domain-specific code) in `shared` — `shared` must be domain-agnostic
- **DO NOT** import across slices at the same layer (e.g., `features/todo` importing from `features/auth`)
- **DO NOT** add `'use client'` to components that don't need browser APIs or event handlers
- **DO NOT** create a new Zustand store outside of `shared/store/` or `features/{domain}/store/`
- **DO NOT** use raw hex colors or inline styles — always use design tokens from `globals.css`

### API Proxy

All client-side API calls go through `src/app/api/[...path]/route.ts`. This handler:

- Attaches `Authorization: Bearer <accessToken>` from cookies
- Automatically refreshes the token on 401/403 and retries
- Proxies to `BACKEND_URL` (server-side env var)

The frontend `apiClient` (`src/shared/lib/api/client.ts`) always calls `/api/...`.

### Data Fetching

- **React Query** for all server state. Query options live in `entities/{domain}/query/`
- Infinite scroll uses cursor-based pagination (`cursorDueDate` or `cursorCreatedAt` + `cursorId`)
- Default `staleTime`: 60s. `throwOnError: true` routes errors to `ErrorBoundary`

### Client State

- Zustand stores: `shared/store/` (global) or `features/{domain}/store/` (domain-scoped)
- Auth state uses `persist` + `immer` middleware, stored in `localStorage` under `taskmate-auth`

### Overlay / Modal System

```ts
const { open, close } = useOverlay();
open("modal-id", <MyModal />);
```

- Managed via Zustand layer stack
- `<Overlay />` in root layout renders the stack
- `exitOnUnmount: true` (default) clears all layers on unmount

### AsyncBoundary

```tsx
<AsyncBoundary
loadingFallback={<Spinner />}
errorFallback={(err, reset) => <ErrorUI />}
>
<DataComponent />
</AsyncBoundary>
```

Wraps `ErrorBoundary` + `Suspense` + `QueryErrorResetBoundary`. Always use this for data-fetching components.

### Styling

- **Tailwind CSS v4** with custom design tokens defined in `src/app/globals.css`
- Use `@theme` tokens: `--color-*`, `--text-*`, `typography-*` utilities
- Use `typography-{scale}` utility classes for text (e.g., `typography-body-2`, `typography-label-1`)
- Use `custom-scroll` class for scrollable containers
- **DO NOT** use arbitrary Tailwind values like `text-[14px]` — use defined tokens

### SVG Icons

SVGs under `shared/ui/Icon/svg/` are imported as React components via `@svgr/webpack`.

### MSW Mocking

MSW runs in `development` only. Initialized via `initMocks()` in `app/layout.tsx`.

- Server handler: `shared/mock/server.ts`
- Client handler: `shared/mock/browser.ts`

### Testing

- **Jest** (`pnpm test`): unit/integration tests. Config in `jest.config.ts`. Path alias `@/` → `src/`
- **Vitest** (`pnpm vitest`): Storybook story tests via `@storybook/addon-vitest` + Playwright

### Environment Variables

| Variable | Side | Purpose |
| -------------------------- | ----------- | ------------------------ |
| `BACKEND_URL` | Server only | Proxied backend base URL |
| `NEXT_PUBLIC_API_URL` | Client | Public API base URL |
| `NEXT_PUBLIC_SSE_BASE_URL` | Client | SSE endpoint base URL |
| `GOOGLE_CLIENT_ID` | Server | Google OAuth |
| `KAKAO_CLIENT_ID` | Server | Kakao OAuth |

---

## References

- Architecture & layer rules: [@docs/architecture.md](@docs/architecture.md)
- Naming & code conventions: [@docs/conventions.md](@docs/conventions.md)
38 changes: 38 additions & 0 deletions docs/app/page.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# app/page.tsx 작성 규칙

## 기본 원칙

- `page.tsx`는 웬만하면 서버 컴포넌트로 유지한다.
- 특별한 이유가 없으면 페이지 파일에 `'use client'`를 선언하지 않는다.
- 페이지는 데이터/상태 로직을 직접 가지기보다, 하위 `widgets`/`features`를 조합하는 역할에 집중한다.

## `'use client'`가 필요한 경우

아래 조건이 페이지 파일 자체에 직접 존재할 때만 허용한다.

- `useState`, `useEffect`, `useRef` 같은 클라이언트 훅을 페이지에서 직접 사용
- `onClick`, `onChange` 등 이벤트 핸들러를 페이지에서 직접 처리
- `window`, `document`, `localStorage` 같은 브라우저 API 접근
- 클라이언트 전용 컴포넌트에 함수/이벤트 핸들러를 props로 직접 전달해야 하는 경우

## 권장 패턴

- `Suspense`, `ErrorBoundary`, `AsyncBoundary` 같은 클라이언트 경계는 가능한 한 페이지가 아닌 `widgets`/`features` 내부로 내린다.
- 페이지에서는 `<Heading />`, `<Summary />`처럼 경계를 감싼 완성 컴포넌트를 조합만 한다.
- 에러/로딩 UI도 페이지에서 직접 조합하기보다 해당 위젯 내부에서 자체 처리한다.

## 예시

```tsx
// page.tsx (Server Component)
import { Heading, Summary } from "@/widgets/goal";

export default function Page() {
return (
<>
<Heading />
<Summary />
</>
);
}
```
Loading
Loading