diff --git a/.github/workflows/code-review.yml b/.github/workflows/code-review.yml index a3e7b15d..5fa39280 100644 --- a/.github/workflows/code-review.yml +++ b/.github/workflows/code-review.yml @@ -1,8 +1,7 @@ name: Claude Code Review on: - pull_request: - types: [opened, synchronize] + workflow_dispatch: # 수동 실행만 허용 (API 크레딧 필요 — 현재 비활성화) permissions: contents: read diff --git a/CLAUDE.md b/CLAUDE.md index 89e5bb14..b5e3a557 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -106,6 +106,25 @@ src/ 6. **트러블슈팅** - 겪은 문제와 해결 과정 (블로그 소재로 가장 중요) 7. **회고 / 배운 점** - 다음에 다르게 할 것, 인사이트 +### PR 코드 리뷰 프로세스 + +> `claude-code-action` GitHub Actions 워크플로우는 **Anthropic API 크레딧이 필요**하므로 현재 비활성화 상태 (`.github/workflows/code-review.yml` → `workflow_dispatch` only). + +PR 리뷰는 **Claude Code CLI (Pro 모드)** 로 수동 진행한다. + +**절차:** +1. Claude Code 세션에서 PR diff 분석 요청 +2. 아래 체크 항목 기준으로 리뷰 작성: + - 코드 품질 및 가독성 + - FSD 레이어 규칙 준수 (shared → entities → features → widgets → pages → app) + - Tailwind 사용 (Emotion 신규 작성 금지) + - 버그 가능성 (타입 오류, 엣지 케이스 등) + - 웹 접근성 (aria 속성, 키보드 네비게이션) + - 보안 취약점 (XSS, 민감 정보 노출 등) +3. `gh pr comment {PR번호} --body "..."` 로 리뷰 코멘트 게시 + +--- + ### 코딩 규칙 - 새로 작성하는 코드는 반드시 Tailwind 사용 (Emotion 신규 작성 금지) - 새로 작성하는 컴포넌트는 FSD 구조에 맞게 위치 diff --git a/docs/progress.md b/docs/progress.md index 1da7bf62..d9e21822 100644 --- a/docs/progress.md +++ b/docs/progress.md @@ -7,7 +7,7 @@ | Phase 0 | 기반 구축 | ✅ 완료 | 2026-03-20 | 2026-03-20 | | Phase 1 | shared 레이어 | ✅ 완료 | 2026-03-20 | 2026-03-20 | | Phase 1.5 | 웹 접근성 보강 | ✅ 완료 | 2026-03-20 | 2026-03-20 | -| Phase 2 | entities 레이어 | 🔜 대기 | - | - | +| Phase 2 | entities 레이어 | ✅ 완료 | 2026-03-21 | 2026-03-21 | | Phase 3 | features 레이어 | 🔜 대기 | - | - | | Phase 4 | pages / widgets 레이어 | 🔜 대기 | - | - | @@ -69,13 +69,17 @@ _작업 완료 후 기록_ ## Phase 2: entities 레이어 ### 체크리스트 -- [ ] `model/` → `entities/{domain}/model` -- [ ] `api/` → `entities/{domain}/api` -- [ ] 도메인별 타입 정리 -- [ ] 각 entity 테스트 작성 - -### 변경 파일 목록 -_작업 완료 후 기록_ +- [x] Phase 1 잔여물 정리 (icons 크로스 레이어 의존성, barrel export, jest-axe 전체 적용) +- [x] `shared/api/` 구성 (axiosInstance, handleApiResponse 이전, console.log 제거) +- [x] `shared/types/` 구성 (RequestErrorType 이전) +- [x] `model/` + `api/` → `entities/{domain}/` (16개 도메인) +- [x] cross-layer 의존성 해소 (Filters, IListParams) + +### 결과 +- 생성: 65개 파일 (entities 48 + shared/api 3 + shared/types 2 + icons 11 + barrel 1) +- 테스트: 134개 → **159개** (전부 통과) +- console.log 위반: 9개 → 0개 +- 참조: [Phase 2 상세 문서](refactoring/phase-2.md) --- diff --git a/docs/refactoring/phase-2.md b/docs/refactoring/phase-2.md new file mode 100644 index 00000000..64f9a20c --- /dev/null +++ b/docs/refactoring/phase-2.md @@ -0,0 +1,277 @@ +# Phase 2: entities 레이어 구축 + +> 이 문서는 블로그/이력서 작성 재료입니다. 기술적 판단 근거와 트러블슈팅 과정을 상세히 기록합니다. + +## 1. 배경 및 문제 정의 + +### Phase 1 잔여물 + +Phase 1 완료 직후 발견된 3가지 잔여 문제가 있었다. + +**크로스 레이어 의존성**: `shared/ui` 내 10개 파일이 `@/components/icons/`를 직접 import하고 있었다. FSD에서 `shared` 레이어는 다른 레이어에 의존할 수 없는데, `components/`는 아직 FSD 외부의 레거시 폴더다. + +```ts +// 위반 예시 (shared → legacy components) +import UpArrowIcon from '@/components/icons/UpArrowIcon'; +``` + +**barrel export 없음**: `shared/ui/`에 `index.ts`가 없어 `import { Button } from '@/shared/ui'` 같은 FSD 표준 임포트가 불가능했다. + +**jest-axe 누락**: Phase 1.5에서 접근성 속성은 추가했지만, 27개 테스트 파일 중 25개에 `jest-axe` 자동 검사가 없었다. + +### entities 레이어 부재 + +Phase 2의 핵심 문제는 도메인 타입과 API 함수가 FSD 구조 밖에 있다는 것이었다. + +``` +Before: +src/model/ ← FSD 외부 (도메인 타입 14개 파일, 385줄) +src/api/ ← FSD 외부 (도메인 API 17개 파일, 1,232줄) +src/shared/api/ ← 비어있음 (.gitkeep) +src/entities/ ← 비어있음 (.gitkeep) +``` + +**문제점:** +- `shared/api/`에 axios 인스턴스가 없어 모든 도메인 API 파일이 `from "."` (api/index.ts)로 상대 import +- `console.log` 5개가 API 레이어에 남아 있음 (CLAUDE.md 금지 사항) +- 도메인 타입과 API가 같은 레이어에서 섞여 있어 레이어 경계 불분명 + +--- + +## 2. 선택지와 의사결정 + +### entities 이전 전략: Option A vs Option B + +Phase 2에서 가장 중요한 결정은 **entities와 features를 지금 나눌 것인가**였다. + +| 옵션 | 설명 | 장점 | 단점 | 선택 | +|------|------|------|------|------| +| **Option A** | 모든 API/타입을 일단 entities로 이전, Phase 3에서 entity/feature 경계 확정 | 빠름, 하위 호환 유지, Phase 3 리팩토링 시 임포트 경로 재검토 | entities에 feature성 API가 섞임 | ✅ | +| **Option B** | 처음부터 entity/feature 분리 (공유 여부로 판단) | 더 정확한 구조 | Phase 3 전 feature 레이어 작업이 필요, 범위 폭발 | ❌ | + +**결정 근거**: 현재 `hooks/`와 `page/`가 `api/`를 직접 참조하고 있어, Phase 3(feature) 진행 시 어차피 임포트 경로를 재검토하게 된다. 지금 단계에서 entity/feature 경계를 확정하려면 `hooks/`와 `page/`를 동시에 분석해야 하므로 범위가 2배 이상 커진다. + +> **FSD entity vs feature 핵심 기준**: "이 타입/API를 2개 이상의 feature에서 쓰나?" → entity. "이 시나리오에서만 쓰나?" → feature. + +### shared/api 분리 전략 + +`axiosInstance`를 `shared/api/`로 이전하되, 기존 17개 도메인 파일의 `from "."` import를 건드리지 않기 위해 `src/api/index.ts`를 re-export 래퍼로 교체했다. + +```ts +// 변경 전: src/api/index.ts (구현) +export const axiosInstance = axios.create({ ... }); + +// 변경 후: src/api/index.ts (re-export만) +export { axiosInstance, handleApiResponse, type ApiResponse } from '@/shared/api'; +``` + +--- + +## 3. 구현 과정 + +### 3-1. Phase 1 잔여물 정리 + +**icons 이전 (`shared/ui/icons/`)** + +`shared/ui` 내 10개 컴포넌트가 의존하는 아이콘을 `shared/ui/icons/`로 복사했다. 레거시 `components/icons/`는 삭제하지 않고 유지 (다른 레거시 코드가 아직 의존). + +``` +src/shared/ui/icons/ +├── CheckIcon.tsx +├── XIcon.tsx +├── UpArrowIcon.tsx +├── Warning.tsx +├── ResetIcon.tsx +├── RightVector.tsx +├── InfoIcon.tsx +├── EmptyHeartIcon.tsx +├── FullHeartIcon.tsx +└── SelectArrow.tsx +``` + +**barrel export 생성** + +```ts +// src/shared/ui/index.ts +export * from './button'; +export * from './input'; +export * from './modal'; +export * from './toast'; +export * from './badge'; +export * from './select'; +export * from './tag'; +export * from './text'; +export * from './profile'; +``` + +이후 `import { Button, Select, BaseModal } from '@/shared/ui'` 형태로 사용 가능. + +**jest-axe 전체 적용** + +25개 테스트 파일에 axe 검사를 추가하는 과정에서 실제 WCAG 위반 13개가 발견됐다. (트러블슈팅 섹션 참조) + +### 3-2. shared/api 구성 + +``` +src/shared/api/ +├── axiosInstance.ts ← axios 인스턴스 + token refresh interceptor +├── handleApiResponse.ts ← 응답 처리 함수 + ApiResponse 인터페이스 +└── index.ts ← barrel export +``` + +`console.log` 4개 제거 (CLAUDE.md 규칙 준수): +- `console.log("error console", error)` → 제거 +- `console.log("new AccessToken", ...)` → 제거 +- `console.log("response", response)` in handleApiResponse → 제거 + +### 3-3. shared/types 구성 + +``` +src/shared/types/ +├── error.ts ← RequestErrorType +└── index.ts ← barrel export +``` + +### 3-4. entities 16개 도메인 이전 + +각 도메인마다 동일한 구조로 생성: + +``` +entities/{domain}/ +├── model.ts ← 도메인 타입 (model/{domain}.ts에서 이전) +├── api.ts ← API 함수 (api/{domain}.ts에서 이전), @/shared/api import +└── index.ts ← public API barrel +``` + +이전된 도메인 목록: + +| 도메인 | model | api 함수 수 | 비고 | +|--------|-------|------------|------| +| contact | IContactCreate | 1 | | +| notification | INotificationContent, INotification | 1 | | +| bookmark | — | 3 | 불필요 import 제거 (ITripList, daysAgo, dayjs) | +| report | PostReport | 2 | console.log 2개 제거 | +| translation | — | 1 | | +| requestedTrip | — | 2 | | +| enrollment | IPostEnrollment | 7 | | +| comment | ICommentPost, IComment, ICommentList | 6 | | +| myTrip | IMyTripList | 3 | | +| search | IContent, ISearchData, **Filters** | 2 | Filters 레이어 위반 해소 | +| user | IRegisterEmail/Google/Kakao, TravelLog | 7 | auth.ts + profile.ts 통합 | +| userProfile | IUserProfileInfo, IUserRelatedTravel | 3 | | +| trip | ITripList, CreateTripReqData, UpdateTripReqData | 5 | home.ts API 병합 | +| tripDetail | ITripDetail | 5 | | +| community | IListParams, PostCommunity, Community 등 | 12 | IListParams 레이어 위반 해소 | +| myPage | ImyPage, IProfileImg, NewPasswordProps | 13 | console.log 3개 제거 | + +--- + +## 4. Before / After 비교 + +### 구조 변화 + +``` +Before: +src/ +├── model/ (14개 파일, FSD 외부) +├── api/ (17개 파일, FSD 외부) +├── shared/api/ (.gitkeep, 비어있음) +├── shared/types/ (.gitkeep, 비어있음) +└── entities/ (.gitkeep, 비어있음) + +After: +src/ +├── model/ (14개 파일, re-export 래퍼로 교체 → 하위 호환) +├── api/ (17개 파일, re-export 래퍼로 교체 → 하위 호환) +├── shared/api/ (axiosInstance.ts, handleApiResponse.ts, index.ts) +├── shared/types/ (error.ts, index.ts) +└── entities/ (16개 도메인 × 3파일 = 48개 파일) +``` + +### 수치 비교 + +| 지표 | Before | After | +|------|--------|-------| +| FSD 준수 파일 | 0개 (model/, api/ 모두 외부) | 48개 (entities/) | +| shared/api | 없음 | axiosInstance + handleApiResponse | +| console.log 위반 | 9개 | 0개 | +| 테스트 | 134개 | **159개** (+25개 jest-axe) | +| 크로스 레이어 의존성 (shared→legacy) | 10개 | 0개 | + +--- + +## 5. 트러블슈팅 + +### 문제 1: jest-axe에서 WCAG 위반 13개 발견 + +jest-axe를 전체 적용하자 첫 실행에서 13개 테스트가 실패했다. + +**발견된 위반 목록:** + +| 컴포넌트 | axe rule | 원인 | 수정 | +|---------|----------|------|------| +| Select | `button-name` | combobox에 접근성 이름 없음 | `aria-label` prop 추가 | +| Select | `nested-interactive` | `
  • ` 안에 `); expect(screen.getByTestId('child')).toBeInTheDocument(); }); + + it('접근성 위반이 없어야 한다', async () => { + const { container } = render(