From fc54121753833cbdd0039107d02074359d29a6b1 Mon Sep 17 00:00:00 2001 From: mayrang Date: Sat, 21 Mar 2026 18:44:21 +0900 Subject: [PATCH 1/3] =?UTF-8?q?Refactor:=20Phase=201=20=EC=9E=94=EC=97=AC?= =?UTF-8?q?=EB=AC=BC=20=EC=A0=95=EB=A6=AC=20+=20Phase=202=20entities=20?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=96=B4=20=EA=B5=AC=EC=B6=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Phase 1 잔여물 정리 - shared/ui/icons/ 생성 (10개) — cross-layer deps 해소 (@/components/icons/ → @/shared/ui/icons/) - shared/ui/index.ts 생성 — barrel export (import { Button } from '@/shared/ui' 가능) - jest-axe 전체 적용 (25개 파일) — WCAG 위반 13개 발견 및 수정 - Select nested-interactive 제거, CommentInput 버튼 aria-label, TextareaField tabIndex - CheckingModal/NoticeModal/ResultModal labelId 연결 - BottomSheetModal aria-label prop 지원 ## shared/api 구성 - shared/api/axiosInstance.ts — axios 인스턴스 + token refresh interceptor - shared/api/handleApiResponse.ts — ApiResponse 인터페이스 + 응답 처리 - console.log 9개 제거 (CLAUDE.md 규칙) - src/api/index.ts → re-export 래퍼로 교체 (17개 도메인 파일 변경 없음) ## shared/types 구성 - shared/types/error.ts — RequestErrorType - src/model/error.ts → re-export 래퍼로 교체 ## entities 레이어 구축 (16개 도메인) - contact, notification, bookmark, report, translation, requestedTrip - enrollment, comment, myTrip, search - user (auth.ts + profile.ts 통합), userProfile, trip (home.ts 병합) - tripDetail, community, myPage - 각 domain: model.ts + api.ts + index.ts - src/model/*.ts, src/api/*.ts → re-export 래퍼로 교체 (하위 호환) ## 레이어 위반 해소 - Filters 타입: hooks/search/useSearch → entities/search/model - IListParams 타입: hooks/useCommunity → entities/community/model 테스트: 134개 → 159개 (전부 통과) Co-Authored-By: Claude Sonnet 4.6 --- docs/progress.md | 20 +- docs/refactoring/phase-2.md | 277 ++++++++++++++++++ src/api/bookmark.ts | 53 +--- src/api/comment.ts | 102 +------ src/api/community.ts | 198 +------------ src/api/contact.ts | 18 +- src/api/enrollment.ts | 115 +------- src/api/home.ts | 63 +--- src/api/index.ts | 89 +----- src/api/myPage.ts | 253 +--------------- src/api/myTrip.ts | 48 +-- src/api/notification.ts | 20 +- src/api/report.ts | 35 +-- src/api/requestedTrip.ts | 31 +- src/api/search.ts | 44 +-- src/api/translation.ts | 21 +- src/api/trip.ts | 33 +-- src/api/tripDetail.ts | 99 +------ src/api/user.ts | 131 +-------- src/api/userProfile.ts | 49 +--- src/entities/bookmark/api.ts | 39 +++ src/entities/bookmark/index.ts | 1 + src/entities/comment/api.ts | 95 ++++++ src/entities/comment/index.ts | 2 + src/entities/comment/model.ts | 33 +++ src/entities/community/api.ts | 175 +++++++++++ src/entities/community/index.ts | 22 ++ src/entities/community/model.ts | 64 ++++ src/entities/contact/api.ts | 16 + src/entities/contact/index.ts | 2 + src/entities/contact/model.ts | 6 + src/entities/enrollment/api.ts | 102 +++++++ src/entities/enrollment/index.ts | 10 + src/entities/enrollment/model.ts | 4 + src/entities/myPage/api.ts | 190 ++++++++++++ src/entities/myPage/index.ts | 16 + src/entities/myPage/model.ts | 27 ++ src/entities/myTrip/api.ts | 41 +++ src/entities/myTrip/index.ts | 2 + src/entities/myTrip/model.ts | 25 ++ src/entities/notification/api.ts | 15 + src/entities/notification/index.ts | 2 + src/entities/notification/model.ts | 21 ++ src/entities/report/api.ts | 24 ++ src/entities/report/index.ts | 2 + src/entities/report/model.ts | 5 + src/entities/requestedTrip/api.ts | 27 ++ src/entities/requestedTrip/index.ts | 1 + src/entities/search/api.ts | 43 +++ src/entities/search/index.ts | 2 + src/entities/search/model.ts | 34 +++ src/entities/translation/api.ts | 19 ++ src/entities/translation/index.ts | 1 + src/entities/trip/api.ts | 60 ++++ src/entities/trip/index.ts | 8 + src/entities/trip/model.ts | 76 +++++ src/entities/tripDetail/api.ts | 67 +++++ src/entities/tripDetail/index.ts | 8 + src/entities/tripDetail/model.ts | 11 + src/entities/user/api.ts | 104 +++++++ src/entities/user/index.ts | 10 + src/entities/user/model.ts | 49 ++++ src/entities/userProfile/api.ts | 52 ++++ src/entities/userProfile/index.ts | 2 + src/entities/userProfile/model.ts | 40 +++ src/hooks/search/useSearch.ts | 11 +- src/hooks/useCommunity.tsx | 8 +- src/model/auth.ts | 32 +- src/model/comment.ts | 34 +-- src/model/community.ts | 60 +--- src/model/contact.ts | 8 +- src/model/enrollment.ts | 6 +- src/model/error.ts | 8 +- src/model/myPages.ts | 28 +- src/model/myTrip.ts | 27 +- src/model/notification.ts | 23 +- src/model/profile.ts | 22 +- src/model/search.ts | 34 +-- src/model/trip.ts | 78 +---- src/model/tripDetail.ts | 14 +- src/model/userProfile.ts | 43 +-- src/shared/api/axiosInstance.ts | 49 ++++ src/shared/api/handleApiResponse.ts | 25 ++ src/shared/api/index.ts | 2 + src/shared/types/error.ts | 6 + src/shared/types/index.ts | 1 + src/shared/ui/badge/Badge.test.tsx | 7 + src/shared/ui/button/ApplyListButton.tsx | 4 +- src/shared/ui/button/Button.test.tsx | 7 + .../ui/button/EditAndDeleteButton.test.tsx | 7 + src/shared/ui/button/FilterButton.test.tsx | 7 + src/shared/ui/button/FilterButton.tsx | 3 +- src/shared/ui/icons/CheckIcon.tsx | 17 ++ src/shared/ui/icons/EmptyHeartIcon.tsx | 35 +++ src/shared/ui/icons/FullHeartIcon.tsx | 30 ++ src/shared/ui/icons/InfoIcon.tsx | 31 ++ src/shared/ui/icons/ResetIcon.tsx | 30 ++ src/shared/ui/icons/RightVector.tsx | 33 +++ src/shared/ui/icons/SelectArrow.tsx | 16 + src/shared/ui/icons/UpArrowIcon.tsx | 35 +++ src/shared/ui/icons/Warning.tsx | 33 +++ src/shared/ui/icons/XIcon.tsx | 47 +++ src/shared/ui/index.ts | 9 + src/shared/ui/input/CodeInput.test.tsx | 8 + src/shared/ui/input/CommentInput.test.tsx | 7 + src/shared/ui/input/CommentInput.tsx | 6 +- src/shared/ui/input/InputField.test.tsx | 7 + src/shared/ui/input/RemoveButton.tsx | 2 +- src/shared/ui/input/StateInputField.test.tsx | 7 + src/shared/ui/input/StateInputField.tsx | 2 +- src/shared/ui/input/TextareaField.test.tsx | 7 + src/shared/ui/input/TextareaField.tsx | 1 + .../ui/input/ValidationInputField.test.tsx | 7 + src/shared/ui/input/ValidationInputField.tsx | 5 +- src/shared/ui/modal/BottomSheetModal.tsx | 4 + src/shared/ui/modal/CheckingModal.test.tsx | 7 + src/shared/ui/modal/CheckingModal.tsx | 4 +- .../ui/modal/EditAndDeleteModal.test.tsx | 7 + src/shared/ui/modal/EditAndDeleteModal.tsx | 2 +- src/shared/ui/modal/ImageModal.test.tsx | 7 + src/shared/ui/modal/NoticeModal.test.tsx | 7 + src/shared/ui/modal/NoticeModal.tsx | 4 +- src/shared/ui/modal/ReportModal.test.tsx | 7 + src/shared/ui/modal/ReportModal.tsx | 2 +- src/shared/ui/modal/ResultModal.test.tsx | 7 + src/shared/ui/modal/ResultModal.tsx | 6 +- src/shared/ui/profile/RoundedImage.test.tsx | 7 + src/shared/ui/select/Select.test.tsx | 7 + src/shared/ui/select/Select.tsx | 38 ++- src/shared/ui/tag/BoxLayoutTag.test.tsx | 7 + src/shared/ui/tag/SearchFilterTag.test.tsx | 7 + src/shared/ui/text/InfoText.test.tsx | 7 + src/shared/ui/text/InfoText.tsx | 2 +- src/shared/ui/text/TextButton.test.tsx | 9 + src/shared/ui/text/TextButton.tsx | 2 +- src/shared/ui/toast/BaseToast.test.tsx | 8 + src/shared/ui/toast/ErrorToast.test.tsx | 8 + src/shared/ui/toast/ErrorToast.tsx | 2 +- src/shared/ui/toast/TripToast.test.tsx | 7 + 139 files changed, 2591 insertions(+), 1767 deletions(-) create mode 100644 docs/refactoring/phase-2.md create mode 100644 src/entities/bookmark/api.ts create mode 100644 src/entities/bookmark/index.ts create mode 100644 src/entities/comment/api.ts create mode 100644 src/entities/comment/index.ts create mode 100644 src/entities/comment/model.ts create mode 100644 src/entities/community/api.ts create mode 100644 src/entities/community/index.ts create mode 100644 src/entities/community/model.ts create mode 100644 src/entities/contact/api.ts create mode 100644 src/entities/contact/index.ts create mode 100644 src/entities/contact/model.ts create mode 100644 src/entities/enrollment/api.ts create mode 100644 src/entities/enrollment/index.ts create mode 100644 src/entities/enrollment/model.ts create mode 100644 src/entities/myPage/api.ts create mode 100644 src/entities/myPage/index.ts create mode 100644 src/entities/myPage/model.ts create mode 100644 src/entities/myTrip/api.ts create mode 100644 src/entities/myTrip/index.ts create mode 100644 src/entities/myTrip/model.ts create mode 100644 src/entities/notification/api.ts create mode 100644 src/entities/notification/index.ts create mode 100644 src/entities/notification/model.ts create mode 100644 src/entities/report/api.ts create mode 100644 src/entities/report/index.ts create mode 100644 src/entities/report/model.ts create mode 100644 src/entities/requestedTrip/api.ts create mode 100644 src/entities/requestedTrip/index.ts create mode 100644 src/entities/search/api.ts create mode 100644 src/entities/search/index.ts create mode 100644 src/entities/search/model.ts create mode 100644 src/entities/translation/api.ts create mode 100644 src/entities/translation/index.ts create mode 100644 src/entities/trip/api.ts create mode 100644 src/entities/trip/index.ts create mode 100644 src/entities/trip/model.ts create mode 100644 src/entities/tripDetail/api.ts create mode 100644 src/entities/tripDetail/index.ts create mode 100644 src/entities/tripDetail/model.ts create mode 100644 src/entities/user/api.ts create mode 100644 src/entities/user/index.ts create mode 100644 src/entities/user/model.ts create mode 100644 src/entities/userProfile/api.ts create mode 100644 src/entities/userProfile/index.ts create mode 100644 src/entities/userProfile/model.ts create mode 100644 src/shared/api/axiosInstance.ts create mode 100644 src/shared/api/handleApiResponse.ts create mode 100644 src/shared/api/index.ts create mode 100644 src/shared/types/error.ts create mode 100644 src/shared/types/index.ts create mode 100644 src/shared/ui/icons/CheckIcon.tsx create mode 100644 src/shared/ui/icons/EmptyHeartIcon.tsx create mode 100644 src/shared/ui/icons/FullHeartIcon.tsx create mode 100644 src/shared/ui/icons/InfoIcon.tsx create mode 100644 src/shared/ui/icons/ResetIcon.tsx create mode 100644 src/shared/ui/icons/RightVector.tsx create mode 100644 src/shared/ui/icons/SelectArrow.tsx create mode 100644 src/shared/ui/icons/UpArrowIcon.tsx create mode 100644 src/shared/ui/icons/Warning.tsx create mode 100644 src/shared/ui/icons/XIcon.tsx create mode 100644 src/shared/ui/index.ts 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(