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
116 changes: 116 additions & 0 deletions .claude/skills/create/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
---
name: create
description: 스펙 문서를 읽고 지정한 경로에 코드를 생성한다. FSD 레이어 책임 범위, conventions, code-quality 4원칙을 준수하며 테스트까지 작성한다.
arguments: [path, spec]
---

# create

스펙 문서와 대상 경로를 바탕으로 코드를 생성하고, 테스트까지 작성한다.

**참조 문서:**

- @docs/architecture.md — FSD 레이어별 책임 범위 및 의존성 방향
- @docs/conventions.md — 파일 네이밍 및 코드 컨벤션
- @docs/code-quality.md — 코드 품질 기준 (가독성 / 예측 가능성 / 응집도 / 결합도)
- @docs/testing-guide.md — 테스트 도구 선택 기준

---

## 실행 절차

### 1단계 — 스펙 파악

`$spec` 경로의 문서를 읽고 다음을 파악한다.

- 구현해야 할 기능과 동작 조건
- 필요한 props / 인터페이스 / 상태
- 예외 처리 및 에러 케이스
- 스펙에 명시되지 않은 부분은 architecture.md와 conventions.md를 기준으로 합리적으로 판단하고, 가정한 내용을 결과 보고에 명시한다.

### 2단계 — FSD 레이어 판단

`$path`에서 레이어를 확인하고 `docs/architecture.md` 기준으로 책임 범위를 파악한다.

- 생성할 코드가 해당 레이어의 책임에 맞는가?
- 의존성 방향이 올바른가? (상위 레이어 → 하위 레이어만 허용)
- 이미 존재하는 인접 파일이 있으면 함께 읽어 패턴을 맞춘다.

### 3단계 — 코드 생성

`docs/conventions.md`의 네이밍 규칙과 `docs/code-quality.md`의 4원칙을 준수하며 코드를 작성한다.

**가독성:**

- 동시에 실행되지 않는 분기는 컴포넌트/함수로 분리한다.
- 구현 세부사항은 적절히 추상화한다.
- 복잡한 조건과 매직 넘버에는 이름을 붙인다.
- 중첩 삼항 연산자 대신 if 문을 사용한다.
- 비교 연산은 왼쪽에서 오른쪽으로 읽히도록 작성한다. (`minPrice <= price && price <= maxPrice`)

**예측 가능성:**

- 같은 유형의 함수는 반환 타입을 통일한다. (API 훅은 Query 객체, validation 함수는 `{ ok, reason }`)
- 함수 이름·파라미터·반환값으로 예측할 수 없는 로직은 함수 밖으로 분리한다.
- 기존 코드베이스의 이름과 충돌하지 않도록 명확한 이름을 사용한다.

**응집도:**

- 함께 수정될 파일은 같은 디렉터리에 둔다.
- 매직 넘버는 상수로 선언해 변경 지점을 하나로 모은다.
- 폼 검증은 필드 레벨/폼 레벨 중 스펙에 맞는 방식을 선택한다.

**결합도:**

- 하나의 함수/훅이 하나의 책임만 가지도록 설계한다.
- Props Drilling이 발생하면 Composition 패턴으로 해소한다.
- 공통 추출보다 중복 허용이 나은 경우를 판단한다. (페이지마다 동작이 달라질 가능성이 있으면 중복 허용)

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

`docs/testing-guide.md`의 레이어별 기준에 따라 테스트를 작성한다.

**테스트 작성 판단 기준:**

- 버그가 생기면 치명적인가?
- 이 코드가 바뀔 가능성이 높은가?
- 로직이 복잡한가?

→ 하나라도 해당되면 Jest + RTL 테스트를 작성한다.

**레이어별 테스트 도구:**

| 경로 패턴 | Jest + RTL | Storybook Chromatic | Storybook play() |
| ------------------------------------- | --------------------- | ------------------- | ---------------- |
| `shared/lib`, `shared/utils` | 반드시 | — | — |
| `shared/hooks`, `shared/store` | 반드시 (`renderHook`) | — | — |
| `shared/ui` | 내부 로직이 있을 때만 | 항상 | 검토 |
| `entities/model`, `entities/api` | 반드시 | — | — |
| `entities/ui` | 검토 | 항상 | 항상 |
| `entities/query` | **작성 금지** | — | — |
| `features/ui` | 반드시 | 검토 | 검토 |
| `features/mutation`, `features/hooks` | 반드시 (MSW 모킹) | — | — |
| `widgets/` | 핵심 인터랙션만 | 검토 | 검토 |

### 5단계 — 검증

```bash
# 타입 체크
pnpm tsc --noEmit

# lint
pnpm lint {생성한 파일 경로}

# 테스트 실행
pnpm test -- {생성한 테스트 파일 경로}
```

실패하면 에러를 읽고 수정한다. 모든 항목이 통과할 때까지 반복한다.

### 6단계 — 결과 보고

- 생성한 파일 목록
- 파일별 역할 요약
- 스펙에 명시되지 않아 가정한 내용
- 원칙 간 트레이드오프가 있었던 경우 판단 이유
- Storybook / Chromatic이 추가로 필요하다 판단되면 언급
99 changes: 99 additions & 0 deletions .claude/skills/refactor/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
---
name: refactor
description: 지정한 경로의 코드를 docs/code-quality.md의 4원칙(가독성·예측 가능성·응집도·결합도) 기준으로 분석하고 개선한다. FSD 레이어와 conventions도 함께 준수한다.
arguments: [path]
---

# refactor

대상 코드를 읽고, 품질 기준에 맞게 개선한 뒤 lint와 테스트로 검증한다.

**참조 문서:**

- @docs/code-quality.md — 리팩터링 판단 기준 (가독성 / 예측 가능성 / 응집도 / 결합도)
- @docs/architecture.md — FSD 레이어별 책임 범위
- @docs/conventions.md — 파일 네이밍 및 코드 컨벤션

---

## 실행 절차

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

`$path` 경로 아래의 모든 소스 파일을 읽는다.
연관된 파일(import 대상, 같은 레이어의 인접 파일)도 함께 읽어 전체 맥락을 파악한다.

### 2단계 — 문제 분석

`docs/code-quality.md`의 4원칙 기준으로 개선이 필요한 부분을 파악한다.
각 문제에 대해 **어떤 원칙을 위반하는지**, **왜 문제인지**를 명시한다.

**가독성 체크리스트:**

- 동시에 실행되지 않는 코드가 한 함수/컴포넌트에 혼재하는가?
- 구현 세부사항이 불필요하게 노출되어 있는가?
- 로직 유형(query param, state 등)으로 함수를 묶고 있는가?
- 복잡한 조건이나 매직 넘버에 이름이 없는가?
- 중첩 삼항 연산자가 있는가?
- 코드를 위에서 아래로 읽을 때 눈의 이동이 많은가?

**예측 가능성 체크리스트:**

- 같은 이름을 가진 함수/변수가 다른 동작을 하는가?
- 유사한 함수의 반환 타입이 일치하지 않는가?
- 함수 이름·파라미터·반환값으로 예측할 수 없는 숨겨진 로직이 있는가?

**응집도 체크리스트:**

- 함께 수정될 파일이 서로 다른 디렉터리에 흩어져 있는가?
- 매직 넘버가 하드코딩되어 변경 지점이 여러 곳에 분산되어 있는가?
- 폼의 검증 로직이 적절한 레벨(필드/폼)에서 관리되고 있는가?

**결합도 체크리스트:**

- 하나의 함수/훅이 지나치게 넓은 책임을 가지고 있는가?
- 중복을 제거하기 위해 공통 훅/컴포넌트로 묶었지만 페이지마다 동작이 달라질 가능성이 있는가?
- Props Drilling이 발생하고 있는가?

**FSD/conventions 체크리스트:**

- 레이어 간 의존성 방향이 올바른가? (상위 레이어 → 하위 레이어)
- 파일명·함수명이 conventions.md를 준수하는가?

### 3단계 — 리팩터링 계획 수립

분석 결과를 바탕으로 수정할 항목을 우선순위와 함께 나열한다.

> 원칙 간 트레이드오프가 있을 때는 현재 코드의 맥락을 고려해 판단하고, 이유를 명시한다.
> 예: "응집도를 높이면 가독성이 낮아지지만, 이 값은 반드시 함께 수정되어야 하므로 응집도를 우선한다."

### 4단계 — 리팩터링 실행

계획한 순서대로 코드를 수정한다.

- 한 번에 하나의 원칙씩 수정하고, 각 수정이 다른 원칙을 침해하지 않는지 확인한다.
- import 경로는 **반드시 현재 실제 경로**를 사용한다. (구 경로 사용 금지)
- 수정 범위가 넓다면 파일별로 나누어 단계적으로 진행한다.

### 5단계 — 검증

```bash
# 타입 체크
pnpm tsc --noEmit

# lint
pnpm lint {수정한 파일 경로}

# 관련 테스트 실행
pnpm test -- {관련 테스트 파일 경로}
```

실패하면 에러를 읽고 수정한다. 모든 항목이 통과할 때까지 반복한다.

### 6단계 — 결과 보고

- 수정한 파일 목록
- 파일별 적용한 원칙과 변경 내용 요약
- 트레이드오프가 있었던 경우 판단 이유 명시
- 이번 리팩터링 범위에서 의도적으로 제외한 항목이 있다면 이유 명시
- 테스트가 추가로 필요하다 판단되면 언급 (`/write-tests` 실행 제안)
140 changes: 124 additions & 16 deletions .claude/skills/write-tests/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,39 @@ arguments: [path]

경로에서 레이어를 확인하고 `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 (핵심 인터랙션만) |
**도구 선택 전 판단 기준 (Jest + RTL):**

Chromatic은 CI 설정이므로 코드로 작성하지 않는다. 해당 컴포넌트라면 주석으로 언급만 한다.
다음 중 하나라도 해당되면 Jest + RTL 테스트를 작성한다.

### 3단계 — 테스트 작성
- 버그가 생기면 치명적인가?
- 이 코드가 바뀔 가능성이 높은가?
- 로직이 복잡한가?

**도구 선택 전 판단 기준 (Story 정의):**

다음 중 하나라도 해당되면 Story를 작성한다.

- 다른 UI에서 재사용될 가능성이 있는가?
- props나 상태 변경에 따른 UI 변화가 있는가?

**레이어별 도구 선택표:**

| 경로 패턴 | Jest + RTL | Storybook Chromatic | Storybook play() |
| ------------------------------------- | --------------------- | ------------------- | ---------------- |
| `shared/lib`, `shared/utils` | 반드시 | — | — |
| `shared/hooks`, `shared/store` | 반드시 (`renderHook`) | — | — |
| `shared/ui` | 내부 로직이 있을 때만 | 항상 | 검토 |
| `entities/model`, `entities/api` | 반드시 | — | — |
| `entities/ui` | 검토 | 항상 | 항상 |
| `entities/query` | **작성 금지** | — | — |
| `features/ui` | 반드시 | 검토 | 검토 |
| `features/mutation`, `features/hooks` | 반드시 (MSW 모킹) | — | — |
| `widgets/` | 핵심 인터랙션만 | 검토 | 검토 |

> `entities/query` — React Query 훅처럼 외부 라이브러리에 의존하는 레이어는 테스트 작성 금지.
> Chromatic은 CI 설정이므로 코드로 작성하지 않는다. 해당 레이어라면 주석으로 언급만 한다.

### 3단계 — Jest + RTL 테스트 작성

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

Expand Down Expand Up @@ -83,6 +102,25 @@ const mockUseParams = useParams as jest.MockedFunction<typeof useParams>;
mockUseParams.mockReturnValue({ teamId: "1" } as ReturnType<typeof useParams>);
```

**훅 반환값 mock 시 타입 단언 규칙:**

`UseMutationResult`, `UseQueryResult` 같이 필드 수가 많은 타입은 일부 필드만 제공한 객체를 `as ReturnType<...>` 단일 단언으로 캐스팅하면 TS 에러가 발생한다.
테스트에 필요한 필드만 제공할 때는 반드시 `as unknown as ReturnType<...>` 이중 단언을 사용한다.

```ts
// ❌ 단일 단언 — UseMutationResult와 구조가 충분히 겹치지 않아 TS 에러
mockUseXxxMutation.mockReturnValue({
mutate: mockMutate,
isPending: false,
} as ReturnType<typeof useXxxMutation>);

// ✅ 이중 단언 — unknown을 경유해 타입 검사를 우회
mockUseXxxMutation.mockReturnValue({
mutate: mockMutate,
isPending: false,
} as unknown as ReturnType<typeof useXxxMutation>);
```

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

```ts
Expand Down Expand Up @@ -114,7 +152,78 @@ describe("{컴포넌트 또는 훅 이름}", () => {
});
```

### 4단계 — 실행 및 검증
### 4단계 — Story 작성

2단계에서 Story 기준에 해당한다고 판단한 컴포넌트에 대해 `{SourceFile}.stories.tsx`를 작성한다.

**파일 위치:** 소스 파일과 같은 디렉터리

**기본 구조:**

```tsx
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { ComponentName } from "./ComponentName";

const meta = {
title: "{layer}/{domain}/{ComponentName}",
component: ComponentName,
tags: ["autodocs"],
} satisfies Meta<typeof ComponentName>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = { args: { ... } };
export const AnotherVariant: Story = { args: { ... } };
```

**Story 케이스 선정 기준:**

- props 값에 따른 시각적 상태 변화 (예: status별 색상, 개수별 레이아웃)
- 빈 상태 / 경계값 (예: 담당자 0명, 최대 초과)
- 이미 커버된 케이스는 작성하지 않는다

**복잡한 hook 의존성이 있는 경우:**

React Query가 필요한 컴포넌트는 전역 `preview.tsx`에 `QueryClientProvider` + `Suspense` 데코레이터가 있는지 확인한다.
없으면 story에 로컬 decorator로 추가한다.

```tsx
decorators: [
(Story) => (
<QueryClientProvider client={new QueryClient({ defaultOptions: { queries: { retry: false } } })}>
<Suspense fallback={null}><Story /></Suspense>
</QueryClientProvider>
),
],
```

MSW로 API를 인터셉트해야 하는 컴포넌트(`useSuspenseQuery` 내부 호출 포함)는
`.storybook/preview.tsx`에 `beforeAll`로 worker가 시작되는지 먼저 확인한다.
없으면 추가한 뒤 story를 작성한다.

```tsx
// .storybook/preview.tsx
import { worker } from "@/shared/mock/browser";
beforeAll: async () => {
await worker.start({ onUnhandledRequest: "bypass" });
},
```

`useParams`가 필요한 경우 `parameters.nextjs.navigation.segments`로 공급한다.

```tsx
parameters: {
nextjs: {
appDirectory: true,
navigation: {
segments: { goalId: "1" },
},
},
},
```

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

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

Expand All @@ -125,9 +234,8 @@ pnpm test -- {작성한 테스트 파일 경로}
실패하면 에러를 읽고 수정한다. 모든 케이스가 통과할 때까지 반복한다.
테스트 통과 후, 수정 파일에 대한 lint 에러도 확인한다.

### 5단계 — 결과 보고
### 6단계 — 결과 보고

- 작성한 파일 목록
- 작성한 파일 목록 (Jest 테스트 + Story 파일 모두)
- 파일별 커버한 시나리오 요약
- 의도적으로 제외한 케이스가 있으면 이유 명시
- Storybook play가 추가로 필요하다 판단되면 언급
Loading
Loading