diff --git a/.gitignore b/.gitignore index b3f3f04c..9cdb6e9b 100644 --- a/.gitignore +++ b/.gitignore @@ -126,3 +126,6 @@ src/app/**/dev/dashboard/ # ---> Deploy script (로컬 전용) deploy.sh + +# Claude 작업 문서 +claudedocs/ diff --git a/claudedocs/.DS_Store b/claudedocs/.DS_Store deleted file mode 100644 index 15466cfd..00000000 Binary files a/claudedocs/.DS_Store and /dev/null differ diff --git a/claudedocs/[FEAT-2026-03-11] qms-checklist-template-management.md b/claudedocs/[FEAT-2026-03-11] qms-checklist-template-management.md deleted file mode 100644 index 8d43b78a..00000000 --- a/claudedocs/[FEAT-2026-03-11] qms-checklist-template-management.md +++ /dev/null @@ -1,96 +0,0 @@ -# QMS 점검표 항목 관리 기능 - -## 개요 -품질인정심사 시스템(QMS)의 "화면 설정" 패널에 **점검표 항목 관리** 섹션을 추가하여, -카테고리/항목의 CRUD + 순서 변경 + 버전 관리를 지원한다. - -## 현재 구조 -- 점검표 데이터: `MOCK_DAY1_CATEGORIES` (mockData.ts) — Mock 상태 -- 타입: `ChecklistCategory` → `ChecklistSubItem[]` -- 설정 패널: `AuditSettingsPanel.tsx` — 레이아웃/점검표 옵션 토글만 존재 -- 데이터 훅: `useDay1Audit.ts` — `USE_MOCK = true` - -## 구현 범위 - -### 1. 점검표 템플릿 관리 UI (화면 설정 패널 내) -**위치**: AuditSettingsPanel → 새 섹션 "점검표 항목 관리" - -**기능**: -- 현재 버전 표시 + 버전 이력 드롭다운 -- 카테고리 CRUD (추가/수정/삭제) -- 하위 항목 CRUD (추가/수정/삭제) -- 순서 변경 (위/아래 버튼 — 드래그앤드롭 라이브러리 미사용) -- "저장 (새 버전 생성)" 버튼 → API 호출 -- "초기화" 버튼 → 마지막 저장 상태로 복원 - -### 2. 데이터 구조 (프론트) - -```typescript -// 점검표 템플릿 버전 -interface ChecklistTemplateVersion { - id: string; - version: number; - createdAt: string; - createdBy: string; - description?: string; // 변경 사유 -} - -// 점검표 템플릿 (API 응답) -interface ChecklistTemplate { - id: string; - currentVersion: number; - categories: ChecklistCategory[]; // 기존 타입 재사용 - versions: ChecklistTemplateVersion[]; -} -``` - -### 3. API 엔드포인트 (Mock → 추후 연동) - -| Method | Endpoint | 설명 | -|--------|----------|------| -| GET | `/api/v1/qms/checklist-templates/current` | 현재 템플릿 조회 | -| POST | `/api/v1/qms/checklist-templates` | 새 버전 저장 | -| GET | `/api/v1/qms/checklist-templates/versions` | 버전 이력 조회 | -| GET | `/api/v1/qms/checklist-templates/versions/:id` | 특정 버전 조회 | -| POST | `/api/v1/qms/checklist-templates/versions/:id/restore` | 버전 복원 | - -### 4. UI 구성 (설정 패널 내) - -``` -━━ 점검표 항목 관리 ━━ - -[v3 (2026-03-10) ▾] ← 버전 셀렉트 (이력 조회/복원) - -── 카테고리 ── -┌─────────────────────────────────────┐ -│ [⬆][⬇] 1. 원재료 품질관리 기준 [✏️][🗑] │ -│ [⬆][⬇] 수입검사 기준 확인 [✏️][🗑] │ -│ [⬆][⬇] 불합격품 처리 기준 확인 [✏️][🗑] │ -│ [⬆][⬇] 자재 보관 기준 확인 [✏️][🗑] │ -│ [+ 항목 추가] │ -├─────────────────────────────────────┤ -│ [⬆][⬇] 2. 제조공정 관리 기준 [✏️][🗑] │ -│ ... │ -└─────────────────────────────────────┘ -[+ 카테고리 추가] - -━━━━━━━━━━━━━━━━━━━━━━━━ -[초기화] [저장 (새 버전)] -``` - -### 5. 작업 목록 - -- [ ] types.ts에 템플릿 관련 타입 추가 -- [ ] ChecklistTemplateEditor 컴포넌트 생성 (편집 UI) -- [ ] AuditSettingsPanel에 탭/섹션 추가 ("화면 설정" / "점검표 관리") -- [ ] useChecklistTemplate 훅 생성 (상태 관리 + Mock 데이터) -- [ ] page.tsx 연동 (훅 → 설정 패널 props) -- [ ] 버전 이력 UI (Select 드롭다운 + 복원 확인) - -### 6. 설계 결정 - -- **드래그앤드롭 미사용**: 패키지 추가 없이 ⬆⬇ 버튼으로 순서 변경 -- **설정 패널 분리**: 기존 "화면 설정"과 "점검표 관리"를 탭으로 분리 -- **Mock 우선**: `USE_MOCK = true`로 시작, API 연동 시 교체 -- **인라인 편집**: 항목명 클릭 시 input으로 전환 (별도 모달 없음) -- **낙관적 업데이트**: 로컬 편집 → 저장 버튼 클릭 시 한번에 API 호출 diff --git a/claudedocs/[FIX-2026-02-26] windows-compatibility-checklist.md b/claudedocs/[FIX-2026-02-26] windows-compatibility-checklist.md deleted file mode 100644 index 9cab31c1..00000000 --- a/claudedocs/[FIX-2026-02-26] windows-compatibility-checklist.md +++ /dev/null @@ -1,109 +0,0 @@ -# Windows 호환성 개선 계획서 - -> 작성일: 2026-02-26 -> 배경: macOS 개발환경 → Windows 공장 PC 사용자 환경 차이로 인한 이슈 -> 상태: 계획 단계 - ---- - -## 완료된 작업 - -- [x] Popover 계열 컴포넌트 Windows 포커스 이슈 수정 (6개 파일) - - `ui/date-picker.tsx` — onPointerDownOutside/onInteractOutside 방어 - - `ui/time-picker.tsx` — 동일 - - `ui/date-range-picker.tsx` — 동일 - - `ui/multi-select-combobox.tsx` — 동일 - - `ui/searchable-select.tsx` — 동일 - - `molecules/ColumnSettingsPopover.tsx` — 동일 -- [x] 삭제 버튼 아이콘 X → Trash2 통일 (23개 파일) - ---- - -## Phase 1: IMPORTANT — 사용자 체감 영향 큰 항목 - -### 1-1. backdrop-filter 성능 최적화 -- **파일**: `src/app/globals.css` (line 269-280) -- **문제**: `backdrop-filter: blur()` 가 Windows GPU에서 비효율적 → 공장 PC에서 스크롤 버벅거림 -- **영향 범위**: `.clean-glass` 클래스 사용하는 전체 레이아웃 (Sidebar, Login 등) -- **수정 방향**: - - `prefers-reduced-motion` / `prefers-reduced-transparency` 미디어쿼리로 분기 - - 성능 낮은 환경에서는 `backdrop-filter: none` + 불투명 배경 대체 -- [ ] globals.css `.clean-glass` 수정 -- [ ] 적용된 컴포넌트에서 시각적 변화 확인 - -### 1-2. 폰트 굵기 렌더링 차이 보정 -- **파일**: `src/app/globals.css` (line 219-235), `src/app/[locale]/layout.tsx` (line 14-19) -- **문제**: Windows 폰트 렌더링이 macOS보다 ~0.5-1.5 weight 더 굵게 표시 (Pretendard 변수 폰트) -- **영향 범위**: 전체 UI 텍스트 -- **수정 방향**: - - `-webkit-font-smoothing: antialiased` 확인 (이미 적용됨) - - `font-weight: 400` 명시적 지정 - - 필요 시 Windows에서 `font-weight: 350` 적용 검토 (변수 폰트이므로 가능) -- [ ] 현재 폰트 설정 확인 -- [ ] Windows에서 시각 비교 테스트 후 보정값 결정 -- [ ] globals.css 수정 - -### 1-3. 스크롤바 동작 차이 -- **파일**: `src/app/globals.css` (line 332-412), `src/layouts/AuthenticatedLayout.tsx` (line 173-197) -- **문제**: macOS는 스크롤바 자동 숨김, Windows는 항상 표시 → 투명→페이드인 방식이 어색 -- **영향 범위**: 모든 스크롤 가능한 영역 -- **수정 방향**: - - 스크롤바 thumb을 기본 살짝 보이게 (`rgba(0,0,0,0.1)`) - - `.is-scrolling` 시 진하게 (`rgba(0,0,0,0.2)`) 유지 - - 너비 8px → 10-12px 로 Windows 기대치에 맞게 조정 검토 -- [ ] globals.css 스크롤바 스타일 수정 -- [ ] Windows에서 시각 확인 - -### 1-4. 숫자 포맷 locale 명시 -- **파일**: `src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx` (line 65-68) -- **문제**: `toLocaleString(undefined, ...)` → Windows 지역 설정에 따라 포맷 달라짐 -- **영향 범위**: 해당 페이지 숫자 표시 (다른 곳에도 동일 패턴 있는지 추가 검색 필요) -- **수정 방향**: - - `undefined` → `'ko-KR'` 명시적 locale 지정 - - 프로젝트 전체에서 동일 패턴 일괄 검색 후 수정 -- [ ] `toLocaleString(undefined` 패턴 전체 검색 -- [ ] locale을 `'ko-KR'`로 일괄 변경 - ---- - -## Phase 2: MINOR — 안정성/효율성 개선 - -### 2-1. number input 스피너 버튼 숨김 -- **파일**: 품질관리 문서 등 `input[type="number"]` 사용처 -- **문제**: Windows에서 ↑↓ 스피너 버튼 표시 → 터치스크린에서 실수 클릭 -- **수정 방향**: 전역 CSS로 스피너 숨김 처리 -- [ ] globals.css에 `input[type="number"]` 스피너 숨김 CSS 추가 - -### 2-2. 클립보드 API 에러 핸들링 -- **파일**: `src/app/[locale]/(protected)/dev/component-registry/ComponentRegistryClient.tsx` (line 44-48) -- **문제**: `navigator.clipboard.writeText()` 가 Windows 보안 정책으로 실패 가능 -- **수정 방향**: try-catch + `document.execCommand('copy')` fallback -- [ ] 클립보드 사용 코드 에러 핸들링 추가 - -### 2-3. 출퇴근 시계 갱신 주기 최적화 -- **파일**: `src/app/[locale]/(protected)/hr/attendance/page.tsx` (line ~170-180) -- **문제**: `setInterval(1000)` 매초 갱신 → 공장 PC CPU 부하 -- **수정 방향**: 날짜만 표시하므로 60초 간격으로 변경 -- [ ] setInterval 주기 1초 → 60초로 변경 - ---- - -## 테스트 체크리스트 - -모든 수정 후 Windows 환경에서 확인: - -- [ ] DatePicker: Dialog 안에서 날짜 선택 → 값 정상 입력 -- [ ] DatePicker: 이전/다음달 날짜 클릭 → 팝업 유지, 월 이동 -- [ ] TimePicker: Dialog 안에서 시간 선택 → 정상 동작 -- [ ] 스크롤: 메인 레이아웃 + 테이블 스크롤 부드러움 확인 -- [ ] 폰트: 텍스트 두께가 macOS와 비슷한 수준인지 확인 -- [ ] 숫자 포맷: 천단위 구분자, 소수점 정상 표시 -- [ ] number input: 스피너 버튼 안 보이는지 확인 - ---- - -## 참고 - -- Windows 공장 PC 사양: 보통 중저사양 (Intel i3-i5, 8GB RAM, 내장 GPU) -- 브라우저: Chrome 또는 Edge (Chromium 기반) -- 터치스크린 사용 가능성 있음 diff --git a/claudedocs/[FIX-2026-03-10] eslint-cleanup-checklist.md b/claudedocs/[FIX-2026-03-10] eslint-cleanup-checklist.md deleted file mode 100644 index b647ecaa..00000000 --- a/claudedocs/[FIX-2026-03-10] eslint-cleanup-checklist.md +++ /dev/null @@ -1,54 +0,0 @@ -# ESLint 코드 정리 체크리스트 - -## 점검 결과 요약 -- **TypeScript**: 0건 (완벽) -- **ESLint**: 923 errors + 220 warnings (1,529개 파일 중 399개) - -## 수정 대상 (exhaustive-deps 제외 - 동작 변경 위험) - -### ✅ 완료 - -| 룰 | 건수 | 상태 | 수정 내용 | -|---|---|---|---| -| `no-unreachable` | 7 | ✅ 완료 | 도달 불가 catch 블록 제거 (construction actions 3파일) | -| `no-constant-binary-expression` | 6 | ✅ 완료 | `false && ...` 조건 제거 (MasterFieldTab, SectionsTab) | -| `no-useless-escape` | 6 | ✅ 완료 | 불필요한 `\` 제거 (CurrencyField, currency-input, number-input, locale.ts) | -| `no-case-declarations` | 21 | ✅ 완료 | switch case에 `{}` 블록 추가 (5파일) | - -### ⏳ 미완료 - -| 룰 | 건수 | 상태 | 수정 방법 | -|---|---|---|---| -| `no-unused-vars` | 707 | ⏳ 대기 | `eslint-plugin-unused-imports` 자동 수정 예정 | - -## unused-vars 수정 계획 - -### 준비 상태 -- `eslint-plugin-unused-imports` 이미 설치됨 (npm install -D 완료) -- eslint.config.mjs 아직 미수정 - -### 실행 순서 -```bash -# 1. eslint.config.mjs에 플러그인 임시 추가 -# 2. npx eslint --fix src/ (unused-imports 룰만) -# 3. eslint.config.mjs 원복 -# 4. npx eslint src/ 로 결과 확인 -# 5. eslint-plugin-unused-imports 패키지 제거 -``` - -### unused-vars 파일 분포 (284개 파일) -- src/app/: 44파일 -- src/components/business/: 33파일 -- src/components/accounting+hr/: 42파일 -- src/components/items+orders+quotes+production/: 55파일 -- src/components/ 기타: 95파일 -- src/lib+stores+types/: 15파일 - -## 수정하지 않는 항목 - -| 룰 | 건수 | 사유 | -|---|---|---| -| `no-explicit-any` | 155 | warning 수준, 타입 정의 필요 (별도 작업) | -| `exhaustive-deps` | 24 | useEffect 재실행 빈도 변경 위험 | -| `no-img-element` | 39 | next/image 전환은 별도 작업 | -| `no-undef` | 168 | globals 설정 추가 필요 (sessionStorage 등) | diff --git a/claudedocs/[IMPL-2026-03-06] account-subject-unification-checklist.md b/claudedocs/[IMPL-2026-03-06] account-subject-unification-checklist.md deleted file mode 100644 index c5274bb5..00000000 --- a/claudedocs/[IMPL-2026-03-06] account-subject-unification-checklist.md +++ /dev/null @@ -1,123 +0,0 @@ -# 계정과목 통합 프로젝트 체크리스트 - -> 시작: 2026-03-06 -> 목표: 계정과목 마스터 통합 → 분개 흐름 통합 → 대시보드 연동 - ---- - -## Phase 1: 계정과목 마스터 강화 (백엔드) - -### 1-1. account_codes 테이블 확장 -- [x] 마이그레이션: sub_category(중분류), depth(계층), parent_code(상위계정), department_type(부문) 추가 -- [x] AccountCode 모델 업데이트 (fillable, casts, 관계) -- [x] AccountCodeService 확장 (계층 조회, 부문 필터 지원) -- [x] AccountSubjectController 확장 (새 필드 지원 API) -- [x] UpdateAccountSubjectRequest 생성 -- [x] 라우트 추가 (PUT /{id}, POST /seed-defaults) - -### 1-2. 표준 계정과목표 시드 데이터 (더존 Smart A 기준) -- [x] 시드 데이터 정의 (대분류 5개 + 중분류 12개 + 소분류 111개 = 128건) -- [x] seedDefaults() API 엔드포인트 (별도 Seeder 대신 API로 제공) -- [x] 기존 데이터와 충돌 방지 로직 (tenant_id+code 중복 시 skip) - ---- - -## Phase 2: 프론트 공용 컴포넌트 - -### 2-1. 공용 계정과목 설정 모달 (리스트 페이지용 - CRUD) -- [x] AccountSubjectSettingModal 공용 컴포넌트 생성 (src/components/accounting/common/) -- [x] 기존 GeneralJournalEntry/AccountSubjectSettingModal 코드 이관 + 확장 -- [x] 계층 표시 (depth별 들여쓰기: 대→중→소) -- [x] 부문 컬럼 추가 -- [x] "기본 계정과목 생성" 버튼 (seedDefaults API 연동) - -### 2-2. 공용 계정과목 Select (세부 페이지/모달용 - 조회/선택) -- [x] AccountSubjectSelect 공용 컴포넌트 생성 -- [x] DB 마스터 API 호출로 옵션 로드 (selectable=true, isActive=true) -- [x] 활성 계정과목만 표시 -- [x] "[코드] 계정과목명" 형태 표시 (예: [51100] 복리후생비(제조)) -- [x] 분류별 필터 지원 (props: category, subCategory, departmentType) - -### 2-3. 공용 타입/API 함수 -- [x] 공용 타입 정의 (src/components/accounting/common/types.ts) -- [x] 공용 actions.ts (계정과목 CRUD + seedDefaults + update API) -- [x] index.ts 배럴 파일 생성 - ---- - -## Phase 3: 7개 모듈 전환 (프론트) - -### 3-1. 일반전표입력 -- [x] 전용 AccountSubjectSettingModal → 공용 컴포넌트로 교체 -- [x] 전용 타입/API → 공용으로 교체 (actions.ts, types.ts 정리) -- [x] ManualJournalEntryModal: getAccountSubjects → 공용 actions -- [x] JournalEditModal: getAccountSubjects → 공용 actions -- [x] 전용 AccountSubjectSettingModal.tsx 삭제 - -### 3-2. 세금계산서관리 -- [x] JournalEntryModal: ACCOUNT_SUBJECT_OPTIONS → AccountSubjectSelect - -### 3-3. 카드사용내역 -- [x] JournalEntryModal: ACCOUNT_SUBJECT_OPTIONS → AccountSubjectSelect -- [x] ManualInputModal: ACCOUNT_SUBJECT_OPTIONS → AccountSubjectSelect -- [x] index.tsx 인라인 Select → AccountSubjectSelect -- 참고: ACCOUNT_SUBJECT_OPTIONS 상수는 엑셀 변환에서 기존 데이터 호환용으로 유지 - -### 3-4. 입금관리 — 스킵 (거래유형 분류이며 계정과목 코드가 아님) -### 3-5. 출금관리 — 스킵 (거래유형 분류이며 계정과목 코드가 아님) - -### 3-6. 미지급비용 -- [x] ACCOUNT_SUBJECT_OPTIONS → AccountSubjectSelect (category="expense" 필터) - -### 3-7. 매출관리 — 보류 (매출유형 분류이며 계정과목 코드가 아님) - ---- - -## Phase 4: 분개 흐름 통합 (백엔드) - -### 4-1. source_type 확장 -- [x] JournalEntry 모델에 SOURCE_CARD_TRANSACTION, SOURCE_TAX_INVOICE 상수 추가 -- [x] source_type은 string(30)이므로 enum 마이그레이션 불필요 (상수 추가만으로 완료) - -### 4-2. 세금계산서 분개 통합 -- [x] JournalSyncService 생성 (공용 분개 CRUD + expense 동기화) -- [x] TaxInvoiceController에 journal CRUD 메서드 추가 (get/store/delete) -- [x] 라우트 추가: GET/POST/PUT/DELETE /api/v1/tax-invoices/{id}/journal-entries -- [x] source_type = 'tax_invoice', source_key = 'tax_invoice_{id}' - -### 4-3. 카드사용내역 분개 통합 -- [x] CardTransactionController에 journal CRUD 메서드 추가 (get/store) -- [x] 라우트 추가: GET/POST /api/v1/card-transactions/{id}/journal-entries -- [x] 카드 items → 차변(비용계정) + 대변(미지급금) 자동 변환 -- [x] source_type = 'card_transaction', source_key = 'card_{id}' - ---- - -## Phase 5: 대시보드 연동 - -### 5-1. expense_accounts 동기화 확장 -- [x] SyncsExpenseAccounts 트레이트 생성 (app/Traits/) -- [x] GeneralJournalEntryService → 트레이트 사용으로 전환 -- [x] JournalSyncService에서 트레이트 사용 (세금계산서/카드 분개 저장 시 자동 동기화) -- [x] source_type별 payment_method 자동 결정 (card_transaction → PAYMENT_CARD) -- [x] 모든 source_type에서 복리후생비/접대비 감지 - -### 5-2. 대시보드 집계 검증 -- [x] expense_accounts에 journal_entry_id/journal_entry_line_id 연결 (기존 마이그레이션 활용) -- [x] CEO 대시보드는 expense_accounts 테이블 기준 집계 → 모든 source_type 반영됨 - ---- - -## 작업 순서 및 의존성 - -``` -Phase 1 (백엔드 마스터 강화) - ↓ -Phase 2 (프론트 공용 컴포넌트) - ↓ -Phase 3 (7개 모듈 전환) — 모듈별 독립, 병렬 가능 - ↓ -Phase 4 (분개 흐름 통합) — Phase 3과 병렬 가능 - ↓ -Phase 5 (대시보드 연동) -``` diff --git a/claudedocs/[IMPL-2026-03-08] frontend-weekly-0302-0308.md b/claudedocs/[IMPL-2026-03-08] frontend-weekly-0302-0308.md deleted file mode 100644 index 3511af63..00000000 --- a/claudedocs/[IMPL-2026-03-08] frontend-weekly-0302-0308.md +++ /dev/null @@ -1,250 +0,0 @@ -# 프론트엔드 주간 구현내역 (2026-03-02 ~ 2026-03-08) - -> 총 커밋 59개 (feat 30 / fix 17 / refactor 3 / chore 3 / merge 1 / 기타 5) - ---- - -## 1. 품질관리 — Mock→API 전환 + 검사 모달/문서 대폭 개선 - -**커밋**: 50e4c72c, 563b240f, 899493a7, fe930b58, e7263fee, 4ea03922, 295585d8, c150d807, e75d8f9b (9개) -**변경 규모**: +2,210 / -566 라인 - -### 1-1. API 전환 -- `USE_MOCK_FALLBACK = false` 설정, Mock 데이터 제거 -- 엔드포인트: `/api/v1/quality/documents`, `/api/v1/quality/performance-reports` -- snake_case → camelCase 변환 함수 구현 -- InspectionFormData에 `clientId`, `inspectorId`, `receptionDate` 필드 추가 - -### 1-2. 검사 모달 개선 (InspectionInputModal) -- 일괄 합격/초기화 토글 버튼 추가 -- 시공 치수 필드 (너비/높이) 추가 -- 변경사유 입력 필드 추가 -- 사진 첨부 (최대 2장, base64) -- 이전/다음 개소 네비게이션 + 자동저장 -- 레거시 검사 데이터 통합 (합격/불합격/진행중/미완) - -### 1-3. 수주선택 모달 (OrderSelectModal) -- 발주처(clientName) 컬럼 추가 -- 동일 발주처 + 동일 모델 필터링 제약 -- `SearchableSelectionModal`에 `isItemDisabled` 콜백 추가 (공통 컴포넌트 확장) -- 비활성 항목 스타일링 + 전체선택 시 비활성 항목 제외 - -### 1-4. 제품검사 성적서 (FqcDocumentContent) -- 8컬럼 동적 렌더링: No / 검사항목 / 세부항목 / 검사기준 / 검사방법 / 검사주기 / 측정값 / 판정 -- rowSpan 병합: 카테고리 단일 + method+frequency 복합 병합 -- measurement_type별 처리: checkbox → 양호/불량, numeric → 숫자입력, none → 비활성 -- FQC 모드 우선 + legacy fallback 패턴 - -### 1-5. 제품검사 요청서 (FqcRequestDocumentContent) — 신규 -- 양식 기반 동적 렌더링 (template_id: 66) -- 결재라인 + 기본정보(7개) + 입력섹션(4개) + 사전통보 테이블 -- EAV 데이터 구조: section_id, column_id, row_index, field_key, field_value -- EAV 문서 없을 때 legacy fallback 적용 - -### 1-6. 수주 연결 동기화 -- order_ids 배열 매핑 (다중 수주 지원) -- 개소별 inspectionData 서버 저장 - -### 주요 파일 -- `src/components/quality/InspectionManagement/actions.ts` -- `src/components/quality/InspectionManagement/ProductInspectionInputModal.tsx` -- `src/components/quality/InspectionManagement/OrderSelectModal.tsx` -- `src/components/quality/InspectionManagement/documents/FqcDocumentContent.tsx` (신규) -- `src/components/quality/InspectionManagement/documents/FqcRequestDocumentContent.tsx` (신규) -- `src/components/organisms/SearchableSelectionModal/SearchableSelectionModal.tsx` - ---- - -## 2. 문서스냅샷 시스템 (Lazy Snapshot) — 신규 기능 - -**커밋**: 31f523c8, a1fb0d4f, 8250eaf2, 72a2a3e9, 04f2a8a7 (5개) -**변경 규모**: +300 라인 - -### 개요 -문서 저장/조회 시 `rendered_html` 스냅샷을 자동 캡처하여 백엔드에 전송하는 시스템. - -### 2-1. 수동 캡처 (저장 시) -- 검사성적서(InspectionReportModal): `contentWrapperRef.innerHTML` 캡처 → 저장 시 `rendered_html` 파라미터 포함 -- 작업일지(WorkLogModal): 동일 패턴 -- 수입검사(ImportInspectionInputModal): 오프스크린 렌더링 방식 - -### 2-2. Lazy Snapshot (조회 시 자동 캡처) -- 조건: `rendered_html === NULL`인 문서 조회 시 -- 동작: 500ms 지연 → innerHTML 캡처 → 백그라운드 PATCH -- 비차단(non-blocking): UI에 영향 없이 백그라운드 처리 -- `patchDocumentSnapshot()` 서버 액션으로 전송 - -### 2-3. 오프스크린 렌더링 유틸리티 -- `src/lib/utils/capture-rendered-html.tsx` (신규) -- 폼 HTML이 아닌 실제 문서 렌더링 결과를 캡처 -- readOnly 모드 자동 캡처 useEffect 제거 (불필요한 PUT 요청 방지) - -### 적용 범위 -| 문서 | 수동 캡처 | Lazy Snapshot | -|------|-----------|---------------| -| 검사성적서 | ✅ | ✅ | -| 작업일지 | ✅ | ✅ | -| 수입검사 | ✅ (오프스크린) | - | -| 제품검사 요청서 | ✅ | ✅ | - -### 주요 파일 -- `src/lib/utils/capture-rendered-html.tsx` (신규) -- `src/components/production/WorkOrders/documents/InspectionReportModal.tsx` -- `src/components/production/WorkerScreen/WorkLogModal.tsx` -- `src/components/material/ReceivingManagement/ImportInspectionInputModal.tsx` -- `src/components/production/WorkOrders/actions.ts` - ---- - -## 3. 생산지시 — API 연동 + 작업자 화면 + 중간검사 - -**커밋**: fa7efb7b, 8b6da749, 4331b84a, 0166601b, 8bcabafd, 2a2a356f, b45c35a5, 0b81e9c1 (8개) -**변경 규모**: +2,000 라인 - -### 3-1. 생산지시 목록/상세 API 연동 -- Mock → API 전환 (`executePaginatedAction` + `buildApiUrl` 패턴) -- 서버사이드 페이지네이션 + 동적 탭 카운트 (stats API) -- WorkOrder 상태 배지 6단계: 미배정→배정→작업중→검사→완료→출하 -- BOM null 상태 처리 - -### 3-2. 절곡 중간검사 입력 모달 (InspectionInputModal) -- 7개 제품 항목 통합 폼 -- 제품 ID 자동 매칭: 정규화 → 키워드 → 인덱스 fallback (3단계) -- cellValues 구조: `{bending_state, length, width, spacing}` -- PO 단위 데이터 공유 모델: 동일 PO 내 모든 아이템이 inspection_data 공유 - -### 3-3. 자재투입 모달 (MaterialInputModal) -- 동일 자재 다중 BOM 그룹 LOT 독립 관리 -- `bomGroupKey = ${item_id}-${category}-${partType}` 그룹핑 -- 카테고리 정렬: 가이드레일(1) → 하단마감재(2) → 셔터박스(3) → 연기차단재(4) -- FIFO 자동충전 시 그룹 간 물리적 LOT 가용량 추적 -- 번호 배지(①②③) + partType 배지 - -### 3-4. 공정 단계 검사범위 설정 (InspectionScope) — 신규 -- 전수검사 / 샘플링 / 그룹 3가지 타입 -- 샘플링 시 샘플 수(n) 입력 지원 -- StepForm 컴포넌트에 UI 추가, options JSON으로 API 저장 - -### 주요 파일 -- `src/components/production/ProductionOrders/actions.ts`, `types.ts` -- `src/components/production/WorkerScreen/InspectionInputModal.tsx` -- `src/components/production/WorkerScreen/MaterialInputModal.tsx` -- `src/components/production/WorkOrders/documents/TemplateInspectionContent.tsx` (신규) -- `src/components/process-management/StepForm.tsx` -- `src/types/process.ts` - ---- - -## 4. 출하/배차 — 배차 다중행 + 차량관리 API + 출고관리 - -**커밋**: 83a23701, 1d380578, 5ff5093d, f653960a, a4f99ae3, 03d129c3 (6개) -**변경 규모**: +2,400 / -1,100 라인 - -### 4-1. 배차정보 다중 행 API 연동 -- `vehicle_dispatches` 배열 지원 (기존 단일 배차 → 다중 배차) -- transform 함수: `transformApiToDetail`, `transformCreateFormToApi`, `transformEditFormToApi` 갱신 -- 레거시 단일 배차 필드 하위호환 유지 - -### 4-2. 배차차량관리 Mock→API 전환 -- `executePaginatedAction` + `buildApiUrl` 패턴 적용 -- `transformToListItem()` / `transformToDetail()` snake_case → camelCase 변환 -- 쿼리 파라미터: `search`, `status`, `start_date`, `end_date`, `page`, `per_page` - -### 4-3. 출고관리 목록 필드 매핑 -- `writer_name`, `writer_id`, `delivery_date` 등 5개 필드 API 매핑 추가 -- `OrderInfoApiData` 타입으로 주문 연결 정보 처리 - -### 4-4. 배차 상세/수정 레이아웃 개선 -- 기본정보 그리드: 1열 → 2×4열 레이아웃 - -### 4-5. 출하관리 캘린더 -- 기본 뷰: day → week-time 변경 - -### 주요 파일 -- `src/components/outbound/ShipmentManagement/actions.ts` -- `src/components/outbound/VehicleDispatchManagement/actions.ts` -- `src/components/outbound/ShipmentManagement/ShipmentDetail.tsx`, `ShipmentEdit.tsx` - ---- - -## 5. 전자결재 — 결재함 확장 + 연결문서 - -**커밋**: 181352d7, 72cf5d86 (2개) -**변경 규모**: +458 / -127 라인 - -### 5-1. 결재함 기능 확장 -- 결재함 API 연동: - - `GET /api/v1/approvals/inbox` — 결재함 목록 - - `GET /api/v1/approvals/inbox/summary` — 통계 - - `POST /api/v1/approvals/{id}/approve` — 승인 - - `POST /api/v1/approvals/{id}/reject` — 반려 -- 문서 상태: DRAFT → PENDING → APPROVED / REJECTED / CANCELLED - -### 5-2. 연결문서 기능 (LinkedDocumentContent) — 신규 -- 검사성적서, 작업일지 등을 결재 문서에 연결하여 렌더링 -- DocumentHeader 컴포넌트 활용, 결재라인/상태배지/메타 정보 표시 - -### 5-3. 모바일 반응형 -- AuthenticatedLayout: 사이드바/메인 콘텐츠 모바일 대응 -- HeaderFavoritesBar 전면 재설계 -- SearchableSelectionModal HTML 유효성 수정 - -### 주요 파일 -- `src/components/approval/ApprovalBox/actions.ts`, `index.tsx`, `types.ts` -- `src/components/approval/DocumentDetail/LinkedDocumentContent.tsx` (신규) -- `src/components/approval/DocumentDetail/DocumentDetailModalV2.tsx` -- `src/layouts/AuthenticatedLayout.tsx` -- `src/components/layout/HeaderFavoritesBar.tsx` - ---- - -## 6. CEO 대시보드 — API 연동 + 섹션 확장 + 리팩토링 - -**커밋**: 9ad4c8ee, 23fa9c0e, cde93336, 4e179d2e, db84d679, 1bccaffe, bec933b3, 1675f3ed (8개) -**별도 문서**: `claudedocs/dashboard/[VERIFY-2026-03-06] ceo-dashboard-data-flow-verification.md` - -### 주요 변경 -- SummaryNavBar 추가 (상단 요약 데이터 네비게이션) -- 접대비/복리후생비/매출채권/캘린더 섹션 개선 -- 컴포넌트 분리 및 모달/섹션 리팩토링 -- mockData/modalConfigs 정리 -- API 연동 강화 (회계/결재/HR 섹션) -- `invalidateDashboard()` 시스템 추가 (5개 도메인 연동) - ---- - -## 7. 회계 — 계정과목 공통화 + 어음 리팩토링 - -**커밋**: 7d369d14, 1691337f, a4f99ae3(일부) (3개) -**별도 문서**: `claudedocs/[IMPL-2026-03-06] account-subject-unification-checklist.md` - -### 주요 변경 -- AccountSubjectSelect 공통 컴포넌트: 7개 페이지에 일괄 적용 -- 매출/매입/부실채권/일일보고 UI 개선 -- BillManagement 섹션 분리: 11개 섹션 컴포넌트 + 커스텀 훅(`useBillForm`, `useBillConditions`) - ---- - -## 8. 기타 - -### E2E 테스트 -- `f5bdc5ba`: 11개 FAIL 시나리오 수정 후 전체 PASS - -### 인프라 -- `f9eea0c9`, `c18c68b6`: Slack 알림 채널 분리 (product_infra → deploy_react) -- `888fae11`: next dev에서 --turbo 플래그 제거 - ---- - -## 문서 현황 - -| 도메인 | 문서 상태 | -|--------|----------| -| 품질관리 Mock→API | ✅ 본 문서 §1 | -| 문서스냅샷 (Lazy Snapshot) | ✅ 본 문서 §2 | -| 생산지시 API 연동 | ✅ 본 문서 §3 | -| 출하/배차 API 연동 | ✅ 본 문서 §4 | -| 전자결재 확장 | ✅ 본 문서 §5 | -| CEO 대시보드 | ✅ 별도 문서 존재 | -| 계정과목 공통화 | ✅ 별도 문서 존재 | -| 백엔드 구현내역 | ✅ 일별 문서 존재 (03-02 ~ 03-08) | diff --git a/claudedocs/[IMPL-2026-03-10] notice-popup-display-integration.md b/claudedocs/[IMPL-2026-03-10] notice-popup-display-integration.md deleted file mode 100644 index 8b85b596..00000000 --- a/claudedocs/[IMPL-2026-03-10] notice-popup-display-integration.md +++ /dev/null @@ -1,103 +0,0 @@ -# [IMPL] 공지 팝업 사용자 표시 연동 - -> 관리자가 등록한 팝업을 사용자에게 자동 표시하는 기능 구현 - -## 현황 - -| 구분 | 상태 | -|------|------| -| 관리자 팝업 관리 UI (CRUD) | ✅ 완성 | -| 백엔드 API (`/api/v1/popups`) | ✅ 완성 | -| `NoticePopupModal` 표시 컴포넌트 | ✅ 완성 | -| 활성 팝업 조회 서버 액션 | ✅ 완성 | -| 레이아웃 자동 표시 연동 | ✅ 완성 | -| 부서별 팝업 필터링 (백엔드) | ✅ 완성 (2026-03-10) | -| 부서별 팝업 필터링 (프론트) | ✅ 완성 (2026-03-10) | -| 부서 선택 UI (관리자 폼) | ✅ 완성 (2026-03-10) | - -## 구현 범위 (프론트만) - -### 1. `getActivePopups()` 서버 액션 -- 위치: `src/components/common/NoticePopupModal/actions.ts` -- `GET /api/v1/popups?status=active` 호출 -- 기존 `PopupApiData` → `NoticePopupData` 변환 - -### 2. `NoticePopupContainer` 컴포넌트 -- 위치: `src/components/common/NoticePopupModal/NoticePopupContainer.tsx` -- 로그인 후 활성 팝업 fetch -- `isPopupDismissedForToday()` 필터링 -- 여러 개 팝업 순차 표시 (하나 닫으면 다음 팝업) - -### 3. `AuthenticatedLayout` 연동 -- `NoticePopupContainer` 렌더링 추가 - -## 기존 파일 활용 - -``` -src/components/common/NoticePopupModal/ -├── NoticePopupModal.tsx ← 기존 (수정 없음) -├── NoticePopupContainer.tsx ← 신규 -└── actions.ts ← 신규 - -src/components/settings/PopupManagement/ -├── utils.ts ← transformApiToFrontend 재사용 -└── types.ts ← PopupApiData 타입 재사용 - -src/layouts/AuthenticatedLayout.tsx ← NoticePopupContainer 추가 -``` - -## 동작 흐름 - -``` -로그인 → AuthenticatedLayout 마운트 - → NoticePopupContainer useEffect - → localStorage에서 user.department_id 조회 - → getActivePopups(departmentId) API 호출 - → 백엔드 scopeForUser(departmentId) 적용 - → target_type='all' 팝업 + 해당 부서 팝업 반환 - → 날짜 범위(startDate~endDate) 필터 - → isPopupDismissedForToday() 필터 - → 표시할 팝업 있으면 첫 번째 팝업 모달 표시 - → 닫기 클릭 → "오늘 하루 안 보기" 체크 시 localStorage 저장 - → 다음 팝업 표시 (없으면 종료) -``` - ---- - -## [2026-03-10] 부서별 팝업 필터링 + 부서 선택 UI - -### 배경 -팝업 대상이 "부서별"일 때 어떤 부서인지 선택할 수 없었고, 사용자에게도 부서 기반 필터링이 적용되지 않았음. - -### 변경사항 - -#### 백엔드 (sam-api) -- `MemberService::getUserInfoForLogin()` — 로그인 응답에 `department_id` 추가 -- `PopupService` — `scopeForUser(?int $departmentId)` 스코프로 부서별 필터링 - -#### 프론트엔드 -| 파일 | 변경 | -|------|------| -| `LoginPage.tsx` | localStorage user에 `department_id` 저장 | -| `NoticePopupContainer.tsx` | `user.department_id`를 `getActivePopups()`에 전달 | -| `popupDetailConfig.ts` | `target` 필드를 custom 렌더로 변경, `TargetSelectorField` 컴포넌트 추가 | -| `PopupDetailClientV2.tsx` | `handleSubmit`에서 `decodeTargetValue()`로 `targetDepartmentId` 추출 | -| `types.ts` | `Popup.targetId`, `Popup.targetName` 필드 추가 | -| `utils.ts` | `transformApiToFrontend`에 `targetId`, `targetName` 매핑 추가 | -| `actions.ts` | `getDepartmentList()` 서버 액션 추가 | - -### 핵심 구현: 대상 필드 값 인코딩 -```typescript -// 단일 form field에 target_type + department_id를 함께 저장 -encodeTargetValue('department', 13) → 'department:13' -decodeTargetValue('department:13') → { targetType: 'department', departmentId: 13 } -encodeTargetValue('all') → 'all' -``` - -### TargetSelectorField 동작 -``` -대상 Select: [전사 | 부서별] - → "부서별" 선택 시 → getDepartmentList() API 호출 - → 부서 Select 추가 표시: [개발팀 | 영업팀 | ...] - → 부서 선택 시 form value = 'department:13' -``` diff --git a/claudedocs/[PLAN-2026-03-06] account-subject-unification.md b/claudedocs/[PLAN-2026-03-06] account-subject-unification.md deleted file mode 100644 index 35b87d3f..00000000 --- a/claudedocs/[PLAN-2026-03-06] account-subject-unification.md +++ /dev/null @@ -1,498 +0,0 @@ -# 계정과목 통합 기획서 - -> 작성일: 2026-03-06 -> 상태: 진행중 -> 관련: `claudedocs/architecture/[ANALYSIS-2026-03-06] account-subject-comparison.md` - ---- - -## 1. 배경 및 목표 - -### 문제점 -현재 계정과목이 **7개 모듈에서 각자 하드코딩**으로 관리되고 있음. -- 일반전표만 DB 마스터(account_codes) 사용, 나머지는 프론트 상수 배열 -- 계정과목 등록은 일반전표 설정에서만 가능 -- 분개 데이터가 3개 테이블에 분산 (journal_entries, hometax_invoice_journals, barobill_card_transactions) -- CEO 대시보드 비용 집계가 일반전표 분개에서만 작동 - -### 목표 -1. **계정과목 마스터 통합**: 하나의 DB 테이블, 전 모듈 공유 -2. **공용 컴포넌트**: 설정 모달(CRUD) + Select(조회) 2개로 전 모듈 대응 -3. **분개 흐름 통합**: 모든 분개 → journal_entries 한 곳에 저장 -4. **대시보드 정확도**: 어디서 분개하든 비용 집계 정상 작동 - -### 회계담당자 요구사항 -- 계정과목을 번호 + 명칭으로 구분 (예: 5201 급여) -- 제조/회계 동일 명칭이지만 번호로 구분 가능해야 함 -- 등록하면 전체 공유, 개별 등록도 가능 - ---- - -## 2. 현재 상태 (AS-IS) - -### 2.1 모듈별 계정과목 관리 - -| 모듈 | 소스 | 옵션 수 | 필드명 | API 필드 | -|------|------|---------|--------|----------| -| 일반전표입력 | DB 마스터 | 동적 | accountSubjectId | account_subject_id | -| 세금계산서관리 | 프론트 상수 | 11개 | accountSubject | account_subject | -| 카드사용내역 | 프론트 상수 | 16개 | accountSubject | account_code | -| 입금관리 | 프론트 상수 | ~11개 | depositType | account_code | -| 출금관리 | 프론트 상수 | ~11개 | withdrawalType | account_code | -| 미지급비용 | 프론트 상수 | 9개 | accountSubject | account_code | -| 매출관리 | 프론트 상수 | 8개 | accountSubject | account_code | - -### 2.2 분개 저장 위치 - -| 소스 | 저장 테이블 | expense_accounts 동기화 | -|------|-----------|----------------------| -| 일반전표 (수기) | journal_entries + journal_entry_lines | O | -| 일반전표 (입출금 연동) | journal_entries + journal_entry_lines | O | -| 세금계산서 분개 | hometax_invoice_journals (별도) | X | -| 카드 계정과목 태그 | barobill_card_transactions.account_code | X | - -### 2.3 백엔드 현재 테이블 - -```sql --- account_codes (계정과목 마스터 - 일반전표만 사용) -id, tenant_id, code(10), name(100), category(enum), sort_order, is_active - --- journal_entries (분개 헤더) -id, tenant_id, entry_no, entry_date, entry_type, description, -total_debit, total_credit, status, source_type, source_key - --- journal_entry_lines (분개 상세) -id, journal_entry_id, tenant_id, line_no, account_code, account_name, -side(debit/credit), amount, trading_partner_id, trading_partner_name, description - --- hometax_invoice_journals (세금계산서 분개 - 별도) -id, tenant_id, hometax_invoice_id, nts_confirm_num, -dc_type, account_code, account_name, debit_amount, credit_amount, ... - --- barobill_card_transactions (카드 거래) -..., account_code, ... -``` - ---- - -## 3. 목표 상태 (TO-BE) - -### 3.1 통합 구조 - -``` -[계정과목 마스터] - account_codes 테이블 (확장) - ├── code: "5201" - ├── name: "급여" - ├── category: "expense" - ├── sub_category: "selling_admin" (판관비) - ├── parent_code: "52" (상위 그룹) - ├── depth: 3 (대=1, 중=2, 소=3) - └── department_type: "common" (공통/제조/관리) - -[분개 통합] - journal_entries (source_type으로 출처 구분) - ├── source_type: 'manual' ← 수기 전표 - ├── source_type: 'bank_transaction' ← 입출금 연동 - ├── source_type: 'tax_invoice' ← 세금계산서 (신규) - └── source_type: 'card_transaction' ← 카드사용내역 (신규) - -[프론트 공용 컴포넌트] - AccountSubjectSettingModal → 리스트 페이지에서 CRUD - AccountSubjectSelect → 세부 페이지/모달에서 선택 -``` - -### 3.2 데이터 흐름 (TO-BE) - -``` -계정과목 등록 (어느 페이지에서든) - → account_codes 테이블에 저장 - → 전 모듈에서 즉시 사용 가능 - -분개 입력 (어느 모듈에서든) - → journal_entries + journal_entry_lines에 저장 - → account_code는 account_codes 마스터 참조 - → expense_accounts 자동 동기화 (복리후생비/접대비) - → CEO 대시보드에 자동 반영 -``` - ---- - -## 4. Phase별 세부 구현 계획 - -### Phase 1: 백엔드 마스터 강화 - -#### 1-1. account_codes 테이블 확장 마이그레이션 - -```php -// database/migrations/2026_03_06_100000_enhance_account_codes_table.php -Schema::table('account_codes', function (Blueprint $table) { - $table->string('sub_category', 50)->nullable()->after('category') - ->comment('중분류 (current_asset, fixed_asset, selling_admin, cogs 등)'); - $table->string('parent_code', 10)->nullable()->after('sub_category') - ->comment('상위 계정과목 코드 (계층 구조)'); - $table->tinyInteger('depth')->default(3)->after('parent_code') - ->comment('계층 깊이 (1=대분류, 2=중분류, 3=소분류)'); - $table->string('department_type', 20)->default('common')->after('depth') - ->comment('부문 (common=공통, manufacturing=제조, admin=관리)'); - $table->string('description', 500)->nullable()->after('department_type') - ->comment('계정과목 설명'); -}); -``` - -**sub_category 값 목록:** - -| category | sub_category | 한글 | -|----------|-------------|------| -| asset | current_asset | 유동자산 | -| asset | fixed_asset | 비유동자산 | -| liability | current_liability | 유동부채 | -| liability | long_term_liability | 비유동부채 | -| capital | - | 자본 | -| revenue | sales_revenue | 매출 | -| revenue | other_revenue | 영업외수익 | -| expense | cogs | 매출원가 | -| expense | selling_admin | 판매비와관리비 | -| expense | other_expense | 영업외비용 | - -**department_type 값:** -- `common`: 공통 (모든 부문에서 사용) -- `manufacturing`: 제조 (매출원가 계정) -- `admin`: 관리 (판관비 계정) - -#### 1-2. AccountCode 모델 업데이트 - -```php -// app/Models/Tenants/AccountCode.php -protected $fillable = [ - 'tenant_id', 'code', 'name', 'category', - 'sub_category', 'parent_code', 'depth', 'department_type', - 'description', 'sort_order', 'is_active', -]; - -// 상수 -const DEPT_COMMON = 'common'; -const DEPT_MANUFACTURING = 'manufacturing'; -const DEPT_ADMIN = 'admin'; - -const DEPTH_MAJOR = 1; // 대분류 -const DEPTH_MIDDLE = 2; // 중분류 -const DEPTH_MINOR = 3; // 소분류 -``` - -#### 1-3. AccountCodeService 확장 - -기존 CRUD에 추가: -- `getHierarchical()`: 계층 구조 조회 (대-중-소 트리) -- `getByCategory(category, sub_category?)`: 분류별 조회 -- `getByDepartment(department_type)`: 부문별 조회 -- 필터: category, sub_category, department_type, depth, search, is_active - -#### 1-4. AccountSubjectController 확장 - -기존 엔드포인트 유지 + 확장: -``` -GET /api/v1/account-subjects ← 기존 (필터 파라미터 확장) - ?category=expense - &sub_category=selling_admin - &department_type=common - &depth=3 - &search=급여 - &is_active=true - &hierarchical=true ← 계층 구조 응답 옵션 - -POST /api/v1/account-subjects ← 기존 (새 필드 추가) -PATCH /api/v1/account-subjects/{id} ← 신규 (수정) -PATCH /api/v1/account-subjects/{id}/status ← 기존 -DELETE /api/v1/account-subjects/{id} ← 기존 - -POST /api/v1/account-subjects/seed-defaults ← 신규 (기본 계정과목표 일괄 생성) -``` - -#### 1-5. 표준 계정과목표 시드 데이터 - -``` -1xxx 자산 - 11xx 유동자산 - 1101 현금 - 1102 보통예금 - 1103 당좌예금 - 1110 매출채권(외상매출금) - 1120 선급금 - 1130 미수금 - 1140 가지급금 - 12xx 비유동자산 - 1201 토지 - 1202 건물 - 1210 기계장치 - 1220 차량운반구 - 1230 비품 - 1240 보증금 - -2xxx 부채 - 21xx 유동부채 - 2101 매입채무(외상매입금) - 2102 미지급금 - 2103 선수금 - 2104 예수금 - 2110 부가세예수금 - 2120 부가세대급금 - 22xx 비유동부채 - 2201 장기차입금 - -3xxx 자본 - 31xx 자본금 - 3101 자본금 - 32xx 잉여금 - 3201 이익잉여금 - -4xxx 수익 - 41xx 매출 - 4101 제품매출 - 4102 상품매출 - 4103 부품매출 - 4104 용역매출 - 4105 공사매출 - 4106 임대수익 - 42xx 영업외수익 - 4201 이자수익 - 4202 외환차익 - -5xxx 비용 - 51xx 매출원가 (제조) - 5101 재료비 ← department: manufacturing - 5102 노무비 ← department: manufacturing - 5103 외주가공비 ← department: manufacturing - 52xx 판매비와관리비 (관리) - 5201 급여 ← department: admin - 5202 복리후생비 ← department: admin - 5203 접대비 ← department: admin - 5204 세금과공과 ← department: admin - 5205 감가상각비 ← department: admin - 5206 임차료 ← department: admin - 5207 보험료(4대보험) ← department: admin - 5208 통신비 ← department: admin - 5209 수도광열비 ← department: admin - 5210 소모품비 ← department: admin - 5211 여비교통비 ← department: admin - 5212 차량유지비 ← department: admin - 5213 운반비 ← department: admin - 5214 재료비 ← department: admin (관리부문) - 5220 경비 ← department: admin - 53xx 영업외비용 - 5301 이자비용 - 5302 외환차손 - 5310 배당금지급 -``` - -기존 하드코딩 옵션과의 매핑: - -| 기존 하드코딩 (영문 키워드) | 매핑될 계정코드 | -|---------------------------|---------------| -| purchasePayment (매입대금) | 2101 매입채무 | -| advance (선급금) | 1120 선급금 | -| suspense (가지급금) | 1140 가지급금 | -| rent (임차료) | 5206 임차료 | -| salary (급여) | 5201 급여 | -| insurance (4대보험) | 5207 보험료 | -| tax (세금) | 5204 세금과공과 | -| utilities (공과금) | 5209 수도광열비 | -| expenses (경비) | 5220 경비 | -| salesRevenue (매출수금) | 4101~4106 매출 | -| accountsReceivable (외상매출금) | 1110 매출채권 | -| accountsPayable (외상매입금) | 2101 매입채무 | -| salesVat (부가세예수금) | 2110 부가세예수금 | -| purchaseVat (부가세대급금) | 2120 부가세대급금 | -| cashAndDeposits (현금및예금) | 1101~1103 현금/예금 | -| advanceReceived (선수금) | 2103 선수금 | - ---- - -### Phase 2: 프론트 공용 컴포넌트 - -#### 2-1. 파일 구조 - -``` -src/components/accounting/common/ -├── types.ts # 공용 타입 정의 -├── actions.ts # 공용 계정과목 API 함수 -├── AccountSubjectSettingModal.tsx # 설정 모달 (CRUD) -└── AccountSubjectSelect.tsx # Select 컴포넌트 (조회/선택) -``` - -#### 2-2. 공용 타입 (types.ts) - -```typescript -export interface AccountSubject { - id: string; - code: string; // "5201" - name: string; // "급여" - category: AccountCategory; // 'asset' | 'liability' | 'capital' | 'revenue' | 'expense' - subCategory: string | null; - parentCode: string | null; - depth: number; // 1=대, 2=중, 3=소 - departmentType: string; // 'common' | 'manufacturing' | 'admin' - description: string | null; - isActive: boolean; -} - -// Select에서 표시할 때: `[${code}] ${name}` → "[5201] 급여" -``` - -#### 2-3. 공용 actions.ts - -```typescript -'use server'; -// 계정과목 조회 (Select용 - 활성만) -export async function getAccountSubjects(params?) - -// 계정과목 CRUD (설정 모달용) -export async function createAccountSubject(data) -export async function updateAccountSubject(id, data) -export async function updateAccountSubjectStatus(id, isActive) -export async function deleteAccountSubject(id) - -// 기본 계정과목표 일괄 생성 -export async function seedDefaultAccountSubjects() -``` - -#### 2-4. AccountSubjectSettingModal (설정 모달) - -기존 GeneralJournalEntry/AccountSubjectSettingModal 기반 확장: -- 계층 구조 표시 (번호대별 그룹핑 또는 들여쓰기) -- 대분류/중분류/부문 필터 -- 등록: 코드 + 명칭 + 분류 + 중분류 + 부문 -- 수정: 명칭, 분류, 상태 -- 삭제: 미사용 계정만 -- "기본 계정과목표 불러오기" 버튼 (초기 세팅용) - -#### 2-5. AccountSubjectSelect (Select 컴포넌트) - -```typescript -interface AccountSubjectSelectProps { - value: string; // 선택된 계정과목 code - onValueChange: (code: string) => void; - category?: AccountCategory; // 특정 분류만 표시 - subCategory?: string; // 특정 중분류만 표시 - departmentType?: string; // 특정 부문만 표시 - placeholder?: string; - disabled?: boolean; - className?: string; - size?: 'default' | 'sm'; -} -``` - -사용 예시: -```tsx -// 세금계산서 분개 - 전체 계정과목 - - -// 카드내역 - 비용 계정만 - - -// 입금관리 - 수익 + 자산 계정 - -``` - ---- - -### Phase 3: 7개 모듈 전환 - -각 모듈에서: -1. 하드코딩 ACCOUNT_SUBJECT_OPTIONS 상수 **제거** -2. Radix Select → **AccountSubjectSelect** 교체 -3. 리스트 페이지에 **설정 모달 버튼** 추가 (필요한 곳만) -4. API 저장 시 영문 키워드 → **계정코드(숫자)** 로 변경 - -#### 데이터 마이그레이션 고려 - -기존 데이터의 영문 키워드를 숫자 코드로 변환하는 마이그레이션 필요: -```php -// 예: barobill_card_transactions.account_code -// 'salary' → '5201' -// 'rent' → '5206' -``` - ---- - -### Phase 4: 분개 흐름 통합 - -#### 4-1. JournalEntry source_type 확장 - -```php -// JournalEntry 모델 -const SOURCE_MANUAL = 'manual'; -const SOURCE_BANK_TRANSACTION = 'bank_transaction'; -const SOURCE_TAX_INVOICE = 'tax_invoice'; // 신규 -const SOURCE_CARD_TRANSACTION = 'card_transaction'; // 신규 -``` - -#### 4-2. 세금계산서 분개 통합 - -현재: `/api/v1/tax-invoices/{id}/journal-entries` → hometax_invoice_journals 저장 -변경: 같은 엔드포인트 → journal_entries + journal_entry_lines 저장 - -- source_type = 'tax_invoice' -- source_key = 'tax_invoice_{id}' -- hometax_invoice_journals는 레거시 호환으로 유지 (향후 제거) - -#### 4-3. 카드사용내역 분개 통합 - -현재: `/api/v1/card-transactions/{id}/journal-entries` → barobill_card_transaction_splits -변경: 같은 엔드포인트 → journal_entries + journal_entry_lines 저장 - -- source_type = 'card_transaction' -- source_key = 'card_{id}' - ---- - -### Phase 5: 대시보드 연동 - -#### 5-1. expense_accounts 동기화 공용화 - -현재 GeneralJournalEntryService에만 있는 syncExpenseAccounts를: -- **JournalEntryService (공용)** 로 분리 -- 모든 분개 저장/수정/삭제 시 자동 호출 -- account_name에 '복리후생비' 또는 '접대비' 포함 → expense_accounts 동기화 - -#### 5-2. 검증 - -- 일반전표에서 복리후생비 분개 → 대시보드 반영 확인 -- 세금계산서에서 복리후생비 분개 → 대시보드 반영 확인 -- 카드내역에서 복리후생비 분개 → 대시보드 반영 확인 - ---- - -## 5. 작업 순서 및 의존성 - -``` -Phase 1: 백엔드 마스터 강화 - ├── 1-1. 마이그레이션 + 모델 - ├── 1-2. 서비스 + 컨트롤러 - └── 1-3. 시드 데이터 - ↓ -Phase 2: 프론트 공용 컴포넌트 - ├── 2-1. 공용 타입 + actions - ├── 2-2. AccountSubjectSettingModal - └── 2-3. AccountSubjectSelect - ↓ -Phase 3: 7개 모듈 전환 ──────────── Phase 4: 분개 흐름 통합 - ├── 3-1. 일반전표 ├── 4-1. source_type 확장 - ├── 3-2. 세금계산서 ├── 4-2. 세금계산서 분개 - ├── 3-3. 카드사용내역 └── 4-3. 카드 분개 - ├── 3-4. 입금관리 ↓ - ├── 3-5. 출금관리 Phase 5: 대시보드 연동 - ├── 3-6. 미지급비용 ├── 5-1. 동기화 공용화 - └── 3-7. 매출관리 └── 5-2. 검증 -``` - ---- - -## 6. 리스크 및 주의사항 - -| 리스크 | 대응 | -|--------|------| -| 기존 데이터 마이그레이션 | 영문 키워드 → 숫자 코드 변환 마이그레이션 작성 | -| 하드코딩 의존 코드 | 엑셀 다운로드 등에서 label 변환 로직 확인 | -| API 하위호환 | 기존 엔드포인트 유지, 새 필드는 optional | -| 시드 데이터 중복 | tenant별 기존 데이터 확인 후 없는 것만 추가 | diff --git a/claudedocs/[QA-2026-03-16] approval-module-qa-report.md b/claudedocs/[QA-2026-03-16] approval-module-qa-report.md deleted file mode 100644 index 38571e94..00000000 --- a/claudedocs/[QA-2026-03-16] approval-module-qa-report.md +++ /dev/null @@ -1,285 +0,0 @@ -# 결재 모듈 QA 검증 보고서 및 수정 계획서 - -**작성일**: 2026-03-16 -**검증 대상**: 결재관리 모듈 전체 (기안함, 결재함, 참조함, 완료함) -**검증 범위**: 문서 분류/양식 선택, 등록/수정/삭제, 벨리데이션, 파일업로드 -**상태**: Phase 0~3 완료, 버그 수정 5건 완료 및 재검수 통과, Phase 2-B 미완료 - ---- - -## Phase 0: 문서 분류 / 양식 선택 검증 ✅ 완료 - -### 7개 카테고리, 17개 양식 전체 목록 확인 - -| 카테고리 | 양식 수 | 양식 목록 | 상태 | -|---------|--------|----------|------| -| 일반 (3) | 3 | 근태신청, 사유서, 품의서 | ✅ | -| 경비 (2) | 2 | 지출결의서, 비용견적서 | ✅ | -| 인사 (2) | 2 | 연차사용촉진 통지서 (1차), 연차사용촉진 통지서 (2차) | ✅ | -| 총무 (2) | 2 | 공문서, 이사회의사록 | ✅ | -| 재무 (1) | 1 | 견적서 | ✅ | -| 총무/기타 (2) | 2 | 위임장, 사용인감계 | ✅ | -| 증명서 (5) | 5 | 사직서, 위촉증명서, 경력증명서, 재직증명서, 사용인감계 | ✅ | - -**결론**: 2단계 Select (카테고리 → 양식)이 정상 동작하며 모든 양식이 노출됨 - ---- - -## Phase 1: 등록/수정/삭제 검증 ✅ 완료 - -### Phase 1-A: 일반 카테고리 ✅ - -#### 품의서 (proposal) — 전용 폼 ✅ - -| 테스트 항목 | 결과 | 비고 | -|-----------|------|------| -| 양식 선택 → 폼 렌더링 | ✅ | 제목, 거래처, 내용, 사유, 예상비용, 첨부파일 | -| 미리보기 | ✅ | DocumentDetailModal에 정상 렌더링 | -| 벨리데이션 (결재선 미지정) | ✅ | "결재선을 지정해주세요" toast | -| 임시저장 | ✅ | AP-20260316-0001 발급 | -| 상신 | ✅ | AP-20260316-0002 발급, 결재대기 전환 | -| 수정 (기안함에서 클릭) | ✅ | 모든 필드 복원, 제목 변경 후 저장 성공 | -| 삭제 | ✅ | 확인 다이얼로그 후 삭제 성공 | - -#### 근태신청 (attendance_request) — 동적 폼 ✅ - -| 테스트 항목 | 결과 | 비고 | -|-----------|------|------| -| 양식 선택 → 폼 렌더링 | ✅ | DynamicFormRenderer 5필드 정상 | -| 미리보기 | ✅ | 동적 폼 미리보기 정상 | -| 임시저장 | ✅ | 부분 입력 시 성공 (빈 폼은 실패 — BUG #13) | -| 상신 | ✅ | 부분 입력으로도 상신 성공 | - -#### 사유서 (reason_report) — 동적 폼 ✅ - -| 테스트 항목 | 결과 | 비고 | -|-----------|------|------| -| 양식 선택 → 폼 렌더링 | ✅ | DynamicFormRenderer 정상 | -| 미리보기 | ✅ | 정상 | - -### Phase 1-B: 경비 카테고리 ✅ - -#### 지출결의서 (expenseReport) — 전용 폼 ✅ - -| 테스트 항목 | 결과 | 비고 | -|-----------|------|------| -| 양식 선택 → 폼 렌더링 | ✅ | 항목 추가/삭제 테이블, 카드 정보 | -| 미리보기 | ✅ | 정상 | -| 임시저장 → 수정 → 상신 | ✅ | 전체 CRUD 정상 | - -#### 비용견적서 (expenseEstimate) — 전용 폼 ✅ - -| 테스트 항목 | 결과 | 비고 | -|-----------|------|------| -| 양식 선택 → 폼 렌더링 | ✅ | 항목 테이블, 지출합계/계좌잔액/최종차액 자동계산 | -| 미리보기 | ✅ | 정상 | -| 임시저장 → 수정 → 상신 | ✅ | 전체 CRUD 정상 | - -### Phase 1-C: 나머지 카테고리 ✅ - -| 카테고리 | 양식 | 렌더링 | 미리보기 | 비고 | -|---------|------|--------|---------|------| -| 인사 | 연차촉진 1차 | ✅ | ✅ | 전체 CRUD 테스트 완료 | -| 인사 | 연차촉진 2차 | ✅ | ✅ | | -| 총무 | 공문서 | ✅ | ✅ | | -| 재무 | 견적서 | ✅ | ✅ | | -| 총무/기타 | 이사회의사록 | ✅ | ✅ | | -| 총무/기타 | 위임장 | ✅ | ✅ | 경미 a11y 이슈 (BUG #12) | -| 증명서 | 사용인감계 | ✅ | ✅ | 경미 a11y 이슈 (BUG #12) | -| 증명서 | 사직서 | ✅ | ✅ | | -| 증명서 | 위촉증명서 | ✅ | ✅ | | -| 증명서 | 경력증명서 | ✅ | ✅ | | -| 증명서 | 재직증명서 | ✅ | ✅ | | - ---- - -## Phase 2: 벨리데이션 체크 및 파일업로드 ✅ 완료 - -### 벨리데이션 테스트 결과 - -| 테스트 시나리오 | 결과 | 동작 | -|--------------|------|------| -| 결재자 미지정 → 상신 | ✅ | "결재선을 지정해주세요." toast (프론트엔드) | -| 결재자 미지정 + 빈 폼 → 상신 | ✅ | 결재선 검증이 먼저 작동 | -| 결재자 지정 + 빈 폼 → 상신 | ✅ | "내용은(는) 필수 항목입니다." toast (백엔드 API) | -| 빈 폼 → 임시저장 | ❌ BUG #13 | 백엔드가 임시저장에도 content 필수 검증 적용 | -| 부분 입력 → 임시저장 | ✅ | AP-20260316-0009 발급, 성공 | -| 부분 입력 → 상신 | ⚠️ | 성공하지만 필드별 검증 부재 (BUG #14) | -| 임시저장 반복 클릭 | ❌ BUG #11 | 매번 새 문서 생성 (중복) | - -### 파일 업로드 테스트 결과 - -| 테스트 항목 | 결과 | 비고 | -|-----------|------|------| -| FileDropzone 렌더링 (품의서) | ✅ | "클릭하거나 파일을 드래그하세요" | -| 이미지 파일 업로드 | ✅ | test-upload.png 정상 첨부 | -| 첨부 파일 표시 | ✅ | "test-upload.png (새 파일) 73 B" | -| 첨부 파일 삭제 | ✅ | 삭제 후 "첨부된 파일이 없습니다" 복원 | - ---- - -## Phase 2-B: 대시보드 연동 검증 ⏳ 미완료 - ---- - -## 발견된 버그 목록 (전체) - -### 🔴 CRITICAL - -#### BUG #11: 임시저장 후 URL 미갱신 → 중복 문서 생성 + 삭제 불가 - -**증상**: -1. 새 문서 작성(`?mode=new`)에서 임시저장 성공 후 URL이 `?mode=new`로 유지 -2. `isEditMode`가 false인 채로 유지됨 -3. 임시저장을 다시 클릭하면 `createApproval()` 재호출 → **매번 새 문서 생성** (AP-0009, AP-0010...) -4. 삭제 버튼 클릭 시 `isEditMode`가 false이므로 API 호출 없이 `router.back()` 실행 - -**재현**: 새 문서 → 내용 입력 → 임시저장 → 임시저장 반복 → 기안함에서 중복 문서 확인 - -**파일**: `src/components/approval/DocumentCreate/index.tsx` lines 526-569 - -**수정 방안**: -```typescript -// handleSaveDraft 성공 후 URL 갱신 추가 -if (result.success && result.data?.id) { - // URL을 edit 모드로 전환하여 이후 저장이 updateApproval을 호출하도록 - router.replace(`/approval/draft/new?id=${result.data.id}&mode=edit`, { scroll: false }); - // 또는 state로 관리 - setDocumentId(String(result.data.id)); -} -``` - -**우선순위**: 🔴 CRITICAL — 데이터 중복 생성, 삭제 불가 - ---- - -### 🟡 MEDIUM - -#### BUG #1: 상신 후 기안함 리다이렉트 시 목록 데이터 미로드 - -**증상**: 문서 상신 후 기안함으로 리다이렉트되지만 목록이 0건으로 표시. 새로고침 후 정상. - -**파일**: `src/components/approval/DocumentCreate/index.tsx` (handleSubmit → router.push) - -**수정 방안**: DraftBox의 데이터 로딩에 pathname 의존성 추가 또는 invalidate 후 딜레이 - -**우선순위**: 🟡 MEDIUM — 새로고침으로 해결 가능 - ---- - -#### BUG #13: 빈 폼 임시저장 시 백엔드 검증 에러 - -**증상**: 폼 필드를 하나도 입력하지 않은 상태에서 임시저장 클릭 시 "내용은(는) 필수 항목입니다." 에러 - -**원인**: 동적 폼의 `dynamicFormData`가 `{}`일 때 백엔드가 content 필수 검증 적용 - -**수정 방안**: -- 프론트엔드: 빈 폼일 때 프론트엔드에서 "최소 1개 필드를 입력해주세요" 안내 -- 또는 백엔드: 임시저장(`is_submitted=false`) 시 content 필수 검증 제외 - -**우선순위**: 🟡 MEDIUM — 임시저장 UX 개선 - ---- - -#### BUG #14: 부분 입력 폼 상신 시 필드별 벨리데이션 미비 - -**증상**: 근태신청에서 신청자와 사유만 입력하고 신청유형/기간/일수 미입력 상태로 상신 성공 - -**원인**: 백엔드에서 `content` JSON 내부 필드별 필수값 검증을 하지 않음 - -**수정 방안**: 백엔드에서 양식별 required 필드 검증 추가 필요 - -**우선순위**: 🟡 MEDIUM — 불완전한 문서가 상신될 수 있음 - ---- - -### 🟢 LOW - -#### BUG #12: 폼 헤더에 로딩 텍스트 a11y 이슈 - -**증상**: PowerOfAttorneyForm, SealUsageForm에서 `

` 안에 로딩 `` 포함 -- 로딩 중: "위임인 불러오는 중..." / "회사 정보 불러오는 중..."이 h3의 일부로 읽힘 -- 로딩 완료 후: 정상 - -**파일**: -- `src/components/approval/DocumentCreate/PowerOfAttorneyForm.tsx` line 47 -- `src/components/approval/DocumentCreate/SealUsageForm.tsx` line 101 - -**수정 방안**: 로딩 텍스트를 `

` 외부로 이동하거나 `aria-hidden` 추가 - -**우선순위**: 🟢 LOW — 일시적 상태, 기능 영향 없음 - ---- - -### ✅ 수정 완료 (이전 세션에서 해결) - -| 버그 | 증상 | 수정 내용 | -|------|------|----------| -| BUG #2 (서버 hang) | startTransition + 서버 액션 deadlock | startTransition 제거, try/catch 패턴 적용 | -| BUG #3 (Select 경고) | controlled/uncontrolled 전환 | value에 undefined 사용 + key prop | -| BUG #7 (content empty) | 전용 폼 content가 빈 객체 | getDocumentContent()에 구조화된 데이터 추가 | -| BUG #8 (명칭 불일치) | "지출 예상 내역서" → "비용견적서" | 11개 파일 명칭 통일 | -| BUG #9 (key 중복 에러) | 저장된 항목 복원 시 id 누락 | transformApiToFormData()에 fallback ID 생성 | -| BUG #10 (null Input) | Input value에 null 전달 | `?? ''` null guard 추가 | - ---- - -## 수정 우선순위 정리 - -| 순위 | 버그 | 심각도 | 수정 난이도 | 파일 | -|------|------|--------|-----------|------| -| 1 | BUG #11 (중복 문서 생성) | 🔴 CRITICAL | 낮음 | `DocumentCreate/index.tsx` | -| 2 | BUG #1 (리다이렉트 미로드) | 🟡 MEDIUM | 중간 | `DocumentCreate/index.tsx`, `DraftBox/index.tsx` | -| 3 | BUG #13 (빈 폼 임시저장) | 🟡 MEDIUM | 낮음 | `DocumentCreate/index.tsx` (프론트) 또는 백엔드 | -| 4 | BUG #14 (필드별 검증 미비) | 🟡 MEDIUM | 높음 | 백엔드 API | -| 5 | BUG #12 (a11y 로딩 텍스트) | 🟢 LOW | 낮음 | `PowerOfAttorneyForm.tsx`, `SealUsageForm.tsx` | - ---- - -## 테스트 데이터 정리 필요 - -QA 과정에서 생성된 테스트 문서: -- AP-20260316-0009 (근태신청, 임시저장) — 중복 1 -- AP-20260316-0010 (근태신청, 임시저장) — 중복 2 -- AP-20260316-0011 (근태신청, 결재대기) — 부분 입력 상신 -- AP-20260316-0008 (연차촉진1차, 임시저장) - ---- - -## 버그 수정 및 재검수 결과 ✅ 완료 - -### 수정 완료 (2026-03-16 14:00) - -| BUG | 수정 내용 | 재검수 결과 | 검증 방법 | -|-----|----------|-----------|----------| -| **#11** (중복 생성) | `savedDocId` state 추가, 첫 저장 후 `isEditMode` 전환 | ✅ PASS | 1차 저장 `createApproval()` → 2차 저장 `updateApproval(55, ...)` 확인 | -| **#1** (리다이렉트 미로드) | `router.back()` → `router.push('/approval/draft')` 변경 | ✅ PASS | 상신 후 기안함 9건 정상 로드, 토스트 표시 | -| **#13** (빈 폼 임시저장) | 프론트엔드 사전 검증 추가 (동적/전용 폼 내용 체크) | ✅ PASS | "문서 내용을 최소 1개 이상 입력해주세요" 토스트 표시 | -| **#14** (필수 필드 검증) | 프론트엔드 동적 폼 required 필드 검증 추가 | ✅ PASS | "필수 항목을 입력해주세요: 신청유형, 기간, 일수" 토스트 표시 | -| **#12** (a11y 로딩) | `

` 내부 로딩 span → `
` wrapper로 sibling 분리 | ✅ PASS | heading에 로딩 텍스트 미포함 확인 | - -### 수정 파일 목록 - -| 파일 | 수정 내용 | -|------|----------| -| `src/components/approval/DocumentCreate/index.tsx` | BUG #11, #1, #13, #14 | -| `src/components/approval/DocumentCreate/PowerOfAttorneyForm.tsx` | BUG #12 | -| `src/components/approval/DocumentCreate/SealUsageForm.tsx` | BUG #12 | - ---- - -## 전체 QA 진행 상태 - -| Phase | 상태 | 비고 | -|-------|------|------| -| Phase 0: 문서 분류/양식 선택 | ✅ 완료 | 7카테고리 17양식 전체 확인 | -| Phase 1-A: 일반 카테고리 CRUD | ✅ 완료 | 품의서 전체 CRUD, 근태신청/사유서 렌더링+미리보기 | -| Phase 1-B: 경비 카테고리 CRUD | ✅ 완료 | 지출결의서, 비용견적서 전체 CRUD | -| Phase 1-C: 나머지 카테고리 | ✅ 완료 | 11개 양식 렌더링+미리보기 전체 통과 | -| Phase 2: 벨리데이션/파일업로드 | ✅ 완료 | 7개 벨리데이션 시나리오, 파일 업로드/삭제 테스트 | -| Phase 2-B: 대시보드 연동 | ⏳ 미완료 | | -| Phase 3: 버그 정리/수정 계획 | ✅ 완료 | 본 문서 | -| **버그 수정 + 재검수** | **✅ 완료** | **5건 수정, 5건 화면 재검수 통과** | - -### QA 중 생성된 테스트 데이터 -- AP-20260316-0012 (근태신청, 결재대기) — BUG #11 재검수용 diff --git a/claudedocs/[TASK-2026-03-03] daily-report-usd-section.md b/claudedocs/[TASK-2026-03-03] daily-report-usd-section.md deleted file mode 100644 index 6df8f060..00000000 --- a/claudedocs/[TASK-2026-03-03] daily-report-usd-section.md +++ /dev/null @@ -1,172 +0,0 @@ -# 일일일보 — USD(외국환) 섹션 누락 - -**유형**: 프론트엔드 UI 누락 -**파일**: `src/components/accounting/DailyReport/index.tsx` -**날짜**: 2026-03-03 - ---- - -## 현상 - -일일일보 페이지에 KRW(원화) 계좌만 표시되고, USD(외국환) 계좌 섹션이 없음. -summary에 `usd_totals`(이월/입금/출금/잔액)이 내려오고, daily-accounts에 `currency: 'USD'` 항목도 내려오지만 UI에서 렌더링하지 않음. - ---- - -## 원인 - -모든 테이블에서 `currency === 'KRW'` 필터만 적용 중: - -```tsx -// line 391 — 계좌별 상세 -filteredDailyAccounts.filter(item => item.currency === 'KRW') - -// line 448 — 입금 테이블 -filteredDailyAccounts.filter(item => item.currency === 'KRW' && item.income > 0) - -// line 497 — 출금 테이블 -filteredDailyAccounts.filter(item => item.currency === 'KRW' && item.expense > 0) -``` - ---- - -## 요구사항 - -기존 KRW 섹션과 동일한 구조로 USD 섹션 추가: - -### 1. 일자별 상세 테이블에 USD 행 추가 -- 기존 KRW 계좌 목록 아래에 USD 계좌 목록 표시 -- 또는 KRW/USD 구분 소계 행으로 분리 -- 합계: `accountTotals.usd` 사용 (이미 계산 로직 있음, line 134-144) - -### 2. 예금 입출금 내역에 USD 입금/출금 테이블 추가 -- 기존 KRW 입금/출금 아래에 USD 입금/출금 테이블 추가 -- 필터: `currency === 'USD' && item.income > 0` / `currency === 'USD' && item.expense > 0` -- 금액 표시: USD 포맷 ($ 또는 달러 표기) - ---- - -## 참고: 이미 준비된 데이터 - -### summary에서 내려오는 USD 데이터 (line 53-58) -```typescript -summary: { - krwTotals: { carryover, income, expense, balance }, // ← 현재 사용 중 - usdTotals: { carryover, income, expense, balance }, // ← 미사용 (여기 추가) -} -``` - -### accountTotals 계산 로직 (line 134-144) -```typescript -// 이미 USD 합계 계산이 있음 — 사용만 하면 됨 -const usdAccounts = dailyAccounts.filter(item => item.currency === 'USD'); -const usdTotal = usdAccounts.reduce( - (acc, item) => ({ - carryover: acc.carryover + item.carryover, - income: acc.income + item.income, - expense: acc.expense + item.expense, - balance: acc.balance + item.balance, - }), - { carryover: 0, income: 0, expense: 0, balance: 0 } -); -// accountTotals.usd 로 접근 가능 -``` - ---- - -## 작업 범위 - -| 작업 | 설명 | -|------|------| -| 일자별 상세 테이블 | USD 계좌 행 추가 + USD 소계 행 | -| 입금 테이블 | USD 입금 내역 추가 | -| 출금 테이블 | USD 출금 내역 추가 | -| 금액 포맷 | USD 표시 (달러 기호 또는 통화 표기) | - -**수정 파일**: `src/components/accounting/DailyReport/index.tsx` (이 파일만) -**새 코드 불필요**: API 데이터, 타입, 계산 로직 모두 이미 있음. 렌더링만 추가. - -**상태**: ✅ 완료 (프론트엔드 렌더링 추가됨) - ---- - -# CEO 대시보드 — 자금현황 데이터 정합성 이슈 - -**유형**: 백엔드 데이터 불일치 -**관련 API**: `GET /api/proxy/daily-report/summary` -**관련 파일**: `sam-api/app/Services/DailyReportService.php` -**날짜**: 2026-03-03 - ---- - -## 현상 - -CEO 대시보드 자금현황 섹션의 **입금 합계**가 입금 관리 페이지(`/accounting/deposits`)의 실제 데이터와 불일치. - -| 항목 | 대시보드 summary API | 입금 관리 페이지 API | 차이 | -|------|---------------------|---------------------|------| -| 3월 입금 합계 | **200,000원** | **50,000원** (1건) | **150,000원 차이** | -| 3월 출금 합계 | 50,000원 | 50,000원 (1건) | 일치 | - ---- - -## 자금현황 각 수치의 의미 (현재 구조) - -``` -현금성 자산 합계 (cash_asset_total) -= KRW 활성 계좌들의 누적 잔액 합계 (당월만이 아닌 전체 잔고) -├── 전월이월(carryover): 49,872,638원 ← 3월 이전 누적 (입금총액 - 출금총액) -├── 당월입금(income): 200,000원 ← 3월 1일~오늘 입금 -├── 당월출금(expense): 50,000원 ← 3월 1일~오늘 출금 -└── 잔액(balance): 50,022,638원 = 이월+입금-출금 - -외국환(USD) 합계 (foreign_currency_total) = USD 계좌 잔액 합계 -입금 합계 = krw_totals.income (당월 KRW 입금만) -출금 합계 = krw_totals.expense (당월 KRW 출금만) -``` - ---- - -## 원인 분석 - -### 대시보드 summary API 쿼리 (DailyReportService.php line 77-80) -```php -$income = Deposit::where('tenant_id', $tenantId) - ->where('bank_account_id', $account->id) - ->whereBetween('deposit_date', [$startOfMonth, $endOfDay]) - ->sum('amount'); -``` - -### 입금 관리 페이지 API 쿼리 -- 별도 컨트롤러/서비스에서 조회 -- 동일한 `deposits` 테이블을 읽지만, 조회 조건이 다를 수 있음 - -### 불일치 가능 원인 -1. **soft delete 차이**: summary는 soft-deleted 레코드 포함, 목록 API는 제외 -2. **tenant_id 조건 차이**: 두 API의 tenant 필터링이 다를 수 있음 -3. **E2E 테스트 데이터**: 테스트가 DB에 직접 삽입한 레코드가 목록 API에서는 필터됨 -4. **status 필터**: 입금 관리 목록에 status 조건이 추가되어 일부 제외 - ---- - -## 확인 필요 사항 (백엔드) - -### 1. deposits 테이블 직접 조회 -```sql -SELECT id, deposit_date, amount, bank_account_id, deleted_at, status -FROM deposits -WHERE tenant_id = [현재테넌트] - AND bank_account_id = 1 - AND deposit_date BETWEEN '2026-03-01' AND '2026-03-03' -ORDER BY id; -``` -→ 실제 레코드 수와 합계 확인 (soft delete, status 포함) - -### 2. 두 API의 쿼리 조건 비교 -- `DailyReportService::dailyAccounts()` — Deposit 모델 조건 -- 입금 관리 컨트롤러/서비스 — Deposit 모델 조건 -- 차이점 확인 (withTrashed, status 등) - -### 3. 해결 방향 -- 두 API가 동일한 데이터 소스를 보도록 통일 -- 또는 대시보드에서 기존 입금/출금 관리 API를 재사용하여 데이터 일관성 확보 diff --git a/claudedocs/_index.md b/claudedocs/_index.md deleted file mode 100644 index 7dce9e7c..00000000 --- a/claudedocs/_index.md +++ /dev/null @@ -1,99 +0,0 @@ -# claudedocs 문서 맵 - -> 프로젝트 기술 문서 인덱스 (Last Updated: 2026-03-09) - -## 빠른 참조 - -| 문서 | 설명 | -|------|------| -| **[`[REF] all-pages-test-urls.md`](./dev/[REF]%20all-pages-test-urls.md)** | 전체 페이지 테스트 URL 목록 | -| **[`[REF] technical-decisions.md`](./architecture/[REF]%20technical-decisions.md)** | 프로젝트 기술 결정 사항 (13개 항목) | -| **[`[GUIDE] common-page-patterns.md`](./guides/[GUIDE]%20common-page-patterns.md)** | 공통 페이지 패턴 가이드 | - -## 주간 구현내역 - -| 기간 | 문서 | -|------|------| -| 2026-03-02 ~ 03-08 | **[`[IMPL-2026-03-08] frontend-weekly-0302-0308.md`](./%5BIMPL-2026-03-08%5D%20frontend-weekly-0302-0308.md)** | -| (백엔드 일별) | `backend/2026-03-02_구현내역.md` ~ `2026-03-08_구현내역.md` | - ---- - -## 폴더 구조 - -``` -claudedocs/ -├── _index.md # 이 파일 - 문서 맵 -├── auth/ # 인증 & 토큰 관리 -├── hr/ # 인사관리 (부서/사원) -├── item-master/ # 품목기준관리 -├── production/ # 생산관리 (생산현황판/작업자화면) -├── quality/ # 품질관리 (검사관리) -├── sales/ # 판매관리 (견적/거래처/단가) -├── accounting/ # 회계관리 (매입/매출/출금) -├── construction/ # 주일 공사 MES -├── board/ # 게시판 관리 -├── settings/ # 설정 관리 -├── dashboard/ # 대시보드 & 사이드바 -├── security/ # 보안 & 권한 -├── api/ # API 통합 -├── dev/ # 개발도구 & 테스트 -├── guides/ # 범용 가이드 -│ ├── mobile/ # 모바일 반응형 -│ ├── universal-list/ # UniversalListPage 관련 -│ └── migration/ # 마이그레이션 체크리스트 -├── architecture/ # 아키텍처 & 시스템 & 기술 결정 -├── changes/ # 변경이력 -├── refactoring/ # 리팩토링 체크리스트 -├── outbound/ # 출하/배차관리 -├── vehicle/ # 차량관리 -├── material/ # 자재관리 -├── approval/ # 결재관리 -├── backend/ # 백엔드 일별 구현내역 -├── customer-center/ # 고객센터 -├── components/ # 컴포넌트 문서 -├── vercel/ # Vercel 배포 -└── archive/ # 레거시/완료된 문서 - └── sessions/ # 만료된 세션 체크포인트 -``` - ---- - -## 문서 작성 규칙 - -### 파일명 컨벤션 -``` -[TYPE-YYYY-MM-DD] description.md -``` - -**TYPE 종류**: -- `IMPL` - 구현 문서 -- `API` - API 명세/요청 -- `GUIDE` - 사용 가이드 -- `REF` - 참조 문서 -- `ANALYSIS` - 분석 노트 -- `PLAN` - 계획 문서 -- `DESIGN` - 설계 문서 -- `TEST` - 테스트 가이드 -- `NEXT` - 다음 작업 목록 (세션 체크포인트) -- `FIX` - 버그 해결 문서 -- `QA` - 품질 검사 문서 -- `HOTFIX` - 긴급 수정 문서 -- `REPORT` - 보고서/전달 문서 - -### 폴더 배치 기준 -1. **기능/도메인 우선**: 문서 주제에 맞는 폴더에 배치 -2. **범용 가이드**: 여러 기능에 적용되면 `guides/`에 배치 -3. **시스템 전체**: 아키텍처/리팩토링/기술결정은 `architecture/`에 배치 -4. **개발도구**: 테스트 URL, 빌드, E2E, 설정은 `dev/`에 배치 -5. **완료된 작업**: 더 이상 활성화되지 않으면 `archive/`로 이동 -6. **만료 세션**: 2개월 이상 경과한 NEXT-* 파일은 `archive/sessions/`로 이동 - -### 파일 목록 확인 -```bash -# 특정 도메인 파일 확인 -ls claudedocs// - -# 전체 파일 검색 -find claudedocs/ -name "*.md" | sort -``` diff --git a/claudedocs/accounting/[IMPL-2025-12-18] bill-management.md b/claudedocs/accounting/[IMPL-2025-12-18] bill-management.md deleted file mode 100644 index 1f6bad99..00000000 --- a/claudedocs/accounting/[IMPL-2025-12-18] bill-management.md +++ /dev/null @@ -1,65 +0,0 @@ -# 어음관리 (Bill Management) 구현 - -## 스크린샷 분석 - -### 리스트 화면 -- 상단: 일괄등록 버튼, 날짜범위 선택, 상태 탭 (전체, 전일, 오늘, 미결, 수취, 우등록) -- 통계 카드: 4개 (건수 표시) -- 테이블 컬럼: No, 어음번호, 구분, 거래처, 금액, 만기일, 이유, 추수, 메모, 상태 -- 필터: 거래처, 상태 - -### 상세/수정 화면 -- 타이틀: "어음 상세" -- 버튼: view 모드 [목록, 삭제, 수정] / edit 모드 [취소, 저장] -- 기본 정보: - - 어음번호 (Input) - - 구분 (Select: 수취/발행) - - 거래처 (Select) - - 금액 (Input) - - 발행일 (Date) - - 만기일 (Date) - - 상태 (Select - 구분에 따라 옵션 변경) - - 비고 (Input) -- 차수 관리 섹션: - - 테이블: 일자, 금액, 비고 - - [+ 추가] 버튼 - -### 타입 정의 -- **구분**: 수취, 발행 -- **상태 (수취)**: 보관중, 만기입금(7일전), 만기결과, 결제완료, 부도 -- **상태 (발행)**: 보관중, 만기입금(7일전), 추심의뢰, 추심완료, 추소중, 부도 - ---- - -## 체크리스트 - -### Phase 1: 기본 구조 -- [x] types.ts 생성 (타입, 상수 정의) -- [x] 폴더 구조 생성 (BillManagement/) - -### Phase 2: 리스트 화면 -- [x] index.tsx 생성 (리스트 컴포넌트) -- [x] 통계 카드 구현 -- [x] 테이블 렌더링 구현 -- [x] 필터 및 정렬 구현 - -### Phase 3: 상세/수정 화면 -- [x] BillDetail.tsx 생성 -- [x] 기본 정보 폼 구현 -- [x] 차수 관리 섹션 구현 -- [x] view/edit 모드 분기 처리 - -### Phase 4: 라우팅 -- [x] page.tsx 파일 생성 (리스트, 상세, 등록) -- [x] 네비게이션 패턴 적용 (?mode=edit) - -### Phase 5: 검증 -- [x] 빌드 테스트 ✅ -- [ ] 기능 확인 (사용자 확인 필요) - ---- - -## 참고 패턴 -- 입금관리 (DepositManagement) 구조 참고 -- IntegratedListTemplateV2 사용 -- PageLayout + PageHeader 패턴 \ No newline at end of file diff --git a/claudedocs/accounting/[IMPL-2025-12-18] expected-expense-checklist.md b/claudedocs/accounting/[IMPL-2025-12-18] expected-expense-checklist.md deleted file mode 100644 index d6ea836d..00000000 --- a/claudedocs/accounting/[IMPL-2025-12-18] expected-expense-checklist.md +++ /dev/null @@ -1,38 +0,0 @@ -# 지출 예상 내역서 구현 체크리스트 - -## 현재 세션 작업 (2025-12-18) - -### 1. 테이블 필터 수정 -- [x] 1.1 첫번째 필터: 전체 → 거래처 목록으로 변경 ✅ - - 현재: 거래유형 필터 (매입, 선급금, 가지급금 등) - - 변경: 거래처 필터 (전체, 거래처1, 거래처2...) -- [x] 1.2 두번째 필터: 정렬 옵션 축소 ✅ - - 현재: 최신순, 등록순, 지급일 빠른순, 지급일 느린순, 금액 높은순, 금액 낮은순 - - 변경: 최신순, 등록순 (2개만) - -### 2. 예상 지급일 변경 팝업 -- [x] 2.1 팝업 다이얼로그 생성 ✅ - - 헤더: "예상 지급일 변경" + X 닫기 버튼 -- [x] 2.2 선택 항목 요약 영역 ✅ - - 항목명 외 N (선택된 항목 수) - - 총 금액 표시 (합계) -- [x] 2.3 예상 지급일 선택 ✅ - - 라벨: "예상 지급일" - - 공용 달력 컴포넌트 사용 (Calendar + Popover) -- [x] 2.4 버튼 영역 ✅ - - 취소 버튼 - - 저장 버튼 (날짜 미선택 시 비활성화) - -### 3. 전자결재 버튼 기능 -- [x] 3.1 버튼 클릭 시 페이지 이동 ✅ - - 이동 경로: `/ko/approval/document-write/expected-expense` (문서 작성_지출 예상 내역서) - - 항목 선택 필수 (미선택 시 버튼 비활성화) - ---- - -## 참고 스크린샷 -- 예상 지급일 변경 팝업: `스크린샷 2025-12-18 오후 4.39.35.png` -- 필터 위치 참고: `스크린샷 2025-12-18 오후 4.19.33.png`, `4.20.20.png` - -## 테스트 URL -- http://localhost:3000/ko/accounting/expected-expenses \ No newline at end of file diff --git a/claudedocs/accounting/[IMPL-2025-12-18] purchase-management.md b/claudedocs/accounting/[IMPL-2025-12-18] purchase-management.md deleted file mode 100644 index a38ac185..00000000 --- a/claudedocs/accounting/[IMPL-2025-12-18] purchase-management.md +++ /dev/null @@ -1,111 +0,0 @@ -# [IMPL-2025-12-18] 매입관리 페이지 구현 - -## 개요 -회계관리 > 매입관리 페이지 구현 (리스트 + 상세) - -## 참고자료 -- 기안함 리스트 페이지: `src/components/approval/DraftBox/index.tsx` -- 공통 템플릿: `IntegratedListTemplateV2` -- 공통 컴포넌트: `DateRangeSelector`, `ListMobileCard` - ---- - -## Phase 1: 리스트 페이지 - -### 1.1 페이지 구조 -- [ ] 라우트 생성: `/accounting/purchase` -- [ ] 컴포넌트 생성: `src/components/accounting/PurchaseManagement/index.tsx` -- [ ] 타입 정의: `src/components/accounting/PurchaseManagement/types.ts` - -### 1.2 상단 영역 -- [ ] 날짜 범위 선택 (DateRangeSelector) -- [ ] 탭 버튼: 담대조건, 진행중, 당일, 이달, 이번, 미결 - -### 1.3 통계 카드 (4개) -- [ ] 매입금액 합계 (원) -- [ ] 출금액 합계 (원) -- [ ] 매입 건수 -- [ ] 미결 건수 - -### 1.4 필터 셀렉트 박스 -- [ ] 부가세여부 필터 (다중 선택): 부가세여부, 상품매입, 오주경비, 소모품비, 수선비, 원재료비, 사무용품비 등 -- [ ] 거래처 필터 (검색 + 다중 선택) -- [ ] 증빙 필터 (다중 선택): 증빙유형 목록 - -### 1.5 테이블 컬럼 -| 컬럼 | 설명 | -|------|------| -| No | 순번 | -| 매입일자 | 매입 등록일 | -| 매입금액 | 금액 | -| 거래처 | 거래처명 | -| 출금액 | 실제 출금액 | -| 부가세 | 부가세 금액 | -| 매입유형 | 유형 분류 | -| 증빙유형 | 세금계산서 등 | - -### 1.6 기능 -- [ ] 매입 자동 등록: 지출예상내역서 승인 완료 시 자동 등록 -- [ ] 매입/매출등록 Alert 표시 (API 연동 예정) -- [ ] 일람표/거래처원장 연계 출력 - ---- - -## Phase 2: 상세 페이지 (모달) - -### 2.1 기본 정보 섹션 -- [ ] 근거 문서명: 품의서 또는 지출결의서 표시 -- [ ] 결재 버튼: 클릭 시 매입 문서 상세 팝업 -- [ ] 예상 비용 표시: 품의서/지출결의서 예상/총 비용 - -### 2.2 매입 정보 섹션 -- [ ] 매입일자: 문서번호 + 상세조회 아이콘 -- [ ] 출금계좌 셀렉트 박스: 등록된 계좌 목록 (계좌번호 마지막 4자리 + 별명) -- [ ] 거래처 셀렉트 박스: 검색 기능 -- [ ] 매입 유형 셀렉트 박스: 부자재매입, 상품매입, 오주경비, 소모품비, 수선비, 원재료비, 사무용품비, 임차료, 수도광열비, 통신비, 차량유지비, 잡비, 보험료, 기타경비, 미상담 - -### 2.3 품목 정보 섹션 -- [ ] 테이블: 품목명, 수량, 단가, 공급가액, 부가세, 적요 -- [ ] 행 추가/삭제 기능 -- [ ] 합계 표시 - -### 2.4 세금계산서 섹션 -- [ ] 세금계산서 수취 토글 버튼 -- [ ] 토글 시 미수취/수취완료 상태 변경 -- [ ] 수취 완료 후 완료 상태로 변경 - ---- - -## Phase 3: 연동 및 마무리 -- [ ] 전자결재 시스템 연동 (지출예상내역서 승인 → 매입 자동 등록) -- [ ] API 연동 준비 (비포템 API 자동 등록 예정) -- [ ] 일람표/거래처원장 출력 기능 - ---- - -## 파일 구조 -``` -src/ -├── app/[locale]/(protected)/accounting/ -│ └── purchase/ -│ └── page.tsx -├── components/accounting/ -│ └── PurchaseManagement/ -│ ├── index.tsx # 리스트 페이지 -│ ├── types.ts # 타입 정의 -│ └── PurchaseDetailModal.tsx # 상세 모달 -``` - ---- - -## 진행 상태 -- [x] 요구사항 분석 완료 -- [x] 계획서 작성 완료 -- [x] Phase 1 완료 (리스트 페이지) -- [x] Phase 2 완료 (상세 모달) -- [ ] Phase 3 대기 (API 연동) - -## 테스트 URL -``` -http://localhost:3000/ko/accounting/purchase -``` \ No newline at end of file diff --git a/claudedocs/accounting/[IMPL-2025-12-18] receivables-status.md b/claudedocs/accounting/[IMPL-2025-12-18] receivables-status.md deleted file mode 100644 index 427657dd..00000000 --- a/claudedocs/accounting/[IMPL-2025-12-18] receivables-status.md +++ /dev/null @@ -1,73 +0,0 @@ -# 미수금 현황 페이지 구현 체크리스트 - -## 기본 정보 -- **생성일**: 2025-12-18 -- **경로**: `/ko/accounting/receivables-status` -- **참고 페이지**: 매출관리, 거래처원장 - ---- - -## Phase 1: 기본 구조 설정 - -- [x] 페이지 라우트 생성 (`src/app/[locale]/(protected)/accounting/receivables-status/page.tsx`) -- [x] 컴포넌트 디렉토리 생성 (`src/components/accounting/ReceivablesStatus/`) -- [x] 메인 컴포넌트 생성 (`index.tsx`) -- [x] 타입 정의 파일 생성 (`types.ts`) - ---- - -## Phase 2: 레이아웃 구현 - -- [x] DateRangeSelector 공통 달력 적용 -- [x] 프리셋 버튼 (당해년도, 전전월, 전월, 당월, 어제, 오늘) -- [x] 엑셀 다운로드 버튼 -- [x] 저장 버튼 (엑셀 다운로드 아래) -- [x] 검색창 (거래처 검색) - ---- - -## Phase 3: 테이블 구현 (특수 구조) - -테이블 구조 (스크린샷 기준): -- 컬럼: 거래처/연체, 구분, 1월~12월, 합계 -- 그룹핑: 거래처별로 묶이고 각 거래처 아래 구분 (5개: 매출, 입금, 어음, 미수금, 메모) - -- [x] 거래처별 그룹핑 테이블 구조 (rowSpan=5 사용) -- [x] 월별 컬럼 (1월~12월 + 합계) -- [x] 구분 행 (매출, 입금, 어음, 미수금, 메모) - 5개 카테고리 -- [x] 연체 토글 (거래처/연체 컬럼 내) -- [x] 연체 영역 전체 하이라이트 (토글 ON 시 해당 월 전체 빨간 배경) - ---- - -## Phase 4: 토글 기능 - -스크린샷 Description 기준: -- ON: 연체 상태로 표시, 연체일수 시작 -- OFF: 정상 상태 -- 거래처 상세에서 연체 설정과 연동 - -- [x] Switch 컴포넌트로 연체 토글 구현 -- [x] 토글 상태에 따른 UI 변화 -- [x] 연체 상태 표시 로직 - ---- - -## Phase 5: 추가 기능 - -- [x] Mock 데이터 생성 -- [x] 합계 행 계산 -- [ ] 모바일 카드 뷰 (추후 필요시) -- [x] 반응형 레이아웃 (overflow-x-auto) - ---- - -## 진행 상태 -- **현재 단계**: 완료 -- **마지막 업데이트**: 2025-12-18 - ---- - -## 참고 사항 -- Description 영역 (수취 어음 등록 시 표시, 메모 입력박스)은 현재 스코프에서 제외 -- 기본 기능 먼저 구현 후 추가 기능 논의 \ No newline at end of file diff --git a/claudedocs/accounting/[IMPL-2025-12-18] vendor-ledger.md b/claudedocs/accounting/[IMPL-2025-12-18] vendor-ledger.md deleted file mode 100644 index 60eaa4af..00000000 --- a/claudedocs/accounting/[IMPL-2025-12-18] vendor-ledger.md +++ /dev/null @@ -1,129 +0,0 @@ -# [IMPL-2025-12-18] 거래처원장 (Vendor Ledger) 구현 - -## 개요 -- **화면명**: 거래처원장 -- **경로**: 회계관리 > 거래처원장 -- **구성**: 리스트 페이지 + 상세 페이지 - ---- - -## Phase 1: 리스트 페이지 (VendorLedger/index.tsx) - -### 1.1 헤더 영역 -- [ ] 제목: "거래처원장" -- [ ] 설명: "거래처별 기간 내역을 조회합니다." -- [ ] 기간 선택기: 2025-09-01 ~ 2025-09-03 형식 -- [ ] 기간 버튼: 당해년도, 전년월, 전월, 당월, 어제, 오늘 -- [ ] 엑셀 다운로드 버튼 - -### 1.2 요약 카드 (4개) -- [ ] 전기 이월: 3,123,000원 -- [ ] 매출: 3,123,000원 -- [ ] 수금: 3,123,000원 -- [ ] 잔액: 3,123,000원 - -### 1.3 테이블 영역 -- [ ] 검색 필드 -- [ ] 총 N건 표시 -- [ ] 테이블 컬럼: - - No. - - 거래처명 - - 이월잔액 - - 매출 - - 수금 - - 잔액 - - 결제일 -- [ ] 합계 행 (테이블 하단) -- [ ] 행 클릭 시 상세 페이지 이동 - ---- - -## Phase 2: 상세 페이지 (VendorLedger/VendorLedgerDetail.tsx) - -### 2.1 헤더 영역 -- [ ] 제목: "거래처원장 상세 (거래명세서별)" -- [ ] 설명: "거래처 상세 내역을 조회합니다." -- [ ] 기간 선택기: 2025-09-01 ~ 2025-09-03 -- [ ] 기간 버튼: 당해년도, 전년월, 전월, 당월, 어제, 오늘 -- [ ] PDF 다운로드 버튼 - -### 2.2 거래처 정보 섹션 (2열 레이아웃) -- [ ] 좌측 열: - - 회사명: [값] - - 사업자등록번호: 123-12-12345 - - 전화번호: 02-1234-1234 - - 팩스: 02-1234-1236 - - 주소: 주소영 -- [ ] 우측 열: - - 기간: 2025-01-01 ~ 2025-12-31 - - 대표자: 홍길동 - - 모바일: 02-1234-1235 - - 이메일: abc@email.com - -### 2.3 판매/수금 내역 테이블 -- [ ] 컬럼: 일자, 적요, 판매, 수금, 잔액, 작업 -- [ ] 이월잔액 행 (첫 행, 적요에 "이월잔액") -- [ ] 거래 행 (◆ 아이콘 + 날짜) - - 클릭 시 어음 상세 화면 이동 -- [ ] 거래명세서 행 (클릭 시 문서 상세 팝업) -- [ ] 품목명 행 (세금계산서 미발행 시 노란색 하이라이트) -- [ ] 누계 행 (※ 123건 누계 (VAT 포함) 형식) -- [ ] 월별 계 행 (회색 배경, 예: "2025/01 계", "2025/09 계") -- [ ] 작업 컬럼: 수정 아이콘 (✏️) - ---- - -## Phase 3: 타입 및 라우트 - -### 3.1 types.ts -- [ ] VendorLedgerItem 인터페이스 -- [ ] VendorLedgerDetail 인터페이스 -- [ ] TransactionEntry 인터페이스 - -### 3.2 라우트 설정 -- [ ] `/ko/accounting/vendor-ledger` - 리스트 페이지 -- [ ] `/ko/accounting/vendor-ledger/[id]` - 상세 페이지 - ---- - -## 스크린샷 상세 분석 - -### 리스트 페이지 테이블 데이터 예시 -| No. | 거래처명 | 이월잔액 | 매출 | 수금 | 잔액 | 결제일 | -|-----|---------|---------|------|------|------|--------| -| 7 | 회사명 | -100,000 | | | | | -| 6 | 회사명 | 10,000,000 | 10,000,000 | 10,000,000 | | | -| 5 | 회사명 | 10,000,000 | | | | | -| ... | | | | | | | -| **합계** | | 10,000,000 | 10,000,000 | 10,000,000 | 10,000,000 | | - -### 상세 페이지 판매/수금 내역 예시 -| 일자 | 적요 | 판매 | 수금 | 잔액 | 작업 | -|------|------|------|------|------|------| -| | 이월잔액 | 10,000,000 | | | | -| ◆ 2025-01-01 | 수취 어음 (만기 1/5) | | 3,000,000 | 10,000,000 | ✏️ | -| ◆ 2025-01-05 | 어음 회수 | | 3,000,000 | | | -| **2025/01 계** | | | | | | -| ◆ 2025-09-25 | 매출 입력 | 1,000,000 | | | | -| | 품목명 | **🟡 500,000** | | | | -| | ※ 123건 누계 (VAT 포함) | 1,000,000 | | 1,000,000 | | -| **2025/09 계** | | 1,000,000 | 8,000,000 | | | - ---- - -## 작업 진행 상황 -- [x] Phase 1: 리스트 페이지 구현 -- [x] Phase 2: 상세 페이지 구현 -- [x] Phase 3: 타입 및 라우트 설정 -- [x] Phase 4: 테스트 및 검증 - -## 생성된 파일 -1. `src/components/accounting/VendorLedger/types.ts` - 타입 정의 -2. `src/components/accounting/VendorLedger/index.tsx` - 리스트 페이지 -3. `src/components/accounting/VendorLedger/VendorLedgerDetail.tsx` - 상세 페이지 -4. `src/app/[locale]/(protected)/accounting/vendor-ledger/page.tsx` - 리스트 라우트 -5. `src/app/[locale]/(protected)/accounting/vendor-ledger/[id]/page.tsx` - 상세 라우트 - -## 접속 URL -- 리스트: `/ko/accounting/vendor-ledger` -- 상세: `/ko/accounting/vendor-ledger/[id]` \ No newline at end of file diff --git a/claudedocs/accounting/[IMPL-2025-12-18] vendor-management-checklist.md b/claudedocs/accounting/[IMPL-2025-12-18] vendor-management-checklist.md deleted file mode 100644 index 5ab96a44..00000000 --- a/claudedocs/accounting/[IMPL-2025-12-18] vendor-management-checklist.md +++ /dev/null @@ -1,287 +0,0 @@ -# 거래처 관리 (Vendor Management) 구현 체크리스트 - -> **상태**: ✅ 완료 (2025-12-18) -> **리스트 수정**: ✅ 완료 (2025-12-18) - 필터/액션버튼/신규등록 수정 - -## 개요 -- **경로**: `/accounting/vendors` (리스트), `/accounting/vendors/[id]` (상세), `/accounting/vendors/new` (신규등록) -- **기능**: 거래처 등록, 조회, 수정, 삭제 -- **참고**: 스크린샷 4장 (리스트 1장, 상세 3장) - ---- - -## Phase 1: 타입 및 상수 정의 ✅ - -### 1.1 types.ts 생성 -- [x] 거래처 구분 타입 (VendorCategory): `sales` | `purchase` | `both` -- [x] 신용등급 타입 (CreditRating): `AAA` | `AA` | `A` | `BBB` | `BB` | `B` | `CCC` | `CC` | `C` | `D` -- [x] 거래등급 타입 (TransactionGrade): `A` | `B` | `C` | `D` | `E` -- [x] 악성채권 상태 타입 (BadDebtStatus): `none` | `normal` | `warning` -- [x] 정렬 옵션 타입 (SortOption) -- [x] 거래처 인터페이스 (Vendor) - - id, vendorCode, businessNumber - - vendorName, representativeName - - category (sales/purchase/both) - - businessType, businessCategory - - address (zipCode, address1, address2) - - phone, mobile, fax, email - - managerName, managerPhone, systemManager - - logoUrl - - purchasePaymentDay, salesPaymentDay - - creditRating, transactionGrade - - taxInvoiceEmail, bankName, accountNumber, accountHolder - - outstandingAmount, overdueAmount, overdueDays - - unpaidAmount, badDebtStatus - - overdueToggle, badDebtToggle - - memos: Memo[] - - createdAt, updatedAt - -### 1.2 상수 정의 -- [x] VENDOR_CATEGORY_OPTIONS (구분 필터) -- [x] CREDIT_RATING_OPTIONS (신용등급 필터) -- [x] TRANSACTION_GRADE_OPTIONS (거래등급 필터) -- [x] BAD_DEBT_STATUS_OPTIONS (악성채권 필터) -- [x] SORT_OPTIONS (정렬 옵션) -- [x] PAYMENT_DAY_OPTIONS (결제일 1~31일) -- [x] BANK_OPTIONS (은행 목록) - ---- - -## Phase 2: 리스트 페이지 구현 ✅ - -### 2.1 페이지 라우트 생성 -- [x] `/src/app/[locale]/(protected)/accounting/vendors/page.tsx` 생성 - -### 2.2 VendorManagement 컴포넌트 -- [x] `/src/components/accounting/VendorManagement/index.tsx` 생성 - -#### 2.2.1 상단 통계 카드 (3개) -| 카드 | 값 | 아이콘 색상 | -|------|-----|------------| -| 전체 거래처 | {count}개 | blue | -| 매출 거래처 | {count}개 | green | -| 매입 거래처 | {count}개 | orange | - -#### 2.2.2 필터 조건 (5개 셀렉트박스) -| 필터 | 옵션 | 기본값 | -|------|------|--------| -| 구분 | 전체, 매출, 매입, 매입매출 | 전체 | -| 신용등급 | 전체, AAA, AA, A, BBB, BB, B, CCC, CC, C, D | 전체 | -| 거래등급 | 전체, A(우수), B(양호), C(보통), D(주의), E(위험) | 전체 | -| 악성채권 | 전체, 미설정, 정상 | 전체 | -| 정렬 | 최신순, 등록순, 거래처명 오름차순, 거래처명 내림차순, 미수금 높은순, 미수금 낮은순 | 최신순 | - -#### 2.2.3 테이블 컬럼 (체크박스 + 9개) -| 순서 | 컬럼명 | 정렬 | 비고 | -|------|--------|------|------| -| 0 | 체크박스 | center | - | -| 1 | 번호 | center | globalIndex + 1 | -| 2 | 구분 | center | Badge (매출/매입/매입매출) | -| 3 | 거래처명 | left | - | -| 4 | 매입 결제일 | center | {n}일 | -| 5 | 매출 결제일 | center | {n}일 | -| 6 | 신용등급 | center | Badge | -| 7 | 거래등급 | center | Badge | -| 8 | 미수금 | right | 금액 포맷 | -| 9 | 악성채권 | center | Badge or - | - -#### 2.2.4 행 선택 시 버튼 -- [x] 상세 버튼 → `/accounting/vendors/{id}` 이동 -- [x] 수정 버튼 → `/accounting/vendors/{id}?mode=edit` 이동 -- [x] 삭제 버튼 → 삭제 확인 AlertDialog -- [x] 취소 버튼 → 선택 해제 - -#### 2.2.5 헤더 액션 -- [x] 신규등록 버튼 → `/accounting/vendors/new` 이동 - ---- - -## Phase 3: 상세/수정/등록 페이지 구현 ✅ - -### 3.1 페이지 라우트 생성 -- [x] `/src/app/[locale]/(protected)/accounting/vendors/[id]/page.tsx` (상세/수정) -- [x] `/src/app/[locale]/(protected)/accounting/vendors/new/page.tsx` (신규등록) - -### 3.2 VendorDetail 컴포넌트 -- [x] `/src/components/accounting/VendorManagement/VendorDetail.tsx` 생성 -- [x] mode prop: `view` | `edit` | `new` - -#### 3.2.1 상단 버튼 -| 모드 | 버튼 | -|------|------| -| view | 삭제, 수정 | -| edit | 취소, 저장 | -| new | 취소, 등록 | - -#### 3.2.2 기본 정보 섹션 -| 필드명 | 타입 | 필수 | 비고 | -|--------|------|------|------| -| 사업자등록번호 | text (마스크: 000-00-00000) | * | - | -| 거래처코드 | text | - | 자동생성 또는 수동입력 | -| 거래처명 | text | * | - | -| 대표자명 | text | - | - | -| 거래처 유형 | select | * | 매출매입, 매출, 매입 | -| 업태 | text | - | - | -| 업종 | text | - | - | - -#### 3.2.3 연락처 정보 섹션 -| 필드명 | 타입 | 필수 | 비고 | -|--------|------|------|------| -| 주소 | address | - | 우편번호 찾기 + 기본주소 + 상세주소 | -| 전화번호 | tel | - | 02-0000-0000 | -| 모바일 | tel | - | 010-0000-0000 | -| 팩스 | tel | - | 02-0000-0000 | -| 이메일 | email | - | - | - -#### 3.2.4 담당자 정보 섹션 -| 필드명 | 타입 | 필수 | 비고 | -|--------|------|------|------| -| 담당자명 | text | - | - | -| 담당자 전화 | tel | - | - | -| 시스템 관리자 | text | - | - | - -#### 3.2.5 회사 정보 섹션 -| 필드명 | 타입 | 필수 | 비고 | -|--------|------|------|------| -| 회사 로고 | file | - | 750x250px, 10MB 이하, PNG/JPEG/GIF | -| 매입 결제일 | select | - | 1~31일 | -| 매출 결제일 | select | - | 1~31일 | - -#### 3.2.6 신용/거래 정보 섹션 -| 필드명 | 타입 | 필수 | 비고 | -|--------|------|------|------| -| 신용등급 | select | - | AAA~D | -| 거래등급 | select | - | A(우수)~E(위험) | -| 세금계산서 이메일 | email | - | - | -| 입금계좌 은행 | select | - | 은행 목록 | -| 계좌 | text | - | 숫자만 | -| 예금주 | text | - | - | - -#### 3.2.7 추가 정보 섹션 -| 필드명 | 타입 | 필수 | 비고 | -|--------|------|------|------| -| 미수금 | number | - | 금액 입력 | -| 연체 | number + toggle | - | 일수 + ON/OFF | -| 미지급 | number | - | 금액 입력 | -| 악성채권 | select + toggle | - | 선택 + ON/OFF (악성채권 추심관리 연동) | - -#### 3.2.8 메모 섹션 -| 필드명 | 타입 | 비고 | -|--------|------|------| -| 메모 | textarea + list | 추가 버튼으로 메모 리스트에 추가 | -| 메모 리스트 | table | 날짜, 내용, 삭제버튼 | - ---- - -## Phase 4: 다이얼로그 ✅ - -### 4.1 삭제 확인 다이얼로그 -- [x] AlertDialog 사용 -- [x] 제목: "거래처(명)을 삭제하시겠습니까?" -- [x] 설명: 확인 클릭 시 거래처관리 목록으로 이동 -- [x] 버튼: 취소, 삭제 - -### 4.2 수정 확인 다이얼로그 -- [x] AlertDialog 사용 -- [x] 제목: "정말 수정하시겠습니까?" -- [x] 설명: 확인 클릭 시 "수정이 완료되었습니다." 알림 -- [x] 버튼: 취소, 확인 - ---- - -## Phase 5: 파일 구조 ✅ - -``` -src/ -├── app/[locale]/(protected)/accounting/vendors/ -│ ├── page.tsx # 리스트 페이지 ✅ -│ ├── [id]/ -│ │ └── page.tsx # 상세/수정 페이지 ✅ -│ └── new/ -│ └── page.tsx # 신규등록 페이지 ✅ -└── components/accounting/VendorManagement/ - ├── index.tsx # 리스트 컴포넌트 ✅ - ├── VendorDetail.tsx # 상세/수정/등록 컴포넌트 ✅ - └── types.ts # 타입 및 상수 정의 ✅ -``` - ---- - -## 구현 완료 요약 - -### Step 1: 기본 구조 ✅ -- [x] types.ts 생성 및 타입/상수 정의 -- [x] 페이지 라우트 파일 생성 - -### Step 2: 리스트 페이지 ✅ -- [x] index.tsx 생성 -- [x] IntegratedListTemplateV2 활용 -- [x] 통계 카드 (3개) -- [x] 필터 (5개 셀렉트박스) -- [x] 테이블 (체크박스 + 번호 + 9개 컬럼) -- [x] 행 선택 시 버튼 (상세/수정/삭제/취소) -- [x] 삭제 확인 다이얼로그 - -### Step 3: 상세 페이지 ✅ -- [x] VendorDetail.tsx 생성 -- [x] 기본 정보 섹션 -- [x] 연락처 정보 섹션 -- [x] 담당자 정보 섹션 -- [x] 회사 정보 섹션 (로고 업로드 영역 포함) -- [x] 신용/거래 정보 섹션 -- [x] 추가 정보 섹션 (토글 포함) -- [x] 메모 섹션 - -### Step 4: 수정/등록 기능 ✅ -- [x] mode별 버튼 및 동작 구현 -- [x] 수정/삭제 확인 다이얼로그 - ---- - -## 테스트 URL - -| 페이지 | URL | -|--------|-----| -| 리스트 | `/ko/accounting/vendors` | -| 상세 | `/ko/accounting/vendors/vendor-1` | -| 수정 | `/ko/accounting/vendors/vendor-1?mode=edit` | -| 신규등록 | `/ko/accounting/vendors/new` | - ---- - -## 참고 사항 - -### 공통 컴포넌트 사용 -- IntegratedListTemplateV2 (리스트 템플릿) -- AlertDialog (삭제/수정 확인) -- Card, CardHeader, CardContent (섹션 구분) -- Select, Input, Textarea (폼 요소) -- Switch (토글) -- Badge (상태 표시) - -### 스타일 가이드 -- 주요 색상: orange (강조), blue/green (통계 카드) -- Badge 색상: - - 구분: 매출(green), 매입(orange), 매입매출(blue) - - 신용등급: AAA~A(green), BBB~B(yellow), CCC~D(red) - - 거래등급: A(green), B(blue), C(yellow), D(orange), E(red) - - 악성채권: 정상(green), 미설정(-), 경고(red) - -### 주의사항 -- 상세 페이지는 **페이지**로 구현 (모달 X) ✅ -- 테이블에 번호 컬럼 필수 (체크박스 바로 뒤) ✅ -- 금액은 toLocaleString() 포맷 사용 ✅ -- 행 클릭 시 상세 페이지로 이동 ✅ - ---- - -## 리스트 페이지 수정 (2025-12-18) - -### 수정 사항 -- [x] 악성채권 필터 옵션: "미설정" → "악성채권" 변경 -- [x] BadDebtStatus 타입: 'warning' → 'badDebt' 변경 -- [x] 작업 버튼: 상단 액션바 → 테이블 맨 끝 컬럼으로 이동 -- [x] 체크박스 선택 시에만 수정/삭제 버튼 표시 -- [x] headerActions (신규등록 버튼) 제거 -- [x] beforeTableContent (상단 액션 바) 제거 -- [x] 미사용 핸들러 정리 (handleNewVendor, handleCancelSelection) \ No newline at end of file diff --git a/claudedocs/accounting/[IMPL-2025-12-18] withdrawal-management-checklist.md b/claudedocs/accounting/[IMPL-2025-12-18] withdrawal-management-checklist.md deleted file mode 100644 index a6c39020..00000000 --- a/claudedocs/accounting/[IMPL-2025-12-18] withdrawal-management-checklist.md +++ /dev/null @@ -1,142 +0,0 @@ -# 출금관리 (Withdrawal Management) 구현 체크리스트 - -> **상태**: ✅ 완료 (2025-12-18) -> **참고**: 입금관리(DepositManagement)와 동일한 구조 - -## 개요 -- **경로**: `/accounting/withdrawals` (리스트), `/accounting/withdrawals/[id]` (상세) -- **기능**: 출금 내역 조회, 수정 (계좌 관리에 등록된 계좌의 자동 출금 내역 수집) -- **참고**: 스크린샷 3장 - ---- - -## Phase 1: 타입 및 상수 정의 - -### 1.1 types.ts 생성 -- [ ] 출금 유형 타입 (WithdrawalType) - - `unset` (미설정) - - `purchasePayment` (매입대금) - - `advance` (선급금) - - `suspense` (가지급금) - - `rent` (임대료) - - `interestExpense` (이자비용) - - `depositPayment` (보증금 지급) - - `loanRepayment` (차입금 상환) - - `dividendPayment` (배당금 지급) - - `vatPayment` (부가세 납부) - - `salary` (급여) - - `insurance` (4대보험) - - `tax` (세금) - - `utilities` (공과금) - - `expenses` (경비) - - `other` (기타) - -- [ ] 정렬 옵션 (SortOption): 최신순, 등록순, 금액 높은순, 금액 낮은순 -- [ ] 출금 레코드 인터페이스 (WithdrawalRecord) - - id, withdrawalDate, withdrawalAmount - - accountName, recipientName (수취인명) - - note (적요), withdrawalType - - vendorId, vendorName - - createdAt, updatedAt - ---- - -## Phase 2: 리스트 페이지 구현 - -### 2.1 페이지 라우트 -- [ ] `/src/app/[locale]/(protected)/accounting/withdrawals/page.tsx` - -### 2.2 WithdrawalManagement 컴포넌트 -- [ ] `/src/components/accounting/WithdrawalManagement/index.tsx` - -#### 2.2.1 상단 통계 카드 (4개) -| 카드 | 값 | 아이콘 색상 | -|------|-----|------------| -| 출금 총액 | {amount}원 | blue | -| 당월 출금 | {amount}원 | green | -| 거래처 미설정 | {count}건 | orange | -| 출금유형 미설정 | {count}건 | red | - -#### 2.2.2 헤더 액션 -- [ ] DateRangeSelector (날짜 범위) -- [ ] 빠른 필터: 당해연도, 전년도, 전월, 당월, 어제, 오늘, 새로고침 - -#### 2.2.3 계정과목명 셀렉트 + 저장 버튼 -- [ ] 계정과목명 Select (출금 유형 옵션) -- [ ] 저장 버튼 → "N개의 출금 유형을 {계정과목명}으로 모두 변경하시겠습니까?" Alert - -#### 2.2.4 필터 (3개) -| 필터 | 옵션 | -|------|------| -| 거래처 | 전체, 거래처 목록 | -| 출금유형 | 전체, 매입대금, 선급금, 가지급금, 임대료, 이자비용, 보증금 지급, 차입금 상환, 배당금 지급, 부가세 납부, 급여, 4대보험, 세금, 공과금, 경비, 기타, 미설정 | -| 정렬 | 최신순, 등록순, 금액 높은순, 금액 낮은순 | - -#### 2.2.5 테이블 컬럼 (체크박스 + 7개 + 작업) -| 순서 | 컬럼명 | 정렬 | 비고 | -|------|--------|------|------| -| 0 | 체크박스 | center | - | -| 1 | 출금일 | center | - | -| 2 | 출금계좌 | left | - | -| 3 | 수취인명 | left | - | -| 4 | 출금금액 | right | 금액 포맷 | -| 5 | 거래처 | left | 미설정시 빨간색 | -| 6 | 출금유형 | center | Badge | -| 7 | 작업 | center | 선택 시 수정/삭제 버튼 | - -#### 2.2.6 테이블 합계 행 -- [ ] 출금금액 합계 - ---- - -## Phase 3: 상세 페이지 구현 - -### 3.1 페이지 라우트 -- [ ] `/src/app/[locale]/(protected)/accounting/withdrawals/[id]/page.tsx` - -### 3.2 WithdrawalDetail 컴포넌트 -- [ ] `/src/components/accounting/WithdrawalManagement/WithdrawalDetail.tsx` -- [ ] mode prop: `view` | `edit` - -#### 3.2.1 상단 버튼 -| 모드 | 버튼 | -|------|------| -| view | 목록, 삭제, 수정 | -| edit | 취소, 저장 | - -#### 3.2.2 기본 정보 섹션 -| 필드명 | 타입 | 편집 가능 | 비고 | -|--------|------|----------|------| -| 출금일 | date | ❌ | 읽기 전용 | -| 출금계좌 | text | ❌ | 읽기 전용 | -| 수취인명 | text | ❌ | 읽기 전용 | -| 출금금액 | number | ❌ | 읽기 전용 | -| 적요 | select | ✅ | 선택 가능 | -| 거래처 | select | ✅ | 필수 | -| 출금 유형 | select | ✅ | 필수 | - ---- - -## Phase 4: 파일 구조 - -``` -src/ -├── app/[locale]/(protected)/accounting/withdrawals/ -│ ├── page.tsx # 리스트 페이지 -│ └── [id]/ -│ └── page.tsx # 상세/수정 페이지 -└── components/accounting/WithdrawalManagement/ - ├── index.tsx # 리스트 컴포넌트 - ├── WithdrawalDetail.tsx # 상세/수정 컴포넌트 - └── types.ts # 타입 및 상수 정의 -``` - ---- - -## 테스트 URL - -| 페이지 | URL | -|--------|-----| -| 리스트 | `/ko/accounting/withdrawals` | -| 상세 | `/ko/accounting/withdrawals/withdrawal-1` | -| 수정 | `/ko/accounting/withdrawals/withdrawal-1?mode=edit` | \ No newline at end of file diff --git a/claudedocs/accounting/[IMPL-2025-12-19] bad-debt-collection-management.md b/claudedocs/accounting/[IMPL-2025-12-19] bad-debt-collection-management.md deleted file mode 100644 index 4f3222a0..00000000 --- a/claudedocs/accounting/[IMPL-2025-12-19] bad-debt-collection-management.md +++ /dev/null @@ -1,230 +0,0 @@ -# 악성채권 추심관리 구현 계획서 - -> 작성일: 2025-12-19 -> URL: `/ko/accounting/bad-debt-collection` - ---- - -## 📋 스크린샷 분석 요약 - -### 리스트 화면 -- **제목**: 악성채권 추심관리 -- **통계 카드 4개**: 총 악성채권, 추심중, 법적조치, 회수완료 -- **필터 3개**: - 1. 거래처 필터 (검색 + 다중선택, 디폴트: 전체) - 2. 상태 필터 (전체, 추심중, 법적조치, 회수완료, 대손처리) - 3. 정렬 (최신순, 등록순, 디폴트: 최신순) -- **테이블 컬럼**: No., 거래처, 채권금액, 발생일, 연체일수, 담당자, 상태, 설정, 작업 -- **설정 컬럼**: ON/OFF 토글 -- **작업 컬럼**: 수정/삭제 아이콘 - -### 상세 화면 (view/edit/new 모드) -1. **기본 정보**: 사업자등록번호, 거래처코드, 거래처명, 대표자명, 거래처유형(토글), 악성채권등록(업태/업종) -2. **연락처 정보**: 주소(우편번호 찾기), 전화번호, 모바일, 팩스, 이메일 -3. **담당자 정보**: 담당자명, 담당자전화, 시스템관리자 -4. **필요 서류**: 사업자등록증, 세금계산서, 추가서류 (파일 첨부) -5. **악성 채권 정보**: - - 미수금 + 상태 셀렉트 (추심중, 법적조치, 회수완료, 대손처리) - - 연체일수 + 본사 담당자 셀렉트 (부서명 이름 직급명 연락처) - - 악성채권 발생일 / 종료일 - - **수취 어음 현황 버튼** → 어음관리 화면 (해당 거래처 필터) - - **거래처 미수금 현황 버튼** → 미수금 현황 화면 (해당 거래처 하이라이트) -6. **메모**: 타임스탬프 형식 메모 목록 - ---- - -## ✅ 구현 체크리스트 - -### Phase 1: 파일 구조 및 타입 정의 -- [x] 1.1 `src/components/accounting/BadDebtCollection/types.ts` 생성 - - BadDebtRecord 인터페이스 - - CollectionStatus 타입 (추심중, 법적조치, 회수완료, 대손처리) - - SortOption 타입 - - 상태 라벨/컬러 상수 -- [x] 1.2 `src/app/[locale]/(protected)/accounting/bad-debt-collection/page.tsx` 생성 -- [x] 1.3 `src/app/[locale]/(protected)/accounting/bad-debt-collection/[id]/page.tsx` 생성 -- [x] 1.4 `src/app/[locale]/(protected)/accounting/bad-debt-collection/[id]/edit/page.tsx` 생성 -- [x] 1.5 `src/app/[locale]/(protected)/accounting/bad-debt-collection/new/page.tsx` 생성 - -### Phase 2: 리스트 컴포넌트 구현 -- [x] 2.1 `src/components/accounting/BadDebtCollection/index.tsx` 생성 - - IntegratedListTemplateV2 사용 -- [x] 2.2 통계 카드 4개 구현 (총 악성채권, 추심중, 법적조치, 회수완료) -- [x] 2.3 필터 3개 구현 - - 거래처 필터 (검색 + 다중선택) - - 상태 필터 (전체, 추심중, 법적조치, 회수완료, 대손처리) - - 정렬 (최신순, 등록순) -- [x] 2.4 테이블 컬럼 구현 (체크박스 + No. + 거래처 + 채권금액 + 발생일 + 연체일수 + 담당자 + 상태 + 설정 + 작업) - - No.: 순번 (1부터) - - 거래처: 거래처명 - - 채권금액: 금액 (원) - - 발생일: YYYY-MM-DD - - 연체일수: 숫자 + "일" - - 담당자: 담당자명 - - 상태: Badge (추심중/법적조치/회수완료/대손처리) - - 설정: ON/OFF 토글 (Switch) - - 작업: 수정/삭제 아이콘 (row 선택 시 표시) -- [x] 2.5 Mock 데이터 생성 함수 -- [x] 2.6 행 클릭 → 상세 페이지 이동 기능 -- [x] 2.7 모바일 카드 렌더링 - -### Phase 3: 상세/수정/등록 컴포넌트 구현 -- [x] 3.1 `src/components/accounting/BadDebtCollection/BadDebtDetail.tsx` 생성 - - mode prop (view/edit/new) -- [x] 3.2 기본 정보 섹션 - - 사업자등록번호 (Input, readonly) - - 거래처 코드 (Input, readonly) - - 거래처명 (Input) - - 대표자명 (Input) - - 거래처 유형 (Switch - 매출매입) - - 악성채권 등록 - 업태 (Input) - - 악성채권 등록 - 업종 (Input) -- [x] 3.3 연락처 정보 섹션 - - 주소 (우편번호 찾기 버튼 + Input 3개) - - 전화번호 (Input) - - 모바일 (Input) - - 팩스 (Input) - - 이메일 (Input) -- [x] 3.4 담당자 정보 섹션 - - 담당자명 (Input) - - 담당자 전화 (Input) - - 시스템 관리자 (Input, readonly) -- [x] 3.5 필요 서류 섹션 - - 사업자등록증 (파일 찾기) - - 세금계산서 (파일 찾기) - - 추가 서류 (파일 찾기 + 추가 버튼 + 삭제 X) -- [x] 3.6 악성 채권 정보 섹션 - - 미수금 (Input, readonly) - - 상태 (Select: 추심중, 법적조치, 회수완료, 대손처리) - - 연체일수 (Switch + Input) - - 본사 담당자 (Select: 부서명 이름 직급명 연락처) - - 악성채권 발생일 (DatePicker) - - 악성채권 종료일 (DatePicker) - - **수취 어음 현황 버튼** → `/ko/accounting/bills?vendorId={id}&type=received` - - **거래처 미수금 현황 버튼** → `/ko/accounting/receivables-status?highlight={id}` -- [x] 3.7 메모 섹션 - - 메모 목록 (Textarea, readonly) - - 메모 추가 기능 (타임스탬프 자동 생성) -- [x] 3.8 삭제/수정 버튼 및 다이얼로그 -- [x] 3.9 저장 기능 (mock) - -### Phase 4: 연동 기능 구현 -- [x] 4.1 어음관리 페이지 수정 - vendorId 쿼리 파라미터 지원 -- [x] 4.2 미수금 현황 페이지 수정 - highlight 쿼리 파라미터 지원 (해당 거래처 행 하이라이트) - -### Phase 5: 최종 검증 및 문서화 -- [x] 5.1 리스트 페이지 테스트 -- [x] 5.2 상세/수정/등록 페이지 테스트 -- [x] 5.3 어음관리 연동 테스트 -- [x] 5.4 미수금 현황 연동 테스트 -- [x] 5.5 모바일 반응형 테스트 -- [x] 5.6 `[REF] all-pages-test-urls.md` 업데이트 - ---- - -## 📁 파일 구조 - -``` -src/ -├── app/[locale]/(protected)/accounting/ -│ └── bad-debt-collection/ -│ ├── page.tsx # 리스트 페이지 -│ ├── new/ -│ │ └── page.tsx # 등록 페이지 -│ └── [id]/ -│ ├── page.tsx # 상세 페이지 -│ └── edit/ -│ └── page.tsx # 수정 페이지 -└── components/accounting/ - └── BadDebtCollection/ - ├── index.tsx # 리스트 컴포넌트 - ├── BadDebtDetail.tsx # 상세/수정/등록 컴포넌트 - └── types.ts # 타입 정의 -``` - ---- - -## 🔗 URL 구조 - -| 페이지 | URL | 비고 | -|--------|-----|------| -| 리스트 | `/ko/accounting/bad-debt-collection` | 메인 | -| 등록 | `/ko/accounting/bad-debt-collection/new` | | -| 상세 | `/ko/accounting/bad-debt-collection/[id]` | | -| 수정 | `/ko/accounting/bad-debt-collection/[id]/edit` | | - ---- - -## 📊 테이블 컬럼 상세 - -| 컬럼 | key | label | 정렬 | 비고 | -|------|-----|-------|------|------| -| 체크박스 | checkbox | - | center | Checkbox | -| 번호 | no | No. | center | 1부터 시작 | -| 거래처 | vendorName | 거래처 | left | | -| 채권금액 | debtAmount | 채권금액 | right | 1,000,000원 | -| 발생일 | occurrenceDate | 발생일 | center | YYYY-MM-DD | -| 연체일수 | overdueDays | 연체일수 | center | 100일 | -| 담당자 | managerName | 담당자 | left | | -| 상태 | status | 상태 | center | Badge | -| 설정 | settingToggle | 설정 | center | Switch | -| 작업 | actions | 작업 | center | 수정/삭제 | - ---- - -## 🎨 상태 Badge 스타일 - -| 상태 | 라벨 | 스타일 | -|------|------|--------| -| collecting | 추심중 | `border-orange-300 text-orange-600 bg-orange-50` | -| legalAction | 법적조치 | `border-red-300 text-red-600 bg-red-50` | -| recovered | 회수완료 | `border-green-300 text-green-600 bg-green-50` | -| badDebt | 대손처리 | `border-gray-300 text-gray-600 bg-gray-50` | - ---- - -## 🔍 필터 상세 - -### 1. 거래처 필터 -- **타입**: 검색 + 다중선택 (Combobox/MultiSelect) -- **옵션**: 전체, 거래처 목록 (API) -- **디폴트**: 전체 - -### 2. 상태 필터 -- **타입**: Select -- **옵션**: 전체, 추심중, 법적조치, 회수완료, 대손처리 -- **디폴트**: 악성채권 설정 시 디폴트 상태 - -### 3. 정렬 -- **타입**: Select -- **옵션**: 최신순, 등록순 -- **디폴트**: 최신순 - ---- - -## 🔗 연동 기능 - -### 수취 어음 현황 버튼 -- **대상**: 어음관리 페이지 (`/ko/accounting/bills`) -- **쿼리**: `?vendorId={id}&type=received` -- **동작**: 해당 거래처의 수취 어음만 필터링하여 표시 - -### 거래처 미수금 현황 버튼 -- **대상**: 미수금 현황 페이지 (`/ko/accounting/receivables-status`) -- **쿼리**: `?highlight={id}` -- **동작**: 해당 거래처 행을 하이라이트 표시 - ---- - -## 📝 작업 완료 후 체크 - -- [x] `claudedocs/[REF] all-pages-test-urls.md` 업데이트 -- [x] 빌드 오류 없음 확인 -- [x] 리스트/상세/수정/등록 페이지 정상 작동 -- [x] 연동 페이지 정상 작동 - ---- - -## ✅ 구현 완료 - -> 완료일: 2025-12-19 \ No newline at end of file diff --git a/claudedocs/accounting/[IMPL-2025-12-19] card-transaction-inquiry.md b/claudedocs/accounting/[IMPL-2025-12-19] card-transaction-inquiry.md deleted file mode 100644 index 572f81b5..00000000 --- a/claudedocs/accounting/[IMPL-2025-12-19] card-transaction-inquiry.md +++ /dev/null @@ -1,89 +0,0 @@ -# 카드 내역 조회 구현 체크리스트 - -## 개요 -- **페이지 경로**: `/ko/accounting/card-transactions` -- **참고**: 입출금 계좌조회 페이지와 유사한 구조 - -## 화면 구성 - -### 1. 상단 영역 -- 제목: "카드 내역 조회" -- 부제: "법인카드 사용 내역을 조회합니다" - -### 2. 날짜 선택 + 빠른 버튼 -- 날짜 범위 선택 -- 빠른 버튼: 당해년도, 전전월, 전월, 당월, 어제, 오늘 -- 새로고침 버튼 (출금관리 스타일, 오른쪽 위치) - -### 3. 요약 카드 (2개) -- 전월 사용액: 금액 표시 -- 당월 사용액: 금액 표시 - -### 4. 필터 영역 -- 검색 입력창 (좌측) -- 총 N건 표시 -- 필터 2개: - - 카드명 필터 (전체, 카드명 목록) - 디폴트: 전체 - - 정렬 필터 (최신순, 등록순, 금액 높은순, 금액 낮은순) - 디폴트: 최신순 - -### 5. 테이블 -- **체크박스 없음** -- **번호 컬럼 없음** -- 컬럼 순서: - 1. 카드 - 2. 카드명 - 3. 사용자 - 4. 사용일시 - 5. 가맹점명 - 6. 사용금액 - -### 6. 합계 행 -- 테이블 마지막에 합계 행 -- 사용금액 열에만 합계 표시 - ---- - -## 구현 체크리스트 - -### Phase 1: 파일 구조 생성 -- [x] types.ts 생성 (타입 정의) -- [x] index.tsx 생성 (메인 컴포넌트) -- [x] page.tsx 생성 (라우트) - -### Phase 2: 타입 정의 -- [x] CardTransaction 인터페이스 -- [x] SortOption 타입 - -### Phase 3: 컴포넌트 구현 -- [x] 헤더 영역 (제목, 부제) -- [x] 날짜 선택 + 빠른 버튼 -- [x] 새로고침 버튼 -- [x] 요약 카드 2개 (전월/당월 사용액) -- [x] 검색 + 필터 영역 (카드명, 정렬) -- [x] 테이블 (체크박스/번호 없음) -- [x] 합계 행 - -### Phase 4: 테스트 및 문서화 -- [x] all-pages-test-urls.md 업데이트 - -### Phase 5: 템플릿 기능 추가 -- [x] IntegratedListTemplateV2에 showCheckbox props 추가 -- [x] 조건부 체크박스 렌더링 구현 - ---- - -## Mock 데이터 예시 -```typescript -{ - card: '신한 1234', - cardName: '법인카드1', - user: '홍길동', - usedAt: '2025-12-12 12:12', - merchantName: '가맹점명', - amount: 100000 -} -``` - -## 참고 파일 -- `src/components/accounting/BankTransactionInquiry/index.tsx` -- `src/components/accounting/BankTransactionInquiry/types.ts` \ No newline at end of file diff --git a/claudedocs/accounting/[PLAN-2025-12-18] sales-management.md b/claudedocs/accounting/[PLAN-2025-12-18] sales-management.md deleted file mode 100644 index 86032a66..00000000 --- a/claudedocs/accounting/[PLAN-2025-12-18] sales-management.md +++ /dev/null @@ -1,270 +0,0 @@ -# [PLAN-2025-12-18] 매출관리 페이지 구현 계획서 - -## 개요 -- **목표**: 회계관리 > 매출관리 페이지 구현 -- **참조**: 기안함(DraftBox) 구조 기반 -- **페이지 수**: 2개 (리스트 + 상세/등록) - ---- - -## 1. 리스트 페이지 (매출관리) - -### 1.1 페이지 구조 -- [ ] 경로: `/accounting/sales` -- [ ] 컴포넌트: `src/components/accounting/SalesManagement/index.tsx` -- [ ] IntegratedListTemplateV2 사용 - -### 1.2 헤더 영역 -- [ ] DateRangeSelector (공통 달력) -- [ ] 상태 필터 버튼들 - - [ ] 당월마감 - - [ ] 전월 - - [ ] 합의 - - [ ] 미수 - - [ ] 전체 -- [ ] 매출 등록 버튼 (클릭 시 등록 페이지로 이동) - -### 1.3 통계 카드 (4개) -- [ ] 매출금액 합계 (예: 3,123,000원) -- [ ] 입금금액 합계 (예: 3,123,000원) -- [ ] 미수건수 (예: 3건) -- [ ] 전체건수 (예: 4건) - -### 1.4 필터 셀렉트 박스들 -| 필터명 | 설명 | 옵션 | 기본값 | -|--------|------|------|--------| -| 계정과목별 | 계정과목명으로 분류 | 전체, 외상 매출, 상품 매출, 부품 매출, 공사 매출, 임대 수익, 기타 매출 | 제품 매출 | -| 거래처 필터 | 거래처별 검색/필터 | 거래처 검색 (검색 가능) | - | -| 매출유형 필터 | 다중 선택 가능 | 외상 매출, 상품 매출, 부품 매출, 공사 매출, 임대 수익, 기타 매출 | 전체 | -| 정렬 | 단독 선택 | 최신순, 금액 높은 순, 금액 낮은 순 | 최신순 | - -### 1.5 테이블 컬럼 -| 컬럼 | 설명 | -|------|------| -| 체크박스 | 다중 선택 | -| 번호 | 순번 | -| 매출번호 | 자동 채번 (포맷: 조합+자동생성) | -| 매출일 | 날짜 | -| 거래처명 | 거래처 | -| 결제대면(?) | 결제 방식 | -| 매출금액 | 금액 | -| 미수금액 | 미수 금액 | -| 비고 | 적요 | -| 작업 | 수정/삭제 아이콘 | - -### 1.6 기능 -- [ ] 검색 기능 (매출번호, 거래처명, 적요) -- [ ] 페이지네이션 (20건씩) -- [ ] 체크박스 전체/개별 선택 -- [ ] 행 클릭 → 상세 페이지 이동 - ---- - -## 2. 상세/등록 페이지 (매출 상세) - -### 2.1 페이지 구조 -- [ ] 경로: `/accounting/sales/[id]` (상세/수정) -- [ ] 경로: `/accounting/sales/new` (신규 등록) -- [ ] 컴포넌트: `src/components/accounting/SalesManagement/SalesDetail.tsx` - -### 2.2 헤더 영역 -- [ ] 페이지 제목: "매출 상세" 또는 "매출 상세_직접 등록" -- [ ] 버튼 - - [ ] 삭제 버튼 (신규 등록 시) - - [ ] 수정 버튼 - -### 2.3 기본 정보 섹션 -| 필드 | 타입 | 설명 | -|------|------|------| -| 매출번호 | 텍스트 (읽기전용/자동채번) | 수정 시 표시, 신규 시 자동 채번 | -| 매출일 | DatePicker | 날짜 선택 | -| 거래처명 | Select (검색 가능) | 거래처 선택 | -| 매출 유형 | Select | 외상 매출, 상품 매출, 부품 매출, 공사 매출, 임대 수익, 기타 매출 (기본: 제품 매출) | - -### 2.4 품목 정보 섹션 -- [ ] 테이블 형태의 품목 입력 -| 컬럼 | 타입 | 설명 | -|------|------|------| -| 번호 | 자동 | 순번 | -| 품목명 | Select/Input | 품목 선택 또는 입력 | -| 수량 | Number | 수량 입력 | -| 단가 | Number | 단가 입력 | -| 공급가액 | Number (계산) | 수량 × 단가 자동 계산 | -| 부가세 | Number (계산) | 공급가액 × 10% 자동 계산 | -| 적요 | Text | 메모 | -| 삭제 | Button | 행 삭제 (X 버튼) | - -- [ ] 합계 행: 공급가액 합계, 부가세 합계 -- [ ] 추가 버튼: 품목 행 추가 - -### 2.5 세금계산서 섹션 -- [ ] 세금계산서 발행 토글 (Switch) - - 클릭 시: 미발행 ↔ 발행완료 토글 - - 세금계산서 수동 발행 후 발행 상태로 변경 - -### 2.6 거래명세서 섹션 -- [ ] 거래명세서 발행 토글 (Switch) - - 클릭 시: 미발행 ↔ 발행완료 토글 -- [ ] 거래명세서 발행하기 버튼 - - 클릭 시: 거래처 이메일로 자동 발송 - - Alert: "거래명세서가 'abc@email.com'으로 발송되었습니다" - - 발행 후 자동으로 발행 상태 변경 - ---- - -## 3. 파일 구조 - -``` -src/ -├── app/[locale]/(protected)/accounting/ -│ └── sales/ -│ ├── page.tsx # 리스트 페이지 -│ ├── [id]/ -│ │ └── page.tsx # 상세/수정 페이지 -│ └── new/ -│ └── page.tsx # 신규 등록 페이지 -│ -└── components/accounting/ - └── SalesManagement/ - ├── index.tsx # 리스트 컴포넌트 - ├── SalesDetail.tsx # 상세/등록 컴포넌트 - ├── SalesItemTable.tsx # 품목 테이블 컴포넌트 - ├── TaxInvoiceSection.tsx # 세금계산서 섹션 - ├── TransactionStatementSection.tsx # 거래명세서 섹션 - └── types.ts # 타입 정의 -``` - ---- - -## 4. 타입 정의 (types.ts) - -```typescript -// 매출 레코드 -interface SalesRecord { - id: string; - salesNo: string; // 매출번호 - salesDate: string; // 매출일 - vendorId: string; // 거래처 ID - vendorName: string; // 거래처명 - salesType: SalesType; // 매출 유형 - items: SalesItem[]; // 품목 목록 - totalSupplyAmount: number; // 공급가액 합계 - totalVat: number; // 부가세 합계 - totalAmount: number; // 총 금액 - receivedAmount: number; // 입금액 - outstandingAmount: number; // 미수금액 - taxInvoiceIssued: boolean; // 세금계산서 발행 여부 - transactionStatementIssued: boolean; // 거래명세서 발행 여부 - note: string; // 비고 - status: SalesStatus; // 상태 - createdAt: string; - updatedAt: string; -} - -// 매출 유형 -type SalesType = - | 'credit' // 외상 매출 - | 'product' // 제품 매출 - | 'goods' // 상품 매출 - | 'parts' // 부품 매출 - | 'construction'// 공사 매출 - | 'rental' // 임대 수익 - | 'other'; // 기타 매출 - -// 매출 품목 -interface SalesItem { - id: string; - itemName: string; // 품목명 - quantity: number; // 수량 - unitPrice: number; // 단가 - supplyAmount: number; // 공급가액 - vat: number; // 부가세 - note: string; // 적요 -} - -// 매출 상태 -type SalesStatus = - | 'monthlyClose' // 당월마감 - | 'lastMonth' // 전월 - | 'agreed' // 합의 - | 'outstanding' // 미수 - | 'all'; // 전체 - -// 필터 옵션 -type FilterOption = 'all' | 'monthlyClose' | 'lastMonth' | 'agreed' | 'outstanding'; - -// 정렬 옵션 -type SortOption = 'latest' | 'amountHigh' | 'amountLow'; -``` - ---- - -## 5. 구현 체크리스트 - -### Phase 1: 기본 구조 설정 -- [ ] 폴더 구조 생성 -- [ ] 라우트 페이지 생성 (page.tsx들) -- [ ] types.ts 작성 - -### Phase 2: 리스트 페이지 구현 -- [ ] SalesManagement/index.tsx 생성 -- [ ] IntegratedListTemplateV2 연동 -- [ ] DateRangeSelector 연동 -- [ ] 상태 필터 버튼 구현 -- [ ] 통계 카드 구현 -- [ ] 필터 셀렉트 박스들 구현 -- [ ] 테이블 렌더링 구현 -- [ ] 모바일 카드 렌더링 구현 -- [ ] 페이지네이션 구현 -- [ ] 검색 기능 구현 -- [ ] Mock 데이터 생성 - -### Phase 3: 상세/등록 페이지 구현 -- [ ] SalesDetail.tsx 생성 -- [ ] 기본 정보 섹션 구현 -- [ ] SalesItemTable.tsx 구현 (품목 테이블) - - [ ] 품목 행 추가/삭제 - - [ ] 자동 계산 (공급가액, 부가세) - - [ ] 합계 행 -- [ ] TaxInvoiceSection.tsx 구현 -- [ ] TransactionStatementSection.tsx 구현 -- [ ] 폼 유효성 검사 -- [ ] 저장/수정 로직 - -### Phase 4: 연동 및 테스트 -- [ ] 리스트 ↔ 상세 페이지 연결 -- [ ] 신규 등록 플로우 확인 -- [ ] 수정 플로우 확인 -- [ ] 반응형 레이아웃 확인 - ---- - -## 6. 참고 사항 - -### 기안함(DraftBox)에서 참고할 패턴 -- IntegratedListTemplateV2 사용법 -- DateRangeSelector 연동 -- 필터/정렬 셀렉트 박스 패턴 -- 테이블/모바일 카드 렌더링 -- 체크박스 선택 관리 -- 페이지네이션 - -### 주의 사항 -1. 매출번호 자동 채번 로직 확인 필요 (API 연동 시) -2. 거래처 목록 API 연동 필요 -3. 품목 목록 API 연동 필요 -4. 세금계산서/거래명세서 발행 API 연동 필요 - ---- - -## 7. 예상 작업 시간 -- Phase 1: 기본 구조 설정 -- Phase 2: 리스트 페이지 구현 -- Phase 3: 상세/등록 페이지 구현 -- Phase 4: 연동 및 테스트 - ---- - -**작성일**: 2025-12-18 -**작성자**: Claude -**상태**: 계획 검토 대기 \ No newline at end of file diff --git a/claudedocs/accounting/[PLAN-2025-12-19] bank-account-transaction-inquiry.md b/claudedocs/accounting/[PLAN-2025-12-19] bank-account-transaction-inquiry.md deleted file mode 100644 index b76a92ac..00000000 --- a/claudedocs/accounting/[PLAN-2025-12-19] bank-account-transaction-inquiry.md +++ /dev/null @@ -1,204 +0,0 @@ -# [PLAN-2025-12-19] 입출금 계좌조회 페이지 구현 계획서 - -> **작성일**: 2025-12-19 -> **상태**: 📋 대기 (사용자 확인 필요) -> **참조**: DepositManagement, IntegratedListTemplateV2 - ---- - -## 1. 페이지 개요 - -| 항목 | 내용 | -|------|------| -| **페이지명** | 입출금 계좌조회 | -| **설명** | 은행 계좌 정보와 입출금 내역을 조회할 수 있습니다 | -| **URL** | `/ko/accounting/bank-transactions` | -| **아이콘** | `Building2` (은행) 또는 `Wallet` | - ---- - -## 2. UI 구성 분석 (스크린샷 기준) - -### 2.1 헤더 영역 -- [x] 페이지 타이틀: "입출금 계좌조회" -- [x] 설명: "은행 계좌 정보와 입출금 내역을 조회할 수 있습니다" - -### 2.2 필터 영역 (DateRangeSelector 확장) -- [ ] 기간 선택: 시작일 ~ 종료일 (DateRangeSelector 컴포넌트) -- [ ] 탭 버튼 그룹: - - `전체(선택)` - 전체 입출금 내역 - - `입금/수입` - 입금만 필터 - - `출금` - 출금만 필터 - - `입금` - (중복인지 확인 필요, 스크린샷 확인) - - `어제` - 어제 날짜 빠른 선택 - - `오늘` - 오늘 날짜 빠른 선택 - - `새로고침` - 데이터 새로고침 버튼 - -### 2.3 통계 카드 (4개) -| 순서 | 라벨 | 값 예시 | 아이콘 색상 | -|------|-----------|---------|-------------| -| 1 | 입금 | 3,123,000원 | 🔵 blue | -| 2 | 출금 | 3,123,000원 | 🔴 red | -| 3 | 입금 유형 미설정 | 4건 | 🟢 green | -| 4 | 출금 유형 미설정 | 4건 | 🟠 orange | - -### 2.4 검색 영역 -- [ ] 검색창 (은행명, 계좌번호, 거래처, 비고 검색) - -### 2.5 테이블 컬럼 (14개) -| 순서 | 컬럼명 | key | 정렬 | 비고 | -|------|--------|-----|------|------| -| 1 | 체크박스 | checkbox | center | 선택용 | -| 2 | 번호 | no | center | globalIndex | -| 3 | 은행명 | bankName | left | | -| 4 | 계좌명 | accountName | left | | -| 5 | 거래일시 | transactionDate | center | | -| 6 | 구분 | type | center | 입금/출금 Badge | -| 7 | 적요 | note | left | | -| 8 | 거래처 | vendorName | left | | -| 9 | 입금자/수취인 | depositorName | left | | -| 10 | 입금 | depositAmount | right | 숫자 포맷 | -| 11 | 출금 | withdrawalAmount | right | 숫자 포맷 | -| 12 | 잔액 | balance | right | 숫자 포맷 | -| 13 | 입출금 유형 | transactionType | center | Badge | -| 14 | 작업 | actions | center | 수정 버튼 (체크 시 표시) | - -### 2.6 테이블 합계 행 -- [ ] 합계 행 표시 (입금액 합계, 출금액 합계) - -### 2.7 수정 버튼 동작 -- [ ] 수정 버튼 클릭 시: - - 입금 데이터 → `/ko/accounting/deposits/{id}?mode=edit` 이동 - - 출금 데이터 → `/ko/accounting/withdrawals/{id}?mode=edit` 이동 - ---- - -## 3. 구현 체크리스트 - -### Phase 1: 기본 구조 설정 -- [ ] 1.1 페이지 라우트 생성 (`/accounting/bank-transactions/page.tsx`) -- [ ] 1.2 컴포넌트 폴더 생성 (`/components/accounting/BankTransactionInquiry/`) -- [ ] 1.3 types.ts 작성 (타입 정의) -- [ ] 1.4 index.tsx 기본 구조 작성 - -### Phase 2: 타입 정의 -- [ ] 2.1 `BankTransaction` 인터페이스 정의 - ```typescript - interface BankTransaction { - id: string; - bankName: string; // 은행명 - accountName: string; // 계좌명 - transactionDate: string; // 거래일시 - type: 'deposit' | 'withdrawal'; // 구분 (입금/출금) - note?: string; // 적요 - vendorId?: string; // 거래처 ID - vendorName?: string; // 거래처명 - depositorName?: string; // 입금자/수취인 - depositAmount: number; // 입금 - withdrawalAmount: number; // 출금 - balance: number; // 잔액 - transactionType?: string; // 입출금 유형 - sourceId: string; // 원본 입금/출금 ID (상세 이동용) - } - ``` -- [ ] 2.2 필터 타입 정의 -- [ ] 2.3 정렬 옵션 정의 - -### Phase 3: 통계 카드 구현 -- [ ] 3.1 총 입금 카드 -- [ ] 3.2 총 출금 카드 -- [ ] 3.3 입금 건 카드 -- [ ] 3.4 출금 건 카드 - -### Phase 4: 필터 영역 구현 -- [ ] 4.1 DateRangeSelector 연동 -- [ ] 4.2 탭 버튼 그룹 구현 (전체/입금/수입/출금/어제/오늘) -- [ ] 4.3 새로고침 버튼 -- [ ] 4.4 빠른 날짜 선택 (어제/오늘) 로직 - -### Phase 5: 테이블 구현 -- [ ] 5.1 IntegratedListTemplateV2 연동 -- [ ] 5.2 테이블 컬럼 정의 (14개 컬럼) -- [ ] 5.3 체크박스 기능 -- [ ] 5.4 번호 컬럼 (globalIndex 사용) -- [ ] 5.5 구분 컬럼 Badge (입금: 파랑, 출금: 빨강) -- [ ] 5.6 금액 컬럼 숫자 포맷팅 -- [ ] 5.7 합계 행 구현 (tableFooter) -- [ ] 5.8 작업 컬럼 (체크 시 수정 버튼 표시) - -### Phase 6: 상세 이동 로직 -- [ ] 6.1 수정 버튼 클릭 핸들러 -- [ ] 6.2 type에 따른 분기 처리 - - deposit → `/ko/accounting/deposits/{sourceId}?mode=edit` - - withdrawal → `/ko/accounting/withdrawals/{sourceId}?mode=edit` - -### Phase 7: Mock 데이터 및 테스트 -- [ ] 7.1 Mock 데이터 생성 함수 -- [ ] 7.2 필터링 로직 테스트 -- [ ] 7.3 정렬 로직 테스트 -- [ ] 7.4 페이지네이션 테스트 - -### Phase 8: 마무리 -- [ ] 8.1 모바일 카드 뷰 구현 -- [ ] 8.2 코드 정리 및 최적화 -- [ ] 8.3 테스트 URL 문서 업데이트 (`[REF] all-pages-test-urls.md`) - ---- - -## 4. 파일 구조 - -``` -src/ -├── app/[locale]/(protected)/accounting/ -│ └── bank-transactions/ -│ └── page.tsx # 페이지 라우트 -└── components/accounting/ - └── BankTransactionInquiry/ - ├── index.tsx # 메인 컴포넌트 - └── types.ts # 타입 정의 -``` - ---- - -## 5. 참고 사항 - -### 5.1 기존 컴포넌트 재사용 -- `IntegratedListTemplateV2` - 리스트 템플릿 -- `DateRangeSelector` - 날짜 범위 선택 -- `StatCard` - 통계 카드 -- `ListMobileCard` - 모바일 카드 - -### 5.2 스크린샷 Description 정보 참고 -- 필터 탭: 전체(선택), 입금/수입, 출금, 입금, 어제, 오늘 -- 수정 버튼 클릭 시 입금/출금 상세 화면으로 이동 -- 종류(정렬): 취입선, 등록순, 입력순 - -### 5.3 레이아웃 일치 확인 사항 -- DepositManagement와 동일한 레이아웃 구조 사용 -- 카드 4개 가로 배치 -- 테이블 합계 행 스타일 일치 - ---- - -## 6. 완료 후 작업 - -- [ ] `claudedocs/[REF] all-pages-test-urls.md` 업데이트 - ```markdown - | 입출금 계좌조회 | `/ko/accounting/bank-transactions` | 🆕 NEW | - ``` - ---- - -## 7. 예상 소요 시간 - -| Phase | 작업 | 예상 | -|-------|------|------| -| 1-2 | 기본 구조 + 타입 | - | -| 3-4 | 카드 + 필터 | - | -| 5 | 테이블 | - | -| 6-7 | 상세 이동 + Mock | - | -| 8 | 마무리 | - | - ---- - -**확인 후 작업 시작하겠습니다!** \ No newline at end of file diff --git a/claudedocs/accounting/[PLAN-2026-01-23] vendor-credit-analysis-modal.md b/claudedocs/accounting/[PLAN-2026-01-23] vendor-credit-analysis-modal.md deleted file mode 100644 index 18e65216..00000000 --- a/claudedocs/accounting/[PLAN-2026-01-23] vendor-credit-analysis-modal.md +++ /dev/null @@ -1,103 +0,0 @@ -# 신규 거래처 신용분석 모달 - -## 개요 -- **목적**: 신규 거래처 등록 시 국가관리 API를 통해 받아온 기업 신용정보를 표시 -- **위치**: 거래처 등록 완료 후 모달로 표시 -- **현재 단계**: 목업 데이터로 UI 구현 (추후 API 연동) - -## 화면 구성 - -### 1. 헤더 -- 로고 + "SAM 기업 신용분석 리포트" -- 조회일시 표시 - -### 2. 기업 정보 -- "신규거래 신용정보 조회" 뱃지 -- "기업 신용 분석" 제목 -- 사업자번호, 법인명 (대표자명), 평가기준일 정보 - -### 3. 자료 효력기간 안내 -- 노란 배경의 알림 박스 -- 데이터 유효기간 및 면책 안내 - -### 4. 종합 신용 신호등 -- 5단계 신호등 표시 (Level 1~5) -- 현재 레벨 강조 (예: 양호 Level 4) -- 신용 등급 설명 텍스트 -- "유료 상세 분석 제공받기" 버튼 - -### 5. 신용 리스크 프로필 -- 오각형 레이더 차트 - - 한국신용평가등급 - - 금융 종합 위험도 - - 매입 결제 - - 매출 결제 - - 저당권설정 - -### 6. 신용 상세 정보 -- 신용채무정보 버튼 -- 신용등급추이정보 버튼 -- 정보 없음 안내 텍스트 - -### 7. 하단 거래 승인 판정 -- 안전/위험 배지 -- 신용등급 (Level 1~5) -- 거래 유형 (계속사업자/신규거래 등) -- 외상 가능 여부 -- "거래 승인 완료" 버튼 - -## 데이터 구조 - -```typescript -interface CreditAnalysisData { - // 기업 정보 - businessNumber: string; // 사업자번호 - companyName: string; // 법인명 - representativeName: string; // 대표자명 - evaluationDate: string; // 평가기준일 - - // 신용 등급 - creditLevel: 1 | 2 | 3 | 4 | 5; // 1: 위험, 5: 최우량 - creditStatus: '위험' | '주의' | '보통' | '양호' | '우량'; - - // 리스크 프로필 (0~100) - riskProfile: { - koreaCreditRating: number; // 한국신용평가등급 - financialRisk: number; // 금융 종합 위험도 - purchasePayment: number; // 매입 결제 - salesPayment: number; // 매출 결제 - mortgageSetting: number; // 저당권설정 - }; - - // 거래 승인 판정 - approval: { - safety: '안전' | '주의' | '위험'; - level: number; - businessType: string; // 계속사업자, 신규거래 등 - creditAvailable: boolean; // 외상 가능 여부 - }; -} -``` - -## 파일 구조 - -``` -src/components/accounting/VendorManagement/ -├── CreditAnalysisModal.tsx # 신용분석 모달 컴포넌트 -└── CreditAnalysisModal/ - ├── index.tsx # 메인 모달 - ├── CreditSignal.tsx # 신용 신호등 컴포넌트 - ├── RiskRadarChart.tsx # 레이더 차트 컴포넌트 - └── types.ts # 타입 정의 - -src/app/[locale]/(protected)/dev/ -└── credit-analysis-test/ - └── page.tsx # 테스트 페이지 -``` - -## 구현 순서 - -1. [x] 계획 md 파일 작성 -2. [ ] CreditAnalysisModal 컴포넌트 생성 -3. [ ] 테스트 페이지 생성 -4. [ ] dev/test-urls에 URL 추가 diff --git a/claudedocs/api/[API-2026-03-10] calendar-bill-integration.md b/claudedocs/api/[API-2026-03-10] calendar-bill-integration.md deleted file mode 100644 index 74e235b1..00000000 --- a/claudedocs/api/[API-2026-03-10] calendar-bill-integration.md +++ /dev/null @@ -1,45 +0,0 @@ -# 어음 만기일 캘린더 연동 - -**날짜**: 2026-03-10 -**범위**: Backend (CalendarService) + Frontend (CalendarSection) - -## 변경 요약 - -대시보드 캘린더에 어음(Bill) 만기일을 5번째 데이터 소스로 추가. -기존 4개 소스(작업지시, 계약, 휴가, 범용일정)와 동일한 패턴. - -## Backend 변경 - -### `app/Services/CalendarService.php` - -- `use App\Models\Tenants\Bill` import 추가 -- `getSchedules()`: `$type === 'bill'` 필터 조건 및 merge 추가 -- `getBillSchedules()` 메서드 신규: - - `maturity_date` 기준 날짜 범위 필터 - - `paymentComplete`, `dishonored` 상태 제외 - - 아이템 형식: `bill_{id}`, `[만기] {거래처명} {금액}원` - - `type: 'bill'`, `isAllDay: true` - -## Frontend 변경 - -### `src/lib/api/dashboard/types.ts` -- `CalendarScheduleType`에 `'bill'` 추가 - -### `src/components/business/CEODashboard/types.ts` -- `CalendarScheduleItem.type`에 `'bill'` 추가 -- `CalendarTaskFilterType`에 `'bill'` 추가 - -### `src/components/business/CEODashboard/sections/CalendarSection.tsx` -- `SCHEDULE_TYPE_COLORS`: `bill: 'amber'` -- `SCHEDULE_TYPE_LABELS`: `bill: '어음'` -- `SCHEDULE_TYPE_BADGE_COLORS`: `bill: amber 배지 스타일` -- `TASK_FILTER_OPTIONS`: `{ value: 'bill', label: '어음' }` -- `ExtendedTaskFilterType`: `'bill'` 추가 -- 모바일 리스트뷰 `colorMap`: `bill: 'bg-amber-500'` - -## 검증 방법 - -1. 대시보드 캘린더에서 어음 만기일이 amber 색상 점으로 표시되는지 확인 -2. 캘린더 필터에서 "어음" 선택 시 어음 일정만 필터링되는지 확인 -3. 어음 만기일 클릭 시 `[만기] 거래처명 금액원` 형식으로 표시되는지 확인 -4. 기존 일정(일정/발주/시공/기타) 정상 동작 확인 diff --git a/claudedocs/api/[API-REQ-2026-03-03] expected-expenses-dashboard-detail-date-filter.md b/claudedocs/api/[API-REQ-2026-03-03] expected-expenses-dashboard-detail-date-filter.md deleted file mode 100644 index 1aeab278..00000000 --- a/claudedocs/api/[API-REQ-2026-03-03] expected-expenses-dashboard-detail-date-filter.md +++ /dev/null @@ -1,52 +0,0 @@ -# 백엔드 API 수정 요청: 당월 예상 지출 상세 - 날짜 범위 필터링 - -## 엔드포인트 -`GET /api/v1/expected-expenses/dashboard-detail` - -## 현재 상태 -- `transaction_type` 파라미터만 지원 (purchase, card, bill) -- `start_date`, `end_date` 파라미터를 **무시**함 -- `items` 배열이 항상 **당월(현재 월)** 기준으로만 반환됨 -- `summary`도 당월 기준 고정 (total_amount, change_rate 등) -- `monthly_trend`만 여러 월 데이터 포함 (최근 7개월) - -## 요청 내용 - -### 1. 날짜 범위 필터 지원 추가 -``` -GET /api/v1/expected-expenses/dashboard-detail?transaction_type=purchase&start_date=2026-01-01&end_date=2026-01-31 -``` - -| 파라미터 | 타입 | 설명 | 기본값 | -|---------|------|------|--------| -| `start_date` | string (yyyy-MM-dd) | 조회 시작일 | 당월 1일 | -| `end_date` | string (yyyy-MM-dd) | 조회 종료일 | 당월 말일 | -| `search` | string | 거래처/항목 검색 | (없음) | - -### 2. 기대 동작 -- `items`: `start_date` ~ `end_date` 범위의 거래 내역만 반환 -- `summary.total_amount`: 해당 기간의 합계 -- `summary.change_rate`: 해당 기간 vs 직전 동일 기간 비교 -- `vendor_distribution`: 해당 기간 기준 분포 -- `footer_summary`: 해당 기간 기준 합계 -- `monthly_trend`: 변경 불필요 (기존처럼 최근 7개월 유지) - -### 3. 검색 필터 (선택) -- `search` 파라미터로 거래처명/항목명 부분 검색 - -## 검증 데이터 -현재 `monthly_trend` 기준 데이터가 있는 월: -- 11월: 14,101,865원 -- 12월: 35,241,935원 -- 1월: 3,000,000원 -- 2월: 1,650,000원 - -`start_date=2026-01-01&end_date=2026-01-31` 조회 시: -- `items`: 1월 거래 내역 (현재 빈 배열) -- `summary.total_amount`: 3,000,000 (현재 0) - -## 프론트엔드 준비 상태 -- 프록시: 쿼리 파라미터 정상 전달 확인 -- 훅: `fetchData(cardId, { startDate, endDate, search })` 지원 -- 모달: 조회 버튼 + 날짜 필터 UI 완료 -- 백엔드 수정만 되면 즉시 동작 diff --git a/claudedocs/api/[API-SPEC-2026-03-03] ceo-dashboard-backend-api.md b/claudedocs/api/[API-SPEC-2026-03-03] ceo-dashboard-backend-api.md deleted file mode 100644 index 44b2d5de..00000000 --- a/claudedocs/api/[API-SPEC-2026-03-03] ceo-dashboard-backend-api.md +++ /dev/null @@ -1,821 +0,0 @@ -# CEO Dashboard 백엔드 API 명세서 - -**작성일**: 2026-03-03 -**기획서**: SAM_ERP_Storyboard_D1.7_260227.pdf p33~60 -**프론트엔드 타입**: `src/lib/api/dashboard/types.ts` -**대상**: 백엔드 팀 (Laravel sam-api) - ---- - -## 공통 규칙 - -### 응답 형식 -```json -{ - "success": true, - "message": "조회 성공", - "data": { ... } -} -``` - -### 인증 -- 모든 API는 `Authorization: Bearer {access_token}` 필수 -- Next.js API route 프록시(`/api/proxy/...`) 경유 - -### 캐싱 -- `sam_stat` 테이블 5분 캐시 (기존 구현 유지) -- 대시보드 API는 실시간성보다 성능 우선 - -### 날짜/기간 파라미터 규칙 -- 날짜: `YYYY-MM-DD` (예: `2026-03-03`) -- 월: `YYYY-MM` (예: `2026-03`) -- 분기: `year=2026&quarter=1` -- 기본값: 파라미터 미지정 시 **당월/당분기** 기준 - ---- - -## 검수 중 발견된 누락 API - -### N1. 오늘의 이슈 — 과거 이력 저장 및 조회 -**우선순위**: 상 -**페이지**: p34 -**현상**: `GET /api/v1/today-issues/summary?date=2026-02-17` 호출 시 항상 `{"items":[], "total_count":0}` 반환. 과거 이슈를 저장하는 구조가 없어서 이전 이슈 탭이 항상 빈 목록. - -**요구사항**: -1. **이슈 이력 테이블** 필요 (예: `dashboard_issue_history`) - - 매일 자정(또는 배치) 시점에 당일 이슈 스냅샷 저장 - - 또는 이슈 발생 시점에 이력 테이블에 INSERT -2. **기존 API 수정**: `GET /api/v1/today-issues/summary` - - `date` 파라미터가 있을 때 해당 날짜의 이력 데이터 반환 - - `date` 파라미터가 없으면 기존대로 실시간 집계 - -**Response** (기존 `TodayIssueApiResponse`와 동일): -```json -{ - "items": [ - { - "id": "issue-20260302-001", - "badge": "수주", - "notification_type": "sales_order", - "content": "대한건설 수주 3건 접수", - "time": "14:30", - "date": "2026-03-02", - "path": "/ko/sales/order-management", - "needs_approval": false - } - ], - "total_count": 5 -} -``` - -**Laravel 힌트**: -- 배치 저장 방식: `App\Console\Commands\SnapshotDailyIssues` (Schedule::daily) -- 또는 이벤트 기반: 수주/채권/재고 변동 시 `dashboard_issue_history` INSERT - -### N2. 자금현황 — 전일 대비 변동률 (daily_change) -**우선순위**: 중 -**페이지**: p33 -**현상**: `GET /api/v1/daily-report/summary` 응답에 `daily_change` 필드가 없음. 프론트엔드에서 하드코딩 fallback 값(+5.2%, +2.1%, +12.0%, -8.0%)을 사용 중. - -**요구사항**: -1. **기존 API 수정**: `GET /api/v1/daily-report/summary` -2. 응답에 `daily_change` 객체 추가 -3. 각 항목의 전일 대비 변동률(%) 계산 로직: - - `cash_asset_change_rate`: (오늘 현금성자산 - 어제 현금성자산) / 어제 현금성자산 × 100 - - `foreign_currency_change_rate`: (오늘 외국환 - 어제 외국환) / 어제 외국환 × 100 - - `income_change_rate`: (오늘 입금 - 어제 입금) / 어제 입금 × 100 - - `expense_change_rate`: (오늘 지출 - 어제 지출) / 어제 지출 × 100 -4. 어제 데이터 없을 시 해당 필드 `null` (프론트에서 fallback 처리) - -**Response** (기존 응답에 `daily_change` 추가): -```json -{ - "date": "2026-03-03", - "day_of_week": "화", - "cash_asset_total": 1250000000, - "foreign_currency_total": 85000, - "krw_totals": { "income": 45000000, "expense": 32000000, "balance": 1250000000 }, - "daily_change": { - "cash_asset_change_rate": 5.2, - "foreign_currency_change_rate": 2.1, - "income_change_rate": 12.0, - "expense_change_rate": -8.0 - } -} -``` - -**Laravel 힌트**: -- `DailyReportService`에서 전일 데이터 조회 추가 -- `sam_stat` 캐시 테이블에 전일 스냅샷 있으면 활용 -- 프론트 타입: `DailyChangeRate` (`src/lib/api/dashboard/types.ts:23`) - -### N3. 일일일보 — daily-accounts에 입출금관리 데이터 미반영 -**우선순위**: 상 -**페이지**: 일일일보 페이지 (`/ko/accounting/daily-report`) -**현상**: 입금관리/출금관리에서 당일 거래를 등록하면 대시보드 자금현황(`daily-report/summary`)의 합계에는 즉시 반영되지만, 일일일보 페이지의 계좌별 상세 테이블(`daily-report/daily-accounts`)에는 표시되지 않음. (출금 테스트로 확인됨, 입금도 동일 구조로 미반영 추정) - -**영향 범위**: -| 데이터 | 관리 테이블 | summary (합계) | daily-accounts (상세) | -|--------|-----------|:-:|:-:| -| 입금 | `deposits` (`/api/v1/deposits`) | ✅ 반영 추정 | ❌ 미반영 추정 | -| 출금 | `withdrawals` (`/api/v1/withdrawals`) | ✅ 반영 확인 | ❌ 미반영 확인 | -| 외국환 (USD) | 별도 관리 페이지 미확인 | ✅ 반영 | ❓ 확인 필요 | - -**원인 분석**: -- `GET /api/v1/daily-report/summary` → `krw_totals`에 `deposits`/`withdrawals` 테이블 데이터 포함 ✅ -- `GET /api/v1/daily-report/daily-accounts` → `bank_accounts` 단위 집계만 반환, `deposits`/`withdrawals` 테이블 미포함 ❌ - -**데이터 흐름**: -``` -입금관리 등록 → deposits 테이블 INSERT (bank_account_id 포함) -출금관리 등록 → withdrawals 테이블 INSERT (bank_account_id 포함) - ├─ summary API → krw_totals.income/expense에 합산 → 대시보드 ✅ - └─ daily-accounts API → bank_accounts 기준만 조회 → 일일일보 상세 ❌ -``` - -**요구사항**: -1. `GET /api/v1/daily-report/daily-accounts` 수정 -2. 각 계좌별로 `deposits` 테이블의 당일 income과 `withdrawals` 테이블의 당일 expense를 합산 -3. 또는 입금/출금 등록 시 해당 계좌의 거래 내역(`bank_account_transactions`)에도 자동 반영 - -**해결 방안 (택 1)**: -- **방안 A** (daily-accounts 쿼리 수정): `bank_accounts` LEFT JOIN `deposits`/`withdrawals` WHERE date = 당일 → 계좌별 income/expense에 합산 -- **방안 B** (트랜잭션 연동): 입금/출금 등록 시 `bank_account_transactions`에도 INSERT → daily-accounts가 자연스럽게 포함 - -**Response** (기존 `DailyAccountItemApi[]`와 동일, 데이터만 보완): -```json -[ - { - "id": "acc_1", - "category": "우리은행 123-456", - "match_status": "matched", - "carryover": 50000000, - "income": 1000000, - "expense": 50000, - "balance": 50950000, - "currency": "KRW" - } -] -``` - -**Laravel 힌트**: -- `DailyReportService`의 `getDailyAccounts()` 메서드 확인 -- `deposits` 테이블: `deposit.bank_account_id`로 해당 계좌 income 합산 -- `withdrawals` 테이블: `withdrawal.bank_account_id`로 해당 계좌 expense 합산 -- USD 계좌도 동일 패턴 적용 필요 - -### N4. 현황판 `purchases`(발주) — path 오류 + 데이터 정합성 이슈 -**우선순위**: 중 -**페이지**: p34 (현황판) - -#### 이슈 A: path 하드코딩 오류 -**현상**: `purchases` 항목의 실제 데이터는 `purchases` 테이블(매입, 공통)에서 조회하면서, path는 건설 모듈 경로로 하드코딩되어 있음. - -**문제 코드** (`StatusBoardService.php` — `getPurchaseStatus()`): -```php -$count = Purchase::query() - ->where('tenant_id', $tenantId) - ->where('status', 'draft') - ->count(); - -return [ - 'id' => 'purchases', - 'label' => '발주', - 'path' => '/construction/order/order-management', // ← 매입 데이터인데 건설 경로 -]; -``` - -- 데이터 출처: `purchases` 테이블 (모든 테넌트 공통 매입 테이블) -- path: `/construction/order/order-management` (건설 전용 페이지) -- **데이터와 path가 불일치** — 매입 draft 건수를 보여주면서 건설 발주 페이지로 링크 - -**현재 프론트 임시 대응**: `status-issue.ts`에서 `/accounting/purchase`(매입관리)로 오버라이드 중 - -**요구사항**: -1. path를 `/accounting/purchase`로 변경 (데이터 출처와 일치시키기) -2. 또는 테넌트 업종에 따라 path 동적 분기 (건설: `/construction/order/order-management`, 기타: `/accounting/purchase`) -3. 라벨도 재검토: "발주"가 맞는지, "매입(임시저장)"이 더 정확한지 - -#### 이슈 B: 데이터 정합성 의심 -**현상**: StatusBoard API에서 `purchases` count=**9건** 반환, 하지만 매입관리 페이지(`/accounting/purchase`)에서 전체 조회 시 **1건**만 표시. - -**확인 사항** (DB 직접 확인 필요): -```sql --- 현재 테넌트의 purchases 테이블 전체 건수 -SELECT COUNT(*), status FROM purchases WHERE tenant_id = {현재 테넌트 ID} GROUP BY status; - --- draft 상태 건수 (StatusBoard가 조회하는 조건) -SELECT COUNT(*) FROM purchases WHERE tenant_id = {현재 테넌트 ID} AND status = 'draft'; -``` - -**가능한 원인**: -1. StatusBoard와 매입관리 페이지가 다른 tenant_id 스코프로 조회 -2. DummyDataSeeder가 다른 tenant_id로 데이터 생성 -3. 매입관리 API에 추가 필터 조건이 있어서 draft 건이 제외됨 -4. StatusBoard가 실제와 다른 데이터를 집계 - -**기대 결과**: StatusBoard 9건 클릭 → 매입관리 페이지에서 9건 확인 가능해야 함 - ---- - -## 신규 API (10개) - -### 1. 매출 현황 Summary -**우선순위**: 중 -**페이지**: p39 - -``` -GET /api/v1/dashboard/sales/summary -``` - -**Query Params**: -| 파라미터 | 타입 | 필수 | 설명 | -|---------|------|------|------| -| year | int | N | 조회 연도 (기본: 당해) | -| month | int | N | 조회 월 (기본: 당월) | - -**Response** (`SalesStatusApiResponse`): -```json -{ - "cumulative_sales": 312300000, - "achievement_rate": 94.5, - "yoy_change": 12.5, - "monthly_sales": 312300000, - "monthly_trend": [ - { "month": "2026-08", "label": "8월", "amount": 250000000 }, - { "month": "2026-09", "label": "9월", "amount": 280000000 } - ], - "client_sales": [ - { "name": "대한건설", "amount": 95000000 }, - { "name": "삼성테크", "amount": 78000000 } - ], - "daily_items": [ - { - "date": "2026-02-01", - "client": "대한건설", - "item": "스크린 외", - "amount": 25000000, - "status": "deposited" - } - ], - "daily_total": 312300000 -} -``` - -**Laravel 힌트**: -- 매출: `sales_orders` 합계 (confirmed 상태) -- 달성률: 매출 목표 대비 (`sales_targets` 테이블) -- YoY: 전년 동월 대비 변화율 -- 거래처별: GROUP BY vendor_id → TOP 5 -- status 코드: `deposited` (입금완료), `unpaid` (미입금), `partial` (부분입금) - ---- - -### 2. 매입 현황 Summary -**우선순위**: 중 -**페이지**: p40 - -``` -GET /api/v1/dashboard/purchases/summary -``` - -**Query Params**: -| 파라미터 | 타입 | 필수 | 설명 | -|---------|------|------|------| -| year | int | N | 조회 연도 (기본: 당해) | -| month | int | N | 조회 월 (기본: 당월) | - -**Response** (`PurchaseStatusApiResponse`): -```json -{ - "cumulative_purchase": 312300000, - "unpaid_amount": 312300000, - "yoy_change": -12.5, - "monthly_trend": [ - { "month": "2026-08", "label": "8월", "amount": 180000000 } - ], - "material_ratio": [ - { "name": "원자재", "value": 55, "percentage": 55, "color": "#3b82f6" }, - { "name": "부자재", "value": 35, "percentage": 35, "color": "#10b981" }, - { "name": "소모품", "value": 10, "percentage": 10, "color": "#f59e0b" } - ], - "daily_items": [ - { - "date": "2026-02-01", - "supplier": "한국철강", - "item": "철판 외", - "amount": 45000000, - "status": "paid" - } - ], - "daily_total": 312300000 -} -``` - -**Laravel 힌트**: -- 매입: `purchase_orders` 합계 -- 미결제: 결제 미완료 건 합계 -- 원자재/부자재/소모품: `item_categories` 기준 분류 -- status 코드: `paid` (결제완료), `unpaid` (미결제), `partial` (부분결제) - ---- - -### 3. 생산 현황 Summary -**우선순위**: 상 -**페이지**: p41 - -``` -GET /api/v1/dashboard/production/summary -``` - -**Query Params**: -| 파라미터 | 타입 | 필수 | 설명 | -|---------|------|------|------| -| date | string | N | 조회 일자 (기본: 오늘, YYYY-MM-DD) | - -**Response** (`DailyProductionApiResponse`): -```json -{ - "date": "2026-02-23", - "day_of_week": "월요일", - "processes": [ - { - "process_name": "스크린", - "total_work": 10, - "todo": 3, - "in_progress": 4, - "completed": 3, - "urgent": 2, - "sub_line": 1, - "regular": 5, - "worker_count": 8, - "work_items": [ - { - "id": "wo_1", - "order_no": "SO-2026-001", - "client": "대한건설", - "product": "스크린 A형", - "quantity": 50, - "status": "in_progress" - } - ], - "workers": [ - { - "name": "김철수", - "assigned": 5, - "completed": 3, - "rate": 60 - } - ] - } - ], - "shipment": { - "expected_amount": 150000000, - "expected_count": 12, - "actual_amount": 120000000, - "actual_count": 9 - } -} -``` - -**Laravel 힌트**: -- 공정: `work_processes` 테이블 (스크린, 슬랫, 절곡 등) -- 작업: `work_orders` JOIN `work_process_id` -- status: `pending` → todo, `in_progress`, `completed` -- urgent: 납기 3일 이내 -- 출고: `shipments` 테이블 (당일 예상 vs 실적) - ---- - -### 4. 출고 현황 (생산 현황에 포함) -**우선순위**: 하 -**페이지**: p41 - -생산 현황 API의 `shipment` 필드로 포함됨. 별도 API 불필요. - ---- - -### 5. 미출고 내역 -**우선순위**: 하 -**페이지**: p42 - -``` -GET /api/v1/dashboard/unshipped/summary -``` - -**Query Params**: -| 파라미터 | 타입 | 필수 | 설명 | -|---------|------|------|------| -| days | int | N | 납기 N일 이내 (기본: 30) | - -**Response** (`UnshippedApiResponse`): -```json -{ - "items": [ - { - "id": "us_1", - "port_no": "P-2026-001", - "site_name": "강남 현장", - "order_client": "대한건설", - "due_date": "2026-02-25", - "days_left": 2 - } - ], - "total_count": 7 -} -``` - -**Laravel 힌트**: -- `shipment_items` WHERE shipped_at IS NULL AND due_date >= NOW() -- days_left: DATEDIFF(due_date, NOW()) -- ORDER BY due_date ASC (납기 임박 순) - ---- - -### 6. 시공 현황 -**우선순위**: 중 -**페이지**: p42 - -``` -GET /api/v1/dashboard/construction/summary -``` - -**Query Params**: -| 파라미터 | 타입 | 필수 | 설명 | -|---------|------|------|------| -| month | int | N | 조회 월 (기본: 당월) | - -**Response** (`ConstructionApiResponse`): -```json -{ - "this_month": 15, - "completed": 5, - "items": [ - { - "id": "cs_1", - "site_name": "강남 현장", - "client": "대한건설", - "start_date": "2026-02-01", - "end_date": "2026-02-28", - "progress": 85, - "status": "in_progress" - } - ] -} -``` - -**Laravel 힌트**: -- `constructions` 테이블 -- status: `in_progress`, `scheduled`, `completed` -- completed: 최근 7일 이내 완료 건 - ---- - -### 7. 근태 현황 -**우선순위**: 중 -**페이지**: p43 - -``` -GET /api/v1/dashboard/attendance/summary -``` - -**Query Params**: -| 파라미터 | 타입 | 필수 | 설명 | -|---------|------|------|------| -| date | string | N | 조회 일자 (기본: 오늘, YYYY-MM-DD) | - -**Response** (`DailyAttendanceApiResponse`): -```json -{ - "present": 42, - "on_leave": 3, - "late": 1, - "absent": 0, - "employees": [ - { - "id": "emp_1", - "department": "생산부", - "position": "과장", - "name": "김철수", - "status": "present" - } - ] -} -``` - -**Laravel 힌트**: -- `attendances` WHERE date = :date -- status: `present`, `on_leave`, `late`, `absent` -- employees: 이상 상태(late, absent, on_leave) 위주 표시 - ---- - -### 8. 일별 매출 내역 -**우선순위**: 하 -**페이지**: p47 (설정 팝업에서 별도 ON/OFF) - -매출 현황 API의 `daily_items`로 이미 포함. 별도 API 필요 시: - -``` -GET /api/v1/dashboard/sales/daily -``` - -**Query Params**: -| 파라미터 | 타입 | 필수 | 설명 | -|---------|------|------|------| -| start_date | string | N | 시작일 (기본: 당월 1일) | -| end_date | string | N | 종료일 (기본: 오늘) | -| page | int | N | 페이지 (기본: 1) | -| per_page | int | N | 건수 (기본: 20) | - ---- - -### 9. 일별 매입 내역 -**우선순위**: 하 - -매입 현황 API의 `daily_items`로 이미 포함. 별도 API 필요 시: - -``` -GET /api/v1/dashboard/purchases/daily -``` - -(매출 일별과 동일 구조) - ---- - -### 10. 접대비 상세 -**우선순위**: 상 -**페이지**: p53-54 - -``` -GET /api/v1/dashboard/entertainment/detail -``` - -**Query Params**: -| 파라미터 | 타입 | 필수 | 설명 | -|---------|------|------|------| -| year | int | N | 연도 | -| quarter | int | N | 분기 (1-4) | -| limit_type | string | N | annual/quarterly | -| company_type | string | N | large/medium/small | - -**Response**: -```json -{ - "summary": { - "total_used": 10000000, - "annual_limit": 40120000, - "remaining": 30120000, - "usage_rate": 24.9 - }, - "limit_calculation": { - "base_limit": 36000000, - "revenue_additional": 4120000, - "total_limit": 40120000, - "revenue": 2060000000, - "company_type": "medium" - }, - "quarterly_status": [ - { - "quarter": 1, - "label": "1분기", - "limit": 10030000, - "used": 3500000, - "remaining": 6530000, - "exceeded": 0 - } - ], - "transactions": [ - { - "id": 1, - "date": "2026-01-15", - "user_name": "홍길동", - "merchant_name": "강남식당", - "amount": 350000, - "counterpart": "대한건설", - "receipt_type": "법인카드", - "risk_flags": ["high_amount"] - } - ] -} -``` - ---- - -## 수정 API (6개) - -### 1. 가지급금 Summary (수정) -**현재**: 카드/가지급금/법인세/종합세 -**변경**: 카드/경조사/상품권/접대비/총합계 (5카드) - -``` -GET /api/proxy/card-transactions/summary -``` - -**Response 변경**: -```json -{ - "cards": [ - { "id": "cm1", "label": "카드", "amount": 3123000, "sub_label": "미정리 5건", "count": 5 }, - { "id": "cm2", "label": "경조사", "amount": 3123000, "sub_label": "미증빙 5건", "count": 5 }, - { "id": "cm3", "label": "상품권", "amount": 3123000, "sub_label": "미증빙 5건", "count": 5 }, - { "id": "cm4", "label": "접대비", "amount": 3123000, "sub_label": "미증빙 5건", "count": 5 }, - { "id": "cm_total", "label": "총 가지급금 합계", "amount": 350000000 } - ], - "check_points": [ - { - "id": "cm-cp1", - "type": "warning", - "message": "법인카드 사용 총 850만원이 가지급금으로 전환되었습니다.", - "highlights": [{ "text": "850만원", "color": "red" }] - } - ], - "warning_banner": "가지급금 인정이자 4.6%, 법인세 및 연말정산 시 대표자 종합세 가중 주의" -} -``` - -**Laravel 힌트**: -- 분류: `card_transactions.category` 기준 (card/congratulation/gift_card/entertainment) -- 미정리/미증빙: `evidence_status = 'pending'` COUNT - ---- - -### 2. 접대비 Summary (수정) -**현재**: 매출/한도/잔여한도/사용금액 -**변경**: 주말심야/기피업종/고액결제/증빙미비 (리스크 4종) - -``` -GET /api/proxy/entertainment/summary -``` - -**Response 변경**: -```json -{ - "cards": [ - { "id": "et1", "label": "주말/심야", "amount": 3123000, "sub_label": "5건", "count": 5 }, - { "id": "et2", "label": "기피업종 (유흥, 귀금속 등)", "amount": 3123000, "sub_label": "불인정 5건", "count": 5 }, - { "id": "et3", "label": "고액 결제", "amount": 3123000, "sub_label": "5건", "count": 5 }, - { "id": "et4", "label": "증빙 미비", "amount": 3123000, "sub_label": "5건", "count": 5 } - ], - "check_points": [...] -} -``` - -**리스크 감지 로직** (p60 참조): -- 주말/심야: 토~일, 22:00~06:00 거래 -- 기피업종: MCC 코드 기반 (유흥업소 7273, 귀금속 5944, 골프장 7941 등) -- 고액 결제: 설정 금액(기본 50만원) 초과 -- 증빙 미비: 적격증빙(세금계산서/카드매출전표) 없는 건 - ---- - -### 3. 복리후생비 Summary (수정) -**현재**: 한도/잔여한도/사용금액 -**변경**: 비과세한도초과/사적사용의심/특정인편중/항목별한도초과 (리스크 4종) - -``` -GET /api/proxy/welfare/summary -``` - -**Response 변경**: -```json -{ - "cards": [ - { "id": "wf1", "label": "비과세 한도 초과", "amount": 3123000, "sub_label": "5건", "count": 5 }, - { "id": "wf2", "label": "사적 사용 의심", "amount": 3123000, "sub_label": "5건", "count": 5 }, - { "id": "wf3", "label": "특정인 편중", "amount": 3123000, "sub_label": "5건", "count": 5 }, - { "id": "wf4", "label": "항목별 한도 초과", "amount": 3123000, "sub_label": "5건", "count": 5 } - ], - "check_points": [...] -} -``` - -**리스크 감지 로직**: -- 비과세 한도 초과: 항목별 비과세 기준 초과 (식대 20만원, 교통비 10만원 등) -- 사적 사용 의심: 주말/야간 + 비업무 업종 조합 -- 특정인 편중: 직원별 사용액 편차 > 평균의 200% -- 항목별 한도 초과: 설정 금액 초과 - ---- - -### 4. 가지급금 Detail (수정) - -기존 `LoanDashboardApiResponse`에 AI분류 컬럼 추가. - -``` -GET /api/v1/loans/dashboard -``` - -**Response 추가 필드**: -```json -{ - "items": [ - { - "...기존 필드...", - "ai_category": "카드", - "evidence_status": "미증빙" - } - ] -} -``` - ---- - -### 5. 복리후생비 Detail (수정) - -기존 `WelfareDetailApiResponse`에 계산방식 파라미터 추가. - -``` -GET /api/proxy/welfare/detail?calculation_type=fixed&fixed_amount_per_month=200000 -``` - -(기존 구현 유지, 계산 파라미터만 반영 확인) - ---- - -### 6. 부가세 Detail (수정) - -기존 `VatApiResponse`에 신고기간 파라미터 반영. - -``` -GET /api/proxy/vat/summary?period_type=quarter&year=2026&period=1 -``` - -(기존 구현 유지, 기간별 필터링 확인) - ---- - -## 리스크 감지 로직 참고 (p58-60) - -### MCC 코드 기피업종 -| MCC | 업종 | 분류 | -|-----|------|------| -| 7273 | 유흥업소 | 기피업종 | -| 5944 | 귀금속 | 기피업종 | -| 7941 | 골프장 | 기피업종 | -| 5813 | 주점 | 기피업종 | -| 7011 | 호텔/리조트 | 주의업종 | - -### 리스크 판별 규칙 -``` -규칙1: 시간대 이상 → 22:00~06:00 또는 토~일 -규칙2: 업종 이상 → MCC 기피업종 해당 -규칙3: 금액 이상 → 설정 금액 초과 (기본 50만원) -규칙4: 빈도 이상 → 월 10회 이상 동일 업종 -규칙5: 증빙 미비 → 적격증빙 없음 - -리스크 등급: -- 2개 이상 해당 → 🔴 고위험 -- 1개 해당 → 🟡 주의 -- 0개 → 🟢 정상 -``` - ---- - -## 계산 공식 참고 - -### 가지급금 인정이자 (p58) -``` -인정이자 = 가지급금잔액 × (4.6% / 365) × 경과일수 -법인세 추가 = 인정이자 × 19% -대표자 소득세 = 인정이자 × 35% -``` - -### 접대비 손금한도 (p59) -``` -기본한도: - 일반법인: 1,200만원/년 - 중소기업: 3,600만원/년 - -수입금액별 추가: - 100억 이하: 수입금액 × 0.2% - 100~500억: 2,000만원 + (수입금액-100억) × 0.1% - 500억 초과: 6,000만원 + (수입금액-500억) × 0.03% -``` - -### 복리후생비 (p60) -``` -방식1 (정액): 직원수 × 월정액 × 12 -방식2 (비율): 연봉총액 × 비율% - -비과세 한도: - 식대: 20만원/월 - 교통비: 10만원/월 - 경조사: 5만원/건 - 건강검진: 연간 총액/12 환산 - 교육훈련: 8만원/월 - 복지포인트: 10만원/월 -``` - ---- - -## 우선순위 정리 - -| 우선순위 | API | 이유 | -|---------|-----|------| -| 🔴 상 | 접대비 summary 수정, 복리후생비 summary 수정 | D1.7 카드 구조 변경 | -| 🔴 상 | 가지급금 summary 수정 | D1.7 카드 구조 변경 | -| 🔴 상 | 접대비 detail 신규 | 모달 확장 | -| 🟡 중 | 매출 현황, 매입 현황, 시공 현황, 근태 현황 | 신규 섹션 | -| 🟡 중 | 생산 현황 | 복잡한 공정 집계 | -| 🟢 하 | 미출고 내역, 일별 매출/매입 | 단순 조회 | diff --git a/claudedocs/api/[CHANGELOG-2026-03-09] sam-api-daily-changes.md b/claudedocs/api/[CHANGELOG-2026-03-09] sam-api-daily-changes.md deleted file mode 100644 index 8e236a5a..00000000 --- a/claudedocs/api/[CHANGELOG-2026-03-09] sam-api-daily-changes.md +++ /dev/null @@ -1,122 +0,0 @@ -# sam-api 변경 내역 (2026-03-09) - -총 **13개 커밋** (중복 1건 제외 실질 12건) - ---- - -## feat: 신규 기능 (6건) - -### 1. [database] codebridge 이관 완료 테이블 58개 삭제 -- **커밋**: `28ae481` / `74e3c21` (동일 커밋 2건) -- **작업자**: 권혁성 -- **변경 파일**: 마이그레이션 1개 -- **내용**: - - sam DB → codebridge DB 이관 완료된 58개 테이블 DROP - - FK 체크 비활성화 후 일괄 삭제 - - 복원 경로: `~/backups/sam_codebridge_tables_20260309.sql` - -### 2. [결재] 테넌트 부트스트랩에 기본 결재 양식 자동 시딩 -- **커밋**: `45a207d` -- **작업자**: 권혁성 -- **변경 파일**: `RecipeRegistry.php`, `ApprovalFormsStep.php` (신규) -- **내용**: - - ApprovalFormsStep 신규 생성 (proposal, expenseReport, expenseEstimate, attendance_request, reason_report) - - RecipeRegistry STANDARD 레시피에 등록 - - 테넌트 생성 시 자동 실행, 기존 테넌트는 `php artisan tenants:bootstrap --all` - -### 3. [quality] 검사 상태 자동 재계산 + 수주처 선택 연동 -- **커밋**: `3fc5f51` -- **작업자**: 권혁성 -- **변경 파일**: `QualityDocumentLocation.php`, `QualityDocumentService.php` -- **내용**: - - 개소별 inspection_status를 검사 데이터 기반 자동 판정 (15개 판정필드 + 사진 유무 → pending/in_progress/completed) - - 문서 status를 개소 상태 집계로 자동 재계산 - - transformToFrontend에 client_id 매핑 추가 - -### 4. [현황판/악성채권] 카드별 sub_label 추가 -- **커밋**: `56c60ec` -- **작업자**: 유병철 -- **변경 파일**: `BadDebtService.php`, `StatusBoardService.php` -- **내용**: - - BadDebtService: 카드별(전체/추심중/법적조치/회수완료) sub_labels 추가 - - StatusBoardService: 악성채권(최다 금액 거래처명), 신규거래처(최근 등록 업체명), 결재(최근 결재 제목) sub_label 추가 - -### 5. [복리후생] 상세 조회 커스텀 날짜 범위 필터 -- **커밋**: `60c4256` -- **작업자**: 유병철 -- **변경 파일**: `WelfareController.php`, `WelfareService.php` -- **내용**: - - start_date, end_date 쿼리 파라미터 추가 - - 커스텀 날짜 범위 지정 시 해당 범위로 일별 사용 내역 조회 - - 미지정 시 기존 분기 기준 유지 - -### 6. [finance] 더존 Smart A 표준 계정과목 추가 시딩 -- **커밋**: `1d5d161` -- **작업자**: 유병철 -- **변경 파일**: 마이그레이션 1개 (467줄) -- **내용**: - - 기획서 14장 기준 누락분 보완 - - tenant_id + code 중복 시 skip (기존 데이터 보호) - ---- - -## fix: 버그 수정 (4건) - -### 7. [현황판] 결재 카드 조회에 approvalOnly 스코프 추가 -- **커밋**: `ee9f4d0` -- **작업자**: 유병철 -- **변경 파일**: `StatusBoardService.php` -- **내용**: ApprovalStep 쿼리에 approvalOnly() 스코프 적용, 결재 유형만 필터링 - -### 8. [악성채권] tenant_id ambiguous 에러 + JOIN 컬럼 prefix 보완 -- **커밋**: `3929c5f`, `ca259cc` -- **작업자**: 유병철 -- **변경 파일**: `BadDebtService.php`, `StatusBoardService.php` -- **내용**: - - JOIN 쿼리에서 `bad_debts.tenant_id`로 테이블 명시 - - is_active, status 컬럼에도 `bad_debts.` prefix 추가 - -### 9. [세금계산서] NOT NULL 컬럼 null 방어 처리 -- **커밋**: `1861f4d` -- **작업자**: 유병철 -- **변경 파일**: `TaxInvoiceService.php` -- **내용**: supplier/buyer corp_num, corp_name null→빈문자열 보정 (ConvertEmptyStringsToNull 미들웨어 대응) - -### 10. [세금계산서] 매입/매출 방향별 필수값 조건 분리 -- **커밋**: `c62e59a` -- **작업자**: 유병철 -- **변경 파일**: `CreateTaxInvoiceRequest.php` -- **내용**: 매입(supplier 필수), 매출(buyer 필수) — `required → required_if:direction` 조건부 검증 - ---- - -## refactor: 리팩토링 (1건) - -### 11. [세금계산서/바로빌] ApiResponse::handle() 클로저 패턴 통일 -- **커밋**: `e6f13e3` -- **작업자**: 유병철 -- **변경 파일**: `BarobillSettingController.php`, `TaxInvoiceController.php` -- **내용**: - - 전체 액션 클로저 방식 전환 (show/save/testConnection, index/show/store/update/destroy/issue/bulkIssue/cancel/checkStatus/summary) - - 중간 변수 할당 제거, 일관된 응답 패턴 적용 - - **-38줄** (91→40+27 구조 정리) - ---- - -## 영향받는 주요 서비스 파일 - -| 파일 | 변경 횟수 | 도메인 | -|------|----------|--------| -| `StatusBoardService.php` | 4회 | 현황판/대시보드 | -| `BadDebtService.php` | 3회 | 악성채권 | -| `TaxInvoiceService.php` | 1회 | 세금계산서 | -| `TaxInvoiceController.php` | 1회 | 세금계산서 | -| `QualityDocumentService.php` | 1회 | 품질검사 | -| `WelfareService.php` | 1회 | 복리후생 | - -## 작업자별 커밋 수 - -| 작업자 | 커밋 수 | 주요 도메인 | -|--------|---------|-------------| -| 유병철 | 9건 | 현황판, 악성채권, 세금계산서, 복리후생, 계정과목 | -| 권혁성 | 4건 | DB 이관, 결재 시딩, 품질검사 | diff --git a/claudedocs/api/[FEAT-2026-03-10] calendar-new-schedule-types.md b/claudedocs/api/[FEAT-2026-03-10] calendar-new-schedule-types.md deleted file mode 100644 index de15e44e..00000000 --- a/claudedocs/api/[FEAT-2026-03-10] calendar-new-schedule-types.md +++ /dev/null @@ -1,77 +0,0 @@ -# 캘린더 신규 일정 타입 추가 (결제예정/납기/출고) - -**작업일**: 2026-03-10 -**목적**: CEO 대시보드 캘린더에서 자금/물류/납기 일정을 한눈에 파악 - ---- - -## 추가된 타입 - -| 타입 | 라벨 | 색상 | ID 형식 | 제목 형식 | -|------|------|------|---------|----------| -| `expected_expense` | 결제예정 | rose (분홍) | `expense_{id}` | `[결제] {거래처명} {금액}원` | -| `delivery` | 납기 | cyan (청록) | `delivery_{id}` | `[납기] {거래처명} {현장명 or 수주번호}` | -| `shipment` | 출고 | teal (틸) | `shipment_{id}` | `[출고] {거래처명} {현장명 or 출하번호}` | - -## 제외 항목 - -| 항목 | 사유 | -|------|------| -| 미수금 입금 예정일 | `Deposit` 모델에 expected_date 필드 없음 → Phase 2 | -| 세금 납부 예정일 | 이미 CalendarScheduleStore + 상수로 orange 색상 표시 중 | - ---- - -## 변경 파일 - -### Backend (1파일) - -**`app/Services/CalendarService.php`** -- import 추가: `Order`, `ExpectedExpense`, `Shipment` -- `getSchedules()`: 3개 merge 블록 추가 (`expected_expense`, `delivery`, `shipment`) -- 신규 private 메서드 3개: - - `getExpectedExpenseSchedules()` — `ExpectedExpense` 모델, `expected_payment_date`, `payment_status != 'paid'` - - `getDeliverySchedules()` — `Order` 모델, `delivery_date`, 활성 status_code 5개 - - `getShipmentSchedules()` — `Shipment` 모델, `scheduled_date`, status in ('scheduled', 'ready') - -### Frontend (3파일) - -**`src/components/business/CEODashboard/types.ts`** -- `CalendarScheduleItem.type` union에 3개 타입 추가 -- `CalendarTaskFilterType` union에 3개 타입 추가 - -**`src/lib/api/dashboard/types.ts`** -- `CalendarScheduleType` union에 3개 타입 추가 - -**`src/components/business/CEODashboard/sections/CalendarSection.tsx`** -- `SCHEDULE_TYPE_COLORS`: rose/cyan/teal 추가 -- `SCHEDULE_TYPE_ROUTES`: 3개 라우트 추가 -- `SCHEDULE_TYPE_LABELS`: 결제예정/납기/출고 추가 -- `SCHEDULE_TYPE_BADGE_COLORS`: rose/cyan/teal 뱃지 스타일 추가 -- `TASK_FILTER_OPTIONS`: 필터 드롭다운 옵션 3개 추가 -- `ExtendedTaskFilterType`: `'bill'` 제거 (CalendarTaskFilterType에 이미 포함) -- `getScheduleLink()`: `expected_expense`는 목록 페이지만 이동 (상세 없음) -- 모바일 `colorMap`: 3개 dot 색상 추가 - ---- - -## 라우트 매핑 - -| 타입 | 상세보기 클릭 시 이동 경로 | 비고 | -|------|--------------------------|------| -| `expected_expense` | `/ko/accounting/expected-expenses` | 목록 페이지 (상세 없음) | -| `delivery` | `/ko/sales/order-management-sales/{id}` | 수주 상세 | -| `shipment` | `/ko/outbound/shipments/{id}` | 출고 상세 | - ---- - -## 검수 결과 (2026-03-10) - -- [x] 캘린더 '전체' 필터에서 결제예정 항목 표시 -- [x] 필터 드롭다운에 결제예정/납기/출고 옵션 추가 -- [x] 결제예정 필터 선택 시 해당 타입만 표시 -- [x] 결제예정 상세보기 링크 동작 -- [x] 결제예정 뱃지 rose 색상 표시 -- [x] 기존 5개 타입 정상 동작 -- [x] TypeScript 빌드 에러 없음 -- [ ] 납기/출고 데이터 표시 (테스트 DB에 해당 날짜 데이터 없어 미확인 — 기능은 정상) diff --git a/claudedocs/api/[IMPL-2025-11-07] api-key-management.md b/claudedocs/api/[IMPL-2025-11-07] api-key-management.md deleted file mode 100644 index c71e1969..00000000 --- a/claudedocs/api/[IMPL-2025-11-07] api-key-management.md +++ /dev/null @@ -1,320 +0,0 @@ -# API Key 관리 가이드 - -## 📋 개요 - -PHP 백엔드에서 발급하는 API Key의 안전한 관리 및 주기적 갱신 대응 방법 - ---- - -## 🔑 현재 API Key 정보 - -```yaml -개발용 API Key: - 키 값: 42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a - 발급일: 2025-11-07 - 용도: 개발 환경 고정 키 - 갱신: 주기적으로 변동 가능 -``` - ---- - -## 🔐 보안 원칙 - -### ✅ DO (반드시 해야 할 것) -- `.env.local`에만 실제 키 저장 -- 서버 사이드 코드에서만 사용 -- Git에 절대 커밋 금지 -- 팀 공유 문서로 키 관리 - -### ❌ DON'T (절대 하지 말 것) -- 하드코딩 금지 -- `NEXT_PUBLIC_` 접두사 사용 금지 -- 브라우저 코드에서 사용 금지 -- 공개 저장소에 업로드 금지 - ---- - -## 📁 파일 구성 - -### .env.local (실제 키 - Git 제외) -```env -# API Key (서버 사이드 전용 - 절대 공개 금지!) -# 개발용 고정 키 (주기적 갱신 예정) -# 발급일: 2025-11-07 -# 갱신 필요 시: PHP 백엔드 팀에 새 키 요청 -API_KEY=42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a -``` - -### .env.example (템플릿 - Git 커밋 OK) -```env -# API Key (⚠️ 서버 사이드 전용 - 절대 공개 금지!) -# 개발팀 공유: 팀 내부 문서에서 키 값 확인 -# 주기적 갱신: PHP 백엔드 팀에서 새 키 발급 시 업데이트 필요 -API_KEY=your-secret-api-key-here -``` - -### .gitignore 확인 -```bash -# 라인 100-101에 이미 포함됨 -.env.local -.env*.local -``` - ---- - -## 🔄 API Key 갱신 프로세스 - -### 1️⃣ PHP 팀에서 새 키 발급 -``` -PHP 백엔드 팀 → 새 API Key 발급 - ↓ - 팀 공유 문서 업데이트 -``` - -### 2️⃣ 로컬 개발 환경 업데이트 -```bash -# .env.local 파일 열기 -vi .env.local - -# 또는 -code .env.local - -# API_KEY 값만 변경 -API_KEY=새로운키값여기에입력 - -# 개발 서버 재시작 -npm run dev -``` - -### 3️⃣ 프로덕션 환경 업데이트 - -#### Vercel 배포 -```bash -# CLI로 업데이트 -vercel env add API_KEY production - -# 또는 대시보드에서 -# Settings → Environment Variables → API_KEY 편집 -``` - -#### AWS/기타 환경 -```bash -# 환경 변수 업데이트 -export API_KEY=새로운키값 - -# 또는 배포 설정에서 환경 변수 수정 -``` - -### 4️⃣ 검증 -```bash -# 개발 서버 시작 시 자동으로 검증됨 -npm run dev - -# 콘솔 출력 확인: -# 🔐 API Key Configuration: -# ├─ Configured: ✅ -# ├─ Valid Format: ✅ -# ├─ Masked Key: 42Jf********************dk1a -# └─ Length: 48 chars -``` - ---- - -## 🛠️ API Key 검증 유틸리티 - -### 자동 검증 기능 -```typescript -// lib/api/auth/api-key-validator.ts -import { apiKeyValidator } from '@/lib/api/auth/api-key-validator'; - -// 개발 서버 시작 시 자동 실행 -console.log(apiKeyValidator.getDebugInfo()); - -// 출력 예시: -// API Key Status: -// ├─ Configured: ✅ -// ├─ Valid Format: ✅ -// ├─ Masked Key: 42Jf********************dk1a -// └─ Length: 48 chars -``` - -### 수동 검증 -```typescript -import { apiKeyValidator } from '@/lib/api/auth/api-key-validator'; - -// API Key 존재 확인 -if (!apiKeyValidator.isConfigured()) { - console.error('API Key not configured!'); -} - -// 형식 검증 -if (!apiKeyValidator.isValid()) { - console.error('Invalid API Key format!'); -} - -// 디버그 정보 출력 -console.log(apiKeyValidator.getDebugInfo()); -``` - ---- - -## 📊 사용 예시 - -### 서버 사이드 (Next.js API Route) -```typescript -// app/api/sync/route.ts -import { createApiKeyClient } from '@/lib/api/auth/api-key-client'; - -export async function GET() { - try { - // 환경 변수에서 자동으로 키를 가져옴 - const client = createApiKeyClient(); - - const data = await client.fetchData('/api/external-data'); - - return Response.json({ success: true, data }); - } catch (error) { - console.error('API request failed:', error); - return Response.json( - { error: 'Failed to fetch data' }, - { status: 500 } - ); - } -} -``` - -### 백그라운드 스크립트 -```typescript -// scripts/sync-data.ts -import { createApiKeyClient } from '@/lib/api/auth/api-key-client'; -import { apiKeyValidator } from '@/lib/api/auth/api-key-validator'; - -async function syncData() { - // 1. 환경 변수 확인 - console.log(apiKeyValidator.getDebugInfo()); - - if (!apiKeyValidator.isValid()) { - throw new Error('Invalid API Key configuration'); - } - - // 2. API 요청 - const client = createApiKeyClient(); - const data = await client.fetchData('/api/sync-endpoint'); - - console.log('Sync completed:', data); -} - -syncData().catch(console.error); -``` - ---- - -## ⚠️ 에러 처리 - -### API Key 미설정 -``` -❌ API_KEY is not configured! -📝 Please check: - 1. .env.local file exists - 2. API_KEY is set correctly - 3. Restart development server (npm run dev) - -💡 Contact backend team if you need a new API key. -``` - -**해결 방법:** -1. `.env.local` 파일 생성 확인 -2. `API_KEY=실제키값` 입력 -3. `npm run dev` 재시작 - -### API Key 형식 오류 -``` -❌ Invalid API Key format! - - Minimum 32 characters required - - Only alphanumeric characters allowed -``` - -**해결 방법:** -1. PHP 팀에서 발급받은 키 확인 -2. 복사 시 공백/줄바꿈 없는지 확인 -3. 정확한 키 값 재입력 - ---- - -## 🔍 만료 경고 (선택사항) - -### 만료 체크 기능 -```typescript -// lib/api/auth/key-expiry-check.ts -import { apiKeyValidator } from './api-key-validator'; - -// API Key 발급일 -const issuedDate = new Date('2025-11-07'); - -// 90일 유효기간으로 체크 -const status = apiKeyValidator.checkExpiry(issuedDate, 90); - -console.log(status.message); -// ✅ API Key valid (75 days left) -// ⚠️ API Key expiring in 10 days -// 🔴 API Key expired! Contact backend team. - -if (status.isExpiring) { - console.warn('⚠️ Please contact backend team for new API key!'); -} -``` - ---- - -## 📚 체크리스트 - -### 초기 설정 -- [ ] `.env.local` 파일 생성 -- [ ] `API_KEY` 값 입력 -- [ ] `.gitignore`에 `.env.local` 포함 확인 -- [ ] 개발 서버 시작 후 검증 확인 - -### 키 갱신 시 -- [ ] PHP 팀에서 새 키 수령 -- [ ] `.env.local` 업데이트 -- [ ] 로컬 개발 서버 재시작 -- [ ] 검증 로그 확인 -- [ ] 프로덕션 환경 변수 업데이트 - -### 보안 점검 -- [ ] Git에 `.env.local` 커밋 안됨 -- [ ] 브라우저 코드에서 사용 안함 -- [ ] `NEXT_PUBLIC_` 접두사 없음 -- [ ] 팀 공유 문서에 키 기록 - ---- - -## 🚀 다음 단계 - -API Key 설정 완료 후: -1. `createApiKeyClient()` 사용하여 API 요청 -2. 서버 사이드 코드에서만 호출 -3. 에러 발생 시 검증 로그 확인 -4. 주기적으로 만료 시간 체크 (선택) - ---- - -## 📞 문의 - -- **API Key 발급**: PHP 백엔드 팀 -- **기술 지원**: 프론트엔드 팀 -- **보안 문제**: DevOps/보안 팀 - ---- - -## 관련 파일 - -### 프론트엔드 -- `src/lib/api/auth/api-key-client.ts` - API Key 클라이언트 -- `src/lib/api/auth/api-key-validator.ts` - API Key 검증 유틸리티 -- `src/app/api/sync/route.ts` - 서버 사이드 API Route 예시 - -### 설정 파일 -- `.env.local` - 환경 변수 (API_KEY 저장) -- `.env.example` - 환경 변수 템플릿 -- `.gitignore` - Git 제외 설정 \ No newline at end of file diff --git a/claudedocs/api/[IMPL-2025-11-11] api-route-type-safety.md b/claudedocs/api/[IMPL-2025-11-11] api-route-type-safety.md deleted file mode 100644 index 74e6ce73..00000000 --- a/claudedocs/api/[IMPL-2025-11-11] api-route-type-safety.md +++ /dev/null @@ -1,334 +0,0 @@ -# API Route 타입 안전성 가이드 - -## 📋 개요 - -Next.js API Route에서 백엔드 API 응답 데이터를 프론트엔드로 전달할 때, TypeScript 타입 정의를 통해 데이터 누락을 방지하는 방법 - ---- - -## 🎯 문제 사례 - -### 발생한 이슈 -로그인 API를 테스트할 때, API 테스트 도구에서는 `roles` 데이터가 정상적으로 나오지만, 프론트엔드에서는 빈 배열로 나오는 현상 발생 - -### 원인 분석 -```typescript -// ❌ 타입 정의 없이 데이터 전달 (문제 코드) -const responseData = { - message: data.message, - user: data.user, - tenant: data.tenant, - menus: data.menus, - // roles: data.roles, ← 누락됨! - token_type: data.token_type, - expires_in: data.expires_in, - expires_at: data.expires_at, -}; -``` - -**문제점:** -- 백엔드에서 `roles` 데이터를 반환했지만 -- Next.js API Route에서 프론트로 전달할 때 `roles` 필드를 포함하지 않음 -- 타입 정의가 없어서 컴파일 타임에 감지 불가 - ---- - -## ✅ 해결 방법 - -### 1. 백엔드 응답 타입 정의 - -```typescript -/** - * 백엔드 API 로그인 응답 타입 - */ -interface BackendLoginResponse { - message: string; - access_token: string; - refresh_token: string; - token_type: string; - expires_in: number; - expires_at: string; - user: { - id: number; - user_id: string; - name: string; - email: string; - phone: string; - }; - tenant: { - id: number; - company_name: string; - business_num: string; - tenant_st_code: string; - other_tenants: any[]; - }; - menus: Array<{ - id: number; - parent_id: number | null; - name: string; - url: string; - icon: string; - sort_order: number; - is_external: number; - external_url: string | null; - }>; - roles: Array<{ - id: number; - name: string; - description: string; - }>; -} -``` - -### 2. 프론트엔드 응답 타입 정의 - -```typescript -/** - * 프론트엔드로 전달할 응답 타입 (토큰 제외) - */ -interface FrontendLoginResponse { - message: string; - user: BackendLoginResponse['user']; - tenant: BackendLoginResponse['tenant']; - menus: BackendLoginResponse['menus']; - roles: BackendLoginResponse['roles']; // ✅ 명시적으로 포함 - token_type: string; - expires_in: number; - expires_at: string; -} -``` - -### 3. 타입 적용 - -```typescript -export async function POST(request: NextRequest) { - try { - // ... 백엔드 API 호출 - - // ✅ 타입 지정 - const data: BackendLoginResponse = await backendResponse.json(); - - // ✅ 타입 지정 + 모든 필드 포함 - const responseData: FrontendLoginResponse = { - message: data.message, - user: data.user, - tenant: data.tenant, - menus: data.menus, - roles: data.roles, // ✅ 누락 방지 - token_type: data.token_type, - expires_in: data.expires_in, - expires_at: data.expires_at, - }; - - return NextResponse.json(responseData, { status: 200 }); - } catch (error) { - // ... 에러 처리 - } -} -``` - ---- - -## 🎁 타입 정의의 장점 - -### 1. 컴파일 타임 에러 감지 -```typescript -// ❌ roles 누락 시 TypeScript 에러 발생 -const responseData: FrontendLoginResponse = { - message: data.message, - user: data.user, - // ... roles 필드 빠짐 - // ⚠️ Type Error: Property 'roles' is missing in type -}; -``` - -### 2. 자동 완성 지원 -- IDE에서 필드명 자동 완성 -- 오타 방지 -- 개발 생산성 향상 - -### 3. API 문서 역할 -- 백엔드 API 스펙이 코드에 명시됨 -- 별도 문서 없이도 데이터 구조 파악 가능 -- 팀원 간 커뮤니케이션 비용 절감 - -### 4. 리팩토링 안정성 -- 백엔드 API 변경 시 즉시 감지 -- 영향 범위 파악 용이 -- 안전한 코드 수정 - ---- - -## 📝 적용 체크리스트 - -### API Route 작성 시 필수 사항 - -- [ ] 백엔드 응답 타입 인터페이스 정의 -- [ ] 프론트엔드 응답 타입 인터페이스 정의 -- [ ] `await response.json()` 시 타입 지정 -- [ ] 프론트 응답 객체에 타입 지정 -- [ ] 모든 필수 필드 포함 확인 - -### 타입 정의 원칙 - -```typescript -// ✅ Good: 명시적 타입 지정 -const data: BackendResponse = await response.json(); -const result: FrontendResponse = { - // ... 모든 필드 포함 -}; - -// ❌ Bad: 타입 없이 작성 -const data = await response.json(); -const result = { - // ... 필드 누락 가능성 -}; -``` - ---- - -## 🔍 실제 적용 예시 - -### 파일 위치 -``` -src/app/api/auth/login/route.ts -``` - -### Before (문제 코드) -```typescript -export async function POST(request: NextRequest) { - // ... - const data = await backendResponse.json(); // 타입 없음 - - const responseData = { - message: data.message, - user: data.user, - menus: data.menus, - // roles 누락! - }; - - return NextResponse.json(responseData); -} -``` - -### After (개선 코드) -```typescript -interface BackendLoginResponse { - // ... 전체 타입 정의 - roles: Array<{ id: number; name: string; description: string }>; -} - -interface FrontendLoginResponse { - // ... 전체 타입 정의 - roles: BackendLoginResponse['roles']; -} - -export async function POST(request: NextRequest) { - // ... - const data: BackendLoginResponse = await backendResponse.json(); - - const responseData: FrontendLoginResponse = { - message: data.message, - user: data.user, - menus: data.menus, - roles: data.roles, // ✅ 명시적 포함 - // ... 기타 필드 - }; - - return NextResponse.json(responseData); -} -``` - ---- - -## 🚨 주의사항 - -### 1. 타입과 실제 데이터 불일치 -```typescript -// ⚠️ 백엔드 API 스펙 변경 시 -interface BackendResponse { - // 타입 정의는 그대로인데 - user_name: string; -} - -// 실제 응답은 변경됨 -{ - "username": "홍길동" // 필드명 변경됨 -} -``` - -**대응 방안:** -- 백엔드 API 스펙 변경 시 타입 정의도 함께 업데이트 -- API 응답 검증 로직 추가 (런타임 체크) -- 백엔드 팀과 스펙 변경 사전 공유 - -### 2. Optional vs Required -```typescript -// 명확한 옵셔널 표시 -interface Response { - required_field: string; // 필수 - optional_field?: string; // 선택 - nullable_field: string | null; // null 가능 -} -``` - -### 3. any 타입 남용 금지 -```typescript -// ❌ Bad -interface Response { - data: any; // 타입 안전성 상실 -} - -// ✅ Good -interface Response { - data: { - id: number; - name: string; - }; -} -``` - ---- - -## 📚 관련 문서 - -- [Authentication Implementation Guide](./[IMPL-2025-11-07]%20authentication-implementation-guide.md) -- [Token Management Guide](./[IMPL-2025-11-10]%20token-management-guide.md) -- [API Requirements](./[REF]%20api-requirements.md) - ---- - -## 📌 핵심 요약 - -1. **API Route는 백엔드와 프론트 사이의 중간 레이어** - - 데이터 변환/필터링 역할 수행 - - 타입 정의로 누락 방지 - -2. **타입 정의의 3가지 핵심 가치** - - 컴파일 타임 에러 감지 - - 개발 생산성 향상 (자동완성) - - 리팩토링 안정성 보장 - -3. **실무 적용 원칙** - - 백엔드 응답 타입 → 프론트 응답 타입 순서로 정의 - - 모든 API Route에 타입 적용 - - 백엔드 스펙 변경 시 타입도 함께 업데이트 - ---- - -**작성일:** 2025-11-11 -**작성자:** Claude Code -**마지막 수정:** 2025-11-11 - ---- - -## 관련 파일 - -### 프론트엔드 -- `src/app/api/auth/login/route.ts` - 로그인 API Route -- `src/types/auth.ts` - 인증 타입 정의 -- `src/lib/api/auth/types.ts` - API 인증 타입 - -### 참조 문서 -- `claudedocs/auth/[IMPL-2025-11-07] authentication-implementation-guide.md` -- `claudedocs/auth/[IMPL-2025-11-10] token-management-guide.md` diff --git a/claudedocs/api/[IMPL-2025-12-30] fetch-wrapper-migration.md b/claudedocs/api/[IMPL-2025-12-30] fetch-wrapper-migration.md deleted file mode 100644 index a06d1c56..00000000 --- a/claudedocs/api/[IMPL-2025-12-30] fetch-wrapper-migration.md +++ /dev/null @@ -1,262 +0,0 @@ -# Fetch Wrapper Migration Checklist - -**생성일**: 2025-12-30 -**목적**: 모든 Server Actions의 API 통신을 `serverFetch`로 중앙화 - -## 목적 및 배경 - -### 왜 fetch-wrapper를 도입했는가? - -1. **중앙화된 인증 처리** - - 401 에러(세션 만료) 발생 시 → 로그인 페이지 리다이렉트 - - 모든 API 호출에서 **일관된 인증 검증** - -2. **개발 규칙 표준화** - - 새 작업자도 `serverFetch` 사용하면 자동으로 인증 검증 적용 - - 개별 파일마다 인증 로직 구현 불필요 - -3. **유지보수성 향상** - - 인증 로직 변경 시 **`fetch-wrapper.ts` 한 파일만** 수정 - - 403, 네트워크 에러 등 공통 에러 처리도 중앙화 - ---- - -## 마이그레이션 패턴 - -### Before (기존 패턴) -```typescript -import { cookies } from 'next/headers'; - -async function getApiHeaders(): Promise { - const cookieStore = await cookies(); - const token = cookieStore.get('access_token')?.value; - return { - 'Authorization': token ? `Bearer ${token}` : '', - // ... - }; -} - -export async function getSomething() { - const headers = await getApiHeaders(); - const response = await fetch(url, { headers }); - // 401 처리 없음! -} -``` - -### After (새 패턴) -```typescript -import { serverFetch } from '@/lib/api/fetch-wrapper'; - -export async function getSomething() { - const { response, error } = await serverFetch(url, { method: 'GET' }); - - if (error) { - // 401/403/네트워크 에러 자동 처리됨 - return { success: false, error: error.message }; - } - - const data = await response.json(); - // ... -} -``` - ---- - -## 마이그레이션 체크리스트 - -### Accounting 도메인 (12 files) ✅ 완료 -- [x] `SalesManagement/actions.ts` -- [x] `VendorManagement/actions.ts` -- [x] `PurchaseManagement/actions.ts` -- [x] `DepositManagement/actions.ts` -- [x] `WithdrawalManagement/actions.ts` -- [x] `VendorLedger/actions.ts` -- [x] `ReceivablesStatus/actions.ts` -- [x] `ExpectedExpenseManagement/actions.ts` -- [x] `CardTransactionInquiry/actions.ts` -- [x] `DailyReport/actions.ts` -- [x] `BadDebtCollection/actions.ts` -- [x] `BankTransactionInquiry/actions.ts` - -### HR 도메인 (6 files) ✅ 완료 -- [x] `EmployeeManagement/actions.ts` ✅ (이미 마이그레이션됨) -- [x] `VacationManagement/actions.ts` ✅ -- [x] `SalaryManagement/actions.ts` ✅ -- [x] `CardManagement/actions.ts` ✅ -- [x] `DepartmentManagement/actions.ts` ✅ -- [x] `AttendanceManagement/actions.ts` ✅ - -### Approval 도메인 (4 files) ✅ 완료 -- [x] `ApprovalBox/actions.ts` -- [x] `DraftBox/actions.ts` -- [x] `ReferenceBox/actions.ts` -- [x] `DocumentCreate/actions.ts` (파일 업로드는 직접 fetch 유지) - -### Production 도메인 (4 files) ✅ 완료 -- [x] `WorkerScreen/actions.ts` -- [x] `WorkOrders/actions.ts` -- [x] `WorkResults/actions.ts` -- [x] `ProductionDashboard/actions.ts` - -### Settings 도메인 (10 files) ✅ 완료 -- [x] `WorkScheduleManagement/actions.ts` -- [x] `SubscriptionManagement/actions.ts` -- [x] `PopupManagement/actions.ts` -- [x] `PaymentHistoryManagement/actions.ts` -- [x] `LeavePolicyManagement/actions.ts` -- [x] `NotificationSettings/actions.ts` -- [x] `AttendanceSettingsManagement/actions.ts` -- [x] `CompanyInfoManagement/actions.ts` -- [x] `AccountInfoManagement/actions.ts` -- [x] `AccountManagement/actions.ts` - -### 기타 도메인 (12 files) ✅ 완료 -- [x] `process-management/actions.ts` -- [x] `outbound/ShipmentManagement/actions.ts` -- [x] `material/StockStatus/actions.ts` -- [x] `material/ReceivingManagement/actions.ts` -- [x] `customer-center/shared/actions.ts` -- [x] `board/actions.ts` -- [x] `reports/actions.ts` -- [x] `quotes/actions.ts` -- [x] `board/BoardManagement/actions.ts` -- [x] `attendance/actions.ts` -- [x] `pricing/actions.ts` -- [x] `quality/InspectionManagement/actions.ts` - ---- - -## 진행 상황 - -| 도메인 | 파일 수 | 완료 | 상태 | -|--------|---------|------|------| -| Accounting | 12 | 12 | ✅ 완료 | -| HR | 6 | 6 | ✅ 완료 | -| Approval | 4 | 4 | ✅ 완료 | -| Production | 4 | 4 | ✅ 완료 | -| Settings | 10 | 10 | ✅ 완료 | -| 기타 | 12 | 12 | ✅ 완료 | -| **총계** | **48** | **48** | **100%** ✅ | - -### 완료된 파일 (완전 마이그레이션) - -**Accounting 도메인 (12/12)** -- [x] `SalesManagement/actions.ts` -- [x] `VendorManagement/actions.ts` -- [x] `PurchaseManagement/actions.ts` -- [x] `DepositManagement/actions.ts` -- [x] `WithdrawalManagement/actions.ts` -- [x] `VendorLedger/actions.ts` -- [x] `ReceivablesStatus/actions.ts` -- [x] `ExpectedExpenseManagement/actions.ts` -- [x] `CardTransactionInquiry/actions.ts` -- [x] `DailyReport/actions.ts` -- [x] `BadDebtCollection/actions.ts` -- [x] `BankTransactionInquiry/actions.ts` - -**HR 도메인 (6/6)** -- [x] `EmployeeManagement/actions.ts` (이미 마이그레이션됨) -- [x] `VacationManagement/actions.ts` -- [x] `SalaryManagement/actions.ts` -- [x] `CardManagement/actions.ts` -- [x] `DepartmentManagement/actions.ts` -- [x] `AttendanceManagement/actions.ts` - -**Approval 도메인 (4/4)** -- [x] `ApprovalBox/actions.ts` -- [x] `DraftBox/actions.ts` -- [x] `ReferenceBox/actions.ts` -- [x] `DocumentCreate/actions.ts` (파일 업로드는 직접 fetch 유지) - -**Production 도메인 (4/4)** -- [x] `WorkerScreen/actions.ts` -- [x] `WorkOrders/actions.ts` -- [x] `WorkResults/actions.ts` -- [x] `ProductionDashboard/actions.ts` - -**Settings 도메인 (10/10)** -- [x] `WorkScheduleManagement/actions.ts` -- [x] `SubscriptionManagement/actions.ts` -- [x] `PopupManagement/actions.ts` -- [x] `PaymentHistoryManagement/actions.ts` -- [x] `LeavePolicyManagement/actions.ts` -- [x] `NotificationSettings/actions.ts` -- [x] `AttendanceSettingsManagement/actions.ts` -- [x] `CompanyInfoManagement/actions.ts` -- [x] `AccountInfoManagement/actions.ts` -- [x] `AccountManagement/actions.ts` - -**기타 도메인 (12/12)** ✅ 완료 -- [x] `process-management/actions.ts` -- [x] `outbound/ShipmentManagement/actions.ts` -- [x] `material/StockStatus/actions.ts` -- [x] `material/ReceivingManagement/actions.ts` -- [x] `customer-center/shared/actions.ts` -- [x] `board/actions.ts` -- [x] `reports/actions.ts` -- [x] `quotes/actions.ts` -- [x] `board/BoardManagement/actions.ts` -- [x] `attendance/actions.ts` -- [x] `pricing/actions.ts` -- [x] `quality/InspectionManagement/actions.ts` - ---- - -## 참조 파일 - -- **fetch-wrapper**: `src/lib/api/fetch-wrapper.ts` -- **errors**: `src/lib/api/errors.ts` -- **완료된 예시**: `src/components/accounting/BillManagement/actions.ts` (참고용) - ---- - -## 주의사항 - -1. **기존 `getApiHeaders()` 함수 제거** - `serverFetch`가 헤더 자동 생성 -2. **`import { cookies } from 'next/headers'` 제거** - wrapper에서 처리 -3. **에러 응답 구조 맞추기** - `{ success: false, error: string }` 형태 유지 -4. **빌드 테스트 필수** - 마이그레이션 후 `npm run build` 확인 - ---- - -## 🔜 추가 작업 (마이그레이션 완료 후) - -### Phase 2: 리프레시 토큰 자동 갱신 적용 - -**현재 문제:** -- access_token 만료 시 (약 2시간) 바로 로그인 리다이렉트됨 -- refresh_token (7일)을 사용한 자동 갱신 로직이 호출되지 않음 -- 결과: 40분~2시간 후 세션 만료 → 재로그인 필요 - -**목표:** -- 401 발생 시 → 리프레시 토큰으로 갱신 시도 → 성공 시 재시도 -- 7일간 세션 유지 (refresh_token 만료 시에만 재로그인) - -**적용 범위:** - -| 영역 | 적용 위치 | 작업 | -|------|----------|------| -| Server Actions | `fetch-wrapper.ts` | 401 시 리프레시 후 재시도 로직 추가 | -| 품목관리 | `ItemListClient.tsx` 등 | 클라이언트 fetch에 리프레시 로직 추가 | -| 품목기준관리 | 관련 컴포넌트들 | 클라이언트 fetch에 리프레시 로직 추가 | - -**관련 파일:** -- `src/lib/auth/token-refresh.ts` - 리프레시 함수 (이미 존재) -- `src/app/api/auth/refresh/route.ts` - 리프레시 API (이미 존재) - -**예상 구현:** -```typescript -// fetch-wrapper.ts 401 처리 부분 -if (response.status === 401 && !options?.skipAuthCheck) { - // 1. 리프레시 토큰으로 갱신 시도 - const refreshResult = await refreshTokenServer(refreshToken); - - if (refreshResult.success) { - // 2. 새 토큰으로 원래 요청 재시도 - return serverFetch(url, { ...options, skipAuthCheck: true }); - } - - // 3. 리프레시도 실패하면 로그인 리다이렉트 - redirect('/login'); -} -``` diff --git a/claudedocs/api/[IMPL-2026-03-18] expense-accounts-receipt-no.md b/claudedocs/api/[IMPL-2026-03-18] expense-accounts-receipt-no.md deleted file mode 100644 index ff98b49c..00000000 --- a/claudedocs/api/[IMPL-2026-03-18] expense-accounts-receipt-no.md +++ /dev/null @@ -1,70 +0,0 @@ -# 접대비 증빙번호(receipt_no) 자동 매핑 및 수기 입력 지원 - -## 날짜: 2026-03-18 - -## 배경 -CEO 대시보드 접대비 현황에서 "증빙 미비"로 표시되는 항목의 근본 원인: -- `expense_accounts.receipt_no`가 항상 `null`로 고정 저장됨 -- 카드 거래(바로빌) 승인번호가 전달되지 않음 -- 수기 전표 입력 시 증빙번호 입력 필드 부재 - -## 수정 파일 - -### 백엔드 (sam-api) - -| 파일 | 변경 내용 | -|------|----------| -| `app/Traits/SyncsExpenseAccounts.php` | `syncExpenseAccounts()`에 `$receiptNo` 파라미터 추가 + `resolveReceiptNo()` 메서드 신규 | -| `app/Services/JournalSyncService.php` | `saveForSource()`에 `$receiptNo` 파라미터 추가 → `syncExpenseAccounts()`에 전달 | -| `app/Services/GeneralJournalEntryService.php` | `store()`, `updateJournal()`에서 `$data['receipt_no']` → `syncExpenseAccounts()`에 전달 | -| `app/Http/Requests/V1/GeneralJournalEntry/StoreManualJournalRequest.php` | `receipt_no` validation 규칙 추가 (`nullable\|string\|max:100`) | -| `app/Http/Requests/V1/GeneralJournalEntry/UpdateJournalRequest.php` | `receipt_no` validation 규칙 추가 (`nullable\|string\|max:100`) | - -### 프론트엔드 (sam-react-prod) - -| 파일 | 변경 내용 | -|------|----------| -| `src/components/accounting/GeneralJournalEntry/ManualJournalEntryModal.tsx` | 증빙번호 입력 필드 추가 (FormField) | -| `src/components/accounting/GeneralJournalEntry/actions.ts` | `createManualJournal()`에 `receiptNo` 파라미터 추가 → `receipt_no` body 전달 | - -## 증빙번호 결정 로직 (우선순위) - -``` -1순위: 명시적 전달 ($receiptNo 파라미터) — 수기 전표에서 사용자가 직접 입력 -2순위: 바로빌 카드 승인번호 자동 조회 — source_type=barobill_card일 때 approval_num -3순위: null — 기본값 (증빙 미비로 판정됨) -``` - -## SyncsExpenseAccounts 변경 상세 - -### Before -```php -ExpenseAccount::create([ - 'receipt_no' => null, // 항상 null -]); -``` - -### After -```php -// 증빙번호 결정: 명시 전달 > 바로빌 승인번호 > null -$resolvedReceiptNo = $receiptNo ?? $this->resolveReceiptNo($entry); - -ExpenseAccount::create([ - 'receipt_no' => $resolvedReceiptNo, -]); -``` - -### resolveReceiptNo() 신규 메서드 -- `SOURCE_BAROBILL_CARD` → `source_key`에서 ID 추출 → `BarobillCardTransaction.approval_num` 조회 -- 그 외 → `null` - -## 영향 범위 -- CEO 대시보드 접대비 현황: 증빙 미비 건수 정확도 향상 -- CEO 대시보드 복리후생비 현황: 동일 트레이트 사용으로 함께 개선 -- 일반전표입력: 증빙번호 필드 추가 (UI) -- 카드사용내역 분개: 바로빌 승인번호 자동 매핑 (추가 UI 변경 없음) - -## 테스트 결과 -- 수기 전표에 증빙번호 입력 → expense_accounts.receipt_no에 저장 확인 -- 기존 미증빙 전표에 증빙번호 PUT → 증빙 미비 해소 확인 -- CEO 대시보드 접대비 현황: 증빙 미비 0건 / 고액 결제 0건 확인 diff --git a/claudedocs/api/[REF] api-analysis.md b/claudedocs/api/[REF] api-analysis.md deleted file mode 100644 index 0175f239..00000000 --- a/claudedocs/api/[REF] api-analysis.md +++ /dev/null @@ -1,342 +0,0 @@ -# SAM API 분석 결과 - -API 문서: https://api.5130.co.kr/docs?api-docs-v1.json - -## 🔍 핵심 발견사항 - -### 1. 인증 방식 - -**현재 API 문서에서 확인된 인증 방식:** -``` -❌ 세션 쿠키 기반 (Sanctum SPA 모드) - 없음 -✅ Bearer Token (JWT) 방식 -✅ API Key 방식 -``` - -### 2. 보안 스킴 - -```yaml -securitySchemes: - ApiKeyAuth: - type: apiKey - in: header - name: X-API-KEY (추정) - - BearerAuth: - type: http - scheme: bearer - bearerFormat: JWT -``` - -**사용 패턴:** -- 대부분의 엔드포인트: `ApiKeyAuth` OR `BearerAuth` -- 두 방식 중 선택 가능 - -### 3. User 관련 엔드포인트 (Admin) - -**POST /api/v1/admin/users** (사용자 생성) -```json -{ - "name": "string", // 필수 - "email": "string", // 필수 - "password": "string", // 필수 - "user_id": "string", // 선택 - "phone": "string", // 선택 - "roles": ["string"] // 선택 -} -``` - -**성공 응답 (201):** -```json -{ - "id": 1, - "name": "John Doe", - "email": "user@example.com", - "created_at": "2024-01-01T00:00:00Z" -} -``` - -**에러 응답:** -- 409: 이메일 중복 -- 400: 필수 파라미터 누락 - -## ⚠️ 중요한 발견 - -### 인증 엔드포인트가 문서에 없음 - -**현재 문서에서 찾을 수 없는 엔드포인트:** -``` -❌ POST /api/auth/login -❌ POST /api/auth/register -❌ POST /api/auth/logout -❌ GET /api/auth/user -❌ POST /api/auth/refresh -❌ GET /sanctum/csrf-cookie -``` - -**이유:** -1. 아직 구성 중이라 문서화 안됨 -2. 별도 인증 서버 존재 가능성 -3. 다른 경로에 존재 (예: /api/v1/auth/*) - -## 🎯 설계 조정 필요 - -### 원래 설계 (Sanctum SPA 모드) -``` -인증: HTTP-only 쿠키 -저장: 서버 세션 -CSRF: 필요 -Middleware: 쿠키 확인 -``` - -### 새로운 설계 (Bearer Token 모드) -``` -인증: JWT Bearer Token -저장: localStorage 또는 쿠키 -CSRF: 불필요 -Middleware: Token 확인 (클라이언트 사이드) -``` - -## 📋 두 가지 시나리오 - -### 시나리오 A: Bearer Token (JWT) 방식 - -**장점:** -- 현재 API 구조와 일치 -- Stateless (서버 세션 불필요) -- 모바일 앱 지원 용이 -- API Key 또는 Token 선택 가능 - -**단점:** -- XSS 취약 (localStorage 사용 시) -- Token 관리 복잡 (refresh token 등) -- CORS 이슈 가능성 - -**구현 방식:** -```typescript -// 1. 로그인 → JWT 토큰 받기 -const { token } = await login(email, password); -localStorage.setItem('token', token); - -// 2. API 요청 시 토큰 포함 -fetch('/api/endpoint', { - headers: { - 'Authorization': `Bearer ${token}` - } -}); - -// 3. Middleware는 클라이언트에서 체크 -// (서버 Middleware에서는 체크 불가) -``` - -**Middleware 제약:** -- Next.js Middleware는 서버사이드 실행 -- localStorage 접근 불가 -- Token 검증 어려움 -- **→ 클라이언트 가드 컴포넌트 필요** - ---- - -### 시나리오 B: 세션 쿠키 방식 (권장) - -**장점:** -- 서버 Middleware에서 인증 체크 가능 -- XSS 방어 (HTTP-only 쿠키) -- CSRF 토큰으로 보안 강화 -- 기존 설계 그대로 사용 - -**단점:** -- Laravel API 수정 필요 -- 세션 관리 필요 - -**필요한 Laravel 변경:** -```php -// config/sanctum.php -'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost:3000')), - -// API Routes -Route::post('/login', [AuthController::class, 'login']); // 세션 생성 -Route::post('/logout', [AuthController::class, 'logout'])->middleware('auth:sanctum'); -Route::get('/user', [AuthController::class, 'user'])->middleware('auth:sanctum'); -``` - -**프론트엔드는 기존 설계 그대로:** -```typescript -// Middleware에서 쿠키 확인 -const sessionCookie = request.cookies.get('laravel_session'); -if (!sessionCookie) redirect('/login'); -``` - ---- - -## 🤔 권장사항 - -### 1차 선택: **백엔드 개발자와 협의 필요** - -**질문할 사항:** -``` -Q1. 인증 방식이 정해졌나요? - A. Bearer Token (JWT) - B. 세션 쿠키 (Sanctum SPA) - C. 둘 다 지원 - -Q2. 로그인/회원가입 API 경로는? - 예: POST /api/v1/auth/login? - -Q3. 로그인 응답 형식은? - A. { token: "xxx" } // JWT - B. { user: {...} } // 세션 + 쿠키 - -Q4. Token refresh 로직 있나요? (JWT인 경우) - -Q5. CORS 설정 완료? - - Allow Origin: http://localhost:3000 - - Allow Credentials: true (쿠키 사용 시) -``` - -### 2차 선택: **시나리오별 구현 방식** - -#### Option A: Bearer Token으로 진행 -```typescript -// 장점: 현재 API 구조 그대로 사용 -// 단점: Middleware 인증 체크 불가, 클라이언트 가드 필요 - -// lib/auth/token-client.ts -class TokenClient { - async login(email: string, password: string) { - const { token } = await fetch('/api/v1/auth/login', { - method: 'POST', - body: JSON.stringify({ email, password }) - }).then(r => r.json()); - - localStorage.setItem('auth_token', token); - } - - getToken() { - return localStorage.getItem('auth_token'); - } -} - -// components/ProtectedRoute.tsx (클라이언트 가드) -function ProtectedRoute({ children }) { - const token = localStorage.getItem('auth_token'); - - if (!token) { - redirect('/login'); - } - - return children; -} -``` - -#### Option B: 세션 쿠키로 진행 (권장) -```typescript -// 장점: Middleware 인증, 보안 강화 -// 단점: Laravel API 수정 필요 - -// 기존 설계 문서 그대로 구현 -// claudedocs/authentication-design.md 참고 -``` - ---- - -## 📝 다음 단계 - -### 1. 백엔드 개발자와 협의 ✅ 최우선 - -**확인 사항:** -- [ ] 인증 방식 확정 (JWT vs 세션) -- [ ] 로그인/회원가입 API 경로 -- [ ] 응답 형식 -- [ ] CORS 설정 - -### 2. 협의 결과에 따라 - -**A. Bearer Token 방식:** -- [ ] Token 클라이언트 구현 -- [ ] AuthContext (Token 저장/관리) -- [ ] 클라이언트 가드 컴포넌트 -- [ ] API 인터셉터 (Token 자동 추가) - -**B. 세션 쿠키 방식:** -- [ ] 기존 설계 그대로 구현 -- [ ] Sanctum 클라이언트 -- [ ] Middleware 인증 로직 -- [ ] 로그인/회원가입 페이지 - -### 3. API 테스트 - -**Bearer Token 테스트:** -```bash -# 로그인 -curl -X POST https://api.5130.co.kr/api/v1/auth/login \ - -H "Content-Type: application/json" \ - -d '{"email":"test@test.com","password":"password"}' - -# 응답 예상 -{"token": "eyJhbGciOiJIUzI1NiIs..."} - -# 인증 요청 -curl -X GET https://api.5130.co.kr/api/v1/user \ - -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." -``` - -**세션 쿠키 테스트:** -```bash -# CSRF 토큰 -curl -X GET https://api.5130.co.kr/sanctum/csrf-cookie -c cookies.txt - -# 로그인 -curl -X POST https://api.5130.co.kr/api/login \ - -b cookies.txt -c cookies.txt \ - -d '{"email":"test@test.com","password":"password"}' - -# 사용자 정보 -curl -X GET https://api.5130.co.kr/api/user \ - -b cookies.txt -``` - ---- - -## 🎯 현재 상태 - -**대기 사항:** -1. ✅ API 문서 분석 완료 -2. ⏳ 인증 방식 확정 대기 -3. ⏳ 실제 로그인 API 경로 확인 대기 -4. ⏳ 응답 형식 확인 대기 - -**다음 액션:** -- 백엔드 개발자와 인증 방식 협의 -- 결정되면 즉시 구현 시작 - ---- - -## 💡 개인적 권장 - -**세션 쿠키 방식 (Sanctum SPA) 추천 이유:** - -1. **보안**: HTTP-only 쿠키로 XSS 방어 -2. **Middleware 활용**: 서버사이드 인증 체크 -3. **간단함**: CSRF 토큰만 관리하면 됨 -4. **Laravel 친화적**: Sanctum이 기본 제공 -5. **우리 설계와 완벽히 일치**: 기존 문서 그대로 사용 - -하지만 최종 결정은 백엔드 아키텍처와 요구사항에 따라야 합니다! - -**백엔드 개발자에게 이 문서 공유 후 협의 추천** 👍 - ---- - -## 관련 파일 - -### 프론트엔드 -- `src/lib/api/client.ts` - 통합 HTTP 클라이언트 -- `src/lib/api/auth/token-storage.ts` - Token 저장 관리 -- `src/lib/api/auth/auth-config.ts` - 인증 설정 -- `src/middleware.ts` - 인증 미들웨어 -- `src/contexts/AuthContext.tsx` - 인증 상태 관리 - -### 설정 파일 -- `.env.local` - 환경 변수 -- `next.config.ts` - Next.js 설정 \ No newline at end of file diff --git a/claudedocs/api/[REF] api-requirements.md b/claudedocs/api/[REF] api-requirements.md deleted file mode 100644 index 3d0eb057..00000000 --- a/claudedocs/api/[REF] api-requirements.md +++ /dev/null @@ -1,436 +0,0 @@ -# Laravel API 요구사항 체크리스트 - -프론트엔드 인증 구현을 위해 백엔드에서 준비해야 할 API 목록입니다. - -## 📋 필수 API 엔드포인트 - -### 1. CSRF 토큰 발급 -```http -GET /sanctum/csrf-cookie -``` - -**응답:** -``` -Set-Cookie: XSRF-TOKEN=xxx; Path=/; HttpOnly -Status: 204 No Content -``` - -**용도:** 로그인/회원가입 전에 CSRF 토큰 획득 - ---- - -### 2. 로그인 -```http -POST /api/login -Content-Type: application/json - -{ - "email": "user@example.com", - "password": "password123" -} -``` - -**성공 응답 (200):** -```json -{ - "user": { - "id": 1, - "name": "John Doe", - "email": "user@example.com", - "created_at": "2024-01-01T00:00:00.000000Z" - }, - "message": "로그인 성공" -} - -Set-Cookie: laravel_session=xxx; Path=/; HttpOnly; SameSite=Lax -``` - -**실패 응답 (422):** -```json -{ - "message": "The provided credentials are incorrect.", - "errors": { - "email": ["The provided credentials are incorrect."] - } -} -``` - -**필요 정보:** -- ✅ 응답에 user 객체 포함 여부? -- ✅ user 객체 구조 (어떤 필드들 포함?) -- ✅ 세션 쿠키 이름 (laravel_session?) - ---- - -### 3. 회원가입 -```http -POST /api/register -Content-Type: application/json - -{ - "name": "John Doe", - "email": "user@example.com", - "password": "password123", - "password_confirmation": "password123" -} -``` - -**성공 응답 (201):** -```json -{ - "user": { - "id": 1, - "name": "John Doe", - "email": "user@example.com", - "created_at": "2024-01-01T00:00:00.000000Z" - }, - "message": "회원가입 성공" -} - -Set-Cookie: laravel_session=xxx; Path=/; HttpOnly; SameSite=Lax -``` - -**Validation 실패 (422):** -```json -{ - "message": "The email has already been taken.", - "errors": { - "email": ["The email has already been taken."], - "password": ["The password must be at least 8 characters."] - } -} -``` - -**필요 정보:** -- ✅ 회원가입 필수 필드? (name, email, password만?) -- ✅ 추가 필드 필요? (phone, company, etc.) -- ✅ 비밀번호 규칙? (최소 8자? 특수문자 필수?) -- ✅ 이메일 인증 필요? (즉시 로그인 vs 이메일 확인 후) - ---- - -### 4. 현재 사용자 정보 -```http -GET /api/user -Cookie: laravel_session=xxx -``` - -**성공 응답 (200):** -```json -{ - "id": 1, - "name": "John Doe", - "email": "user@example.com", - "role": "user", - "permissions": ["read", "write"], - "created_at": "2024-01-01T00:00:00.000000Z" -} -``` - -**인증 실패 (401):** -```json -{ - "message": "Unauthenticated." -} -``` - -**필요 정보:** -- ✅ user 객체 전체 구조 -- ✅ role/permission 시스템 사용 여부? -- ✅ 추가 사용자 정보 (profile, settings 등) - ---- - -### 5. 로그아웃 -```http -POST /api/logout -Cookie: laravel_session=xxx -``` - -**성공 응답 (200):** -```json -{ - "message": "로그아웃 성공" -} - -Set-Cookie: laravel_session=; expires=Thu, 01 Jan 1970 00:00:00 GMT -``` - ---- - -### 6. 비밀번호 재설정 (선택적) -```http -POST /api/forgot-password -Content-Type: application/json - -{ - "email": "user@example.com" -} -``` - -**성공 응답 (200):** -```json -{ - "message": "비밀번호 재설정 링크가 이메일로 전송되었습니다." -} -``` - ---- - -## 🔧 Laravel 설정 확인 사항 - -### 1. Sanctum 설정 (config/sanctum.php) -```php -'stateful' => explode(',', env( - 'SANCTUM_STATEFUL_DOMAINS', - 'localhost,localhost:3000,127.0.0.1,127.0.0.1:3000,::1' -)), -``` - -**확인 필요:** -- ✅ Next.js 개발 서버 도메인 포함? (localhost:3000) -- ✅ 프로덕션 도메인 설정? - ---- - -### 2. CORS 설정 (config/cors.php) -```php -'paths' => ['api/*', 'sanctum/csrf-cookie'], -'supports_credentials' => true, -'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:3000')], -'allowed_methods' => ['*'], -'allowed_headers' => ['*'], -'exposed_headers' => [], -'max_age' => 0, -``` - -**확인 필요:** -- ✅ `supports_credentials` = true? -- ✅ `allowed_origins`에 Next.js URL 포함? - ---- - -### 3. 세션 설정 (config/session.php) -```php -'driver' => env('SESSION_DRIVER', 'file'), -'lifetime' => 120, -'expire_on_close' => false, -'encrypt' => false, -'http_only' => true, -'same_site' => 'lax', -'secure' => env('SESSION_SECURE_COOKIE', false), -'domain' => env('SESSION_DOMAIN'), -``` - -**확인 필요:** -- ✅ `http_only` = true? -- ✅ `same_site` = 'lax'? -- ✅ `domain` 설정 (개발: null, 프로덕션: .yourdomain.com) -- ✅ 세션 쿠키 이름? (기본: laravel_session) - ---- - -### 4. 환경 변수 (.env) -```env -# Frontend URL -FRONTEND_URL=http://localhost:3000 - -# Sanctum -SANCTUM_STATEFUL_DOMAINS=localhost:3000 - -# Session -SESSION_DOMAIN=localhost -SESSION_SECURE_COOKIE=false # 개발: false, 프로덕션: true - -# CORS -``` - -**확인 필요:** -- ✅ FRONTEND_URL 설정? -- ✅ SANCTUM_STATEFUL_DOMAINS 설정? - ---- - -## 📝 API 테스트 시나리오 - -### 테스트 1: CSRF + 로그인 플로우 -```bash -# 1. CSRF 토큰 획득 -curl -X GET http://localhost:8000/sanctum/csrf-cookie \ - -H "Accept: application/json" \ - -c cookies.txt - -# 2. 로그인 -curl -X POST http://localhost:8000/api/login \ - -H "Content-Type: application/json" \ - -H "Accept: application/json" \ - -b cookies.txt \ - -c cookies.txt \ - -d '{"email":"test@test.com","password":"password123"}' - -# 3. 사용자 정보 확인 -curl -X GET http://localhost:8000/api/user \ - -H "Accept: application/json" \ - -b cookies.txt -``` - -### 테스트 2: 회원가입 플로우 -```bash -# 1. CSRF 토큰 -curl -X GET http://localhost:8000/sanctum/csrf-cookie \ - -c cookies.txt - -# 2. 회원가입 -curl -X POST http://localhost:8000/api/register \ - -H "Content-Type: application/json" \ - -b cookies.txt \ - -c cookies.txt \ - -d '{ - "name":"New User", - "email":"new@test.com", - "password":"password123", - "password_confirmation":"password123" - }' -``` - ---- - -## 🎯 프론트엔드에서 필요한 정보 - -### 1. API Base URL -``` -개발: http://localhost:8000 -프로덕션: https://api.yourdomain.com -``` - -### 2. 세션 쿠키 이름 -``` -기본: laravel_session -커스텀: ___? -``` - -### 3. User 객체 구조 -```typescript -interface User { - id: number; - name: string; - email: string; - // 추가 필드? - role?: string; - permissions?: string[]; - avatar?: string; - created_at: string; - updated_at: string; -} -``` - -### 4. 에러 응답 형식 -```typescript -interface ApiError { - message: string; - errors?: Record; // Validation errors -} -``` - -### 5. 회원가입 필수 필드 -```typescript -interface RegisterData { - name: string; - email: string; - password: string; - password_confirmation: string; - // 추가 필드? - phone?: string; - company?: string; -} -``` - ---- - -## ✅ 체크리스트 - -### Laravel 백엔드 준비 사항 - -- [ ] Sanctum 패키지 설치 및 설정 -- [ ] CORS 설정 완료 -- [ ] 세션 설정 확인 (http_only, same_site) -- [ ] API 엔드포인트 구현 - - [ ] GET /sanctum/csrf-cookie - - [ ] POST /api/login - - [ ] POST /api/register - - [ ] GET /api/user - - [ ] POST /api/logout -- [ ] Validation 규칙 정의 -- [ ] 에러 응답 형식 통일 -- [ ] 로컬 테스트 (curl 또는 Postman) - -### Next.js 프론트엔드 대기 항목 - -- [x] 인증 설계 완료 -- [ ] API 구조 확인 후 구현 시작 - - [ ] lib/auth/sanctum.ts - - [ ] lib/auth/auth-config.ts - - [ ] middleware.ts 업데이트 - - [ ] 로그인 페이지 - - [ ] 회원가입 페이지 - - [ ] 인증 테스트 - ---- - -## 📞 다음 단계 - -**백엔드 개발자에게 전달:** -1. 이 문서의 API 엔드포인트 구현 -2. 위의 curl 테스트로 동작 확인 -3. 다음 정보 공유: - - API Base URL - - User 객체 구조 - - 회원가입 필수 필드 - - 세션 쿠키 이름 (변경한 경우) - -**정보 받으면 즉시 시작:** -1. Sanctum 클라이언트 구현 -2. 로그인/회원가입 페이지 -3. Middleware 인증 로직 추가 -4. 통합 테스트 - ---- - -## 🔍 테스트 계획 - -### Phase 1: API 연동 테스트 -1. CSRF 토큰 획득 확인 -2. 로그인 성공/실패 케이스 -3. 회원가입 Validation -4. 세션 쿠키 저장 확인 - -### Phase 2: Middleware 테스트 -1. 비로그인 상태 → /dashboard 접근 → /login 리다이렉트 -2. 로그인 상태 → /dashboard 접근 → 페이지 표시 -3. 로그인 상태 → /login 접근 → /dashboard 리다이렉트 -4. 로그아웃 → 쿠키 삭제 확인 - -### Phase 3: 통합 테스트 -1. 회원가입 → 자동 로그인 → 대시보드 -2. 로그인 → 페이지 새로고침 → 세션 유지 -3. 로그아웃 → 보호된 페이지 접근 → 차단 - ---- - -**API 준비되면 바로 알려주세요! 🚀** - ---- - -## 관련 파일 - -### 프론트엔드 -- `src/lib/api/client.ts` - 통합 HTTP 클라이언트 -- `src/lib/api/auth/sanctum-client.ts` - Sanctum 클라이언트 -- `src/lib/api/auth/auth-config.ts` - 인증 설정 (라우트, URL) -- `src/middleware.ts` - 인증 미들웨어 -- `src/app/[locale]/(auth)/login/page.tsx` - 로그인 페이지 -- `src/app/[locale]/(auth)/signup/page.tsx` - 회원가입 페이지 - -### 설정 파일 -- `.env.local` - 환경 변수 (API URL, API Key) -- `next.config.ts` - Next.js 설정 \ No newline at end of file diff --git a/claudedocs/approval/[IMPL-2025-12-17] approval-document-checklist.md b/claudedocs/approval/[IMPL-2025-12-17] approval-document-checklist.md deleted file mode 100644 index 4fa1bcf2..00000000 --- a/claudedocs/approval/[IMPL-2025-12-17] approval-document-checklist.md +++ /dev/null @@ -1,134 +0,0 @@ -# 전자결재 문서 작성/상세 기능 구현 체크리스트 - -## 개요 -- **작업일**: 2025-12-17 -- **목표**: 전자결재 기안함 문서 작성 및 상세 모달 구현 - ---- - -## 1. 기안함 목록 페이지 (완료) - -- [x] 기안함 컴포넌트 구현 (`src/components/approval/DraftBox/`) -- [x] 타입 정의 (`types.ts`) -- [x] 메인 컴포넌트 (`index.tsx`) -- [x] 라우트 페이지 (`src/app/[locale]/(protected)/approval/draft/page.tsx`) -- [x] 통계 카드 수정 (진행, 완료, 반려, 임시 저장) -- [x] 체크박스 선택 시에만 작업 버튼 표시 -- [x] 헤더 버튼 순서 조정 (상신/삭제 → 문서 작성) - -**접속 URL**: `http://localhost:3000/ko/approval/draft` - ---- - -## 2. 문서 작성 페이지 (완료) - -### 2.1 공통 컴포넌트 -- [x] 타입 정의 (`src/components/approval/DocumentCreate/types.ts`) -- [x] 기본 정보 섹션 (`BasicInfoSection.tsx`) -- [x] 결재선 섹션 (`ApprovalLineSection.tsx`) -- [x] 참조 섹션 (`ReferenceSection.tsx`) - -### 2.2 문서 유형별 폼 -- [x] 품의서 폼 (`ProposalForm.tsx`) -- [x] 지출결의서 폼 (`ExpenseReportForm.tsx`) -- [x] 지출 예상 내역서 폼 (`ExpenseEstimateForm.tsx`) - - [x] Fragment key 에러 수정 - -### 2.3 메인 컴포넌트 및 라우트 -- [x] 메인 컴포넌트 (`index.tsx`) -- [x] 라우트 페이지 (`src/app/[locale]/(protected)/approval/draft/new/page.tsx`) -- [x] 기안함에서 문서 작성 버튼 클릭 시 페이지 이동 연결 - -**접속 URL**: `http://localhost:3000/ko/approval/draft/new` - ---- - -## 3. 문서 상세 모달 (완료) - -### 3.1 디자인 참고 -- [x] sam-design 프로젝트 `QuoteDetailView.tsx` 산출내역서 모달 구조 분석 - -### 3.2 공통 컴포넌트 (`src/components/approval/DocumentDetail/`) -- [x] 타입 정의 (`types.ts`) -- [x] 결재선 박스 (`ApprovalLineBox.tsx`) - -### 3.3 문서 유형별 컴포넌트 -- [x] 품의서 문서 (`ProposalDocument.tsx`) -- [x] 지출결의서 문서 (`ExpenseReportDocument.tsx`) -- [x] 지출 예상 내역서 문서 (`ExpenseEstimateDocument.tsx`) - -### 3.4 메인 모달 컴포넌트 -- [x] 메인 모달 (`index.tsx` - DocumentDetailModal) - - [x] 상단 버튼: 복제, 수정, 반려, 승인, 인쇄, 공유, 닫기 - - [x] 공유 드롭다운: PDF, 이메일, 팩스, 카카오톡 - - [x] 스크롤 가능한 문서 영역 (A4 형식) - -### 3.5 기안함 연결 -- [x] 기안함 목록에서 문서 클릭 시 조건부 처리 - - 임시저장 상태 → 문서 작성 페이지 (수정 모드) - - 그 외 상태 → 문서 상세 모달 -- [x] 문서 작성 화면에서 상세 버튼 클릭 시 미리보기 모달 - ---- - -## 4. 추가 작업 (완료) - -- [x] 빌드 테스트 (2025-12-17 완료) - - ✓ Compiled successfully in 7.0s - - ✓ Generating static pages (108/108) -- [ ] 근태관리 작업 버튼 수정 확인 (별도 작업) -- [ ] 문서 URL 목록 업데이트 (`claudedocs/[REF] all-pages-test-urls.md`) (별도 작업) - ---- - -## 파일 구조 - -``` -src/components/approval/ -├── DraftBox/ -│ ├── types.ts -│ └── index.tsx -├── DocumentCreate/ -│ ├── types.ts -│ ├── BasicInfoSection.tsx -│ ├── ApprovalLineSection.tsx -│ ├── ReferenceSection.tsx -│ ├── ProposalForm.tsx -│ ├── ExpenseReportForm.tsx -│ ├── ExpenseEstimateForm.tsx -│ └── index.tsx -└── DocumentDetail/ - ├── types.ts - ├── ApprovalLineBox.tsx - ├── ProposalDocument.tsx - ├── ExpenseReportDocument.tsx - ├── ExpenseEstimateDocument.tsx ✅ - └── index.tsx ✅ - -src/app/[locale]/(protected)/approval/ -├── draft/ -│ ├── page.tsx -│ └── new/ -│ └── page.tsx -``` - ---- - -## 참고 사항 - -### 문서 유형 -1. **품의서** (`proposal`) - - 구매처 정보, 제목, 품의 내역, 품의 사유, 예상 비용, 첨부파일 - -2. **지출결의서** (`expenseReport`) - - 지출 요청일/결제일, 내역 테이블, 법인카드, 총 비용, 첨부파일 - -3. **지출 예상 내역서** (`expenseEstimate`) - - 월별 테이블, 소계, 지출 합계, 계좌 잔액, 최종 차액 - -### 모달 디자인 구조 (sam-design 참고) -- Dialog: `max-w-[95vw] md:max-w-[800px] lg:max-w-[900px] h-[95vh]` -- 헤더: 고정 (`flex-shrink-0`) -- 버튼 영역: 고정 (`flex-shrink-0 bg-muted/30`) -- 문서 영역: 스크롤 (`flex-1 overflow-y-auto bg-gray-100`) -- A4 크기: `max-w-[210mm] mx-auto bg-white shadow-lg rounded-lg p-8` \ No newline at end of file diff --git a/claudedocs/approval/[IMPL-2026-03-07] approval-box-linked-documents.md b/claudedocs/approval/[IMPL-2026-03-07] approval-box-linked-documents.md deleted file mode 100644 index fd183b32..00000000 --- a/claudedocs/approval/[IMPL-2026-03-07] approval-box-linked-documents.md +++ /dev/null @@ -1,59 +0,0 @@ -# 전자결재 결재함 확장 및 연결문서 기능 - -> **작업일**: 2026-03-01 ~ 03-07 -> **상태**: ✅ 완료 -> **커밋**: 181352d7, 72cf5d86 - ---- - -## 개요 - -결재함(ApprovalBox) API 연동, 연결문서(LinkedDocumentContent) 렌더링, -모바일 반응형 레이아웃 개선. - ---- - -## 1. 결재함 API 연동 - -- [x] 결재함 목록: `GET /api/v1/approvals/inbox` -- [x] 결재함 통계: `GET /api/v1/approvals/inbox/summary` -- [x] 승인 처리: `POST /api/v1/approvals/{id}/approve` -- [x] 반려 처리: `POST /api/v1/approvals/{id}/reject` -- [x] 문서 상태 매핑: DRAFT → PENDING → APPROVED / REJECTED / CANCELLED -- [x] 결재함 상태 헬퍼 함수 추가 - -### 주요 파일 -- `src/components/approval/ApprovalBox/actions.ts` (+123/-7) -- `src/components/approval/ApprovalBox/index.tsx` (+47/-1) -- `src/components/approval/ApprovalBox/types.ts` (+9/-1) - ---- - -## 2. 연결문서 기능 (LinkedDocumentContent) — 신규 - -검사성적서, 작업일지 등 문서관리 시스템의 문서를 결재 문서에 연결하여 렌더링. - -- [x] `LinkedDocumentContent` 컴포넌트 신규 생성 -- [x] `DocumentHeader` 컴포넌트 활용 (일관된 스타일) -- [x] 결재라인 / 상태배지 / 문서 메타정보 표시 -- [x] `DocumentDetailModalV2`에 연결문서 렌더링 통합 - -### 주요 파일 -- `src/components/approval/DocumentDetail/LinkedDocumentContent.tsx` (신규, +133) -- `src/components/approval/DocumentDetail/DocumentDetailModalV2.tsx` -- `src/components/approval/DocumentDetail/types.ts` (+27/-1) - ---- - -## 3. 모바일 반응형 개선 - -- [x] `AuthenticatedLayout`: 사이드바/메인 콘텐츠 모바일 대응 -- [x] `HeaderFavoritesBar`: 전면 재설계 (+315/-127) -- [x] `Sidebar`: 반응형 숨김/표시 -- [x] `SearchableSelectionModal`: HTML 유효성 에러 수정 - -### 주요 파일 -- `src/layouts/AuthenticatedLayout.tsx` (+12/-1) -- `src/components/layout/HeaderFavoritesBar.tsx` (+315/-127) -- `src/components/layout/Sidebar.tsx` (+8/-1) -- `src/components/organisms/SearchableSelectionModal.tsx` (+79/-2) diff --git a/claudedocs/architecture/.DS_Store b/claudedocs/architecture/.DS_Store deleted file mode 100644 index 5008ddfc..00000000 Binary files a/claudedocs/architecture/.DS_Store and /dev/null differ diff --git a/claudedocs/architecture/[ANALYSIS-2026-01-20] 공통화-현황-분석.md b/claudedocs/architecture/[ANALYSIS-2026-01-20] 공통화-현황-분석.md deleted file mode 100644 index 39a40728..00000000 --- a/claudedocs/architecture/[ANALYSIS-2026-01-20] 공통화-현황-분석.md +++ /dev/null @@ -1,213 +0,0 @@ -# 프로젝트 공통화 현황 분석 - -## 1. 핵심 지표 요약 - -| 구분 | 적용 현황 | 비고 | -|------|----------|------| -| **IntegratedDetailTemplate** | 96개 파일 (228회 사용) | 상세/수정/등록 페이지 통합 | -| **IntegratedListTemplateV2** | 50개 파일 (60회 사용) | 목록 페이지 통합 | -| **DetailConfig 파일** | 39개 생성 | 설정 기반 페이지 구성 | -| **레거시 패턴 (PageLayout 직접 사용)** | ~40-50개 파일 | 마이그레이션 대상 | - ---- - -## 2. 공통화 달성률 - -### 2.1 상세 페이지 (Detail) -``` -총 Detail 컴포넌트: ~105개 -IntegratedDetailTemplate 적용: ~65개 -적용률: 약 62% -``` - -### 2.2 목록 페이지 (List) -``` -총 List 컴포넌트: ~61개 -IntegratedListTemplateV2 적용: ~50개 -적용률: 약 82% -``` - -### 2.3 폼 컴포넌트 (Form) -``` -총 Form 컴포넌트: ~72개 -공통 템플릿 미적용 (개별 구현) -적용률: 0% -``` - ---- - -## 3. 잘 공통화된 영역 ✅ - -### 3.1 템플릿 시스템 -| 템플릿 | 용도 | 적용 현황 | -|--------|------|----------| -| IntegratedDetailTemplate | 상세/수정/등록 | 96개 파일 | -| IntegratedListTemplateV2 | 목록 페이지 | 50개 파일 | -| UniversalListPage | 범용 목록 | 7개 파일 | - -### 3.2 UI 컴포넌트 (Radix UI 기반) -- **AlertDialog**: 65개 파일에서 일관되게 사용 -- **Dialog**: 142개 파일에서 사용 -- **Toast (Sonner)**: 133개 파일에서 일관되게 사용 -- **Pagination**: 54개 파일에서 통합 사용 - -### 3.3 데이터 테이블 -- **DataTable**: 공통 컴포넌트로 추상화됨 -- **IntegratedListTemplateV2에 통합**: 자동 페이지네이션, 필터링 - ---- - -## 4. 추가 공통화 기회 🔧 - -### 4.1 우선순위 높음 (High Priority) - -#### 📋 Form 템플릿 (IntegratedFormTemplate) -**현황**: 72개 Form 컴포넌트가 개별적으로 구현됨 -**제안**: -```typescript -// 제안: IntegratedFormTemplate - } -/> -``` -**효과**: -- 폼 레이아웃 일관성 -- 버튼 영역 통합 (저장/취소/삭제) -- 유효성 검사 패턴 통합 - -#### 📝 레거시 페이지 마이그레이션 -**현황**: ~40-50개 파일이 PageLayout/PageHeader 직접 사용 -**대상 파일** (샘플): -- `SubscriptionClient.tsx` -- `SubscriptionManagement.tsx` -- `ComprehensiveAnalysis/index.tsx` -- `DailyReport/index.tsx` -- `ReceivablesStatus/index.tsx` -- `FAQManagement/FAQList.tsx` -- `DepartmentManagement/index.tsx` -- 등등 - ---- - -### 4.2 우선순위 중간 (Medium Priority) - -#### 🗑️ 삭제 확인 다이얼로그 통합 -**현황**: 각 컴포넌트에서 AlertDialog 반복 구현 -**제안**: -```typescript -// 제안: useDeleteConfirm hook -const { openDeleteConfirm, DeleteConfirmDialog } = useDeleteConfirm({ - title: '삭제 확인', - description: '정말 삭제하시겠습니까?', - onConfirm: handleDelete, -}); - -// 또는 공통 컴포넌트 - setIsOpen(false)} -/> -``` - -#### 📁 파일 업로드/다운로드 패턴 통합 -**현황**: 여러 컴포넌트에서 파일 처리 로직 중복 -**제안**: -```typescript -// 제안: useFileUpload hook -const { uploadFile, downloadFile, FileDropzone } = useFileUpload({ - accept: ['image/*', '.pdf'], - maxSize: 10 * 1024 * 1024, -}); -``` - -#### 🔄 로딩 상태 표시 통합 -**현황**: 43개 파일에서 다양한 로딩 패턴 사용 -**제안**: -- `LoadingOverlay` 컴포넌트 확대 적용 -- `Skeleton` 패턴 표준화 - ---- - -### 4.3 우선순위 낮음 (Low Priority) - -#### 📊 대시보드 카드 컴포넌트 -**현황**: CEO 대시보드, 생산 대시보드 등에서 유사 패턴 -**제안**: `DashboardCard`, `StatCard` 공통 컴포넌트 - -#### 🔍 검색/필터 패턴 -**현황**: IntegratedListTemplateV2에 이미 통합됨 -**추가**: 독립 검색 컴포넌트 표준화 - ---- - -## 5. 레거시 파일 정리 대상 - -### 5.1 _legacy 폴더 (삭제 검토) -``` -src/components/hr/CardManagement/_legacy/ - - CardDetail.tsx - - CardForm.tsx - -src/components/settings/AccountManagement/_legacy/ - - AccountDetail.tsx -``` - -### 5.2 V1/V2 중복 파일 (통합 검토) -- `LaborDetailClient.tsx` vs `LaborDetailClientV2.tsx` -- `PricingDetailClient.tsx` vs `PricingDetailClientV2.tsx` -- `DepositDetail.tsx` vs `DepositDetailClientV2.tsx` -- `WithdrawalDetail.tsx` vs `WithdrawalDetailClientV2.tsx` - ---- - -## 6. 권장 액션 플랜 - -### Phase 7: 레거시 페이지 마이그레이션 -| 순서 | 대상 | 예상 작업량 | -|------|------|------------| -| 1 | 설정 관리 페이지 (8개) | 중간 | -| 2 | 회계 관리 페이지 (5개) | 중간 | -| 3 | 인사 관리 페이지 (5개) | 중간 | -| 4 | 보고서/분석 페이지 (3개) | 낮음 | - -### Phase 8: Form 템플릿 개발 -1. IntegratedFormTemplate 설계 -2. 파일럿 적용 (2-3개 Form) -3. 점진적 마이그레이션 - -### Phase 9: 유틸리티 Hook 개발 -1. useDeleteConfirm -2. useFileUpload -3. useFormState (공통 폼 상태 관리) - -### Phase 10: 레거시 정리 -1. _legacy 폴더 삭제 -2. V1/V2 중복 파일 통합 -3. 미사용 컴포넌트 정리 - ---- - -## 7. 결론 - -### 공통화 성과 -- **상세 페이지**: 62% 공통화 달성 (Phase 6 완료) -- **목록 페이지**: 82% 공통화 달성 -- **UI 컴포넌트**: Radix UI 기반 일관성 확보 -- **토스트/알림**: Sonner로 완전 통합 - -### 남은 과제 -- **Form 템플릿**: 72개 폼 컴포넌트 공통화 필요 -- **레거시 페이지**: ~40-50개 마이그레이션 필요 -- **코드 정리**: _legacy, V1/V2 중복 파일 정리 - -### 예상 효과 (추가 공통화 시) -- 코드 중복 30% 추가 감소 -- 신규 페이지 개발 시간 50% 단축 -- 유지보수성 대폭 향상 diff --git a/claudedocs/architecture/[ANALYSIS-2026-02-05] SAM-ERP-MES-정체성-분석.md b/claudedocs/architecture/[ANALYSIS-2026-02-05] SAM-ERP-MES-정체성-분석.md deleted file mode 100644 index 840d9e02..00000000 --- a/claudedocs/architecture/[ANALYSIS-2026-02-05] SAM-ERP-MES-정체성-분석.md +++ /dev/null @@ -1,256 +0,0 @@ -# SAM 프로젝트 정체성 및 현장 효용성 분석 - -> 작성일: 2026-02-05 -> 목적: ERP/MES 관점에서 SAM 시스템의 포지션, 강점/약점, 현장 효용성 분석 - ---- - -## 1. SAM의 정체성: "제조+설치 통합형 ERP/MES" - -### 포지셔닝 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ SAM 시스템 포지션 │ -├─────────────────────────────────────────────────────────────┤ -│ │ -│ Pure MES ◄────────── SAM ──────────► Pure ERP │ -│ (공장 실행) │ (경영 관리) │ -│ │ │ -│ ┌────────┴────────┐ │ -│ │ 70% ERP │ │ -│ │ 30% MES │ │ -│ │ + 건설 프로젝트 │ │ -│ └─────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────┘ -``` - -SAM은 **순수 MES도 아니고 순수 ERP도 아닌**, 제조업체가 실제로 필요로 하는 기능들을 통합한 시스템이다. - -### 타겟 산업: 블라인드/셔터 제조 + 설치 - -| 특징 | SAM의 대응 | -|------|-----------| -| 주문생산(Make-to-Order) | 수주 → 생산지시 → 작업실적 흐름 | -| 다품종 소량생산 | 동적 품목 마스터 (빌더 시스템) | -| 설치 서비스 병행 | 건설/시공 프로젝트 모듈 | -| 품질 인증 필요 | QMS 검사성적서 시스템 | -| 중소기업 규모 | SaaS 멀티테넌트 구조 | - ---- - -## 2. ERP 관점 분석 - -### 커버리지 - -| ERP 영역 | SAM 구현 수준 | 비고 | -|----------|-------------|------| -| **재무회계** | ⭐⭐⭐⭐ (80%) | 매입/매출/입출금/어음/카드/채권 | -| **영업관리** | ⭐⭐⭐⭐ (85%) | 견적→수주→생산지시 연동 | -| **구매관리** | ⭐⭐⭐ (70%) | 입고 중심, 발주 모듈 약함 | -| **재고관리** | ⭐⭐⭐ (65%) | 재고현황 중심, 창고이동 미흡 | -| **인사관리** | ⭐⭐⭐⭐ (80%) | 근태/급여/휴가/문서 | -| **전자결재** | ⭐⭐⭐ (70%) | 기안/결재/참조 기본 구조 | -| **프로젝트** | ⭐⭐⭐⭐⭐ (90%) | 건설 모듈이 매우 정교함 | - -### ERP로서의 강점 - -1. **영업-생산 연동**: 수주가 바로 생산지시로 연결되는 구조 -2. **프로젝트 관리**: 입찰→계약→시공→정산까지 풀 사이클 -3. **회계 통합**: 매출/매입이 거래처원장과 연동 -4. **멀티테넌트**: 신규 고객사 온보딩이 빠름 - -### ERP로서의 약점 - -1. **구매/발주**: 입고 위주, 구매요청→발주→입고 흐름 미흡 -2. **원가계산**: 제조원가 계산 로직이 명시적이지 않음 -3. **창고관리**: 다창고, 로케이션 관리 부재 -4. **BI/분석**: 대시보드는 있으나 심층 분석 약함 - ---- - -## 3. MES 관점 분석 - -### 커버리지 - -| MES 영역 | SAM 구현 수준 | 비고 | -|----------|-------------|------| -| **작업지시** | ⭐⭐⭐⭐ (80%) | 생산지시 생성/관리 | -| **작업실적** | ⭐⭐⭐⭐ (80%) | 실적 입력/조회 | -| **품질관리** | ⭐⭐⭐⭐⭐ (90%) | 다양한 검사성적서 | -| **설비관리** | ⭐ (20%) | 거의 없음 | -| **실시간 모니터링** | ⭐⭐⭐ (60%) | 대시보드 있음, PLC 연동 없음 | -| **작업자 화면** | ⭐⭐⭐⭐ (75%) | 현장 터치 인터페이스 | -| **추적성(Traceability)** | ⭐⭐⭐ (65%) | 로트 추적 기본 구조 | - -### MES로서의 강점 - -1. **품질 시스템**: QMS가 상당히 정교함 (6종 검사성적서) -2. **작업자 친화적**: 현장용 작업자 화면 별도 존재 -3. **생산-영업 연결**: 수주 기반 생산이라 주문 추적 용이 - -### MES로서의 약점 - -1. **설비 연동 없음**: PLC, 바코드 스캐너 등 현장 장비 연동 부재 -2. **실시간성 부족**: 폴링 기반, 실시간 푸시 아님 -3. **공정 스케줄링**: 단순 작업지시, APS(고급계획) 없음 -4. **설비 모니터링**: OEE, 설비 가동률 등 없음 - ---- - -## 4. 현장 효용성 평가 - -### 실제로 잘 맞는 업종 - -``` -✅ 주문생산 제조업 (블라인드, 가구, 인테리어 자재) -✅ 설치 서비스 병행 업체 -✅ 다품종 소량생산 -✅ 품질 인증 필요 업종 (ISO, KS 등) -✅ 직원 50명 이하 중소기업 -✅ IT 인력이 부족한 회사 (SaaS로 운영부담 최소화) -``` - -### 맞지 않는 업종 - -``` -❌ 대량생산 (자동차, 반도체) - MES 깊이 부족 -❌ 연속공정 (화학, 식품) - 배치/레시피 관리 없음 -❌ 설비 집약 산업 - 설비 연동/모니터링 없음 -❌ 복잡한 원가계산 필요 업종 - 원가 모듈 약함 -❌ 대기업 (100명+) - 워크플로우 복잡도 한계 -``` - -### 현장에서의 실제 가치 - -| 관점 | 효용 | -|------|------| -| **경영진** | 수주~매출까지 한눈에, 프로젝트별 손익 파악 | -| **영업팀** | 견적→수주→생산현황 실시간 확인 | -| **생산팀** | 작업지시 받고 실적 입력, 품질 기록 | -| **품질팀** | 검사성적서 발행, 인증심사 대응 | -| **경리팀** | 매입/매출/입출금 통합 관리 | -| **현장 작업자** | 터치 화면으로 작업 확인/실적 입력 | - ---- - -## 5. 경쟁 포지션 - -### vs 범용 ERP (더존, 영림원) - -| 항목 | SAM | 범용 ERP | -|------|-----|---------| -| 제조 특화 | ⭐⭐⭐⭐ | ⭐⭐ | -| 건설/시공 | ⭐⭐⭐⭐⭐ | ⭐ | -| 회계 깊이 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | -| 커스터마이징 | ⭐⭐⭐⭐ (빌더) | ⭐⭐ | -| 도입 비용 | 낮음 (SaaS) | 높음 | - -### vs 전문 MES (포스코ICT, 미라콤) - -| 항목 | SAM | 전문 MES | -|------|-----|---------| -| 설비 연동 | ❌ | ⭐⭐⭐⭐⭐ | -| 실시간성 | ⭐⭐ | ⭐⭐⭐⭐⭐ | -| 품질 관리 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | -| ERP 통합 | ⭐⭐⭐⭐⭐ | ⭐⭐ (별도 연동) | -| 도입 기간 | 짧음 | 길음 | - -### SAM의 틈새 (Niche) - -``` -"전문 MES는 과하고, 범용 ERP는 제조 기능이 부족한" -중소 제조+설치 업체를 위한 통합 솔루션 -``` - ---- - -## 6. 빌더 확장 시 기대효과 - -현재 품목기준관리 빌더를 다른 영역으로 확장하면: - -| 확장 영역 | 예상 효과 | -|----------|----------| -| **폼 빌더** (등록/수정) | 신규 업종 대응 시 개발 50% 절감 | -| **리스트 빌더** (조회) | 화면 추가/변경 무코딩 가능 | -| **문서 빌더** (성적서) | 업종별 양식 빠른 대응 | -| **워크플로우 빌더** | 결재/승인 프로세스 설정화 | - -**신규 업체 온보딩 시나리오**: -``` -현재: 요구분석 → 개발 → 테스트 → 배포 (4-8주) -목표: 요구분석 → 빌더 설정 → 배포 (1-2주) -``` - ---- - -## 7. 종합 평가 - -### SAM의 정체성 한 문장 - -> **"주문생산 중소 제조업을 위한 ERP+MES 통합 SaaS로, 생산-품질-영업-회계를 하나로 연결하고, 설치 프로젝트까지 관리하는 올인원 솔루션"** - -### 핵심 차별점 - -1. **제조+설치 통합** - 대부분의 시스템이 둘 중 하나만 함 -2. **품질 시스템 내장** - QMS가 기본 탑재 -3. **빌더 기반 확장성** - 업종별 커스터마이징 용이 -4. **SaaS 멀티테넌트** - 도입 부담 최소화 - -### 발전 방향 제안 - -| 단기 | 중기 | 장기 | -|------|------|------| -| 빌더 → 리스트까지 확장 | 바코드/QR 스캐닝 | 설비 연동 (IoT) | -| 발주 모듈 보강 | 모바일 앱 강화 | AI 수요 예측 | -| 원가계산 기본 기능 | 실시간 알림 (WebSocket) | APS 스케줄링 | - ---- - -## 8. 시스템 규모 현황 - -### 프로젝트 스케일 - -- **24개** 주요 기능 모듈 -- **250+** 페이지 -- **900+** 컴포넌트 파일 -- 멀티테넌트 아키텍처 -- 다국어 지원 (한국어, 영어, 일본어) - -### 모듈별 복잡도 - -| 모듈 | 복잡도 | 페이지 수 | 핵심 기능 | -|------|--------|----------|----------| -| Construction | ⭐⭐⭐⭐⭐ | 57 | 프로젝트 풀 라이프사이클 | -| Accounting | ⭐⭐⭐⭐ | 31 | 재무 관리 전체 | -| Production | ⭐⭐⭐⭐ | 12 | 실시간 MES 코어 | -| Quality | ⭐⭐⭐⭐ | 24 | 다중 검사 QMS | -| Master Data | ⭐⭐⭐⭐⭐ | 12 | 동적 폼 템플릿 | -| Sales | ⭐⭐⭐ | 20 | 견적→수주 흐름 | -| HR | ⭐⭐⭐ | 17 | 직원 라이프사이클 | -| Material | ⭐⭐ | 6 | 재고 & 입고 | -| Outbound | ⭐⭐ | 7 | 출고 & 배차 | - ---- - -## 부록: 기술 스택 - -**Frontend:** -- Next.js 15 (App Router) -- React 18 -- TypeScript -- Tailwind CSS -- Radix UI -- Zustand - -**Backend:** -- PHP Laravel API (별도 코드베이스) -- MySQL/MariaDB -- JWT 인증 -- 멀티테넌트 아키텍처 - -**인프라:** -- HttpOnly 쿠키 보안 -- 멀티테넌트 데이터 격리 -- RESTful API 설계 diff --git a/claudedocs/architecture/[ANALYSIS-2026-02-05] list-page-commonization-status.md b/claudedocs/architecture/[ANALYSIS-2026-02-05] list-page-commonization-status.md deleted file mode 100644 index a94830a4..00000000 --- a/claudedocs/architecture/[ANALYSIS-2026-02-05] list-page-commonization-status.md +++ /dev/null @@ -1,505 +0,0 @@ -# 리스트 페이지 공통화 현황 분석 - -> 작성일: 2026-02-05 -> 목적: 리스트 페이지 반복 패턴 식별 및 공통화 가능성 분석 - ---- - -## 📊 전체 현황 - -| 구분 | 수량 | -|------|------| -| 총 리스트 페이지 | 37개 | -| UniversalListPage 사용 | 15개+ | -| IntegratedListTemplateV2 직접 사용 | 5개+ | -| 레거시 패턴 | 10개+ | - ---- - -## 🏗️ 템플릿 계층 구조 - -``` -UniversalListPage (최상위 - config 기반) - └── IntegratedListTemplateV2 (하위 - props 기반) - └── 공통 UI 컴포넌트 - ├── PageLayout - ├── PageHeader - ├── StatCards - ├── DateRangeSelector - ├── MobileFilter - ├── ListMobileCard - └── Table, Pagination 등 -``` - ---- - -## 📁 리스트 페이지 목록 및 사용 템플릿 - -### UniversalListPage 사용 (최신 패턴) - -| 파일 | 도메인 | 특징 | -|------|--------|------| -| `items/ItemListClient.tsx` | 품목관리 | 외부 훅(useItemList) 사용, 엑셀 업로드/다운로드 | -| `pricing/PricingListClient.tsx` | 가격관리 | 외부 훅 사용 | -| `production/WorkOrders/WorkOrderList.tsx` | 생산 | 공정 기반 탭, 외부 통계 API | -| `outbound/ShipmentManagement/ShipmentList.tsx` | 출고 | 캘린더 통합, 날짜범위 필터 | -| `outbound/VehicleDispatchManagement/VehicleDispatchList.tsx` | 배차 | - | -| `material/ReceivingManagement/ReceivingList.tsx` | 입고 | - | -| `material/StockStatus/StockStatusList.tsx` | 재고 | - | -| `customer-center/NoticeManagement/NoticeList.tsx` | 공지사항 | 클라이언트 사이드 필터링 | -| `customer-center/EventManagement/EventList.tsx` | 이벤트 | 클라이언트 사이드 필터링 | -| `customer-center/InquiryManagement/InquiryList.tsx` | 문의 | - | -| `customer-center/FAQManagement/FAQList.tsx` | FAQ | - | -| `quality/InspectionManagement/InspectionList.tsx` | 품질검사 | - | -| `process-management/ProcessListClient.tsx` | 공정관리 | - | -| `pricing-table-management/PricingTableListClient.tsx` | 단가표 | - | -| `pricing-distribution/PriceDistributionList.tsx` | 가격배포 | - | - -### 건설 도메인 (UniversalListPage 사용) - -| 파일 | 기능 | -|------|------| -| `construction/management/ProjectListClient.tsx` | 프로젝트 목록 | -| `construction/management/ConstructionManagementListClient.tsx` | 공사관리 목록 | -| `construction/contract/ContractListClient.tsx` | 계약 목록 | -| `construction/estimates/EstimateListClient.tsx` | 견적 목록 | -| `construction/bidding/BiddingListClient.tsx` | 입찰 목록 | -| `construction/pricing-management/PricingListClient.tsx` | 단가관리 목록 | -| `construction/partners/PartnerListClient.tsx` | 협력사 목록 | -| `construction/order-management/OrderManagementListClient.tsx` | 발주관리 목록 | -| `construction/site-management/SiteManagementListClient.tsx` | 현장관리 목록 | -| `construction/site-briefings/SiteBriefingListClient.tsx` | 현장브리핑 목록 | -| `construction/handover-report/HandoverReportListClient.tsx` | 인수인계 목록 | -| `construction/issue-management/IssueManagementListClient.tsx` | 이슈관리 목록 | -| `construction/structure-review/StructureReviewListClient.tsx` | 구조검토 목록 | -| `construction/utility-management/UtilityManagementListClient.tsx` | 유틸리티 목록 | -| `construction/worker-status/WorkerStatusListClient.tsx` | 작업자 현황 | -| `construction/progress-billing/ProgressBillingManagementListClient.tsx` | 기성관리 목록 | - -### 기타/레거시 - -| 파일 | 비고 | -|------|------| -| `settings/PopupManagement/PopupList.tsx` | 팝업관리 | -| `production/WorkResults/WorkResultList.tsx` | 작업실적 | -| `quality/PerformanceReportManagement/PerformanceReportList.tsx` | 성과보고서 | -| `board/BoardList/BoardListUnified.tsx` | 통합 게시판 | - ---- - -## 🔄 반복 패턴 분석 - -### 1. Badge 색상 매핑 (매우 반복적) - -각 페이지마다 개별 정의되어 있는 패턴: - -```typescript -// ItemListClient.tsx -const badges: Record = { - FG: { variant: 'default', className: 'bg-purple-100 text-purple-700 border-purple-200' }, - PT: { variant: 'default', className: 'bg-orange-100 text-orange-700 border-orange-200' }, - // ... -}; - -// WorkOrderList.tsx -const PRIORITY_COLORS: Record = { - '긴급': 'bg-red-100 text-red-700', - '우선': 'bg-orange-100 text-orange-700', - '일반': 'bg-gray-100 text-gray-700', -}; - -// ShipmentList.tsx - types.ts에서 import -export const SHIPMENT_STATUS_STYLES: Record = { ... }; -``` - -**현황**: -- `src/lib/utils/status-config.ts`에 `createStatusConfig` 유틸 존재 -- 일부 페이지만 사용 중 (대부분 개별 정의) - -### 2. 상태 라벨 정의 (반복적) - -```typescript -// WorkOrderList.tsx - types.ts에서 import -export const WORK_ORDER_STATUS_LABELS: Record = { - pending: '대기', - in_progress: '진행중', - completed: '완료', -}; - -// ShipmentList.tsx - types.ts에서 import -export const SHIPMENT_STATUS_LABELS: Record = { ... }; -``` - -**현황**: 각 도메인 types.ts에서 개별 정의 - -### 3. 필터 설정 (filterConfig) - -```typescript -// WorkOrderList.tsx -const filterConfig: FilterFieldConfig[] = [ - { - key: 'status', - label: '상태', - type: 'single', - options: [ - { value: 'waiting', label: '작업대기' }, - { value: 'in_progress', label: '진행중' }, - { value: 'completed', label: '작업완료' }, - ], - }, - { - key: 'priority', - label: '우선순위', - type: 'single', - options: [ - { value: 'urgent', label: '긴급' }, - { value: 'priority', label: '우선' }, - { value: 'normal', label: '일반' }, - ], - }, -]; -``` - -**공통 필터 패턴**: -- 상태 필터 (대기/진행/완료) -- 우선순위 필터 (긴급/우선/일반) -- 유형 필터 (전체/유형1/유형2...) - -### 4. 행 클릭 핸들러 패턴 - -```typescript -// 모든 페이지에서 동일한 패턴 -const handleRowClick = useCallback( - (item: SomeType) => { - router.push(`/ko/${basePath}/${item.id}?mode=view`); - }, - [router] -); -``` - -### 5. 테이블 행 렌더링 (renderTableRow) - -```typescript -// 공통 구조 - handleRowClick(item)}> - e.stopPropagation()}> - - - {globalIndex} - {/* 데이터 컬럼들 */} - - - {getStatusLabel(item.status)} - - - -``` - ---- - -## ✅ 이미 공통화된 것 - -| 유틸/컴포넌트 | 위치 | 사용률 | -|--------------|------|--------| -| `UniversalListPage` | templates/ | 높음 (15개+) | -| `IntegratedListTemplateV2` | templates/ | 높음 | -| `ListMobileCard`, `InfoField` | organisms/ | 높음 | -| `MobileFilter` | molecules/ | 높음 | -| `DateRangeSelector` | molecules/ | 높음 | -| `StatCards` | organisms/ | 높음 | -| `createStatusConfig` | lib/utils/ | **낮음** (일부만 사용) | - ---- - -## ❌ 공통화 필요한 것 - -### 높은 우선순위 (ROI 높음) - -| 패턴 | 현황 | 공통화 방안 | -|------|------|-------------| -| **Badge 색상 매핑** | 각 페이지 개별 정의 | `src/lib/utils/badge-styles.ts` 생성 | -| **공통 필터 프리셋** | 각 페이지 개별 정의 | `src/lib/constants/filter-presets.ts` 생성 | -| **우선순위 색상** | 각 페이지 개별 정의 | 공통 상수로 추출 | - -### 중간 우선순위 - -| 패턴 | 현황 | 공통화 방안 | -|------|------|-------------| -| 상태 라벨 | 도메인별 types.ts | 도메인별 유지 (비즈니스 로직) | -| 행 클릭 핸들러 | 각 페이지 개별 | UniversalListPage에서 처리 중 | - ---- - -## 📋 공통화 대상 상세 - -### 1. Badge 스타일 공통화 - -**현재 분산된 위치**: -- `items/ItemListClient.tsx` - getItemTypeBadge() -- `production/WorkOrders/types.ts` - WORK_ORDER_STATUS_COLORS -- `outbound/ShipmentManagement/types.ts` - SHIPMENT_STATUS_STYLES -- 기타 각 도메인별 개별 정의 - -**이미 존재하는 공통 유틸** (`src/lib/utils/status-config.ts`): -```typescript -export const BADGE_STYLE_PRESETS: Record = { - default: 'bg-gray-100 text-gray-800', - success: 'bg-green-100 text-green-800', - warning: 'bg-yellow-100 text-yellow-800', - destructive: 'bg-red-100 text-red-800', - info: 'bg-blue-100 text-blue-800', - muted: 'bg-gray-100 text-gray-500', - orange: 'bg-orange-100 text-orange-800', - purple: 'bg-purple-100 text-purple-800', -}; -``` - -**문제**: 존재하지만 대부분의 페이지에서 사용하지 않음 - -### 2. 공통 필터 프리셋 - -**추출 가능한 공통 필터**: -```typescript -// 상태 필터 (거의 모든 페이지) -export const COMMON_STATUS_FILTER: FilterFieldConfig = { - key: 'status', - label: '상태', - type: 'single', - options: [ - { value: 'pending', label: '대기' }, - { value: 'in_progress', label: '진행중' }, - { value: 'completed', label: '완료' }, - ], -}; - -// 우선순위 필터 (생산, 출고 등) -export const COMMON_PRIORITY_FILTER: FilterFieldConfig = { - key: 'priority', - label: '우선순위', - type: 'single', - options: [ - { value: 'urgent', label: '긴급' }, - { value: 'priority', label: '우선' }, - { value: 'normal', label: '일반' }, - ], -}; -``` - -### 3. 우선순위 색상 통합 - -**현재 상태**: 여러 파일에서 동일한 색상 반복 -```typescript -// 긴급: bg-red-100 text-red-700 -// 우선: bg-orange-100 text-orange-700 -// 일반: bg-gray-100 text-gray-700 -``` - ---- - -## 🎯 권장 액션 - -### Phase 1: 즉시 실행 가능 - -1. **`createStatusConfig` 사용률 높이기** - - 기존 유틸 활용도 확인 - - 새 페이지 작성 시 필수 사용 권장 - -2. **공통 필터 프리셋 파일 생성** - - 위치: `src/lib/constants/filter-presets.ts` - - 상태/우선순위/유형 필터 템플릿 - -3. **우선순위 색상 상수 통합** - - 위치: `src/lib/utils/status-config.ts`에 추가 - -### Phase 2: 점진적 적용 - -1. 신규 페이지는 공통 유틸 필수 사용 -2. 기존 페이지는 수정 시 점진적 마이그레이션 -3. 기능 변경 없이 import만 변경 - ---- - -## 📊 공통화 효과 예측 - -| 항목 | Before | After | -|------|--------|-------| -| Badge 정의 위치 | 37개 파일에 분산 | 1개 파일 (+ import) | -| 필터 프리셋 | 각 페이지 개별 | 공통 상수 재사용 | -| 색상 변경 시 수정 범위 | 37개 파일 | 1개 파일 | -| 신규 페이지 개발 시간 | 기존 페이지 참고 필요 | 공통 유틸 import만 | - ---- - -## 📝 결론 - -1. **UniversalListPage는 이미 잘 구축됨** - 대부분 리스트가 사용 중 -2. **Badge/필터 공통화가 주요 개선점** - 반복 코드 제거 가능 -3. **기존 유틸(`createStatusConfig`) 활용도 낮음** - 홍보/가이드 필요 -4. **기능 변경 없이 공통화 가능** - 리팩토링 리스크 낮음 - ---- - -## ✅ 공통화 작업 완료 현황 (2026-02-05) - -### 생성된 파일 - -| 파일 | 설명 | -|------|------| -| `src/lib/constants/filter-presets.ts` | 공통 필터 프리셋 (상태/우선순위/품목유형 등) | -| `claudedocs/guides/badge-commonization-guide.md` | Badge 공통화 사용 가이드 | - -### 수정된 파일 - -| 파일 | 변경 내용 | -|------|----------| -| `src/lib/utils/status-config.ts` | 우선순위/품목유형 설정 추가, 한글 라벨 지원 | -| `src/components/production/WorkOrders/WorkOrderList.tsx` | 공통 유틸 적용 (샘플 마이그레이션) | - -### 추가된 공통 유틸 - -**filter-presets.ts**: -- `COMMON_STATUS_FILTER` - 대기/진행/완료 -- `WORK_STATUS_FILTER` - 작업대기/진행중/작업완료 -- `COMMON_PRIORITY_FILTER` - 긴급/우선/일반 -- `ITEM_TYPE_FILTER` - 품목유형 -- `createSingleFilter()`, `createMultiFilter()` - 커스텀 필터 생성 - -**status-config.ts**: -- `getPriorityLabel()`, `getPriorityStyle()` - 우선순위 (한글/영문 모두 지원) -- `getItemTypeLabel()`, `getItemTypeStyle()` - 품목유형 -- `COMMON_STATUS_CONFIG`, `WORK_STATUS_CONFIG` 등 - 미리 정의된 상태 설정 - -### 샘플 마이그레이션 결과 (WorkOrderList.tsx) - -**Before**: -```tsx -// 개별 정의 -const PRIORITY_COLORS: Record = { - '긴급': 'bg-red-100 text-red-700', - '우선': 'bg-orange-100 text-orange-700', - '일반': 'bg-gray-100 text-gray-700', -}; - -const filterConfig: FilterFieldConfig[] = [ - { key: 'status', label: '상태', type: 'single', options: [...] }, - { key: 'priority', label: '우선순위', type: 'single', options: [...] }, -]; - - -``` - -**After**: -```tsx -// 공통 유틸 사용 -import { WORK_STATUS_FILTER, COMMON_PRIORITY_FILTER } from '@/lib/constants/filter-presets'; -import { getPriorityStyle } from '@/lib/utils/status-config'; - -const filterConfig = [WORK_STATUS_FILTER, COMMON_PRIORITY_FILTER]; - - -``` - -**효과**: -- 코드 라인 20줄 → 3줄 -- 필터 옵션 중복 정의 제거 -- 색상 일관성 보장 - ---- - -## 🔄 추가 마이그레이션 (2026-02-05 업데이트) - -### 완료된 마이그레이션 - -| 파일 | 적용 내용 | 효과 | -|------|----------|------| -| `WorkOrderList.tsx` | WORK_STATUS_FILTER + COMMON_PRIORITY_FILTER + getPriorityStyle | 20줄 → 3줄 | -| `ItemListClient.tsx` | getItemTypeStyle (품목유형 Badge) | 17줄 → 4줄 | -| `ItemDetailClient.tsx` | getItemTypeStyle (품목유형 Badge) | 17줄 → 4줄 | - -### 마이그레이션 제외 대상 (도메인 특화 설정) - -| 파일 | 제외 사유 | -|------|----------| -| `PricingListClient.tsx` | 다른 색상 체계 (SM=cyan, BENDING 추가 타입) | -| `StockStatus/types.ts` | 레거시 타입 지원 (raw_material, bent_part 등) | -| `ShipmentManagement/types.ts` | 다른 우선순위 라벨 (보통/낮음) | -| `issue-management/types.ts` | 2단계 우선순위 (긴급/일반만) | -| `WipProductionModal.tsx` | 버튼 스타일 우선순위 (Badge 아님) | -| `ReceivingList.tsx` | 도메인 특화 상태 (입고대기/입고완료/검사완료) | -| HR 페이지들 | 도메인 특화 상태 설정 | -| 건설 도메인 페이지들 | 도메인 특화 상태 설정 | - -### 분석 결과 요약 - -1. **공통 유틸 적용 완료 페이지**: 3개 (WorkOrderList, ItemListClient, ItemDetailClient) -2. **도메인 특화 설정 페이지**: 34개 (개별 유지가 적절) -3. **결론**: 대부분의 페이지는 도메인별 특화된 상태/라벨/색상을 사용하며, 이는 비즈니스 로직을 명확히 반영하기 위해 의도된 설계 - -### 공통 유틸 권장 사용 시나리오 - -1. **신규 리스트 페이지 생성 시**: 표준 패턴(대기/진행/완료, 긴급/우선/일반) 사용 -2. **품목유형 Badge**: 일관된 색상 적용 필요 시 `getItemTypeStyle` 사용 -3. **우선순위 Badge**: 표준 3단계(긴급/우선/일반) 사용 시 `getPriorityStyle` 사용 - ---- - -## 🎨 getPresetStyle 마이그레이션 완료 (2026-02-05 최종) - -### 마이그레이션 완료 파일 (22개) - -| 파일 | 적용 내용 | -|------|----------| -| `orders/OrderRegistration.tsx` | success, info preset | -| `pricing-distribution/PriceDistributionDetail.tsx` | success preset | -| `pricing/PricingFormClient.tsx` | purple, info, success preset | -| `quality/InspectionManagement/InspectionList.tsx` | success, destructive preset | -| `quality/InspectionManagement/InspectionCreate.tsx` | success, destructive preset | -| `quality/InspectionManagement/InspectionDetail.tsx` | success, destructive preset | -| `accounting/PurchaseManagement/index.tsx` | info preset | -| `accounting/PurchaseManagement/PurchaseDetail.tsx` | orange preset (기존) | -| `accounting/PurchaseManagement/PurchaseDetailModal.tsx` | orange preset (기존) | -| `accounting/VendorManagement/CreditAnalysisModal/CreditAnalysisDocument.tsx` | info preset | -| `quotes/QuoteRegistration.tsx` | success preset | -| `pricing/PricingHistoryDialog.tsx` | info preset | -| `business/construction/management/KanbanColumn.tsx` | info preset | -| `business/construction/management/DetailCard.tsx` | warning preset | -| `business/construction/management/StageCard.tsx` | warning preset | -| `business/construction/management/ProjectCard.tsx` | info preset | -| `production/WorkerScreen/WorkCard.tsx` | success, destructive preset | -| `production/WorkerScreen/ProcessDetailSection.tsx` | warning preset | -| `production/ProductionDashboard/index.tsx` | orange, success preset (기존) | -| `items/ItemForm/BOMSection.tsx` | info preset (기존) | -| `items/DynamicItemForm/sections/DynamicBOMSection.tsx` | info preset (기존) | -| `items/ItemMasterDataManagement/tabs/MasterFieldTab/index.tsx` | info preset | -| `customer-center/InquiryManagement/InquiryList.tsx` | warning, success preset (기존) | -| `hr/EmployeeManagement/CSVUploadDialog.tsx` | success, destructive preset (기존) | - -### 마이그레이션 제외 파일 (유지) - -| 파일 | 제외 사유 | -|------|----------| -| `business/MainDashboard.tsx` | CEO 대시보드 - 다양한 데이터 시각화용 고유 색상 (achievement %, overdue days 등) | -| `pricing/PricingListClient.tsx` | 도메인 특화 색상 체계 (SM=cyan, BENDING type 등) | -| `business/CEODashboard/sections/TodayIssueSection.tsx` | 알림 유형별 고유 색상+아이콘 (notification_type 기반) | -| `dev/DevToolbar.tsx` | 개발 도구 (운영 무관) | -| `ui/status-badge.tsx` | 이미 status-config.ts 사용 중 | -| `items/ItemDetailClient.tsx` | getItemTypeStyle 사용 (도메인 특화) | -| `items/ItemListClient.tsx` | getItemTypeStyle 사용 (도메인 특화) | - -### 사용된 Preset 유형 통계 - -| Preset | 사용 횟수 | 용도 | -|--------|----------|------| -| `success` | 15+ | 완료, 일치, 활성, 긍정적 상태 | -| `info` | 10+ | 정보성 라벨, 진행 상태, 문서 타입 | -| `warning` | 6+ | 진행중, 주의 필요, 선행 생산 | -| `destructive` | 5+ | 오류, 불일치, 긴급 | -| `orange` | 3+ | 품의서/지출결의서, 지연 | -| `purple` | 2+ | 최종 확정, 특수 상태 | - -### 마이그레이션 효과 - -1. **코드 일관성**: 22개 파일에서 동일한 유틸리티 함수 사용 -2. **유지보수성**: 색상 변경 시 `status-config.ts` 한 곳만 수정 -3. **가독성 향상**: `getPresetStyle('success')` vs `bg-green-100 text-green-700 border-green-200` -4. **타입 안전성**: TypeScript로 프리셋 이름 자동완성 \ No newline at end of file diff --git a/claudedocs/architecture/[ANALYSIS-2026-02-19] frontend-comprehensive-review.md b/claudedocs/architecture/[ANALYSIS-2026-02-19] frontend-comprehensive-review.md deleted file mode 100644 index 27178ed8..00000000 --- a/claudedocs/architecture/[ANALYSIS-2026-02-19] frontend-comprehensive-review.md +++ /dev/null @@ -1,165 +0,0 @@ -# SAM ERP 프론트엔드 종합 검수 보고서 - -> 작성일: 2026-02-19 -> 분석 범위: src/ 전체 (1,438개 TS/TSX 파일, ~314K줄) -> 분석 방법: 5개 에이전트 병렬 분석 (코드품질, 번들/성능, 에러/UX, 아키텍처, 모바일/보안) - ---- - -## 종합 스코어카드 - -| 영역 | 점수 | 등급 | 핵심 이슈 | -|------|------|------|-----------| -| **코드 품질** | 7.5/10 | 🟢 양호 | TS 규율 우수, any 133건/TODO 121건 잔존 | -| **번들/성능** | 8.5/10 | 🟢 우수 | 동적 로드 적용, tree-shaking 양호 | -| **에러/UX 일관성** | 5.5/10 | 🟡 보통 | 에러바운더리 우수, 로딩UI/접근성 미흡 | -| **아키텍처** | 6.5/10 | 🟡 보통 | 순환의존 없음, 상태관리 중복 | -| **모바일 대응** | 6/10 | 🟡 보통 | 57% 반응형, 터치영역 미달 | -| **보안** | 7/10 | 🟢 양호 | 인증 강함, CSP unsafe 허용 | - -**전체: 6.8/10** — 기능적으로 안정적이나, UX 일관성과 아키텍처 정리에 개선 여지 - ---- - -## 우선순위별 개선 항목 - -### P0: 보안 이슈 (즉시 조치) - -| # | 항목 | 심각도 | 현황 | 조치 | -|---|------|--------|------|------| -| S-1 | CSP `unsafe-inline`/`unsafe-eval` | 🔴 높음 | middleware.ts에서 허용 중 | nonce 기반으로 전환 | -| S-2 | `new Function()` 코드 주입 | 🔴 높음 | ComputedField.tsx에서 사용 | 사용자 입력 검증 추가 또는 safe-eval 대체 | -| S-3 | sanitizeHTML 함수 강도 | 🟡 중간 | 5개 파일에서 사용 중 | DOMPurify 사용 여부 확인 | - -### P1: 아키텍처 정리 (1~2주) - -| # | 항목 | 현황 | 개선안 | -|---|------|------|--------| -| A-1 | **상태관리 중복** | ItemMasterContext + itemStore + useItemMasterStore 3중 | Zustand 하나로 통합 | -| A-2 | **테마 중복** | ThemeContext + themeStore 병존 | Zustand로 완전 마이그레이션 | -| A-3 | **utils 폴더 중복** | `src/utils/` (2개) + `src/lib/utils/` (11개) 병존 | `src/utils/` → `src/lib/utils/`로 통합 | -| A-4 | **상수 산재** | constants/ 1개 파일만, 나머지 각 컴포넌트 내부 하드코딩 | 도메인별 `constants/` 정리 | - -### P2: 코드 품질 (2~3주) - -| # | 항목 | 건수 | 현황 | 조치 | -|---|------|------|------|------| -| Q-1 | `as any` 타입 캐스트 | 64건 | 주로 form errors 처리 | 제네릭 타입 정의 | -| Q-2 | `: any` 타입 선언 | 48건 | API 응답/props 타입 | 인터페이스 정의 | -| Q-3 | TODO/FIXME 누적 | 121건 (68파일) | useItemMasterStore 15건 등 | 이슈화 → 점진적 해소 | -| Q-4 | God 컴포넌트 | 5개 | ItemMasterContext 2,200줄, MainDashboard 1,400줄 | 단계적 분리 | -| Q-5 | 거대 훅 | 1개 | useCEODashboard 37.9KB | stats/charts/timeline 분리 | -| Q-6 | `alert()`/`confirm()` 잔존 | 32건 | 15개 alert + 17개 confirm | ConfirmDialog/toast로 교체 | - -### P3: UX 일관성 (3~4주) - -| # | 항목 | 현황 | 목표 | -|---|------|------|------| -| U-1 | **로딩 UI** | 40+ 페이지에서 `"로딩 중..."` 텍스트만 사용, Skeleton 2개만 | Skeleton 기반 로딩으로 통일 | -| U-2 | **접근성 (a11y)** | aria-label 3건, role 9건 | 주요 폼/테이블에 ARIA 추가 | -| U-3 | **i18n 사용률** | 인프라 완성(ko/en/ja), 실제 사용 ~5% | 점진적 적용 확대 | -| U-4 | **Zod 검증** | 2개 폼만 적용 | 신규 폼 필수, 기존은 유지 | -| U-5 | **EmptyState 활용** | 컴포넌트 존재하나 하드코딩 "데이터 없음" 다수 | EmptyState 컴포넌트 통일 | - -### P4: 모바일/성능 (선택) - -| # | 항목 | 현황 | 조치 | -|---|------|------|------| -| M-1 | **반응형 커버리지** | 57% 페이지 적용 | HR/대시보드 등 미적용 페이지 보강 | -| M-2 | **터치 영역** | Checkbox 20x20px (권장 44x44px) | 모바일 터치 타겟 확대 | -| M-3 | **html2canvas + dom-to-image** 중복 | 2개 라이브러리 공존 | 하나로 통합 (~50-80KB 절감) | -| M-4 | **Tiptap 동적 로딩** | 보드/팝업에서만 사용하나 번들 포함 | next/dynamic 적용 (~80-100KB 절감) | -| M-5 | **도메인별 actions.ts 표준화** | accounting만 page-level actions, 나머지는 컴포넌트 내부 | accounting 패턴으로 통일 | - ---- - -## 잘 되어있는 점 (유지 사항) - -### 코드 품질 -- ✅ **TypeScript 규율**: @ts-ignore 0건, @ts-nocheck 1건(레거시) -- ✅ **console.log 관리**: 23건만 (16건은 logger 유틸리티) -- ✅ **에러 바운더리**: 글로벌 + Protected 레벨 4개, Slack 연동 -- ✅ **Toast 시스템**: sonner 기반 1,277개 인스턴스 일관 사용 - -### 번들/성능 -- ✅ **XLSX 동적 로드**: 버튼 클릭 시에만 ~400KB 로드 -- ✅ **대시보드 코드 스플리팅**: ~850KB 초기 번들에서 제외 -- ✅ **tree-shaking**: `import *` 0건, lodash/moment 미사용 -- ✅ **Zustand 정규화**: 체계적 상태 + Immer + selector hooks -- ✅ **Tailwind v4**: 최신 버전, 효율적 트리셰이킹 - -### 아키텍처 -- ✅ **순환 의존성 없음**: pages→components→ui 단방향 -- ✅ **API 계층**: buildApiUrl 43개 actions.ts 전면 적용 -- ✅ **executePaginatedAction**: 14개 파일 표준화 - -### 보안 -- ✅ **Bot 차단**: 25개 패턴 필터링 -- ✅ **다층 인증**: Bearer Token + Authorization 헤더 + Sanctum + API Key -- ✅ **Open Redirect 방지**: 내부 경로 검증 -- ✅ **환경변수 분리**: NEXT_PUBLIC_ 적절히 사용 -- ✅ **민감 정보 노출 없음**: console.log에 토큰/비밀번호 출력 0건 - ---- - -## 주요 파일 참조 - -### God 컴포넌트 (분리 대상) -- `src/contexts/ItemMasterContext.tsx` (2,200줄) -- `src/components/business/MainDashboard.tsx` (1,400줄) -- `src/hooks/useCEODashboard.ts` (37.9KB) - -### any 타입 집중 지역 -- `src/components/items/ItemForm/forms/parts/` (22건) -- `src/components/items/ItemMasterDataManagement/` (18건) -- `src/components/quotes/LocationDetailPanel.tsx` (10건) - -### 보안 확인 대상 -- `src/middleware.ts` (CSP 설정) -- `src/components/**/ComputedField.tsx` (new Function) -- sanitizeHTML 사용 파일 5개 (게시판, 팝업, 고객센터) - -### 상태관리 중복 -- `src/contexts/ItemMasterContext.tsx` vs `src/stores/itemStore.ts` vs `src/stores/item-master/useItemMasterStore.ts` -- `src/contexts/ThemeContext.tsx` vs `src/stores/themeStore.ts` - ---- - -## 기존 로드맵과의 관계 - -| 기존 항목 | 상태 | 이번 분석 결과 | -|-----------|------|---------------| -| D-1 God 컴포넌트 분리 | ⏳ 대기 | → P2-Q4로 재확인, 여전히 필요 | -| D-2 `as` 타입 캐스트 | 보류 | → P2-Q1/Q2로 133건 확인 (기존 ~200건에서 감소) | -| D-6 TODO 102건 | ⏳ 대기 | → P2-Q3으로 121건 확인 (소폭 증가) | -| A-2 DataTable 최적화 | ⏳ 대기 | → 에이전트 분석 결과 re-render 위험 낮음 (우선순위 하향) | - -### 신규 발견 항목 (기존 로드맵에 없었던 것) -- **S-1~S-3**: 보안 이슈 (CSP, code injection, sanitization) -- **A-1~A-2**: 상태관리 3중 중복 -- **U-1~U-5**: UX 일관성 전반 (로딩/접근성/i18n/빈상태) -- **M-3~M-4**: 라이브러리 중복/동적 로딩 기회 - ---- - -## 실행 로드맵 요약 - -``` -Week 1-2: P0 보안 + P1 아키텍처 정리 - ├── CSP nonce 전환 - ├── ComputedField 보안 패치 - ├── 상태관리 중복 정리 (Context → Zustand) - └── utils 폴더 통합 - -Week 3-4: P2 코드 품질 - ├── any 타입 정리 (form errors 제네릭) - ├── alert/confirm → ConfirmDialog 교체 - └── TODO/FIXME 이슈 정리 - -Week 5-6: P3 UX 일관성 (선택) - ├── Skeleton 로딩 UI 통일 - ├── EmptyState 활용 확대 - └── 접근성 기본 적용 - -이후: P4 모바일/성능 (필요 시) -``` \ No newline at end of file diff --git a/claudedocs/architecture/[ANALYSIS-2026-02-23] deep-analysis-util-component-zustand.md b/claudedocs/architecture/[ANALYSIS-2026-02-23] deep-analysis-util-component-zustand.md deleted file mode 100644 index dc10c79e..00000000 --- a/claudedocs/architecture/[ANALYSIS-2026-02-23] deep-analysis-util-component-zustand.md +++ /dev/null @@ -1,396 +0,0 @@ -# SAM ERP 프로젝트 심층분석 종합 보고서 -> 분석일: 2026-02-23 | 분석 영역: Util 분리 / 컴포넌트 공통화 / Zustand 통합 - ---- - -## 목차 -1. [Executive Summary](#1-executive-summary) -2. [Util 함수 분리 분석](#2-util-함수-분리-분석) -3. [컴포넌트 공통화 분석](#3-컴포넌트-공통화-분석) -4. [Zustand 스토어 통합 분석](#4-zustand-스토어-통합-분석) -5. [통합 리팩토링 로드맵](#5-통합-리팩토링-로드맵) - ---- - -## 1. Executive Summary - -### 전체 현황 스코어카드 - -| 영역 | 현재 수준 | 주요 이슈 | 예상 절감 | -|------|----------|----------|----------| -| **Util 분리** | 🟡 보통 | 중복 함수 6건, 과대 파일 4개, 인라인 유틸 6패턴 | ~800줄 | -| **컴포넌트 공통화** | 🟡 보통 | 중복 다이얼로그 5건, Detail 버전 혼재, 패턴 비일관 | ~1,500줄 | -| **Zustand 통합** | 🟢 양호 | Context→Zustand 미전환 3건, 셀렉터 훅 미비 | 리렌더 최적화 | - -### Top 5 우선 조치 항목 - -1. 🔴 **AuthContext → Zustand 마이그레이션** (전역 리렌더 제거) -2. 🔴 **GenericCRUDDialog 추출** (5개 중복 다이얼로그 통합) -3. 🔴 **파일 다운로드 로직 통합** (3곳 중복 → 1곳) -4. 🟡 **dashboard/transformers.ts 분할** (1,700줄 → 도메인별 분리) -5. 🟡 **Detail/DetailClient/DetailClientV2 정리** (버전 혼재 제거) - ---- - -## 2. Util 함수 분리 분석 - -### 2.1 현재 유틸 파일 인벤토리 - -``` -src/lib/ -├── utils.ts (cn, safeJsonParse - 최소) -├── formatters.ts (phone, businessNumber, card, account 포맷터) -├── print-utils.ts (인쇄 유틸) -├── sanitize.ts (데이터 정제) -├── error-reporting.ts (에러 리포팅) -├── utils/ (13개 파일, ~82KB) -│ ├── amount.ts (금액 포맷: 원/만원) -│ ├── date.ts (날짜 유틸) -│ ├── validation.ts (Zod 스키마 - 725줄 ⚠️) -│ ├── excel-download.ts (엑셀 다운로드 - 528줄 ⚠️) -│ ├── fileDownload.ts (파일 다운로드) -│ ├── export.ts (엑셀 내보내기 - 중복 ⚠️) -│ ├── search.ts (검색/필터 파이프라인) -│ ├── materialTransform.ts (자재 데이터 변환) -│ ├── menuTransform.ts (메뉴 구조 변환) -│ ├── menuRefresh.ts (메뉴 새로고침) -│ ├── status-config.ts (상태 스타일 설정) -│ ├── redirect-error.ts (Next.js 리다이렉트 에러) -│ └── locale.ts (로케일 유틸) -├── api/ (25개 파일) -│ ├── error-handler.ts (API 에러 처리) -│ ├── toast-utils.ts (토스트 유틸 - 중복 ⚠️) -│ ├── transformers.ts (변환기 - 454줄 ⚠️) -│ ├── dashboard/transformers.ts (대시보드 변환 - 1,700줄 🔴) -│ ├── execute-server-action.ts -│ ├── execute-paginated-action.ts -│ └── query-params.ts (buildApiUrl - 표준화 완료) -├── permissions/ (3개 파일) -├── auth/ (2개 파일) -└── cache/ (2개 파일) -``` - -### 2.2 중복 로직 탐지 (6건) - -#### 🔴 HIGH PRIORITY - -| # | 중복 항목 | 위치 | 상세 | -|---|----------|------|------| -| 1 | **Blob 다운로드** | `export.ts`, `excel-download.ts`, `fileDownload.ts` | 동일한 `URL.createObjectURL → link.click → revokeObjectURL` 패턴이 3곳에 존재 | -| 2 | **날짜 문자열 생성** | `export.ts:58`, `excel-download.ts:78` | `toISOString().slice(0,10).replace(/-/g,'')` 동일 패턴, 시간 정밀도만 다름(초 vs 분) | -| 3 | **에러 메시지 포맷** | `error-handler.ts:122`, `toast-utils.ts:106` | `getErrorMessage()` vs `formatApiError()` - 동일 로직 | -| 4 | **숫자 포맷팅** | `amount.ts:15`, `formatters.ts:178` | `Intl.NumberFormat` vs regex 기반 - 3가지 접근법 혼재 | - -#### 🟡 MEDIUM PRIORITY - -| # | 중복 항목 | 위치 | -|---|----------|------| -| 5 | 엑셀 파일명 생성 | `export.ts:54` vs `excel-download.ts:78` | -| 6 | 쿼리 파라미터 빌드 | 레거시 `URLSearchParams` 패턴 (마이그레이션 완료 상태) | - -### 2.3 인라인 유틸 추출 후보 (6패턴) - -컴포넌트 내부에 반복적으로 등장하지만 util로 분리되지 않은 패턴: - -| 패턴 | 발견 위치 | 영향 파일 | 추천 위치 | -|------|----------|----------|----------| -| 월/분기 날짜 범위 계산 | TaxInvoice, HR 페이지들 | 5+ | `lib/utils/dateRange.ts` | -| 시간 문자열 포맷팅 | TransactionFormModal, time-picker | 4+ | `lib/utils/timeFormatter.ts` | -| 포맷된 숫자 파싱 | VendorManagement, Withdrawal 등 | 8+ | `lib/formatters.ts` 확장 | -| 에러 객체→메시지 변환 | attendance/page, employee/page | 3+ | `lib/utils/errorFormatter.ts` | -| 배열 합계/카운트 reduce | 대시보드, 주문관리 등 | 6+ | `lib/utils/aggregation.ts` | -| 파일 크기 포맷팅 | file-input.tsx | 2 | `lib/utils/fileSizeFormatter.ts` | - -### 2.4 과대 파일 (분할 필요) - -| 파일 | 줄 수 | 문제 | 분할 방안 | -|------|-------|------|----------| -| 🔴 `api/dashboard/transformers.ts` | **1,700+** | 10+ 도메인 변환 혼재 | `dashboard/transformers/{sales,production,quality,accounting,hr,common}.ts` | -| 🟡 `utils/validation.ts` | 725 | 5개 아이템 타입 스키마 혼재 | `validations/{item-master-base,product,part,material,filters}.ts` | -| 🟡 `utils/excel-download.ts` | 528 | 다운로드/내보내기/템플릿 혼재 | `{blob-download,excel-export,excel-template}.ts` | -| 🟡 `api/transformers.ts` | 454 | 27개 export 함수 | `transformers/{pages,sections,fields,bom,templates,options}.ts` | - -### 2.5 미사용 유틸 (후보) - -| 함수 | 파일 | 상태 | -|------|------|------| -| `parsePhoneNumber()` | `formatters.ts:36` | import 0건 | -| `extractNumbers()` | `formatters.ts:220` | import 0건 | -| `formatPersonalNumber()` | `formatters.ts:84` | 실제 사용은 `formatPersonalNumberMasked` | - ---- - -## 3. 컴포넌트 공통화 분석 - -### 3.1 현재 컴포넌트 계층 구조 - -``` -src/components/ -├── ui/ (49개 - Radix UI 래퍼) -├── atoms/ (3개 - 최소 단위) -├── molecules/ (9개 - 복합 폼/표시) -├── organisms/ (11개 - 비즈니스 컴포넌트) -├── templates/ (2+1개 - UniversalListPage, IntegratedDetailTemplate, IntegratedListTemplateV2) -├── accounting/ (18개 도메인 폴더, 100+ 컴포넌트) -├── settings/ (12개 도메인 폴더) -└── [기타 도메인] (15+ 폴더) -``` - -### 3.2 중복 컴포넌트 패턴 (핵심 발견) - -#### 🔴 CRITICAL: 단순 CRUD 다이얼로그 중복 (5건) - -거의 동일한 구조: Dialog 래퍼 → 폼 필드 → 유효성 검증 → 제출/취소 버튼 - -| 컴포넌트 | 줄 수 | 차이점 | -|----------|-------|--------| -| `settings/RankManagement/RankDialog.tsx` | 89 | 라벨명만 다름 | -| `settings/TitleManagement/TitleDialog.tsx` | 90 | 라벨명만 다름 | -| `settings/PermissionManagement/PermissionDialog.tsx` | ~90 | 라벨명만 다름 | -| `settings/NotificationSettings/ItemSettingsDialog.tsx` | ~90 | 라벨명만 다름 | -| `accounting/VendorManagement/CreditAnalysisModal/` | ~100 | 약간 복잡 | - -**해결안**: `GenericCRUDDialog` 제네릭 컴포넌트 생성 -```typescript -// src/components/molecules/GenericCRUDDialog.tsx -interface GenericCRUDDialogProps { - isOpen: boolean; - onOpenChange: (open: boolean) => void; - mode: 'add' | 'edit'; - title: string; - fields: FormFieldDefinition[]; - data?: T; - onSubmit: (data: T) => Promise; -} -``` -→ **~400줄 절감** - -#### 🔴 CRITICAL: Detail 파일 버전 혼재 - -한 엔티티에 대해 여러 버전의 Detail 파일이 공존: - -| 엔티티 | 파일들 | 문제 | -|--------|--------|------| -| BadDebt | `BadDebtDetail.tsx`, `BadDebtDetailClientV2.tsx` | V2 마이그레이션 미완 | -| Withdrawal | `WithdrawalDetailClientV2.tsx` | ClientV2 접미사 | -| Deposit | `DepositDetailClientV2.tsx` | ClientV2 접미사 | -| Vendor | `VendorDetail.tsx`, `VendorDetailClient.tsx` | 두 파일 공존 | - -→ **단일 소스로 통합 필요, ~300줄 절감** - -#### 🟡 HIGH: 리스트 페이지 설정 중복 - -`UniversalListPage`로 통합은 잘 되어있으나, 설정(config) 코드가 각 페이지에 반복: - -| 반복 요소 | 발견 위치 | 해결안 | -|-----------|----------|--------| -| 상태 관리 (data, filters, pagination) | Sales, Purchase, Vendor 등 | 설정 파일 분리 | -| DateRange 선택기 | 8+ 회계 페이지 | `useDateRange()` 훅 표준화 | -| Stats 계산 useMemo | 대부분의 리스트 페이지 | `DataStatsCard` 추출 | - -→ **~500줄 절감** - -### 3.3 재사용률 분석 - -#### 높은 재사용 (Good) -- **UniversalListPage**: 40+ 페이지 (우수) -- **IntegratedDetailTemplate**: 20+ 상세 페이지 -- **FormField**: 50+ 폼 - -#### 활용 부족 (Should Use More) -- **SearchableSelectionModal**: 실제 3곳만 사용 → 더 광범위 적용 가능 -- **StandardDialog**: 존재하지만 단순 다이얼로그들이 미사용 -- **MobileCard**: 정의되었지만 비일관적 사용 - -### 3.4 패턴 비일관성 - -| 패턴 | 현재 상태 | 표준화 방향 | -|------|----------|------------| -| 날짜 범위 선택 | 3가지 방식 혼재 (컴포넌트/훅/인라인) | `useDateRange()` + `` | -| 검색/필터 | 3가지 경쟁 패턴 (A: UniversalListPage, B: 커스텀 useState, C: IntegratedListTemplateV2) | Pattern A로 통일 | -| 모달 vs 페이지 | VendorDetail→풀페이지, PurchaseDetail→모달 혼재 | 도메인별 기준 확립 | - -### 3.5 추출 필요 공유 컴포넌트 - -| 컴포넌트 | 사용처 | 설명 | -|----------|--------|------| -| `LineItemsTable` | SalesDetail, PurchaseDetail | 품목 추가/삭제/계산 테이블 (~150줄×2 절감) | -| `DataStatsCard` | 회계 리스트 페이지들 | 유연한 통계 표시 카드 | -| `DocumentTemplate` | CreditAnalysis, InspectionReport | 인쇄용 문서 래퍼 (헤더/푸터/워터마크) | -| `DataTableWithActions` | 대부분의 리스트 | 페이지네이션+선택+액션 통합 | - ---- - -## 4. Zustand 스토어 통합 분석 - -### 4.1 현재 스토어 인벤토리 (7개) - -| 스토어 | 파일 | 줄 수 | 미들웨어 | 용도 | -|--------|------|-------|---------|------| -| `useItemMasterStore` | `stores/item-master/useItemMasterStore.ts` | 1,150 | devtools, immer | 품목기준관리 정규화 상태 | -| `useMasterDataStore` | `stores/masterDataStore.ts` | 450 | devtools | 동적 폼 설정 캐싱 | -| `useMenuStore` | `stores/menuStore.ts` | ~100 | persist | 사이드바/메뉴 상태 | -| `useFavoritesStore` | `stores/favoritesStore.ts` | ~100 | persist + custom storage | 즐겨찾기 (최대 10개) | -| `useThemeStore` | `stores/themeStore.ts` | ~50 | persist | 테마 (light/dark/senior) | -| `useTableColumnStore` | `stores/useTableColumnStore.ts` | ~100 | persist + custom storage | 테이블 컬럼 가시성/너비 | -| `useCalendarScheduleStore` | `stores/useCalendarScheduleStore.ts` | ~100 | devtools | 캘린더 일정 연도별 캐싱 | - -### 4.2 핵심 발견: Context → Zustand 미전환 (3건) - -#### 🔴 #1: AuthContext (최우선) - -| 항목 | 현재 | 문제 | -|------|------|------| -| **위치** | `/src/contexts/AuthContext.tsx` (278줄) | React Context + useState | -| **상태** | users[], currentUser, roles, tenants | Provider 리렌더 전파 | -| **localStorage** | 수동 동기화 (line 162-190) | Zustand persist가 자동 처리 가능 | -| **영향** | 사이드바, 대시보드, 모든 인증 페이지 | 상태 변경 시 전체 앱 리렌더 | - -**전환 방안**: -```typescript -// /src/stores/authStore.ts -export const useAuthStore = create()( - persist( - devtools((set) => ({ - currentUser: null, - setCurrentUser: (user) => set({ currentUser: user }), - // ... 기타 액션 - })), - { name: 'mes-currentUser' } - ) -); -``` - -#### 🟡 #2: ItemMasterContext (중복 제거) - -| 항목 | 현재 | 문제 | -|------|------|------| -| **Context** | `contexts/ItemMasterContext.tsx` (27,922 토큰) | useState 13개+ 상태 | -| **Zustand** | `stores/item-master/useItemMasterStore.ts` (1,150줄) | 유사 데이터 관리 | -| **중복** | 양쪽에서 품목 마스터 데이터 관리 | 캐싱/API 레이어 분리 | - -→ **Context를 Zustand 스토어로 통합, Context는 얇은 래퍼로만 유지** - -#### 🟡 #3: PermissionContext - -| 항목 | 현재 | 문제 | -|------|------|------| -| **위치** | `contexts/PermissionContext.tsx` | 순수 데이터/셀렉터 패턴 | -| **적합도** | Zustand 셀렉터 패턴에 완벽 부합 | Provider 불필요 | - -### 4.3 셀렉터 훅 미비 (성능 이슈) - -| 스토어 | 셀렉터 훅 | 문제 | -|--------|----------|------| -| ✅ `masterDataStore` | `usePageConfig()`, `usePageConfigLoading()` 등 | 양호 | -| ❌ `useTableColumnStore` | 없음 - 전체 스토어 구독 | 불필요한 리렌더 | -| ❌ `useMenuStore` | 없음 - 전체 스토어 구독 | 사이드바 토글이 모든 구독자 리렌더 | -| ❌ `useThemeStore` | 없음 | 경미 | - -**해결 패턴**: -```typescript -// ✅ 추가 필요 -export const useTableSettings = (pageId: string) => - useTableColumnStore((state) => state.pageSettings[pageId]); - -export const useMenuActiveId = () => - useMenuStore((state) => state.activeMenu); - -export const useSidebarCollapsed = () => - useMenuStore((state) => state.sidebarCollapsed); -``` - -### 4.4 Custom Storage 중복 - -`favoritesStore`와 `tableColumnStore`에서 동일한 사용자별 localStorage 래퍼가 반복: - -```typescript -// 두 파일 모두 동일 패턴 반복: -const customStorage = { - getItem: (name) => { /* userId 기반 키 생성 */ }, - setItem: (name, value) => { /* userId 기반 키로 저장 */ }, - removeItem: (name) => { /* userId 기반 키로 삭제 */ }, -}; -``` - -**해결안**: `/src/lib/storage/user-scoped-storage.ts` 추출 -```typescript -export function createUserScopedStorage(prefix: string): StateStorage { - return { getItem, setItem, removeItem }; -} -``` - -### 4.5 누락된 스토어 기회 - -| 스토어 | 용도 | 현재 상태 | -|--------|------|----------| -| 🔴 `useUIStore` | 전역 모달/노티/로딩 | 각 컴포넌트에서 로컬 관리 | -| 🟡 글로벌 필터 상태 | 리스트 페이지 공통 필터 | useState로 산재 | - ---- - -## 5. 통합 리팩토링 로드맵 - -### Phase 1: 즉시 (1주) - -| 작업 | 영역 | 영향도 | 난이도 | -|------|------|--------|--------| -| AuthContext → Zustand 마이그레이션 | Zustand | 🔴 전역 리렌더 제거 | 중 | -| GenericCRUDDialog 추출 (5개 다이얼로그 통합) | 컴포넌트 | 🔴 ~400줄 절감 | 저 | -| Blob 다운로드 로직 통합 (3곳→1곳) | Util | 🔴 중복 제거 | 저 | -| 에러 메시지 포맷 통합 (`formatApiError` 제거) | Util | 🟡 API 레이어 정리 | 저 | -| Zustand 셀렉터 훅 추가 (3개 스토어) | Zustand | 🟡 리렌더 최적화 | 저 | - -### Phase 2: 단기 (2~3주) - -| 작업 | 영역 | 영향도 | 난이도 | -|------|------|--------|--------| -| `dashboard/transformers.ts` 분할 (1,700줄) | Util | 🟡 유지보수성 | 중 | -| Detail 파일 버전 정리 (V2 통합) | 컴포넌트 | 🟡 ~300줄 절감 | 중 | -| `LineItemsTable` organism 추출 | 컴포넌트 | 🟡 Sales/Purchase 공통화 | 중 | -| Custom Storage 유틸 추출 | Zustand | 🟡 DRY | 저 | -| 날짜 범위 선택 표준화 | 컴포넌트 | 🟡 패턴 통일 | 중 | - -### Phase 3: 중기 (3~4주) - -| 작업 | 영역 | 영향도 | 난이도 | -|------|------|--------|--------| -| `validation.ts` 분할 (725줄) | Util | 🟢 유지보수성 | 저 | -| ItemMasterContext → Zustand 통합 | Zustand | 🟡 중복 제거 | 고 | -| IntegratedListTemplateV2 폐기 | 컴포넌트 | 🟢 레거시 제거 | 중 | -| 인라인 유틸 추출 (6패턴) | Util | 🟢 코드 품질 | 저 | -| 미사용 유틸 함수 정리 | Util | 🟢 코드 청결 | 저 | - -### Phase 4: 장기 (4주+) - -| 작업 | 영역 | 영향도 | 난이도 | -|------|------|--------|--------| -| PermissionContext → Zustand | Zustand | 🟢 아키텍처 통일 | 중 | -| DocumentTemplate organism 추출 | 컴포넌트 | 🟢 인쇄 공통화 | 중 | -| useUIStore 생성 (전역 UI 상태) | Zustand | 🟢 모달/노티 통합 | 중 | -| 숫자 포맷팅 API 표준화 | Util | 🟢 일관성 | 저 | - ---- - -## 부록: 핵심 파일 참조 - -### 리팩토링 대상 (Util) -- `/src/lib/utils/export.ts` - 중복 제거 대상 -- `/src/lib/utils/excel-download.ts` - 분할 대상 (528줄) -- `/src/lib/utils/validation.ts` - 분할 대상 (725줄) -- `/src/lib/api/dashboard/transformers.ts` - 분할 대상 (1,700줄) -- `/src/lib/api/toast-utils.ts` - `formatApiError` 제거 대상 - -### 리팩토링 대상 (컴포넌트) -- `/src/components/settings/RankManagement/RankDialog.tsx` - GenericCRUDDialog로 대체 -- `/src/components/settings/TitleManagement/TitleDialog.tsx` - GenericCRUDDialog로 대체 -- `/src/components/accounting/BadDebtCollection/BadDebtDetailClientV2.tsx` - 버전 통합 -- `/src/components/accounting/WithdrawalManagement/WithdrawalDetailClientV2.tsx` - 버전 통합 -- `/src/components/accounting/DepositManagement/DepositDetailClientV2.tsx` - 버전 통합 - -### 리팩토링 대상 (Zustand) -- `/src/contexts/AuthContext.tsx` → `/src/stores/authStore.ts` -- `/src/contexts/ItemMasterContext.tsx` → `/src/stores/item-master/` 통합 -- `/src/stores/useTableColumnStore.ts` - 셀렉터 훅 추가 -- `/src/stores/menuStore.ts` - 셀렉터 훅 추가 -- `/src/stores/favoritesStore.ts` - custom storage 유틸 추출 diff --git a/claudedocs/architecture/[ANALYSIS-2026-02-27] ceo-dashboard-analysis.md b/claudedocs/architecture/[ANALYSIS-2026-02-27] ceo-dashboard-analysis.md deleted file mode 100644 index 8ec2c525..00000000 --- a/claudedocs/architecture/[ANALYSIS-2026-02-27] ceo-dashboard-analysis.md +++ /dev/null @@ -1,176 +0,0 @@ -# CEO Dashboard 분석 (기획서 D1.7 기준) - -**기획서**: `SAM_ERP_Storyboard_D1.7_260227.pdf` p33~60 -**분석일**: 2026-02-27 -**상태**: 기획서 분석 완료, 구현 대기 - ---- - -## 1. 전체 구성 - -| 구분 | 페이지 | 수량 | -|------|--------|------| -| 메인 대시보드 섹션 | p33~43 | 20개 | -| 상세 모달 | p44~57 | 10개 | -| 참고 자료 (계산공식) | p58~60 | 3페이지 | - ---- - -## 2. 섹션별 현황 (20개) - -### API 연동 완료 (11개) - -| # | 섹션 | 페이지 | hook | API endpoint | -|---|------|--------|------|-------------| -| 1 | 오늘의 이슈 | p33 | useTodayIssue | today-issues/summary | -| 2 | 자금 현황 | p33-34 | useCEODashboard | daily-report/summary | -| 3 | 현황판 | p34 | useStatusBoard | status-board/summary | -| 4 | 당월 예상 지출 | p34-35 | useMonthlyExpense | expected-expenses/summary | -| 5 | 가지급금 현황 | p35 | useCardManagement | card-transactions/summary + 2개 | -| 6 | 접대비 현황 | p35-36 | useEntertainment | entertainment/summary | -| 7 | 복리후생비 현황 | p36 | useWelfare | welfare/summary | -| 8 | 미수금 현황 | p36 | useReceivable | receivables/summary | -| 9 | 채권추심 현황 | p37 | useDebtCollection | bad-debts/summary | -| 10 | 부가세 현황 | p37-38 | useVat | vat/summary | -| 11 | 캘린더 | p38 | useCalendar | calendar/schedules | - -### Mock 데이터만 (9개) - API 신규 필요 - -| # | 섹션 | 페이지 | 필요 데이터 | -|---|------|--------|-----------| -| 12 | 매출 현황 | p39 | 누적매출, 달성률, YoY, 당월매출 + 차트2 + 테이블 | -| 13 | 일별 매출 내역 | p47(설정) | 매출일, 거래처, 매출금액 (🆕 신규 섹션) | -| 14 | 매입 현황 | p40 | 누적매입, 미결제, YoY + 차트2 + 테이블 | -| 15 | 일별 매입 내역 | p47(설정) | 매입일, 거래처, 매입금액 (🆕 신규 섹션) | -| 16 | 생산 현황 | p41 | 공정별(스크린/슬랫/절곡) 집계 + 작업자현황 | -| 17 | 출고 현황 | p41 | 예상출고 7일/30일 금액+건수 | -| 18 | 미출고 내역 | p42 | 로트번호, 현장명, 수주처, 잔량, 납기일 | -| 19 | 시공 현황 | p42 | 진행/완료(7일이내) + 현장카드 | -| 20 | 근태 현황 | p43 | 출근/휴가/지각/결근 + 직원테이블 | - ---- - -## 3. 🔴 D1.7 핵심 변경사항 - -### 카드 구조 변경 (한도관리형 → 리스크감지형) - -| 섹션 | 기존 구현 | D1.7 기획서 | -|------|---------|-----------| -| **가지급금** | 카드, 가지급금, 법인세예상, 종합세예상 | 카드, 경조사, 상품권, 접대비, 총합계 (5카드) | -| **접대비** | 매출, 분기한도, 잔여한도, 사용금액 | **주말/심야, 기피업종, 고액결제, 증빙미비** | -| **복리후생비** | 당해한도, 분기한도, 잔여한도, 사용금액 | **비과세한도초과, 사적사용의심, 특정인편중, 항목별한도초과** | - -### 신규 섹션 (2개) -- 일별 매출 내역: 항목 설정(p47)에서 별도 ON/OFF -- 일별 매입 내역: 항목 설정(p47)에서 별도 ON/OFF - -### 설정 팝업 확장 (p45-47) -- 접대비: 한도관리(연간/반기/분기/월), 기업구분(일반법인/중소기업), 고액결제기준금액 -- 복리후생비: 한도관리, 계산방식(직원당정액 or 연봉총액×비율), 조건부입력필드, 1회결제기준금액 - ---- - -## 4. 상세 모달 (10개) - -| # | 모달 | 페이지 | 프론트 config | API 상태 | -|---|------|--------|-------------|---------| -| 1 | 일정 상세 | p44 | ✅ ScheduleDetailModal | ✅ 연동 | -| 2 | 항목 설정 | p45-47 | ✅ DashboardSettingsDialog | localStorage | -| 3 | 당월 매입 상세 | p48 | ✅ me1 config | ⚠️ 부분연동 | -| 4 | 당월 카드 상세 | p49 | ✅ me2 config | ⚠️ 부분연동 | -| 5 | 당월 발행어음 상세 | p50 | ✅ me3 config | ⚠️ 부분연동 | -| 6 | 당월 지출 예상 상세 | p51 | ✅ me4 config | ⚠️ 부분연동 | -| 7 | 가지급금 상세 | p52 | ✅ cm2 config | ⚠️ 구조변경 필요 | -| 8 | 접대비 상세 | p53-54 | ✅ et config | ⚠️ 대폭확장 | -| 9 | 복리후생비 상세 | p55-56 | ✅ wf config | ⚠️ 대폭확장 | -| 10 | 예상 납부세액 상세 | p57 | ✅ vat config | ⚠️ 확장필요 | - ---- - -## 5. 필요 API 작업 (16개) - -### 백엔드 API 수정 (6개) - -| # | API | 변경 내용 | -|---|-----|---------| -| 1 | 가지급금 summary | 카드/경조사/상품권/접대비 분류 집계 | -| 2 | 접대비 summary | 리스크 4종 (주말심야/기피업종/고액/증빙미비) - MCC코드 판별 | -| 3 | 복리후생비 summary | 리스크 4종 (비과세초과/사적사용/편중/한도초과) | -| 4 | 가지급금 detail | 분류별 상세 + AI분류 컬럼 | -| 5 | 복리후생비 detail | 계산방식별 + 분기별현황 | -| 6 | 부가세 detail | 신고기간별 + 부가세요약 + 미발행/미수취 | - -### 백엔드 API 신규 (10개) - -| # | API | 용도 | 난이도 | -|---|-----|------|--------| -| 1 | 접대비 detail | 한도계산 + 분기별현황 + 내역테이블 | 상 | -| 2 | 매출 현황 summary | 누적/달성률/YoY/당월 + 차트 | 중 | -| 3 | 일별 매출 내역 | 매출일, 거래처, 매출금액 | 하 | -| 4 | 매입 현황 summary | 누적/미결제/YoY + 차트 | 중 | -| 5 | 일별 매입 내역 | 매입일, 거래처, 매입금액 | 하 | -| 6 | 생산 현황 | 공정별 집계 + 작업자실적 | 상 | -| 7 | 출고 현황 | 7일/30일 예상출고 | 하 | -| 8 | 미출고 내역 | 납기기준 미출고 조회 | 하 | -| 9 | 시공 현황 | 진행/완료(7일이내) + 카드 | 중 | -| 10 | 근태 현황 | 출근/휴가/지각/결근 집계 | 중 | - ---- - -## 6. 프론트엔드 작업 (8개) - -| # | 작업 | 대상 | -|---|------|------| -| 1 | 가지급금 카드 구조 변경 | CardManagementSection | -| 2 | 접대비 카드 → 리스크형 | EntertainmentSection | -| 3 | 복리후생비 카드 → 리스크형 | WelfareSection | -| 4 | 일별 매출 내역 섹션 신규 | 새 컴포넌트 | -| 5 | 일별 매입 내역 섹션 신규 | 새 컴포넌트 | -| 6 | 항목 설정 팝업 업데이트 | DashboardSettingsDialog | -| 7 | 모달 config API 연동 | 각 modalConfigs | -| 8 | Mock 섹션 API 연동 | 매출~근태 hook 생성 | - ---- - -## 7. 데이터 아키텍처 - -대시보드 전용 테이블 없음. 모든 데이터는 각 도메인 페이지 입력 데이터의 실시간 집계. - -### 자금 현황 데이터 조합 -| 카드 | 출처 | -|------|------| -| 일일일보 | bank_accounts 잔액 합계 | -| 미수금 잔액 | sales 합계 - deposits 합계 | -| 미지급금 잔액 | purchases 합계 - payments 합계 | -| 당월 예상 지출 | 매입예정 + 카드결제 + 어음만기 합산 | - -### 리스크 감지 로직 (접대비/복리후생비) -- MCC 코드 기반 업종 판별 (p60: 유흥업소, 귀금속, 골프장 등) -- 체크 규칙: 시간대이상(22~06시), 업종이상, 금액이상(50만원), 빈도이상(월10회) -- 사적사용 의심: 토요일 23시 + 유흥주점 + 25만원 → 2개 규칙 해당 - -### 캐싱 -- sam_stat 테이블 5분 캐시 (백엔드 기존 구현) - ---- - -## 8. 참고 계산 공식 (p58-60) - -### 가지급금 인정이자 -- 인정이자율: 4.6% (당좌대출이자율 기준, 매년 고시) -- 인정이자 = 가지급금 × 일이자율(연이자율/365) × 경과일수 -- 법인세 추가: 인정이자 × 0.19 -- 대표자 소득세 추가: 인정이자 × 0.35 - -### 접대비 손금한도 -- 기본한도: 일반법인 1,200만원/년, 중소기업 3,600만원/년 -- 수입금액별 추가한도: - - 100억 이하: 수입금액 × 0.2% - - 100억~500억: 2,000만원 + (수입금액-100억) × 0.1% - - 500억 초과: 6,000만원 + (수입금액-500억) × 0.03% - -### 복리후생비 계산 -- 방식1 (직원당 정액): 직원수 × 월정액 × 12 -- 방식2 (연봉총액 비율): 연봉총액 × 비율% -- 법정 복리후생비: 4대보험 회사부담분 -- 비과세 항목별 기준: 식대 20만원, 교통비 10만원, 경조사 5만원, 건강검진 월환산, 교육훈련 8만원, 복지포인트 10만원 \ No newline at end of file diff --git a/claudedocs/architecture/[ANALYSIS-2026-03-06] account-subject-comparison.md b/claudedocs/architecture/[ANALYSIS-2026-03-06] account-subject-comparison.md deleted file mode 100644 index abf55a1b..00000000 --- a/claudedocs/architecture/[ANALYSIS-2026-03-06] account-subject-comparison.md +++ /dev/null @@ -1,281 +0,0 @@ -# 계정과목(Chart of Accounts) 현황 분석 및 일반 ERP 비교 - -> 작성일: 2026-03-06 -> 목적: 회계담당자 피드백 기반, 현재 시스템 vs 일반 ERP 계정과목 체계 비교 - ---- - -## 1. 회계담당자 요구사항 요약 - -| # | 요구사항 | 핵심 | -|---|---------|------| -| 1 | 계정과목을 통일해서 관리 | 하나의 마스터에서 전사적 관리 | -| 2 | 번호와 명칭으로 구분 | 코드 체계 필수 (예: 401-매출, 501-급여) | -| 3 | 제조/회계 동일 명칭이지만 번호가 다른 경우 존재 | 부문별 세분화 필요 | -| 4 | 등록하면 전체가 공유 + 개별등록도 가능 | 공통 + 부문별 계정 | - ---- - -## 2. 현재 시스템 계정과목 사용 현황 - -### 2.1 모듈별 계정과목 관리 방식 - -| 모듈 | 계정과목 소스 | 옵션 수 | 관리 방식 | API 필드명 | -|------|-------------|---------|----------|-----------| -| **일반전표입력** | DB 마스터 (account_codes) | 동적 | API CRUD | `account_subject_id` | -| **카드사용내역** | 프론트 하드코딩 | 16개 | 상수 배열 | `account_code` | -| **미지급비용** | 프론트 하드코딩 | 9개 | 상수 배열 | `account_code` | -| **매출관리** | 프론트 하드코딩 | 8개 | 상수 배열 | `account_code` | -| **입금관리** | 프론트 하드코딩 | ~11개 | depositType 상수 | `account_code` | -| **출금관리** | 프론트 하드코딩 | ~11개 | withdrawalType 상수 | `account_code` | -| **세금계산서관리** | 프론트 하드코딩 | 11개 | 상수 배열 (분개 모달) | `account_subject` | -| **CEO 대시보드** | 표시만 | - | account_title 표시 | `account_title` | - -### 2.2 핵심 문제점 - -``` -[문제 1] 계정과목 이원화 - 일반전표: DB 마스터 (code + name + category) ← 유일하게 정상 - 나머지: 프론트엔드 하드코딩 상수 배열 ← 각자 따로 관리 - -[문제 2] 코드 체계 불일치 - 일반전표: { code: "101", name: "현금", category: "asset" } - 카드내역: { value: "purchasePayment", label: "매입대금" } ← 영문 키워드 - 입금관리: { value: "salesRevenue", label: "매출수금" } ← 또 다른 영문 키워드 - -[문제 3] 옵션 중복 + 불일치 - "급여"가 카드내역(salary), 미지급비용(salary), 입출금(salary)에 각각 존재 - 세금계산서(분개)는 또 다른 옵션 세트 (매출, 부가세예수금 등) - 하지만 서로 독립적이라 추가/수정 시 각 파일 개별 수정 필요 - -[문제 4] 번호 체계 없음 - 카드내역의 "매입대금" = 코드 없이 "purchasePayment"라는 문자열만 존재 - 제조에서 쓰는 "재료비"와 회계에서 쓰는 "재료비"를 구분할 방법 없음 -``` - -### 2.3 백엔드 DB 구조 (현재) - -``` -account_codes 테이블 (일반전표 전용 마스터) -├── id (PK) -├── tenant_id (테넌트 격리) -├── code (varchar 10) ← 계정번호 -├── name (varchar 100) ← 계정명 -├── category (enum: asset/liability/capital/revenue/expense) -├── sort_order -├── is_active -├── created_at / updated_at -└── unique(tenant_id, code) - -journal_entry_lines (분개 상세) -├── account_code (varchar) ← 코드 저장 -├── account_name (varchar) ← 명칭 스냅샷 저장 -└── ... (side, amount 등) - -barobill_card_transactions (카드거래) -├── account_code (varchar) ← 문자열 직접 저장 ("purchasePayment" 등) -└── ... - -barobill_card_transaction_splits (카드 분개) -├── account_code (varchar) ← 문자열 직접 저장 -└── ... -``` - ---- - -## 3. 일반적인 ERP의 계정과목(Chart of Accounts) 체계 - -### 3.1 표준 구조 - -``` -[계정과목표 = Chart of Accounts] - -계정분류(대분류) -├── 1xxx: 자산 (Assets) -│ ├── 11xx: 유동자산 -│ │ ├── 1101: 현금 -│ │ ├── 1102: 보통예금 -│ │ ├── 1103: 당좌예금 -│ │ ├── 1110: 매출채권 -│ │ └── 1120: 선급금 -│ └── 12xx: 비유동자산 -│ ├── 1201: 토지 -│ ├── 1202: 건물 -│ └── 1210: 기계장치 -│ -├── 2xxx: 부채 (Liabilities) -│ ├── 21xx: 유동부채 -│ │ ├── 2101: 매입채무 -│ │ ├── 2102: 미지급금 -│ │ └── 2110: 예수금 -│ └── 22xx: 비유동부채 -│ -├── 3xxx: 자본 (Equity) -│ ├── 3101: 자본금 -│ └── 3201: 이익잉여금 -│ -├── 4xxx: 수익 (Revenue) -│ ├── 4101: 제품매출 -│ ├── 4102: 상품매출 -│ └── 4201: 임대수익 -│ -└── 5xxx: 비용 (Expenses) - ├── 51xx: 매출원가 - │ ├── 5101: 재료비 (제조) ← 코드로 구분! - │ └── 5102: 노무비 - ├── 52xx: 판매비와관리비 - │ ├── 5201: 급여 - │ ├── 5202: 복리후생비 - │ ├── 5203: 접대비 - │ ├── 5210: 재료비 (관리) ← 같은 명칭, 다른 코드! - │ └── 5220: 임차료 - └── 53xx: 영업외비용 - ├── 5301: 이자비용 - └── 5302: 외환차손 -``` - -### 3.2 일반 ERP 계정과목 마스터 구조 - -``` -account_subjects (계정과목 마스터) -├── id (PK) -├── code (varchar 10) ← "5101" 같은 번호 (4~6자리) -├── name (varchar 100) ← "재료비" -├── category (대분류) ← 자산/부채/자본/수익/비용 -├── sub_category (중분류) ← 유동자산/비유동자산/매출원가/판관비 등 -├── parent_code (상위 계정) ← 계층 구조용 -├── depth (계층 깊이) ← 1=대, 2=중, 3=소 -├── department_type (부문) ← 제조/관리/공통 등 -├── is_control (통제계정) ← 하위 세부계정 존재 여부 -├── is_active (사용여부) -├── sort_order -├── description (설명) -└── tenant_id -``` - -### 3.3 일반 ERP vs 현재 SAM ERP 비교 - -| 항목 | 일반 ERP | SAM ERP (현재) | 차이 | -|------|---------|---------------|------| -| **마스터 테이블** | 1개 (전사 공유) | 1개 있지만 일반전표만 사용 | 다른 모듈 미연동 | -| **코드 체계** | 4~6자리 숫자 (1101, 5201) | 일반전표만 code 있음, 나머지 영문 키워드 | 번호 체계 불통일 | -| **계층 구조** | 대-중-소 분류 (parent_code) | 대분류(5개)만 존재 | 중/소분류 없음 | -| **부문 구분** | department_type으로 제조/관리 분리 | 없음 | 제조vs회계 구분 불가 | -| **공유 범위** | 전 모듈이 같은 마스터 참조 | 각 모듈 독자 관리 | 핵심 문제 | -| **등록 방식** | 계정과목 설정 화면 1곳 | 일반전표 설정에서만 등록 | 접근성 제한 | -| **사용처 추적** | 어떤 전표에서 사용되는지 추적 | 없음 | 감사 추적 불가 | -| **잠금/보호** | 사용 중인 계정 삭제 방지 | 없음 | 데이터 무결성 위험 | - ---- - -## 4. 담당자 요구사항 vs 현재 시스템 GAP 분석 - -### 요구 1: "계정과목을 통일해서 관리" - -``` -현재 상태: - 일반전표 → account_codes 테이블 (DB) - 세금계산서 → ACCOUNT_SUBJECT_OPTIONS (프론트 상수, 11개) - 분개 모달 - 카드내역 → ACCOUNT_SUBJECT_OPTIONS (프론트 상수, 16개) - 미지급비용 → ACCOUNT_SUBJECT_OPTIONS (프론트 상수, 9개) - 매출관리 → ACCOUNT_SUBJECT_OPTIONS (프론트 상수, 8개) - 입금관리 → depositType 상수 - 출금관리 → withdrawalType 상수 - -필요한 것: - 모든 모듈 → account_codes 테이블 (DB) 하나만 참조 - -GAP: 크다 (프론트 하드코딩 → DB 마스터 참조로 전환 필요) -``` - -### 요구 2: "번호와 명칭으로 구분" - -``` -현재 상태: - 일반전표: code="101", name="현금" ← 있음 - 카드내역: value="salary", label="급여" ← 영문 키워드, 번호 없음 - -필요한 것: - 모든 곳에서: code="5201", name="급여" 형태로 표시 - UI에서: "5201 - 급여" 또는 "[5201] 급여" 식으로 코드+명칭 동시 표시 - -GAP: 중간 (코드 체계는 DB에 이미 있으나, 다른 모듈이 참조하지 않음) -``` - -### 요구 3: "제조/회계 동일 명칭, 번호로 구분" - -``` -현재 상태: - 구분 불가. "재료비"가 제조인지 관리인지 알 방법 없음 - -필요한 것: - 5101: 재료비 (제조 - 매출원가) - 5210: 재료비 (판관비 - 관리비용) - → 코드가 다르므로 자동 구분 - -GAP: 크다 (중분류 + 부문 구분 필드 추가 필요) -``` - -### 요구 4: "전체 공유 + 개별 등록 가능" - -``` -현재 상태: - 일반전표 설정에서만 등록 가능. 다른 모듈은 하드코딩이라 등록 개념 없음. - -필요한 것: - - 기본 계정과목표 (회사 설정 시 일괄 생성) - - 추가 등록 (필요에 따라 개별 계정과목 추가) - - 전 모듈 공유 (등록 즉시 카드, 입출금, 세금계산서 등에서 사용 가능) - -GAP: 중간 (DB 마스터는 있으니, 다른 모듈이 참조하도록 연결만 하면 됨) -``` - ---- - -## 5. 결론 및 권장사항 - -### 5.1 담당자 말씀이 맞는가? - -**맞습니다.** 일반적인 ERP에서 계정과목은 반드시: -- 하나의 마스터(Chart of Accounts)로 전사 통합 관리 -- 숫자 코드 + 명칭으로 식별 (코드가 PK 역할) -- 코드 번호로 계정 분류/부문 구분 (제조 5101 vs 관리 5210) -- 한 번 등록하면 모든 회계 모듈에서 공유 - -현재 SAM ERP는 일반전표에만 정상적인 마스터가 있고, 나머지는 각자 하드코딩이므로 -**회계적으로 올바르지 않은 상태**입니다. - -### 5.2 개선 방향 (단계별) - -``` -[Phase 1] 계정과목 마스터 강화 (백엔드) - - account_codes 테이블에 sub_category, parent_code, depth, department_type 추가 - - 표준 계정과목표 시드 데이터 준비 (대/중/소 분류) - - 코드 체계 확정 (4자리 vs 6자리) - -[Phase 2] 계정과목 설정 화면 독립 (프론트) - - 일반전표 내부 모달 → 독립 메뉴로 분리 (회계 > 계정과목 설정) - - 계층 구조 표시 (트리뷰 또는 들여쓰기 목록) - - 대량 등록 (Excel import), 기본 계정과목표 초기 세팅 - -[Phase 3] 전 모듈 통합 (프론트 + 백엔드) - - 세금계산서관리: ACCOUNT_SUBJECT_OPTIONS 상수 (11개) → DB 마스터 API 호출로 전환 - - 카드사용내역: ACCOUNT_SUBJECT_OPTIONS 상수 (16개) → DB 마스터 API 호출로 전환 - - 입금/출금관리: depositType/withdrawalType → DB 마스터 참조로 전환 - - 미지급비용, 매출관리: 동일하게 전환 - - Select UI에 "코드 - 명칭" 형태로 표시 (예: "[5201] 급여") - -[Phase 4] 고급 기능 - - 사용중 계정 삭제 방지 (참조 무결성) - - 계정과목별 거래 내역 조회 - - 기간별 잔액 집계 -``` - -### 5.3 작업 규모 예상 - -| Phase | 범위 | 핵심 변경 | -|-------|------|----------| -| 1 | 백엔드 마이그레이션 + 시드 | account_codes 테이블 확장, 시드 데이터 | -| 2 | 프론트 1개 페이지 신규 | 계정과목 설정 독립 페이지 | -| 3 | 프론트 6~7개 모듈 수정, 백엔드 API 조정 | 하드코딩 → API 참조 전환 | -| 4 | 양쪽 추가 개발 | 무결성, 집계, 조회 | diff --git a/claudedocs/architecture/[ANALYSIS-2026-03-13] mes-data-integrity-report-v2.md b/claudedocs/architecture/[ANALYSIS-2026-03-13] mes-data-integrity-report-v2.md deleted file mode 100644 index f6c04dca..00000000 --- a/claudedocs/architecture/[ANALYSIS-2026-03-13] mes-data-integrity-report-v2.md +++ /dev/null @@ -1,498 +0,0 @@ -# MES 데이터 정합성 심층 분석 보고서 v2 - -**분석일**: 2026-03-13 (v2 - 코드 업데이트 반영) -**범위**: 수주(Order) → 생산(Production) → 품질(Quality) → 출고(Shipment) 전체 파이프라인 -**방법**: 프론트엔드(sam-react-prod) + 백엔드(sam-api) 코드 레벨 재분석 - ---- - -## v1 대비 변경사항 요약 - -| 항목 | v1 (초기 분석) | v2 (코드 업데이트 반영) | -|------|---------------|----------------------| -| StockLot.work_order_id FK | 확인 안됨 | ✅ 2026-02-21 추가 확인 (생산→재고 연결 기반 마련) | -| QualityDocument 시스템 | 존재 인지 | ✅ 2026-03-05~10 활발히 개선 중 (inspection_data, options JSON 추가) | -| 출하 자동생성 | 언급 | ✅ 상세 분석 완료: createShipmentFromOrder() 중복방지 + ensureShipmentExists() | -| 3월 MES FK 추가 | 미확인 | ❌ 3월 마이그레이션에 MES FK 추가 없음 확인 | -| 나머지 4개 이슈 | 발견 | 🔴 여전히 미해결 (can_ship, LOT, ShipmentItem FK) | - ---- - -## Executive Summary - -| # | 이슈 | 심각도 | v1 판정 | v2 판정 | 변경 | -|---|------|--------|---------|---------|------| -| 1 | 생산완료→수주 PRODUCED 자동전환 | 🟡→🟢 | 조건부 동작 | **정상 동작 + 자동출하 생성** | ⬆ 개선 | -| 2 | 품질검사 이중 시스템 | 🔴 | 구조적 문제 | 🔴 **구조적 문제 지속** (QualityDocument 활발 개발 중이나 출고 연동 미완) | 유지 | -| 3 | 출고 시 can_ship 검증 | 🔴 | 누락 | 🔴 **여전히 누락** (canProceedToShip 호출 0회) | 유지 | -| 4 | 출고 시 재고 차감 | ✅ | 구현됨 | ✅ **구현됨**, ⚠️ soft fail 리스크 유지 | 유지 | -| 5 | LOT 추적 체계 | 🔴 | 단절 | 🟡 **부분 개선** (StockLot.work_order_id FK 추가, 그러나 LOT 전달 로직 미구현) | ⬆ 부분 | -| 6 | 출고품목↔수주품목 FK | 🔴 | 없음 | 🔴 **여전히 없음** (3월 마이그레이션에도 미추가) | 유지 | - ---- - -## 이슈 1: 생산완료 → 수주 상태 자동전환 + 출하 자동생성 - -### v2 판정: 🟢 정상 동작 (v1 대비 상향) - -v2 분석에서 자동 출하 생성 로직까지 상세 확인 완료. **정상 동작 확인**. - -### 전체 흐름 - -``` -WorkOrder 상태 변경 (updateStatus) - ↓ -syncOrderStatus() 자동 호출 (L971-1059) - ↓ -메인 WO 필터링: is_auxiliary=false AND process_id≠null - ↓ -전체 완료 시 → Order.status = PRODUCED - ↓ -createShipmentFromOrder() 자동 호출 (L719-809) - ↓ -Shipment 생성: status='scheduled', can_ship=true(자동) - ↓ -기존 Shipment 있으면 → 중복 생성 방지 (L721-728) -``` - -### 코드 근거 - -**syncOrderStatus**: `WorkOrderService.php:971-1059` - -```php -// L989-995: 메인 WO 필터 (보조공정 + process_id=null 제외) -$mainWorkOrders = $allWorkOrders->filter(fn ($wo) => - !$this->isAuxiliaryWorkOrder($wo) && $wo->process_id !== null -); - -// L1001-1019: 상태 결정 -if ($shippedCount === $totalCount) { - $newOrderStatus = Order::STATUS_SHIPPED; -} elseif (($completedCount + $shippedCount) === $totalCount) { - $newOrderStatus = Order::STATUS_PRODUCED; -} -``` - -**createShipmentFromOrder**: `WorkOrderService.php:719-809` - -```php -// L721-728: 중복 방지 -$existingShipment = Shipment::where('order_id', $order->id)->first(); -if ($existingShipment) return $existingShipment; - -// L732-744: 출하 자동 생성 -$shipment = Shipment::create([ - 'order_id' => $order->id, - 'work_order_id' => null, // 수주 레벨 (WO 레벨 아님) - 'status' => 'scheduled', - 'can_ship' => true, // ← 자동으로 true 설정 -]); - -// L746-790: WO 아이템 → ShipmentItem 복사 -``` - -**ensureShipmentExists**: 이미 PRODUCED인데 출하가 없는 경우 보완 (L1027-1033) - -### 잔존 리스크 (낮음) - -| 조건 | 원인 | 발생 가능성 | -|------|------|------------| -| `process_id = NULL`인 WO | 공정 매핑 실패 | 낮음 (생성 시 검증됨) | -| `is_auxiliary` 오설정 | options JSON 수동 수정 | 매우 낮음 | - -### 회의 논의 포인트 -- ✅ 이 부분은 정상 동작 확인됨. 추가 조치 불필요 -- (선택) process_id=null WO가 실데이터에 존재하는지 한번 쿼리 확인 - ---- - -## 이슈 2: 품질검사 이중 시스템 - -### v2 판정: 🔴 구조적 문제 지속 (QualityDocument 활발 개발 중이나 출고 연동은 미완) - -### v1 대비 변화 - -| 변경 사항 | 시기 | 내용 | -|-----------|------|------| -| `quality_document_locations.inspection_data` JSON 추가 | 2026-03-06 | 개소별 검사 데이터 저장 | -| `quality_document_locations.options` JSON 추가 | 2026-03-10 | 검사 옵션 확장 | -| QualityDocumentService 개선 | 2026-03 | inspectLocation() 등 기능 확장 | - -### 여전히 해결 안 된 핵심 문제 - -``` -QualityDocument.complete() 호출 시: - → inspection_status = 'completed' (QualityDocument 내부만 업데이트) - → ❌ Shipment.can_ship 업데이트 없음 - → ❌ Inspection 테이블 동기화 없음 -``` - -**두 시스템 현재 상태**: - -| 항목 | 경로A: Inspection | 경로B: QualityDocument | -|------|-------------------|----------------------| -| **테이블** | `inspections` | `quality_documents` + `quality_document_locations` | -| **FK 연결** | `work_order_id` (작업지시) | `order_id` (수주) + `order_item_id` (개소) | -| **3월 업데이트** | 변경 없음 | ✅ 활발히 개선 중 | -| **출고 참조** | ❌ 안됨 | ❌ 안됨 | - -### 회의 논의 포인트 -- QualityDocument가 활발히 개발 중 → **경로B를 표준으로 확정하는 것이 합리적** -- 품질 완료 시 Shipment.can_ship 자동 업데이트 연동 필요 -- 경로A(Inspection)는 IQC/PQC 전용으로 역할 한정, FQC는 경로B로 통일 - ---- - -## 이슈 3: 출고 시 can_ship 검증 누락 - -### v2 판정: 🔴 여전히 미해결 (canProceedToShip() 호출 0회 확인) - -### 코드 현황 (변경 없음) - -**canProceedToShip()**: `Shipment.php:220-223` — 정의만 존재 - -```php -public function canProceedToShip(): bool { - return $this->can_ship && $this->deposit_confirmed; -} -// grep 결과: 모델 정의 외 호출 0회 -``` - -**updateStatus()**: `ShipmentService.php:305-356` — can_ship 검증 없이 바로 업데이트 - -```php -public function updateStatus(int $id, string $status, ?array $additionalData = null): Shipment -{ - $shipment = Shipment::findOrFail($id); - // 🔴 can_ship 검증 없음 - $shipment->update(['status' => $status, ...]); -} -``` - -**프론트엔드**: `ShipmentDetail.tsx:304-314` — can_ship 무시하고 버튼 표시 - -```typescript -const STATUS_TRANSITIONS: Record = { - scheduled: 'ready', ready: 'shipping', shipping: 'completed', completed: null, -}; -// can_ship=false여도 상태 변경 버튼 표시됨 -``` - -### v2 신규 발견: 자동 출하에서 can_ship=true 자동 설정 - -```php -// createShipmentFromOrder (L732-744) -'can_ship' => true, // 자동 생성 시 무조건 true -``` - -→ 자동 생성된 출하는 can_ship=true이므로 문제 경감 -→ **그러나** 수동 생성 출하에서는 여전히 검증 없음 - -### 위험 시나리오 - -``` -수동 출하 생성 (can_ship=false) - → 사용자가 "출하대기" 클릭 → 검증 없이 ready - → "배송중" → "배송완료" → 재고 차감 시도 - → 재고 부족 시 soft fail (로그만, 상태는 completed) ❌ -``` - -### 수정안 (최소 변경) - -**백엔드** (1곳 수정): -```php -// ShipmentService::updateStatus() 시작부에 추가 -if (in_array($status, ['ready', 'shipping', 'completed']) && !$shipment->can_ship) { - throw new \Exception('출하 불가 상태입니다. 품질 검수를 완료해주세요.'); -} -``` - -**프론트엔드** (1곳 수정): -```typescript -// ShipmentDetail.tsx 버튼 표시 조건 -{STATUS_TRANSITIONS[detail.status] && detail.canShip && ( - -)} -``` - ---- - -## 이슈 4: 출고 시 재고 차감 - -### v2 판정: ✅ 구현됨, ⚠️ Soft Fail 리스크 유지 (변경 없음) - -**코드**: `ShipmentService.php:361-401` - -```php -private function decreaseStockForShipment(Shipment $shipment): void -{ - foreach ($items as $item) { - try { - $stockService->decreaseForShipment(...); - } catch (\Exception $e) { - // 🟡 SOFT FAIL: 로그만 기록, 출하 상태는 completed 유지 - Log::warning('Failed to decrease stock', [...]); - // throw 없음 → 다음 아이템으로 계속 - } - } -} -``` - -### 회의 논의 포인트 -- **Hard Fail 전환 여부**: `throw`로 변경하면 하나라도 실패 시 출하 전체 롤백 -- **현재 방식 장점**: 일부 품목 재고 부족해도 출하는 진행 가능 -- **권장**: 최소한 재고 차감 실패 건수를 프론트에 표시 + 관리자 알림 - ---- - -## 이슈 5: LOT 추적 체계 - -### v2 판정: 🟡 부분 개선 (v1 🔴 → v2 🟡) - -### v1 대비 개선 사항 - -| 개선 | 시기 | 코드 근거 | -|------|------|-----------| -| `stock_lots.work_order_id` FK 추가 | 2026-02-21 | 마이그레이션 확인 | -| `inspections.work_order_id` FK 추가 | 2026-02-27 | 마이그레이션 확인 | - -→ 재고↔생산, 검사↔생산 연결 **기반은 마련됨** - -### 여전히 해결 안 된 핵심 문제 - -**1. 프론트에서 LOT 생성 → 백엔드 전송 안 됨** - -```typescript -// WorkerScreen/actions.ts:246 — 프론트에서만 LOT 생성 -const lotNo = `KD-SA-${new Date().toISOString().slice(2, 10).replace(/-/g, '')}-01`; -// ← 이 값이 API 요청 body에 포함되지 않음 -``` - -**2. 백엔드 LOT 저장 로직 없음** - -```php -// WorkOrderService.php:578-583 -case WorkOrder::STATUS_COMPLETED: - $workOrder->completed_at = now(); - $this->saveItemResults($workOrder, $resultData, $userId); - // ❌ LOT 자동 채번/저장 로직 없음 - break; -``` - -**3. 생산입고 시 LOT 전달 실패** - -```php -// WorkOrderService.php:620-637 -private function stockInFromProduction(WorkOrder $workOrder): void { - foreach ($workOrder->items as $woItem) { - $lotNo = $woItem->options['result']['lot_no'] ?? ''; // ← 항상 빈값 - if ($goodQty > 0 && $lotNo) { // ← 조건 불충족 → 실행 안됨 - $this->stockService->increaseFromProduction(...); - } - } -} -``` - -→ **StockLot.work_order_id FK는 추가됐지만, 실제 LOT를 생성/저장하는 코드가 없어서 FK가 활용되지 않음** - -### LOT 추적 현황 (업데이트) - -``` -수주 KD-TS-260313-01 - → 생산 완료 (LOT 미생성 ❌ — 프론트에서만 생성, 백엔드 저장 안됨) - → 재고 입고 (LOT 전달 실패 ❌ — stockInFromProduction 조건 불충족) - → [신규] StockLot.work_order_id FK 존재 (✅ 기반 마련) - → 품질검사 (별도 LOT 입력 ⚠️) - → 출고 (자재 LOT만 선택 가능 ❌, 생산 LOT 없음) -``` - -### 수정 방향 (StockLot.work_order_id 활용) - -```php -// 1. 백엔드에서 LOT 자동 채번 (WorkOrderService) -$lotNo = $this->numberingService->generate('production-lot', $tenantId); - -// 2. saveItemResults()에서 lot_no 저장 -$woItem->options = array_merge($woItem->options, ['result' => ['lot_no' => $lotNo]]); - -// 3. stockInFromProduction()에서 정상 동작 → StockLot 생성 시 work_order_id 연결 -$this->stockService->increaseFromProduction( - lotNo: $lotNo, - workOrderId: $workOrder->id // ← 이미 FK 존재 -); -``` - ---- - -## 이슈 6: 출고품목 ↔ 수주품목 FK 부재 - -### v2 판정: 🔴 여전히 미해결 (3월 마이그레이션에도 미추가 확인) - -### ShipmentItem 실제 컬럼 (변경 없음) - -``` -id, tenant_id, shipment_id(FK), seq, -item_code, item_name, floor_unit, specification, -quantity, unit, lot_no, stock_lot_id(index only), remarks -``` - -- ❌ `order_item_id` → 없음 -- ❌ `work_order_item_id` → 없음 - -### 3월 마이그레이션 확인 결과 - -3월에 추가된 마이그레이션 중 `shipment_items` 관련 변경 **0건**. -주요 3월 마이그레이션은 QualityDocument 관련 (`inspection_data`, `options` JSON 추가)에 집중. - -### 추적 불가 질문들 (여전히) - -| 질문 | 답변 가능 여부 | -|------|--------------| -| "출고 #1234에서 어떤 수주 품목이 출고됐나?" | ❌ 불가 | -| "수주 품목 #999는 어느 출고에서 출고됐나?" | ❌ 역추적 불가 | -| "수주 10개 품목 중 미출고 품목은?" | ❌ 집계 불가 | -| "부분 출고 진행률은?" | ❌ 계산 불가 | - -### 자동 출하 생성 시 연결 기회 놓침 - -```php -// createShipmentFromOrder (L746-790) -// WO 아이템을 ShipmentItem으로 복사하면서 source 정보 저장 안 함 -ShipmentItem::create([ - 'shipment_id' => $shipment->id, - 'item_code' => $woItem->item_id ? "ITEM-{$woItem->item_id}" : null, - 'quantity' => $result['good_qty'] ?? $woItem->quantity, - // ❌ 'order_item_id' => $woItem->source_order_item_id ← 이것만 추가하면 됨 - // ❌ 'work_order_item_id' => $woItem->id ← 이것만 추가하면 됨 -]); -``` - -### 수정안 - -**마이그레이션** (새 파일): -```php -Schema::table('shipment_items', function (Blueprint $table) { - $table->unsignedBigInteger('order_item_id')->nullable()->after('stock_lot_id'); - $table->unsignedBigInteger('work_order_item_id')->nullable()->after('order_item_id'); - $table->foreign('order_item_id')->references('id')->on('order_items')->nullOnDelete(); - $table->foreign('work_order_item_id')->references('id')->on('work_order_items')->nullOnDelete(); -}); -``` - -**createShipmentFromOrder** (2줄 추가): -```php -ShipmentItem::create([ - ...기존 필드, - 'order_item_id' => $woItem->source_order_item_id, // 추가 - 'work_order_item_id' => $woItem->id, // 추가 -]); -``` - ---- - -## 전체 FK 연결 현황도 (v2 업데이트) - -``` -orders ──────────────────── order_items ──────── order_nodes - │ (order_id FK) │ (order_node_id FK) │ - │ │ │ - ├─── work_orders │ │ - │ │ (sales_order_id FK) │ │ - │ │ │ │ - │ └─── work_order_items │ │ - │ │ │ │ - │ │ source_order_item_id ──→ ❌ FK 없음 (인덱스만) - │ │ │ - │ inspections │ - │ │ (work_order_id FK ✅) [2026-02-27 추가] │ - │ │ (lot_no ← 연결 안됨 ❌) │ - │ │ - │ stock_lots │ - │ │ (work_order_id FK ✅) [2026-02-21 추가] ← 🆕 v1에서 미확인 - │ │ - ├─── quality_document_orders ──→ quality_documents │ - │ │ (order_id FK ✅) │ - │ │ │ - │ └─── quality_document_locations │ - │ │ (order_item_id FK ✅) │ - │ │ (inspection_data JSON 🆕 2026-03-06) │ - │ │ (options JSON 🆕 2026-03-10) │ - │ │ - └─── shipments │ - │ (order_id FK ✅, work_order_id FK ✅) │ - │ │ - └─── shipment_items │ - │ (shipment_id FK ✅) │ - │ (stock_lot_id → 인덱스만, FK 없음) │ - │ (order_item_id ❌ 컬럼 없음) │ - │ (work_order_item_id ❌ 컬럼 없음) │ -``` - ---- - -## 개선 우선순위 로드맵 (v2 업데이트) - -### P0 (즉시 - 운영 리스크) — 변경 없음 - -| # | 작업 | 수정 범위 | 난이도 | -|---|------|---------|--------| -| 1 | **can_ship 검증 추가** | ShipmentService::updateStatus() 1곳 + ShipmentDetail.tsx 1곳 | 하 (수정 3줄) | -| 2 | **재고 차감 실패 알림** | ShipmentService::decreaseStockForShipment() → 최소 결과 반환 | 하 | - -### P1 (단기 - 데이터 정합성) — 🆕 StockLot.work_order_id 활용 추가 - -| # | 작업 | 수정 범위 | 난이도 | -|---|------|---------|--------| -| 3 | **생산 LOT 백엔드 자동 채번** | WorkOrderService::saveItemResults() + NumberingService | 중 | -| 4 | **생산입고 LOT 연결** | WorkOrderService::stockInFromProduction() → StockLot.work_order_id 활용 | 중 | -| 5 | **shipment_items에 order_item_id 추가** | 마이그레이션 + createShipmentFromOrder() 2줄 추가 | 중 | - -### P2 (중기 - 구조 개선) — 🆕 QualityDocument 기반 통합 명시 - -| # | 작업 | 수정 범위 | 난이도 | -|---|------|---------|--------| -| 6 | **품질검사 정본 = QualityDocument** | Inspection은 IQC/PQC 전용, FQC는 QualityDocument로 통일 | 상 | -| 7 | **품질완료 → can_ship 자동 연동** | QualityDocumentService::complete() → Shipment.can_ship 업데이트 | 중 | -| 8 | **work_order_items.source_order_item_id FK** | 마이그레이션 1줄 | 하 | -| 9 | **stock_lot_id FK constraint 추가** | shipment_items 마이그레이션 | 하 | - ---- - -## 정상 동작 확인 항목 (v2) - -- ✅ 수주 → 생산지시 생성 (공정별 자동 분류) -- ✅ 작업지시 상태 관리 (유효 상태 전환 + auxiliary 필터링) -- ✅ **syncOrderStatus()**: 메인 WO 완료 → Order PRODUCED 자동 전환 -- ✅ **createShipmentFromOrder()**: PRODUCED 전환 시 출하 자동 생성 (중복 방지 포함) -- ✅ **ensureShipmentExists()**: 이미 PRODUCED인데 출하 없는 경우 보완 -- ✅ 자재 투입 재고 차감 (WorkOrderMaterialInput + StockService) -- ✅ 출고 완료 시 재고 차감 (FIFO + lockForUpdate + stock_transactions) -- ✅ 출고 완료 → 수주 상태 SHIPPED 자동 전환 -- ✅ 매출 자동 생성 (sales_recognition 조건부) -- ✅ 수주 상태별 수정/삭제 제한 -- ✅ 생산지시 되돌리기 (WorkOrder/Item/Result 삭제) -- 🆕 ✅ StockLot.work_order_id FK (생산→재고 연결 기반) -- 🆕 ✅ Inspection.work_order_id FK (검사→생산 연결) - ---- - -## 회의 토론 안건 정리 - -### 즉시 결정 필요 (P0) - -1. **can_ship 검증**: 백엔드 1줄 + 프론트 1줄 수정으로 해결 가능. 즉시 적용? -2. **재고 차감 실패 처리**: Hard fail(롤백) vs Soft fail(현행) + 알림 추가? - -### 설계 방향 결정 필요 (P1) - -3. **LOT 채번 규칙**: 생산 LOT 형식 결정 (현재 프론트: `KD-SA-YYMMDD-NN`) -4. **생산 LOT 생성 시점**: WO 완료 시? WO 생성 시? 첫 작업 보고 시? -5. **ShipmentItem FK**: 마이그레이션 타이밍 (기존 데이터 소급 매칭 필요?) - -### 방향성 논의 (P2) - -6. **품질 시스템 정본**: QualityDocument를 표준으로 확정하는 것에 이견 있는지? -7. **품질→출하 자동 연동**: 어떤 조건에서 can_ship=true로 전환할 것인지? - - 전체 개소(location) 검사 완료 시? - - 합격률 기준? - - 수동 최종 승인 필요? diff --git a/claudedocs/architecture/[ANALYSIS-2026-03-13] mes-data-integrity-report.md b/claudedocs/architecture/[ANALYSIS-2026-03-13] mes-data-integrity-report.md deleted file mode 100644 index 51a3a764..00000000 --- a/claudedocs/architecture/[ANALYSIS-2026-03-13] mes-data-integrity-report.md +++ /dev/null @@ -1,421 +0,0 @@ -# MES 데이터 정합성 심층 분석 보고서 - -**분석일**: 2026-03-13 -**범위**: 수주(Order) → 생산(Production) → 품질(Quality) → 출고(Shipment) 전체 파이프라인 -**방법**: 프론트엔드(sam-react-prod) + 백엔드(sam-api) 코드 레벨 분석 - ---- - -## Executive Summary - -| # | 이슈 | 심각도 | 현황 | 코드 근거 | -|---|------|--------|------|-----------| -| 1 | 생산완료→수주 PRODUCED 자동전환 | 🟡 조건부 동작 | 로직 있으나 edge case에서 실패 가능 | `WorkOrderService.php:974-1062` | -| 2 | 품질검사 이중 시스템 | 🔴 구조적 문제 | Inspection vs QualityDocument 분리, 출고 연동 없음 | 양쪽 모두 Shipment 참조 안함 | -| 3 | 출고 시 can_ship 검증 | 🔴 누락 | canProceedToShip() 정의만 있고 호출 0회 | `ShipmentService.php:305-356` | -| 4 | 출고 시 재고 차감 | ✅ 구현됨 | completed 전환 시 FIFO 자동 차감 | `ShipmentService.php:361-401` | -| 5 | LOT 추적 체계 | 🔴 단절 | 프론트에서만 LOT 생성, 백엔드 저장 안됨 | `WorkerScreen/actions.ts:246` | -| 6 | 출고품목↔수주품목 FK | 🔴 없음 | ShipmentItem에 order_item_id 컬럼 자체 부재 | `shipment_items 마이그레이션` | - ---- - -## 이슈 1: 생산완료 → 수주 상태 자동전환 - -### 결론: ✅ 로직 있음, 🟡 조건부 실패 가능 - -### 동작 원리 - -``` -WorkOrder 상태 변경 (updateStatus) - ↓ (라인 603) -syncOrderStatus() 자동 호출 - ↓ (라인 1004-1022) -메인 작업지시 집계 → 조건 충족 시 Order.status = PRODUCED - ↓ (라인 1059-1061) -PRODUCED 전환 시 → 출고(Shipment) 자동 생성 -``` - -**코드**: `sam-api/app/Services/WorkOrderService.php` - -```php -// 라인 998: 메인 작업지시 필터 -$mainWorkOrders = $allWorkOrders->filter(fn ($wo) => - !$this->isAuxiliaryWorkOrder($wo) && $wo->process_id !== null -); - -// 라인 1011-1022: 상태 결정 -if ($shippedCount === $totalCount) { - $newOrderStatus = Order::STATUS_SHIPPED; -} elseif (($completedCount + $shippedCount) === $totalCount) { - $newOrderStatus = Order::STATUS_PRODUCED; // ← 핵심 조건 -} elseif ($inProgressCount > 0 || $completedCount > 0 || $shippedCount > 0) { - $newOrderStatus = Order::STATUS_IN_PRODUCTION; -} -``` - -### 실패 가능 조건 - -| 조건 | 원인 | 영향 | -|------|------|------| -| `process_id = NULL`인 WO 존재 | 공정 매핑 실패로 생성된 작업지시 | 메인 WO 카운트에서 제외 → 조건식 계산 오류 | -| `is_auxiliary = true` 오설정 | options JSON에 잘못 저장 | 메인 WO로 인식 안 됨 | - -### 검증 SQL - -```sql --- 해당 수주의 작업지시 현황 확인 -SELECT id, work_order_no, status, process_id, - JSON_EXTRACT(options, '$.is_auxiliary') as is_auxiliary -FROM work_orders -WHERE sales_order_id = {order_id} AND status != 'cancelled'; -``` - -### 회의 논의 포인트 -- process_id=null인 작업지시가 실제로 존재하는지 DB 확인 필요 -- 존재한다면 → 생산지시 생성 시 process_id null 방지 로직 추가 - ---- - -## 이슈 2: 품질검사 이중 시스템 - -### 결론: 🔴 두 시스템이 독립 운영, 출고와 연동 없음 - -### 두 시스템 비교 - -| 항목 | 경로A: Inspection | 경로B: QualityDocument | -|------|-------------------|----------------------| -| **테이블** | `inspections` | `quality_documents` + `quality_document_locations` | -| **생성일** | 2025-12-29 | 2026-03-05 (최근 추가) | -| **연결 키** | `work_order_id` (작업지시) | `order_id` (수주) + `order_item_id` (개소) | -| **판정 필드** | `result: pass/fail` | `inspection_status: pending/completed` | -| **검사 단위** | 전체 건 | 개소(location)별 | -| **프론트 진입점** | 검사 메뉴 | 제품검사 메뉴 | -| **FQC 문서** | JSON items 배열 | Document 시스템 (EAV) | -| **출고 참조** | ❌ 안됨 | ❌ 안됨 | - -### 핵심 문제: 출고에서 둘 다 참조 안함 - -**코드**: `sam-api/app/Services/ShipmentService.php` - -```php -// 라인 207: 출고 생성 시 -'can_ship' => $data['can_ship'] ?? false, // ← 수동 입력만, 품질 검사 결과 참조 없음 - -// 라인 220-223: 출고 가능 여부 메서드 -public function canProceedToShip(): bool { - return $this->can_ship && $this->deposit_confirmed; - // ❌ Inspection.result 참조 없음 - // ❌ QualityDocumentLocation.inspection_status 참조 없음 -} -``` - -### 프론트엔드 판정 우선순위 - -**코드**: `src/components/quality/InspectionManagement/InspectionDetail.tsx` - -``` -경로B (QualityDocument/FQC 문서) 우선 → 경로A (Inspection) fallback -``` - -### 회의 논의 포인트 -- **정본 결정 필요**: 경로A(Inspection) vs 경로B(QualityDocument) 중 하나를 표준으로 -- 경로B가 최근(3월) 추가된 것 → 경로B를 표준으로 하고 경로A는 호환 레이어? -- 출고 시 품질 판정 자동 참조 로직 추가 필수 - ---- - -## 이슈 3: 출고 시 can_ship 검증 누락 - -### 결론: 🔴 canProceedToShip() 메서드 정의만 있고, 실제 호출 0회 - -### 현재 상태 변경 코드 - -**코드**: `sam-api/app/Services/ShipmentService.php:305-356` - -```php -public function updateStatus(int $id, string $status, ?array $additionalData = null): Shipment -{ - $shipment = Shipment::where('tenant_id', $tenantId)->findOrFail($id); - - // 🔴 can_ship 검증 로직 전혀 없음 - - $shipment->update(['status' => $status, ...]); // ← 바로 업데이트 - - // completed 시 재고 차감 (이것은 동작함) - if ($status === 'completed' && $previousStatus !== 'completed') { - $this->decreaseStockForShipment($shipment); - } -} -``` - -### 프론트엔드도 미검증 - -**코드**: `src/components/outbound/ShipmentManagement/ShipmentDetail.tsx:304-314` - -```typescript -// 상태 전이 맵만 확인, canShip 체크 없음 -const STATUS_TRANSITIONS: Record = { - scheduled: 'ready', - ready: 'shipping', - shipping: 'completed', - completed: null, -}; - -// can_ship=false여도 버튼이 표시됨 ❌ -{STATUS_TRANSITIONS[detail.status] && ( - -)} -``` - -### 위험 시나리오 - -``` -can_ship=false (품질 미통과) + status=scheduled - → 사용자가 "출하대기로 변경" 클릭 - → 백엔드 검증 없음 → status='ready' ❌ - → "배송중" → "배송완료" → 재고 차감 시도 - → 재고 부족 시 soft fail (로그만 기록, 상태는 변경됨) ❌ -``` - -### 회의 논의 포인트 -- 백엔드: `updateStatus()`에 `can_ship` 검증 추가 (1줄 수정) -- 프론트: 버튼 표시 조건에 `detail.canShip` 추가 -- 재고 차감 실패 시 hard fail로 변경할지 논의 필요 - ---- - -## 이슈 4: 출고 시 재고 차감 - -### 결론: ✅ 완전 구현됨 - -**코드**: `sam-api/app/Services/ShipmentService.php:347-350` - -```php -if ($status === 'completed' && $previousStatus !== 'completed') { - $this->decreaseStockForShipment($shipment); -} -``` - -**StockService FIFO 차감**: `StockService.php:1236-1354` -- Stock 행 잠금 (lockForUpdate) -- LOT별 FIFO 순서 차감 -- stock_transactions 거래 기록 (reason: SHIPMENT) -- 감사 로그 기록 - -**⚠️ 주의**: 개별 품목 차감 실패 시 soft fail (로그만 기록, 트랜잭션 미롤백) - ---- - -## 이슈 5: LOT 추적 체계 단절 - -### 결론: 🔴 4개 모듈이 완전 독립적 LOT 관리, 추적 불가 - -### LOT 생성/관리 현황 - -| 모듈 | LOT 형식 | 생성 위치 | 저장 위치 | 상태 | -|------|----------|-----------|-----------|------| -| **수주** | - | - | Order에 lot_no 필드 없음 | ❌ 필드 없음 | -| **생산** | `KD-SA-YYMMDD-NN` | 프론트 `WorkerScreen/actions.ts:246` | ❌ 백엔드 전송 안됨 | ❌ 저장 안됨 | -| **자재** | 입고 시 생성 | `StockService` | `stock_lots.lot_no` | ✅ 동작 | -| **품질** | 검사팀 별도 입력 | `InspectionService` | `inspections.lot_no` | ⚠️ 연결 없음 | -| **출고** | StockLot에서 선택 | `ShipmentService:getLotOptions()` | `shipments.lot_no` | ⚠️ 자재 LOT만 | - -### 핵심 단절 코드 - -**프론트에서 LOT 생성하지만 전송 안 함**: - -```typescript -// WorkerScreen/actions.ts:246 -const lotNo = `KD-SA-${new Date().toISOString().slice(2, 10).replace(/-/g, '')}-01`; -// ← 이 값이 API 요청에 포함되지 않음 -``` - -**백엔드에서 LOT 저장 안 함**: - -```php -// WorkOrderService.php:578-583 -case WorkOrder::STATUS_COMPLETED: - $workOrder->completed_at = now(); - $this->saveItemResults($workOrder, $resultData, $userId); - // ❌ LOT 생성/저장 로직 없음 - break; -``` - -**생산입고 시 LOT 전달 실패**: - -```php -// WorkOrderService.php:620-637 -private function stockInFromProduction(WorkOrder $workOrder): void { - foreach ($workOrder->items as $woItem) { - $lotNo = $woItem->options['result']['lot_no'] ?? ''; // ← 항상 빈값 - if ($goodQty > 0 && $lotNo) { // ← 조건 불충족으로 실행 안됨 - $this->stockService->increaseFromProduction(...); - } - } -} -``` - -**출고 LOT 옵션에서 생산 LOT 제외**: - -```php -// ShipmentService.php:525-550 -public function getLotOptions(): array { - return StockLot::where(...) // ← 구매입고 LOT만 조회 - ->whereIn('status', ['available', 'reserved']) - ->get(); - // ❌ 생산 완료 LOT(KD-SA-*) 미포함 -} -``` - -### 추적 불가 시나리오 - -``` -수주 KD-TS-260313-01 - → 생산 완료 (LOT 미생성) - → 재고 입고 (LOT 전달 실패 → 입고 안됨?) - → 품질검사 (별도 LOT 입력) - → 출고 (자재 LOT만 선택 가능, 생산품 LOT 없음) - -결과: "이 출고 건이 어느 생산 LOT인지" → 답 불가 -``` - -### 회의 논의 포인트 -- **최우선**: 백엔드에서 생산 LOT 자동 채번/저장 로직 구현 -- WorkResult.lot_no에 실제 저장 -- StockLot.work_order_id (이미 2026-02-21 추가됨) 활용하여 연결 -- getLotOptions()에 생산 LOT 포함 - ---- - -## 이슈 6: 출고품목 ↔ 수주품목 FK 부재 - -### 결론: 🔴 ShipmentItem에 order_item_id, work_order_item_id 컬럼 자체가 없음 - -### ShipmentItem 실제 컬럼 - -**마이그레이션**: `2025_12_26_150605_create_shipment_items_table.php` - -``` -id, tenant_id, shipment_id(FK), seq, -item_code, item_name, floor_unit, specification, -quantity, unit, lot_no, stock_lot_id(FK), remarks -``` - -- ❌ `order_item_id` → 없음 -- ❌ `work_order_item_id` → 없음 -- 품목 데이터는 **텍스트 복사**만 (품명, 규격, 수량) - -### ShipmentItem 생성 코드 - -**코드**: `sam-api/app/Services/ShipmentService.php:468-493` - -```php -ShipmentItem::create([ - 'item_code' => $item['item_code'] ?? null, - 'item_name' => $item['item_name'], - 'quantity' => $item['quantity'] ?? 0, - // ❌ order_item_id 없음 - // ❌ work_order_item_id 없음 -]); -``` - -### 추적 불가 질문들 - -| 질문 | 답변 가능 여부 | -|------|--------------| -| "출고 #1234에서 어떤 수주 품목이 출고됐나?" | ❌ 불가 | -| "수주 품목 #999는 어느 출고에서 출고됐나?" | ❌ 역추적 불가 | -| "수주 10개 품목 중 미출고 품목은?" | ❌ 집계 불가 | -| "부분 출고 진행률은?" | ❌ 계산 불가 | - -### 관련 FK도 불완전 - -**WorkOrderItem.source_order_item_id**: 인덱스만 있고 FK constraint 없음 - -```php -// 마이그레이션 2026_01_16 -$table->unsignedBigInteger('source_order_item_id')->nullable(); -$table->index('source_order_item_id'); // ← 인덱스만 -// ❌ $table->foreign('source_order_item_id')->references('id')->on('order_items') 없음 -``` - -### 회의 논의 포인트 -- shipment_items에 `order_item_id`, `work_order_item_id` 컬럼 추가 마이그레이션 -- 기존 데이터 마이그레이션 방안 (품명+규격으로 매칭?) -- work_order_items.source_order_item_id에 FK constraint 추가 - ---- - -## 전체 FK 연결 현황도 - -``` -orders ──────────────────── order_items ──────── order_nodes - │ (order_id FK) │ (order_node_id FK) │ - │ │ │ - ├─── work_orders │ │ - │ │ (sales_order_id FK) │ │ - │ │ │ │ - │ └─── work_order_items │ │ - │ │ │ │ - │ │ source_order_item_id ──→ ❌ FK 없음 (인덱스만) - │ │ │ - │ inspections │ - │ │ (work_order_id FK ✅) │ - │ │ (lot_no ← 연결 안됨 ❌) │ - │ │ - ├─── quality_document_orders ──→ quality_documents │ - │ │ (order_id FK ✅) │ - │ │ │ - │ └─── quality_document_locations │ - │ │ (order_item_id FK ✅) │ - │ │ - └─── shipments │ - │ (order_id FK ✅, work_order_id FK ✅) │ - │ │ - └─── shipment_items │ - │ (shipment_id FK ✅) │ - │ (stock_lot_id FK ✅) │ - │ (order_item_id ❌ 없음) │ - │ (work_order_item_id ❌ 없음) │ -``` - ---- - -## 개선 우선순위 로드맵 - -### P0 (즉시 - 운영 리스크) - -| # | 작업 | 영향범위 | 예상 난이도 | -|---|------|---------|------------| -| 1 | **can_ship 검증 추가** (백엔드 updateStatus + 프론트 버튼 조건) | ShipmentService 1곳 + ShipmentDetail 1곳 | 하 | -| 2 | **재고 차감 실패 시 hard fail** (try-catch에서 throw로 변경) | ShipmentService 1곳 | 하 | - -### P1 (단기 - 데이터 정합성) - -| # | 작업 | 영향범위 | 예상 난이도 | -|---|------|---------|------------| -| 3 | **생산 LOT 백엔드 자동 채번/저장** | WorkOrderService + NumberingService | 중 | -| 4 | **생산입고 LOT 연결 수정** (stockInFromProduction) | WorkOrderService + StockService | 중 | -| 5 | **shipment_items에 order_item_id 추가** (마이그레이션 + 서비스) | 마이그레이션 + ShipmentService | 중 | - -### P2 (중기 - 구조 개선) - -| # | 작업 | 영향범위 | 예상 난이도 | -|---|------|---------|------------| -| 6 | **품질검사 정본 결정** (Inspection vs QualityDocument 통합) | 양쪽 서비스 + 프론트 | 상 | -| 7 | **출고 시 품질 판정 자동 참조** (can_ship 자동 설정) | ShipmentService + 품질 연동 | 상 | -| 8 | **work_order_items.source_order_item_id FK 추가** | 마이그레이션 | 하 | -| 9 | **process_id=null 작업지시 생성 방지** | OrderService.createProductionOrder | 하 | - ---- - -## 참고: 정상 동작하는 부분 - -- ✅ 수주 → 생산지시 생성 (공정별 자동 분류) -- ✅ 작업지시 상태 관리 (유효한 상태 전환 규칙) -- ✅ 자재 투입 재고 차감 (WorkOrderMaterialInput + StockService) -- ✅ 출고 완료 시 재고 차감 (FIFO + 거래 기록) -- ✅ 출고 완료 → 수주 상태 SHIPPED 자동 전환 -- ✅ 매출 자동 생성 (sales_recognition 조건부) -- ✅ 수주 상태별 수정/삭제 제한 -- ✅ 생산지시 되돌리기 (WorkOrder/Item/Result 삭제) diff --git a/claudedocs/architecture/[ANALYSIS-2026-03-17] tenant-module-separation-dependency-audit.md b/claudedocs/architecture/[ANALYSIS-2026-03-17] tenant-module-separation-dependency-audit.md deleted file mode 100644 index af4f9a18..00000000 --- a/claudedocs/architecture/[ANALYSIS-2026-03-17] tenant-module-separation-dependency-audit.md +++ /dev/null @@ -1,437 +0,0 @@ -# Tenant-Based Module Separation: Cross-Module Dependency Audit - -**Date**: 2026-03-17 -**Scope**: Full dependency analysis for tenant-based module separation -**Status**: COMPLETE - ---- - -## Module Separation Plan - -| Tenant Package | Modules | File Count | -|---|---|---| -| Common ERP | dashboard, accounting, sales, HR, approval, board, customer-center, settings, master-data, material, outbound | ~majority | -| Kyungdong (Shutter MES) | production (56 files), quality (35 files) | 91 files | -| Juil (Construction) | construction (161 files) | 161 files | -| Optional | vehicle-management (13 files), vehicle (10 files) | 23 files | - ---- - -## CRITICAL RISK (Will break immediately on separation) - -### C1. Approval Module -> Production Component Import -**Files affected**: 1 file, blocks entire approval workflow -``` -src/components/approval/ApprovalBox/index.tsx:76 - import { InspectionReportModal } from '@/components/production/WorkOrders/documents/InspectionReportModal'; -``` -**Impact**: The approval inbox (Common ERP) directly imports a production document modal. If production module is removed, the approval box crashes entirely. -**Fix**: Extract `InspectionReportModal` to a shared `@/components/document-system/` or a shared document viewer module. Alternatively, use dynamic import with fallback. - ---- - -### C2. Sales Module -> Production Component Imports (3 page files) -**Files affected**: 3 files under `src/app/[locale]/(protected)/sales/` - -``` -sales/order-management-sales/production-orders/page.tsx - import { getProductionOrders, getProductionOrderStats } from "@/components/production/ProductionOrders/actions"; - import { ProductionOrder, ProductionOrderStats } from "@/components/production/ProductionOrders/types"; - -sales/order-management-sales/production-orders/[id]/page.tsx - import { getProductionOrderDetail } from "@/components/production/ProductionOrders/actions"; - import { ProductionOrderDetail, ProductionOrderStats } from "@/components/production/ProductionOrders/types"; - -sales/order-management-sales/[id]/production-order/page.tsx - import { AssigneeSelectModal } from "@/components/production/WorkOrders/AssigneeSelectModal"; - import { getProcessList } from "@/components/process-management/actions"; - import { createProductionOrder } from "@/components/orders/actions"; -``` -**Impact**: The sales module has a "production orders" sub-page that directly imports production actions, types, and UI components. This entire sub-section becomes non-functional without the production module. -**Fix strategy**: -1. The `production-orders` sub-pages under sales should be conditionally loaded (tenant feature flag) -2. `ProductionOrders/actions.ts` and `types.ts` should be extracted to a shared interface package -3. `AssigneeSelectModal` should be moved to a shared component or lazy-loaded with fallback - ---- - -### C3. Sales -> Production Route Navigation -**File**: `sales/order-management-sales/production-orders/[id]/page.tsx:247` -``` -router.push("/production/work-orders"); -``` -**Impact**: Hard navigation to production route from sales. Will 404 if production routes don't exist. -**Fix**: Conditional navigation wrapped in tenant feature check. - ---- - -### C4. QMS Page (Quality) -> Production + Outbound + Orders Imports -**File**: `src/app/[locale]/(protected)/quality/qms/page.tsx` -``` -import { InspectionReportModal } from '@/components/production/WorkOrders/documents'; -import { WorkLogModal } from '@/components/production/WorkOrders/documents'; -import { ProductInspectionViewModal } from '@/components/quality/InspectionManagement/ProductInspectionViewModal'; -``` -**File**: `src/app/[locale]/(protected)/quality/qms/mockData.ts` -``` -import type { WorkOrder } from '@/components/production/ProductionDashboard/types'; -import type { ShipmentDetail } from '@/components/outbound/ShipmentManagement/types'; -``` -**File**: `src/app/[locale]/(protected)/quality/qms/components/InspectionModal.tsx` -``` -import { DeliveryConfirmation } from '@/components/outbound/ShipmentManagement/documents/DeliveryConfirmation'; -import { ShippingSlip } from '@/components/outbound/ShipmentManagement/documents/ShippingSlip'; -import { SalesOrderDocument } from '@/components/orders/documents/SalesOrderDocument'; -import type { ShipmentDetail } from '@/components/outbound/ShipmentManagement/types'; -``` -**Impact**: QMS (assigned to Kyungdong tenant with quality) imports from production (same tenant -- OK), but also from outbound and orders (Common ERP). If QMS is extracted WITH quality, it will still need access to outbound/orders document components from Common ERP. -**Fix**: This is actually a reverse dependency -- quality module needs Common ERP's outbound/orders docs. This direction is acceptable (tenant module depends on common). However, the production type imports need a shared types interface. - ---- - -### C5. CEO Dashboard -> Production + Construction Data Sections -**Files affected**: Multiple files in `src/components/business/CEODashboard/` -``` -CEODashboard.tsx: 'production' and 'construction' section rendering -sections/DailyProductionSection.tsx: production data display -sections/ConstructionSection.tsx: construction data display -sections/CalendarSection.tsx: '/production/work-orders' and '/construction/project/contract' route references -types.ts: DailyProductionData, ConstructionData, production/construction settings flags -useSectionSummary.ts: production and construction summary logic -``` -**File**: `src/lib/api/dashboard/transformers/production-logistics.ts` -``` -import type { DailyProductionData, UnshippedData, ConstructionData } from '@/components/business/CEODashboard/types'; -``` -**File**: `src/hooks/useCEODashboard.ts` -``` -useDashboardFetch('dashboard/production/summary', ...) -useDashboardFetch('dashboard/construction/summary', ...) -``` -**Impact**: The CEO Dashboard (Common ERP) renders production and construction sections. If modules are removed, these sections will fail. The dashboard types contain production/construction data structures hardcoded. -**Fix**: -1. Dashboard sections must be conditionally rendered based on tenant configuration -2. Dashboard types need module-awareness (optional types, feature flags) -3. Dashboard API hooks should gracefully handle missing endpoints -4. Route references in CalendarSection need tenant-aware navigation - ---- - -### C6. Dashboard Invalidation System -**File**: `src/lib/dashboard-invalidation.ts` -```typescript -type DomainKey = '...' | 'production' | 'shipment' | 'construction'; -const DOMAIN_SECTION_MAP = { - production: ['statusBoard', 'dailyProduction'], - construction: ['statusBoard', 'construction'], -}; -``` -**Impact**: The dashboard invalidation system in `@/lib/` (Common ERP) has hardcoded production and construction domains. If production/construction modules call `invalidateDashboard('production')`, this works cross-module. If they are removed, stale keys remain. -**Fix**: Make domain-section mapping configurable/dynamic. Register domains at module initialization. - ---- - -## HIGH RISK (Will cause issues during development/testing) - -### H1. Construction -> HR Module Import -**File**: `src/components/business/construction/site-briefings/SiteBriefingForm.tsx:61-64` -``` -import { getEmployees } from '@/components/hr/EmployeeManagement/actions'; -import type { Employee } from '@/components/hr/EmployeeManagement/types'; -``` -**Impact**: Construction module (Juil tenant) depends on HR module (Common ERP). This direction (tenant -> common) is acceptable for single-binary, but if construction is extracted to a separate package, it needs access to HR's interface. -**Fix**: Extract employee lookup to a shared API interface. Or accept that tenant modules depend on Common ERP (allowed direction). - ---- - -### H2. Production -> Process-Management Import -**File**: `src/components/production/WorkerScreen/index.tsx:47` -``` -import { getProcessList } from '@/components/process-management/actions'; -``` -**Impact**: Process management is under `master-data` (Common ERP). Production depends on it. -**Fix**: This is allowed (tenant -> common), but if extracting to separate package, needs clear interface. - ---- - -### H3. Shared Type: `@/types/process.ts` -**Files**: 8 production files import from this shared type file -``` -src/components/production/WorkerScreen/index.tsx -src/components/production/WorkOrders/documents/BendingInspectionContent.tsx -src/components/production/WorkOrders/documents/BendingWipInspectionContent.tsx -src/components/production/WorkOrders/documents/InspectionReportModal.tsx -src/components/production/WorkOrders/documents/ScreenInspectionContent.tsx -src/components/production/WorkOrders/documents/SlatInspectionContent.tsx -src/components/production/WorkOrders/documents/SlatJointBarInspectionContent.tsx -src/components/production/WorkOrders/documents/inspection-shared.tsx -``` -**Impact**: `@/types/process.ts` (296 lines) contains `InspectionSetting`, `InspectionScope`, `Process`, `ProcessStep` types. These are used heavily by production but defined at the project root level. -**Fix**: This file should remain in Common ERP (it's process master-data definition). Production depends on it -- that direction is acceptable. - ---- - -### H4. Dev Generator -> Production Type Import -**File**: `src/components/dev/generators/workOrderData.ts:13` -``` -import type { ProcessOption } from '@/components/production/WorkOrders/actions'; -``` -**Impact**: Dev tooling imports a type from production. Low runtime risk (dev only) but will cause TS errors if production module is absent. -**Fix**: Move `ProcessOption` type to a shared types location, or make dev generators tenant-aware. - ---- - -### H5. Production -> Dashboard Invalidation (Bidirectional) -**Files**: -``` -src/components/production/WorkOrders/WorkOrderCreate.tsx:10 -> import { invalidateDashboard } from '@/lib/dashboard-invalidation'; -src/components/production/WorkOrders/WorkOrderDetail.tsx:10 -> same -src/components/production/WorkOrders/WorkOrderEdit.tsx:11 -> same -src/components/business/construction/management/ConstructionDetailClient.tsx:4 -> same -``` -**Impact**: Production and construction call `invalidateDashboard()` from `@/lib/` (Common ERP). This is allowed direction (tenant -> common). But the function's domain keys include `'production'` and `'construction'` -- if those modules are absent, orphan event listeners remain. -**Fix**: Register domain keys dynamically. Modules register their dashboard sections at init. - ---- - -### H6. CEO Dashboard CalendarSection Route References -**File**: `src/components/business/CEODashboard/sections/CalendarSection.tsx` -``` -order: '/production/work-orders', -construction: '/construction/project/contract', -``` -**Impact**: Clicking calendar items navigates to production/construction routes. Will 404 if those tenant routes don't exist. -**Fix**: Tenant-aware route resolution with fallback or hidden navigation for unavailable modules. - ---- - -### H7. Menu Transform Production Icon Mapping -**File**: `src/lib/utils/menuTransform.ts:89` -``` -production: Factory, -``` -**Impact**: Menu icon mapping contains production key. Low risk (backend controls menu visibility), but vestigial code remains. -**Fix**: Menu rendering is already dynamic from backend. Icon map can safely retain unused entries. - ---- - -## MEDIUM RISK (Requires attention but not immediately breaking) - -### M1. Shared Component Dependencies (template/document-system) - -All three target modules (production, quality, construction) heavily depend on these shared components: -- `@/components/templates/UniversalListPage` -- used by all list pages -- `@/components/templates/IntegratedDetailTemplate` -- used by all detail pages -- `@/components/document-system/` -- DocumentViewer, SectionHeader, ConstructionApprovalTable -- `@/components/common/ServerErrorPage` -- `@/components/common/ScheduleCalendar` -- `@/components/organisms/` -- PageLayout, PageHeader, MobileCard - -**Impact**: These are all Common ERP components. The dependency direction (tenant -> common) is allowed. No breakage on separation. -**Risk**: If extracting to separate npm packages, these become peer dependencies. - ---- - -### M2. Zustand Store Dependencies - -Target modules use these stores (all Common ERP): -``` -production/WorkerScreen -> menuStore (useSidebarCollapsed) -quality/EquipmentRepair -> menuStore (useMenuStore) -quality/EquipmentManagement -> menuStore (useMenuStore) -quality/EquipmentForm -> menuStore (useMenuStore) -construction/estimates -> authStore (useAuthStore) -``` -**Impact**: All stores are in Common ERP. Direction is allowed. No breakage on separation. -**Risk**: If separate packages, stores become shared singletons requiring careful provider setup. - ---- - -### M3. Shared Hook Dependencies - -Target modules import from `@/hooks/`: -``` -production: usePermission -quality: useStatsLoader -construction: useStatsLoader, useListHandlers, useDateRange, useDaumPostcode, useCurrentTime -``` -**Impact**: All hooks are Common ERP. Allowed direction. - ---- - -### M4. `@/lib/` Utility Dependencies - -All modules depend on standard utilities: -``` -@/lib/utils (cn) -@/lib/utils/amount (formatNumber, formatAmount) -@/lib/utils/date (formatDate) -@/lib/utils/excel-download -@/lib/utils/redirect-error (isNextRedirectError) -@/lib/utils/status-config (getPresetStyle, getPriorityStyle) -@/lib/api/* (executeServerAction, executePaginatedAction, buildApiUrl, apiClient, serverFetch) -@/lib/formatters -@/lib/constants/filter-presets -``` -**Impact**: All in Common ERP. Direction allowed. - ---- - -### M5. document-system `ConstructionApprovalTable` Name Confusion -**File**: `src/components/document-system/components/ConstructionApprovalTable.tsx` -**Impact**: Despite the name, this is a generic 4-column approval table component in the shared `document-system`. It is imported by both production and construction modules. The name is misleading but the component is tenant-agnostic. -**Fix**: Consider renaming to `FourColumnApprovalTable` or similar during separation to avoid confusion. - ---- - -### M6. Production -> Bending Image API References -**File**: `src/components/production/WorkOrders/documents/bending/utils.ts` -``` -return `${API_BASE}/images/bending/guiderail/...` -return `${API_BASE}/images/bending/bottombar/...` -return `${API_BASE}/images/bending/box/...` -``` -**Impact**: Production references backend image API paths specific to the shutter MES domain. These endpoints exist on the backend and are module-specific. -**Fix**: These stay with the production module. No cross-dependency issue. - ---- - -### M7. Two Vehicle Modules -There are TWO vehicle-related component directories: -- `src/components/vehicle/` (10 files) -- older, simpler -- `src/components/vehicle-management/` (13 files) -- newer, IntegratedDetailTemplate-based - -Both have separate app routes: -- `src/app/[locale]/(protected)/vehicle/` (old) -- `src/app/[locale]/(protected)/vehicle-management/` (new) - -**Impact**: No cross-references found between them or from other modules. Both are fully self-contained. -**Fix**: Clean separation. May want to consolidate before extracting. - ---- - -## LOW RISK (Informational / No action needed) - -### L1. Module-Internal Dynamic Imports -Quality and production use `next/dynamic` for their own internal components (modals, heavy components). All dynamic imports are within their own module boundaries. No cross-module dynamic imports found. - -### L2. No Shared CSS/Style Modules -All modules use Tailwind utility classes. No module-specific CSS modules or shared stylesheets exist. No breakage risk. - -### L3. No React Context Providers in Modules -No module-specific React Context providers were found in production/quality/construction. All context comes from the shared `(protected)/layout.tsx` (RootProvider, ApiErrorProvider, FCMProvider, DevFillProvider, PermissionGate). - -### L4. No Module-Specific Layout Files -No nested `layout.tsx` files exist under production/quality/construction app routes. All pages use the shared `(protected)/layout.tsx`. - -### L5. No Test Files -No test files (`*.test.*`, `*.spec.*`) exist in the codebase. No test dependency issues. - -### L6. No Cross-Module API Endpoint Calls -Each module calls only its own backend API endpoints: -- Production: `/api/v1/work-orders/*`, `/api/v1/work-results/*`, `/api/v1/production-orders/*` -- Quality: `/api/v1/quality/*`, `/api/v1/equipment/*` -- Construction: `/construction/*` (via apiClient) - -No module calls another module's backend API directly. Clean backend separation. - -### L7. CustomEvent System -The `dashboard:invalidate` CustomEvent in `@/lib/dashboard-invalidation.ts` is the only cross-module event system. Production and construction dispatch events; the CEO Dashboard listens. This is a pub/sub pattern and tolerant of missing publishers. - -### L8. Tenant-Aware Cache Already Exists -`src/lib/cache/TenantAwareCache.ts` already implements tenant-id-based cache key isolation. This utility supports the separation strategy. - -### L9. Middleware Has No Module-Specific Logic -`src/middleware.ts` handles i18n and bot detection. No module-specific routing or tenant-based path filtering exists in middleware. Clean. - ---- - -## Dependency Flow Diagram (ASCII) - -``` - +-----------------+ - | Common ERP | - | (dashboard, | - | accounting, | - | sales, HR, | - | approval, | - | settings, | - | master-data, | - | outbound, | - | templates, | - | document-sys) | - +--------+--------+ - | - +--------------+---------------+ - | | | - +--------v------+ +----v-------+ +-----v----------+ - | Kyungdong | | Juil | | Vehicle | - | (production | | (construc | | (vehicle-mgmt) | - | + quality) | | tion) | | | - +------+--------+ +-----+------+ +----------------+ - | | | - v v | - depends on depends on | - Common ERP Common ERP v - self-contained - -FORBIDDEN ARROWS (must be broken): - Common ERP --X--> Production (approval, sales, dashboard) - Common ERP --X--> Construction (dashboard) -``` - ---- - -## Action Items Summary - -### Must Fix Before Separation (6 items) - -| # | Severity | Issue | Effort | -|---|----------|-------|--------| -| C1 | CRITICAL | ApprovalBox imports InspectionReportModal from production | Medium | -| C2 | CRITICAL | Sales production-orders pages import from production | High | -| C3 | CRITICAL | Sales page hard-navigates to /production/work-orders | Low | -| C4 | CRITICAL | QMS page imports production document modals | Medium | -| C5 | CRITICAL | CEO Dashboard hardcodes production/construction sections | High | -| C6 | CRITICAL | Dashboard invalidation hardcodes production/construction domains | Medium | - -### Recommended Actions (6 items) - -| # | Severity | Issue | Effort | -|---|----------|-------|--------| -| H1 | HIGH | Construction SiteBriefing imports HR actions | Low | -| H2 | HIGH | Production WorkerScreen imports process-management | Low | -| H4 | HIGH | Dev generator imports production type | Low | -| H5 | HIGH | Bidirectional dashboard invalidation coupling | Medium | -| H6 | HIGH | CEO Calendar hardcoded module routes | Low | -| H7 | HIGH | Menu transform production icon mapping | Low | - -### Total Estimated Effort - -- **CRITICAL fixes**: ~3-5 days of focused refactoring -- **HIGH fixes**: ~1-2 days -- **MEDIUM/LOW**: informational, no code changes needed - ---- - -## Recommended Separation Strategy - -### Phase 1: Shared Interface Layer (1-2 days) -1. Create `@/interfaces/production.ts` with shared types (ProcessOption, WorkOrder summary types) -2. Create `@/interfaces/quality.ts` with shared types (InspectionReport view props) -3. Move InspectionReportModal and WorkLogModal to `@/components/document-system/modals/` - -### Phase 2: Feature Flags (1-2 days) -1. Add tenant feature flags: `hasProduction`, `hasQuality`, `hasConstruction`, `hasVehicle` -2. Conditionally render CEO Dashboard sections based on flags -3. Conditionally render sales production-order sub-pages based on flags -4. Make dashboard invalidation domain registry dynamic - -### Phase 3: Route Guards (0.5 day) -1. Replace hardcoded route strings with a tenant-aware route resolver -2. Add fallback/redirect for unavailable module routes - -### Phase 4: Clean Separation (1 day) -1. Move production + quality components to a Kyungdong-specific directory or package -2. Move construction components to a Juil-specific directory or package -3. Verify all builds pass with each module removed independently diff --git a/claudedocs/architecture/[DESIGN-2025-12-20] item-master-zustand-refactoring.md b/claudedocs/architecture/[DESIGN-2025-12-20] item-master-zustand-refactoring.md deleted file mode 100644 index 2639c3f2..00000000 --- a/claudedocs/architecture/[DESIGN-2025-12-20] item-master-zustand-refactoring.md +++ /dev/null @@ -1,538 +0,0 @@ -# 품목기준관리 Zustand 리팩토링 설계서 - -> **핵심 목표**: 모든 기능을 100% 동일하게 유지하면서, 수정 절차를 간단화 - -## 📌 핵심 원칙 - -``` -⚠️ 중요: 모든 품목기준관리 기능을 그대로 가져와야 함 -⚠️ 중요: 수정 절차 간단화가 핵심 (3방향 동기화 → 1곳 수정) -⚠️ 중요: 모든 기능이 정확히 동일하게 작동해야 함 -``` - -## 🔴 최종 검증 기준 (가장 중요!) - -### 페이지 관계도 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ [DB / API] │ -│ (단일 진실 공급원) │ -└─────────────────────────────────────────────────────────────┘ - ↑ ↑ ↓ - │ │ │ -┌───────┴───────┐ ┌────────┴────────┐ ┌────────┴────────┐ -│ 품목기준관리 │ │ 품목기준관리 │ │ 품목관리 │ -│ 테스트 페이지 │ │ 페이지 (기존) │ │ 페이지 │ -│ (Zustand) │ │ (Context) │ │ (동적 폼 렌더링) │ -└───────────────┘ └──────────────────┘ └──────────────────┘ - [신규] [기존] [최종 사용처] -``` - -### 검증 시나리오 - -``` -1. 테스트 페이지에서 섹션/필드 수정 - ↓ -2. API 호출 → DB 저장 - ↓ -3. 품목기준관리 페이지 (기존)에서 동일하게 표시되어야 함 - ↓ -4. 품목관리 페이지에서 동적 폼이 변경된 구조로 렌더링되어야 함 -``` - -### 필수 검증 항목 - -| # | 검증 항목 | 설명 | -|---|----------|------| -| 1 | **API 동일성** | 테스트 페이지가 기존 페이지와 동일한 API 엔드포인트 사용 | -| 2 | **데이터 동일성** | API 응답/요청 데이터 형식 100% 동일 | -| 3 | **기존 페이지 반영** | 테스트 페이지에서 수정 → 기존 품목기준관리 페이지에 반영 | -| 4 | **품목관리 반영** | 테스트 페이지에서 수정 → 품목관리 동적 폼에 반영 | - -### 왜 이게 중요한가? - -``` -테스트 페이지 (Zustand) ──┐ - ├──→ 같은 API ──→ 같은 DB ──→ 품목관리 페이지 -기존 페이지 (Context) ────┘ - -→ 상태 관리 방식만 다르고, API/DB는 공유 -→ 테스트 페이지에서 수정한 내용이 품목관리 페이지에 그대로 적용되어야 함 -→ 이것이 성공하면 Zustand 리팩토링이 완전히 검증된 것 -``` - ---- - -## 1. 현재 문제점 분석 - -### 1.1 중복 상태 관리 (3방향 동기화) - -현재 `ItemMasterContext.tsx`에서 섹션 수정 시: - -```typescript -// updateSection() 함수 내부 (Line 1464-1486) -setItemPages(...) // 1. 계층구조 탭 -setSectionTemplates(...) // 2. 섹션 탭 -setIndependentSections(...) // 3. 독립 섹션 -``` - -**문제점**: -- 같은 데이터를 3곳에서 중복 관리 -- 한 곳 업데이트 누락 시 데이터 불일치 -- 모든 CRUD 함수에 동일 패턴 반복 -- 새 기능 추가 시 3곳 모두 수정 필요 - -### 1.2 현재 상태 변수 목록 (16개) - -| # | 상태 변수 | 설명 | 중복 여부 | -|---|----------|------|----------| -| 1 | `itemMasters` | 품목 마스터 | - | -| 2 | `specificationMasters` | 규격 마스터 | - | -| 3 | `materialItemNames` | 자재 품목명 | - | -| 4 | `itemCategories` | 품목 분류 | - | -| 5 | `itemUnits` | 단위 | - | -| 6 | `itemMaterials` | 재질 | - | -| 7 | `surfaceTreatments` | 표면처리 | - | -| 8 | `partTypeOptions` | 부품유형 옵션 | - | -| 9 | `partUsageOptions` | 부품용도 옵션 | - | -| 10 | `guideRailOptions` | 가이드레일 옵션 | - | -| 11 | `sectionTemplates` | 섹션 템플릿 | ⚠️ 중복 | -| 12 | `itemMasterFields` | 필드 마스터 | ⚠️ 중복 | -| 13 | `itemPages` | 페이지 (섹션/필드 포함) | ⚠️ 중복 | -| 14 | `independentSections` | 독립 섹션 | ⚠️ 중복 | -| 15 | `independentFields` | 독립 필드 | ⚠️ 중복 | -| 16 | `independentBomItems` | 독립 BOM | ⚠️ 중복 | - -**중복 문제가 있는 엔티티**: -- **섹션**: `sectionTemplates`, `itemPages.sections`, `independentSections` -- **필드**: `itemMasterFields`, `itemPages.sections.fields`, `independentFields` -- **BOM**: `itemPages.sections.bom_items`, `independentBomItems` - ---- - -## 2. 리팩토링 설계 - -### 2.1 정규화된 상태 구조 (Normalized State) - -```typescript -// stores/useItemMasterStore.ts -interface ItemMasterState { - // ===== 정규화된 엔티티 (ID 기반 딕셔너리) ===== - entities: { - pages: Record; - sections: Record; - fields: Record; - bomItems: Record; - }; - - // ===== ID 목록 (순서 관리) ===== - ids: { - pages: number[]; - independentSections: number[]; // page_id가 null인 섹션 - independentFields: number[]; // section_id가 null인 필드 - independentBomItems: number[]; // section_id가 null인 BOM - }; - - // ===== 참조 데이터 (중복 없음) ===== - references: { - itemMasters: ItemMaster[]; - specificationMasters: SpecificationMaster[]; - materialItemNames: MaterialItemName[]; - itemCategories: ItemCategory[]; - itemUnits: ItemUnit[]; - itemMaterials: ItemMaterial[]; - surfaceTreatments: SurfaceTreatment[]; - partTypeOptions: PartTypeOption[]; - partUsageOptions: PartUsageOption[]; - guideRailOptions: GuideRailOption[]; - }; - - // ===== UI 상태 ===== - ui: { - isLoading: boolean; - error: string | null; - selectedPageId: number | null; - selectedSectionId: number | null; - }; -} -``` - -### 2.2 엔티티 구조 - -```typescript -// 페이지 엔티티 (섹션 ID만 참조) -interface PageEntity { - id: number; - page_name: string; - item_type: string; - description?: string; - order_no: number; - is_active: boolean; - sectionIds: number[]; // 섹션 객체 대신 ID만 저장 - created_at?: string; - updated_at?: string; -} - -// 섹션 엔티티 (필드/BOM ID만 참조) -interface SectionEntity { - id: number; - title: string; - page_id: number | null; // null이면 독립 섹션 - order_no: number; - is_collapsible: boolean; - default_open: boolean; - fieldIds: number[]; // 필드 ID 목록 - bomItemIds: number[]; // BOM ID 목록 - created_at?: string; - updated_at?: string; -} - -// 필드 엔티티 -interface FieldEntity { - id: number; - field_key: string; - field_name: string; - field_type: string; - section_id: number | null; // null이면 독립 필드 - order_no: number; - is_required: boolean; - options?: any; - default_value?: any; - created_at?: string; - updated_at?: string; -} - -// BOM 엔티티 -interface BOMItemEntity { - id: number; - section_id: number | null; // null이면 독립 BOM - child_item_code: string; - child_item_name: string; - quantity: number; - unit: string; - order_no: number; - created_at?: string; - updated_at?: string; -} -``` - -### 2.3 수정 절차 비교 - -#### Before (현재): 3방향 동기화 - -```typescript -const updateSection = async (sectionId, updates) => { - const response = await api.update(sectionId, updates); - - // 1. itemPages 업데이트 - setItemPages(prev => prev.map(page => ({ - ...page, - sections: page.sections.map(s => s.id === sectionId ? {...s, ...updates} : s) - }))); - - // 2. sectionTemplates 업데이트 - setSectionTemplates(prev => prev.map(t => - t.id === sectionId ? {...t, ...updates} : t - )); - - // 3. independentSections 업데이트 - setIndependentSections(prev => prev.map(s => - s.id === sectionId ? {...s, ...updates} : s - )); -}; -``` - -#### After (Zustand): 1곳만 수정 - -```typescript -const updateSection = async (sectionId, updates) => { - const response = await api.update(sectionId, updates); - - // 딱 1곳만 수정하면 끝! - set((state) => ({ - entities: { - ...state.entities, - sections: { - ...state.entities.sections, - [sectionId]: { ...state.entities.sections[sectionId], ...updates } - } - } - })); -}; -``` - -### 2.4 파생 상태 (Selectors) - -```typescript -// 계층구조 탭용: 페이지 + 섹션 + 필드 조합 -const usePageWithDetails = (pageId: number) => { - return useItemMasterStore((state) => { - const page = state.entities.pages[pageId]; - if (!page) return null; - - return { - ...page, - sections: page.sectionIds.map(sId => { - const section = state.entities.sections[sId]; - return { - ...section, - fields: section.fieldIds.map(fId => state.entities.fields[fId]), - bom_items: section.bomItemIds.map(bId => state.entities.bomItems[bId]), - }; - }), - }; - }); -}; - -// 섹션 탭용: 모든 섹션 (페이지 연결 여부 무관) -const useAllSections = () => { - return useItemMasterStore((state) => - Object.values(state.entities.sections) - ); -}; - -// 독립 섹션만 -const useIndependentSections = () => { - return useItemMasterStore((state) => - state.ids.independentSections.map(id => state.entities.sections[id]) - ); -}; -``` - ---- - -## 3. 기능 매핑 체크리스트 - -### 3.1 페이지 관리 - -| 기존 함수 | 새 함수 | 상태 | -|----------|--------|------| -| `loadItemPages` | `loadPages` | ⬜ | -| `addItemPage` | `createPage` | ⬜ | -| `updateItemPage` | `updatePage` | ⬜ | -| `deleteItemPage` | `deletePage` | ⬜ | - -### 3.2 섹션 관리 - -| 기존 함수 | 새 함수 | 상태 | -|----------|--------|------| -| `loadSectionTemplates` | `loadSections` | ⬜ | -| `loadIndependentSections` | (loadSections에 통합) | ⬜ | -| `addSectionTemplate` | `createSection` | ⬜ | -| `addSectionToPage` | `createSectionInPage` | ⬜ | -| `createIndependentSection` | `createSection` (page_id: null) | ⬜ | -| `updateSectionTemplate` | `updateSection` | ⬜ | -| `updateSection` | `updateSection` | ⬜ | -| `deleteSectionTemplate` | `deleteSection` | ⬜ | -| `deleteSection` | `deleteSection` | ⬜ | -| `linkSectionToPage` | `linkSectionToPage` | ⬜ | -| `unlinkSectionFromPage` | `unlinkSectionFromPage` | ⬜ | -| `getSectionUsage` | `getSectionUsage` | ⬜ | - -### 3.3 필드 관리 - -| 기존 함수 | 새 함수 | 상태 | -|----------|--------|------| -| `loadItemMasterFields` | `loadFields` | ⬜ | -| `loadIndependentFields` | (loadFields에 통합) | ⬜ | -| `addItemMasterField` | `createField` | ⬜ | -| `addFieldToSection` | `createFieldInSection` | ⬜ | -| `createIndependentField` | `createField` (section_id: null) | ⬜ | -| `updateItemMasterField` | `updateField` | ⬜ | -| `updateField` | `updateField` | ⬜ | -| `deleteItemMasterField` | `deleteField` | ⬜ | -| `deleteField` | `deleteField` | ⬜ | -| `linkFieldToSection` | `linkFieldToSection` | ⬜ | -| `unlinkFieldFromSection` | `unlinkFieldFromSection` | ⬜ | -| `getFieldUsage` | `getFieldUsage` | ⬜ | - -### 3.4 BOM 관리 - -| 기존 함수 | 새 함수 | 상태 | -|----------|--------|------| -| `loadIndependentBomItems` | `loadBomItems` | ⬜ | -| `addBOMItem` | `createBomItem` | ⬜ | -| `createIndependentBomItem` | `createBomItem` (section_id: null) | ⬜ | -| `updateBOMItem` | `updateBomItem` | ⬜ | -| `deleteBOMItem` | `deleteBomItem` | ⬜ | - -### 3.5 참조 데이터 관리 - -| 기존 함수 | 새 함수 | 상태 | -|----------|--------|------| -| `addItemMaster` / `updateItemMaster` / `deleteItemMaster` | `itemMasterActions` | ⬜ | -| `addSpecificationMaster` / `updateSpecificationMaster` / `deleteSpecificationMaster` | `specificationActions` | ⬜ | -| `addMaterialItemName` / `updateMaterialItemName` / `deleteMaterialItemName` | `materialItemNameActions` | ⬜ | -| `addItemCategory` / `updateItemCategory` / `deleteItemCategory` | `categoryActions` | ⬜ | -| `addItemUnit` / `updateItemUnit` / `deleteItemUnit` | `unitActions` | ⬜ | -| `addItemMaterial` / `updateItemMaterial` / `deleteItemMaterial` | `materialActions` | ⬜ | -| `addSurfaceTreatment` / `updateSurfaceTreatment` / `deleteSurfaceTreatment` | `surfaceTreatmentActions` | ⬜ | -| `addPartTypeOption` / `updatePartTypeOption` / `deletePartTypeOption` | `partTypeActions` | ⬜ | -| `addPartUsageOption` / `updatePartUsageOption` / `deletePartUsageOption` | `partUsageActions` | ⬜ | -| `addGuideRailOption` / `updateGuideRailOption` / `deleteGuideRailOption` | `guideRailActions` | ⬜ | - ---- - -## 4. 구현 계획 - -### Phase 1: 기반 구축 ✅ 완료 (2025-12-20) - -- [x] Zustand, Immer 설치 -- [x] 테스트 페이지 라우트 생성 (`/items-management-test`) -- [x] 기본 스토어 구조 생성 (`useItemMasterStore.ts`) -- [x] 타입 정의 (`types.ts`) - -### Phase 2: API 연동 ✅ 완료 (2025-12-20) - -- [x] 기존 API 구조 분석 (`item-master.ts`) -- [x] API 응답 → 정규화 상태 변환 함수 (`normalizers.ts`) -- [x] 스토어에 `initFromApi()` 함수 구현 -- [x] 테스트 페이지에서 실제 API 데이터 로드 기능 추가 - -**생성된 파일**: -- `src/stores/item-master/normalizers.ts` - API 응답 정규화 함수 - -**테스트 페이지 기능**: -- "실제 API 로드" 버튼 - 백엔드 API에서 실제 데이터 로드 -- "테스트 데이터 로드" 버튼 - 하드코딩된 테스트 데이터 로드 -- 데이터 소스 표시 (API/테스트/없음) - -### Phase 3: 핵심 엔티티 구현 - -- [x] 페이지 CRUD 구현 (로컬 상태) -- [x] 섹션 CRUD 구현 (로컬 상태) -- [x] 필드 CRUD 구현 (로컬 상태) -- [x] BOM CRUD 구현 (로컬 상태) -- [x] link/unlink 기능 구현 (로컬 상태) -- [ ] API 연동 CRUD (DB 저장) - **다음 단계** - -### Phase 3: 참조 데이터 구현 - -- [ ] 품목 마스터 관리 -- [ ] 규격 마스터 관리 -- [ ] 분류/단위/재질 등 옵션 관리 - -### Phase 4: 파생 상태 & 셀렉터 - -- [ ] 계층구조 뷰용 셀렉터 -- [ ] 섹션 탭용 셀렉터 -- [ ] 필드 탭용 셀렉터 -- [ ] 독립 항목 셀렉터 - -### Phase 5: UI 연동 - -- [ ] 테스트 페이지 컴포넌트 생성 -- [ ] 기존 컴포넌트 재사용 (스토어만 교체) -- [ ] 동작 검증 - -### Phase 6: 검증 & 마이그레이션 - -- [ ] 기존 페이지와 1:1 동작 비교 -- [ ] 엣지 케이스 테스트 -- [ ] 성능 비교 -- [ ] 기존 페이지 마이그레이션 결정 - ---- - -## 5. 파일 구조 - -``` -src/ -├── stores/ -│ └── item-master/ -│ ├── useItemMasterStore.ts # 메인 스토어 -│ ├── slices/ -│ │ ├── pageSlice.ts # 페이지 액션 -│ │ ├── sectionSlice.ts # 섹션 액션 -│ │ ├── fieldSlice.ts # 필드 액션 -│ │ ├── bomSlice.ts # BOM 액션 -│ │ └── referenceSlice.ts # 참조 데이터 액션 -│ ├── selectors/ -│ │ ├── pageSelectors.ts # 페이지 파생 상태 -│ │ ├── sectionSelectors.ts # 섹션 파생 상태 -│ │ └── fieldSelectors.ts # 필드 파생 상태 -│ └── types.ts # 타입 정의 -│ -├── app/[locale]/(protected)/ -│ └── items-management-test/ -│ └── page.tsx # 테스트 페이지 -``` - ---- - -## 6. 테스트 시나리오 - -### 6.1 섹션 수정 동기화 테스트 - -``` -시나리오: 섹션 이름 수정 -1. 계층구조 탭에서 섹션 선택 -2. 섹션 이름 "기본정보" → "기본 정보" 수정 -3. 검증: - - [ ] 계층구조 탭에 반영 - - [ ] 섹션 탭에 반영 - - [ ] 독립 섹션(연결 해제 시) 반영 - - [ ] API 호출 1회만 발생 -``` - -### 6.2 필드 이동 테스트 - -``` -시나리오: 필드를 다른 섹션으로 이동 -1. 섹션 A에서 필드 선택 -2. 섹션 B로 이동 (unlink → link) -3. 검증: - - [ ] 섹션 A에서 필드 제거 - - [ ] 섹션 B에 필드 추가 - - [ ] 계층구조 탭 반영 - - [ ] 필드 탭에서 section_id 변경 -``` - -### 6.3 독립 → 연결 테스트 - -``` -시나리오: 독립 섹션을 페이지에 연결 -1. 독립 섹션 선택 -2. 페이지에 연결 (linkSectionToPage) -3. 검증: - - [ ] 독립 섹션 목록에서 제거 - - [ ] 페이지의 섹션 목록에 추가 - - [ ] 섹션 탭에서 page_id 변경 -``` - ---- - -## 7. 롤백 계획 - -문제 발생 시: -1. 테스트 페이지 라우트 제거 -2. 스토어 코드 삭제 -3. 기존 `ItemMasterContext` 그대로 사용 - -**리스크 최소화**: -- 기존 코드 수정 없음 -- 새 코드만 추가 -- 언제든 롤백 가능 - ---- - -## 8. 성공 기준 - -| 항목 | 기준 | -|-----|------| -| **기능 동등성** | 기존 모든 기능 100% 동작 | -| **동기화** | 1곳 수정으로 모든 뷰 업데이트 | -| **코드량** | CRUD 함수 코드 50% 이상 감소 | -| **버그** | 데이터 불일치 버그 0건 | -| **성능** | 기존 대비 동등 또는 향상 | - ---- - -## 변경 이력 - -| 날짜 | 작성자 | 내용 | -|-----|--------|------| -| 2025-12-20 | Claude | 초안 작성 | -| 2025-12-20 | Claude | Phase 1 완료 - 기반 구축 | -| 2025-12-20 | Claude | Phase 2 완료 - API 연동 (normalizers.ts, initFromApi) | \ No newline at end of file diff --git a/claudedocs/architecture/[DESIGN-2026-02-11] dynamic-field-type-extension.md b/claudedocs/architecture/[DESIGN-2026-02-11] dynamic-field-type-extension.md deleted file mode 100644 index 4f311bb0..00000000 --- a/claudedocs/architecture/[DESIGN-2026-02-11] dynamic-field-type-extension.md +++ /dev/null @@ -1,1299 +0,0 @@ -# 품목기준관리 동적 필드 타입 확장 설계 - -> 작성일: 2026-02-11 -> 목적: 멀티테넌시 품목기준관리의 필드 타입 확장 및 범용 테이블 섹션 설계 -> 범위: 프론트엔드 컴포넌트 설계 + 백엔드 API 계약 + 산업별 확장 구조 - ---- - -## 목차 - -1. [배경 및 목표](#1-배경-및-목표) -2. [현재 시스템 분석](#2-현재-시스템-분석) -3. [확장 필드 타입 레지스트리](#3-확장-필드-타입-레지스트리) -4. [범용 테이블 섹션 설계](#4-범용-테이블-섹션-설계) -5. [섹션 템플릿 라이브러리](#5-섹션-템플릿-라이브러리) -6. [산업별 확장 구조](#6-산업별-확장-구조) -7. [백엔드 API 계약](#7-백엔드-api-계약) -8. [프론트엔드 컴포넌트 아키텍처](#8-프론트엔드-컴포넌트-아키텍처) -9. [조건부 표시 확장](#9-조건부-표시-확장) -10. [검증 프레임워크](#10-검증-프레임워크) -11. [구현 로드맵](#11-구현-로드맵) - ---- - -## 1. 배경 및 목표 - -### 1.1 현재 문제 - -품목기준관리(`/master-data/item-master-data-management`)는 **동적 폼 구성 시스템**이 이미 존재하지만, 필드 타입이 6가지로 제한되어 제조 ERP의 다양한 요구를 충족하지 못함. - -| 현재 있는 것 | 없어서 부족한 것 | -|-------------|-----------------| -| textbox | 다른 테이블 참조/검색 선택 (거래처, 품목, 고객) | -| number | 복수 선택 (태그형) | -| dropdown | 파일/이미지 업로드 | -| checkbox | 통화/금액 (단위 포함) | -| date | 값+단위 조합 (100mm, 50kg) | -| textarea | 범용 테이블/그리드 (BOM 외) | - -또한 BOM이 유일한 "테이블형 섹션"이지만, 제조 ERP에서는 **공정, 품질검사, 구매처, 단가이력** 등도 테이블 구조가 필요함. - -### 1.2 설계 목표 - -``` -핵심 원칙: "항목을 미리 정의"하지 않고 "필드 타입과 config 조합"을 확장한다. -``` - -1. **필드 타입 확장**: 6종 → 14종으로 확장 (입력 원자 단위) -2. **범용 테이블 섹션**: BOM 전용 → config 기반 범용 테이블로 일반화 -3. **섹션 템플릿 라이브러리**: 산업별 표준 섹션 프리셋 제공 -4. **백엔드 key + type + config 체계**: 백엔드가 스키마만 정의하면 프론트가 자동 렌더링 -5. **멀티테넌시 확장성**: 테넌트마다 다른 항목 구성 가능 -6. **산업 불문 확장**: 제조/공사/유통/물류 전방위 커버 - -### 1.3 설계 원칙 - -- **하위 호환**: 기존 6가지 필드 타입은 그대로 유지, 기존 코드 변경 없음 -- **점진적 확장**: 새 필드 타입 추가 = 새 컴포넌트 1개 추가 + DynamicFieldRenderer switch 1줄 추가 -- **config 기반**: 필드의 동작은 `field_type` + `properties` 조합으로 결정 -- **백엔드 독립**: 프론트 컴포넌트는 미리 만들고, 백엔드는 나중에 key-config 매핑만 추가 - ---- - -## 2. 현재 시스템 분석 - -### 2.1 아키텍처 - -``` -품목기준관리 (Admin) 품목 등록/수정 (User) -ItemMasterDataManagement.tsx DynamicItemForm/index.tsx - ↓ 구조 정의 ↓ 구조 기반 렌더링 - Pages → Sections → Fields GET /pages/{id}/structure - → BOM Items ↓ - DynamicFieldRenderer (switch) - → TextField - → NumberField - → DropdownField - → CheckboxField - → DateField - → TextareaField -``` - -### 2.2 핵심 파일 - -| 파일 | 줄 수 | 역할 | -|------|-------|------| -| `DynamicItemForm/index.tsx` | 1,048 | 메인 폼 컴포넌트 | -| `DynamicItemForm/types.ts` | 261 | 타입 정의 | -| `DynamicItemForm/fields/DynamicFieldRenderer.tsx` | 44 | 필드 타입 라우터 | -| `DynamicItemForm/sections/DynamicBOMSection.tsx` | 515 | BOM 테이블 섹션 | -| `DynamicItemForm/hooks/` | 7개 훅 | 상태/검증/조건부 표시 | -| `types/item-master-api.ts` | 745 | API 타입 정의 | -| `ItemMasterDataManagement.tsx` | 1,005 | Admin 관리 페이지 | - -### 2.3 현재 필드 응답 구조 (ItemFieldResponse) - -```typescript -{ - id: number, - field_name: string, // "품목명" - field_key: string | null, // "98_item_name" - field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea', - is_required: boolean, - placeholder: string | null, - default_value: string | null, - options: [{label, value}] | null, // dropdown 옵션 - properties: Record | null, // 추가 메타데이터 - validation_rules: Record | null, - display_condition: Record | null, -} -``` - -### 2.4 현재 섹션 타입 - -```typescript -section.type: 'fields' | 'bom' -// 'fields' → DynamicFieldRenderer로 각 필드 렌더링 -// 'bom' → DynamicBOMSection (하드코딩된 BOM 전용 UI) -``` - ---- - -## 3. 확장 필드 타입 레지스트리 - -### 3.1 전체 필드 타입 목록 - -#### 기존 유지 (6종) - -| field_type | 컴포넌트 | 설명 | -|-----------|---------|------| -| `textbox` | TextField | 단일 행 텍스트 | -| `number` | NumberField | 숫자 입력 | -| `dropdown` | DropdownField | 단일 선택 | -| `checkbox` | CheckboxField | 체크박스 | -| `date` | DateField | 날짜 선택 | -| `textarea` | TextareaField | 여러 행 텍스트 | - -#### 신규 추가 (8종) - -| field_type | 컴포넌트 | 설명 | 우선순위 | -|-----------|---------|------|---------| -| `reference` | ReferenceField | 다른 테이블 참조 검색/선택 | 🔴 Phase 1 | -| `multi-select` | MultiSelectField | 복수 선택 (태그형) | 🔴 Phase 1 | -| `file` | FileField | 파일/이미지 업로드 | 🔴 Phase 1 | -| `currency` | CurrencyField | 통화 금액 (포맷 + 단위) | 🟡 Phase 2 | -| `unit-value` | UnitValueField | 값 + 단위 조합 | 🟡 Phase 2 | -| `radio` | RadioField | 라디오 버튼 그룹 | 🟡 Phase 2 | -| `toggle` | ToggleField | On/Off 토글 스위치 | 🟢 Phase 3 | -| `computed` | ComputedField | 읽기 전용 계산 필드 | 🟢 Phase 3 | - -### 3.2 각 필드 타입별 상세 스펙 - ---- - -#### 3.2.1 `reference` — 참조 룩업 필드 - -**용도**: 다른 테이블의 데이터를 검색하여 선택 (거래처, 품목, 고객, 공정, 현장, 차량 등) - -**UI**: 검색 입력 + 드롭다운 팝업 (SearchableSelectionModal 기반) - -**properties 스키마**: -```jsonc -{ - "source": "vendors", // 참조할 데이터 소스 (필수) — 프리셋 또는 "custom" - "displayField": "name", // 선택 후 표시할 필드 (기본: "name") - "valueField": "id", // 저장할 값 필드 (기본: "id") - "searchFields": ["name", "code"], // 검색 대상 필드 (기본: ["name"]) - "searchApiUrl": "/api/proxy/vendors", // 검색 API URL ("custom" source일 때 필수) - "minSearchLength": 1, // 최소 검색 글자 수 (기본: 1) - "modalTitle": "거래처 선택", // 모달 제목 (선택, 없으면 field_name + " 선택") - "columns": [ // 검색 결과 표시 컬럼 (선택) - { "key": "code", "label": "코드", "width": "120px" }, - { "key": "name", "label": "이름" }, - { "key": "contact", "label": "연락처", "width": "150px" } - ], - "displayFormat": "{code} - {name}", // 선택 후 표시 포맷 (선택) - "returnFields": ["id", "code", "name"] // 선택 시 폼에 저장할 필드들 (선택) -} -``` - -**저장되는 값**: -```jsonc -// 단일 값: valueField 기준 -{ "vendor_id": "123" } - -// returnFields 설정 시: 여러 필드 동시 저장 -{ "vendor_id": "123", "vendor_code": "V-001", "vendor_name": "삼성전자" } -``` - -**소스 프리셋** (프론트에서 미리 정의, 산업별 확장 가능): - -| source | 산업 | API | displayField | -|--------|------|-----|--------------| -| `vendors` | 공통 | `/api/proxy/vendors` | name | -| `items` | 공통 | `/api/proxy/items` | name | -| `customers` | 공통 | `/api/proxy/customers` | company_name | -| `employees` | 공통 | `/api/proxy/employees` | name | -| `processes` | 제조 | `/api/proxy/processes` | process_name | -| `warehouses` | 공통 | `/api/proxy/warehouses` | name | -| `materials` | 제조 | `/api/proxy/item-master/materials` | material_name | -| `surface_treatments` | 제조 | `/api/proxy/item-master/surface-treatments` | treatment_name | -| `sites` | 공사 | `/api/proxy/sites` | site_name | -| `vehicles` | 물류 | `/api/proxy/vehicles` | plate_number | -| `routes` | 물류 | `/api/proxy/routes` | route_name | -| `stores` | 유통 | `/api/proxy/stores` | store_name | -| `custom` | — | properties.searchApiUrl | properties.displayField | - -> `custom` source를 사용하면 백엔드에 새 API만 추가하면 프론트 코드 수정 없이 어떤 참조든 연결 가능. - ---- - -#### 3.2.2 `multi-select` — 복수 선택 필드 - -**용도**: 여러 항목을 동시에 선택 (태그/칩 형태) - -**UI**: Combobox + 태그 칩 - -**properties 스키마**: -```jsonc -{ - "maxSelections": 5, // 최대 선택 수 (선택, 기본: 무제한) - "allowCustom": false, // 사용자 직접 입력 허용 여부 (기본: false) - "layout": "chips" // "chips" | "list" (기본: "chips") -} -``` - -**options 사용**: 기존 dropdown과 동일한 `[{label, value}]` 형태 - -**저장되는 값**: -```jsonc -{ "applicable_processes": ["CUT", "BEND", "WELD", "PAINT"] } -``` - ---- - -#### 3.2.3 `file` — 파일/이미지 업로드 필드 - -**용도**: 문서, 이미지, 도면 첨부 - -**UI**: 파일 선택 버튼 + 미리보기 (이미지일 경우) - -**properties 스키마**: -```jsonc -{ - "accept": ".pdf,.doc,.docx", // 허용 파일 타입 (기본: "*") - "maxSize": 10485760, // 최대 파일 크기 bytes (기본: 10MB) - "maxFiles": 5, // 최대 파일 수 (기본: 1) - "preview": true, // 미리보기 표시 여부 (기본: true, 이미지 파일만) - "uploadApiUrl": "/api/proxy/files/upload", // 업로드 API (선택) - "category": "drawing" // 파일 카테고리 태그 (선택) -} -``` - -**저장되는 값**: -```jsonc -// 단일 파일 -{ "drawing_file": { "fileId": "uuid-123", "fileName": "도면_v2.pdf", "fileUrl": "/files/uuid-123" } } - -// 복수 파일 -{ "attachments": [ - { "fileId": "uuid-123", "fileName": "도면.pdf", "fileUrl": "/files/uuid-123" }, - { "fileId": "uuid-456", "fileName": "사진.jpg", "fileUrl": "/files/uuid-456" } - ] -} -``` - ---- - -#### 3.2.4 `currency` — 통화/금액 필드 - -**용도**: 단가, 총액 등 금액 입력 (천 단위 콤마 + 통화 기호) - -**UI**: 숫자 입력 + 통화 기호 + 천단위 포맷 - -**properties 스키마**: -```jsonc -{ - "currency": "KRW", // 통화 코드 (기본: "KRW") - "precision": 0, // 소수점 자릿수 (기본: KRW=0, USD=2) - "showSymbol": true, // 통화 기호 표시 (기본: true) - "allowNegative": false // 음수 허용 (기본: false) -} -``` - -**저장되는 값**: 숫자 (`{ "unit_price": 15000 }`) - ---- - -#### 3.2.5 `unit-value` — 값+단위 조합 필드 - -**용도**: 치수, 무게, 용량, 거리 등 (숫자 + 단위 동시 입력) - -**UI**: 숫자 입력 + 단위 선택 드롭다운 (inline) - -**properties 스키마**: -```jsonc -{ - "units": ["mm", "cm", "m", "inch"], // 선택 가능 단위 목록 (필수) - "defaultUnit": "mm", // 기본 단위 (선택) - "precision": 1, // 소수점 자릿수 (기본: 0) - "showConversion": false // 단위 변환 표시 (기본: false) -} -``` - -**저장되는 값**: `{ "thickness": { "value": 2.5, "unit": "mm" } }` - ---- - -#### 3.2.6 `radio` — 라디오 버튼 그룹 - -**용도**: 상호 배타적 선택 (3~5개 이내 옵션에 적합) - -**UI**: 라디오 버튼 그룹 (수평/수직) - -**properties 스키마**: -```jsonc -{ - "layout": "horizontal" // "horizontal" | "vertical" (기본: "horizontal") -} -``` - -**options 사용**: dropdown과 동일한 `[{label, value}]` - ---- - -#### 3.2.7 `toggle` — 토글 스위치 - -**용도**: On/Off 상태 전환 - -**UI**: Switch 컴포넌트 - -**properties 스키마**: -```jsonc -{ - "onLabel": "활성", // On 상태 라벨 (선택) - "offLabel": "비활성", // Off 상태 라벨 (선택) - "onValue": "active", // On 상태 저장값 (기본: "true") - "offValue": "inactive" // Off 상태 저장값 (기본: "false") -} -``` - ---- - -#### 3.2.8 `computed` — 읽기 전용 계산 필드 - -**용도**: 다른 필드 값을 기반으로 자동 계산되는 필드 (표시 전용) - -**UI**: 읽기 전용 표시 (배경색 구분) - -**properties 스키마**: -```jsonc -{ - "formula": "{quantity} * {unit_price}", // 계산식 (필수) - "dependsOn": ["quantity", "unit_price"], // 의존 필드 키 목록 (필수) - "format": "currency", // 표시 포맷: "number" | "currency" | "percent" (기본: "number") - "precision": 0 // 소수점 자릿수 (기본: 0) -} -``` - ---- - -### 3.3 field_type 확장 타입 정의 (TypeScript) - -```typescript -// 기존 -type FieldType = 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea'; - -// 확장 -type ExtendedFieldType = FieldType - | 'reference' // Phase 1 - | 'multi-select' // Phase 1 - | 'file' // Phase 1 - | 'currency' // Phase 2 - | 'unit-value' // Phase 2 - | 'radio' // Phase 2 - | 'toggle' // Phase 3 - | 'computed'; // Phase 3 -``` - ---- - -## 4. 범용 테이블 섹션 설계 - -### 4.1 현재 문제 - -``` -현재: section.type === 'bom' → DynamicBOMSection (515줄, BOM 전용 하드코딩) -필요: 공정, 품질검사, 구매처, 단가이력 등도 테이블 필요 -``` - -### 4.2 설계: section.type 확장 - -```typescript -// 기존 -section.type: 'fields' | 'bom' - -// 확장 -section.type: 'fields' | 'bom' | 'table' -// ↑ 신규: 범용 테이블 -``` - -### 4.3 범용 테이블 섹션 config - -`section.properties`에 테이블 설정을 담음: - -```jsonc -{ - "table_config": { - // 컬럼 정의 (핵심) - "columns": [ - { - "key": "process_code", // 컬럼 키 (데이터 저장 키) - "label": "공정코드", // 컬럼 헤더 - "type": "reference", // 셀 입력 타입 (필드 타입과 동일한 14종) - "width": "150px", // 컬럼 너비 (선택) - "required": true, // 필수 여부 - "config": { // 타입별 추가 설정 (properties와 동일 구조) - "source": "processes", - "displayField": "process_name", - "searchFields": ["process_name", "process_code"] - } - }, - { - "key": "process_name", - "label": "공정명", - "type": "textbox", - "width": "200px", - "readOnly": true, // 참조 필드에서 자동 채움 - "autoFillFrom": "process_code.process_name" // 자동 채움 소스 - }, - { - "key": "quantity", - "label": "수량", - "type": "number", - "width": "100px", - "config": { "min": 0, "precision": 2 } - }, - { - "key": "unit", - "label": "단위", - "type": "dropdown", - "width": "100px", - "config": { "source": "unitOptions" } - }, - { - "key": "start_date", - "label": "시작일", - "type": "date", - "width": "140px" - }, - { - "key": "note", - "label": "비고", - "type": "textbox" // width 미지정 = 나머지 공간 - } - ], - - // 행 동작 - "addable": true, // 행 추가 가능 (기본: true) - "deletable": true, // 행 삭제 가능 (기본: true) - "reorderable": true, // 행 순서 변경 가능 (기본: false) - "maxRows": 100, // 최대 행 수 (선택, 기본: 무제한) - "minRows": 0, // 최소 행 수 (선택, 기본: 0) - - // 표시 - "showRowNumber": true, // 행 번호 표시 (기본: true) - "showCheckbox": false, // 행 선택 체크박스 (기본: false) - "emptyMessage": "데이터가 없습니다.", // 빈 상태 메시지 - - // 요약행 (선택) - "summaryRow": { - "enabled": true, - "columns": { - "quantity": { "type": "sum", "label": "합계" }, - "amount": { "type": "sum", "format": "currency" } - } - }, - - // 데이터 소스 (기존 데이터 로드용, 선택) - "dataApiUrl": "/api/proxy/items/{itemId}/routings", - "saveApiUrl": "/api/proxy/items/{itemId}/routings" - } -} -``` - -### 4.4 기존 BOM과의 관계 - -``` -DynamicBOMSection (기존) → 유지 (하위 호환) -DynamicTableSection (신규) → 범용 테이블 - -section.type === 'bom' → DynamicBOMSection (기존 그대로) -section.type === 'table' → DynamicTableSection (신규) -``` - -**점진적 마이그레이션**: BOM도 나중에 `type: 'table'`로 전환 가능하지만, 당장은 불필요. - -### 4.5 테이블 셀 = 필드 컴포넌트 재사용 - -테이블 각 셀의 입력은 **DynamicFieldRenderer와 동일한 컴포넌트**를 사용: - -``` -table column.type === "reference" → ReferenceField (인라인 축소판) -table column.type === "number" → NumberField -table column.type === "dropdown" → DropdownField -table column.type === "date" → DateField -table column.type === "currency" → CurrencyField -... (14종 모두 사용 가능) -``` - -즉, **필드 타입 컴포넌트 1개 = 폼 필드에서도, 테이블 셀에서도 동일하게 사용**. - -### 4.6 테이블 섹션 저장 데이터 구조 - -```jsonc -{ - "table_section_123": [ - { - "_rowId": "uuid-1", - "process_code": "CUT-001", - "process_name": "절단", - "quantity": 10, - "unit": "EA", - "start_date": "2026-03-01", - "note": "레이저 절단" - } - ] -} -``` - ---- - -## 5. 섹션 템플릿 라이브러리 - -### 5.1 전체 프리셋 목록 (산업 공통 + 산업별) - -#### 공통 프리셋 - -| 프리셋 ID | 이름 | type | 설명 | -|----------|------|------|------| -| `basic-info` | 기본정보 | fields | 코드, 이름, 유형, 상태, 비고 | -| `drawing` | 도면/문서 | fields | 파일 업로드 + 버전 관리 | -| `custom` | 사용자 정의 | fields/table | 빈 섹션 (직접 구성) | - -#### 제조 프리셋 - -| 프리셋 ID | 이름 | type | 설명 | -|----------|------|------|------| -| `specifications` | 규격/치수 | fields | 두께, 너비, 높이, 무게, 공차 | -| `bom` | BOM (자재명세서) | bom | 기존 BOM 구조 유지 | -| `routing` | 공정/라우팅 | table | 공정코드, 작업시간, 작업장 | -| `quality-spec` | 품질검사 항목 | table | 검사항목, 규격, 허용치, 검사방법 | -| `procurement` | 구매정보 | table | 공급업체, 단가, 리드타임, MOQ | -| `cost-breakdown` | 원가 구성 | table | 원가항목, 금액, 비율 | -| `inventory` | 재고 정보 | fields | 창고, 안전재고, 발주점 | - -#### 공사 프리셋 - -| 프리셋 ID | 이름 | type | 설명 | -|----------|------|------|------| -| `work-schedule` | 공정표 | table | 공종, 수량, 단가, 착수일, 완료일, 진행률 | -| `material-plan` | 자재투입계획 | table | 자재, 수량, 단위, 투입시기, 발주여부 | -| `labor-plan` | 인력투입계획 | table | 직종, 인원, 기간, 일단가, 금액 | -| `equipment-plan` | 장비투입계획 | table | 장비명, 규격, 수량, 기간, 단가 | -| `safety-checklist` | 안전점검 항목 | table | 점검항목, 점검주기, 담당자, 결과 | -| `site-info` | 현장정보 | fields | 현장명, 주소, 발주처, 감리사, 공사기간 | - -#### 유통 프리셋 - -| 프리셋 ID | 이름 | type | 설명 | -|----------|------|------|------| -| `pricing` | 가격정보 | table | 거래처유형, 단가, 할인율, 적용기간 | -| `packaging` | 포장정보 | fields | 포장단위, 박스수량, 바코드, 중량 | -| `store-allocation` | 매장배분 | table | 매장, 배분수량, 배분일, 상태 | -| `promotion` | 프로모션 | table | 프로모션명, 할인율, 시작일, 종료일, 조건 | - -#### 물류 프리셋 - -| 프리셋 ID | 이름 | type | 설명 | -|----------|------|------|------| -| `transport-spec` | 운송규격 | fields | 중량, 부피, 위험물등급, 보관온도, 적재방법 | -| `route-plan` | 배차/경로 | table | 출발지, 도착지, 거리, 소요시간, 운임 | -| `loading-plan` | 적재계획 | table | 품목, 수량, 중량, 적재순서, 위치 | -| `tracking` | 추적정보 | table | 일시, 위치, 상태, 온도, 비고 | - -### 5.2 프리셋 상세 예시 - -#### `work-schedule` (공사 — 공정표) - -```jsonc -{ - "preset_id": "work-schedule", - "type": "table", - "table_config": { - "columns": [ - { "key": "work_type", "label": "공종", "type": "reference", "width": "180px", - "config": { "source": "custom", "searchApiUrl": "/api/proxy/work-types", - "displayField": "name" }, "required": true }, - { "key": "quantity", "label": "수량", "type": "number", "width": "100px", - "config": { "min": 0, "precision": 2 } }, - { "key": "unit", "label": "단위", "type": "dropdown", "width": "80px", - "config": { "options": [ - {"label":"m²","value":"m2"}, {"label":"m³","value":"m3"}, - {"label":"m","value":"m"}, {"label":"EA","value":"EA"}, - {"label":"TON","value":"TON"}, {"label":"식","value":"SET"} - ]}}, - { "key": "unit_price", "label": "단가", "type": "currency", "width": "130px", - "config": { "currency": "KRW" } }, - { "key": "amount", "label": "금액", "type": "computed", "width": "140px", - "config": { "formula": "{quantity} * {unit_price}", "format": "currency" } }, - { "key": "start_date", "label": "착수일", "type": "date", "width": "130px" }, - { "key": "end_date", "label": "완료일", "type": "date", "width": "130px" }, - { "key": "progress", "label": "진행률(%)", "type": "number", "width": "100px", - "config": { "min": 0, "max": 100 } }, - { "key": "note", "label": "비고", "type": "textbox" } - ], - "addable": true, - "deletable": true, - "reorderable": true, - "summaryRow": { - "enabled": true, - "columns": { "amount": { "type": "sum", "format": "currency" } } - }, - "emptyMessage": "공정 항목을 추가하세요." - } -} -``` - -#### `route-plan` (물류 — 배차/경로) - -```jsonc -{ - "preset_id": "route-plan", - "type": "table", - "table_config": { - "columns": [ - { "key": "seq", "label": "순번", "type": "number", "width": "70px" }, - { "key": "origin", "label": "출발지", "type": "reference", "width": "180px", - "config": { "source": "custom", "searchApiUrl": "/api/proxy/locations", - "displayField": "name" }, "required": true }, - { "key": "destination", "label": "도착지", "type": "reference", "width": "180px", - "config": { "source": "custom", "searchApiUrl": "/api/proxy/locations", - "displayField": "name" }, "required": true }, - { "key": "distance", "label": "거리", "type": "unit-value", "width": "120px", - "config": { "units": ["km", "m"], "defaultUnit": "km", "precision": 1 } }, - { "key": "duration", "label": "소요시간(분)", "type": "number", "width": "110px" }, - { "key": "vehicle", "label": "차량", "type": "reference", "width": "150px", - "config": { "source": "vehicles", "displayField": "plate_number" } }, - { "key": "freight", "label": "운임", "type": "currency", "width": "130px", - "config": { "currency": "KRW" } }, - { "key": "note", "label": "비고", "type": "textbox" } - ], - "addable": true, - "deletable": true, - "reorderable": true, - "emptyMessage": "경로를 추가하세요." - } -} -``` - -#### `pricing` (유통 — 가격정보) - -```jsonc -{ - "preset_id": "pricing", - "type": "table", - "table_config": { - "columns": [ - { "key": "customer_type", "label": "거래처유형", "type": "dropdown", "width": "140px", - "config": { "options": [ - {"label":"도매","value":"WHOLESALE"}, {"label":"소매","value":"RETAIL"}, - {"label":"온라인","value":"ONLINE"}, {"label":"특판","value":"SPECIAL"} - ]}, "required": true }, - { "key": "unit_price", "label": "단가", "type": "currency", "width": "130px", - "config": { "currency": "KRW" }, "required": true }, - { "key": "discount_rate", "label": "할인율(%)", "type": "number", "width": "100px", - "config": { "min": 0, "max": 100, "precision": 1 } }, - { "key": "final_price", "label": "최종가", "type": "computed", "width": "130px", - "config": { "formula": "{unit_price} * (1 - {discount_rate}/100)", "format": "currency" } }, - { "key": "valid_from", "label": "적용시작", "type": "date", "width": "130px" }, - { "key": "valid_to", "label": "적용종료", "type": "date", "width": "130px" }, - { "key": "note", "label": "비고", "type": "textbox" } - ], - "addable": true, - "deletable": true, - "emptyMessage": "가격 정보를 추가하세요." - } -} -``` - ---- - -## 6. 산업별 확장 구조 - -### 6.1 핵심 개념: 4-Level 아키텍처 - -``` -┌──────────────────────────────────────────────────────────────┐ -│ Level 1: 필드 타입 컴포넌트 (14종) │ -│ ─────────────────────────────────────────────────────────── │ -│ 코드 레벨. 거의 안 바뀜. │ -│ textbox | number | dropdown | checkbox | date | textarea │ -│ reference | multi-select | file | currency | unit-value │ -│ radio | toggle | computed │ -│ │ -│ → UI 입력의 "원자(atom)" 단위. 모든 산업의 입력 형태를 커버. │ -│ → 새 컴포넌트 추가 = 완전히 새로운 입력 패러다임이 등장할 때만. │ -└──────────────────────────────────────────────────────────────┘ - ▼ -┌──────────────────────────────────────────────────────────────┐ -│ Level 2: properties config (JSON) │ -│ ─────────────────────────────────────────────────────────── │ -│ 설정 레벨. 필드 타입의 동작을 결정. 코드 변경 없음. │ -│ │ -│ 같은 "reference" 타입이지만: │ -│ 제조: { "source": "processes" } → 공정 선택 │ -│ 공사: { "source": "custom", │ -│ "searchApiUrl": "/api/proxy/work-types" } → 공종선택 │ -│ 물류: { "source": "vehicles" } → 차량 선택 │ -│ 유통: { "source": "stores" } → 매장 선택 │ -│ │ -│ 같은 "unit-value" 타입이지만: │ -│ 제조: { "units": ["mm","cm","m","inch"] } → 치수 │ -│ 물류: { "units": ["km","m"] } → 거리 │ -│ 유통: { "units": ["g","kg","ton"] } → 중량 │ -└──────────────────────────────────────────────────────────────┘ - ▼ -┌──────────────────────────────────────────────────────────────┐ -│ Level 3: 섹션 프리셋 (JSON) │ -│ ─────────────────────────────────────────────────────────── │ -│ 템플릿 레벨. 산업별 표준 섹션 구성. 코드 변경 없음. │ -│ │ -│ 제조: basic-info + specifications + bom + routing + quality │ -│ 공사: basic-info + site-info + work-schedule + material-plan │ -│ 유통: basic-info + packaging + pricing + store-allocation │ -│ 물류: basic-info + transport-spec + route-plan + loading-plan │ -│ │ -│ → 관리자가 "섹션 추가" → 프리셋 선택 → 자동 구성 │ -│ → 새 프리셋 = JSON 추가만, 컴포넌트 수정 없음 │ -└──────────────────────────────────────────────────────────────┘ - ▼ -┌──────────────────────────────────────────────────────────────┐ -│ Level 4: reference sources (API URL) │ -│ ─────────────────────────────────────────────────────────── │ -│ 연결 레벨. 새 데이터 소스를 reference 필드에 연결. 코드 변경 없음. │ -│ │ -│ 새 산업/도메인 추가 시: │ -│ 1. 백엔드에 API 추가 (예: /api/proxy/work-types) │ -│ 2. reference 필드에 source: "custom" + searchApiUrl 설정 │ -│ 3. 끝. 프론트 코드 수정 없음. │ -│ │ -│ 자주 사용되는 source는 프리셋으로 등록: │ -│ reference-sources.ts에 추가 → source: "work_types"로 단축 │ -└──────────────────────────────────────────────────────────────┘ -``` - -### 6.2 산업별 변경 범위 매트릭스 - -| 변경 항목 | 코드 변경 | DB 변경 | config 변경 | -|----------|----------|---------|------------| -| 새 필드 타입 추가 | ✅ 컴포넌트 1개 | ✅ field_type 값 추가 | — | -| 새 reference 소스 추가 | ❌ | ✅ API 엔드포인트 | ✅ source config | -| 새 테이블 섹션 구성 | ❌ | ✅ section + properties | ✅ table_config JSON | -| 새 섹션 프리셋 추가 | ❌ (또는 프리셋 JSON 1건) | ❌ | ✅ 프리셋 JSON | -| 새 산업 진출 | ❌ | ✅ API들 | ✅ 프리셋 + source | -| 테넌트별 커스터마이징 | ❌ | ✅ 테넌트 config | ❌ | - -> **핵심**: "새 산업 진출" 시에도 프론트엔드 코드 변경 = 0줄. -> 백엔드 API + config JSON만 추가. - -### 6.3 산업별 구성 예시 - -#### 제조업 테넌트 (금속 가공) - -``` -페이지: 제품(FG) 등록 -├── 기본정보 (fields) -│ ├── 품목코드 [textbox, required] -│ ├── 품목명 [textbox, required] -│ ├── 품목유형 [dropdown: FG/PT/SM/RM/CS] -│ ├── 단위 [dropdown: EA/SET/M] -│ └── 상태 [toggle: 활성/비활성] -├── 규격/치수 (fields) -│ ├── 두께 [unit-value: mm/cm/inch] -│ ├── 너비 [unit-value: mm/cm/m] -│ ├── 높이 [unit-value: mm/cm/m] -│ ├── 무게 [unit-value: g/kg/ton] -│ ├── 재질 [reference → materials] -│ └── 표면처리 [reference → surface_treatments] -├── BOM (bom) — 기존 유지 -├── 공정/라우팅 (table) -│ └── [공정, 작업장, 셋업시간, 사이클타임, 비고] -├── 품질검사 (table) -│ └── [검사항목, 규격, 상한, 하한, 검사방법, 측정장비] -└── 도면 (fields) - ├── 도면파일 [file: .pdf/.dwg/.dxf] - ├── 도면번호 [textbox] - └── 도면버전 [textbox] -``` - -#### 공사관리 테넌트 (건설) - -``` -페이지: 공사 항목 등록 -├── 기본정보 (fields) -│ ├── 항목코드 [textbox, required] -│ ├── 항목명 [textbox, required] -│ ├── 공사구분 [dropdown: 토목/건축/설비/전기] -│ └── 상태 [toggle] -├── 현장정보 (fields) -│ ├── 현장 [reference → sites] -│ ├── 발주처 [reference → customers] -│ ├── 감리사 [reference → vendors] -│ ├── 착공일 [date] -│ ├── 준공예정일 [date] -│ └── 공사금액 [currency: KRW] -├── 공정표 (table) -│ └── [공종, 수량, 단위, 단가, 금액(computed), 착수일, 완료일, 진행률] -├── 자재투입계획 (table) -│ └── [자재(reference→items), 수량, 단위, 투입시기, 발주여부(checkbox)] -├── 인력투입계획 (table) -│ └── [직종, 인원, 기간(일), 일단가(currency), 금액(computed)] -└── 안전점검 (table) - └── [점검항목, 점검주기(dropdown), 담당자(reference→employees), 최근점검일(date)] -``` - -#### 유통업 테넌트 (도소매) - -``` -페이지: 상품 등록 -├── 기본정보 (fields) -│ ├── 상품코드 [textbox, required] -│ ├── 상품명 [textbox, required] -│ ├── 카테고리 [reference → categories] -│ ├── 브랜드 [reference → brands] -│ └── 상태 [toggle] -├── 포장정보 (fields) -│ ├── 포장단위 [dropdown: 낱개/박스/팔레트] -│ ├── 입수량 [number] -│ ├── 바코드 [textbox] -│ ├── 중량 [unit-value: g/kg] -│ └── 상품이미지 [file: .jpg/.png, maxFiles:5] -├── 가격정보 (table) -│ └── [거래처유형, 단가, 할인율, 최종가(computed), 적용기간] -├── 매장배분 (table) -│ └── [매장(reference→stores), 배분수량, 배분일, 상태(dropdown)] -└── 프로모션 (table) - └── [프로모션명, 할인율, 시작일, 종료일, 적용조건] -``` - -#### 물류업 테넌트 (운송) - -``` -페이지: 화물 등록 -├── 기본정보 (fields) -│ ├── 화물코드 [textbox, required] -│ ├── 화물명 [textbox, required] -│ ├── 화물유형 [dropdown: 일반/냉장/냉동/위험물] -│ └── 상태 [toggle] -├── 운송규격 (fields) -│ ├── 총중량 [unit-value: kg/ton] -│ ├── 부피 [unit-value: m³/CBM] -│ ├── 위험물등급 [dropdown: 1~9등급/해당없음] -│ ├── 보관온도 [unit-value: ℃] -│ ├── 적재방법 [radio: 팔레트/산적/컨테이너] -│ └── 특수요구사항 [textarea] -├── 배차/경로 (table) -│ └── [출발지, 도착지, 거리, 소요시간, 차량(reference), 운임(currency)] -├── 적재계획 (table) -│ └── [품목(reference→items), 수량, 중량, 적재순서, 위치] -└── 추적정보 (table) - └── [일시(date), 위치, 상태(dropdown), 온도(number), 비고] -``` - -### 6.4 코드 변경이 필요한 예외 케이스 - -14종 필드 타입으로 커버 불가능한 **완전히 새로운 입력 패러다임**: - -| 새 입력 패러다임 | 필요한 field_type | 작업량 | -|----------------|-----------------|-------| -| 지도 위치 선택 (GPS 좌표) | `map-picker` | 컴포넌트 1개 + switch 1줄 | -| 간트차트 편집 | `gantt` (섹션 타입) | 섹션 컴포넌트 1개 | -| 전자서명/도장 | `signature` | 컴포넌트 1개 + switch 1줄 | -| 바코드/QR 스캔 | `barcode-scan` | 컴포넌트 1개 + switch 1줄 | -| 조직도 선택 | `org-chart-picker` | 컴포넌트 1개 + switch 1줄 | -| 색상 선택 (컬러피커) | `color-picker` | 컴포넌트 1개 + switch 1줄 | - -> 이런 경우에도 **컴포넌트 1개 파일 추가 + switch문 1줄 추가**로 끝. -> 기존 코드 수정 없음. 다른 필드 타입에 영향 없음. - -### 6.5 확장 가능성 요약 - -``` -Q: 새 산업(예: 의료)을 추가하려면? -A: 프론트 코드 변경 0줄. - 1. 백엔드에 의료 도메인 API 추가 (환자, 의약품, 의료기기 등) - 2. reference source config 추가 (JSON) - 3. 의료 섹션 프리셋 추가 (JSON) - 4. 테넌트 생성 시 의료 프리셋 자동 적용 - -Q: 새 필드 타입(예: 지도)이 필요하면? -A: MapPickerField.tsx 1개 생성 + switch 1줄 추가. - 기존 14종 컴포넌트/기존 config/기존 프리셋 전부 영향 없음. - -Q: 기존 테넌트가 산업을 추가하면? (제조 + 물류 겸업) -A: 해당 테넌트의 페이지에 물류 프리셋 섹션만 추가. - 코드 변경 없음. Admin UI에서 클릭으로 완료. -``` - ---- - -## 7. 백엔드 API 계약 - -### 7.1 field_type 확장 — DB 변경 - -```sql --- item_fields 테이블의 field_type 컬럼 확장 --- 기존: ENUM('textbox','number','dropdown','checkbox','date','textarea') --- 변경: VARCHAR(30) 또는 ENUM 확장 - -ALTER TABLE item_fields -MODIFY COLUMN field_type VARCHAR(30) NOT NULL DEFAULT 'textbox'; - --- 허용 값: textbox, number, dropdown, checkbox, date, textarea, --- reference, multi-select, file, currency, unit-value, radio, toggle, computed --- 향후 추가 가능: map-picker, signature, barcode-scan 등 -``` - -### 7.2 section type 확장 - -```sql -ALTER TABLE item_sections -MODIFY COLUMN type VARCHAR(20) NOT NULL DEFAULT 'fields'; - --- 허용 값: fields, bom, table --- 향후 추가 가능: gantt, calendar 등 -``` - -### 7.3 properties 필드 활용 - -**기존 `properties` 컬럼** (`JSON` 타입)이 이미 `item_fields`와 `item_sections` 테이블에 존재함. -신규 필드 타입의 config는 이 컬럼에 저장. - -```sql --- reference 타입 필드 -UPDATE item_fields SET - field_type = 'reference', - properties = '{"source":"vendors","displayField":"name","searchFields":["name","code"]}' -WHERE id = 100; - --- table 타입 섹션 -UPDATE item_sections SET - type = 'table', - properties = '{"table_config":{"columns":[...],"addable":true}}' -WHERE id = 50; -``` - -### 7.4 Init API / Page Structure API — 변경 없음 - -기존 응답 구조 그대로. `field_type`과 `properties`에 새로운 값이 들어올 뿐. - -```jsonc -// GET /v1/item-master/pages/{id}/structure — 응답 구조 동일 -{ - "page": { ... }, - "sections": [ - { - "section": { "type": "fields", ... }, - "fields": [ - { "field": { "field_type": "reference", "properties": { "source": "vendors" } } } - ] - }, - { - "section": { - "type": "table", - "properties": { "table_config": { "columns": [...] } } - }, - "fields": [], - "bom_items": [] - } - ] -} -``` - -### 7.5 테이블 섹션 데이터 CRUD API (신규) - -``` -GET /v1/items/{itemId}/section-data/{sectionId} -→ { "data": [{ "process": "CUT-001", "cycle_time": 5.0, ... }, ...] } - -PUT /v1/items/{itemId}/section-data/{sectionId} -← { "rows": [{ "process": "CUT-001", "cycle_time": 5.0, ... }, ...] } - -POST /v1/items/{itemId}/section-data/{sectionId} -← { "process": "CUT-001", "cycle_time": 5.0, ... } - -DELETE /v1/items/{itemId}/section-data/{sectionId}/{rowId} -``` - -### 7.6 Reference 필드 검색 API - -기존 API 활용 + custom source: - -| source | API | 비고 | -|--------|-----|------| -| vendors | `GET /v1/vendors?search={q}&size=20` | 기존 | -| items | `GET /v1/items?search={q}&size=20` | 기존 | -| customers | `GET /v1/customers?search={q}&size=20` | 기존 | -| employees | `GET /v1/employees?search={q}&size=20` | 기존 | -| processes | `GET /v1/processes?search={q}&size=20` | 기존 | -| warehouses | `GET /v1/warehouses?search={q}&size=20` | 기존 | -| materials | `GET /v1/item-master/materials?search={q}` | 기존 | -| custom | `properties.searchApiUrl?search={q}&size=20` | **신규 산업별 API** | - -> 새 산업 추가 시: 해당 도메인 API 생성 → source: "custom" + searchApiUrl 설정 - -### 7.7 파일 업로드 API - -``` -POST /v1/files/upload ← multipart/form-data -GET /v1/files/{fileId} → binary -DELETE /v1/files/{fileId} -``` - ---- - -## 8. 프론트엔드 컴포넌트 아키텍처 - -### 8.1 파일 구조 (신규 추가분) - -``` -DynamicItemForm/ -├── fields/ -│ ├── DynamicFieldRenderer.tsx # 수정: switch문 확장 -│ ├── TextField.tsx # 기존 유지 -│ ├── NumberField.tsx # 기존 유지 -│ ├── DropdownField.tsx # 기존 유지 -│ ├── CheckboxField.tsx # 기존 유지 -│ ├── DateField.tsx # 기존 유지 -│ ├── TextareaField.tsx # 기존 유지 -│ ├── ReferenceField.tsx # ★ 신규 Phase 1 -│ ├── MultiSelectField.tsx # ★ 신규 Phase 1 -│ ├── FileField.tsx # ★ 신규 Phase 1 -│ ├── CurrencyField.tsx # ★ 신규 Phase 2 -│ ├── UnitValueField.tsx # ★ 신규 Phase 2 -│ ├── RadioField.tsx # ★ 신규 Phase 2 -│ ├── ToggleField.tsx # ★ 신규 Phase 3 -│ └── ComputedField.tsx # ★ 신규 Phase 3 -├── sections/ -│ ├── DynamicBOMSection.tsx # 기존 유지 -│ ├── DynamicTableSection.tsx # ★ 신규 Phase 1 -│ └── TableCellRenderer.tsx # ★ 신규 Phase 1 -├── presets/ -│ ├── index.ts # ★ 신규: 프리셋 레지스트리 -│ └── section-presets.ts # ★ 신규: 전 산업 프리셋 정의 -└── config/ - └── reference-sources.ts # ★ 신규: 참조 소스 프리셋 -``` - -### 8.2 DynamicFieldRenderer 확장 - -```typescript -export function DynamicFieldRenderer(props: DynamicFieldRendererProps) { - switch (props.field.field_type) { - // 기존 6종 (변경 없음) - case 'textbox': return ; - case 'number': return ; - case 'dropdown': return ; - case 'checkbox': return ; - case 'date': return ; - case 'textarea': return ; - // Phase 1 - case 'reference': return ; - case 'multi-select': return ; - case 'file': return ; - // Phase 2 - case 'currency': return ; - case 'unit-value': return ; - case 'radio': return ; - // Phase 3 - case 'toggle': return ; - case 'computed': return ; - default: - return ; - } -} -``` - -### 8.3 테이블 셀 = 필드 컴포넌트 재사용 - -```typescript -// sections/TableCellRenderer.tsx -// DynamicFieldRenderer를 테이블 셀용으로 래핑 (축소 UI) -export function TableCellRenderer({ column, value, onChange }: TableCellProps) { - // column config → ItemFieldResponse 호환 객체로 변환 - const fieldLike: ItemFieldResponse = { - field_type: column.type, - field_name: column.label, - properties: column.config, - options: column.config?.options, - is_required: column.required || false, - // ... 최소 필수 필드 - }; - - return ( - - ); -} -``` - -### 8.4 참조 소스 프리셋 - -```typescript -// config/reference-sources.ts -export const REFERENCE_SOURCES: Record = { - // 공통 - vendors: { apiUrl: '/api/proxy/vendors', displayField: 'name', ... }, - items: { apiUrl: '/api/proxy/items', displayField: 'name', ... }, - customers: { apiUrl: '/api/proxy/customers', displayField: 'company_name', ... }, - employees: { apiUrl: '/api/proxy/employees', displayField: 'name', ... }, - warehouses: { apiUrl: '/api/proxy/warehouses', displayField: 'name', ... }, - // 제조 - processes: { apiUrl: '/api/proxy/processes', displayField: 'process_name', ... }, - materials: { apiUrl: '/api/proxy/item-master/materials', displayField: 'material_name', ... }, - surface_treatments: { apiUrl: '/api/proxy/item-master/surface-treatments', ... }, - // 공사 - sites: { apiUrl: '/api/proxy/sites', displayField: 'site_name', ... }, - // 물류 - vehicles: { apiUrl: '/api/proxy/vehicles', displayField: 'plate_number', ... }, - routes: { apiUrl: '/api/proxy/routes', displayField: 'route_name', ... }, - // 유통 - stores: { apiUrl: '/api/proxy/stores', displayField: 'store_name', ... }, -}; -// "custom" source → properties.searchApiUrl 직접 사용 -``` - ---- - -## 9. 조건부 표시 확장 - -### 9.1 현재 (유지) - -```jsonc -{ "fieldKey": "item_type", "expectedValue": "FG", "targetFieldIds": ["150"] } -``` - -### 9.2 확장: 비교 연산자 지원 - -```jsonc -{ "fieldKey": "item_type", "operator": "in", "expectedValue": ["FG","PT"], "targetFieldIds": ["150"] } -``` - -| operator | 설명 | 하위 호환 | -|----------|------|----------| -| `equals` | 같음 (기본) | ✅ operator 없으면 equals | -| `not_equals` | 다름 | | -| `in` | 배열 포함 | | -| `not_in` | 배열 미포함 | | -| `is_empty` | 비어있음 | | -| `is_not_empty` | 비어있지 않음 | | -| `greater_than` | 초과 | | -| `less_than` | 미만 | | - ---- - -## 10. 검증 프레임워크 - -### 10.1 필드 타입별 추가 검증 - -| field_type | 추가 검증 | -|-----------|----------| -| `reference` | 선택 항목 존재 여부 | -| `multi-select` | maxSelections 초과 | -| `file` | maxSize, accept 타입, maxFiles | -| `currency` | allowNegative, precision | -| `unit-value` | 값이 숫자, 단위가 유효 | -| `computed` | 검증 없음 (자동 계산) | - -### 10.2 테이블 섹션 검증 - -```typescript -function validateTableRows(rows, columns): string[] { - const errors = []; - rows.forEach((row, idx) => { - columns.forEach(col => { - if (col.required && !row[col.key]) { - errors.push(`${idx + 1}행: ${col.label}은(는) 필수입니다.`); - } - }); - }); - return errors; -} -``` - ---- - -## 11. 구현 로드맵 - -### Phase 1: 핵심 확장 (🔴) - -| 작업 | 예상 줄 수 | -|------|-----------| -| ReferenceField | ~200 | -| MultiSelectField | ~120 | -| FileField | ~180 | -| DynamicTableSection + TableCellRenderer | ~450 | -| reference-sources.ts | ~120 | -| 타입 정의 확장 | +50 | -| DynamicFieldRenderer switch 확장 | +10 | -| **총** | **~1,130줄 신규, ~30줄 수정** | - -### Phase 2: 편의 필드 (🟡) - -| 작업 | 예상 줄 수 | -|------|-----------| -| CurrencyField | ~80 | -| UnitValueField | ~100 | -| RadioField | ~60 | -| 섹션 프리셋 라이브러리 (전 산업) | ~400 | -| 프리셋 선택 UI | +100 | -| **총** | **~740줄 신규** | - -### Phase 3: 고급 필드 (🟢) - -| 작업 | 예상 줄 수 | -|------|-----------| -| ToggleField | ~50 | -| ComputedField | ~120 | -| 조건부 표시 연산자 확장 | +40 | -| 테이블 검증 강화 | +60 | -| **총** | **~270줄 신규** | - -### 백엔드 작업 (프론트와 병렬) - -| 작업 | 설명 | -|------|------| -| field_type 컬럼 확장 | VARCHAR(30) | -| section type 확장 | 'table' 추가 | -| 테이블 데이터 API | section-data CRUD | -| 산업별 도메인 API | 해당 산업 진출 시 추가 | -| 프리셋 시딩 | 테넌트 생성 시 산업별 프리셋 자동 적용 | - ---- - -## 부록 - -### A. 기존 코드 영향 분석 - -| 기존 파일 | 변경 | 내용 | -|----------|------|------| -| `DynamicFieldRenderer.tsx` | switch 추가 | +8 case문 | -| `DynamicItemForm/index.tsx` | 섹션 렌더링 | +10줄 (table case) | -| `types.ts` | 타입 확장 | field_type union + 신규 인터페이스 | -| `item-master-api.ts` | field_type 확장 | union 값 추가 | -| **기존 필드 컴포넌트 6개** | **변경 없음** | | -| **DynamicBOMSection** | **변경 없음** | | -| **hooks 7개** | **변경 없음** | | - -### B. 전체 아키텍처 다이어그램 - -``` -┌───────────────────────────────────────────────────────────┐ -│ Admin (품목기준관리) │ -│ → 산업 선택 → 프리셋 선택 → 필드 config 설정 │ -└────────────────────┬──────────────────────────────────────┘ - │ 저장 (field_type + properties JSON) - ▼ -┌───────────────────────────────────────────────────────────┐ -│ 백엔드 DB │ -│ item_pages → item_sections → item_fields │ -│ type: fields/bom/table │ -│ properties: { table_config / field config } │ -│ │ -│ field_type (14종): 모든 산업의 입력 원자 단위 │ -│ properties (JSON): 산업/테넌트별 동작 결정 │ -└────────────────────┬──────────────────────────────────────┘ - │ 조회 (기존 API 구조 그대로) - ▼ -┌───────────────────────────────────────────────────────────┐ -│ User (품목 등록/수정) │ -│ DynamicItemForm │ -│ ├─ DynamicFieldRenderer (14종 switch) │ -│ │ └─ 각 컴포넌트가 properties를 읽어 동작 결정 │ -│ ├─ DynamicBOMSection (기존 유지) │ -│ └─ DynamicTableSection (columns config 기반 렌더링) │ -│ └─ TableCellRenderer → DynamicFieldRenderer 재사용 │ -└───────────────────────────────────────────────────────────┘ -``` - ---- - -**문서 버전**: 1.1 (산업별 확장 구조 추가) -**마지막 업데이트**: 2026-02-11 -**다음 단계**: 백엔드 검토 → Phase 1 구현 착수 diff --git a/claudedocs/architecture/[FIX-2026-01-29] masterdata-cache-tenant-isolation.md b/claudedocs/architecture/[FIX-2026-01-29] masterdata-cache-tenant-isolation.md deleted file mode 100644 index 2438f58a..00000000 --- a/claudedocs/architecture/[FIX-2026-01-29] masterdata-cache-tenant-isolation.md +++ /dev/null @@ -1,145 +0,0 @@ -# masterDataStore 캐시 테넌트 격리 수정 - -**작성일**: 2026-01-29 -**타입**: 버그 수정 (캐시 격리 누락) -**관련 문서**: `[REF-2025-11-19] multi-tenancy-implementation.md` - ---- - -## 배경 - -멀티테넌시 검토 결과, `TenantAwareCache`(`mes-{tenantId}-{key}`)는 테넌트별로 캐시가 격리되어 있지만, `masterDataStore`의 sessionStorage 캐시는 테넌트 구분 없이 `page_config_{pageType}` 키를 사용하고 있었음. - -추가로 `setCurrentTenantId` 액션이 인터페이스에만 선언되어 있고 **구현도, 호출도 없는** dead code 상태였음. - ---- - -## 문제 - -### 1. 캐시 키에 tenantId 미포함 - -``` -TenantAwareCache: mes-282-itemMasters ← 테넌트 격리됨 -masterDataStore: page_config_item-master ← 테넌트 격리 안됨 -``` - -### 2. 발생 가능한 시나리오 - -``` -1. 테넌트 282 사용자가 품목관리 접속 - → sessionStorage: page_config_item-master = {테넌트282 설정} - -2. 세션 내 테넌트 500으로 전환 (로그아웃 없이) - → clearTenantCache()는 mes-282-* 만 삭제 - → page_config_item-master 는 삭제되지 않음 - -3. 테넌트 500 사용자에게 테넌트 282의 페이지 설정이 노출 -``` - -### 3. setCurrentTenantId 미구현 - -```typescript -// 인터페이스에 선언만 있고 구현 없음 -interface MasterDataStore { - currentTenantId: number | null; // ← initialState에도 누락 - setCurrentTenantId: (tenantId: number | null) => void; // ← 구현 없음 -} -``` - ---- - -## 수정 내역 - -### masterDataStore.ts - -| 영역 | Before | After | -|------|--------|-------| -| initialState | `currentTenantId` 누락 | `currentTenantId: null` 추가 | -| 캐시 키 포맷 | `page_config_{pageType}` | `page_config_{tenantId}_{pageType}` | -| setCurrentTenantId | 인터페이스만 선언 | 구현 추가 | -| fetchPageConfig | tenantId 미사용 | `currentTenantId`를 캐시 함수에 전달 | -| invalidateConfig | tenantId 미사용 | `currentTenantId` 기반 삭제 | -| invalidateAllConfigs | tenantId 미사용 | `currentTenantId` 기반 삭제 | -| reset() | pageType 목록 순회 삭제 | `page_config_` 프리픽스 기반 전체 삭제 | - -#### 핵심 변경: 캐시 키 생성 함수 추가 - -```typescript -function getStorageKey(tenantId: number | null, pageType: PageType): string { - return tenantId != null - ? `${STORAGE_PREFIX}${tenantId}_${pageType}` // page_config_282_item-master - : `${STORAGE_PREFIX}${pageType}`; // page_config_item-master (하위 호환) -} -``` - -#### 핵심 변경: reset()을 프리픽스 기반으로 변경 - -```typescript -// Before: 고정된 pageType 목록으로 삭제 (tenantId 포함 키를 찾지 못함) -pageTypes.forEach((pt) => removeConfigFromSessionStorage(pt)); - -// After: page_config_ 프리픽스로 모든 테넌트 캐시 일괄 삭제 -Object.keys(window.sessionStorage).forEach(key => { - if (key.startsWith(STORAGE_PREFIX)) { - window.sessionStorage.removeItem(key); - } -}); -``` - -### AuthContext.tsx - -| 영역 | Before | After | -|------|--------|-------| -| import | - | `useMasterDataStore` 추가 | -| tenantId 동기화 | 없음 | `currentUser.tenant.id` 변경 시 `setCurrentTenantId()` 호출 | -| clearTenantCache | `mes-{tenantId}-*` 만 삭제 | `mes-{tenantId}-*` + `page_config_{tenantId}_*` 삭제 | - -#### 핵심 변경: tenantId 동기화 useEffect - -```typescript -useEffect(() => { - const tenantId = currentUser?.tenant?.id ?? null; - useMasterDataStore.getState().setCurrentTenantId(tenantId); -}, [currentUser?.tenant?.id]); -``` - -#### 핵심 변경: clearTenantCache 범위 확장 - -```typescript -const tenantAwarePrefix = `mes-${tenantId}-`; -const pageConfigPrefix = `page_config_${tenantId}_`; - -Object.keys(sessionStorage).forEach(key => { - if (key.startsWith(tenantAwarePrefix) || key.startsWith(pageConfigPrefix)) { - sessionStorage.removeItem(key); - } -}); -``` - ---- - -## 하위 호환 - -| 항목 | 영향 | -|------|------| -| 기존 캐시 키 | `page_config_item-master` → 키 불일치로 miss → API 재요청 → 새 포맷으로 자동 전환 | -| logout.ts | `page_config_` 프리픽스 매칭이 새 키 포맷(`page_config_282_item-master`)도 커버 | -| sessionStorage TTL | 10분 만료이므로 기존 키는 자연 소멸 | -| tenantId가 null인 경우 | 기존 포맷(`page_config_{pageType}`) 유지하여 동작 보장 | - ---- - -## 효과 - -1. **세션 내 테넌트 전환 시 캐시 누수 차단**: `clearTenantCache`가 `page_config_{tenantId}_*`까지 삭제 -2. **캐시 패턴 일관성**: TenantAwareCache(`mes-{tenantId}-`)와 masterDataStore(`page_config_{tenantId}_`) 모두 테넌트 격리 -3. **dead code 해소**: `currentTenantId` 필드와 `setCurrentTenantId` 액션이 실제로 동작 - ---- - -## 관련 파일 - -- `src/stores/masterDataStore.ts` - 캐시 키 변경, setCurrentTenantId 구현 -- `src/contexts/AuthContext.tsx` - tenantId 동기화, clearTenantCache 범위 확장 -- `src/lib/auth/logout.ts` - 기존 `page_config_` 프리픽스 매칭 (변경 없음, 호환 확인) -- `src/lib/cache/TenantAwareCache.ts` - 참고 (기존 정상 동작) diff --git a/claudedocs/architecture/[GUIDE] component-tier-definition.md b/claudedocs/architecture/[GUIDE] component-tier-definition.md deleted file mode 100644 index dafbf5e5..00000000 --- a/claudedocs/architecture/[GUIDE] component-tier-definition.md +++ /dev/null @@ -1,153 +0,0 @@ -# Component Tier 정의 - -> SAM 프로젝트의 컴포넌트 계층(tier) 기준 정의. -> 새 컴포넌트 작성 시 어디에 배치할지 판단하는 기준 문서. - -## Tier 구조 요약 - -``` -ui 원시 빌딩블록 (HTML 래퍼, 단일 기능) - ↓ 조합 -atoms 최소 단위 UI 조각 (ui 1~2개 조합) - ↓ 조합 -molecules 의미 있는 UI 패턴 (atoms/ui 여러 개 조합) - ↓ 조합 -organisms 페이지 섹션 단위 (molecules/atoms 조합, 레이아웃 포함) - ↓ 사용 -domain 도메인별 비즈니스 컴포넌트 (organisms/molecules 사용) -``` - -## Tier별 정의 - -### ui (원시 빌딩블록) -| 항목 | 기준 | -|------|------| -| 위치 | `src/components/ui/` | -| 역할 | HTML 요소를 감싼 최소 단위. 스타일링 + 접근성만 담당 | -| 특징 | 비즈니스 로직 없음, 범용적, Radix UI 래퍼 포함 | -| 예시 | Button, Input, Select, Badge, Dialog, DatePicker, EmptyState | -| 판단 기준 | "이 컴포넌트가 다른 프로젝트에 그대로 복사해도 동작하는가?" → Yes면 ui | - -### atoms (최소 UI 조각) -| 항목 | 기준 | -|------|------| -| 위치 | `src/components/atoms/` | -| 역할 | ui 1~2개를 조합한 작은 패턴. 단일 목적 | -| 특징 | props 2~5개, 상태 관리 최소 | -| 예시 | BadgeSm, TabChip, ScrollableButtonGroup | -| 판단 기준 | "ui 하나로는 부족하지만, 독립적인 의미 단위인가?" → Yes면 atoms | - -### molecules (의미 있는 UI 패턴) -| 항목 | 기준 | -|------|------| -| 위치 | `src/components/molecules/` | -| 역할 | atoms/ui 여러 개를 조합하여 하나의 기능 패턴을 구성 | -| 특징 | Label + Input + Error 같은 조합, 내부 상태 가능 | -| 예시 | FormField, StatusBadge, DateRangeSelector, StandardDialog, TableActions | -| 판단 기준 | "여러 ui/atoms의 조합이고, 재사용 가능한 패턴인가?" → Yes면 molecules | - -### organisms (페이지 섹션) -| 항목 | 기준 | -|------|------| -| 위치 | `src/components/organisms/` | -| 역할 | 페이지의 독립적인 섹션. molecules/atoms를 조합하여 레이아웃 포함 | -| 특징 | 데이터 테이블, 검색 필터, 폼 섹션 등 페이지 구성 단위 | -| 예시 | DataTable, PageHeader, StatCards, FormSection, SearchableSelectionModal | -| 판단 기준 | "페이지에서 하나의 영역으로 독립 가능한가?" → Yes면 organisms | - -### common (공용 페이지/레이아웃) -| 항목 | 기준 | -|------|------| -| 위치 | `src/components/common/` | -| 역할 | 에러 페이지, 권한 없음 페이지 등 전역 공통 화면 | -| 특징 | 라우터 사용, 전체 페이지 레이아웃 | -| 예시 | AccessDenied, EmptyPage, ServerErrorPage | -| 판단 기준 | "전체 화면을 차지하는 공통 페이지인가?" → Yes면 common | - -### layout (레이아웃 구조) -| 항목 | 기준 | -|------|------| -| 위치 | `src/components/layout/` | -| 역할 | 앱 전체 레이아웃 골격 (사이드바, 헤더, 네비게이션) | -| 예시 | AuthenticatedLayout, Sidebar, TopNav | - -### dev (개발 도구) -| 항목 | 기준 | -|------|------| -| 위치 | `src/components/dev/` | -| 역할 | 개발 환경 전용 도구 (프로덕션 미포함) | -| 예시 | DevToolbar | - -### domain (도메인 비즈니스) -| 항목 | 기준 | -|------|------| -| 위치 | `src/components/{도메인명}/` (hr, sales, accounting 등) | -| 역할 | 특정 도메인의 비즈니스 로직이 포함된 컴포넌트 | -| 특징 | API 호출, 도메인 타입, 비즈니스 규칙 포함 | -| 예시 | EmployeeManagement, OrderRegistration, BillDetail | -| 판단 기준 | "특정 도메인에서만 사용되는가?" → Yes면 domain | - -## 자주 혼동되는 케이스 - -| 상황 | 올바른 tier | 이유 | -|------|-------------|------| -| EmptyState (프리셋/variant 있음) | **ui** | 범용 빌딩블록, 비즈니스 로직 없음 | -| StatusBadge (icon/dot/색상 커스텀) | **molecules** | Badge + BadgeSm 조합, DataTable 연동 | -| ConfirmDialog (삭제/저장 확인) | **ui** | AlertDialog 래퍼, 범용적 | -| StandardDialog (범용 컨테이너) | **molecules** | Dialog + Header + Footer 조합 패턴 | -| DataTable (정렬/페이지네이션/선택) | **organisms** | 페이지 섹션 단위, 다수 하위 컴포넌트 | -| SearchableSelectionModal | **organisms** | 검색+선택 완결 기능, 독립 섹션 | - -## 중복 방지 규칙 - -1. **새 컴포넌트 작성 전**: 같은 이름/기능이 다른 tier에 이미 있는지 확인 -2. **ui에 이미 있으면**: molecules/organisms에 동일 컴포넌트 만들지 않음. 필요하면 ui를 확장 -3. **re-export 허용**: organisms/index.ts에서 ui 컴포넌트를 re-export 가능 (편의성) -4. **확인(Confirm) 다이얼로그**: `ui/confirm-dialog.tsx` 하나만 사용 (52개 파일 사용 중) - -## StatusBadge 역할 구분 - -이름이 같지만 tier와 용도가 다른 두 컴포넌트. **둘 다 유지**. - -### `ui/status-badge.tsx` — 범용 상태 배지 - -| 항목 | 내용 | -|------|------| -| import | `import { StatusBadge } from '@/components/ui/status-badge'` | -| 용도 | `createStatusConfig`와 연동하는 **config 기반** 상태 표시 | -| API | `children` 또는 `status + config` 자동 라벨/스타일 | -| 특화 기능 | `mode` (badge/text), `ConfiguredStatusBadge` 제네릭 | -| 사용 예시 | 템플릿/유틸과 연동하는 범용 상태 표시 | - -```tsx -// config 기반 사용 - - -// children 기반 사용 -완료 -``` - -### `molecules/StatusBadge.tsx` — DataTable 특화 배지 - -| 항목 | 내용 | -|------|------| -| import | `import { StatusBadge } from '@/components/molecules/StatusBadge'` | -| 용도 | DataTable 셀에서 상태를 **아이콘/도트와 함께** 표시 | -| API | `label` 필수, `variant`로 색상 지정 | -| 특화 기능 | `icon` (LucideIcon), `showDot`, 커스텀 `bgColor/textColor/borderColor` | -| 기반 | Badge + BadgeSm 조합 (size="sm"일 때 BadgeSm으로 자동 전환) | - -```tsx -// DataTable 셀 렌더링 - - -``` - -### 선택 기준 - -| 상황 | 사용할 컴포넌트 | -|------|----------------| -| `createStatusConfig` 결과와 연동 | **ui** StatusBadge | -| DataTable 컬럼 셀 렌더링 | **molecules** StatusBadge | -| 아이콘이나 도트가 필요한 배지 | **molecules** StatusBadge | -| 단순 텍스트 상태 표시 (badge/text 모드) | **ui** StatusBadge | diff --git a/claudedocs/architecture/[IMPL-2025-11-13] browser-support-policy.md b/claudedocs/architecture/[IMPL-2025-11-13] browser-support-policy.md deleted file mode 100644 index fd465918..00000000 --- a/claudedocs/architecture/[IMPL-2025-11-13] browser-support-policy.md +++ /dev/null @@ -1,516 +0,0 @@ -# 브라우저 지원 정책 - -## 📋 목차 -1. [지원 브라우저](#지원-브라우저) -2. [지원하지 않는 브라우저](#지원하지-않는-브라우저) -3. [기술적 배경](#기술적-배경) -4. [구현 내용](#구현-내용) -5. [테스트 가이드](#테스트-가이드) -6. [사용자 안내 프로세스](#사용자-안내-프로세스) -7. [향후 정책](#향후-정책) - ---- - -## 지원 브라우저 - -### ✅ 공식 지원 브라우저 - -| 브라우저 | 최소 버전 | 권장 버전 | 플랫폼 | 우선순위 | -|---------|----------|----------|--------|---------| -| **Google Chrome** | 90+ | 최신 버전 | Windows, macOS, Linux | 🔴 High | -| **Microsoft Edge** | 90+ | 최신 버전 | Windows, macOS | 🔴 High | -| **Safari** | 14+ | 최신 버전 | macOS, iOS | 🔴 High | - -### 브라우저별 권장 사유 - -#### Chrome (권장) -- ✅ 가장 안정적인 성능 -- ✅ 개발 도구 우수 -- ✅ 자동 업데이트 -- ✅ 크로스 플랫폼 지원 - -#### Edge (Windows 권장) -- ✅ Windows 기본 브라우저 -- ✅ Chrome 엔진 기반 (Chromium) -- ✅ Microsoft 공식 지원 -- ✅ 엔터프라이즈 환경 최적화 - -#### Safari (macOS/iOS 권장) -- ✅ Apple 기기 최적화 -- ✅ 배터리 효율 우수 -- ✅ 개인정보 보호 강화 -- ✅ iOS 필수 브라우저 - ---- - -## 지원하지 않는 브라우저 - -### ❌ Internet Explorer (모든 버전) - -**지원 중단 사유:** - -1. **Microsoft 공식 지원 종료** - - 2022년 6월 15일부로 IE 지원 완전 종료 - - 보안 업데이트 중단 - - Edge로 마이그레이션 권장 - -2. **기술적 한계** - - 현대 웹 표준 미지원 - - JavaScript ES6+ 미지원 - - CSS3 고급 기능 미지원 - - 성능 문제 - -3. **보안 취약점** - - 패치되지 않는 보안 결함 - - XSS, CSRF 등 공격에 취약 - - 개인정보 유출 위험 - -4. **프로젝트 기술 스택 비호환** - - Next.js 15: IE 지원 중단 (v12부터) - - React 19: IE 지원 중단 (v18부터) - - Tailwind CSS 4: IE 미지원 - - Modern JavaScript (ES6+): 네이티브 미지원 - ---- - -## 기술적 배경 - -### 현재 기술 스택과 IE 비호환성 - -```json -{ - "next": "15.5.6", // IE 지원 중단: v12 (2021) - "react": "19.2.0", // IE 지원 중단: v18 (2022) - "tailwindcss": "4", // IE 미지원 - "typescript": "5" // ES6+ 트랜스파일 필요 -} -``` - -### IE 지원을 위한 대안과 비용 - -| 방안 | 가능 여부 | 비용 | 문제점 | -|------|----------|------|--------| -| **다운그레이드** | ⚠️ 가능 | 2-3주 개발 | 보안 취약점, 최신 기능 사용 불가 | -| **폴리필 추가** | ❌ 불가능 | - | Next.js 15/React 19는 폴리필로 해결 불가 | -| **별도 레거시 버전** | ⚠️ 가능 | 1-2개월 개발 | 유지보수 부담 증가 | -| **Edge 마이그레이션** | ✅ 권장 | 0원 | 사용자 교육 필요 | - -**결론**: IE 지원 비용 대비 효과가 낮아 **지원하지 않기로 결정** - ---- - -## 구현 내용 - -### 1. IE 감지 및 차단 로직 - -**파일**: `src/middleware.ts` - -```typescript -/** - * Check if user-agent is Internet Explorer - * IE 11: Contains "Trident" in user-agent - * IE 10 and below: Contains "MSIE" in user-agent - */ -function isInternetExplorer(userAgent: string): boolean { - if (!userAgent) return false; - - return /MSIE|Trident/.test(userAgent); -} - -export function middleware(request: NextRequest) { - const { pathname } = request.nextUrl; - const userAgent = request.headers.get('user-agent') || ''; - - // 🚨 Internet Explorer Detection (최우선 처리) - if (isInternetExplorer(userAgent)) { - // unsupported-browser.html 페이지는 제외 (무한 리다이렉트 방지) - if (!pathname.includes('unsupported-browser')) { - console.log(`[IE Blocked] ${userAgent} attempted to access ${pathname}`); - return NextResponse.redirect(new URL('/unsupported-browser.html', request.url)); - } - } - - // ... 나머지 로직 -} -``` - -**동작 방식**: -1. 모든 요청에서 User-Agent 확인 -2. IE 패턴 감지 시 `/unsupported-browser.html`로 리다이렉트 -3. 안내 페이지는 무한 리다이렉트 방지 처리 - ---- - -### 2. 브라우저 업그레이드 안내 페이지 - -**파일**: `public/unsupported-browser.html` - -**주요 기능**: -- ✅ IE 사용 불가 안내 -- ✅ 권장 브라우저 다운로드 링크 제공 -- ✅ IE 지원 중단 사유 설명 -- ✅ 반응형 디자인 (모바일 대응) -- ✅ 접근성 고려 (고대비, 큰 폰트) - -**안내 브라우저**: -1. **Microsoft Edge** (권장) - Windows 사용자용 -2. **Google Chrome** - 범용 -3. **Safari** - macOS/iOS 사용자용 - ---- - -### 3. User-Agent 감지 패턴 - -| IE 버전 | User-Agent 패턴 | 감지 정규식 | -|---------|----------------|------------| -| IE 11 | `Trident/7.0` | `/Trident/` | -| IE 10 | `MSIE 10.0` | `/MSIE/` | -| IE 9 이하 | `MSIE 9.0`, `MSIE 8.0` | `/MSIE/` | - -**감지 코드**: -```javascript -/MSIE|Trident/.test(userAgent) -``` - ---- - -## 테스트 가이드 - -### 1. Chrome DevTools를 사용한 IE 시뮬레이션 - -```javascript -// Chrome DevTools Console에서 실행 -// 1. F12 → Console 탭 -// 2. 다음 코드 붙여넣기 - -// IE 11 시뮬레이션 -Object.defineProperty(navigator, 'userAgent', { - get: function() { - return 'Mozilla/5.0 (Windows NT 10.0; Trident/7.0; rv:11.0) like Gecko'; - } -}); - -// 페이지 새로고침 -location.reload(); -``` - -**예상 결과**: `/unsupported-browser.html`로 리다이렉트 - ---- - -### 2. 실제 IE에서 테스트 (Windows 전용) - -#### Windows 10 IE 11 테스트 -```bash -# 1. Windows 검색 → "Internet Explorer" -# 2. http://localhost:3000 접속 -# 3. 안내 페이지 표시 확인 -``` - -#### 가상 머신 테스트 -- [Microsoft Edge Developer](https://developer.microsoft.com/microsoft-edge/tools/vms/) 가상 머신 사용 -- Windows 7/8/10 + IE 버전별 테스트 가능 - ---- - -### 3. 지원 브라우저 테스트 - -| 브라우저 | 테스트 항목 | 예상 결과 | -|---------|-----------|----------| -| **Chrome** | 로그인 → 대시보드 이동 | ✅ 정상 작동 | -| **Edge** | 로그인 → 대시보드 이동 | ✅ 정상 작동 | -| **Safari** | 로그인 → 대시보드 이동 | ✅ 정상 작동 | -| **IE 11** | 모든 페이지 접근 | ⚠️ 안내 페이지로 리다이렉트 | - ---- - -## 사용자 안내 프로세스 - -### 1. 사전 공지 (배포 1개월 전) - -**공지 채널**: -- 📧 이메일: 전체 사용자 대상 -- 📢 시스템 공지: 로그인 시 팝업 -- 📄 홈페이지: 공지사항 게시 - -**공지 내용 예시**: -``` -[중요] 브라우저 업그레이드 안내 - -안녕하세요. SAM ERP 시스템 운영팀입니다. - -보안 및 성능 향상을 위해 2024년 XX월 XX일부터 -Internet Explorer 지원을 중단합니다. - -▶ 권장 브라우저 - - Microsoft Edge (Windows 권장) - - Google Chrome - - Safari (macOS/iOS) - -▶ 다운로드 링크 - - Edge: https://www.microsoft.com/edge - - Chrome: https://www.google.com/chrome - -문의사항은 고객센터(02-XXXX-XXXX)로 연락주세요. - -감사합니다. -``` - ---- - -### 2. 배포 시점 - -**IE 사용자 안내**: -1. IE로 접속 시 자동으로 안내 페이지 표시 -2. 권장 브라우저 다운로드 링크 제공 -3. 지원 중단 사유 명확히 안내 - -**고객 지원**: -- 📞 전화 지원: 브라우저 설치 안내 -- 💬 채팅 상담: 실시간 도움 -- 📋 가이드: 브라우저별 설치 매뉴얼 - ---- - -### 3. 배포 후 모니터링 - -**수집 지표**: -```yaml -metrics: - - ie_access_attempts: IE 접근 시도 횟수 - - browser_distribution: 브라우저별 사용 비율 - - support_tickets: 브라우저 관련 문의 건수 - - migration_rate: Edge/Chrome 전환율 -``` - -**모니터링 코드 (선택사항)**: -```typescript -// middleware.ts에 추가 -if (isInternetExplorer(userAgent)) { - // 통계 수집 - await fetch('/api/analytics/browser', { - method: 'POST', - body: JSON.stringify({ - event: 'ie_blocked', - timestamp: new Date(), - path: pathname, - userAgent: userAgent - }) - }); - - return NextResponse.redirect(new URL('/unsupported-browser.html', request.url)); -} -``` - ---- - -## 향후 정책 - -### 1. 브라우저 버전 관리 - -**업데이트 정책**: -- ✅ 최신 브라우저 버전 권장 -- ✅ 최소 지원 버전: 현재 버전 -2 (약 6개월) -- ⚠️ 구버전 사용 시 업데이트 권장 안내 - -**예시**: -``` -현재 Chrome 120 사용 중 -→ Chrome 118 이상 지원 -→ Chrome 117 이하는 업데이트 권장 -``` - ---- - -### 2. 신규 브라우저 지원 검토 - -**평가 기준**: -1. **시장 점유율**: 5% 이상 -2. **웹 표준 준수**: ECMAScript 2020+, CSS3 -3. **보안 업데이트**: 정기적인 패치 제공 -4. **개발자 도구**: 디버깅 환경 제공 - -**현재 지원 검토 대상**: -- ✅ **Firefox**: 지원 검토 중 (시장 점유율 고려) -- ⚠️ **Opera, Vivaldi**: 시장 점유율 낮음 (Chrome 기반이므로 호환 가능) - ---- - -### 3. 모바일 브라우저 정책 - -**모바일 지원**: - -| 플랫폼 | 브라우저 | 지원 여부 | -|--------|---------|----------| -| **iOS** | Safari | ✅ 지원 | -| **iOS** | Chrome | ✅ 지원 (Safari 엔진 사용) | -| **Android** | Chrome | ✅ 지원 | -| **Android** | Samsung Internet | ⚠️ 호환 가능 (Chrome 기반) | - -**참고**: iOS는 WebKit 엔진 강제 정책으로 모든 브라우저가 Safari 엔진 사용 - ---- - -## 크로스 브라우저 개발 원칙 - -### 개발 시 준수 사항 - -#### 1. 브라우저 테스트 필수 -```yaml -feature_development: - - step_1: Chrome에서 개발 및 테스트 - - step_2: Safari에서 호환성 테스트 - - step_3: Edge에서 최종 확인 - - step_4: 모바일 Safari (iOS) 테스트 -``` - -#### 2. Safari 우선 개발 -```typescript -// Safari를 기준으로 개발하면 다른 브라우저에서도 작동 -// Safari가 가장 엄격한 정책을 가지고 있기 때문 - -// ✅ Safari 호환 코드 (모든 브라우저 작동) -const cookie = [ - 'token=xxx', - 'HttpOnly', - ...(isProduction ? ['Secure'] : []), // 환경별 조건부 - 'SameSite=Lax', // Safari 호환 -].join('; '); - -// ❌ Chrome만 작동 (Safari 실패) -const cookie = 'token=xxx; Secure; SameSite=Strict'; // HTTP에서 Safari 거부 -``` - -#### 3. 기능 감지 (Feature Detection) -```typescript -// ✅ 올바른 방법: 기능 감지 -if ('IntersectionObserver' in window) { - // IntersectionObserver 사용 -} - -// ❌ 잘못된 방법: 브라우저 감지 -if (userAgent.includes('Chrome')) { - // Chrome 전용 기능 사용 -} -``` - -#### 4. 폴백 제공 -```typescript -// localStorage 지원 여부 확인 (Safari Private Mode 대응) -try { - localStorage.setItem('test', 'test'); - localStorage.removeItem('test'); -} catch (error) { - // Safari Private Mode: localStorage 제한 - // 대안: sessionStorage 또는 메모리 저장소 사용 -} -``` - ---- - -## 문제 해결 가이드 - -### Q1. IE 사용자가 계속 접속을 시도하는 경우 - -**해결 방법**: -1. 고객센터 연락 유도 -2. Edge 설치 원격 지원 -3. 브라우저 설치 가이드 제공 - -**Edge 설치 가이드**: -``` -1. https://www.microsoft.com/edge 접속 -2. "다운로드" 버튼 클릭 -3. 설치 파일 실행 -4. 설치 완료 후 SAM ERP 재접속 -``` - ---- - -### Q2. 안내 페이지가 표시되지 않는 경우 - -**체크 포인트**: -```bash -# 1. middleware.ts 적용 확인 -npm run build - -# 2. 로그 확인 -# 개발 환경: 터미널에서 "[IE Blocked]" 메시지 확인 -# 프로덕션: 로그 모니터링 시스템 확인 - -# 3. User-Agent 확인 -# Chrome DevTools → Network → 요청 헤더에서 User-Agent 확인 -``` - ---- - -### Q3. 특정 브라우저에서 기능이 작동하지 않는 경우 - -**디버깅 절차**: -```typescript -// 1. 브라우저 콘솔에서 에러 확인 -// Chrome: F12 → Console -// Safari: 개발자 메뉴 활성화 → 웹 검사기 → 콘솔 - -// 2. 브라우저 호환성 확인 -// https://caniuse.com 에서 기능 검색 - -// 3. 폴백 코드 추가 -if (typeof feature === 'undefined') { - // 대체 구현 -} -``` - ---- - -## 관련 문서 - -- [Safari 쿠키 호환성 가이드](./safari-cookie-compatibility.md) -- [사이드바 스크롤 개선](./sidebar-scroll-improvements.md) -- [Next.js 브라우저 지원](https://nextjs.org/docs/architecture/supported-browsers) -- [React 브라우저 지원](https://react.dev/learn/start-a-new-react-project#browser-support) - ---- - -## 업데이트 히스토리 - -| 날짜 | 내용 | 작성자 | -|------|------|--------| -| 2024-XX-XX | 브라우저 지원 정책 문서 작성 및 IE 차단 구현 | Claude | - ---- - -## 요약 - -### ✅ 지원 브라우저 -- **Chrome** (90+) -- **Edge** (90+) -- **Safari** (14+) - -### ❌ 지원하지 않는 브라우저 -- **Internet Explorer** (모든 버전) - -### 🎯 핵심 원칙 -1. **Safari 우선 개발**: 가장 엄격한 정책 기준 -2. **크로스 브라우저 테스트 필수**: Chrome, Safari, Edge -3. **사용자 친화적 안내**: IE 사용자에게 명확한 업그레이드 안내 - -**문의**: 고객센터 또는 개발팀 - ---- - -## 관련 파일 - -### 프론트엔드 -- `src/middleware.ts` - IE 감지 및 차단 미들웨어 (isInternetExplorer 함수) -- `public/unsupported-browser.html` - 브라우저 업그레이드 안내 페이지 -- `src/lib/api/auth/token-storage.ts` - Safari 호환 토큰 저장소 - -### 설정 파일 -- `next.config.ts` - Next.js 브라우저 타겟 설정 -- `package.json` - 브라우저 호환 의존성 (next, react 버전) -- `tsconfig.json` - TypeScript 타겟 설정 - -### 참조 문서 -- `claudedocs/auth/[IMPL-2025-11-13] safari-cookie-compatibility.md` - Safari 쿠키 호환성 -- `claudedocs/architecture/[IMPL-2025-11-18] ssr-hydration-fix.md` - SSR/Hydration 에러 해결 \ No newline at end of file diff --git a/claudedocs/architecture/[IMPL-2025-11-18] ssr-hydration-fix.md b/claudedocs/architecture/[IMPL-2025-11-18] ssr-hydration-fix.md deleted file mode 100644 index 428ee35d..00000000 --- a/claudedocs/architecture/[IMPL-2025-11-18] ssr-hydration-fix.md +++ /dev/null @@ -1,106 +0,0 @@ -# SSR Hydration 에러 해결 작업 기록 - -## 문제 상황 - -### 1차 에러: useData is not defined -- **위치**: ItemMasterDataManagement.tsx:389 -- **원인**: 리팩토링 후 `useData()` → `useItemMaster()` 변경 누락 -- **해결**: 함수 호출 변경 - -### 2차 에러: Hydration Mismatch -``` -Hydration failed because the server rendered HTML didn't match the client -``` -- **원인**: Context 파일에서 localStorage를 useState 초기화 시점에 접근 -- **영향**: 서버는 초기값 렌더링, 클라이언트는 localStorage 데이터 렌더링 → HTML 불일치 - -## 근본 원인 분석 - -### ❌ 문제가 되는 패턴 (React SPA) -```typescript -const [data, setData] = useState(() => { - if (typeof window === 'undefined') return initialData; - const saved = localStorage.getItem('key'); - return saved ? JSON.parse(saved) : initialData; -}); -``` - -**문제점**: -- 서버: `typeof window === 'undefined'` → initialData 반환 -- 클라이언트: localStorage 값 반환 -- 결과: 서버/클라이언트 HTML 불일치 → Hydration 에러 - -### ✅ SSR-Safe 패턴 (Next.js) -```typescript -const [data, setData] = useState(initialData); - -useEffect(() => { - try { - const saved = localStorage.getItem('key'); - if (saved) setData(JSON.parse(saved)); - } catch (error) { - console.error('Failed to load data:', error); - localStorage.removeItem('key'); - } -}, []); -``` - -**장점**: -- 서버/클라이언트 모두 동일한 초기값으로 렌더링 -- useEffect는 클라이언트에서만 실행 -- Hydration 후 localStorage 데이터로 업데이트 -- 에러 처리로 손상된 데이터 복구 - -## 수정 내역 - -### AuthContext.tsx -- 2개 state: users, currentUser -- localStorage 로드를 단일 useEffect로 통합 -- 에러 처리 추가 - -### ItemMasterContext.tsx -- 13개 state 전체 SSR-safe 패턴 적용 -- 통합 useEffect로 모든 localStorage 로드 처리 -- 버전 관리 유지: - - specificationMasters: v1.0 - - materialItemNames: v1.1 -- 포괄적 에러 처리 및 손상 데이터 정리 - -## 예상 부작용 및 완화 - -### Flash of Initial Content (FOIC) -- **현상**: 초기값 표시 → localStorage 데이터로 전환 -- **영향**: 매우 짧은 시간 (보통 눈에 띄지 않음) -- **완화**: 필요시 loading state 추가 가능 - -### localStorage 데이터 손상 -- **대응**: try-catch로 감싸고 손상 시 localStorage 클리어 -- **결과**: 기본값으로 재시작하여 앱 정상 동작 유지 - -## 테스트 결과 -- ✅ Hydration 에러 해결 -- ✅ localStorage 정상 로드 -- ✅ 서버/클라이언트 렌더링 일치 -- ✅ 에러 없이 페이지 로드 - -## 향후 고려사항 -- 나머지 8개 Context (Facilities, Accounting, HR, etc.)는 실제 사용 시 동일 패턴 적용 필요 -- 복잡한 초기 데이터가 있는 경우 서버에서 데이터 pre-fetch 고려 -- Critical한 초기 데이터는 서버 컴포넌트에서 직접 전달하는 방식 검토 가능 - -## 참고 문서 -- Next.js SSR/Hydration: https://nextjs.org/docs/messages/react-hydration-error -- React useEffect: https://react.dev/reference/react/useEffect - ---- - -## 관련 파일 - -### 프론트엔드 -- `src/contexts/AuthContext.tsx` - SSR-safe 패턴 적용된 인증 Context -- `src/contexts/ItemMasterContext.tsx` - SSR-safe 패턴 적용된 품목 마스터 Context (13개 state) -- `src/components/items/ItemMasterDataManagement.tsx` - 품목기준관리 컴포넌트 - -### 참조 문서 -- `claudedocs/auth/[IMPL-2025-11-07] authentication-implementation-guide.md` - 인증 구현 가이드 -- `claudedocs/architecture/[REF-2025-11-19] multi-tenancy-implementation.md` - 멀티테넌시 구현 (localStorage 패턴) \ No newline at end of file diff --git a/claudedocs/architecture/[IMPL-2026-01-21] input-form-componentization.md b/claudedocs/architecture/[IMPL-2026-01-21] input-form-componentization.md deleted file mode 100644 index 5e0f7593..00000000 --- a/claudedocs/architecture/[IMPL-2026-01-21] input-form-componentization.md +++ /dev/null @@ -1,349 +0,0 @@ -# 입력폼 공통 컴포넌트화 구현 계획서 - -**작성일**: 2026-01-21 -**작성자**: Claude Code -**상태**: ✅ Phase 1-3 VendorDetail 적용 완료 -**최종 수정**: 2026-01-21 - ---- - -## 1. 개요 - -### 1.1 목적 -- 숫자 입력필드의 선행 0(leading zero) 문제 해결 -- 금액/수량 입력 시 천단위 콤마 및 포맷팅 일관성 확보 -- 전화번호, 사업자번호, 주민번호 등 포맷팅이 필요한 입력필드 공통화 -- 소수점 입력이 필요한 필드 지원 (비율, 환율 등) - -### 1.2 현재 문제점 -| 문제 | 현상 | 영향 범위 | -|------|------|----------| -| 숫자 입력 leading zero | `01`, `001` 등 표시 | 전체 숫자 입력 | -| 금액 포맷팅 불일치 | 콤마 처리 제각각 | **147개 파일** | -| 전화번호 포맷팅 없음 | `01012341234` 그대로 표시 | 거래처, 직원 관리 | -| 사업자번호 포맷팅 없음 | `1234567890` 그대로 표시 | 거래처 관리 | -| Number 타입 일관성 | string/number 혼용 | 타입 에러 가능성 | - ---- - -## 2. 구현 우선순위 - -### 🔴 Phase 1: 핵심 숫자 입력 (최우선) -| 순서 | 컴포넌트 | 용도 | 영향 범위 | -|------|---------|------|----------| -| 1 | **NumberInput** | 범용 숫자 입력 (leading zero 해결) | 전체 | -| 2 | **CurrencyInput** | 금액 입력 (₩, 천단위 콤마) | 147개 파일 | -| 3 | **QuantityInput** | 수량 입력 (정수, 최소값 0) | 재고/주문 | - -### 🟠 Phase 2: 포맷팅 입력 (완료) -| 순서 | 컴포넌트 | 용도 | 상태 | -|------|---------|------|------| -| 4 | **PhoneInput** | 전화번호 자동 하이픈 | ✅ 완료 | -| 5 | **BusinessNumberInput** | 사업자번호 포맷팅 | ✅ 완료 | -| 6 | **PersonalNumberInput** | 주민번호 포맷팅/마스킹 | ✅ 완료 | - -### 🟢 Phase 3: 통합 및 확장 -| 순서 | 작업 | 설명 | -|------|------|------| -| 7 | ui/index.ts export | 새 컴포넌트 내보내기 | -| 8 | FormField 확장 | 새 타입 지원 추가 | -| 9 | 실사용 적용 테스트 | VendorDetail 등 | - ---- - -## 3. 생성/수정 파일 목록 - -### 3.1 새로 생성한 파일 - -``` -src/ -├── lib/ -│ └── formatters.ts ✅ 완료 -├── components/ -│ └── ui/ -│ ├── phone-input.tsx ✅ 완료 -│ ├── business-number-input.tsx ✅ 완료 -│ ├── personal-number-input.tsx ✅ 완료 -│ ├── number-input.tsx ✅ 완료 -│ ├── currency-input.tsx ✅ 완료 -│ └── quantity-input.tsx ✅ 완료 -``` - -### 3.2 수정한 파일 - -| 파일 | 수정 내용 | 상태 | -|------|----------|------| -| `src/components/molecules/FormField.tsx` | 새 타입 지원 추가 (phone, businessNumber, personalNumber, currency, quantity) | ✅ 완료 | - ---- - -## 4. 컴포넌트 상세 설계 - -### 4.1 NumberInput (범용 숫자 입력) - -```typescript -interface NumberInputProps { - value: number | string | undefined; - onChange: (value: number | undefined) => void; - - // 포맷 옵션 - allowDecimal?: boolean; // 소수점 허용 (기본: false) - decimalPlaces?: number; // 소수점 자릿수 제한 - allowNegative?: boolean; // 음수 허용 (기본: false) - useComma?: boolean; // 천단위 콤마 (기본: false) - - // 범위 제한 - min?: number; - max?: number; - - // 표시 옵션 - suffix?: string; // 접미사 (원, 개, % 등) - allowEmpty?: boolean; // 빈 값 허용 (기본: true) -} -``` - -**사용 예시**: -```tsx -// 기본 정수 입력 - - -// 소수점 2자리 (비율, 환율) - - -// 퍼센트 입력 (0-100 제한) - - -// 음수 허용 (재고 조정) - -``` - -### 4.2 CurrencyInput (금액 입력) - -```typescript -interface CurrencyInputProps { - value: number | undefined; - onChange: (value: number | undefined) => void; - - currency?: '₩' | '$' | '¥'; // 통화 기호 (기본: ₩) - showCurrency?: boolean; // 통화 기호 표시 (기본: true) - allowNegative?: boolean; // 음수 허용 (기본: false) -} -``` - -**특징**: -- 항상 천단위 콤마 표시 -- 정수만 허용 (원 단위) -- 포커스 해제 시 통화 기호 표시 - -### 4.3 QuantityInput (수량 입력) - -```typescript -interface QuantityInputProps { - value: number | undefined; - onChange: (value: number | undefined) => void; - - min?: number; // 최소값 (기본: 0) - max?: number; // 최대값 - step?: number; // 증감 단위 (기본: 1) - showButtons?: boolean; // +/- 버튼 표시 - suffix?: string; // 단위 (개, EA, 박스 등) -} -``` - -**특징**: -- 정수만 허용 -- 기본 최소값 0 -- 선택적 +/- 버튼 - -### 4.4 PhoneInput ✅ 완료 - -```typescript -interface PhoneInputProps { - value: string; - onChange: (value: string) => void; // 숫자만 반환 - error?: boolean; -} -``` - -### 4.5 BusinessNumberInput ✅ 완료 - -```typescript -interface BusinessNumberInputProps { - value: string; - onChange: (value: string) => void; - showValidation?: boolean; // 유효성 검사 아이콘 - error?: boolean; -} -``` - -### 4.6 PersonalNumberInput ✅ 완료 - -```typescript -interface PersonalNumberInputProps { - value: string; - onChange: (value: string) => void; - maskBack?: boolean; // 뒷자리 마스킹 - error?: boolean; -} -``` - ---- - -## 5. 검수 계획서 - -### 5.1 NumberInput 테스트 - -| 테스트 항목 | 입력 | 기대 결과 | -|------------|------|----------| -| Leading zero 제거 | `01` | 표시: `1`, 값: `1` | -| Leading zero 제거 | `007` | 표시: `7`, 값: `7` | -| 소수점 (허용시) | `3.14` | 표시: `3.14`, 값: `3.14` | -| 소수점 자릿수 제한 | `3.14159` (2자리) | 표시: `3.14`, 값: `3.14` | -| 음수 (허용시) | `-100` | 표시: `-100`, 값: `-100` | -| 콤마 표시 | `1000000` | 표시: `1,000,000`, 값: `1000000` | -| 범위 제한 (max:100) | `150` | 값: `100` (제한) | -| 빈 값 | `` | 값: `undefined` | -| 문자 입력 차단 | `abc` | 입력 안됨 | - -### 5.2 CurrencyInput 테스트 - -| 테스트 항목 | 입력 | 기대 결과 | -|------------|------|----------| -| 기본 입력 | `50000` | 표시: `50,000`, 값: `50000` | -| 통화 기호 | `50000` (blur) | 표시: `₩50,000` | -| 소수점 차단 | `100.5` | 표시: `100`, 값: `100` | -| 대용량 | `1000000000` | 표시: `1,000,000,000` | - -### 5.3 QuantityInput 테스트 - -| 테스트 항목 | 입력 | 기대 결과 | -|------------|------|----------| -| 기본 입력 | `10` | 표시: `10`, 값: `10` | -| 음수 차단 | `-5` | 값: `0` (최소값) | -| 소수점 차단 | `10.5` | 표시: `10`, 값: `10` | -| +/- 버튼 | 클릭 | 1씩 증감 | - -### 5.4 실사용 테스트 페이지 - -| 페이지 | 경로 | 테스트 항목 | -|--------|------|------------| -| 거래처 관리 | `/accounting/vendor-management` | 전화번호, 사업자번호 | -| 직원 관리 | `/hr/employee-management` | 전화번호, 주민번호 | -| 견적 등록 | `/quotes` | 수량, 금액 | -| 주문 관리 | `/sales/order-management-sales` | 수량, 금액 | -| 재고 관리 | `/material/stock-status` | 수량 | - ---- - -## 6. 완료 체크리스트 - -### Phase 1: 유틸리티 및 기본 컴포넌트 -- [x] formatters.ts 유틸리티 함수 생성 -- [x] PhoneInput 컴포넌트 생성 -- [x] BusinessNumberInput 컴포넌트 생성 -- [x] PersonalNumberInput 컴포넌트 생성 -- [x] NumberInput 컴포넌트 생성 -- [x] CurrencyInput 컴포넌트 생성 -- [x] QuantityInput 컴포넌트 생성 - -### Phase 2: 통합 -- [x] ui/index.ts export 추가 (개별 import 방식 사용) -- [x] FormField 타입 확장 - -### Phase 3: 테스트 및 적용 -- [ ] 개별 컴포넌트 동작 테스트 -- [x] VendorDetail 적용 완료 - - [x] PhoneInput: phone, mobile, fax, managerPhone - - [x] BusinessNumberInput: businessNumber (유효성 검사 포함) - - [x] CurrencyInput: outstandingAmount, unpaidAmount - - [x] NumberInput: overdueDays -- [ ] 문서 최종 업데이트 - ---- - -## 7. 롤백 계획 - -문제 발생 시: -1. 새 컴포넌트 import 제거 -2. 기존 `` 컴포넌트로 복원 -3. FormField 타입 변경 롤백 - ---- - -## 8. 참고사항 - -### 기존 컴포넌트 위치 -- Input: `src/components/ui/input.tsx` -- FormField: `src/components/molecules/FormField.tsx` - -### 생성된 파일 -| 파일 | 경로 | -|------|------| -| formatters | `src/lib/formatters.ts` | -| PhoneInput | `src/components/ui/phone-input.tsx` | -| BusinessNumberInput | `src/components/ui/business-number-input.tsx` | -| PersonalNumberInput | `src/components/ui/personal-number-input.tsx` | -| NumberInput | `src/components/ui/number-input.tsx` | -| CurrencyInput | `src/components/ui/currency-input.tsx` | -| QuantityInput | `src/components/ui/quantity-input.tsx` | - ---- - -## 9. 사용 예시 - -### 직접 import 방식 -```tsx -import { PhoneInput } from '@/components/ui/phone-input'; -import { CurrencyInput } from '@/components/ui/currency-input'; -import { NumberInput } from '@/components/ui/number-input'; - -// 전화번호 - - -// 금액 - - -// 소수점 허용 숫자 - -``` - -### FormField 통합 방식 -```tsx -import { FormField } from '@/components/molecules/FormField'; - -// 전화번호 - - -// 사업자번호 (유효성 검사 표시) - - -// 금액 - - -// 수량 (+/- 버튼) - -``` \ No newline at end of file diff --git a/claudedocs/architecture/[IMPL-2026-01-21] phase4-input-migration-rollout.md b/claudedocs/architecture/[IMPL-2026-01-21] phase4-input-migration-rollout.md deleted file mode 100644 index 734b12aa..00000000 --- a/claudedocs/architecture/[IMPL-2026-01-21] phase4-input-migration-rollout.md +++ /dev/null @@ -1,372 +0,0 @@ -# Phase 4: 입력 컴포넌트 전체 적용 계획서 - -**작성일**: 2026-01-21 -**작성자**: Claude Code -**상태**: 🔵 계획 수립 완료 -**근거 문서**: [IMPL-2026-01-21] input-form-componentization.md - ---- - -## 1. 스캔 결과 요약 - -### 1.1 대상 파일 통계 -| 카테고리 | 파일 수 | 비고 | -|----------|--------|------| -| `type="number"` 사용 | 52개 | 직접 Input 사용 | -| 전화번호 관련 | 70개 | phone, tel, 전화, 연락처 | -| 사업자번호 관련 | 33개 | businessNumber, 사업자번호 | -| 금액 관련 | 197개 | price, amount, 금액, 단가 | -| 수량 관련 | 106개 | quantity, qty, 수량 | - -### 1.2 마이그레이션 접근 전략 - -**전략 1: 템플릿 레벨 수정 (최고 효율)** -- `IntegratedDetailTemplate/FieldInput.tsx` 수정 -- `IntegratedDetailTemplate/FieldRenderer.tsx` 수정 -- 이 템플릿을 사용하는 **모든 페이지**에 자동 적용 - -**전략 2: FormField 타입 확장 (이미 완료)** -- `FormField.tsx`에 새 타입 추가 완료 -- FormField를 사용하는 컴포넌트는 타입만 변경하면 됨 - -**전략 3: 개별 컴포넌트 수정** -- 직접 `` 사용하는 컴포넌트 -- 커스텀 로직이 있어 템플릿 적용 불가한 컴포넌트 - ---- - -## 2. 마이그레이션 우선순위 - -### 🔴 Tier 1: 템플릿 레벨 (최우선) -> 한 번 수정으로 다수 페이지에 적용 - -| 파일 | 수정 내용 | 영향 범위 | -|------|----------|----------| -| `IntegratedDetailTemplate/FieldInput.tsx` | number 타입에 NumberInput/CurrencyInput 적용, phone/businessNumber 타입 추가 | 템플릿 사용 전체 | -| `IntegratedDetailTemplate/FieldRenderer.tsx` | 동일 | 템플릿 사용 전체 | -| `IntegratedDetailTemplate/types.ts` | FieldType에 새 타입 추가 | 타입 시스템 | - -### 🟠 Tier 2: 핵심 폼 컴포넌트 -> 사용 빈도가 높거나 중요한 폼 - -**회계 도메인 (accounting/)** -| 파일 | 적용 대상 | 우선순위 | -|------|----------|----------| -| ✅ `VendorDetail.tsx` | phone, businessNumber, currency | 완료 | -| `PurchaseDetail.tsx` | currency (금액) | 높음 | -| `SalesDetail.tsx` | currency (금액) | 높음 | -| `BillDetail.tsx` | currency (금액) | 높음 | -| `DepositDetail.tsx` | currency (금액) | 높음 | -| `WithdrawalDetail.tsx` | currency (금액) | 높음 | -| `BadDebtDetail.tsx` | currency, phone | 높음 | - -**주문/견적 도메인 (orders/, quotes/)** -| 파일 | 적용 대상 | 우선순위 | -|------|----------|----------| -| `OrderRegistration.tsx` | currency, quantity | 높음 | -| `OrderSalesDetailEdit.tsx` | currency, quantity | 높음 | -| `QuoteRegistration.tsx` | currency, quantity, number | 높음 | -| `QuoteRegistrationV2.tsx` | currency, quantity, number | 높음 | -| `LocationDetailPanel.tsx` | currency, quantity | 중간 | -| `LocationListPanel.tsx` | currency, quantity | 중간 | - -**인사 도메인 (hr/)** -| 파일 | 적용 대상 | 우선순위 | -|------|----------|----------| -| `EmployeeForm.tsx` | phone, personalNumber | 높음 | -| `EmployeeDetail.tsx` | phone, personalNumber | 높음 | -| `EmployeeDialog.tsx` | phone | 높음 | -| `SalaryDetailDialog.tsx` | currency | 중간 | -| `VacationRegisterDialog.tsx` | number | 중간 | -| `VacationGrantDialog.tsx` | number | 중간 | - -**고객 도메인 (clients/)** -| 파일 | 적용 대상 | 우선순위 | -|------|----------|----------| -| `ClientDetail.tsx` | phone, businessNumber | 높음 | -| `ClientRegistration.tsx` | phone, businessNumber | 높음 | -| `ClientDetailClientV2.tsx` | phone, businessNumber | 높음 | - -### 🟡 Tier 3: 보조 컴포넌트 -> 중요하지만 사용 빈도 낮음 - -**품목 관리 (items/)** -| 파일 | 적용 대상 | -|------|----------| -| `ItemDetailEdit.tsx` | currency, quantity | -| `ItemDetailView.tsx` | currency, quantity | -| `DynamicItemForm/` | number, currency | -| `BOMSection.tsx` | quantity | -| `ItemAddDialog.tsx` (orders) | quantity, currency | - -**자재/생산 (material/, production/)** -| 파일 | 적용 대상 | -|------|----------| -| `ReceivingDetail.tsx` | quantity | -| `ReceivingProcessDialog.tsx` | quantity | -| `StockStatusDetail.tsx` | quantity | -| `WorkOrderDetail.tsx` | quantity | -| `InspectionDetail.tsx` | quantity | -| `InspectionCreate.tsx` | quantity | - -**건설 도메인 (construction/)** -| 파일 | 적용 대상 | -|------|----------| -| `ContractDetailForm.tsx` | currency | -| `EstimateDetailForm.tsx` | currency, quantity | -| `BiddingDetailForm.tsx` | currency | -| `PartnerForm.tsx` | phone, businessNumber | -| `HandoverReportDetailForm.tsx` | number | -| `PricingDetailClient.tsx` | currency | -| `ProgressBillingItemTable.tsx` | currency, quantity | -| `OrderDetailItemTable.tsx` | currency, quantity | - -### 🟢 Tier 4: 기타 컴포넌트 -> 낮은 우선순위, 점진적 적용 - -**설정 (settings/)** -- `CompanyInfoManagement/` - businessNumber -- `PopupManagement/` - phone -- `AddCompanyDialog.tsx` - businessNumber - -**결재 (approval/)** -- `ExpenseReportForm.tsx` - currency -- `ProposalForm.tsx` - currency - -**문서 컴포넌트 (documents/)** -> 대부분 표시용으로 입력 필드 없음 - 확인 필요 -- `OrderDocumentModal.tsx` -- `TransactionDocument.tsx` -- `ContractDocument.tsx` - ---- - -## 3. 작업 단계별 계획 - -### Phase 4-1: 템플릿 레벨 수정 (핵심) -**목표**: IntegratedDetailTemplate에 새 입력 타입 지원 추가 - -``` -수정 파일: -1. src/components/templates/IntegratedDetailTemplate/types.ts - - FieldType에 'phone' | 'businessNumber' | 'currency' | 'quantity' 추가 - -2. src/components/templates/IntegratedDetailTemplate/FieldInput.tsx - - PhoneInput, BusinessNumberInput, CurrencyInput, QuantityInput import - - switch case에 새 타입 처리 추가 - -3. src/components/templates/IntegratedDetailTemplate/FieldRenderer.tsx - - 동일하게 수정 -``` - -**예상 영향**: 템플릿 사용 페이지 전체 자동 적용 - -### Phase 4-2: 회계 도메인 마이그레이션 -``` -1. PurchaseDetail.tsx → CurrencyInput -2. SalesDetail.tsx → CurrencyInput -3. BillDetail.tsx → CurrencyInput -4. DepositDetail.tsx → CurrencyInput -5. WithdrawalDetail.tsx → CurrencyInput -6. BadDebtDetail.tsx → CurrencyInput, PhoneInput -``` - -### Phase 4-3: 주문/견적 도메인 마이그레이션 -``` -1. OrderRegistration.tsx → CurrencyInput, QuantityInput -2. OrderSalesDetailEdit.tsx → CurrencyInput, QuantityInput -3. QuoteRegistration.tsx → CurrencyInput, QuantityInput, NumberInput -4. QuoteRegistrationV2.tsx → CurrencyInput, QuantityInput, NumberInput -``` - -### Phase 4-4: 인사 도메인 마이그레이션 -``` -1. EmployeeForm.tsx → PhoneInput, PersonalNumberInput -2. EmployeeDetail.tsx → PhoneInput, PersonalNumberInput -3. EmployeeDialog.tsx → PhoneInput -4. SalaryDetailDialog.tsx → CurrencyInput -``` - -### Phase 4-5: 고객/품목/자재 도메인 마이그레이션 -``` -1. ClientDetail.tsx → PhoneInput, BusinessNumberInput -2. ClientRegistration.tsx → PhoneInput, BusinessNumberInput -3. ItemDetailEdit.tsx → CurrencyInput, QuantityInput -4. ReceivingDetail.tsx → QuantityInput -5. StockStatusDetail.tsx → QuantityInput -``` - -### Phase 4-6: 건설/기타 도메인 마이그레이션 -``` -1. ContractDetailForm.tsx → CurrencyInput -2. EstimateDetailForm.tsx → CurrencyInput, QuantityInput -3. PartnerForm.tsx → PhoneInput, BusinessNumberInput -4. 기타 낮은 우선순위 파일들 -``` - ---- - -## 4. 마이그레이션 패턴 - -### 4.1 직접 Input → 새 컴포넌트 변환 - -**Before (기존)**: -```tsx - handleChange('price', e.target.value)} -/> -``` - -**After (CurrencyInput)**: -```tsx -import { CurrencyInput } from '@/components/ui/currency-input'; - - handleChange('price', value ?? 0)} -/> -``` - -**After (PhoneInput)**: -```tsx -import { PhoneInput } from '@/components/ui/phone-input'; - - handleChange('phone', value)} -/> -``` - -### 4.2 FormField 타입 변경 - -**Before**: -```tsx - -``` - -**After**: -```tsx - -``` - -### 4.3 Config 기반 (IntegratedDetailTemplate) - -**Before (config)**: -```tsx -{ - key: 'price', - label: '금액', - type: 'number', -} -``` - -**After (config)**: -```tsx -{ - key: 'price', - label: '금액', - type: 'currency', -} -``` - ---- - -## 5. 검증 계획 - -### 5.1 각 Phase 완료 후 검증 -- [ ] TypeScript 컴파일 오류 없음 -- [ ] 해당 페이지 렌더링 정상 -- [ ] 입력 필드 동작 확인 - - 포맷팅 정상 (콤마, 하이픈 등) - - leading zero 제거 확인 - - 값 저장/불러오기 정상 - -### 5.2 주요 테스트 시나리오 - -| 컴포넌트 | 테스트 입력 | 기대 결과 | -|----------|------------|----------| -| CurrencyInput | `1234567` | 표시: `₩ 1,234,567`, 값: `1234567` | -| PhoneInput | `01012345678` | 표시: `010-1234-5678`, 값: `01012345678` | -| BusinessNumberInput | `1234567890` | 표시: `123-45-67890`, 값: `1234567890` | -| QuantityInput | `007` | 표시: `7`, 값: `7` | -| NumberInput | `00123.45` | 표시: `123.45`, 값: `123.45` | - ---- - -## 6. 롤백 계획 - -문제 발생 시: -1. 해당 파일의 import 변경 롤백 -2. 컴포넌트 사용 부분을 기존 `` 으로 복원 -3. 템플릿 수정의 경우 FieldInput.tsx, FieldRenderer.tsx 롤백 - ---- - -## 7. 진행 상황 체크리스트 - -### Phase 4-1: 템플릿 수정 -- [ ] IntegratedDetailTemplate/types.ts 수정 -- [ ] IntegratedDetailTemplate/FieldInput.tsx 수정 -- [ ] IntegratedDetailTemplate/FieldRenderer.tsx 수정 -- [ ] 템플릿 사용 페이지 동작 확인 - -### Phase 4-2: 회계 도메인 -- [ ] PurchaseDetail.tsx -- [ ] SalesDetail.tsx -- [ ] BillDetail.tsx -- [ ] DepositDetail.tsx -- [ ] WithdrawalDetail.tsx -- [ ] BadDebtDetail.tsx - -### Phase 4-3: 주문/견적 도메인 -- [ ] OrderRegistration.tsx -- [ ] OrderSalesDetailEdit.tsx -- [ ] QuoteRegistration.tsx -- [ ] QuoteRegistrationV2.tsx - -### Phase 4-4: 인사 도메인 -- [ ] EmployeeForm.tsx -- [ ] EmployeeDetail.tsx -- [ ] EmployeeDialog.tsx -- [ ] SalaryDetailDialog.tsx - -### Phase 4-5: 고객/품목/자재 도메인 -- [ ] ClientDetail.tsx -- [ ] ClientRegistration.tsx -- [ ] ItemDetailEdit.tsx -- [ ] ReceivingDetail.tsx -- [ ] StockStatusDetail.tsx - -### Phase 4-6: 건설/기타 도메인 -- [ ] ContractDetailForm.tsx -- [ ] EstimateDetailForm.tsx -- [ ] PartnerForm.tsx -- [ ] 기타 파일들 - ---- - -## 8. 다음 단계 - -1. **즉시**: Phase 4-1 템플릿 레벨 수정 (최대 효과) -2. **순차**: Phase 4-2 ~ 4-6 도메인별 마이그레이션 -3. **최종**: 전체 빌드 및 통합 테스트 - ---- - -**참고**: VendorDetail.tsx 적용 결과 검증 완료됨 (2026-01-21) -- PhoneInput ✅ -- BusinessNumberInput ✅ -- CurrencyInput ✅ -- NumberInput ✅ diff --git a/claudedocs/architecture/[IMPL-2026-02-05] detail-hooks-migration-plan.md b/claudedocs/architecture/[IMPL-2026-02-05] detail-hooks-migration-plan.md deleted file mode 100644 index 79baa12b..00000000 --- a/claudedocs/architecture/[IMPL-2026-02-05] detail-hooks-migration-plan.md +++ /dev/null @@ -1,304 +0,0 @@ -# 상세 페이지 훅 마이그레이션 계획서 - -> 작성일: 2026-02-05 -> 상태: 계획 수립 - ---- - -## 1. 개요 - -### 1.1 목적 -- 상세/등록/수정 페이지의 반복 코드를 공통 훅으로 통합 -- 코드 일관성 확보 및 유지보수성 향상 -- 서비스 런칭 전 기술 부채 최소화 - -### 1.2 생성된 공통 훅 -| 훅 | 위치 | 역할 | -|----|------|------| -| `useDetailPageState` | `src/hooks/useDetailPageState.ts` | 페이지 상태 관리 (mode, id, navigation) | -| `useDetailData` | `src/hooks/useDetailData.ts` | 데이터 로딩 + 로딩/에러 상태 | -| `useCRUDHandlers` | `src/hooks/useCRUDHandlers.ts` | 등록/수정/삭제 + toast/redirect | -| `useDetailPermissions` | `src/hooks/useDetailPermissions.ts` | 권한 체크 | - -### 1.3 테스트 완료 -- [x] `BillDetail.tsx` → `BillDetailV2.tsx` 마이그레이션 성공 -- [x] 조회/수정/등록 모드 정상 작동 확인 -- [x] 유효성 검사 정상 작동 확인 - ---- - -## 2. 마이그레이션 대상 - -### 2.1 전체 현황 -| 구분 | 개수 | 비고 | -|------|------|------| -| IntegratedDetailTemplate 사용 | 47개 | 훅 마이그레이션 대상 | -| 레거시/커스텀 패턴 | 36개 | 별도 검토 (이번 범위 외) | -| **총계** | 83개 | | - -### 2.2 복잡도별 분류 -| 복잡도 | 기준 | 개수 | -|--------|------|------| -| 단순 | < 200줄, useState 3~4개 | 12개 | -| 보통 | 200~500줄, useState 5~7개 | 18개 | -| 복잡 | > 500줄, useState 8~11개 | 17개 | - ---- - -## 3. 도메인별 대상 목록 - -### 3.1 회계관리 (10개) - -| # | 파일 | 라인 | 복잡도 | 상태 | -|---|------|------|--------|------| -| 1 | `accounting/BadDebtCollection/BadDebtDetail.tsx` | 966 | 복잡 | ⬜ | -| 2 | `accounting/BillManagement/BillDetail.tsx` | 474 | 보통 | ✅ 완료 | -| 3 | `accounting/CardTransactionInquiry/CardTransactionDetailClient.tsx` | 138 | 단순 | ⬜ | -| 4 | `accounting/DepositManagement/DepositDetailClientV2.tsx` | 143 | 단순 | ⬜ | -| 5 | `accounting/PurchaseManagement/PurchaseDetail.tsx` | 698 | 복잡 | ⬜ | -| 6 | `accounting/SalesManagement/SalesDetail.tsx` | 581 | 복잡 | ⬜ | -| 7 | `accounting/VendorLedger/VendorLedgerDetail.tsx` | 385 | 보통 | ⬜ | -| 8 | `accounting/VendorManagement/VendorDetail.tsx` | 683 | 복잡 | ⬜ | -| 9 | `accounting/VendorManagement/VendorDetailClient.tsx` | 585 | 복잡 | ⬜ | -| 10 | `accounting/WithdrawalManagement/WithdrawalDetail.tsx` | 327 | 보통 | ⬜ | - -### 3.2 건설관리 (13개) - -| # | 파일 | 라인 | 복잡도 | 상태 | -|---|------|------|--------|------| -| 1 | `construction/bidding/BiddingDetailForm.tsx` | 544 | 복잡 | ⬜ | -| 2 | `construction/contract/ContractDetailForm.tsx` | 546 | 복잡 | ⬜ | -| 3 | `construction/estimates/EstimateDetailForm.tsx` | 763 | 복잡 | ⬜ | -| 4 | `construction/handover-report/HandoverReportDetailForm.tsx` | 699 | 복잡 | ⬜ | -| 5 | `construction/issue-management/IssueDetailForm.tsx` | 627 | 복잡 | ⬜ | -| 6 | `construction/item-management/ItemDetailClient.tsx` | 486 | 보통 | ⬜ | -| 7 | `construction/labor-management/LaborDetailClientV2.tsx` | 120 | 단순 | ⬜ | -| 8 | `construction/management/ConstructionDetailClient.tsx` | 739 | 복잡 | ⬜ | -| 9 | `construction/order-management/OrderDetailForm.tsx` | 275 | 보통 | ⬜ | -| 10 | `construction/pricing-management/PricingDetailClientV2.tsx` | 134 | 단순 | ⬜ | -| 11 | `construction/progress-billing/ProgressBillingDetailForm.tsx` | 193 | 단순 | ⬜ | -| 12 | `construction/site-management/SiteDetailForm.tsx` | 385 | 보통 | ⬜ | -| 13 | `construction/structure-review/StructureReviewDetailForm.tsx` | 392 | 보통 | ⬜ | - -### 3.3 기타 도메인 (24개) - -#### 고객센터 (3개) -| # | 파일 | 라인 | 복잡도 | 상태 | -|---|------|------|--------|------| -| 1 | `customer-center/EventManagement/EventDetail.tsx` | 101 | 단순 | ⬜ | -| 2 | `customer-center/InquiryManagement/InquiryDetail.tsx` | 357 | 보통 | ⬜ | -| 3 | `customer-center/NoticeManagement/NoticeDetail.tsx` | 101 | 단순 | ⬜ | - -#### 인사관리 (1개) -| # | 파일 | 라인 | 복잡도 | 상태 | -|---|------|------|--------|------| -| 1 | `hr/EmployeeManagement/EmployeeDetail.tsx` | 221 | 단순 | ⬜ | - -#### 자재관리 (2개) -| # | 파일 | 라인 | 복잡도 | 상태 | -|---|------|------|--------|------| -| 1 | `material/ReceivingManagement/ReceivingDetail.tsx` | ~350 | 보통 | ⬜ | -| 2 | `material/StockStatus/StockStatusDetail.tsx` | ~300 | 보통 | ⬜ | - -#### 주문관리 (2개) -| # | 파일 | 라인 | 복잡도 | 상태 | -|---|------|------|--------|------| -| 1 | `orders/OrderSalesDetailEdit.tsx` | 735 | 복잡 | ⬜ | -| 2 | `orders/OrderSalesDetailView.tsx` | 668 | 복잡 | ⬜ | - -#### 출고관리 (2개) -| # | 파일 | 라인 | 복잡도 | 상태 | -|---|------|------|--------|------| -| 1 | `outbound/ShipmentManagement/ShipmentDetail.tsx` | 670 | 복잡 | ⬜ | -| 2 | `outbound/VehicleDispatchManagement/VehicleDispatchDetail.tsx` | 180 | 단순 | ⬜ | - -#### 생산관리 (1개) -| # | 파일 | 라인 | 복잡도 | 상태 | -|---|------|------|--------|------| -| 1 | `production/WorkOrders/WorkOrderDetail.tsx` | 531 | 복잡 | ⬜ | - -#### 품질관리 (1개) -| # | 파일 | 라인 | 복잡도 | 상태 | -|---|------|------|--------|------| -| 1 | `quality/InspectionManagement/InspectionDetail.tsx` | 949 | 복잡 | ⬜ | - -#### 설정 (2개) -| # | 파일 | 라인 | 복잡도 | 상태 | -|---|------|------|--------|------| -| 1 | `settings/PermissionManagement/PermissionDetail.tsx` | 455 | 보통 | ⬜ | -| 2 | `settings/PopupManagement/PopupDetailClientV2.tsx` | 198 | 단순 | ⬜ | - -#### 거래처 (1개) -| # | 파일 | 라인 | 복잡도 | 상태 | -|---|------|------|--------|------| -| 1 | `clients/ClientDetailClientV2.tsx` | 252 | 단순 | ⬜ | - -#### 기타 (9개) -| # | 파일 | 라인 | 복잡도 | 상태 | -|---|------|------|--------|------| -| 1 | `board/BoardManagement/BoardDetail.tsx` | 119 | 단순 | ⬜ | -| 2 | `process-management/ProcessDetail.tsx` | 346 | 보통 | ⬜ | -| 3 | `process-management/StepDetail.tsx` | 143 | 단순 | ⬜ | -| 4 | `settings/AccountManagement/AccountDetail.tsx` | 355 | 보통 | ⬜ | -| 5 | `accounting/DepositManagement/DepositDetail.tsx` | 327 | 보통 | ⬜ | -| 6 | `clients/ClientDetail.tsx` | 253 | 보통 | ⬜ | -| 7 | `construction/labor-management/LaborDetailClient.tsx` | 471 | 보통 | ⬜ | -| 8 | `construction/pricing-management/PricingDetailClient.tsx` | 464 | 보통 | ⬜ | -| 9 | `quotes/LocationDetailPanel.tsx` | 826 | 복잡 | ⬜ | - ---- - -## 4. 작업 방식 - -### 4.1 Git 브랜치 전략 -``` -main - └── feature/detail-hooks-migration - ├── 회계관리 커밋 - ├── 건설관리 커밋 - └── 기타 도메인 커밋 -``` - -### 4.2 파일별 작업 순서 -1. 파일 읽기 및 현재 패턴 파악 -2. `useDetailData` 적용 (데이터 로딩 부분) -3. `useCRUDHandlers` 적용 (CRUD 핸들러 부분) -4. 개별 useState → 통합 formData 객체로 변환 (선택) -5. 기능 테스트 -6. 커밋 - -### 4.3 적용할 변경 패턴 - -#### Before (기존) -```tsx -const [data, setData] = useState(null); -const [isLoading, setIsLoading] = useState(true); -const [error, setError] = useState(null); - -useEffect(() => { - fetchData(id).then(result => { - if (result.success) setData(result.data); - else setError(result.error); - }).finally(() => setIsLoading(false)); -}, [id]); - -const handleSubmit = async () => { - const result = await updateData(id, formData); - if (result.success) { - toast.success('저장되었습니다.'); - router.push('/list'); - } else { - toast.error(result.error); - } -}; -``` - -#### After (신규) -```tsx -const { data, isLoading, error } = useDetailData(id, fetchData); - -const { handleUpdate, isSubmitting } = useCRUDHandlers({ - onUpdate: updateData, - successRedirect: '/list', - successMessages: { update: '저장되었습니다.' }, -}); -``` - ---- - -## 5. 일정 계획 - -| Phase | 대상 | 파일 수 | 예상 기간 | -|-------|------|---------|----------| -| Phase 1 | 회계관리 | 10개 | 1일 | -| Phase 2 | 건설관리 | 13개 | 1.5일 | -| Phase 3 | 기타 도메인 | 24개 | 2일 | -| Phase 4 | 통합 테스트 | - | 1일 | -| **총계** | | **47개** | **약 5~6일** | - ---- - -## 6. 체크리스트 - -### 6.1 사전 준비 -- [x] 공통 훅 4개 생성 완료 -- [x] 테스트 마이그레이션 (BillDetail) 완료 -- [x] 계획서 작성 -- [ ] 브랜치 생성 - -### 6.2 Phase 1: 회계관리 (0/10) -- [ ] BadDebtDetail.tsx -- [x] BillDetail.tsx ✅ -- [ ] CardTransactionDetailClient.tsx -- [ ] DepositDetailClientV2.tsx -- [ ] PurchaseDetail.tsx -- [ ] SalesDetail.tsx -- [ ] VendorLedgerDetail.tsx -- [ ] VendorDetail.tsx -- [ ] VendorDetailClient.tsx -- [ ] WithdrawalDetail.tsx - -### 6.3 Phase 2: 건설관리 (0/13) -- [ ] BiddingDetailForm.tsx -- [ ] ContractDetailForm.tsx -- [ ] EstimateDetailForm.tsx -- [ ] HandoverReportDetailForm.tsx -- [ ] IssueDetailForm.tsx -- [ ] ItemDetailClient.tsx -- [ ] LaborDetailClientV2.tsx -- [ ] ConstructionDetailClient.tsx -- [ ] OrderDetailForm.tsx -- [ ] PricingDetailClientV2.tsx -- [ ] ProgressBillingDetailForm.tsx -- [ ] SiteDetailForm.tsx -- [ ] StructureReviewDetailForm.tsx - -### 6.4 Phase 3: 기타 도메인 (0/24) -- [ ] EventDetail.tsx -- [ ] InquiryDetail.tsx -- [ ] NoticeDetail.tsx -- [ ] EmployeeDetail.tsx -- [ ] ReceivingDetail.tsx -- [ ] StockStatusDetail.tsx -- [ ] OrderSalesDetailEdit.tsx -- [ ] OrderSalesDetailView.tsx -- [ ] ShipmentDetail.tsx -- [ ] VehicleDispatchDetail.tsx -- [ ] WorkOrderDetail.tsx -- [ ] InspectionDetail.tsx -- [ ] PermissionDetail.tsx -- [ ] PopupDetailClientV2.tsx -- [ ] ClientDetailClientV2.tsx -- [ ] BoardDetail.tsx -- [ ] ProcessDetail.tsx -- [ ] StepDetail.tsx -- [ ] AccountDetail.tsx -- [ ] DepositDetail.tsx -- [ ] ClientDetail.tsx -- [ ] LaborDetailClient.tsx -- [ ] PricingDetailClient.tsx -- [ ] LocationDetailPanel.tsx - -### 6.5 완료 후 -- [ ] 전체 기능 테스트 -- [ ] 코드 리뷰 -- [ ] PR 머지 -- [ ] BillDetailV2.tsx 정리 (원본으로 교체) - ---- - -## 7. 위험 요소 및 대응 - -| 위험 | 가능성 | 대응 | -|------|--------|------| -| 기존 기능 손상 | 중 | 파일별 테스트, Git 롤백 준비 | -| 예상보다 복잡한 파일 | 중 | 복잡한 파일은 부분 적용 허용 | -| 타입 에러 | 높 | 래퍼 함수로 타입 호환성 확보 | - ---- - -## 8. 참고 자료 - -- 공통 훅 소스: `src/hooks/index.ts` -- 테스트 케이스: `BillDetailV2.tsx` -- 기존 템플릿: `IntegratedDetailTemplate.tsx` diff --git a/claudedocs/architecture/[IMPL-2026-02-05] formatter-commonization-plan.md b/claudedocs/architecture/[IMPL-2026-02-05] formatter-commonization-plan.md deleted file mode 100644 index 5cb6e846..00000000 --- a/claudedocs/architecture/[IMPL-2026-02-05] formatter-commonization-plan.md +++ /dev/null @@ -1,88 +0,0 @@ -# 금액/날짜 포맷터 공통화 계획 - -> 작성일: 2026-02-05 -> 상태: ✅ 완료 -> 목적: 중복 정의된 formatAmount, formatDate 함수를 공통 유틸로 통합 - ---- - -## 📊 현황 분석 - -### 이미 존재하는 유틸 - -| 파일 | 함수 | 설명 | -|------|------|------| -| `src/utils/formatAmount.ts` | `formatAmount()` | 자동 만원 변환 (1만 이상 → "N만원") | -| | `formatAmountWon()` | 항상 원 단위 ("N원") | -| | `formatAmountManwon()` | 항상 만원 단위 ("N만원") | -| | `formatKoreanAmount()` | 억/만 축약 ("1억 5,000만") | -| | `formatNumber()` | **신규** 단순 천단위 콤마 | -| `src/utils/date.ts` | `getLocalDateString()` | YYYY-MM-DD 반환 | -| | `getTodayString()` | 오늘 날짜 YYYY-MM-DD | -| | `formatDateForInput()` | input용 날짜 변환 | -| | `formatDate()` | **신규** YYYY-MM-DD 표시용 | -| | `formatDateRange()` | **신규** "시작 ~ 종료" 형식 | - ---- - -## 📊 결과 요약 - -### 마이그레이션 완료 파일 - -#### formatAmount → formatNumber (12개 파일) ✅ -| 파일 | 상태 | -|------|------| -| `construction/contract/ContractListClient.tsx` | ✅ 완료 | -| `construction/contract/ContractDetailForm.tsx` | ✅ 완료 | -| `construction/bidding/BiddingListClient.tsx` | ✅ 완료 | -| `construction/bidding/BiddingDetailForm.tsx` | ✅ 완료 | -| `construction/estimates/EstimateListClient.tsx` | ✅ 완료 | -| `construction/estimates/modals/EstimateDocumentContent.tsx` | ✅ 완료 | -| `construction/handover-report/HandoverReportListClient.tsx` | ✅ 완료 | -| `construction/handover-report/HandoverReportDetailForm.tsx` | ✅ 완료 | -| `construction/handover-report/modals/HandoverReportDocumentModal.tsx` | ✅ 완료 | -| `construction/utility-management/UtilityManagementListClient.tsx` | ✅ 완료 | -| `construction/estimates/utils/formatters.ts` | ✅ re-export로 변경 | - -#### formatDate 공통화 (7개 파일) ✅ -| 파일 | 상태 | -|------|------| -| `construction/contract/ContractListClient.tsx` | ✅ 완료 (formatDateRange) | -| `construction/bidding/BiddingListClient.tsx` | ✅ 완료 | -| `construction/handover-report/HandoverReportListClient.tsx` | ✅ 완료 (formatDateRange) | -| `construction/utility-management/UtilityManagementListClient.tsx` | ✅ 완료 | -| `construction/issue-management/IssueManagementListClient.tsx` | ✅ 완료 | -| `construction/structure-review/StructureReviewListClient.tsx` | ✅ 완료 | -| `construction/management/ConstructionDetailClient.tsx` | ✅ 완료 | - -#### 마이그레이션 제외 (한글 형식 유지) -| 파일 | 사유 | -|------|------| -| `handover-report/modals/HandoverReportDocumentModal.tsx` | 한글 형식 ("년 월 일") | -| `order-management/modals/OrderDocumentModal.tsx` | 한글 형식 ("년 월 일") | - ---- - -## 📋 효과 - -| 항목 | Before | After | -|------|--------|-------| -| formatAmount 정의 | 12개 파일 | 1개 파일 (`formatNumber`) | -| formatDate 정의 | 8개 파일 | 1개 파일 | -| 중복 코드 라인 | ~150줄 | 0줄 | -| 포맷 변경 시 수정 | 20개 파일 | 1개 파일 | - ---- - -## ⚠️ 주의사항 - -1. **기존 formatAmount()와 formatNumber() 차이** - - 기존 `formatAmount()`: 자동 만원 변환 (유지됨) - - 신규 `formatNumber()`: 단순 천단위 콤마만 - -2. **한글 날짜 형식은 별도 유지** - - 문서 모달에서 사용하는 "년 월 일" 형식은 로컬 유지 - - 공통 `formatDate()`는 YYYY-MM-DD 형식만 처리 - -3. **backward compatibility** - - `estimates/utils/formatters.ts`는 `formatNumber`를 `formatAmount`로 re-export diff --git a/claudedocs/architecture/[IMPL-2026-02-11] dynamic-field-components.md b/claudedocs/architecture/[IMPL-2026-02-11] dynamic-field-components.md deleted file mode 100644 index 7a54aed4..00000000 --- a/claudedocs/architecture/[IMPL-2026-02-11] dynamic-field-components.md +++ /dev/null @@ -1,343 +0,0 @@ -# 동적 필드 타입 컴포넌트 — 프론트엔드 구현 기획서 - -> 작성일: 2026-02-11 -> 설계 근거: `[DESIGN-2026-02-11] dynamic-field-type-extension.md` -> 상태: ✅ 프론트 구현 완료 — 백엔드 작업 대기 -> 백엔드 스펙: `item-master/[API-REQUEST-2026-02-12] dynamic-field-type-backend-spec.md` - ---- - -## 목적 - -현재 DynamicItemForm의 필드 타입이 6종(textbox, number, dropdown, checkbox, date, textarea)으로 제한되어 있어 제조/공사/유통/물류 등 다양한 산업의 품목관리 요구를 충족하지 못함. - -**이 작업의 목표**: -- 8종의 신규 필드 컴포넌트를 미리 만들어둔다 -- 범용 테이블 섹션(DynamicTableSection)을 만든다 -- 백엔드가 `field_type` + `properties` config를 보내면 자동 렌더링되는 구조를 완성한다 -- 백엔드 작업 전에도 mock props로 독립 테스트 가능하게 한다 - -**API 연동 시 동작 흐름**: -``` -백엔드 DB: field_type = "reference", properties = { "source": "vendors" } - ↓ -API 응답: GET /v1/item-master/pages/{id}/structure - ↓ -프론트: DynamicFieldRenderer → switch("reference") → - ReferenceField가 properties.source 읽어서 /api/proxy/vendors 검색 API 호출 -``` - ---- - -## 컴포넌트 구현 목록 - -### Phase 1: 핵심 컴포넌트 - -| # | 컴포넌트 | field_type | 상태 | 비고 | -|---|---------|-----------|------|------| -| 1-1 | ReferenceField | `reference` | ✅ 완료 | 다른 테이블 검색/선택 | -| 1-2 | MultiSelectField | `multi-select` | ✅ 완료 | 복수 선택 태그 | -| 1-3 | FileField | `file` | ✅ 완료 | 파일/이미지 업로드 | -| 1-4 | DynamicTableSection | (섹션) | ✅ 완료 | 범용 테이블 섹션 | -| 1-5 | TableCellRenderer | (내부) | ✅ 완료 | 테이블 셀 렌더러 | -| 1-6 | reference-sources.ts | (config) | ✅ 완료 | 참조 소스 프리셋 정의 | -| 1-7 | DynamicFieldRenderer 확장 | (수정) | ✅ 완료 | switch문 + 신규 import | -| 1-8 | 타입 정의 확장 | (수정) | ✅ 완료 | ItemFieldType 통합 + config 인터페이스 | - -### Phase 2: 편의 컴포넌트 - -| # | 컴포넌트 | field_type | 상태 | 비고 | -|---|---------|-----------|------|------| -| 2-1 | CurrencyField | `currency` | ✅ 완료 | 통화 금액 (천단위 포맷) | -| 2-2 | UnitValueField | `unit-value` | ✅ 완료 | 값+단위 조합 (100mm) | -| 2-3 | RadioField | `radio` | ✅ 완료 | 라디오 버튼 그룹 | -| 2-4 | section-presets.ts | (config) | ✅ 완료 | 산업별 섹션 프리셋 JSON | - -### Phase 3: 고급 컴포넌트 - -| # | 컴포넌트 | field_type | 상태 | 비고 | -|---|---------|-----------|------|------| -| 3-1 | ToggleField | `toggle` | ✅ 완료 | On/Off 스위치 | -| 3-2 | ComputedField | `computed` | ✅ 완료 | 계산 필드 (읽기전용) | -| 3-3 | 조건부 표시 연산자 확장 | (수정) | ✅ 완료 | in/not_in/greater_than 등 9종 | - ---- - -## 각 컴포넌트 스펙 - -### 1-1. ReferenceField - -**파일**: `DynamicItemForm/fields/ReferenceField.tsx` - -**역할**: 다른 테이블의 데이터를 검색하여 선택 (거래처, 품목, 고객, 현장, 차량 등) - -**UI 구성**: -- 읽기전용 Input + 검색 버튼(돋보기 아이콘) -- 클릭 시 SearchableSelectionModal 열림 -- 선택 후 displayField 값 표시 -- X 버튼으로 선택 해제 - -**properties에서 읽는 값**: -```typescript -interface ReferenceConfig { - source: string; // "vendors" | "items" | "custom" 등 - displayField?: string; // 기본 "name" - valueField?: string; // 기본 "id" - searchFields?: string[]; // 기본 ["name"] - searchApiUrl?: string; // source="custom"일 때 필수 - columns?: Array<{ key: string; label: string; width?: string }>; - displayFormat?: string; // "{code} - {name}" - returnFields?: string[]; // ["id", "code", "name"] -} -``` - -**API 연동 시**: -- `REFERENCE_SOURCES[source]`에서 apiUrl 조회 -- `GET {apiUrl}?search={query}&size=20` 호출 -- 결과를 SearchableSelectionModal에 표시 - -**API 연동 전 (mock)**: -- props로 전달된 options 사용 또는 -- 빈 상태에서 UI/UX만 확인 - ---- - -### 1-2. MultiSelectField - -**파일**: `DynamicItemForm/fields/MultiSelectField.tsx` - -**역할**: 여러 항목을 동시에 선택 (태그 칩 형태로 표시) - -**UI 구성**: -- Combobox (검색 가능한 드롭다운) -- 선택된 항목은 칩(Chip/Badge)으로 표시 -- 칩의 X 버튼으로 개별 해제 - -**properties에서 읽는 값**: -```typescript -interface MultiSelectConfig { - maxSelections?: number; // 최대 선택 수 (기본: 무제한) - allowCustom?: boolean; // 직접 입력 허용 (기본: false) - layout?: 'chips' | 'list'; // 기본: "chips" -} -``` - -**options**: 기존 dropdown과 동일 `[{label, value}]` - -**저장값**: `string[]` (예: `["CUT", "BEND", "WELD"]`) - ---- - -### 1-3. FileField - -**파일**: `DynamicItemForm/fields/FileField.tsx` - -**역할**: 파일/이미지 첨부 - -**UI 구성**: -- 파일 선택 버튼 ("파일 선택" 또는 드래그 앤 드롭 영역) -- 선택된 파일 목록 표시 (이름, 크기, 삭제 버튼) -- 이미지 파일일 경우 미리보기 썸네일 - -**properties에서 읽는 값**: -```typescript -interface FileConfig { - accept?: string; // ".pdf,.doc" (기본: "*") - maxSize?: number; // bytes (기본: 10MB = 10485760) - maxFiles?: number; // 기본: 1 - preview?: boolean; // 이미지 미리보기 (기본: true) - category?: string; // 파일 카테고리 태그 -} -``` - -**API 연동 시**: `POST /v1/files/upload` (multipart) -**API 연동 전**: File 객체를 로컬 상태로 관리, URL.createObjectURL로 미리보기 - ---- - -### 1-4. DynamicTableSection - -**파일**: `DynamicItemForm/sections/DynamicTableSection.tsx` - -**역할**: config 기반 범용 테이블 (공정, 품질검사, 구매처, 공정표, 배차 등) - -**UI 구성**: -- 테이블 헤더 (columns config 기반) -- 행 추가/삭제 버튼 -- 각 셀은 TableCellRenderer (= DynamicFieldRenderer 재사용) -- 요약행 (선택, summaryRow config) -- 빈 상태 메시지 - -**props**: -```typescript -interface DynamicTableSectionProps { - section: ItemSectionResponse; - tableConfig: TableConfig; - rows: Record[]; - onRowsChange: (rows: Record[]) => void; - disabled?: boolean; -} -``` - -**tableConfig 구조**: 설계서 섹션 4.3 참조 - -**API 연동 시**: `GET/PUT /v1/items/{itemId}/section-data/{sectionId}` -**API 연동 전**: rows를 폼 상태(formData)에 로컬 관리 - ---- - -### 1-5. TableCellRenderer - -**파일**: `DynamicItemForm/sections/TableCellRenderer.tsx` - -**역할**: 테이블 셀 = DynamicFieldRenderer를 테이블 셀용 축소 모드로 래핑 - -**핵심**: column config → ItemFieldResponse 호환 객체로 변환 → DynamicFieldRenderer 호출 - -```typescript -function TableCellRenderer({ column, value, onChange, compact }) { - const fieldLike = columnToFieldResponse(column); - return ; -} -``` - ---- - -### 1-6. reference-sources.ts - -**파일**: `DynamicItemForm/config/reference-sources.ts` - -**역할**: reference 필드의 소스별 기본 설정 (API URL, 표시 필드, 검색 컬럼) - -**내용**: 공통(vendors, items, customers, employees, warehouses) + 산업별(processes, sites, vehicles, stores 등) - -**확장 방법**: 새 소스 추가 = 이 파일에 객체 1개 추가 - ---- - -### 2-1. CurrencyField - -**파일**: `DynamicItemForm/fields/CurrencyField.tsx` - -**역할**: 통화 금액 입력 (천단위 콤마, 통화 기호) - -**UI**: Input + 통화기호(₩) prefix + 천단위 포맷 -- 입력 중: 숫자만 -- 포커스 아웃: "₩15,000" 포맷 - -**properties**: `{ currency, precision, showSymbol, allowNegative }` - -**저장값**: `number` (포맷 없이) - ---- - -### 2-2. UnitValueField - -**파일**: `DynamicItemForm/fields/UnitValueField.tsx` - -**역할**: 값 + 단위 조합 입력 (100mm, 50kg) - -**UI**: Input(숫자) + Select(단위) 가로 배치 - -**properties**: `{ units, defaultUnit, precision }` - -**저장값**: `{ value: number, unit: string }` - ---- - -### 2-3. RadioField - -**파일**: `DynamicItemForm/fields/RadioField.tsx` - -**역할**: 라디오 버튼 그룹 - -**UI**: RadioGroup (수평/수직) - -**properties**: `{ layout: "horizontal" | "vertical" }` - -**options**: `[{label, value}]` - ---- - -### 3-1. ToggleField - -**파일**: `DynamicItemForm/fields/ToggleField.tsx` - -**역할**: On/Off 토글 스위치 - -**UI**: Switch + 라벨 - -**properties**: `{ onLabel, offLabel, onValue, offValue }` - ---- - -### 3-2. ComputedField - -**파일**: `DynamicItemForm/fields/ComputedField.tsx` - -**역할**: 다른 필드 기반 자동 계산 (읽기 전용) - -**UI**: 읽기전용 표시 (배경색 구분, muted) - -**properties**: `{ formula, dependsOn, format, precision }` - -**동작**: `dependsOn` 필드 값이 변경될 때마다 formula 재계산 - ---- - -## 파일 구조 - -``` -DynamicItemForm/ -├── fields/ -│ ├── DynamicFieldRenderer.tsx ← switch 확장 -│ ├── TextField.tsx (기존) -│ ├── NumberField.tsx (기존) -│ ├── DropdownField.tsx (기존) -│ ├── CheckboxField.tsx (기존) -│ ├── DateField.tsx (기존) -│ ├── TextareaField.tsx (기존) -│ ├── ReferenceField.tsx ★ Phase 1 -│ ├── MultiSelectField.tsx ★ Phase 1 -│ ├── FileField.tsx ★ Phase 1 -│ ├── CurrencyField.tsx ★ Phase 2 -│ ├── UnitValueField.tsx ★ Phase 2 -│ ├── RadioField.tsx ★ Phase 2 -│ ├── ToggleField.tsx ★ Phase 3 -│ └── ComputedField.tsx ★ Phase 3 -├── sections/ -│ ├── DynamicBOMSection.tsx (기존) -│ ├── DynamicTableSection.tsx ★ Phase 1 -│ └── TableCellRenderer.tsx ★ Phase 1 -├── config/ -│ └── reference-sources.ts ★ Phase 1 -├── presets/ -│ └── section-presets.ts ★ Phase 2 -├── hooks/ (기존, 변경 없음) -├── types.ts ← 타입 확장 -└── index.tsx ← table 섹션 렌더링 추가 -``` - -## 기존 코드 수정 범위 - -| 파일 | 수정 내용 | 줄 수 | -|------|----------|-------| -| `DynamicFieldRenderer.tsx` | switch case 8개 추가 + import | +20줄 | -| `types.ts` | ExtendedFieldType union + config 인터페이스 | +80줄 | -| `index.tsx` | 섹션 렌더링에 `case 'table'` 추가 | +15줄 | -| `item-master-api.ts` | field_type union 확장 | +3줄 | - -**기존 컴포넌트 6개 + BOM + hooks 7개 = 변경 없음** - ---- - -## 상태 범례 - -- ⬜ 대기 -- 🔄 진행 중 -- ✅ 완료 -- ⏸️ 보류 - ---- - -**마지막 업데이트**: 2026-02-12 — Phase 1+2+3 전체 완료 (15/15 항목) diff --git a/claudedocs/architecture/[IMPL-2026-02-23] phase1-item4-error-format.md b/claudedocs/architecture/[IMPL-2026-02-23] phase1-item4-error-format.md deleted file mode 100644 index 50bf72af..00000000 --- a/claudedocs/architecture/[IMPL-2026-02-23] phase1-item4-error-format.md +++ /dev/null @@ -1,112 +0,0 @@ -# Phase 1-4: 에러 메시지 포맷 통합 (`formatApiError` 제거) - -> 난이도: 저 | 영향도: 🟡 API 레이어 정리 | 예상 변경: 1파일 삭제 - ---- - -## 현황 요약 - -에러 메시지 포맷팅 함수가 2곳에 중복: - -| 파일 | 함수 | 외부 사용처 | -|------|------|------------| -| `src/lib/api/error-handler.ts:122` | `getErrorMessage()` | **5+ 파일** (활발히 사용) | -| `src/lib/api/toast-utils.ts:106` | `formatApiError()` | **0건** (dead code) | - -또한 `SHOW_ERROR_CODE` 상수도 양쪽에 중복 정의됨. - ---- - -## 핵심 발견: toast-utils.ts 전체가 dead code - -`from '@/lib/api/toast-utils'` 를 import하는 파일이 **0건**. - -``` -toast-utils.ts 내보내는 함수 전부 미사용: -- toastApiError() → 0 import -- toastSuccess() → 0 import -- toastWarning() → 0 import -- toastInfo() → 0 import -- formatApiError() → 0 import -``` - -현재 프로젝트에서 에러 토스트 표시는 직접 `toast.error(getErrorMessage(err))` 패턴으로 처리 중. - ---- - -## 작업 내역 - -### Step 1: `src/lib/api/toast-utils.ts` 삭제 - -파일 전체가 dead code이므로 삭제. - -### Step 2: (선택) 유용한 헬퍼를 error-handler.ts로 이동 - -`toastApiError()` 함수는 validation 에러의 첫 번째 필드를 표시하는 로직이 있어, -향후 유용할 수 있으면 error-handler.ts 하단에 통합 가능. - -```typescript -// src/lib/api/error-handler.ts 하단에 추가 (선택) -import { toast } from 'sonner'; - -export function toastApiError(error: unknown, fallbackMessage = '오류가 발생했습니다.'): void { - if (error instanceof ApiError && error.errors && SHOW_ERROR_CODE) { - const firstField = Object.keys(error.errors)[0]; - if (firstField) { - toast.error(`${getErrorMessage(error)}\n${firstField}: ${error.errors[firstField][0]}`); - return; - } - } - toast.error(getErrorMessage(error) || fallbackMessage); -} -``` - -이 step은 **선택**. 현재 사용처가 없으므로 당장은 삭제만으로 충분. - -### Step 3: 검증 - -```bash -npx tsc --noEmit -``` - -toast-utils.ts를 삭제해도 외부 import가 없으므로 타입 에러 없음. - ---- - -## 관련 파일 참조 - -### 활발히 사용 중인 함수 (변경 없음) - -`getErrorMessage()` 사용처 (error-handler.ts에서 export): -- `src/contexts/ItemMasterContext.tsx` (line 7, 589, 682) -- `src/components/items/ItemMasterDataManagement/hooks/usePageManagement.ts` (line 7, 122, 159, 198, 219) -- `src/components/items/ItemMasterDataManagement/hooks/useImportManagement.ts` (line 5, 58, 80, 92) -- `src/components/items/ItemMasterDataManagement/hooks/useInitialDataLoading.ts` (line 7, 130) -- `src/app/[locale]/(protected)/sales/client-management-sales-admin/page.tsx` (line 40, 301, 347) - -### 삭제 대상 - -- `src/lib/api/toast-utils.ts` (전체 116줄) - ---- - -## 중복 구조 비교 - -``` -error-handler.ts toast-utils.ts (삭제 대상) -───────────────── ────────────────────────── -const SHOW_ERROR_CODE = true; const SHOW_ERROR_CODE = true; ← 중복 - -getErrorMessage(error): formatApiError(error): - DuplicateCodeError → [status] ApiError → [status] msg - ApiError → [status] msg else → getErrorMessage() ← 결국 위임 - Error → .message - unknown → 기본 메시지 - toastApiError(error): - DuplicateCodeError → toast ← getErrorMessage와 동일 로직 - ApiError → toast - Error → toast - unknown → toast -``` - -`formatApiError`는 결국 `getErrorMessage`를 호출하는 래퍼에 불과. 삭제해도 기능 손실 없음. diff --git a/claudedocs/architecture/[IMPL-2026-02-23] phase1-item5-zustand-selectors.md b/claudedocs/architecture/[IMPL-2026-02-23] phase1-item5-zustand-selectors.md deleted file mode 100644 index cbc53aca..00000000 --- a/claudedocs/architecture/[IMPL-2026-02-23] phase1-item5-zustand-selectors.md +++ /dev/null @@ -1,229 +0,0 @@ -# Phase 1-5: Zustand 셀렉터 훅 추가 (3개 스토어) - -> 난이도: 저 | 영향도: 🟡 리렌더 최적화 | 예상 변경: 3 스토어 + 4 컨슈머 - ---- - -## 현황 요약 - -셀렉터 없이 전체 스토어를 구독하면, 무관한 상태 변경에도 컴포넌트가 리렌더됩니다. - -| 스토어 | 셀렉터 훅 | 사용처 | 문제 | -|--------|----------|--------|------| -| ✅ `masterDataStore` | `usePageConfig()` 등 | 다수 | 양호 | -| ✅ `authStore` | `useCurrentUser()` 등 | 4곳 | 양호 (방금 추가) | -| ❌ `useTableColumnStore` | 없음 | 1곳 | 전체 스토어 구독 | -| ❌ `useMenuStore` | 없음 | 15곳 | 일부 전체 구독 | -| ❌ `useThemeStore` | 없음 | 2곳 | 전체 구독 | - ---- - -## 작업 내역 - -### Step 1: `src/stores/useTableColumnStore.ts` — 셀렉터 훅 추가 - -파일 끝에 추가: - -```typescript -// ===== 셀렉터 훅 ===== - -/** 특정 페이지의 컬럼 설정만 구독 */ -export const usePageColumnSettings = (pageId: string) => - useTableColumnStore((state) => state.pageSettings[pageId] ?? DEFAULT_PAGE_SETTINGS); - -/** 특정 페이지의 숨김 컬럼만 구독 */ -export const useHiddenColumns = (pageId: string) => - useTableColumnStore((state) => state.pageSettings[pageId]?.hiddenColumns ?? []); - -/** 특정 페이지의 컬럼 너비만 구독 */ -export const useColumnWidths = (pageId: string) => - useTableColumnStore((state) => state.pageSettings[pageId]?.columnWidths ?? {}); -``` - -**주의**: `DEFAULT_PAGE_SETTINGS` 객체는 파일 내에 이미 정의되어 있음 (line 30-33). - -**컨슈머 변경** — `src/hooks/useColumnSettings.ts`: - -```typescript -// Before (line 17) -const store = useTableColumnStore(); // 전체 스토어 구독 -const settings = store.getPageSettings(pageId); - -// After -const settings = usePageColumnSettings(pageId); // 해당 페이지 설정만 구독 -const { setColumnWidth: storeSetWidth, toggleColumnVisibility: storeToggle, resetPageSettings } = useTableColumnStore.getState(); -// 또는 액션만 별도 구독 (액션은 참조 안정적이라 리렌더 유발 안 함): -const setColumnWidth = useTableColumnStore((s) => s.setColumnWidth); -const toggleColumnVisibility = useTableColumnStore((s) => s.toggleColumnVisibility); -const resetPageSettings = useTableColumnStore((s) => s.resetPageSettings); -``` - ---- - -### Step 2: `src/stores/menuStore.ts` — 셀렉터 훅 추가 - -파일 끝에 추가: - -```typescript -// ===== 셀렉터 훅 ===== - -/** 사이드바 접힘 상태만 구독 */ -export const useSidebarCollapsed = () => - useMenuStore((state) => state.sidebarCollapsed); - -/** 활성 메뉴 ID만 구독 */ -export const useActiveMenu = () => - useMenuStore((state) => state.activeMenu); - -/** 메뉴 아이템 목록만 구독 */ -export const useMenuItems = () => - useMenuStore((state) => state.menuItems); - -/** 하이드레이션 완료 여부만 구독 */ -export const useMenuHydrated = () => - useMenuStore((state) => state._hasHydrated); -``` - -**컨슈머 변경 대상**: - -#### 2-A. `src/layouts/AuthenticatedLayout.tsx` (line 99) — 🔴 핵심 - -현재: 전체 스토어 디스트럭처링 -```typescript -const { menuItems, activeMenu, setActiveMenu, setMenuItems, sidebarCollapsed, toggleSidebar, _hasHydrated } = useMenuStore(); -``` - -변경: -```typescript -const menuItems = useMenuItems(); -const activeMenu = useActiveMenu(); -const sidebarCollapsed = useSidebarCollapsed(); -const _hasHydrated = useMenuHydrated(); -// 액션은 참조 안정적이므로 별도 셀렉터: -const setActiveMenu = useMenuStore((s) => s.setActiveMenu); -const setMenuItems = useMenuStore((s) => s.setMenuItems); -const toggleSidebar = useMenuStore((s) => s.toggleSidebar); -``` - -#### 2-B. `src/components/production/WorkerScreen/index.tsx` (line 327) - -현재: -```typescript -const { sidebarCollapsed } = useMenuStore(); // 전체 구독 -``` - -변경: -```typescript -const sidebarCollapsed = useSidebarCollapsed(); -``` - -#### 2-C. `src/components/layout/CommandMenuSearch.tsx` (line 68) - -현재: -```typescript -const { menuItems } = useMenuStore(); // 전체 구독 -``` - -변경: -```typescript -const menuItems = useMenuItems(); -``` - -#### 2-D. 나머지 sidebarCollapsed 사용 파일 (이미 셀렉터 패턴) - -아래 파일들은 이미 `useMenuStore((state) => state.sidebarCollapsed)` 패턴을 사용 중이므로 **변경 불필요**: -- `ItemDetail.tsx`, `ChecklistDetail.tsx`, `PriceDistributionDetail.tsx` -- `StepDetail.tsx`, `PermissionDetailClient.tsx`, `BoardDetail/index.tsx` -- `ProcessDetail.tsx`, `PricingTableForm.tsx`, `DynamicItemForm/index.tsx` -- `ItemDetailClient.tsx`, `ClientDetail.tsx`, `DetailActions.tsx` - -단, 셀렉터 훅이 추가되면 이 파일들도 향후 `useSidebarCollapsed()`로 전환 가능 (선택). - ---- - -### Step 3: `src/stores/themeStore.ts` — 셀렉터 훅 추가 - -파일 끝에 추가: - -```typescript -// ===== 셀렉터 훅 ===== - -/** 현재 테마만 구독 */ -export const useTheme = () => - useThemeStore((state) => state.theme); - -/** setTheme 액션만 구독 */ -export const useSetTheme = () => - useThemeStore((state) => state.setTheme); -``` - -**컨슈머 변경 대상**: - -#### 3-A. `src/layouts/AuthenticatedLayout.tsx` (line 100) - -현재: -```typescript -const { theme, setTheme } = useThemeStore(); -``` - -변경: -```typescript -const theme = useTheme(); -const setTheme = useSetTheme(); -``` - -#### 3-B. `src/components/ThemeSelect.tsx` (line 24) - -현재: -```typescript -const { theme, setTheme } = useThemeStore(); -``` - -변경: -```typescript -const theme = useTheme(); -const setTheme = useSetTheme(); -``` - ---- - -## 검증 - -```bash -npx tsc --noEmit -``` - -셀렉터 훅은 기존 API에 추가만 하는 것이므로 기존 코드에 영향 없음. -컨슈머 변경은 import 경로와 호출 패턴만 바뀌므로 타입 에러 가능성 낮음. - ---- - -## 변경 파일 총 정리 - -| # | 파일 | 작업 | 내용 | -|---|------|------|------| -| 1 | `src/stores/useTableColumnStore.ts` | 추가 | 셀렉터 훅 3개 (`usePageColumnSettings`, `useHiddenColumns`, `useColumnWidths`) | -| 2 | `src/stores/menuStore.ts` | 추가 | 셀렉터 훅 4개 (`useSidebarCollapsed`, `useActiveMenu`, `useMenuItems`, `useMenuHydrated`) | -| 3 | `src/stores/themeStore.ts` | 추가 | 셀렉터 훅 2개 (`useTheme`, `useSetTheme`) | -| 4 | `src/hooks/useColumnSettings.ts` | 수정 | `useTableColumnStore()` → 셀렉터 패턴 | -| 5 | `src/layouts/AuthenticatedLayout.tsx` | 수정 | menuStore/themeStore 전체 구독 → 셀렉터 | -| 6 | `src/components/production/WorkerScreen/index.tsx` | 수정 | `useMenuStore()` → `useSidebarCollapsed()` | -| 7 | `src/components/layout/CommandMenuSearch.tsx` | 수정 | `useMenuStore()` → `useMenuItems()` | -| 8 | `src/components/ThemeSelect.tsx` | 수정 | `useThemeStore()` → `useTheme()` + `useSetTheme()` | - ---- - -## 참고: Zustand 셀렉터가 중요한 이유 - -``` -// ❌ 전체 구독 — menuItems 변경 시 sidebarCollapsed만 쓰는 컴포넌트도 리렌더 -const { sidebarCollapsed } = useMenuStore(); - -// ✅ 셀렉터 — sidebarCollapsed 변경 시에만 리렌더 -const sidebarCollapsed = useMenuStore((state) => state.sidebarCollapsed); -// 또는 -const sidebarCollapsed = useSidebarCollapsed(); -``` - -Zustand는 `Object.is`로 반환값을 비교. 셀렉터가 원시값(string, boolean, number)을 반환하면 참조 비교로 정확히 변경 감지. -객체를 반환하는 셀렉터(예: `usePageColumnSettings`)는 같은 참조를 반환하므로 해당 pageId의 설정이 변경될 때만 리렌더. diff --git a/claudedocs/architecture/[IMPL-2026-03-06] lazy-snapshot-system.md b/claudedocs/architecture/[IMPL-2026-03-06] lazy-snapshot-system.md deleted file mode 100644 index e792e398..00000000 --- a/claudedocs/architecture/[IMPL-2026-03-06] lazy-snapshot-system.md +++ /dev/null @@ -1,103 +0,0 @@ -# 문서스냅샷 시스템 (Lazy Snapshot) - -> **작업일**: 2026-03-06 ~ 03-07 -> **상태**: ✅ 완료 -> **커밋**: 31f523c8, a1fb0d4f, 8250eaf2, 72a2a3e9, 04f2a8a7 - ---- - -## 개요 - -문서 저장/조회 시 `rendered_html` 스냅샷을 자동 캡처하여 백엔드에 전송하는 시스템. -MNG 측에서 문서 인쇄 시 스냅샷 기반 렌더링에 활용. - ---- - -## 아키텍처 - -``` -[문서 저장 시] - 컴포넌트 → contentWrapperRef.innerHTML 캡처 - → API 요청에 rendered_html 파라미터 포함 → 백엔드 저장 - -[문서 조회 시 — Lazy Snapshot] - rendered_html === NULL 감지 - → 500ms 대기 (렌더링 완료 대기) - → innerHTML 캡처 - → 백그라운드 PATCH 전송 (비차단) -``` - ---- - -## 1. 수동 캡처 (저장 시) - -문서 저장 시 DOM에서 `innerHTML`을 읽어 `rendered_html` 파라미터로 함께 전송. - -- [x] 검사성적서 (InspectionReportModal) — `contentWrapperRef.innerHTML` -- [x] 작업일지 (WorkLogModal) — `contentWrapperRef.innerHTML` -- [x] 수입검사 (ImportInspectionInputModal) — 오프스크린 렌더링 방식 - -### 주요 파일 -- `src/components/production/WorkOrders/documents/InspectionReportModal.tsx` -- `src/components/production/WorkerScreen/WorkLogModal.tsx` -- `src/components/material/ReceivingManagement/ImportInspectionInputModal.tsx` - ---- - -## 2. Lazy Snapshot (조회 시 자동 캡처) - -`rendered_html`이 NULL인 기존 문서를 조회할 때 자동으로 스냅샷을 캡처하여 백그라운드 저장. - -### 동작 흐름 -1. 문서 조회 API 응답에서 `snapshot_document_id` 확인 -2. `rendered_html === NULL` → Lazy Snapshot 트리거 -3. 500ms 지연 (콘텐츠 렌더링 완료 대기) -4. `contentWrapperRef.innerHTML` 캡처 -5. `patchDocumentSnapshot()` 서버 액션으로 백그라운드 PATCH - -### 특성 -- **비차단(non-blocking)**: UI에 영향 없이 백그라운드 처리 -- **1회성**: 스냅샷 저장 후 재조회 시 캡처하지 않음 -- **readOnly 자동 캡처 제거**: 불필요한 PUT 요청 방지 - -### 적용 대상 -| 문서 | 수동 캡처 | Lazy Snapshot | -|------|-----------|---------------| -| 검사성적서 | ✅ | ✅ | -| 작업일지 | ✅ | ✅ | -| 수입검사 | ✅ (오프스크린) | — | -| 제품검사 요청서 | ✅ | ✅ | - ---- - -## 3. 오프스크린 렌더링 유틸리티 - -폼 HTML이 아닌 실제 문서 렌더링 결과를 캡처하기 위한 유틸리티. - -```typescript -// src/lib/utils/capture-rendered-html.tsx -// 오프스크린 DOM에 문서 컴포넌트를 렌더링하여 innerHTML 추출 -``` - -- [x] 수입검사 모달에서 활용 (폼 캡처 → 문서 캡처 전환) -- [x] DocumentViewer 스냅샷 렌더링 지원 - -### 주요 파일 -- `src/lib/utils/capture-rendered-html.tsx` (신규) -- `src/components/document-system/viewer/DocumentViewer.tsx` - ---- - -## 4. 서버 액션 - -```typescript -// patchDocumentSnapshot — 백그라운드 PATCH -export async function patchDocumentSnapshot( - documentId: string, - rendered_html: string -): Promise<{ success: boolean }>; -``` - -### 주요 파일 -- `src/components/production/WorkOrders/actions.ts` — `patchDocumentSnapshot` -- `src/components/quality/InspectionManagement/fqcActions.ts` — `patchDocumentSnapshot` diff --git a/claudedocs/architecture/[IMPL] IntegratedDetailTemplate-checklist.md b/claudedocs/architecture/[IMPL] IntegratedDetailTemplate-checklist.md deleted file mode 100644 index eefc5227..00000000 --- a/claudedocs/architecture/[IMPL] IntegratedDetailTemplate-checklist.md +++ /dev/null @@ -1,254 +0,0 @@ -# IntegratedDetailTemplate 마이그레이션 체크리스트 - -> 최종 수정: 2026-01-21 -> 브랜치: `feature/universal-detail-component` - ---- - -## 📊 전체 진행 현황 - -| 단계 | 내용 | 상태 | 대상 | -|------|------|------|------| -| **Phase 1-5** | V2 URL 패턴 통합 | ✅ 완료 | 37개 | -| **Phase 6** | 폼 템플릿 공통화 | ✅ 완료 | 41개 | - -### 통계 요약 - -| 구분 | 개수 | -|------|------| -| ✅ V2 URL 패턴 완료 | 37개 | -| ✅ IntegratedDetailTemplate 적용 완료 | 41개 | -| ❌ 제외 (특수 레이아웃) | 10개 | -| ⚪ 불필요 (View only 등) | 8개 | - ---- - -## 📌 V2 URL 패턴이란? - -``` -기존: /[id] (조회) + /[id]/edit (수정) → 별도 페이지 -V2: /[id]?mode=view (조회) + /[id]?mode=edit (수정) → 단일 페이지 -``` - -**핵심**: `searchParams.get('mode')` 로 view/edit 분기 - ---- - -## 🎯 마이그레이션 목표 - -- **타이틀/버튼 영역** (목록, 상세, 취소, 수정) 공통화 -- **반응형 입력 필드** 통합 -- **특수 기능** (테이블, 모달, 문서 미리보기 등)은 `renderView`/`renderForm`으로 유지 -- **한 파일 수정으로 전체 페이지 일괄 적용** 가능 - ---- - -## 🔧 마이그레이션 패턴 가이드 - -### Pattern 1: Config 기반 템플릿 - -```typescript -// 1. config 파일 생성 -export const xxxConfig: DetailConfig = { - title: '페이지 타이틀', - description: '설명', - icon: IconComponent, - basePath: '/path/to/list', - fields: [], // renderView/renderForm 사용 시 빈 배열 - gridColumns: 2, - actions: { - showBack: true, - showDelete: true, - showEdit: true, - showSave: true, // false로 설정하면 기본 저장 버튼 숨김 - submitLabel: '저장', - cancelLabel: '취소', - }, -}; - -// 2. 컴포넌트에서 IntegratedDetailTemplate 사용 - - onDelete={handleDelete} // Promise<{ success: boolean; error?: string }> - headerActions={customHeaderActions} // 커스텀 버튼 - renderView={() => renderContent()} - renderForm={() => renderContent()} -/> -``` - -### Pattern 2: View/Edit 컴포넌트 분리 - -```tsx -// View와 Edit가 완전히 다른 구현인 경우 -const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view'; - -if (mode === 'edit') { - return ; -} -return ; -``` - -### Pattern 3: 커스텀 버튼이 필요한 경우 - -```tsx -// config에서 showSave: false 설정 -// headerActions prop으로 커스텀 버튼 전달 - - - - - } -/> -``` - ---- - -## ✅ Phase 6 적용 완료 (41개) - -| No | 카테고리 | 컴포넌트 | 파일 | 특이사항 | -|----|---------|---------|------|----------| -| 1 | 건설 | 협력업체 | PartnerForm.tsx | - | -| 2 | 건설 | 시공관리 | ConstructionDetailClient.tsx | - | -| 3 | 건설 | 기성관리 | ProgressBillingDetailForm.tsx | - | -| 4 | 건설 | 발주관리 | OrderDetailForm.tsx | - | -| 5 | 건설 | 계약관리 | ContractDetailForm.tsx | - | -| 6 | 건설 | 인수인계보고서 | HandoverReportDetailForm.tsx | - | -| 7 | 건설 | 견적관리 | EstimateDetailForm.tsx | - | -| 8 | 건설 | 현장브리핑 | SiteBriefingForm.tsx | - | -| 9 | 건설 | 이슈관리 | IssueDetailForm.tsx | - | -| 10 | 건설 | 입찰관리 | BiddingDetailForm.tsx | - | -| 11 | 건설 | 구조검토 | StructureReviewDetailForm.tsx | view/edit/new 모드, 파일 드래그앤드롭 | -| 12 | 건설 | 현장관리 | SiteDetailForm.tsx | 다음 우편번호 API, 파일 드래그앤드롭 | -| 13 | 건설 | 품목관리 | ItemDetailClient.tsx | view/edit/new 모드, 동적 발주 항목 리스트 | -| 14 | 영업 | 견적관리(V2) | QuoteRegistrationV2.tsx | hideHeader prop, 자동견적/푸터바 유지 | -| 15 | 영업 | 고객관리(V2) | ClientDetailClientV2.tsx | - | -| 16 | 영업 | 수주관리 | OrderSalesDetailView/Edit.tsx | 문서 모달, 상태별 버튼, 확정/취소 다이얼로그 | -| 17 | 회계 | 청구관리 | BillDetail.tsx | - | -| 18 | 회계 | 매입관리 | PurchaseDetail.tsx | - | -| 19 | 회계 | 매출관리 | SalesDetail.tsx | - | -| 20 | 회계 | 거래처관리 | VendorDetail.tsx | - | -| 21 | 회계 | 입금관리(V2) | DepositDetailClientV2.tsx | - | -| 22 | 회계 | 출금관리(V2) | WithdrawalDetailClientV2.tsx | - | -| 23 | 회계 | 악성채권 | BadDebtDetail.tsx | 저장 확인 다이얼로그, 파일 업로드/다운로드 | -| 24 | 회계 | 거래처원장 | VendorLedgerDetail.tsx | 기간선택, PDF 다운로드, 판매/수금 테이블 | -| 25 | 생산 | 작업지시 | WorkOrderDetail.tsx | 상태변경버튼, 작업일지 모달 유지 | -| 26 | 품질 | 검수관리 | InspectionDetail.tsx | 성적서 버튼 | -| 27 | 출고 | 출하관리 | ShipmentDetail.tsx | 문서 미리보기 모달, 조건부 수정/삭제 | -| 28 | 자재 | 입고관리 | ReceivingDetail.tsx | 입고증/입고처리/성공 다이얼로그, 상태별 버튼 | -| 29 | 자재 | 재고현황 | StockStatusDetail.tsx | LOT별 상세 재고 테이블, FIFO 권장 메시지 | -| 30 | 기준정보 | 단가관리(V2) | PricingDetailClientV2.tsx | - | -| 31 | 기준정보 | 노무관리(V2) | LaborDetailClientV2.tsx | - | -| 32 | 설정 | 팝업관리(V2) | PopupDetailClientV2.tsx | - | -| 33 | 설정 | 계정관리 | accounts/[id]/page.tsx | - | -| 34 | 설정 | 공정관리 | process-management/[id]/page.tsx | - | -| 35 | 설정 | 게시판관리 | board-management/[id]/page.tsx | - | -| 36 | 설정 | 권한관리 | PermissionDetail.tsx | 인라인 수정, 메뉴별 권한 테이블, 자동 저장 | -| 37 | 인사 | 명함관리 | card-management/[id]/page.tsx | - | -| 38 | 인사 | 직원관리 | EmployeeDetail.tsx | 기본정보/인사정보/사용자정보 카드 | -| 39 | 고객센터 | 문의관리 | InquiryDetail.tsx | 댓글 CRUD, 작성자/상태별 버튼 표시 | -| 40 | 고객센터 | 이벤트관리 | EventDetail.tsx | view 모드만 | -| 41 | 고객센터 | 공지관리 | NoticeDetail.tsx | view 모드만, 이미지/첨부파일 | - ---- - -## 📋 등록/수정 페이지 마이그레이션 (Phase 1-8) - -### Phase 1 - 기안함 -- [x] DocumentCreate (기안함 등록/수정) - - 파일: `src/components/approval/DocumentCreate/index.tsx` - - 특이사항: 커스텀 headerActions (미리보기, 삭제, 상신, 임시저장) - -### Phase 2 - 생산관리 -- [x] WorkOrderCreate/Edit (작업지시 등록/수정) - - 파일: `src/components/production/WorkOrders/WorkOrderCreate.tsx` - -### Phase 3 - 출고관리 -- [x] ShipmentCreate/Edit (출하 등록/수정) - - 파일: `src/components/outbound/ShipmentManagement/ShipmentCreate.tsx` - -### Phase 4 - HR -- [x] EmployeeForm (사원 등록/수정/상세) - - 파일: `src/components/hr/EmployeeManagement/EmployeeForm.tsx` - - 특이사항: "항목 설정" 버튼, 복잡한 섹션 구조 - -### Phase 5 - 게시판 -- [x] BoardForm (게시판 글쓰기/수정) - - 파일: `src/components/board/BoardForm/index.tsx` - -### Phase 6 - 고객센터 -- [x] InquiryForm (문의 등록/수정) - - 파일: `src/components/customer-center/InquiryManagement/InquiryForm.tsx` - -### Phase 7 - 기준정보 -- [x] ProcessForm (공정 등록/수정) - - 파일: `src/components/process-management/ProcessForm.tsx` - -### Phase 8 - 자재/품질 -- [x] InspectionCreate - 자재 (수입검사 등록) -- [x] InspectionCreate - 품질 (품질검사 등록) - ---- - -## ❌ 마이그레이션 제외 (특수 레이아웃) - -| 페이지 | 경로 | 사유 | -|--------|------|------| -| CEO 대시보드 | - | 대시보드 (특수 레이아웃) | -| 생산 대시보드 | - | 대시보드 (특수 레이아웃) | -| 작업자 화면 | - | 특수 UI | -| 설정 페이지들 | - | 트리 구조, 특수 레이아웃 | -| 부서 관리 | - | 트리 구조 | -| 일일보고서 | - | 특수 레이아웃 | -| 미수금현황 | - | 특수 레이아웃 | -| 종합분석 | - | 특수 레이아웃 | -| 현장종합현황 | `/construction/project/management/[id]` | 칸반 보드 | -| 권한관리 | `/settings/permissions/[id]` | Matrix UI | - ---- - -## 📚 Config 파일 위치 참조 - -| 컴포넌트 | Config 파일 | -|---------|------------| -| 출하관리 | shipmentConfig.ts | -| 작업지시 | workOrderConfig.ts | -| 검수관리 | inspectionConfig.ts | -| 견적관리(V2) | quoteConfig.ts | -| 수주관리 | orderSalesConfig.ts | -| 입고관리 | receivingConfig.ts | -| 재고현황 | stockStatusConfig.ts | -| 악성채권 | badDebtConfig.ts | -| 거래처원장 | vendorLedgerConfig.ts | -| 구조검토 | structureReviewConfig.ts | -| 현장관리 | siteConfig.ts | -| 품목관리 | itemConfig.ts | -| 문의관리 | inquiryConfig.ts | -| 이벤트관리 | eventConfig.ts | -| 공지관리 | noticeConfig.ts | -| 직원관리 | employeeConfig.ts | -| 권한관리 | permissionConfig.ts | - ---- - -## 📝 변경 이력 - -
-전체 변경 이력 보기 - -| 날짜 | 내용 | -|------|------| -| 2026-01-17 | 체크리스트 초기 작성 | -| 2026-01-19 | Phase 1-5 V2 URL 패턴 마이그레이션 완료 (37개) | -| 2026-01-20 | Phase 6 폼 템플릿 공통화 마이그레이션 완료 (41개) | -| 2026-01-20 | 기안함, 작업지시, 출하, 사원, 게시판, 문의, 공정, 검사 마이그레이션 완료 | -| 2026-01-21 | 문서 통합 (중복 3개 파일 → 1개) | - -
diff --git a/claudedocs/architecture/[PLAN-2025-02-10] frontend-improvement-roadmap.md b/claudedocs/architecture/[PLAN-2025-02-10] frontend-improvement-roadmap.md deleted file mode 100644 index bb98546b..00000000 --- a/claudedocs/architecture/[PLAN-2025-02-10] frontend-improvement-roadmap.md +++ /dev/null @@ -1,74 +0,0 @@ -# SAM ERP 프론트엔드 개선 로드맵 - -> 작성일: 2025-02-10 -> 분석 기준: src/ 전체 (500+ 파일, ~163K줄) - ---- - -## Phase A: 즉시 개선 — ✅ 완료 - -| # | 항목 | 상태 | 비고 | -|---|------|------|------| -| A-1 | `` → `next/image` 전환 | ✅ **전환 불필요 결정** | 폐쇄형 ERP, 전량 외부 동적 이미지, blob URL 비호환 (`_index.md` 참조) | -| A-2 | DataTable 렌더링 최적화 | ⏳ 대기 | TableRow memo + useCallback | - ---- - -## Phase B: 단기 개선 — ✅ 완료 - -| # | 항목 | 상태 | 비고 | -|---|------|------|------| -| B-1 | `next/dynamic` 코드 스플리팅 | ✅ **완료** | 대시보드 4개 + xlsx 동적 로드, ~850KB 절감 (`_index.md` 참조) | -| B-2 | API 병렬 호출 (`Promise.all`) | ✅ **완료** | | -| B-3 | `store/` vs `stores/` 통합 | ✅ **완료** | | - ---- - -## Phase C: 중기 개선 — ✅ 완료 - -| # | 항목 | 상태 | 비고 | -|---|------|------|------| -| C-1 | 테이블 가상화 (react-window) | ✅ **보류 결정** | 페이지네이션 사용 중, YAGNI (`_index.md` 참조) | -| C-2 | SWR / React Query | ✅ **보류 결정** | Zustand 캐싱 충족 (`_index.md` 참조) | -| C-3 | Action 팩토리 패턴 확대 | ✅ **규칙 확정** | 신규 CRUD만 팩토리 사용 (`_index.md` 참조) | -| C-4 | V1/V2 컴포넌트 정리 | ✅ **완료** | V2 최종본 확정, V1 삭제, 접미사 제거 | - ---- - -## Phase D: 장기 개선 (필요 시) - -| # | 항목 | 상태 | -|---|------|------| -| D-1 | God 컴포넌트 분리 (5개, 1200~2700줄) | ⏳ 대기 | -| D-2 | `as` 타입 캐스트 점진적 제거 (926건) | ✅ **보류 결정** | 실제 ~200건만 actionable, 신규 코드에서 제네릭 활용 (2026-02-11) | -| D-3 | `@deprecated` 함수 정리 (13파일) | ✅ **즉시 삭제분 완료** | uploadFile/deleteFile/getSiteNames/deprecated props 삭제 (2026-02-11) | -| D-4 | Molecules 레이어 활성화 | ✅ **보류 결정** | 사용률 ~0%, organisms/templates로 충분 (2026-02-11) | -| D-5 | 모달 컴포넌트 통합 | ✅ **완료** | InspectionPreviewModal → DocumentViewer 전환 (2026-02-11) | -| D-6 | 기타 (TODO 102건, strictMode 등) | ⏳ 대기 | - ---- - -## 이전 리팩토링 완료 항목 (참고) - -| 항목 | 상태 | 날짜 | -|------|------|------| -| Phase 1: 공통 훅 추출 (executeServerAction 등) | ✅ 완료 | 이전 세션 | -| 중복 코드 공통화 (buildApiUrl 전체 43개 actions.ts 마이그레이션) | ✅ 완료 | 2026-02-12 | -| executePaginatedAction 전체 마이그레이션 (14개 actions.ts, ~220줄 감소) | ✅ 완료 | 2026-02-12 | -| Phase 3: 공용 유틸 추출 (PaginatedApiResponse 등) | ✅ 완료 | 이전 세션 | -| Phase 4: SearchableSelectionModal 공통화 | ✅ 완료 | 이전 세션 | -| Phase 5: any 21건 + memo 3개 정리 | ✅ 완료 | 이전 세션 | -| console.log 524건 → 22건 정리 | ✅ 완료 | 2025-02-10 | -| TODO 주석 정리 (login route) | ✅ 완료 | 2025-02-10 | -| SSR 가드 추가 (ThemeContext, ApiErrorContext, useDetailPageState) | ✅ 완료 | 2025-02-10 | -| 커스텀 훅 불필요 'use client' 15개 제거 | ✅ 완료 | 2025-02-10 | -| formatDate 이름 충돌 해소 → formatCalendarDate | ✅ 완료 | 2025-02-10 | - ---- - -## 우선순위 요약 - -``` -Phase A~C: ✅ 전체 완료/결정 완료 (A-2 DataTable 최적화만 대기) -Phase D: 남은 작업 → A-2, D-1, D-6 -``` diff --git a/claudedocs/architecture/[PLAN-2025-12-29] dynamic-menu-refresh.md b/claudedocs/architecture/[PLAN-2025-12-29] dynamic-menu-refresh.md deleted file mode 100644 index 0df29fba..00000000 --- a/claudedocs/architecture/[PLAN-2025-12-29] dynamic-menu-refresh.md +++ /dev/null @@ -1,346 +0,0 @@ -# 동적 메뉴 갱신 시스템 - -## 개요 - -관리자가 게시판/메뉴를 추가하면 사용자가 **재로그인 없이** 즉시 메뉴를 갱신받을 수 있는 시스템 구현. - -## 현재 문제점 - -``` -현재 흐름: - 로그인 → API 응답에서 메뉴 수신 → localStorage.user.menu 저장 → 세션 종료까지 고정 - -문제: - - 관리자가 게시판 추가해도 사용자는 재로그인 전까지 새 메뉴 안 보임 - - 메뉴 전용 갱신 API 없음 - - 실시간 알림 메커니즘 없음 -``` - -## 데이터 흐름 (현재) - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 로그인 시 │ -├─────────────────────────────────────────────────────────────┤ -│ POST /api/v1/login │ -│ ↓ │ -│ 응답: { user, tenant, roles, menus } │ -│ ↓ │ -│ transformApiMenusToMenuItems(menus) │ -│ ↓ │ -│ localStorage.setItem('user', { ...userData, menu }) │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 페이지 로드 시 │ -├─────────────────────────────────────────────────────────────┤ -│ AuthenticatedLayout.tsx │ -│ ↓ │ -│ localStorage.getItem('user') → userData.menu │ -│ ↓ │ -│ deserializeMenuItems(userData.menu) │ -│ ↓ │ -│ menuStore.setMenuItems(deserializedMenus) │ -│ ↓ │ -│ Sidebar 컴포넌트 렌더링 │ -└─────────────────────────────────────────────────────────────┘ -``` - -## 관련 파일 - -| 파일 | 역할 | -|------|------| -| `src/store/menuStore.ts` | Zustand 메뉴 상태 관리 | -| `src/lib/utils/menuTransform.ts` | API 메뉴 → UI 메뉴 변환 | -| `src/lib/utils/menuRefresh.ts` | 메뉴 갱신 유틸리티 (해시 비교, localStorage/Zustand 동시 업데이트) | -| `src/hooks/useMenuPolling.ts` | 메뉴 폴링 훅 (30초 간격, 탭 가시성, 세션 만료 처리) | -| `src/layouts/AuthenticatedLayout.tsx` | 메뉴 로드 및 스토어 설정 | -| `src/components/layout/Sidebar.tsx` | 메뉴 렌더링 | -| `src/contexts/AuthContext.tsx` | 사용자 인증 컨텍스트 | - ---- - -## 구현 계획 - -### 1단계: 폴링 방식 (현재 구현 목표) - -**방식**: 30초마다 메뉴 API 호출하여 변경사항 확인 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 폴링 방식 흐름 │ -├─────────────────────────────────────────────────────────────┤ -│ │ -│ [30초마다] │ -│ ↓ │ -│ GET /api/menus (메뉴 전용 API 필요) │ -│ ↓ │ -│ 현재 메뉴와 비교 (해시 또는 버전 비교) │ -│ ↓ │ -│ 변경 있으면 → refreshMenus() 호출 │ -│ ↓ │ -│ localStorage.user.menu 업데이트 │ -│ menuStore.setMenuItems() 호출 │ -│ ↓ │ -│ UI 즉시 반영 │ -│ │ -└─────────────────────────────────────────────────────────────┘ -``` - -**장점**: -- 구현 단순 -- 백엔드 수정 최소화 (메뉴 조회 API만 추가) -- 기존 인프라 그대로 사용 - -**단점**: -- 최대 30초 지연 -- 불필요한 API 호출 발생 - -#### 프론트엔드 구현 사항 - -1. **메뉴 갱신 유틸리티 함수** (`src/lib/utils/menuRefresh.ts`) -2. **폴링 훅** (`src/hooks/useMenuPolling.ts`) -3. **AuthenticatedLayout에 훅 적용** - -#### 백엔드 요청 사항 - -| 항목 | 설명 | -|------|------| -| **엔드포인트** | `GET /api/v1/menus` | -| **인증** | Bearer 토큰 필요 | -| **응답** | 현재 사용자의 메뉴 목록 (로그인 응답의 menus와 동일 구조) | -| **선택사항** | `menu_version` 또는 `menu_hash` 필드 추가 (변경 감지 최적화용) | - ---- - -### 2단계: SSE 고도화 (향후 계획) - -**방식**: 서버에서 메뉴 변경 시 SSE로 클라이언트에 푸시 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 백엔드 (Laravel) │ -├─────────────────────────────────────────────────────────────┤ -│ 1. 관리자가 메뉴 추가 → DB 저장 │ -│ 2. MenuUpdatedEvent 발생 │ -│ 3. 해당 테넌트의 SSE 채널로 푸시 │ -└─────────────────────────────────────────────────────────────┘ - ↓ SSE -┌─────────────────────────────────────────────────────────────┐ -│ 프론트엔드 (Next.js) │ -├─────────────────────────────────────────────────────────────┤ -│ 1. EventSource로 SSE 연결 유지 │ -│ 2. 'menu-updated' 이벤트 수신 │ -│ 3. refreshMenus() 호출 → UI 즉시 갱신 │ -└─────────────────────────────────────────────────────────────┘ -``` - -**장점**: -- 실시간 갱신 (지연 없음) -- 효율적 (변경 시에만 통신) - -**단점**: -- 백엔드 SSE 인프라 구축 필요 -- 동시 접속자 관리 필요 -- 멀티테넌트 채널 분리 필요 - -#### 백엔드 요구사항 (SSE) - -| 항목 | 설명 | -|------|------| -| **SSE 엔드포인트** | `GET /api/v1/sse/menu-updates` | -| **인증** | Bearer 토큰 또는 쿼리 파라미터 | -| **이벤트 타입** | `menu-updated` | -| **채널 분리** | 테넌트별로 분리 필요 | -| **구현 옵션** | Laravel Broadcasting + Redis, 직접 구현 등 | - ---- - -## 구현 체크리스트 - -### 1단계: 폴링 방식 - -#### 프론트엔드 ✅ 구현 완료 (2025-12-29) -- [x] `src/lib/utils/menuRefresh.ts` 생성 - - [x] `refreshMenus()` 함수 구현 - - [x] `forceRefreshMenus()` 강제 갱신 함수 - - [x] localStorage + Zustand 동시 업데이트 - - [x] 해시 기반 변경 감지 -- [x] `src/hooks/useMenuPolling.ts` 생성 - - [x] 30초 간격 폴링 로직 - - [x] 탭 가시성 변경 시 자동 중지/재개 - - [x] pause/resume 기능 - - [x] 컴포넌트 언마운트 시 정리 -- [x] `src/app/api/menus/route.ts` 생성 (Next.js 프록시) - - [x] 백엔드 메뉴 API 프록시 - - [x] HttpOnly 쿠키 토큰 처리 - - [x] `{ data: [...] }` 응답 구조 처리 -- [x] `AuthenticatedLayout.tsx`에 훅 적용 -- [ ] 테스트: 관리자 메뉴 추가 → 30초 내 사용자 메뉴 갱신 확인 - -#### 백엔드 (이미 존재!) -- [x] `GET /api/v1/menus` API 존재 확인 ✅ -- [x] `MenuController::index` → `MenuService::index` (사용자 권한 기반 필터링) -- [x] 응답 구조: `{ data: [...] }` (ApiResponse::handle 표준) - -### 2단계: SSE 고도화 (향후) - -- [ ] 백엔드 SSE 인프라 구축 -- [ ] 프론트엔드 EventSource 훅 구현 -- [ ] 폴링 → SSE 전환 -- [ ] 폴백: SSE 연결 실패 시 폴링으로 대체 - ---- - -## 코드 스니펫 - -### refreshMenus 함수 - -```typescript -// src/lib/utils/menuRefresh.ts -import { transformApiMenusToMenuItems, deserializeMenuItems } from './menuTransform'; -import { useMenuStore } from '@/store/menuStore'; - -export async function refreshMenus(): Promise { - try { - const response = await fetch('/api/menus'); - if (!response.ok) return false; - - const { menus } = await response.json(); - const transformedMenus = transformApiMenusToMenuItems(menus); - - // 1. localStorage 업데이트 (새로고침 대응) - const userData = JSON.parse(localStorage.getItem('user') || '{}'); - userData.menu = transformedMenus; - localStorage.setItem('user', JSON.stringify(userData)); - - // 2. Zustand 스토어 업데이트 (UI 즉시 반영) - const { setMenuItems } = useMenuStore.getState(); - setMenuItems(deserializeMenuItems(transformedMenus)); - - console.log('[Menu] 메뉴 갱신 완료'); - return true; - } catch (error) { - console.error('[Menu] 메뉴 갱신 실패:', error); - return false; - } -} -``` - -### useMenuPolling 훅 - -```typescript -// src/hooks/useMenuPolling.ts -// 주요 기능: 30초 폴링, 탭 가시성 처리, 세션 만료 감지(3회 연속 401), 토큰 갱신 쿠키 감지 - -export function useMenuPolling(options: UseMenuPollingOptions = {}): UseMenuPollingReturn { - // ⚠️ 콜백 안정화 패턴 (2026-02-03 버그 수정) - // 부모 컴포넌트에서 인라인 콜백을 전달하면 매 렌더마다 새 참조가 생성되어 - // executeRefresh → useEffect 의존성이 변경 → setInterval이 매 렌더마다 리셋되는 버그 발생. - // 해결: 콜백을 ref로 저장하여 executeRefresh 의존성에서 제거. - const onMenuUpdatedRef = useRef(onMenuUpdated); - const onErrorRef = useRef(onError); - const onSessionExpiredRef = useRef(onSessionExpired); - - useEffect(() => { - onMenuUpdatedRef.current = onMenuUpdated; - onErrorRef.current = onError; - onSessionExpiredRef.current = onSessionExpired; - }); - - // executeRefresh 의존성: [stopPolling] 만 — 안정적 - const executeRefresh = useCallback(async () => { - // ref를 통해 최신 콜백 호출 - onMenuUpdatedRef.current?.(); - onSessionExpiredRef.current?.(); - onErrorRef.current?.(result.error); - }, [stopPolling]); -} -``` - -### Next.js API 프록시 - -```typescript -// src/app/api/menus/route.ts -import { NextRequest, NextResponse } from 'next/server'; - -export async function GET(request: NextRequest) { - const token = request.cookies.get('access_token')?.value; - - if (!token) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - - const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/v1/menus`, { - headers: { - 'Authorization': `Bearer ${token}`, - 'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '', - }, - }); - - const data = await response.json(); - return NextResponse.json(data); -} -``` - ---- - -## 참고 사항 - -### 메뉴 데이터 저장 위치 - -| 저장소 | 키 | 용도 | -|--------|-----|------| -| localStorage | `user.menu` | 새로고침 시 복구용 | -| Zustand | `menuStore.menuItems` | UI 렌더링용 | - -### 갱신 시 동기화 필수 - -```typescript -// 반드시 둘 다 업데이트! -localStorage.user.menu = newMenus; // 새로고침 대응 -menuStore.setMenuItems(newMenus); // UI 즉시 반영 -``` - ---- - -## 작성 정보 - -- **작성일**: 2025-12-29 -- **최종 수정**: 2026-02-03 -- **상태**: ✅ 1단계 구현 완료 + 폴링 버그 수정 -- **담당**: 프론트엔드 팀 -- **백엔드**: `GET /api/v1/menus` API 이미 존재 ✅ - ---- - -## 변경 이력 - -### 2026-02-03: 폴링 인터벌 리셋 버그 수정 - -**문제**: 메뉴 폴링이 실제로 실행되지 않아 백엔드에서 메뉴를 추가해도 재로그인 전까지 반영되지 않음. - -**원인**: `useMenuPolling` 훅의 `executeRefresh` 콜백이 매 렌더마다 새 참조를 생성하여 `setInterval`이 리셋됨. - -``` -AuthenticatedLayout에서 인라인 콜백 전달: - onMenuUpdated: () => { ... } ← 매 렌더마다 새 함수 - onSessionExpired: () => { ... } ← 매 렌더마다 새 함수 - ↓ - executeRefresh deps: [onMenuUpdated, onError, onSessionExpired, stopPolling] - ↓ 매 렌더마다 변경 - useEffect deps: [executeRefresh] → clearInterval → setInterval 재설정 - ↓ - 알림 폴링이 30초마다 state 업데이트 → 리렌더 → 메뉴 인터벌 리셋 - ↓ - 메뉴 폴링이 30초에 도달하지 못하고 영원히 미실행 -``` - -**수정**: 콜백을 `useRef`로 안정화하여 `executeRefresh` 의존성에서 제거. - -``` -수정 전: executeRefresh deps = [onMenuUpdated, onError, onSessionExpired, stopPolling] -수정 후: executeRefresh deps = [stopPolling] ← 안정적, 인터벌 리셋 없음 -``` - -**수정 파일**: `src/hooks/useMenuPolling.ts` \ No newline at end of file diff --git a/claudedocs/architecture/[PLAN-2026-01-16] layout-restructure.md b/claudedocs/architecture/[PLAN-2026-01-16] layout-restructure.md deleted file mode 100644 index 5e84fceb..00000000 --- a/claudedocs/architecture/[PLAN-2026-01-16] layout-restructure.md +++ /dev/null @@ -1,96 +0,0 @@ -# 레이아웃 구조 변경 계획 - -> **상태**: 📋 대기 (기능 검수 완료 후 진행) -> **작성일**: 2026-01-16 -> **적용 대상**: IntegratedListTemplateV2.tsx (55개 페이지 일괄 적용) - ---- - -## 현재 구조 - -``` -1. 타이틀 -2. 달력 / 버튼들 (등록 버튼 여기) -3. 통계 카드 -4. 검색창 (Card로 감싸짐) -5. 테이블 Card - └─ 탭 버튼들 / 필터 / 삭제 버튼 - └─ 테이블 -``` - ---- - -## 변경 후 구조 - -``` -1. 타이틀 -2. 달력 / 달력버튼 / 검색창 (한 줄) -3. 카드섹션 (한 줄, 줄넘김 없음) -4. [탭 버튼들] ─────────────── [등록] [CSV] 버튼들 ← Card 밖 -5. 테이블 Card - ├─ 총 N건 / 선택건 / 필터 - └─ 테이블 -``` - ---- - -## 시각화 - -``` -┌─ 페이지 ─────────────────────────────────────────────────┐ -│ 휴가관리 │ -│ 직원들의 휴가 현황을 관리합니다 │ -├──────────────────────────────────────────────────────────┤ -│ [📅 2025-12-01] ~ [📅 2025-12-31] [당월][전월] [🔍검색] │ -├──────────────────────────────────────────────────────────┤ -│ [승인대기 1명] [연차 4명] [경조사 0명] [사용률 4.3%] │ ← 카드 (줄넘김X) -├──────────────────────────────────────────────────────────┤ -│ [사용현황 4] [부여현황 2] [신청현황 3] [등록] [CSV] │ ← Card 밖 -├──────────────────────────────────────────────────────────┤ -│ ┌─ 테이블 Card ────────────────────────────────────────┐ │ -│ │ 총 55건 | 3개 선택됨 [필터1] [필터2] │ │ -│ ├──────────────────────────────────────────────────────┤ │ -│ │ □ | 번호 | 부서 | 이름 | ... │ │ -│ │ □ | 1 | 개발 | 홍길동 | ... │ │ -│ └──────────────────────────────────────────────────────┘ │ -└──────────────────────────────────────────────────────────┘ -``` - ---- - -## 주요 변경점 - -| 항목 | 현재 | 변경 후 | -|------|------|---------| -| 검색창 | Card로 감싸짐, 별도 영역 | 달력 옆 한 줄에 배치 | -| 카드섹션 | flex-wrap (줄넘김) | flex-nowrap + overflow-x-auto | -| 탭 버튼 | 테이블 Card 내부 | 테이블 Card 위 (밖) | -| 등록/액션 버튼 | 헤더 영역 | 탭 버튼 오른쪽 | -| 총 N건/선택건 | 탭과 같은 줄 | 테이블 Card 내부 첫 줄 | -| 필터 | 탭과 같은 줄 | 테이블 Card 내부 첫 줄 | - ---- - -## 수정 대상 파일 - -1. **IntegratedListTemplateV2.tsx** - 전체 레이아웃 구조 변경 -2. **UniversalListPage/index.tsx** - prop 전달 방식 조정 (필요시) - ---- - -## 체크리스트 - -- [ ] 검색창 위치 이동 (달력 옆) -- [ ] 카드섹션 줄넘김 방지 (flex-nowrap) -- [ ] 탭 버튼 테이블 Card 밖으로 이동 -- [ ] 등록/액션 버튼 탭 옆으로 이동 -- [ ] 총 N건/선택건/필터 테이블 Card 내부로 이동 -- [ ] PC/모바일 반응형 확인 -- [ ] 55개 페이지 일괄 테스트 - ---- - -## 진행 조건 - -✅ **기능 검수 완료 후 진행** -- 현재 화면과 비교 검수가 필요하므로 레이아웃 변경은 기능 검수 이후에 진행 diff --git a/claudedocs/architecture/[PLAN-2026-01-22] ui-component-abstraction.md b/claudedocs/architecture/[PLAN-2026-01-22] ui-component-abstraction.md deleted file mode 100644 index d9a8007e..00000000 --- a/claudedocs/architecture/[PLAN-2026-01-22] ui-component-abstraction.md +++ /dev/null @@ -1,546 +0,0 @@ -# UI 컴포넌트 공통화/추상화 계획 - -> **작성일**: 2026-01-22 -> **상태**: 🟢 진행 중 -> **범위**: 공통 UI 컴포넌트 추상화 및 스켈레톤 시스템 구축 - ---- - -## 결정 사항 (2026-01-22) - -| 항목 | 결정 | -|------|------| -| 스켈레톤 전환 범위 | **Option A: 전체 스켈레톤 전환** | -| 구현 우선순위 | **Phase 1 먼저** (ConfirmDialog → StatusBadge → EmptyState) | -| 확장 전략 | **옵션 기반 확장** - 새 패턴 발견 시 props 옵션으로 추가 | - ---- - -## 1. 현황 분석 요약 - -### 반복 패턴 현황 - -| 패턴 | 파일 수 | 발생 횟수 | 복잡도 | 우선순위 | -|------|---------|----------|--------|----------| -| 확인 다이얼로그 (삭제/저장) | 67개 | 170회 | 낮음 | 🔴 높음 | -| 상태 스타일 매핑 | 80개 | 다수 | 낮음 | 🔴 높음 | -| 날짜 범위 필터 | 55개 | 146회 | 중간 | 🟡 중간 | -| 빈 상태 UI | 70개 | 86회 | 낮음 | 🟡 중간 | -| 로딩 스피너/버튼 | 59개 | 120회 | 중간 | 🟡 중간 | -| 스켈레톤 UI | 4개 | 92회 | 높음 | 🔴 높음 | - -### 현재 스켈레톤 현황 - -**기존 구현:** -- `src/components/ui/skeleton.tsx` - 기본 스켈레톤 (단순 animate-pulse div) -- `IntegratedDetailTemplate/components/skeletons/` - 상세 페이지용 3종 - - `DetailFieldSkeleton.tsx` - - `DetailSectionSkeleton.tsx` - - `DetailGridSkeleton.tsx` -- `loading.tsx` - 4개 파일만 존재 (대부분 PageLoadingSpinner 사용) - -**문제점:** -1. 대부분 페이지에서 로딩 스피너 사용 (스켈레톤 미적용) -2. 리스트 페이지용 스켈레톤 없음 -3. 카드/대시보드용 스켈레톤 없음 -4. 페이지별 loading.tsx 부재 (4개만 존재) - ---- - -## 2. 공통화 대상 상세 - -### Phase 1: 핵심 공통 컴포넌트 (1주차) - -#### 1-1. ConfirmDialog 컴포넌트 - -**현재 (반복 코드):** -```tsx -// 67개 파일에서 거의 동일하게 반복 -const [showDeleteDialog, setShowDeleteDialog] = useState(false); - - - - - 삭제 확인 - - 정말 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다. - - - - 취소 - - {isLoading && } - 삭제 - - - - -``` - -**개선안:** -```tsx -// src/components/ui/confirm-dialog.tsx -interface ConfirmDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - title: string; - description: string; - confirmText?: string; - cancelText?: string; - variant?: 'default' | 'destructive' | 'warning'; - loading?: boolean; - onConfirm: () => void | Promise; -} - -// 사용 예시 - -``` - -**효과:** -- 코드량: ~30줄 → ~10줄 (70% 감소) -- 일관된 UX 보장 -- 로딩 상태 자동 처리 - ---- - -#### 1-2. StatusBadge 컴포넌트 + createStatusConfig 유틸 - -**현재 (반복 코드):** -```tsx -// 80개 파일에서 각각 정의 -// estimates/types.ts -export const STATUS_STYLES: Record = { - pending: 'bg-yellow-100 text-yellow-800', - inProgress: 'bg-blue-100 text-blue-800', - completed: 'bg-green-100 text-green-800', -}; -export const STATUS_LABELS: Record = { - pending: '대기', - inProgress: '진행중', - completed: '완료', -}; - -// site-management/types.ts (거의 동일) -export const SITE_STATUS_STYLES: Record = { ... }; -export const SITE_STATUS_LABELS: Record = { ... }; -``` - -**개선안:** -```tsx -// src/lib/utils/status-config.ts -export type StatusVariant = 'default' | 'success' | 'warning' | 'error' | 'info'; - -export interface StatusConfig { - value: T; - label: string; - variant: StatusVariant; - description?: string; -} - -export function createStatusConfig( - configs: StatusConfig[] -): { - options: { value: T; label: string }[]; - getLabel: (status: T) => string; - getVariant: (status: T) => StatusVariant; - isValid: (status: string) => status is T; -} - -// src/components/ui/status-badge.tsx -interface StatusBadgeProps { - status: T; - config: ReturnType>; - size?: 'sm' | 'md' | 'lg'; -} - -// 사용 예시 -// estimates/types.ts -export const estimateStatusConfig = createStatusConfig([ - { value: 'pending', label: '대기', variant: 'warning' }, - { value: 'inProgress', label: '진행중', variant: 'info' }, - { value: 'completed', label: '완료', variant: 'success' }, -]); - -// 컴포넌트에서 - -``` - -**효과:** -- 타입 안전성 강화 -- 일관된 색상 체계 -- options 자동 생성 (Select용) - ---- - -#### 1-3. EmptyState 컴포넌트 - -**현재 (반복 코드):** -```tsx -// 70개 파일에서 다양한 형태로 반복 -{data.length === 0 && ( -
- 데이터가 없습니다 -
-)} - -// 또는 - - - 등록된 항목이 없습니다 - - -``` - -**개선안:** -```tsx -// src/components/ui/empty-state.tsx -interface EmptyStateProps { - icon?: ReactNode; - title?: string; - description?: string; - action?: ReactNode; - variant?: 'default' | 'table' | 'card' | 'minimal'; -} - -// 사용 예시 -} - title="데이터가 없습니다" - description="새로운 항목을 등록하거나 검색 조건을 변경해보세요." - action={} -/> - -// 테이블 내 사용 - -``` - ---- - -### Phase 2: 스켈레톤 시스템 구축 (2주차) - -#### 2-1. 스켈레톤 컴포넌트 확장 - -**현재 문제:** -- 기본 Skeleton만 존재 (단순 div) -- 페이지 유형별 스켈레톤 부재 -- 대부분 PageLoadingSpinner 사용 (스켈레톤 미적용) - -**추가할 스켈레톤:** - -```tsx -// src/components/ui/skeletons/ -├── index.ts // 통합 export -├── ListPageSkeleton.tsx // 리스트 페이지용 -├── DetailPageSkeleton.tsx // 상세 페이지용 (기존 확장) -├── CardGridSkeleton.tsx // 카드 그리드용 -├── DashboardSkeleton.tsx // 대시보드용 -├── TableSkeleton.tsx // 테이블용 -├── FormSkeleton.tsx // 폼용 -└── ChartSkeleton.tsx // 차트용 -``` - -**1. ListPageSkeleton (리스트 페이지용)** -```tsx -interface ListPageSkeletonProps { - hasFilters?: boolean; - filterCount?: number; - hasDateRange?: boolean; - rowCount?: number; - columnCount?: number; - hasActions?: boolean; - hasPagination?: boolean; -} - -// 사용 예시 -export default function EstimateListLoading() { - return ( - - ); -} -``` - -**2. CardGridSkeleton (카드 그리드용)** -```tsx -interface CardGridSkeletonProps { - cardCount?: number; - cols?: 1 | 2 | 3 | 4; - cardHeight?: 'sm' | 'md' | 'lg'; - hasImage?: boolean; - hasFooter?: boolean; -} - -// 대시보드 카드, 칸반 보드 등에 사용 - -``` - -**3. TableSkeleton (테이블용)** -```tsx -interface TableSkeletonProps { - rowCount?: number; - columnCount?: number; - hasCheckbox?: boolean; - hasActions?: boolean; - columnWidths?: string[]; // ['w-12', 'w-32', 'flex-1', ...] -} - - -``` - ---- - -#### 2-2. loading.tsx 파일 생성 전략 - -**현재:** 4개 파일만 존재 -**목표:** 주요 페이지 경로에 맞춤형 loading.tsx 생성 - -**생성 대상 (우선순위):** - -| 경로 | 스켈레톤 타입 | 우선순위 | -|------|-------------|----------| -| `/construction/project/bidding/estimates` | ListPageSkeleton | 🔴 | -| `/construction/project/bidding` | ListPageSkeleton | 🔴 | -| `/construction/project/contract` | ListPageSkeleton | 🔴 | -| `/construction/order/*` | ListPageSkeleton | 🔴 | -| `/accounting/*` | ListPageSkeleton | 🟡 | -| `/hr/*` | ListPageSkeleton | 🟡 | -| `/settings/*` | ListPageSkeleton | 🟢 | -| `상세 페이지` | DetailPageSkeleton | 🟡 | -| `대시보드` | DashboardSkeleton | 🟡 | - ---- - -### Phase 3: 날짜 범위 필터 + 로딩 버튼 (3주차) - -#### 3-1. DateRangeFilter 컴포넌트 - -**현재 (반복 코드):** -```tsx -// 55개 파일에서 반복 -const [startDate, setStartDate] = useState(''); -const [endDate, setEndDate] = useState(''); - -
- - ~ - -
-``` - -**개선안:** -```tsx -// src/components/ui/date-range-filter.tsx -interface DateRangeFilterProps { - value: { start: string; end: string }; - onChange: (range: { start: string; end: string }) => void; - presets?: ('today' | 'week' | 'month' | 'quarter' | 'year')[]; - disabled?: boolean; -} - -// 사용 예시 - { - setStartDate(start); - setEndDate(end); - }} - presets={['today', 'week', 'month']} -/> -``` - ---- - -#### 3-2. LoadingButton 컴포넌트 - -**현재 (반복 코드):** -```tsx -// 59개 파일에서 반복 - -``` - -**개선안:** -```tsx -// src/components/ui/loading-button.tsx -interface LoadingButtonProps extends ButtonProps { - loading?: boolean; - loadingText?: string; - spinnerPosition?: 'left' | 'right'; -} - -// 사용 예시 - - 저장 - -``` - ---- - -## 3. 로딩 스피너 vs 스켈레톤 전략 - -### 논의 사항 - -**Option A: 전체 스켈레톤 전환** -- 장점: 더 나은 UX, 레이아웃 시프트 방지 -- 단점: 구현 비용 높음, 페이지별 커스텀 필요 - -**Option B: 하이브리드 (권장)** -- 페이지 로딩: 스켈레톤 (loading.tsx) -- 버튼/액션 로딩: 스피너 유지 (LoadingButton) -- 데이터 갱신: 스피너 유지 - -**Option C: 현행 유지** -- 대부분 스피너 유지 -- 특정 페이지만 스켈레톤 - -### 권장안: Option B (하이브리드) - -| 상황 | 로딩 UI | 이유 | -|------|---------|------| -| 페이지 초기 로딩 | 스켈레톤 | 레이아웃 힌트 제공 | -| 페이지 전환 | 스켈레톤 | Next.js loading.tsx 활용 | -| 버튼 클릭 (저장/삭제) | 스피너 | 짧은 작업, 버튼 내 피드백 | -| 데이터 갱신 (필터 변경) | 스피너 or 스켈레톤 | 상황에 따라 | -| 무한 스크롤 | 스켈레톤 | 추가 컨텐츠 힌트 | - ---- - -## 4. 구현 로드맵 - -### Week 1: 핵심 컴포넌트 -- [x] ConfirmDialog 컴포넌트 생성 ✅ (2026-01-22) - - `src/components/ui/confirm-dialog.tsx` - - variants: default, destructive, warning, success - - presets: DeleteConfirmDialog, SaveConfirmDialog, CancelConfirmDialog - - 내부/외부 로딩 상태 자동 관리 -- [x] StatusBadge + createStatusConfig 유틸 생성 ✅ (2026-01-22) - - `src/lib/utils/status-config.ts` - - `src/components/ui/status-badge.tsx` - - 프리셋: default, success, warning, destructive, info, muted, orange, purple - - 모드: badge (배경+텍스트), text (텍스트만) - - OPTIONS, LABELS, STYLES 자동 생성 -- [x] EmptyState 컴포넌트 생성 ✅ (2026-01-22) - - `src/components/ui/empty-state.tsx` - - variants: default, compact, large - - presets: noData, noResults, noItems, error - - TableEmptyState 추가 (테이블용) -- [x] 기존 코드 마이그레이션 (10개 파일 시범) ✅ (2026-01-22) - - PricingDetailClient.tsx - 삭제 확인 - - ItemManagementClient.tsx - 단일/일괄 삭제 - - LaborDetailClient.tsx - 삭제 확인 - - ConstructionDetailClient.tsx - 완료 확인 (warning) - - QuoteManagementClient.tsx - 단일/일괄 삭제 - - OrderDialogs.tsx - 저장/삭제/카테고리삭제 - - DepartmentManagement/index.tsx - 삭제 확인 - - VacationManagement/index.tsx - 승인/거절 확인 - - AccountDetail.tsx - 삭제 확인 - - ProcessListClient.tsx - 삭제 확인 - -### Week 2: 스켈레톤 시스템 -- [ ] ListPageSkeleton 컴포넌트 생성 -- [ ] TableSkeleton 컴포넌트 생성 -- [ ] CardGridSkeleton 컴포넌트 생성 -- [ ] 주요 경로 loading.tsx 생성 (construction/*) - -### Week 3: 필터 + 버튼 + 마이그레이션 -- [ ] DateRangeFilter 컴포넌트 생성 -- [ ] LoadingButton 컴포넌트 생성 -- [ ] 전체 코드 마이그레이션 - -### Week 4: 마무리 + QA -- [ ] 남은 마이그레이션 -- [ ] 문서화 -- [ ] 성능 테스트 - ---- - -## 5. 예상 효과 - -### 코드량 감소 -| 컴포넌트 | Before | After | 감소율 | -|---------|--------|-------|--------| -| ConfirmDialog | ~30줄 | ~10줄 | 67% | -| StatusBadge | ~20줄 | ~5줄 | 75% | -| EmptyState | ~10줄 | ~3줄 | 70% | -| DateRangeFilter | ~15줄 | ~5줄 | 67% | - -### 일관성 향상 -- 동일한 UX 패턴 적용 -- 디자인 시스템 강화 -- 유지보수 용이성 증가 - -### 성능 개선 -- 스켈레톤으로 인지 성능 향상 -- 레이아웃 시프트 감소 -- 사용자 이탈률 감소 - ---- - -## 6. 결정 필요 사항 - -### Q1: 스켈레톤 전환 범위 -- [ ] Option A: 전체 스켈레톤 전환 -- [ ] Option B: 하이브리드 (권장) -- [ ] Option C: 현행 유지 - -### Q2: 구현 우선순위 -- [ ] Phase 1 먼저 (ConfirmDialog, StatusBadge, EmptyState) -- [ ] Phase 2 먼저 (스켈레톤 시스템) -- [ ] 동시 진행 - -### Q3: 마이그레이션 범위 -- [ ] 전체 파일 한번에 -- [ ] 점진적 (신규/수정 파일만) -- [ ] 도메인별 순차 (construction → accounting → hr) - ---- - -## 7. 파일 구조 (최종) - -``` -src/components/ui/ -├── confirm-dialog.tsx # Phase 1 -├── status-badge.tsx # Phase 1 -├── empty-state.tsx # Phase 1 -├── date-range-filter.tsx # Phase 3 -├── loading-button.tsx # Phase 3 -├── skeleton.tsx # 기존 -└── skeletons/ # Phase 2 - ├── index.ts - ├── ListPageSkeleton.tsx - ├── DetailPageSkeleton.tsx - ├── CardGridSkeleton.tsx - ├── DashboardSkeleton.tsx - ├── TableSkeleton.tsx - ├── FormSkeleton.tsx - └── ChartSkeleton.tsx - -src/lib/utils/ -└── status-config.ts # Phase 1 -``` - ---- - -**다음 단계**: 위 결정 사항에 대한 의견 확정 후 구현 시작 diff --git a/claudedocs/architecture/[PLAN-2026-02-06] multi-tenancy-optimization-roadmap.md b/claudedocs/architecture/[PLAN-2026-02-06] multi-tenancy-optimization-roadmap.md deleted file mode 100644 index e6c9c41b..00000000 --- a/claudedocs/architecture/[PLAN-2026-02-06] multi-tenancy-optimization-roadmap.md +++ /dev/null @@ -1,666 +0,0 @@ -# 멀티테넌시 공통화 및 최적화 로드맵 - -**작성일**: 2026-02-06 -**목적**: 전체 프로젝트 멀티테넌시 준비 상태 점검 및 공통화/최적화 계획 수립 -**이전 문서**: `[REF-2025-11-19] multi-tenancy-implementation.md` (Phase 1-2 완료) - ---- - -## 현재 상태 요약 (2026-02-06 기준) - -### 완료된 항목 (이전 로드맵 Phase 1-2) - -| 항목 | 상태 | 파일 | -|------|------|------| -| User 타입에 Tenant 객체 포함 | ✅ 완료 | `src/contexts/AuthContext.tsx` | -| Tenant 인터페이스 정의 (id, company_name 등) | ✅ 완료 | `src/contexts/AuthContext.tsx` | -| TenantAwareCache 유틸리티 | ✅ 완료 | `src/lib/cache/TenantAwareCache.ts` | -| 테넌트 전환 감지 + 캐시 클리어 | ✅ 완료 | `src/contexts/AuthContext.tsx` | -| masterDataStore 테넌트 스코프 캐시 키 | ✅ 완료 | `src/stores/masterDataStore.ts` | -| sessionStorage/localStorage 테넌트 격리 | ✅ 완료 | `mes-{tenantId}-{key}` 패턴 | - -### 미완료 / 개선 필요 항목 - -| 영역 | 우선순위 | 현재 상태 | -|------|----------|-----------| -| API 프록시에 테넌트 컨텍스트 전달 | 🔴 | X-Tenant-ID 헤더 없음 | -| Server Actions 테넌트 인식 | 🔴 | 70+ actions.ts에 테넌트 미포함 | -| 포매터/유틸리티 다국어/다통화 | 🔴 | 한국어 하드코딩 | -| 브랜딩 동적화 (로고, 앱이름) | 🟡 | "SAM", sam-logo.png 하드코딩 | -| 상수/공휴일 외부화 | 🟡 | 한국 공휴일 하드코딩 | -| localStorage 직접 사용 잔재 | 🟡 | TenantAwareCache 미사용 곳 존재 | -| tenantId 타입 불일치 | 🟡 | string vs number 혼재 | -| 테넌트 라우팅 | 🟢 | 현재 없음 (필요 시 추가) | -| TenantContext Provider | 🟢 | 테넌트 설정 전용 Context 없음 | - ---- - -## 작업 영역 구분: 프론트 단독 vs 백엔드 협의 - -### 선행 확인 사항 - -> **핵심 질문**: "백엔드가 이미 JWT 토큰 안의 tenant_id로 데이터를 필터링하고 있는가?" -> -> - **Yes** → 프론트에서 별도 X-Tenant-ID 안 보내도 됨. Phase 1은 불필요하고 프론트 단독 Phase부터 진행 -> - **No** → 백엔드도 같이 수정 필요. Phase 1이 최우선 - -### 프론트 단독 가능 (백엔드 수정 불필요) - -| Phase | 작업 | 이유 | -|-------|------|------| -| **3** | 포매터 다국어/다통화 전환 | `formatAmount()`, `formatDate()` 등 프론트 유틸리티 내부 수정. 기본값을 한국어로 유지하면 하위 호환 | -| **6** | localStorage 잔재 정리 + tenantId 타입 통일 | 프론트 코드 정리. TenantAwareCache 미사용 곳 마이그레이션, `string` → `number` 통일 | -| **8** | 테넌트 라우팅 (필요 시) | Next.js App Router 구조 변경. 순수 프론트 라우팅 | - -> **즉시 착수 가능**: 백엔드 협의 결과를 기다리지 않고 바로 시작할 수 있음 - -### 백엔드 협의 필요 (프론트 + 백엔드 동시 수정) - -| Phase | 작업 | 백엔드 필요 이유 | -|-------|------|-----------------| -| **1** | API 테넌트 컨텍스트 주입 | 프론트에서 `X-Tenant-ID` 헤더를 보내도, **백엔드가 읽고 필터링**해줘야 의미 있음. 안 읽으면 보내봤자 무용지물 | -| **2** | Server Actions 마이그레이션 | Phase 1에 종속. 백엔드가 헤더 or URL로 테넌트를 구분 안 하면 프론트만 바꿔도 소용없음 | -| **4** | 브랜딩 동적화 | 테넌트별 로고/앱이름을 **어디서 가져오나?** → 백엔드 API 필요 (`GET /api/v1/tenant/config`) | -| **5** | 상수/공휴일 외부화 | 공휴일 데이터를 **DB에서 서빙**해야 함 → 백엔드 API 필요 (`GET /api/v1/holidays?year=2026`) | -| **7** | TenantConfigService | 테넌트 설정 통합 API 필요 → branding + regional + features를 한 번에 가져오는 엔드포인트 | - -### 추천 진행 순서 - -``` -[즉시 시작 - 프론트 단독] - Phase 3 (포매터) + Phase 6 (localStorage/타입) 병렬 진행 - -[백엔드 협의 후 시작] - Phase 1 (API 헤더) → Phase 2 (Actions) - -[백엔드 API 준비 후 시작] - Phase 7 (TenantConfigService) → Phase 4 (브랜딩) + Phase 5 (상수) -``` - ---- - -## Phase 1: API 레이어 테넌트 컨텍스트 주입 🔴 `백엔드 협의 필요` - -> **목표**: 모든 백엔드 API 호출에 테넌트 식별 정보가 전달되도록 함 -> **예상**: 3-5일 - -### 1-1. 로그인 시 tenant_id 쿠키 추가 - -**파일**: `src/app/api/auth/login/route.ts` - -**현재**: access_token, refresh_token 쿠키만 설정 -**변경**: tenant_id 쿠키 추가 (HttpOnly, API 프록시에서 읽기용) - -```typescript -// 로그인 성공 후 추가 -const tenantCookie = [ - `tenant_id=${data.tenant.id}`, - 'HttpOnly', - ...(isProduction ? ['Secure'] : []), - 'SameSite=Lax', - 'Path=/', - `Max-Age=${data.expires_in || 7200}`, -].join('; '); -response.headers.append('Set-Cookie', tenantCookie); -``` - -### 1-2. API 프록시에 X-Tenant-ID 헤더 추가 - -**파일**: `src/app/api/proxy/[...path]/route.ts` - -**현재**: -```typescript -const headers = { - 'Accept': 'application/json', - 'X-API-KEY': process.env.API_KEY || '', - 'Authorization': `Bearer ${token}`, -}; -``` - -**변경**: -```typescript -const tenantId = request.cookies.get('tenant_id')?.value; -const headers = { - 'Accept': 'application/json', - 'X-API-KEY': process.env.API_KEY || '', - 'Authorization': `Bearer ${token}`, - ...(tenantId && { 'X-Tenant-ID': tenantId }), -}; -``` - -### 1-3. serverFetch 래퍼에 테넌트 헤더 추가 - -**파일**: `src/lib/api/fetch-wrapper.ts` - -**현재**: Authorization 헤더만 전달 -**변경**: tenant_id 쿠키 읽어서 X-Tenant-ID 헤더 자동 추가 - -```typescript -export async function serverFetch(url: string, options?: RequestInit) { - const cookieStore = await cookies(); - const token = cookieStore.get('access_token')?.value; - const tenantId = cookieStore.get('tenant_id')?.value; - - const headers = { - ...options?.headers, - 'Authorization': `Bearer ${token}`, - ...(tenantId && { 'X-Tenant-ID': tenantId }), - }; - // ... -} -``` - -### 1-4. ApiClient 클래스에 테넌트 지원 - -**파일**: `src/lib/api/client.ts` - -**변경**: `getAuthHeaders()`에 X-Tenant-ID 포함 - -### 체크리스트 - -``` -- [ ] login/route.ts에 tenant_id 쿠키 Set-Cookie 추가 -- [ ] proxy/[...path]/route.ts에서 tenant_id 쿠키 읽기 + X-Tenant-ID 헤더 전달 -- [ ] fetch-wrapper.ts serverFetch에 X-Tenant-ID 자동 추가 -- [ ] client.ts ApiClient에 tenantId 옵션 추가 -- [ ] authenticated-fetch.ts에도 테넌트 헤더 전파 확인 -- [ ] 로그아웃 시 tenant_id 쿠키 삭제 확인 -- [ ] 토큰 갱신 시 tenant_id 쿠키 유지 확인 -- [ ] 백엔드와 X-Tenant-ID 헤더 수신 방식 협의 -``` - ---- - -## Phase 2: Server Actions 점진적 마이그레이션 🔴 `백엔드 협의 필요` - -> **목표**: 70+ actions.ts에서 테넌트 컨텍스트가 자동 전달되도록 함 -> **예상**: 1-2주 (Phase 1 완료 후 자동 적용되는 부분 다수) - -### 2-1. 현재 패턴 분석 - -대부분의 actions.ts가 이 패턴을 따름: -```typescript -const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/endpoint`; -const { response, error } = await serverFetch(url, { method: 'GET' }); -``` - -### 2-2. 자동 적용 범위 (Phase 1 완료 시) - -Phase 1에서 `serverFetch`에 X-Tenant-ID를 자동 추가하면, **기존 actions.ts 대부분은 수정 없이** 테넌트 헤더가 전달됨. - -### 2-3. 수동 확인 필요 케이스 - -`serverFetch`를 사용하지 않고 직접 `fetch()`를 호출하는 곳: -```bash -# 검색 대상 -grep -r "fetch(" src/components/*/actions.ts --include="*.ts" | grep -v serverFetch -``` - -### 2-4. 선택적 URL 테넌트 프리픽스 - -백엔드가 URL 경로에 테넌트를 요구하는 경우만: -```typescript -// 필요한 경우에만 적용 -function buildTenantUrl(endpoint: string, tenantId?: string): string { - if (endpoint.startsWith('http')) return endpoint; // 레거시 호환 - const base = process.env.NEXT_PUBLIC_API_URL; - return tenantId - ? `${base}/api/v1/tenant/${tenantId}/${endpoint}` - : `${base}/api/v1/${endpoint}`; -} -``` - -### 체크리스트 - -``` -- [ ] serverFetch 사용하지 않는 actions.ts 목록 확인 -- [ ] 직접 fetch() 호출하는 곳 serverFetch로 마이그레이션 -- [ ] 백엔드와 URL 패턴 vs 헤더 패턴 최종 협의 -- [ ] 고빈도 도메인 우선 검증: clients, items, production, sales -- [ ] 에러 시 테넌트 컨텍스트 누락 로그 추가 -``` - ---- - -## Phase 3: 포매터 & 유틸리티 테넌트 설정 기반 전환 🔴 `프론트 단독 가능` - -> **목표**: 한국어 하드코딩된 포매터를 테넌트 설정 기반으로 변경 -> **예상**: 3-5일 - -### 3-1. 영향받는 파일 목록 - -| 파일 | 함수 | 하드코딩 내용 | -|------|------|---------------| -| `src/utils/formatAmount.ts` | `formatAmount()` | "원", "만원" | -| `src/utils/formatAmount.ts` | `formatKoreanAmount()` | "억", "만" | -| `src/lib/formatters.ts` | `formatBusinessNumber()` | 한국 사업자번호 (XXX-XX-XXXXX) | -| `src/lib/formatters.ts` | `formatPhoneNumber()` | 한국 전화 (02-, 010-) | -| `src/utils/date.ts` | `formatDate()` | `'ko-KR'` 로케일 | - -### 3-2. TenantRegionalConfig 인터페이스 - -```typescript -// src/types/tenant-config.ts (신규) -export interface TenantRegionalConfig { - locale: string; // 'ko-KR' | 'en-US' | 'ja-JP' - timezone: string; // 'Asia/Seoul' | 'America/New_York' - currency: { - code: string; // 'KRW' | 'USD' | 'JPY' - symbol: string; // '원' | '$' | '¥' - locale: string; // Intl.NumberFormat 로케일 - largeUnitName?: string; // '만' (한국 전용) - largeUnitValue?: number; // 10000 - }; - phone: { - countryCode: string; // '+82' | '+1' | '+81' - format: string; // 'XXX-XXXX-XXXX' - }; - businessNumber: { - format: string; // 'XXX-XX-XXXXX' - label: string; // '사업자번호' | 'Business No.' | '法人番号' - }; -} -``` - -### 3-3. 마이그레이션 접근 (하위 호환) - -기존 함수를 바로 변경하지 않고, 오버로드 + 기본값 패턴 적용: - -```typescript -// 기존 호출 코드를 깨지 않는 방식 -export function formatAmount(amount: number, config?: TenantCurrencyConfig): string { - const cfg = config ?? DEFAULT_KR_CURRENCY_CONFIG; // 기본값: 한국 - // ... 테넌트 설정 기반 포매팅 -} -``` - -### 3-4. 기존 공통화 작업 참조 - -**이미 작성된 관련 문서**: -- `claudedocs/[IMPL-2026-02-05] formatter-commonization-plan.md` -- `claudedocs/[ANALYSIS-2026-01-20] 공통화-현황-분석.md` - -이 문서들의 포매터 공통화 계획과 병합하여 진행. - -### 체크리스트 - -``` -- [ ] TenantRegionalConfig 인터페이스 정의 -- [ ] DEFAULT_KR_CONFIG 기본값 생성 (하위 호환) -- [ ] formatAmount() 테넌트 설정 지원 추가 -- [ ] formatDate() 테넌트 로케일 지원 추가 -- [ ] formatBusinessNumber() 포맷 설정 지원 추가 -- [ ] formatPhoneNumber() 국가 코드 지원 추가 -- [ ] 기존 호출 코드 깨지지 않는지 검증 -- [ ] formatter-commonization-plan.md와 통합 -``` - ---- - -## Phase 4: 브랜딩 동적화 🟡 `백엔드 API 필요` - -> **목표**: 하드코딩된 회사명/로고를 테넌트 설정 기반으로 변경 -> **예상**: 2-3일 - -### 4-1. 영향받는 파일 목록 - -| 파일 | 하드코딩 | 변경 방향 | -|------|----------|-----------| -| `src/layouts/AuthenticatedLayout.tsx` | `APP_NAME = 'SAM'` | `tenant.company_name` 또는 테넌트 설정 | -| `src/layouts/AuthenticatedLayout.tsx` | `` | 테넌트별 로고 URL | -| `src/layouts/AuthenticatedLayout.tsx` | `MOCK_COMPANIES` 배열 | `user.tenant.other_tenants` 연동 | -| `src/app/[locale]/layout.tsx` | `APP_TITLE = 'SAM - 내 손안의 대시보드'` | 테넌트 설정 기반 | -| `src/app/[locale]/layout.tsx` | SEO 메타데이터 | 테넌트별 (단, 폐쇄형이므로 낮은 우선순위) | - -### 4-2. 테넌트 브랜딩 설정 구조 - -```typescript -// src/types/tenant-config.ts에 추가 -export interface TenantBrandingConfig { - appName: string; // 'SAM' | '주일 MES' | 커스텀 - appSubtitle?: string; // 'Smart Automation Management' - logoUrl: string; // '/sam-logo.png' | '/tenants/282/logo.png' - faviconUrl?: string; - primaryColor?: string; // 테마 주색상 - loginBackground?: string; // 로그인 페이지 배경 -} -``` - -### 4-3. 적용 방식 - -```typescript -// AuthenticatedLayout.tsx 내부 -const { currentUser } = useAuth(); -const branding = currentUser?.tenant?.branding ?? DEFAULT_BRANDING; - -// 로고 -{branding.appName} - -// 앱 이름 -

{branding.appName}

-``` - -### 4-4. MOCK_COMPANIES → other_tenants 연동 - -**현재**: 하드코딩 목업 -```typescript -const MOCK_COMPANIES = [ - { id: 'all', name: '전체' }, - { id: 'company1', name: '(주)삼성건설' }, - ... -]; -``` - -**변경**: 실제 테넌트 데이터 연동 -```typescript -const tenantOptions = useMemo(() => { - const current = currentUser?.tenant; - const others = current?.other_tenants ?? []; - return [current, ...others].filter(Boolean); -}, [currentUser]); -``` - -### 체크리스트 - -``` -- [ ] TenantBrandingConfig 인터페이스 정의 -- [ ] DEFAULT_BRANDING 기본값 (현재 SAM 설정) -- [ ] AuthenticatedLayout 로고/앱이름 동적화 -- [ ] MOCK_COMPANIES를 other_tenants 기반으로 교체 -- [ ] 로그인 페이지 브랜딩 동적화 -- [ ] favicon 동적 변경 (선택) -- [ ] 테넌트별 로고 파일 서빙 방식 결정 (public/ vs API) -``` - ---- - -## Phase 5: 상수 & 비즈니스 로직 외부화 🟡 `백엔드 API 필요` - -> **목표**: 한국 특화 상수를 테넌트/국가별 설정으로 외부화 -> **예상**: 3-5일 - -### 5-1. 영향받는 항목 - -| 항목 | 파일 | 현재 | 변경 | -|------|------|------|------| -| 공휴일 | `src/constants/calendarEvents.ts` | 한국 공휴일 하드코딩 | DB/API 기반 | -| 프로세스 타입 | `src/types/process.ts` | "생산", "검사" 등 | i18n 라벨 | -| 상태 라벨 | `src/lib/utils/status-config.ts` | "대기", "완료" 등 | i18n 라벨 | -| 품목 타입 | `src/types/item.ts` | "제품", "부품" 등 | i18n 라벨 | -| 근무일 | 관련 컴포넌트 | 월-금 하드코딩 | 테넌트 설정 | - -### 5-2. 외부화 전략 - -**공휴일**: 백엔드 API로 이동 (테넌트별 국가 설정에 따라 반환) -```typescript -// AS-IS: 하드코딩 -const HOLIDAYS_2026 = [ - { date: '2026-01-01', name: '신정', type: 'holiday' }, - ... -]; - -// TO-BE: API 호출 -const holidays = await getHolidays(tenantId, year); -``` - -**라벨/상태**: next-intl 다국어 시스템 활용 (이미 ko/en/ja 구조 있음) -```typescript -// AS-IS -const statusLabels = { pending: '대기', completed: '완료' }; - -// TO-BE -const t = useTranslations('status'); -const label = t('pending'); // 로케일에 따라 자동 변환 -``` - -### 체크리스트 - -``` -- [ ] calendarEvents.ts 공휴일 데이터 → API 엔드포인트로 이동 -- [ ] 프로세스 타입 라벨 → messages/ko.json, en.json, ja.json으로 이동 -- [ ] 상태 라벨 → i18n 키로 변환 -- [ ] 품목 타입 라벨 → i18n 키로 변환 -- [ ] 근무일 설정 → 테넌트 config로 이동 -- [ ] 백엔드에 공휴일 API 요청 -``` - ---- - -## Phase 6: localStorage 잔재 정리 & 타입 통일 🟡 `프론트 단독 가능` - -> **목표**: TenantAwareCache 미사용 곳 정리 + tenantId 타입 통일 -> **예상**: 2-3일 - -### 6-1. localStorage 직접 사용 감사 - -```bash -# 검색 대상 -grep -r "localStorage\.\(setItem\|getItem\)" src/ --include="*.ts" --include="*.tsx" -``` - -**알려진 비-테넌트-스코프 키**: -- `mes-users` → 사용자 목록 (테넌트 스코프 필요 여부 검토) -- `mes-currentUser` → 현재 사용자 (로그인 상태이므로 테넌트 무관) -- 기타 직접 사용 곳 → TenantAwareCache 또는 테넌트 프리픽스 적용 - -### 6-2. tenantId 타입 통일 - -**현재 상황**: -- `User.tenant.id` → `number` (AuthContext) -- `PageConfig.tenantId` → `string` (masterDataStore) -- TenantAwareCache → `number` - -**통일**: `number`로 표준화 (백엔드 응답 기준) - -```typescript -// 수정 대상 찾기 -grep -r "tenantId.*string" src/ --include="*.ts" -``` - -### 체크리스트 - -``` -- [ ] localStorage 직접 사용 전수 조사 -- [ ] TenantAwareCache로 마이그레이션 가능한 곳 목록화 -- [ ] 테넌트 스코프 불필요한 곳 명시 (mes-currentUser 등) -- [ ] tenantId: string → number 통일 -- [ ] PageConfig 타입 수정 -- [ ] 관련 타입 참조 전부 업데이트 -``` - ---- - -## Phase 7: TenantConfigService & TenantContext (선택) 🟢 `백엔드 API 필요` - -> **목표**: 테넌트 설정을 한곳에서 관리하는 서비스 레이어 -> **예상**: 3-5일 (Phase 3-5 진행 중 필요에 따라) - -### 7-1. TenantConfigService - -```typescript -// src/services/TenantConfigService.ts (신규) -export interface TenantConfiguration { - tenantId: number; - branding: TenantBrandingConfig; - regional: TenantRegionalConfig; - features: { - enabledModules: string[]; - customFields?: Record; - }; - calendar: { - workingDays: number[]; // [1,2,3,4,5] = 월-금 - holidays: HolidayEntry[]; - }; -} - -class TenantConfigService { - private cache: Map = new Map(); - - async getConfig(tenantId: number): Promise { - if (this.cache.has(tenantId)) return this.cache.get(tenantId)!; - const config = await this.fetchFromApi(tenantId); - this.cache.set(tenantId, config); - return config; - } -} -``` - -### 7-2. TenantContext Provider - -```typescript -// src/contexts/TenantContext.tsx (신규) -export function TenantProvider({ children }: { children: ReactNode }) { - const { currentUser } = useAuth(); - const [config, setConfig] = useState(); - - useEffect(() => { - if (currentUser?.tenant?.id) { - tenantConfigService.getConfig(currentUser.tenant.id) - .then(setConfig); - } - }, [currentUser?.tenant?.id]); - - return ( - - {children} - - ); -} - -// 사용 -const tenantConfig = useTenantConfig(); -const currencySymbol = tenantConfig.regional.currency.symbol; -``` - -### 체크리스트 - -``` -- [ ] TenantConfiguration 통합 인터페이스 설계 -- [ ] TenantConfigService 구현 (캐시 + API 호출) -- [ ] TenantContext Provider 구현 -- [ ] useTenantConfig() 훅 구현 -- [ ] Protected Layout에 TenantProvider 추가 -- [ ] 기존 코드에서 점진적 마이그레이션 -``` - ---- - -## Phase 8: 테넌트 라우팅 (필요 시) 🟢 `프론트 단독 가능` - -> **목표**: URL에 테넌트 식별자 포함 (필요한 경우에만) -> **예상**: 1주+ - -### 현재 라우팅 -``` -/[locale]/(protected)/dashboard -``` - -### 옵션 A: 경로 기반 (권장 - 필요 시) -``` -/[tenant]/[locale]/(protected)/dashboard -/acme/ko/dashboard -``` - -### 옵션 B: 서브도메인 기반 -``` -acme.sam.com/ko/dashboard -``` - -### 옵션 C: 현재 유지 (인증 기반만) -``` -/[locale]/(protected)/dashboard ← 테넌트는 JWT/쿠키로만 식별 -``` - -**결정**: 현재는 **옵션 C 유지**. 다중 테넌트 URL 분리가 필요해지면 옵션 A 도입. - ---- - -## 백엔드 협의 사항 - -### 필수 협의 (Phase 1 시작 전) - -| 항목 | 질문 | 결정 사항 | -|------|------|-----------| -| 테넌트 식별 방식 | `X-Tenant-ID` 헤더 vs URL 경로 vs JWT만? | TBD | -| X-Tenant-ID 수신 | 백엔드가 이 헤더를 읽고 필터링하는지? | TBD | -| JWT 내 tenant_id | 토큰에 tenant_id가 포함되어 있는지? | TBD | -| 공휴일 API | `GET /api/v1/holidays?year=2026` 지원? | TBD | -| 테넌트 설정 API | `GET /api/v1/tenant/config` 지원? | TBD | - -### 선택 협의 (Phase 4-5 시작 전) - -| 항목 | 질문 | 결정 사항 | -|------|------|-----------| -| 테넌트 로고 | 로고 URL을 어디서 제공? (API vs 파일서버) | TBD | -| 브랜딩 설정 | 테넌트별 앱이름/테마 API 제공 가능? | TBD | -| 다국어 라벨 | 백엔드 코드 라벨이 다국어 지원? | TBD | - ---- - -## 실행 우선순위 요약 - -``` -[프론트 단독] Phase 3: 포매터 테넌트 설정 기반 🔴 3-5일 ← 즉시 시작 가능 -[프론트 단독] Phase 6: localStorage 정리/타입 통일 🟡 2-3일 ← 즉시 시작 가능 -[프론트 단독] Phase 8: 테넌트 라우팅 🟢 필요시 ← 당분간 불필요 - -[백엔드 협의] Phase 1: API 테넌트 컨텍스트 주입 🔴 3-5일 ← 백엔드 확인 후 -[백엔드 협의] Phase 2: Server Actions 마이그레이션 🔴 1-2주 ← Phase 1 후 자동 적용 범위 큼 - -[백엔드 API] Phase 4: 브랜딩 동적화 🟡 2-3일 ← 테넌트 설정 API 필요 -[백엔드 API] Phase 5: 상수/공휴일 외부화 🟡 3-5일 ← 공휴일 API 필요 -[백엔드 API] Phase 7: TenantConfigService 🟢 3-5일 ← 통합 설정 API 필요 -``` - -### 병렬 진행 가능 조합 - -``` -[즉시 시작 - 프론트 단독] -├─ Phase 3 (포매터) ─────────→ 독립 완료 -└─ Phase 6 (localStorage) ──→ 독립 완료 - -[백엔드 협의 후 - 프론트+백엔드] -└─ Phase 1 (API 헤더) ──────→ Phase 2 (Actions 자동 적용) - -[백엔드 API 준비 후 - 프론트+백엔드] -└─ Phase 7 (TenantConfig) ──→ Phase 4 (브랜딩) + Phase 5 (상수) -``` - ---- - -## 위험 요소 & 대응 - -| 위험 | 확률 | 영향 | 대응 | -|------|------|------|------| -| 70+ actions.ts 수동 마이그레이션 | 높음 | 중간 | serverFetch 자동 주입으로 대부분 해결 | -| 백엔드 X-Tenant-ID 미지원 | 중간 | 높음 | Phase 1 시작 전 백엔드 팀 협의 필수 | -| 포매터 변경 시 기존 UI 깨짐 | 낮음 | 중간 | 기본값 패턴으로 하위 호환 유지 | -| 캐시 무효화 누락 | 낮음 | 높음 | TenantAwareCache 이미 검증됨 | -| 다국어 번역 리소스 부족 | 중간 | 낮음 | 한국어 기본값 유지, 점진 추가 | - ---- - -## 관련 문서 - -| 문서 | 설명 | -|------|------| -| `architecture/[REF-2025-11-19] multi-tenancy-implementation.md` | 이전 멀티테넌시 구현 (Phase 1-2 → 완료됨) | -| `architecture/[TEST-2025-11-19] multi-tenancy-test-guide.md` | 캐시 격리 테스트 가이드 | -| `architecture/[FIX-2026-01-29] masterdata-cache-tenant-isolation.md` | masterDataStore 캐시 테넌트 격리 수정 | -| `[IMPL-2026-02-05] formatter-commonization-plan.md` | 포매터 공통화 계획 (Phase 3과 병합) | -| `[ANALYSIS-2026-01-20] 공통화-현황-분석.md` | 공통화 현황 분석 | -| `[ANALYSIS-2026-02-05] list-page-commonization-status.md` | 리스트 페이지 공통화 현황 | -| `auth/[IMPL-2025-11-07] jwt-cookie-authentication-final.md` | JWT 쿠키 인증 구현 | -| `api/[REF] api-requirements.md` | API 요구사항 | - ---- - -## 변경 이력 - -| 날짜 | 변경 내용 | -|------|-----------| -| 2026-02-06 | 초기 작성 - 전체 코드베이스 분석 기반 8 Phase 로드맵 | - ---- - -**다음 액션**: Phase 1 시작 전 백엔드 팀과 `X-Tenant-ID` 헤더 수신 방식 협의 diff --git a/claudedocs/architecture/[PLAN-2026-02-06] refactoring-roadmap.md b/claudedocs/architecture/[PLAN-2026-02-06] refactoring-roadmap.md deleted file mode 100644 index 432fbd97..00000000 --- a/claudedocs/architecture/[PLAN-2026-02-06] refactoring-roadmap.md +++ /dev/null @@ -1,446 +0,0 @@ -# 리팩토링 로드맵 - -**작성일**: 2026-02-06 -**목적**: 전체 코드베이스 리팩토링 포인트 점검 및 실행 계획 -**상태**: Phase 1 완료, Phase 3 완료 (공용 유틸 추출), Phase 4 SearchableSelectionModal 완료 - ---- - -## 현재 코드베이스 수치 (2026-02-06 기준) - -| 지표 | 수치 | 비고 | -|------|------|------| -| 전체 코드 | ~301,000줄 | TS/TSX | -| 컴포넌트 파일 | ~551개 | | -| 페이지 파일 | ~253개 | | -| action.ts 파일 | 80개 | 거의 동일 CRUD 패턴 | -| types.ts 파일 | 94개 | 중복 타입 다수 | -| 모달 컴포넌트 | 42개 | 유사 패턴 반복 | -| 2000줄+ 파일 | 4개 | God 컴포넌트 | -| 1000~2000줄 파일 | 25+개 | 분리 대상 | -| 500~1000줄 파일 | 50+개 | 검토 대상 | - ---- - -## God 컴포넌트 / 대형 파일 목록 - -### 🔴 2000줄 이상 (즉시 분리 필요) - -| 파일 | 줄수 | 핵심 문제 | 분리 방향 | -|------|------|-----------|-----------| -| `components/business/MainDashboard.tsx` | 2,651 | CEO/영업/생산/품질 대시보드 한 파일 | 역할별 섹션 컴포넌트 분리 | -| `contexts/ItemMasterContext.tsx` | 2,406 | useState 17개, useEffect 15개, 함수 50+개 | 도메인별 5개 Context 분리 | -| `lib/api/item-master.ts` | 2,232 | 모든 품목 API 한 파일 | 도메인별 API 모듈 분리 | -| `lib/api/dashboard/transformers.ts` | 1,576 | 전체 대시보드 변환 로직 | 섹션별 transformer 분리 | - -### 🟡 1000~2000줄 (우선 검토) - -| 파일 | 줄수 | 도메인 | 분리 방향 | -|------|------|--------|-----------| -| `components/orders/actions.ts` | 1,394 | 수주 | 서비스 레이어 분리 | -| `components/accounting/ExpectedExpenseManagement/index.tsx` | 1,299 | 회계 | 서브 컴포넌트 추출 | -| `layouts/AuthenticatedLayout.tsx` | 1,289 | 레이아웃 | 훅 24개 → 섹션별 분리 | -| `components/quotes/QuoteRegistration.tsx` | 1,268 | 견적 | 폼 섹션 추출, useState 13개 | -| `components/quotes/actions.ts` | 1,266 | 견적 | API 레이어 분리 | -| `components/business/construction/management/actions.ts` | 1,222 | 건설 | 도메인 서비스 추출 | -| `components/business/construction/estimates/actions.ts` | 1,222 | 건설 | 도메인 서비스 추출 | -| `components/production/WorkerScreen/index.tsx` | 1,198 | 생산 | 화면 섹션 분리 | -| `hooks/useCEODashboard.ts` | 1,172 | 대시보드 | useState 18개 → 섹션별 훅 분리 | -| `components/material/ReceivingManagement/actions.ts` | 1,152 | 자재 | API 서비스 레이어 | -| `components/quotes/types.ts` | 1,149 | 견적 | 타입 조직화 | -| `components/quality/InspectionManagement/InspectionDetail.tsx` | 1,125 | 품질 | 컴포넌트 추출 | -| `components/hr/VacationManagement/actions.ts` | 1,125 | HR | 서비스 레이어 분리 | -| `components/orders/OrderRegistration.tsx` | 1,123 | 수주 | 폼 섹션 추출, useState 12개 | -| `components/items/DynamicItemForm/index.tsx` | 1,073 | 품목 | 복합 폼 로직 추출 | -| `components/templates/IntegratedListTemplateV2.tsx` | 1,066 | 템플릿 | 템플릿 특화 | -| `components/hr/EmployeeManagement/EmployeeForm.tsx` | 1,051 | HR | 폼 섹션 분리 | -| `components/quotes/QuoteRegistrationV2.tsx` | 1,020 | 견적 | 폼 리팩토링 | -| `components/templates/UniversalListPage/index.tsx` | 1,007 | 템플릿 | 템플릿 최적화 | -| `components/items/ItemMasterDataManagement.tsx` | 1,005 | 품목 | 도메인 로직 추출 | - ---- - -## 중복 패턴 분석 - -### 1. 액션 파일 80개 동일 패턴 (~24,000줄 중복) - -**현재**: 모든 도메인이 이 구조를 복붙 -```typescript -'use server'; -import { serverFetch } from '@/lib/api/fetch-wrapper'; - -interface Api[Domain]Data { ... } // 타입 정의 100~300줄 -function transform(data) { ... } // API→프론트 변환 50~100줄 - -export async function getList(params) { // 목록 조회 - const url = `${API_URL}/api/v1/endpoint`; - const { response } = await serverFetch(url, { method: 'GET' }); - return transform(response); -} -export async function getById(id) { ... } // 상세 조회 -export async function create(data) { ... } // 생성 -export async function update(id, data) { ... } // 수정 -export async function delete(id) { ... } // 삭제 -export async function bulkDelete(ids) { ... } // 일괄 삭제 -``` - -**해당 도메인**: orders, quotes, clients, accounting(13모듈), hr(6모듈), production(4모듈), material(2모듈), quality(2모듈), construction(17모듈), settings(14모듈) - -**해결 방향**: 제네릭 API 서비스 팩토리 -```typescript -// lib/api/createCrudService.ts -function createCrudService(config: { - endpoint: string; - transform: (api: TApi) => TFront; - reverseTransform: (front: TFront) => Partial; -}) { - return { - getList: async (params) => { ... }, - getById: async (id) => { ... }, - create: async (data) => { ... }, - update: async (id, data) => { ... }, - delete: async (id) => { ... }, - bulkDelete: async (ids) => { ... }, - }; -} - -// 사용: 10줄로 끝 -const orderService = createCrudService({ - endpoint: 'orders', - transform: transformOrder, - reverseTransform: reverseTransformOrder, -}); -``` - -### 2. 데이터 페칭 패턴 3가지 혼재 - -| 패턴 | 사용 비율 | 위치 | -|------|-----------|------| -| useEffect + .then() 직접 호출 | ~75% (99+ 컴포넌트) | 대부분의 도메인 | -| 커스텀 훅 (useDetailData 등) | ~15% (~15 컴포넌트) | 신규 구현 | -| ApiClient 클래스 | ~10% (15 컴포넌트) | 건설 도메인만 | - -**수동 로딩 상태 관리**: 262곳에서 반복 -```typescript -// 이 패턴이 262번 반복됨 -const [data, setData] = useState([]); -const [isLoading, setIsLoading] = useState(true); -const [error, setError] = useState(null); - -useEffect(() => { - setIsLoading(true); - fetchData() - .then(result => setData(result)) - .catch(err => setError(err)) - .finally(() => setIsLoading(false)); -}, []); -``` - -### 3. 폼 검증 3가지 방식 혼재 - -| 방식 | 사용 파일 수 | 비율 | -|------|-------------|------| -| Zod 스키마 (정석) | 3개 (로그인, 회원가입, 품목) | 5% | -| 수동 if문 검증 | 50+개 | 60% | -| 검증 없음 | ~30개 | 35% | - -### 4. 리스트 페이지 템플릿 이중화 - -| 방식 | 사용 | 비율 | -|------|------|------| -| `UniversalListPage` (신규 표준) | 20개 페이지 | 25% | -| 수동 구현 (레거시) | 60+ 페이지 | 75% | - -### 5. 모달/다이얼로그 42개 유사 패턴 - -**검색/선택 모달 5개+ 거의 동일**: -- `quotes/ItemSearchModal.tsx` -- `production/WorkOrders/AssigneeSelectModal.tsx` -- `material/ReceivingManagement/SupplierSearchModal.tsx` -- `quality/InspectionManagement/OrderSelectModal.tsx` -- `production/WorkOrders/SalesOrderSelectModal.tsx` - -전부 "검색 입력 → API 호출 → 목록 표시 → 체크박스 선택 → 확인" 동일 구조 -→ `SearchableSelectionModal` 하나로 통합 가능 - ---- - -## 성능 최적화 포인트 - -| 항목 | 현재 상태 | 영향도 | 해결 방향 | -|------|-----------|--------|-----------| -| React.memo | 551개 컴포넌트 중 **1개만** 사용 | 🔴 높음 | 리스트 아이템/카드 컴포넌트에 적용 | -| 인라인 화살표 함수 | **746곳** `onClick={() => ...}` | 🟡 중간 | 대형 컴포넌트에서 useCallback 적용 | -| useMemo 미사용 | 대용량 배열 필터링/정렬 곳곳 | 🟡 중간 | 비용 높은 계산에 적용 | - -**React.memo 우선 적용 대상** (리스트 내 반복 렌더링 컴포넌트): -- `production/WorkerScreen/WorkItemCard.tsx` -- `board/CommentSection/CommentItem.tsx` -- `business/construction/management/ProjectCard.tsx` -- 기타 *Row, *Item, *Card 컴포넌트 30+개 - ---- - -## 타입 시스템 문제 - -| 항목 | 수치 | 영향 | -|------|------|------| -| `any` 타입 사용 | 102곳 (29개 파일) | 타입 안전성 저하 | -| 동일 엔티티 다중 타입 정의 | Vendor, Item, Order 등 | 변환 코드 ~800줄 중복 | -| types.ts 파일 | 94개 | 정규 타입 찾기 어려움 | -| @ts-ignore/eslint-disable | 25개 파일 | 숨겨진 타입 에러 | - ---- - -## 추출 가능한 공통 훅 목록 - -### 즉시 생성 가능 (프론트 단독) - -| 훅 이름 | 대체 범위 | 예상 절감 | 기존 참고 | -|---------|-----------|-----------|-----------| -| `useListData` | 60+ 리스트 페이지 | ~4,000줄 | hooks/useDetailData.ts 패턴 확장 | -| `useFormSubmit` | 80+ 폼 | ~3,000줄 | 신규 | -| `usePagination` | 60+ 컴포넌트 | ~1,000줄 | 신규 | -| `useModal` | 42 모달 | ~500줄 | 신규 | -| `useClientSideFiltering` | 55+ 컴포넌트 | ~800줄 | 신규 | - -### 기존 훅 (활용 확대 필요) - -| 훅 | 현재 사용 | 전체 적용 시 | -|----|-----------|-------------| -| `useDetailData` | ~15 컴포넌트 | 100+ 상세 페이지 | -| `useDetailPageState` | ~10 컴포넌트 | 100+ 상세 페이지 | -| `useCRUDHandlers` | ~10 컴포넌트 | 80+ CRUD 페이지 | - ---- - -## 실행 계획 - -### Phase 1: 공통 훅 추출 ✅ 완료 (2026-02-09) - -> 실제 코드 분석 결과 계획 수정 → 실증 기반 리팩토링 실행 - -**실행 결과** (계획 vs 실제): -``` -기존 계획의 useListData, usePagination, useClientSideFiltering, useModal은 -UniversalListPage 템플릿이 이미 내부 처리 → 불필요 판정. - -실제 실행: -- [x] Step 1: executeServerAction (82개 action.ts 에러처리 래퍼) → ~3,000줄 절감 -- [x] Step 2: useDeleteDialog (6개 파일 삭제 다이얼로그 통합) → ~150줄 절감 -- [x] Step 3: useStatsLoader (7개 파일 stats 로딩 통합) → ~100줄 절감 -- [x] Step 4: React.memo 3개 + any→unknown 7건 + @ts-ignore 0건 -``` - -**실제 효과**: ~3,750줄 절감, 82개 action.ts 패턴 통일, 타입 안전성 향상 -**상세**: `refactoring/[IMPL-2026-02-09] phase1-common-hooks-checklist.md` - ---- - -### Phase 2: God 컴포넌트 분리 (2-3주) `프론트 단독` - -> 2000줄+ 파일 4개 + 핵심 1000줄+ 파일 우선 분리 - -**작업 항목**: -``` -- [ ] MainDashboard.tsx (2,651줄) 분리 - → sections/CEOSection, SalesSection, ProductionSection, QualitySection - → hooks/useDashboardData - → utils/calculations -- [ ] ItemMasterContext.tsx (2,406줄) 분리 - → ItemContext, SpecificationContext, MaterialContext - → TemplateContext, AttributeContext -- [ ] useCEODashboard.ts (1,172줄) 분리 - → useDailyReport, useReceivables, useMonthlyExpense 등 개별 훅 - → 훅 팩토리 패턴 적용 -- [ ] lib/api/item-master.ts (2,232줄) 분리 - → 도메인별 API 모듈 (items, specifications, materials, templates) -- [ ] AuthenticatedLayout.tsx (1,289줄) - → useLayoutState, useNavigation, useTenantBranding 훅 추출 -``` - -**예상 효과**: 유지보수성 +50%, 단위 테스트 가능성 확보 - ---- - -### Phase 3: 액션 파일 공용 유틸 추출 ✅ 완료 (2026-02-10) - -> 전수 분석 → 팩토리 ROI 재평가 → 공용 유틸 추출로 전략 변경 - -**전수 분석 결과** (82개 action 파일): -``` -- 35개: executeServerAction 패턴 (Phase 1에서 통일) -- 15개: 모의 데이터 (mock, API 미연동) -- 13개: ApiClient 클래스 패턴 (건설 도메인) -- 나머지: 특수 도메인 로직 (견적, 수주, 품목 등) -``` - -**팩토리 마이그레이션 ROI 재평가**: -``` -- createCrudService 팩토리: 2개(Rank, Title)만 적합 → ROI ~6% (너무 낮음) -- 대부분 파일: 페이지네이션, 커스텀 쿼리 파라미터, 도메인 특화 로직으로 팩토리 패턴 부적합 -- 결론: 팩토리 대량 마이그레이션 대신 공용 유틸 추출로 전략 전환 -``` - -**실행 결과** (2026-02-10): -``` -Step 1: 공용 타입 추출 (src/lib/api/types.ts) - - [x] PaginatedApiResponse — 25+ 파일에서 중복 정의 제거 - - [x] PaginationMeta, PaginatedResult — 프론트엔드 표준 페이지네이션 타입 - - [x] toPaginationMeta() — snake_case → camelCase 변환 헬퍼 - - [x] SelectOption — 공용 선택 옵션 타입 - -Step 2: 공용 룩업 헬퍼 추출 (src/lib/api/shared-lookups.ts) - - [x] fetchVendorOptions() — 거래처 목록 조회 (4개 파일 중복 제거) - - [x] fetchBankAccountOptions() — 계좌 목록 조회 (심플) - - [x] fetchBankAccountDetailOptions() — 계좌 상세 조회 (bankName, accountNumber 포함) - - [x] BankAccountOption 타입 - -Step 3: PaginatedResponse 타입 마이그레이션 (~20개 파일) - - [x] 제네릭 패턴 (interface PaginatedResponse) → import PaginatedApiResponse - - [x] 도메인 패턴 (interface XxxPaginatedResponse) → type alias - - 스킵: VendorManagement/types.ts (page?/size? 비표준), PermissionManagement/types.ts (meta 래퍼) - -Step 4: 공용 룩업 헬퍼 마이그레이션 (4개 파일) - - [x] DepositManagement/actions.ts — getVendors + getBankAccounts 교체 - - [x] WithdrawalManagement/actions.ts — getVendors + getBankAccounts 교체 - - [x] PurchaseManagement/actions.ts — getVendors + getBankAccounts(상세) 교체 - - [x] ExpectedExpenseManagement/actions.ts — getBankAccounts(상세) 교체 - -Step 5: TypeScript 검증 통과 ✅ -``` - -**실측 효과**: -- PaginatedResponse 중복 제거: ~20개 파일, 파일당 ~7줄 = ~140줄 절감 -- 공용 룩업 헬퍼: 4개 파일, 파일당 ~20줄 = ~80줄 절감 -- 총 ~220줄 직접 절감 + 향후 새 파일에서 중복 방지 -- createCrudService + TitleManagement 마이그레이션: ~36줄 절감 (프로토타입 포함) - -**생성된 공용 파일**: -- `src/lib/api/types.ts` — 공용 API 타입 (PaginatedApiResponse, PaginationMeta 등) -- `src/lib/api/shared-lookups.ts` — 공용 룩업 헬퍼 (fetchVendorOptions 등) -- `src/lib/api/create-crud-service.ts` — CRUD 팩토리 (Rank, Title 2개 사용) - ---- - -### Phase 4: 템플릿/패턴 통일 (2-3주) `프론트 단독` `SearchableSelectionModal 완료` - -> UniversalListPage 확대 + 검증 표준화 + 모달 통합 - -**SearchableSelectionModal 완료** (2026-02-10): -``` -- [x] SearchableSelectionModal 공통 컴포넌트 생성 - - types.ts, useSearchableData.ts, SearchableSelectionModal.tsx, index.ts - - 단일선택(single) + 다중선택(multiple) + listWrapper(테이블용) 지원 -- [x] ItemSearchModal 교체 (212→113줄, -47%) -- [x] SupplierSearchModal 교체 (268→161줄, -40%) -- [x] SalesOrderSelectModal 교체 (163→101줄, -38%) -- [x] QuotationSelectDialog 교체 (196→113줄, -42%) -- [x] OrderSelectModal 교체 (220→107줄, -51%) -- [x] organisms/index.ts export 추가 -- [x] CLAUDE.md 공통 컴포넌트 사용 규칙 + claudedocs 가이드 문서 작성 -``` -**실측 효과**: 1,059줄 → 595줄 (464줄 절감, -44%) + 공통 컴포넌트 ~430줄 - -**남은 작업**: -``` -- [ ] UniversalListPage 기능 보강 - - 고급 필터 UI - - 컬럼 커스터마이징 - - 내보내기 기능 -- [ ] 레거시 리스트 페이지 → UniversalListPage 마이그레이션 (우선 20개) -- [ ] Zod 검증 스키마 라이브러리 구축 - - lib/validations/common.ts (이메일, 전화, 사업자번호) - - lib/validations/vendor.ts, order.ts, item.ts 등 -- [ ] 수동 검증 50+ 폼 → Zod 마이그레이션 (우선 10개) -``` - -**예상 효과**: ~5,000줄 절감 (SearchableSelectionModal ~464줄 달성), UX 일관성 +80% - ---- - -### Phase 5: 성능 + 타입 정리 (1-2주) `프론트 단독` `일부 Phase 1에서 선처리` - -> React.memo 적용 + any 제거 + 타입 통합 - -**Phase 1에서 선처리된 항목** (2026-02-09): -``` -- [x] React.memo 3개 적용 (InfoField, CommentItem, WorkItemCard) -- [x] any→unknown 7건 (logger.ts) -- [x] action error handler any 50+곳 (executeServerAction으로 자동 해결) -- [x] @ts-ignore 0건 (이미 제거 완료) -``` - -**남은 작업**: -``` -- [ ] React.memo 추가 적용 (나머지 리스트 아이템 컴포넌트) -- [ ] 대형 컴포넌트 useCallback 적용 -- [ ] any 타입 잔여 92건 - - items/ 도메인 60건 (복잡도 높음, 별도 작업) - - Form 에러 캐스팅 26건 (RHF 타입 시스템 변경 필요) - - dev/ 프로토타입 6건 (비프로덕션) -- [ ] 공통 타입 라이브러리 정리 - - types/shared/ 폴더 생성 - - PaginatedApiResponse ✅ Phase 3에서 완료 (src/lib/api/types.ts) - - FormState, SelectOption 등 추가 타입 -``` - -**예상 효과**: 리스트 렌더링 30-50% 개선, 타입 안전성 +60% - ---- - -## 전체 예상 효과 요약 - -| 지표 | Phase 1 ✅ | Phase 2 | Phase 3 ✅ | Phase 4 | Phase 5 | 합계 | -|------|-----------|---------|---------|---------|---------|------| -| 코드 절감 | ~3,750줄 (실측) | (구조 개선) | ~256줄 (실측) | ~5,000줄 | (품질 개선) | **~9,000줄+** | -| 중복 제거 | 82개 action 통일 | - | 25+ 타입 + 4 룩업 통합 | 5 모달 통합 | - | 종합 개선 | -| 패턴 일관성 | +60% | +50% | +30% (타입 표준화) | +80% | +60% | 종합 개선 | -| 유지보수성 | 높음 | 매우 높음 | 중간 (공용 유틸) | 중간 | 중간 | 종합 개선 | -| 위험도 | 낮음 | 중간 | 낮음 (완료) | 낮음 | 낮음 | - | - ---- - -## 병렬 진행 가능 조합 - -``` -[완료] -├─ Phase 1 (공통 훅) ──────→ ✅ 완료 (2026-02-09) -├─ Phase 3 (공용 유틸 추출) ──→ ✅ 완료 (2026-02-10) -├─ Phase 4 (SearchableSelectionModal) → ✅ 완료 (2026-02-10) -│ -[즉시 시작 가능] -├─ Phase 2 (God 컴포넌트 분리) ──→ Phase 1 훅 + Phase 3 공용 타입 활용 -├─ Phase 4 남은 작업 (UniversalListPage 확대, Zod 검증) -├─ Phase 5 (성능/타입) ─────→ 일부 Phase 1/3에서 선처리됨 -``` - ---- - -## 관련 문서 - -| 문서 | 설명 | -|------|------| -| `[PLAN-2026-02-06] multi-tenancy-optimization-roadmap.md` | 멀티테넌시 공통화 로드맵 (별도 트랙) | -| `[ANALYSIS-2026-01-20] 공통화-현황-분석.md` | 공통화 현황 분석 | -| `[ANALYSIS-2026-02-05] list-page-commonization-status.md` | 리스트 페이지 공통화 현황 | -| `[IMPL-2026-02-05] detail-hooks-migration-plan.md` | 상세 페이지 훅 마이그레이션 계획 | -| `[IMPL-2026-02-05] formatter-commonization-plan.md` | 포매터 공통화 계획 | -| `[PLAN-2026-01-22] ui-component-abstraction.md` | UI 컴포넌트 추상화 계획 | -| `guides/[PLAN-2025-12-23] common-component-extraction-plan.md` | 공통 컴포넌트 추출 계획 | - ---- - -## 변경 이력 - -| 날짜 | 변경 내용 | -|------|-----------| -| 2026-02-06 | 초기 작성 - 전체 코드베이스 분석 기반 5 Phase 로드맵 | -| 2026-02-09 | Phase 1 완료 반영 - 실측 기반 효과 수치 보정 (8,500줄→3,750줄), executeServerAction/useDeleteDialog/useStatsLoader 3개 훅 생성 완료 | -| 2026-02-09 | Phase 3 프로토타입 검증 완료 - createCrudService 팩토리 생성, RankManagement 5/5 CRUD 정상, Server Action 호환성 확인 | -| 2026-02-10 | Phase 4 SearchableSelectionModal 완료 - 5개 모달 통합, 464줄 절감(-44%), 가이드 문서 작성 | -| 2026-02-10 | Phase 3 완료 - 전수 분석 후 팩토리 ROI 재평가(~6%), 공용 유틸 추출로 전략 전환. PaginatedApiResponse 25+파일 타입 통합, 공용 룩업 헬퍼 4파일 중복 제거, ~256줄 절감 | - ---- - -**모든 Phase 프론트 단독 가능** - 백엔드 의존성 없음 diff --git a/claudedocs/architecture/[PLAN-2026-03-11] dynamic-multi-tenant-page-system.md b/claudedocs/architecture/[PLAN-2026-03-11] dynamic-multi-tenant-page-system.md deleted file mode 100644 index c8bce86a..00000000 --- a/claudedocs/architecture/[PLAN-2026-03-11] dynamic-multi-tenant-page-system.md +++ /dev/null @@ -1,950 +0,0 @@ -# 동적 멀티테넌트 페이지 시스템 설계 - -> 작성일: 2026-03-11 -> 최종 업데이트: 2026-03-18 -> 상태: 초안 (백엔드 논의 진행 중) -> 관련 문서: -> - `[VISION-2026-02-19] dynamic-rendering-platform-strategy.md` -> - `[PLAN-2026-02-06] multi-tenancy-optimization-roadmap.md` -> - `[DESIGN-2026-02-11] dynamic-field-type-extension.md` -> - `[ANALYSIS-2026-03-17] tenant-module-separation-dependency-audit.md` -> - `[PLAN-2026-03-17] tenant-module-separation-plan.md` — **본 설계의 실행 계획 (Phase 0~3)** - ---- - -## 1. 핵심 목표 - -``` -현재: 테넌트(업종)별 페이지를 하드코딩 → 신규 테넌트마다 개발 필요 -목표: 백엔드 기준관리에서 설정 → JSON API → 프론트 동적 렌더링 -결과: 프론트엔드 코드 변경 0줄로 새 테넌트 대응 -``` - ---- - -## 2. 전체 아키텍처 - -``` -┌─────────────────────────────────────────────────────────┐ -│ 백엔드 어드민 (mng) │ -│ ┌───────────────────────────────────────────────────┐ │ -│ │ 기준관리 페이지 │ │ -│ │ 레이아웃 / 섹션 / 항목 / 속성 등록 │ │ -│ └───────────────┬───────────────────────────────────┘ │ -│ │ 저장 │ -│ ↓ │ -│ ┌───────────────────────────────────────────────────┐ │ -│ │ DB (테넌트별 페이지 config) │ │ -│ └───────────────┬───────────────────────────────────┘ │ -│ │ API │ -└──────────────────┼──────────────────────────────────────┘ - ↓ -┌──────────────────────────────────────────────────────────┐ -│ 프론트엔드 (Next.js) │ -│ │ -│ ┌────────────┐ ┌─────────────────────────────────┐ │ -│ │ 정적 페이지 │ │ 동적 페이지 │ │ -│ │ - 로그인 │ │ - catch-all route │ │ -│ │ - 회원가입 │ │ - JSON config → 동적 렌더링 │ │ -│ │ - 404 등 │ │ - pageType별 렌더러 선택 │ │ -│ └────────────┘ └─────────────────────────────────┘ │ -│ │ │ │ -│ └──── 공유 컴포넌트 ───────┘ │ -│ (ui/, molecules/, organisms/) │ -└──────────────────────────────────────────────────────────┘ -``` - ---- - -## 3. 규칙 정의 - -### 규칙 1: 기준관리 → 백엔드 어드민 - -| 항목 | 내용 | -|------|------| -| 현재 | 프론트 `ItemMasterDataManagement` 등에서 기준관리 | -| 변경 | 백엔드 어드민(mng) 페이지로 이동 | -| 이유 | 프론트 번들 크기 감소, 설정 변경 = 배포 불필요 | -| 담당 | 🔵 백엔드 | - -``` -Before: 프론트 기준관리 UI → 프론트 API 호출 → DB 저장 -After: 백엔드 어드민 UI → 직접 DB 저장 → API로 config 전달 -``` - ---- - -### 규칙 2: 페이지 정보를 JSON API로 제공 - -| 항목 | 내용 | -|------|------| -| 방식 | 메뉴 API처럼 페이지 config도 JSON API로 제공 | -| 엔드포인트 | `GET /api/v1/page-configs/{slug}` (제안) | -| 응답 | 페이지 타입, 레이아웃, 섹션, 필드, 검증규칙, API 매핑 등 | -| 담당 | 🔵 백엔드 API 설계 | - -**페이지 config JSON 구조 (제안)**: - -```jsonc -{ - "pageId": "sales-order-list", - "pageType": "list", // list | detail | form | dashboard | document - "title": "수주 관리", - "slug": "sales/order-management", - - // --- 규칙 11: API 엔드포인트 매핑 --- - "api": { - "list": "/api/v1/orders", - "detail": "/api/v1/orders/:id", - "create": "/api/v1/orders", - "update": "/api/v1/orders/:id", - "delete": "/api/v1/orders/:id" - }, - - // --- 규칙 4: 레이아웃 > 섹션 > 항목 > 속성 --- - "layout": { - "sections": [ - { - "sectionId": "filters", - "sectionType": "filter", - "fields": [ - { - "fieldId": "status", - "type": "select", - "label": "상태", - "options": [ - { "value": "all", "label": "전체" }, - { "value": "pending", "label": "대기" }, - { "value": "confirmed", "label": "확정" } - ], - "defaultValue": "all" - }, - { - "fieldId": "dateRange", - "type": "dateRange", - "label": "기간" - } - ] - }, - { - "sectionId": "table", - "sectionType": "dataTable", - "columns": [ - { "key": "orderNo", "label": "수주번호", "width": 120 }, - { "key": "clientName", "label": "거래처명", "width": 150 }, - { "key": "amount", "label": "금액", "type": "currency", "align": "right" }, - { "key": "status", "label": "상태", "type": "badge" } - ], - "actions": ["view", "edit", "delete"], - "pagination": true - } - ] - }, - - // --- 규칙 12: 검증 규칙 --- - "validation": { - "quantity": { "required": true, "min": 1, "message": "1 이상 입력하세요" }, - "clientId": { "required": true, "message": "거래처를 선택하세요" } - }, - - // --- 규칙 13: 필드 간 의존성 --- - "dependencies": [ - { - "type": "visibility", - "when": { "field": "itemType", "equals": "motor" }, - "show": ["motorSpec", "voltage"] - }, - { - "type": "computed", - "target": "amount", - "formula": "quantity * unitPrice" - }, - { - "type": "cascade", - "source": "category1", - "target": "category2", - "api": "/api/v1/categories/:parentId/children" - } - ], - - // --- 규칙 14: 권한 --- - "permissions": { - "fieldLevel": { - "unitPrice": { "view": ["admin", "sales_manager"], "edit": ["admin"] } - }, - "actionLevel": { - "delete": ["admin"], - "export": ["admin", "sales_manager"] - } - } -} -``` - -> ⚠️ **백엔드 논의 필요**: JSON 구조의 세부 스펙 확정 - -#### 2-2. 백엔드 저장 방식: JSONB (확정) - -> ✅ **확정**: 페이지 config는 PostgreSQL **JSONB** 타입으로 저장 - -| 항목 | JSON | JSONB (채택) | -|------|------|:---:| -| 저장 형태 | 텍스트 그대로 | 바이너리 (파싱된 형태) | -| 읽기 속도 | 매번 파싱 필요 | 이미 파싱됨 → **빠름** | -| 인덱싱 | ❌ 불가 | ✅ **GIN 인덱스 가능** | -| 내부 검색 | ❌ 전체 꺼내서 비교 | ✅ **특정 키/값으로 쿼리** | -| 부분 수정 | ❌ 전체 교체 | ✅ **특정 키만 업데이트** | - -**JSONB가 필요한 이유 — 우리 시스템과의 연관**: - -```sql --- 1. 테넌트별 특정 타입 페이지만 조회 (인덱싱) -SELECT * FROM page_configs -WHERE tenant_id = 282 -AND config->>'pageType' = 'list'; - --- 2. 특정 필드 타입을 쓰는 페이지 검색 (내부 검색) -SELECT * FROM page_configs -WHERE config @> '{"layout":{"sections":[{"fields":[{"type":"reference"}]}]}}'; - --- 3. 기준관리에서 섹션 하나만 수정 (부분 수정) -UPDATE page_configs -SET config = jsonb_set(config, '{layout,sections,0,title}', '"수정된 섹션명"'); -``` - -**JSONB 채택이 config 구조 설계에 미치는 영향**: - -| 영향 | 설명 | -|------|------| -| **구조 단순화** | 하나의 큰 JSONB에 전체 config를 담아도 부분 쿼리/수정 가능 → 테이블 분리 최소화 | -| **테넌트 분기** | JSONB 인덱스로 테넌트+pageType 조합 쿼리가 빠름 → 별도 테이블 불필요 | -| **기준관리 UI** | 섹션 하나만 수정해도 전체 config를 다시 저장할 필요 없음 → UX 향상 | -| **프론트 영향** | **없음** — 프론트는 동일한 JSON을 받아서 렌더링, 저장 방식 무관 | - -``` -DB 테이블 구조 (제안): - -page_configs -├── id (PK) -├── tenant_id (FK, 인덱스) -├── slug (UNIQUE per tenant, 인덱스) -├── config (JSONB) ← 페이지 config 전체 -├── created_at -└── updated_at - -GIN 인덱스: config에 대해 생성 → 내부 검색 고속화 -복합 인덱스: (tenant_id, slug) → 테넌트별 페이지 조회 최적화 -``` - -> ⚠️ **백엔드 논의 필요**: JSONB 기반 테이블 설계 세부 확정 (위 제안 구조 검토) - ---- - -### 규칙 3: 정적 페이지 vs 동적 페이지 분류 - -| 분류 | 정적 페이지 | 동적 페이지 | -|------|------------|------------| -| 정의 | 테넌트 무관, 고정 UI | 테넌트 config 기반 동적 생성 | -| 예시 | 로그인, 회원가입, 404, 500 | 수주관리, 품목관리, 공정관리 등 | -| 라우팅 | 기존 파일 기반 라우트 | catch-all `[...slug]` | -| 컴포넌트 | 직접 코딩 | JSON → 동적 렌더러 | -| 변경 빈도 | 거의 없음 | 테넌트별/설정별 수시 변경 | - -**정적 페이지 목록 (확정)**: - -| 경로 | 페이지 | 이유 | -|------|--------|------| -| `/login` | 로그인 | 인증 전 접근, 공통 UI | -| `/signup` | 회원가입 | 인증 전 접근, 공통 UI | -| `/404` | Not Found | 에러 페이지 | -| `/500` | Server Error | 에러 페이지 | -| `/settings/*` | 설정 | 시스템 설정은 공통 | - -> ⚠️ **논의 필요**: 설정 페이지 중 일부(구독, 결제)도 동적 대상인지? -> ⚠️ **논의 필요**: 대시보드는 동적 페이지? 위젯 기반 별도 시스템? - ---- - -### 규칙 4: 계층 구조 — 레이아웃 > 섹션 > 항목 > 속성 - -``` -Page (pageType에 의해 렌더러 결정) - └─ Layout (전체 레이아웃: single-column, two-column, tabs 등) - └─ Section (논리적 그룹: 기본정보, 상세정보, 테이블 등) - └─ Field (개별 입력 항목: input, select, date 등) - └─ Attribute (필드의 속성: label, placeholder, validation 등) -``` - -| 계층 | 역할 | 기준관리 등록 항목 | 프론트 컴포넌트 | -|------|------|-------------------|----------------| -| Layout | 전체 배치 | 레이아웃 타입 선택 | `DynamicPageLayout` | -| Section | 논리적 그룹 | 섹션 추가/순서/조건부 표시 | `DynamicSection` | -| Field | 개별 항목 | 필드 타입/라벨/기본값 | `DynamicFieldRenderer` (14종) | -| Attribute | 필드 속성 | 검증규칙/옵션/의존성 | props로 전달 | - ---- - -### 규칙 5: 컴포넌트 책임 분리 - -``` -┌─────────────────────────────────────────────────┐ -│ 상위: 데이터 처리 컴포넌트 (Layout, Section) │ -│ - API 호출 / 데이터 가공 │ -│ - 조건부 표시 로직 │ -│ - props 전달 / 이벤트 핸들링 │ -│ - Zustand store 구독 │ -└──────────────────┬──────────────────────────────┘ - │ props (순수 데이터) - ↓ -┌─────────────────────────────────────────────────┐ -│ 하위: 순수 기능 컴포넌트 (Field, Attribute) │ -│ - UI 렌더링만 담당 │ -│ - 외부 의존성 없음 │ -│ - value + onChange 패턴 │ -│ - 테스트 용이 │ -└─────────────────────────────────────────────────┘ -``` - -| 구분 | 상위 (Layout/Section) | 하위 (Field/Attribute) | -|------|----------------------|----------------------| -| 역할 | 데이터 처리, 조건 분기 | 순수 렌더링 | -| 상태 | Zustand 구독 | props only | -| API | 호출 가능 | 호출 안 함 | -| 예시 | `DynamicSection`, `DynamicListPage` | `Input`, `Select`, `DatePicker` | -| 테스트 | 통합 테스트 | 단위 테스트 | - ---- - -### 규칙 6: Zustand 기반 상태 관리 - -``` -┌────────────────────────────────────────────────┐ -│ pageConfigStore (Zustand) │ -│ │ -│ state: │ -│ configs: Map │ -│ currentPage: PageConfig | null │ -│ loading: boolean │ -│ │ -│ actions: │ -│ fetchPageConfig(slug) → API 호출 + 캐시 │ -│ invalidateConfig(slug) → 캐시 무효화 │ -│ subscribeToPage(slug) → 실시간 구독 │ -└────────────────────────────────────────────────┘ - │ - │ 구독 - ↓ -┌────────────────┐ ┌────────────────┐ -│ DynamicListPage │ │ DynamicFormPage │ ... -└────────────────┘ └────────────────┘ -``` - -| 항목 | 설명 | -|------|------| -| Store 위치 | `src/stores/pageConfigStore.ts` (신규) | -| 캐시 전략 | 메모리(Zustand) → localStorage → API | -| 변경 감지 | 해시 비교 (메뉴 갱신과 동일 방식) | -| 테넌트 격리 | 기존 `TenantAwareCache` 패턴 재사용 | - ---- - -### 규칙 7: 테넌트 + 하위 구성요소별 화면 분기 - -``` -테넌트 A (셔터 제조업) - ├─ 메뉴: 품목관리, 생산관리, 출하관리 - ├─ 품목 폼: 셔터 규격 필드 포함 - └─ 생산 공정: 셔터 전용 공정 단계 - -테넌트 B (건설업) - ├─ 메뉴: 프로젝트관리, 공사관리, 기성관리 - ├─ 프로젝트 폼: 현장정보 필드 포함 - └─ 공사 공정: 건설 전용 단계 - -같은 테넌트 내에서도: - ├─ 부서 A → 메뉴 5개, 필드 20개 표시 - └─ 부서 B → 메뉴 3개, 필드 12개 표시 -``` - -| 분기 기준 | 설명 | 예시 | -|----------|------|------| -| 테넌트 (company) | 업종별 전체 화면 구성 | 셔터업 vs 건설업 | -| 부서 (department) | 같은 테넌트 내 부서별 | 영업팀 vs 생산팀 | -| 역할 (role) | 같은 부서 내 역할별 | 관리자 vs 일반 | -| 사용자 (user) | 개인 설정 | 즐겨찾기, 컬럼 순서 | - -> ⚠️ **백엔드 논의 필요**: 분기 우선순위 및 상속 정책 -> (테넌트 설정 → 부서 설정으로 오버라이드 → 사용자 설정으로 오버라이드?) - ---- - -### 규칙 8: 정적/동적 컴포넌트 공유 - -``` -src/components/ - ├── ui/ ← 공유 (정적+동적 모두 사용) - │ ├── Input.tsx - │ ├── Select.tsx - │ ├── DatePicker.tsx - │ └── ... - │ - ├── molecules/ ← 공유 - │ ├── FormField.tsx - │ ├── SearchFilter.tsx - │ └── ... - │ - ├── organisms/ ← 공유 - │ ├── DataTable.tsx - │ ├── MobileCard.tsx - │ └── ... - │ - ├── dynamic/ ← 동적 전용 (신규) - │ ├── renderers/ - │ │ ├── DynamicListPage.tsx - │ │ ├── DynamicDetailPage.tsx - │ │ ├── DynamicFormPage.tsx - │ │ └── DynamicDashboardPage.tsx - │ ├── sections/ - │ │ ├── DynamicSection.tsx - │ │ ├── DynamicFilterSection.tsx - │ │ └── DynamicTableSection.tsx ← 기존 이동 - │ ├── fields/ - │ │ └── DynamicFieldRenderer.tsx ← 기존 이동 (14종) - │ └── store/ - │ └── pageConfigStore.ts - │ - └── static/ ← 정적 전용 (기존 유지) - ├── auth/LoginPage.tsx - └── auth/SignupPage.tsx -``` - -| 레이어 | 공유 여부 | 예시 | -|--------|----------|------| -| ui/ | ✅ 100% 공유 | Input, Select, Button | -| molecules/ | ✅ 100% 공유 | FormField, StatusBadge | -| organisms/ | ✅ 대부분 공유 | DataTable, SearchFilter | -| dynamic/renderers/ | ❌ 동적 전용 | DynamicListPage | -| 기존 도메인 컴포넌트 | ❌ 정적 전용 (점진적 전환) | OrderSalesDetailEdit | - ---- - -### 규칙 9: 페이지 타입 분류 체계 - -| pageType | 용도 | 핵심 구성 요소 | 기존 대응 패턴 | -|----------|------|--------------|---------------| -| `list` | 목록 조회 | 필터 + 테이블 + 페이지네이션 + 액션 | UniversalListPage | -| `detail` | 상세 보기 | 읽기전용 섹션 + 수정/삭제 버튼 | IntegratedDetailTemplate | -| `form` | 등록/수정 | 입력 섹션 + 저장/취소 | DynamicItemForm (범용화) | -| `dashboard` | 대시보드 | 위젯/카드 그리드 | CEODashboard | -| `document` | 문서/프린트 | 프린트 레이아웃 + 결재란 | ContractDocument 등 | - -``` -pageType 결정 흐름: - -API 응답의 pageType 값 - │ - ├─ "list" → - ├─ "detail" → - ├─ "form" → - ├─ "dashboard" → - ├─ "document" → - └─ 미지원 → (에러 표시) -``` - ---- - -### 규칙 10: 동적 라우팅 전략 - -``` -src/app/[locale]/(protected)/ - │ - ├── (static-pages)/ ← 정적 페이지 그룹 - │ ├── settings/ - │ └── ... - │ - └── [...slug]/ ← 동적 페이지 catch-all - └── page.tsx ← 아래 로직 수행 -``` - -**catch-all page.tsx 동작 흐름**: - -``` -1. URL에서 slug 추출 (예: ["sales", "order-management"]) -2. slug로 pageConfigStore에서 config 조회 (캐시 우선) -3. 캐시 없으면 → API 호출: GET /api/v1/page-configs/sales/order-management -4. config.pageType으로 렌더러 선택 -5. 렌더러에 config 전달 → 동적 페이지 렌더링 -``` - -| 라우트 우선순위 | 경로 | 설명 | -|---------------|------|------| -| 1 (최우선) | `/login`, `/signup` | 정적 페이지 (파일 존재) | -| 2 | `/settings/*` | 정적 그룹 (파일 존재) | -| 3 (폴백) | `/*` (나머지 전부) | catch-all → 동적 처리 | - -> Next.js 라우팅 규칙: 구체적 경로 > catch-all → 충돌 없음 - -> ⚠️ **논의 필요**: 기존 정적 페이지를 동적으로 전환 시, 해당 파일 삭제 후 catch-all로 자연스럽게 이관 - ---- - -### 규칙 11: API 엔드포인트 동적 매핑 - -#### 11-1. API 호출 유형 - -| API 유형 | config 키 | 용도 | -|---------|----------|------| -| `list` | `api.list` | 목록 조회 (GET) | -| `detail` | `api.detail` | 상세 조회 (GET) | -| `create` | `api.create` | 등록 (POST) | -| `update` | `api.update` | 수정 (PUT/PATCH) | -| `delete` | `api.delete` | 삭제 (DELETE) | -| `export` | `api.export` | 엑셀 다운로드 (GET) | -| `custom` | `api.custom[actionName]` | 커스텀 액션 | - -#### 11-2. 백엔드 API 제공 방식 (3가지 방향) - -동적 페이지는 **데이터를 어디서 가져올지도 동적**이어야 합니다. -백엔드가 API를 어떤 방식으로 제공하느냐에 따라 3가지 방향이 있습니다. - -| 방향 | 설명 | 장점 | 단점 | -|------|------|------|------| -| **A. 개별 API** | 페이지마다 전용 API 존재, config에 경로 명시 | 기존 API 재사용, 복잡한 로직 처리 가능 | 새 페이지마다 백엔드 개발 필요 | -| **B. 범용 Entity API** | 하나의 엔드포인트가 entityType으로 분기 | 새 페이지 추가 시 백엔드 코드 변경 없음 | 복잡한 비즈니스 로직 처리 어려움 | -| **C. 하이브리드 (권장)** | 단순 CRUD는 범용 API, 복잡한 로직은 전용 API | 양쪽 장점 모두 취함 | 두 방식 공존에 따른 관리 비용 | - -**방향 A: 개별 API (config에 경로 포함)** -```jsonc -{ - "pageType": "list", - "slug": "sales/order-management", - "api": { - "list": "/api/v1/orders", - "detail": "/api/v1/orders/:id", - "create": "/api/v1/orders", - "delete": "/api/v1/orders/:id" - } -} -``` -→ 기존에 이미 만들어둔 API를 그대로 config에 연결 -→ 견적 계산, 세금 처리 등 **비즈니스 로직이 있는 페이지에 적합** - -**방향 B: 범용 Entity API** -```jsonc -{ - "pageType": "list", - "slug": "master/equipment", - "entityType": "equipment" -} -``` -``` -// 범용 API 1개로 모든 entity 처리 -GET /api/v1/entities/{entityType} -GET /api/v1/entities/{entityType}/{id} -POST /api/v1/entities/{entityType} -PUT /api/v1/entities/{entityType}/{id} -DELETE /api/v1/entities/{entityType}/{id} -``` -→ 백엔드에서 entityType에 따라 테이블/모델 동적 매핑 -→ 단순 CRUD(거래처, 설비, 자재 등) **마스터 데이터 페이지에 적합** - -**방향 C: 하이브리드 (권장)** -``` -┌──────────────────────────────────────────────────┐ -│ 단순 CRUD 페이지 (거래처, 설비, 자재 등) │ -│ → 방향 B: 범용 entity API │ -│ → config에 entityType만 지정 │ -│ → 새 페이지 추가 시 백엔드 코드 변경 없음 │ -│ → 동적 시스템의 최대 효과 │ -└──────────────────────────────────────────────────┘ - -┌──────────────────────────────────────────────────┐ -│ 비즈니스 로직 페이지 (견적, 생산, 세금계산서) │ -│ → 방향 A: 전용 API 경로를 config에 명시 │ -│ → 계산/검증/워크플로우 등 복잡한 로직 처리 │ -│ → 기존 API 재사용으로 마이그레이션 용이 │ -└──────────────────────────────────────────────────┘ -``` - -#### 11-3. 프론트 처리 방식 (어느 방향이든 동일) - -``` -프론트는 API 제공 방식에 무관하게 동일한 패턴으로 처리: - -1. config에서 API 경로 결정 - ├─ api.list 있으면 → 그 경로 사용 (방향 A) - └─ entityType 있으면 → `/api/v1/entities/${entityType}` 생성 (방향 B) - ↓ -2. buildApiUrl(경로, params) ← 기존 유틸 재사용 - ↓ -3. Server Action에서 API 프록시 호출 - ↓ -4. 응답을 config.columns 기준으로 렌더링 -``` - -```typescript -// 프론트 API 경로 결정 유틸 (예시) -function resolveApiUrl(config: PageConfig, action: 'list' | 'detail' | 'create' | 'update' | 'delete') { - // 방향 A: 전용 API 경로가 있으면 사용 - if (config.api?.[action]) { - return config.api[action]; - } - // 방향 B: entityType으로 범용 API 생성 - if (config.entityType) { - const base = `/api/v1/entities/${config.entityType}`; - if (action === 'list' || action === 'create') return base; - return `${base}/:id`; - } - throw new Error(`No API config for action: ${action}`); -} -``` - -#### 11-4. API 응답 구조 통일 - -어느 방향이든 **응답 구조는 통일**되어야 프론트가 범용 처리 가능: - -| API 유형 | 응답 구조 | -|---------|----------| -| list | `{ data: [...], meta: { total, current_page, per_page, last_page } }` | -| detail | `{ data: { ... } }` | -| create | `{ data: { id, ... }, message: "..." }` | -| update | `{ data: { id, ... }, message: "..." }` | -| delete | `{ message: "..." }` | - -> ⚠️ **백엔드 논의 필요**: -> - 범용 entity API 도입 여부 및 범위 -> - 기존 API 중 응답 구조가 통일되지 않은 것 정리 -> - 전용 API와 범용 API의 분류 기준 합의 - ---- - -### 규칙 12: 검증(Validation) 규칙 - -| 검증 타입 | JSON 표현 | 프론트 변환 | -|----------|----------|------------| -| 필수값 | `{ "required": true }` | `z.string().min(1)` | -| 최솟값 | `{ "min": 1 }` | `z.number().min(1)` | -| 최댓값 | `{ "max": 100 }` | `z.number().max(100)` | -| 정규식 | `{ "pattern": "^\\d{3}-\\d{2}$" }` | `z.string().regex()` | -| 커스텀 메시지 | `{ "message": "올바른 형식이 아닙니다" }` | 에러 메시지 | -| 이메일 | `{ "type": "email" }` | `z.string().email()` | -| 전화번호 | `{ "type": "phone" }` | `z.string().regex()` | - -``` -JSON validation config - ↓ 런타임 변환 -Zod 스키마 자동 생성 - ↓ -react-hook-form zodResolver에 주입 - ↓ -폼 검증 자동 적용 -``` - ---- - -### 규칙 13: 필드 간 의존성 - -| 의존성 타입 | 설명 | 예시 | -|------------|------|------| -| `visibility` | 조건부 표시/숨김 | 품목타입=모터 → 전압 필드 표시 | -| `computed` | 자동 계산 | 수량 × 단가 = 금액 | -| `cascade` | 연쇄 선택 | 대분류 → 중분류 → 소분류 | -| `setValue` | 값 자동 설정 | 거래처 선택 → 담당자 자동 입력 | -| `disable` | 조건부 비활성화 | 상태=확정 → 수량 수정 불가 | - -``` -기존 자산 활용: -DynamicItemForm의 DisplayCondition → visibility 타입으로 범용화 -DynamicItemForm의 ComputedField → computed 타입으로 범용화 -``` - -> ✅ **확정**: 복잡한 계산식(견적 할인율 등)은 **백엔드에서 전부 처리**하여 결과만 전달 - ---- - -### 규칙 14: 권한 통합 - -#### 14-1. 현재 권한 시스템 검증 결과 - -✅ **현재 권한 시스템으로 동적 페이지도 컨트롤 가능** (검증 완료) - -현재 권한 시스템이 **메뉴 ID 기반 + URL 패턴 매칭**으로 동작하므로, 페이지가 정적이든 동적이든 해당 URL이 menu 테이블에 등록되어 있으면 권한 관리 페이지에서 동일하게 컨트롤됩니다. - -``` -현재 (정적 페이지): - 백엔드 menu 테이블에 URL 등록 → 권한 매트릭스 체크박스 on/off - → PermissionGate가 URL 매칭 → 접근 허용/차단 - -동적 페이지도 동일: - 백엔드 menu 테이블에 동적 페이지 URL(slug) 등록 - → 권한 매트릭스에서 동일하게 체크박스 on/off - → PermissionGate가 URL 매칭 → 동일하게 동작 -``` - -#### 14-2. 권한 레벨별 동적 페이지 호환성 - -| 권한 레벨 | 현재 지원 | 동적 페이지 호환 | 사용 컴포넌트 | -|----------|:---:|:---:|------| -| 페이지 접근 (view) | ✅ | ✅ | `PermissionGate` (URL 매칭) | -| 생성 (create) | ✅ | ✅ | `usePermission()` / `PermissionGuard` | -| 수정 (update) | ✅ | ✅ | `usePermission()` / `PermissionGuard` | -| 삭제 (delete) | ✅ | ✅ | `usePermission()` / `PermissionGuard` | -| 승인 (approve) | ✅ | ✅ | `usePermission()` / `PermissionGuard` | -| 내보내기 (export) | ✅ | ✅ | `usePermission()` / `PermissionGuard` | -| 관리 (manage) | ✅ | ✅ | `usePermission()` / `PermissionGuard` | -| **필드 단위 권한** | ❌ | ❌ | 현재 미지원 → **v2 고려사항** | - -#### 14-3. 권한 적용 흐름 - -``` -권한 적용 흐름 (정적/동적 공통): - -1. 페이지 접근: PermissionGate → URL longest prefix 매칭 → view 권한 확인 -2. 액션 권한: usePermission() → canCreate/canDelete 등 → 버튼 표시/숨김 -3. 필드 권한: 현재 미지원 (v2에서 config.permissions.fieldLevel 추가 시 구현) -``` - -> ⚠️ **백엔드 논의 필요**: 동적 페이지 URL(slug)을 menu 테이블에 자동 등록하는 방안 -> (기준관리에서 페이지 생성 시 → menu 테이블에도 자동 연동?) - ---- - -### 규칙 15: 캐싱 & 성능 전략 - -``` -요청 흐름: - -1차 캐시 (Zustand 메모리) - ↓ miss -2차 캐시 (localStorage, 테넌트별 격리) - ↓ miss -3차 (API 호출) - ↓ 응답 -1차 + 2차 캐시 갱신 -``` - -| 전략 | 방법 | 갱신 주기 | -|------|------|----------| -| 초기 로드 | 로그인 시 전체 config 프리페치 | 1회 | -| 변경 감지 | 해시 비교 (메뉴 갱신과 동일) | 30초~5분 | -| 강제 갱신 | 관리자가 기준관리 변경 시 push | 즉시 | -| 캐시 무효화 | 테넌트 전환 시 전체 클리어 | 즉시 | - ---- - -### 규칙 16: 비즈니스 로직 처리 - -> ✅ **확정**: 복잡한 계산 수식은 **백엔드에서 전부 처리**하여 결과만 전달 - -| 로직 복잡도 | 처리 방식 | 예시 | -|------------|----------|------| -| 단순 계산 | config formula (프론트) | 수량 × 단가 = 금액 | -| 복잡한 계산 | **백엔드 API** | 견적 할인, 세금, 재고 검증 등 | - -```jsonc -// config에서 로직 지정 -{ - "businessLogic": { - // 단순: 프론트 formula (기존 ComputedField 재사용) - "amount": { "type": "formula", "expression": "quantity * unitPrice" }, - - // 복잡: 백엔드 위임 (확정) - "totalDiscount": { - "type": "api", - "endpoint": "/api/v1/quotes/:id/calculate-discount", - "trigger": "onFieldChange", - "watchFields": ["quantity", "unitPrice", "discountRate"] - } - } -} -``` - -프론트는 단순 사칙연산(ComputedField)만 담당하고, 그 외 모든 비즈니스 로직은 백엔드 API로 위임합니다. - ---- - -### 규칙 17: 점진적 마이그레이션 전략 - -#### 17-1. 3단계 아키텍처 방향 (2026-03-17 확인) - -``` -1단계: 현재 → 모듈 분리 - - 공통 ERP / 테넌트별 모듈 물리적 분리 - - 선결과제 해소 (아래 17-2 참조) - -2단계: 모듈 분리 → JSON 동적 조립 - - 테넌트 모듈을 manifest/JSON 기반으로 전환 - - 동적 페이지 렌더러 도입 - -3단계: 최종 — 빈 페이지 셸 + 백엔드 JSON으로 페이지 자동 조립 - - 이 문서의 최종 목표 -``` - -#### 17-2. 선결과제 (모듈 분리 전 해결 필수) - -| # | 과제 | 내용 | 예상 | -|---|------|------|------| -| 1 | CEO 대시보드 테넌트 의존성 해소 | 생산/건설 섹션 직접 import → 동적 로딩 전환 | - | -| 2 | 공유 컴포넌트 추출 | 결재/영업(공통)이 생산(경동) 코드 직접 import | - | -| 3 | 라우트 가드 추가 | 테넌트 미보유 모듈 URL 직접 접근 차단 | - | -| 4 | dashboard-invalidation 동적화 | production/construction 도메인 키 하드코딩 제거 | - | - -> 선결과제 해소 예상: 3~4일, 이후 모듈 분리 본작업은 별도 산정 - -**핵심 의존성 위반 (공통 → 테넌트 방향, 수정 필요)**: -``` -ApprovalBox → production/InspectionReportModal -Sales/production-orders → production/ProductionOrders (actions+types+UI) -Sales → router.push("/production/work-orders") 하드코딩 -CEODashboard → DailyProductionSection, ConstructionSection 직접 import -dashboard-invalidation.ts → production/construction 도메인 키 -``` - -**안전한 부분**: -- 테넌트 간 교차 의존성 없음 (생산↔건설 = 0) -- 건설(주일) 모듈 완전 독립 → 바로 분리 가능 -- Zustand 스토어, API 프록시, 메뉴 시스템은 무관 - -#### 17-3. 테넌트별 페이지 현황 (2026-03-17 분석) - -| 테넌트 | 업종 | 전용 모듈 | 페이지 수 | -|--------|------|----------|:---:| -| 공통 ERP | 전 업종 | 회계, 인사, 결재, 게시판, 설정, 고객센터 등 | ~165 | -| 경동 | 셔터 제조 (MES) | 생산, 품질관리 | ~27 | -| 주일 | 건설 시공 | 건설/프로젝트, 입찰, 기성 | ~48 | -| (옵션) | - | 차량관리 | ~13 | - -#### 17-4. 마이그레이션 Phase - -| Phase | 범위 | 예상 기간 | 상태 | -|-------|------|----------|------| -| **선결과제** | 의존성 해소 (17-2) | 3-4일 | ⏳ 준비 | -| **Phase 0** | 인프라 구축 | 2-3주 | ⏳ | -| | - catch-all 라우터 | | | -| | - pageConfigStore | | | -| | - DynamicListPage/FormPage 렌더러 | | | -| | - 백엔드 page-config API | | | -| **Phase 1** | 신규 테넌트/페이지만 동적 | 2-4주 | ⏳ | -| | - 새로 추가되는 페이지는 동적으로 생성 | | | -| | - 기존 페이지는 그대로 유지 | | | -| **Phase 2** | 단순 CRUD 페이지 전환 | 4-6주 | ⏳ | -| | - 리스트+상세만 있는 단순 페이지 | | | -| | - 거래처관리, 설비관리 등 | | | -| **Phase 3** | 복잡한 비즈니스 페이지 전환 | 6-8주 | ⏳ | -| | - 견적, 수주, 생산 등 로직 있는 페이지 | | | -| **Phase 4** | 기존 정적 → 동적 완전 전환 | 지속적 | ⏳ | -| | - 남은 하드코딩 페이지 점진적 전환 | | | - -``` -전환 판단 기준: - -[선행] 선결과제 해소 (의존성 분리) → 선결과제 Phase -[쉬움] 순수 CRUD (리스트+폼) → Phase 2에서 전환 -[보통] CRUD + 단순 계산 → Phase 2~3 -[어려움] 복잡한 비즈니스 로직 → Phase 3 -[마지막] 문서/프린트, 대시보드 → Phase 4 -``` - ---- - -## 4. 이미 있는 자산 → 재사용 매핑 - -| 기존 자산 | 현재 용도 | 동적 시스템에서의 역할 | -|----------|----------|---------------------| -| DynamicFieldRenderer (14종) | 품목 폼 필드 | → 모든 동적 폼 필드 | -| DynamicTableSection | 품목 BOM 테이블 | → 모든 동적 테이블 | -| DisplayCondition | 품목 조건부 표시 | → 범용 visibility 규칙 | -| ComputedField | 품목 자동 계산 | → 범용 computed 규칙 | -| UniversalListPage | 리스트 페이지 템플릿 | → DynamicListPage 기반 | -| IntegratedDetailTemplate | 상세 페이지 템플릿 | → DynamicDetailPage 기반 | -| TenantAwareCache | 캐시 격리 | → pageConfigStore 캐시 | -| menuRefresh (해시 비교) | 메뉴 갱신 | → config 변경 감지 | -| buildApiUrl | URL 빌더 | → 동적 API 호출에 재사용 | - ---- - -## 5. 논의 현황 정리 - -### 확정 사항 - -| 항목 | 확정 내용 | 비고 | -|------|----------|------| -| API 제공 방식 | 하이브리드 (C) — 단순 CRUD는 범용, 복잡 로직은 전용 | 범용 API 세분화 가능성 있음 | -| 복잡한 계산 수식 | 백엔드에서 전부 처리, 결과만 전달 | 프론트는 단순 사칙연산만 | -| 권한 관리 호환성 | 현재 권한 시스템으로 동적 페이지 컨트롤 가능 | 메뉴 ID + URL 패턴 매칭 방식 | -| 기존 동적 필드 재사용 | DynamicFieldRenderer 14종 등 90%+ 재사용 가능 | 기준관리 UI가 mng로 이동해도 렌더링 컴포넌트 유지 | -| DB 저장 방식 | PostgreSQL **JSONB** 사용 | 인덱싱/부분수정/내부검색 가능, 프론트 영향 없음 | - -### 협의 필요 사항 - -| 항목 | 현재 상태 | 논의 포인트 | -|------|----------|------------| -| JSON config 세부 구조 | 제안 구조 작성됨 (규칙 2 참조) | 회의에서 세부 항목 결정 후 확정 | -| 정적/동적 페이지 분류 | 초안 목록 작성됨 (규칙 3 참조) | 어떤 페이지를 정적으로 남길지 최종 확정 | -| 테넌트 하위 분기 정책 | 개념 정리됨 (규칙 7 참조) | 테넌트→부서→역할 오버라이드 정책, config를 최종 결과물로 줄지 프론트가 조합할지 | -| 동적 라우팅 전략 | catch-all 방식 제안 (규칙 10 참조) | 기존 정적 페이지와의 공존/전환 전략 | -| 범용 entity API 범위 | 하이브리드 방향 합의 | 페이지 렌더링 분기에 따라 범용 API 세분화 가능 | -| page-config API 스펙 | 미정 | `GET /api/v1/page-configs/{slug}` 응답 구조 | -| 기준관리 어드민 UI | 미정 | mng에서 레이아웃/섹션/필드 등록 화면 설계 | -| API 응답 통일 | 미정 | list/detail/create/update/delete 응답 포맷 표준화 | -| 캐시 무효화 | 미정 | 기준관리 변경 시 프론트 캐시 갱신 방법 (polling vs push) | -| 프리페치 범위 | 미정 | 로그인 시 전체 config vs 페이지 접근 시 개별 로드 | -| 검증/의존성 JSON 스펙 | 제안 구조 작성됨 (규칙 12, 13 참조) | 세부 스펙 확정 | -| 마이그레이션 순서 | Phase 0~4 제안 (규칙 17 참조) | 어떤 페이지부터 동적 전환할지 | -| 동적 페이지 → menu 자동 등록 | 미정 | 기준관리에서 페이지 생성 시 menu 테이블 자동 연동 방안 | -| 필드 단위 권한 | 현재 미지원 | v2 고려사항 (필요 시 추가 개발) | - ---- - -## 6. 기존 자산 재사용 현황 - -### 즉시 재사용 가능 (코드 변경 없음) - -| 자산 | 현재 용도 | 동적 시스템 역할 | 재사용도 | -|------|----------|----------------|:---:| -| DynamicFieldRenderer (14종) | 품목 폼 필드 | 모든 동적 폼 필드 | 100% | -| DynamicTableSection | 품목 BOM 테이블 | 모든 동적 테이블 | 99% | -| DisplayCondition (9개 연산자) | 품목 조건부 표시 | 범용 visibility 규칙 | 100% | -| ComputedField | 품목 자동 계산 | 범용 단순 계산 | 100% | -| Reference Sources 프리셋 | 거래처/품목 등 조회 | 새 source 추가만으로 확장 | 100% | -| TenantAwareCache | 캐시 격리 | pageConfigStore 캐시 | 100% | -| menuRefresh (해시 비교) | 메뉴 갱신 | config 변경 감지 | 100% | -| buildApiUrl | URL 빌더 | 동적 API 호출 | 100% | -| PermissionGate / usePermission | 정적 페이지 권한 | 동적 페이지 권한 (동일) | 100% | - -### 범용화 필요 (약간의 리팩토링) - -| 자산 | 변경 사항 | -|------|----------| -| useDynamicFormState | API URL을 파라미터로 받도록 | -| useFormStructure | 품목 전용 API → 범용 API 경로 | -| types.ts | `ItemFieldResponse` → `DynamicFieldResponse` 리네이밍 | - -### 신규 개발 필요 - -| 자산 | 역할 | -|------|------| -| DynamicListPage | 동적 리스트 페이지 렌더러 (UniversalListPage 기반) | -| DynamicDetailPage | 동적 상세 페이지 렌더러 (IntegratedDetailTemplate 기반) | -| DynamicDashboardPage | 동적 대시보드 렌더러 | -| pageConfigStore | 페이지 config Zustand 스토어 | -| catch-all route | `[...slug]/page.tsx` 동적 라우터 | -| resolveApiUrl | API 경로 결정 유틸 (개별/범용 분기) | - ---- - -## 7. 관련 문서 - -| 문서 | 위치 | 내용 | -|------|------|------| -| 동적 렌더링 플랫폼 비전 | `claudedocs/architecture/[VISION-2026-02-19]` | 전체 비전 및 자산 현황 | -| 멀티테넌시 최적화 로드맵 | `claudedocs/architecture/[PLAN-2026-02-06]` | 테넌트 격리/최적화 8 Phase | -| 동적 필드 타입 설계 | `claudedocs/architecture/[DESIGN-2026-02-11]` | 4-Level 구조, 14종 필드 | -| 동적 필드 구현 현황 | `claudedocs/architecture/[IMPL-2026-02-11]` | Phase 1~3 프론트 구현 완료 | -| 백엔드 API 스펙 | `claudedocs/item-master/[API-REQUEST-2026-02-12]` | 동적 필드 타입 백엔드 요청서 | -| 테넌트 모듈 의존성 분석 | `claudedocs/architecture/[ANALYSIS-2026-03-17]` | 3테넌트 분리, 선결과제 4개, 의존성 위반 목록 | - ---- - -**문서 버전**: 1.3 -**마지막 업데이트**: 2026-03-18 -**다음 단계**: 백엔드 회의 → 협의 필요 항목 확정 → v2.0 작성 → `sam-docs/frontend/v2/`에 최종본 등록 diff --git a/claudedocs/architecture/[PLAN-2026-03-17] tenant-module-separation-plan.md b/claudedocs/architecture/[PLAN-2026-03-17] tenant-module-separation-plan.md deleted file mode 100644 index 11dd8e1c..00000000 --- a/claudedocs/architecture/[PLAN-2026-03-17] tenant-module-separation-plan.md +++ /dev/null @@ -1,1844 +0,0 @@ -# Tenant-Based Module Separation: Implementation Plan - -**Date**: 2026-03-17 -**Status**: APPROVED PLAN -**Prerequisite**: `[ANALYSIS-2026-03-17] tenant-module-separation-dependency-audit.md` -**Estimated Total Effort**: 12-16 working days across 4 phases -**Zero Downtime Requirement**: All changes are additive; no page removal until Phase 3 - -### 관련 문서 (로드맵 상 위치) - -| 문서 | 역할 | 관계 | -|------|------|------| -| `[VISION-2026-02-19] dynamic-rendering-platform-strategy.md` | 플랫폼 비전 | 최상위 방향성 | -| `[PLAN-2026-02-06] multi-tenancy-optimization-roadmap.md` | 멀티테넌시 로드맵 | 전체 단계 정의 | -| `[DESIGN-2026-02-11] dynamic-field-type-extension.md` | 동적 필드 타입 설계 | v3 렌더러 기초 | -| `[PLAN-2026-03-11] dynamic-multi-tenant-page-system.md` | **v3 최종 설계 (JSON 동적 페이지)** | **본 계획의 도착점** | -| `[ANALYSIS-2026-03-17] tenant-module-separation-dependency-audit.md` | 의존성 감사 | 본 계획의 근거 데이터 | -| **본 문서 (Phase 0~3)** | **프론트 모듈 분리 실행 계획** | **v3로 가는 징검다리** | - -``` -로드맵 전체 흐름: - -[VISION 02-19] 플랫폼 비전 - │ -[PLAN 02-06] 멀티테넌시 로드맵 - │ -[DESIGN 02-11] 동적 필드 타입 - │ -[PLAN 03-11] v3 최종 설계 (JSON 동적 페이지 + JSONB + pageType 렌더러) - │ -[ANALYSIS 03-17] 의존성 감사 ──→ 현재 코드의 모듈 간 결합 6건 발견 - │ -[PLAN 03-17] ★ 본 문서 ★ (Phase 0~3: 프론트 모듈 분리) - │ -[v2] 백엔드 page_configs JSONB API → useModules() 연결 - │ -[v3] catch-all route + DynamicListPage/FormPage 렌더러 - │ -[최종] 테넌트 추가 = 어드민 config 등록 → 코드 변경 0줄 -``` - ---- - -## Table of Contents - -1. [Architecture Overview](#1-architecture-overview) -2. [Phase 0: Prerequisite Fixes (4-5 days)](#2-phase-0-prerequisite-fixes) -3. [Phase 1: Module Registry & Route Guard (3-4 days)](#3-phase-1-module-registry--route-guard) -4. [Phase 2: Dashboard Decoupling (2-3 days)](#4-phase-2-dashboard-decoupling) -5. [Phase 3: Physical Separation (2-3 days)](#5-phase-3-physical-separation) -6. [Phase 4: Manifest-Based Module Loading (Future)](#6-phase-4-manifest-based-module-loading) -7. [Testing Strategy](#7-testing-strategy) -8. [Risk Register](#8-risk-register) -9. [Folder Structure Before/After](#9-folder-structure-beforeafter) -10. [Migration Order & Parallelism](#10-migration-order--parallelism) - ---- - -## 1. Architecture Overview - -### Current State - -``` -src/ - app/[locale]/(protected)/ - production/ (12 pages) -- Kyungdong tenant - quality/ (14 pages) -- Kyungdong tenant - construction/ (57 pages) -- Juil tenant - vehicle-management/ (12 pages) -- Optional module - accounting/ (32 pages) -- Common ERP - sales/ (22 pages) -- Common ERP (has 3 pages that import from production) - approval/ (6 pages) -- Common ERP (1 component imports from production) - ...other common modules - components/ - production/ (56 files) - quality/ (35+ files) - business/construction/ (161 files) - vehicle-management/ (13 files) - ...common components -``` - -### Target State (End of Phase 3) - -``` -src/ - modules/ # NEW: module registry + manifest - index.ts # module registry - types.ts # TenantModule, ModuleManifest types - tenant-config.ts # tenant -> module mapping - route-guard.ts # route access check utility - interfaces/ # NEW: shared type contracts - production-orders.ts # types+actions shared between sales & production - inspection-documents.ts # InspectionReportModal props interface - dashboard-sections.ts # dynamic section registration types - app/[locale]/(protected)/ - production/ # unchanged location, guarded by middleware - quality/ # unchanged location, guarded by middleware - construction/ # unchanged location, guarded by middleware - vehicle-management/ # unchanged location, guarded by middleware - components/ - document-system/modals/ # NEW: extracted shared modals - InspectionReportModal.tsx # moved from production - WorkLogModal.tsx # moved from production - production/ # unchanged, but no longer imported by common - quality/ # unchanged - business/construction/ # unchanged -``` - -### Dependency Direction Rules - -``` -ALLOWED: Tenant Module -----> Common ERP - (production -> @/lib/, @/components/ui/, @/hooks/, etc.) - -FORBIDDEN: Common ERP ----X---> Tenant Module - (approval -> production components) - (sales -> production actions/types) - (dashboard -> production/construction data) - -EXCEPTION: Common ERP ~~~?~~~> Tenant Module via: - 1. Dynamic import with fallback (lazy + error boundary) - 2. Shared interface in @/interfaces/ (types only) - 3. Module registry callback (runtime registration) -``` - ---- - -## 2. Phase 0: Prerequisite Fixes - -> **Goal**: Break all forbidden dependency arrows (Common -> Tenant) without changing runtime behavior. -> **Duration**: 4-5 days -> **Risk**: LOW (pure refactoring, no user-facing changes) -> **Rollback**: `git revert` each commit independently - -### Task 0.1: Extract InspectionReportModal to Shared Location - -**Problem**: `ApprovalBox/index.tsx` (common) imports `InspectionReportModal` from `production/WorkOrders/documents/`. Also `quality/qms/page.tsx` imports it. - -**Strategy**: Create a shared document modal system under `@/components/document-system/modals/`. - -**Files to create**: -- `src/components/document-system/modals/InspectionReportModal.tsx` -- `src/components/document-system/modals/WorkLogModal.tsx` -- `src/components/document-system/modals/index.ts` - -**Files to modify**: -- `src/components/approval/ApprovalBox/index.tsx` (change import path) -- `src/app/[locale]/(protected)/quality/qms/page.tsx` (change import path) -- `src/components/production/WorkOrders/documents/InspectionReportModal.tsx` (re-export from shared) -- `src/components/production/WorkOrders/documents/WorkLogModal.tsx` (re-export from shared) -- `src/components/production/WorkOrders/documents/index.ts` (update exports) - -**Code pattern**: -```typescript -// NEW: src/components/document-system/modals/InspectionReportModal.tsx -// Copy the full 570-line component here (from production/WorkOrders/documents/) -// Keep all existing props and behavior identical - -// MODIFY: src/components/production/WorkOrders/documents/InspectionReportModal.tsx -// Replace with re-export to avoid breaking internal production imports: -export { InspectionReportModal } from '@/components/document-system/modals/InspectionReportModal'; - -// MODIFY: src/components/approval/ApprovalBox/index.tsx line 76 -// FROM: -import { InspectionReportModal } from '@/components/production/WorkOrders/documents/InspectionReportModal'; -// TO: -import { InspectionReportModal } from '@/components/document-system/modals/InspectionReportModal'; - -// MODIFY: src/app/[locale]/(protected)/quality/qms/page.tsx lines 11-12 -// FROM: -import { InspectionReportModal } from '@/components/production/WorkOrders/documents'; -import { WorkLogModal } from '@/components/production/WorkOrders/documents'; -// TO: -import { InspectionReportModal } from '@/components/document-system/modals'; -import { WorkLogModal } from '@/components/document-system/modals'; -``` - -**Dependency analysis for InspectionReportModal.tsx (570 lines)**: -The modal imports from: -- `@/types/process.ts` (shared types, already in Common ERP) -- `@/lib/api/` utilities (Common ERP) -- `@/components/ui/` (Common ERP) -- `./inspection-shared` and `./Slat*Content`, `./Screen*Content`, `./Bending*Content` (production-internal) - -The production-internal content components (SlatInspectionContent, ScreenInspectionContent, etc.) are **rendered inside** InspectionReportModal via a switch statement. These are Kyungdong-specific inspection forms. - -**Revised strategy**: Instead of moving the entire modal with its production-specific content components, create a **wrapper pattern**: - -```typescript -// NEW: src/components/document-system/modals/InspectionReportModal.tsx -// This is a THIN wrapper that dynamic-imports the actual modal -'use client'; - -import dynamic from 'next/dynamic'; -import type { ComponentProps } from 'react'; - -// Dynamic import with loading fallback -const InspectionReportModalImpl = dynamic( - () => import('@/components/production/WorkOrders/documents/InspectionReportModal') - .then(mod => ({ default: mod.InspectionReportModal })), - { - loading: () => null, - ssr: false, - } -); - -// Re-export props type from a shared interface (no production dependency) -export interface InspectionReportModalProps { - isOpen: boolean; - onClose: () => void; - workOrderId: number | null; - type?: 'inspection' | 'worklog'; -} - -export function InspectionReportModal(props: InspectionReportModalProps) { - if (!props.isOpen) return null; - return ; -} -``` - -This pattern: -- Breaks the static import chain (Common no longer statically depends on production) -- Uses `next/dynamic` so the production code is only loaded at runtime when needed -- If production module is absent at build time, the dynamic import fails gracefully (modal just doesn't render) -- Zero behavior change for existing users - -**Estimated time**: 0.5 day -**Risk**: LOW -**Dependencies**: None -**Rollback**: Revert import paths - ---- - -### Task 0.2: Extract Production Orders Shared Interface - -**Problem**: 3 files under `sales/order-management-sales/production-orders/` import from `@/components/production/ProductionOrders/actions.ts` and `types.ts`. - -**Strategy**: Create a shared interface file with the types and action signatures that sales needs. The sales pages will use this shared interface. The actual implementation stays in production. - -**Files to create**: -- `src/interfaces/production-orders.ts` - -**Files to modify**: -- `src/app/[locale]/(protected)/sales/order-management-sales/production-orders/page.tsx` -- `src/app/[locale]/(protected)/sales/order-management-sales/production-orders/[id]/page.tsx` -- `src/app/[locale]/(protected)/sales/order-management-sales/[id]/production-order/page.tsx` - -**Code pattern**: -```typescript -// NEW: src/interfaces/production-orders.ts -// Extract ONLY the types and action signatures that sales needs - -export interface ProductionOrder { - id: number; - order_number: string; - status: ProductionStatus; - // ... (copy from production/ProductionOrders/types.ts) -} - -export type ProductionStatus = 'pending' | 'in_progress' | 'completed' | 'cancelled'; - -export interface ProductionOrderDetail extends ProductionOrder { - work_orders: ProductionWorkOrder[]; - // ... -} - -export interface ProductionWorkOrder { - id: number; - // ... -} - -export interface ProductionOrderStats { - total: number; - pending: number; - in_progress: number; - completed: number; -} - -// Server actions -- these call the SAME backend API endpoint regardless of module presence -// The backend API /api/v1/production-orders/* exists independently of the frontend module -'use server'; - -import { executeServerAction, executePaginatedAction } from '@/lib/api/server-actions'; -import { buildApiUrl } from '@/lib/api/query-params'; - -export async function getProductionOrders(params: { page?: number; search?: string; status?: string }) { - return executePaginatedAction({ - url: buildApiUrl('/api/v1/production-orders', params), - }); -} - -export async function getProductionOrderDetail(id: number) { - return executeServerAction({ - url: buildApiUrl(`/api/v1/production-orders/${id}`), - }); -} - -export async function getProductionOrderStats() { - return executeServerAction({ - url: buildApiUrl('/api/v1/production-orders/stats'), - }); -} -``` - -**Important**: The actions in this shared interface call the **same backend API endpoints** as the production module's actions. The backend API exists independently. This is safe because the sales production-orders view is read-only (viewing production order status from sales perspective). - -**For AssigneeSelectModal** (imported by sales/[id]/production-order/page.tsx): -```typescript -// Same dynamic import pattern as Task 0.1 -// NEW: src/interfaces/components/AssigneeSelectModal.tsx -import dynamic from 'next/dynamic'; - -const AssigneeSelectModalImpl = dynamic( - () => import('@/components/production/WorkOrders/AssigneeSelectModal') - .then(mod => ({ default: mod.AssigneeSelectModal })), - { loading: () => null, ssr: false } -); - -export interface AssigneeSelectModalProps { - isOpen: boolean; - onClose: () => void; - onSelect: (assignee: { id: number; name: string }) => void; -} - -export function AssigneeSelectModal(props: AssigneeSelectModalProps) { - if (!props.isOpen) return null; - return ; -} -``` - -**Estimated time**: 1 day -**Risk**: MEDIUM (sales production-orders functionality must be verified) -**Dependencies**: None -**Rollback**: Revert 3 page files to original imports - ---- - -### Task 0.3: Fix Hardcoded Route Navigation - -**Problem**: `sales/production-orders/[id]/page.tsx` line 247 has `router.push("/production/work-orders")`. - -**Strategy**: Wrap in a module-aware navigation helper. - -**File to create**: -- `src/modules/route-resolver.ts` - -**File to modify**: -- `src/app/[locale]/(protected)/sales/order-management-sales/production-orders/[id]/page.tsx` - -**Code pattern**: -```typescript -// NEW: src/modules/route-resolver.ts -/** - * Resolve routes that may point to tenant-specific modules. - * Falls back to a safe alternative if the target module is not available. - */ -export function resolveTenantRoute( - path: string, - fallback: string = '/dashboard' -): string { - // Phase 1: Simple passthrough (all modules present in monolith) - // Phase 2+: Check module registry for route availability - return path; -} - -// Common route mappings for cross-module navigation -export const CROSS_MODULE_ROUTES = { - workOrders: '/production/work-orders', - constructionContract: '/construction/project/contract', -} as const; - -// MODIFY: sales/production-orders/[id]/page.tsx line 247 -// FROM: -router.push("/production/work-orders"); -// TO: -import { resolveTenantRoute } from '@/modules/route-resolver'; -// ... -router.push(resolveTenantRoute("/production/work-orders", "/sales/order-management-sales/production-orders")); -``` - -**Estimated time**: 0.25 day -**Risk**: LOW -**Dependencies**: None -**Rollback**: Revert to hardcoded string - ---- - -### Task 0.4: Fix QMS Production Type Imports - -**Problem**: `quality/qms/mockData.ts` imports `WorkOrder` type from `production/ProductionDashboard/types` and `ShipmentDetail` from `outbound/ShipmentManagement/types`. - -**Strategy**: The QMS page and quality module are in the SAME tenant package (Kyungdong) as production, so production->quality dependencies are acceptable. However, the `outbound` import is cross-tenant (quality -> Common ERP direction), which is actually the ALLOWED direction. No change needed for outbound imports. - -For the production type import in mockData: since this is mock data, we can inline the type or import from the shared interface. - -**Files to modify**: -- `src/app/[locale]/(protected)/quality/qms/mockData.ts` (replace production type import) - -**Code pattern**: -```typescript -// MODIFY: quality/qms/mockData.ts -// FROM: -import type { WorkOrder } from '@/components/production/ProductionDashboard/types'; -// TO: -// Inline the minimal type needed for mock data -interface WorkOrderMock { - id: number; - work_order_number: string; - status: string; - // ... only fields used in mockData -} -``` - -**Estimated time**: 0.25 day -**Risk**: LOW -**Dependencies**: None - ---- - -### Task 0.5: Fix Dev Generator Production Import - -**Problem**: `src/components/dev/generators/workOrderData.ts` imports `ProcessOption` from production. - -**Strategy**: Move `ProcessOption` type to shared interfaces. - -**Files to modify**: -- `src/components/dev/generators/workOrderData.ts` -- `src/interfaces/production-orders.ts` (add ProcessOption type) - -**Estimated time**: 0.25 day -**Risk**: LOW (dev-only code) -**Dependencies**: Task 0.2 - ---- - -### Task 0.6: Make Dashboard Invalidation Dynamic - -**Problem**: `src/lib/dashboard-invalidation.ts` hardcodes `'production'` and `'construction'` as `DomainKey` values. - -**Strategy**: Change from hardcoded union type to a registry-based system. - -**File to modify**: -- `src/lib/dashboard-invalidation.ts` - -**Code pattern**: -```typescript -// MODIFY: src/lib/dashboard-invalidation.ts - -// BEFORE: hardcoded type union -type DomainKey = 'deposit' | 'withdrawal' | ... | 'production' | 'construction'; -const DOMAIN_SECTION_MAP: Record = { ... }; - -// AFTER: registry-based -type CoreDomainKey = 'deposit' | 'withdrawal' | 'sales' | 'purchase' | 'badDebt' - | 'expectedExpense' | 'bill' | 'giftCertificate' | 'journalEntry' - | 'order' | 'stock' | 'schedule' | 'client' | 'leave' - | 'approval' | 'attendance'; - -// Extendable domain key (modules can register additional domains) -type DomainKey = CoreDomainKey | string; - -// Core mappings (always present) -const CORE_DOMAIN_SECTION_MAP: Record = { - deposit: ['dailyReport', 'receivable'], - withdrawal: ['dailyReport', 'monthlyExpense'], - // ... all core mappings -}; - -// Extended mappings (registered by modules) -const extendedDomainMap = new Map(); - -/** Register module-specific dashboard domain mappings */ -export function registerDashboardDomain(domain: string, sections: DashboardSectionKey[]): void { - extendedDomainMap.set(domain, sections); -} - -// Updated function -export function invalidateDashboard(domain: DomainKey): void { - const sections = (CORE_DOMAIN_SECTION_MAP as Record)[domain] - ?? extendedDomainMap.get(domain) - ?? []; - if (sections.length === 0) return; - // ... rest unchanged -} -``` - -Then in production/construction modules, register on load: -```typescript -// src/components/production/WorkOrders/WorkOrderCreate.tsx (at module level) -import { registerDashboardDomain } from '@/lib/dashboard-invalidation'; -registerDashboardDomain('production', ['statusBoard', 'dailyProduction']); -registerDashboardDomain('shipment', ['statusBoard', 'unshipped']); - -// src/components/business/construction/.../ConstructionDetailClient.tsx -registerDashboardDomain('construction', ['statusBoard', 'construction']); -``` - -**Estimated time**: 0.5 day -**Risk**: LOW (backward compatible, registration is additive) -**Dependencies**: None -**Rollback**: Revert to hardcoded map - ---- - -### Phase 0 Summary - -| Task | Effort | Risk | Parallel? | -|------|--------|------|-----------| -| 0.1 Extract InspectionReportModal | 0.5d | LOW | Yes | -| 0.2 Extract Production Orders interface | 1d | MEDIUM | Yes | -| 0.3 Fix hardcoded route navigation | 0.25d | LOW | Yes | -| 0.4 Fix QMS production type imports | 0.25d | LOW | Yes | -| 0.5 Fix dev generator import | 0.25d | LOW | After 0.2 | -| 0.6 Make dashboard invalidation dynamic | 0.5d | LOW | Yes | -| **Total** | **2.75d** | | | -| **Buffer** | **+1.25d** | | | -| **Phase 0 Total** | **4d** | | | - -**Phase 0 Exit Criteria**: -- Zero imports from `@/components/production/` in any file under `src/components/approval/` -- Zero imports from `@/components/production/` in any file under `src/app/[locale]/(protected)/sales/` -- Zero hardcoded production/construction route strings outside their own modules -- `dashboard-invalidation.ts` has no hardcoded `'production'` or `'construction'` in its type definitions -- All existing functionality works identically (regression test) - ---- - -## 3. Phase 1: Module Registry & Route Guard - -> **Goal**: Create a tenant-aware module system and enforce route-level access control. -> **Duration**: 3-4 days -> **Prerequisite**: Phase 0 complete -> **Risk**: MEDIUM (middleware change affects all routes) - -### Task 1.1: Define Module Registry Types - -**File to create**: `src/modules/types.ts` - -```typescript -/** - * Module definition for tenant-based separation. - * Each module represents a group of pages and components - * that belong to a specific tenant or are optional add-ons. - */ - -export type ModuleId = - | 'common' // Always available - | 'production' // Kyungdong: Shutter MES - | 'quality' // Kyungdong: Quality management - | 'construction' // Juil: Construction management - | 'vehicle-management'; // Optional add-on - -export interface ModuleManifest { - id: ModuleId; - name: string; - description: string; - /** Route prefixes owned by this module (e.g., ['/production', '/quality']) */ - routePrefixes: string[]; - /** Dashboard section keys this module contributes */ - dashboardSections?: string[]; - /** Dashboard domain keys for invalidation */ - dashboardDomains?: Record; -} - -export interface TenantModuleConfig { - tenantId: number; - /** Which industry type this tenant belongs to */ - industry?: string; - /** Explicitly enabled modules (overrides industry defaults) */ - enabledModules: ModuleId[]; -} - -/** Runtime module availability check result */ -export interface ModuleAccess { - allowed: boolean; - reason?: 'not_licensed' | 'not_configured' | 'route_not_found'; - redirectTo?: string; -} -``` - -**Estimated time**: 0.25 day - ---- - -### Task 1.2: Create Module Registry - -**File to create**: `src/modules/index.ts` - -```typescript -import type { ModuleManifest, ModuleId } from './types'; - -/** - * Static module manifest registry. - * - * Phase 1: All modules registered here. - * Phase 2: Loaded from backend JSON. - */ -const MODULE_REGISTRY: Record = { - common: { - id: 'common', - name: 'Common ERP', - description: 'Core ERP modules available to all tenants', - routePrefixes: [ - '/dashboard', '/accounting', '/sales', '/hr', '/approval', - '/board', '/boards', '/customer-center', '/settings', - '/master-data', '/material', '/outbound', '/reports', - '/company-info', '/subscription', '/payment-history', - ], - }, - production: { - id: 'production', - name: 'Production Management', - description: 'Shutter MES production and work order management', - routePrefixes: ['/production'], - dashboardSections: ['production', 'shipment'], - dashboardDomains: { - production: ['statusBoard', 'dailyProduction'], - shipment: ['statusBoard', 'unshipped'], - }, - }, - quality: { - id: 'quality', - name: 'Quality Management', - description: 'Equipment and inspection management', - routePrefixes: ['/quality'], - dashboardSections: [], - }, - construction: { - id: 'construction', - name: 'Construction Management', - description: 'Juil construction project management', - routePrefixes: ['/construction'], - dashboardSections: ['construction'], - dashboardDomains: { - construction: ['statusBoard', 'construction'], - }, - }, - 'vehicle-management': { - id: 'vehicle-management', - name: 'Vehicle Management', - description: 'Vehicle and forklift management', - routePrefixes: ['/vehicle-management', '/vehicle'], - dashboardSections: [], - }, -}; - -/** Get module manifest by ID */ -export function getModuleManifest(moduleId: ModuleId): ModuleManifest | undefined { - return MODULE_REGISTRY[moduleId]; -} - -/** Get all registered module manifests */ -export function getAllModules(): ModuleManifest[] { - return Object.values(MODULE_REGISTRY); -} - -/** Find which module owns a given route prefix */ -export function getModuleForRoute(pathname: string): ModuleId { - for (const [moduleId, manifest] of Object.entries(MODULE_REGISTRY)) { - if (moduleId === 'common') continue; // Check specific modules first - for (const prefix of manifest.routePrefixes) { - if (pathname === prefix || pathname.startsWith(prefix + '/')) { - return moduleId as ModuleId; - } - } - } - return 'common'; // Default: common ERP -} - -/** Get all route prefixes for a set of enabled modules */ -export function getAllowedRoutePrefixes(enabledModules: ModuleId[]): string[] { - const prefixes: string[] = []; - for (const moduleId of ['common' as ModuleId, ...enabledModules]) { - const manifest = MODULE_REGISTRY[moduleId]; - if (manifest) { - prefixes.push(...manifest.routePrefixes); - } - } - return prefixes; -} - -/** Get dashboard section keys for enabled modules */ -export function getEnabledDashboardSections(enabledModules: ModuleId[]): string[] { - const sections: string[] = []; - for (const moduleId of enabledModules) { - const manifest = MODULE_REGISTRY[moduleId]; - if (manifest?.dashboardSections) { - sections.push(...manifest.dashboardSections); - } - } - return sections; -} -``` - -**Estimated time**: 0.5 day - ---- - -### Task 1.3: Create Tenant Configuration - -**File to create**: `src/modules/tenant-config.ts` - -```typescript -import type { ModuleId, TenantModuleConfig } from './types'; - -/** - * Phase 1: Hardcoded tenant-module mappings. - * Phase 2: Loaded from backend API response (/api/auth/user). - * - * Industry-based default modules: - * - 'shutter_mes' (Kyungdong): production + quality - * - 'construction' (Juil): construction - * - undefined / other: common only - */ - -const INDUSTRY_MODULE_MAP: Record = { - shutter_mes: ['production', 'quality'], - construction: ['construction'], -}; - -/** - * Resolve enabled modules for a tenant. - * - * Priority: - * 1. Explicit tenant configuration from backend (Phase 2) - * 2. Industry-based defaults - * 3. Empty (common ERP only) - */ -export function resolveEnabledModules(options: { - industry?: string; - explicitModules?: ModuleId[]; - optionalModules?: ModuleId[]; -}): ModuleId[] { - const { industry, explicitModules, optionalModules = [] } = options; - - // Phase 2: Backend provides explicit module list - if (explicitModules && explicitModules.length > 0) { - return [...explicitModules, ...optionalModules]; - } - - // Phase 1: Industry-based defaults - const industryModules = industry ? (INDUSTRY_MODULE_MAP[industry] ?? []) : []; - return [...industryModules, ...optionalModules]; -} - -/** - * Check if a specific module is enabled for the current tenant. - * This is the primary API for components to check module availability. - */ -export function isModuleEnabled( - moduleId: ModuleId, - enabledModules: ModuleId[] -): boolean { - if (moduleId === 'common') return true; - return enabledModules.includes(moduleId); -} -``` - -**Estimated time**: 0.25 day - ---- - -### Task 1.4: Create useModules Hook - -**File to create**: `src/hooks/useModules.ts` - -```typescript -'use client'; - -import { useMemo } from 'react'; -import { useAuthStore } from '@/stores/authStore'; -import type { ModuleId } from '@/modules/types'; -import { resolveEnabledModules, isModuleEnabled } from '@/modules/tenant-config'; -import { getModuleForRoute, getEnabledDashboardSections } from '@/modules'; - -/** - * Hook to access tenant module configuration. - * Returns enabled modules and helper functions. - */ -export function useModules() { - const tenant = useAuthStore((state) => state.currentUser?.tenant); - - const enabledModules = useMemo(() => { - if (!tenant) return []; - return resolveEnabledModules({ - industry: tenant.options?.industry, - // Phase 2: read from tenant.options?.modules - }); - }, [tenant]); - - const isEnabled = useMemo(() => { - return (moduleId: ModuleId) => isModuleEnabled(moduleId, enabledModules); - }, [enabledModules]); - - const isRouteAllowed = useMemo(() => { - return (pathname: string) => { - const owningModule = getModuleForRoute(pathname); - if (owningModule === 'common') return true; - return enabledModules.includes(owningModule); - }; - }, [enabledModules]); - - const dashboardSections = useMemo(() => { - return getEnabledDashboardSections(enabledModules); - }, [enabledModules]); - - return { - enabledModules, - isEnabled, - isRouteAllowed, - dashboardSections, - tenantIndustry: tenant?.options?.industry, - }; -} -``` - -**Estimated time**: 0.25 day - ---- - -### Task 1.5: Add Route Guard to Middleware - -**Problem**: Currently, any authenticated user can access any route via URL. Tenant users should only access their licensed modules. - -**Strategy**: Add a module check step to the existing middleware at `src/middleware.ts`, between the authentication check (step 7) and the i18n middleware (step 8). - -**File to modify**: `src/middleware.ts` - -**Design considerations**: -- Middleware runs on Edge Runtime -- cannot access Zustand store -- Must read tenant info from cookies or a lightweight API call -- First iteration: read `tenant_modules` cookie set at login -- The cookie is set by the login flow (or the API proxy refresh flow) - -**Code pattern**: -```typescript -// ADD to src/middleware.ts after step 7 (authentication check) - -// 7.5: Module-based route guard -// Read tenant's enabled modules from cookie (set at login) -const tenantModulesCookie = request.cookies.get('tenant_modules')?.value; -if (tenantModulesCookie) { - const enabledModules: string[] = JSON.parse(tenantModulesCookie); - - // Import route check logic (keep it simple for Edge Runtime) - const moduleRouteMap: Record = { - production: ['/production'], - quality: ['/quality'], - construction: ['/construction'], - 'vehicle-management': ['/vehicle-management', '/vehicle'], - }; - - // Check if the requested path belongs to a module the tenant doesn't have - for (const [moduleId, prefixes] of Object.entries(moduleRouteMap)) { - for (const prefix of prefixes) { - if ( - (pathnameWithoutLocale === prefix || pathnameWithoutLocale.startsWith(prefix + '/')) && - !enabledModules.includes(moduleId) && - !enabledModules.includes('all') // Superadmin override - ) { - // Redirect to dashboard with a message parameter - return NextResponse.redirect( - new URL('/dashboard?module_denied=true', request.url) - ); - } - } - } -} -``` - -**Backend requirement**: The login API response or `/api/auth/user` response needs to include `enabled_modules` or `tenant.options.modules`. This cookie must be set during login flow. - -**File to modify for cookie setting**: The login server action or API proxy that handles authentication. Specifically: -- `src/lib/api/auth/` -- wherever the login response is processed and cookies are set - -**Alternative (simpler, Phase 1 only)**: Skip middleware route guard initially. Instead, add a client-side `` component to the `(protected)/layout.tsx` that checks the route against enabled modules and shows an "access denied" page. - -```typescript -// NEW: src/components/auth/ModuleGuard.tsx -'use client'; - -import { usePathname } from 'next/navigation'; -import { useModules } from '@/hooks/useModules'; - -export function ModuleGuard({ children }: { children: React.ReactNode }) { - const pathname = usePathname(); - const { isRouteAllowed } = useModules(); - - // Remove locale prefix for checking - const cleanPath = pathname.replace(/^\/[a-z]{2}\//, '/'); - - if (!isRouteAllowed(cleanPath)) { - return ( -
-

접근 권한 없음

-

- 현재 계약에 포함되지 않은 모듈입니다. -

- - 대시보드로 돌아가기 - -
- ); - } - - return <>{children}; -} -``` - -**Modify**: `src/app/[locale]/(protected)/layout.tsx` -```typescript -// ADD import -import { ModuleGuard } from '@/components/auth/ModuleGuard'; - -// WRAP children - - - {children} - - -``` - -**Recommended approach**: Start with client-side `ModuleGuard` (simpler, no backend cookie change needed). Add middleware guard in Phase 2 when backend provides `enabled_modules`. - -**Estimated time**: 1 day -**Risk**: MEDIUM (affects all page loads, needs thorough testing) -**Dependencies**: Task 1.2, 1.3, 1.4 - ---- - -### Task 1.6: Add Module-Denied Toast to Dashboard - -**File to modify**: `src/app/[locale]/(protected)/dashboard/page.tsx` - -```typescript -'use client'; - -import { useEffect } from 'react'; -import { useSearchParams } from 'next/navigation'; -import { toast } from 'sonner'; -import { Dashboard } from '@/components/business/Dashboard'; - -export default function DashboardPage() { - const searchParams = useSearchParams(); - - useEffect(() => { - if (searchParams.get('module_denied') === 'true') { - toast.error('접근 권한이 없는 모듈입니다. 관리자에게 문의하세요.'); - // Clean up URL - window.history.replaceState(null, '', '/dashboard'); - } - }, [searchParams]); - - return ; -} -``` - -**Estimated time**: 0.25 day -**Risk**: LOW - ---- - -### Task 1.7: Backend Coordination -- Tenant Module Data - -**Requirement**: The backend `/api/auth/user` response (or the login response) should include the tenant's enabled modules. - -**Proposed backend response extension**: -```json -{ - "user": { ... }, - "tenant": { - "id": 282, - "company_name": "(주)경동", - "business_num": "123-45-67890", - "tenant_st_code": "active", - "options": { - "industry": "shutter_mes", - "modules": ["production", "quality", "vehicle-management"] - } - }, - "menus": [ ... ] -} -``` - -**Backend API request document**: -```markdown -## Backend API Modification Request - -### Endpoint: GET /api/v1/auth/user (or login response) -### Change: Add `modules` to `tenant.options` - -**Current response** (tenant.options): -```json -{ - "company_scale": "중소기업", - "industry": "shutter_mes" -} -``` - -**Requested response** (tenant.options): -```json -{ - "company_scale": "중소기업", - "industry": "shutter_mes", - "modules": ["production", "quality"] -} -``` - -**Module values**: "production", "quality", "construction", "vehicle-management" -**Default behavior**: If `modules` is absent, use industry-based defaults -**Backend table**: `tenant_options` or `tenant_modules` (new table) -``` - -**Until backend provides this**: The frontend falls back to `industry` field for module resolution (Task 1.3 already handles this). - -**Estimated time**: 0 days (frontend) -- backend team separate -**Risk**: None for frontend (graceful fallback exists) - ---- - -### Phase 1 Summary - -| Task | Effort | Risk | Parallel? | -|------|--------|------|-----------| -| 1.1 Module types | 0.25d | LOW | Yes | -| 1.2 Module registry | 0.5d | LOW | After 1.1 | -| 1.3 Tenant config | 0.25d | LOW | After 1.1 | -| 1.4 useModules hook | 0.25d | LOW | After 1.2, 1.3 | -| 1.5 Route guard (client-side) | 1d | MEDIUM | After 1.4 | -| 1.6 Module-denied toast | 0.25d | LOW | After 1.5 | -| 1.7 Backend coordination | 0d | - | Parallel | -| **Total** | **2.5d** | | | -| **Buffer** | **+1d** | | | -| **Phase 1 Total** | **3.5d** | | | - -**Phase 1 Exit Criteria**: -- Module registry exists with correct route prefix mappings -- `useModules()` hook returns correct enabled modules based on tenant industry -- Navigating to `/production/*` as a construction-only tenant shows "access denied" -- Dashboard page shows toast when redirected from denied module -- All existing tenants with correct industry setting see no change in behavior - ---- - -## 4. Phase 2: Dashboard Decoupling - -> **Goal**: Make the CEO Dashboard dynamically render only sections for the tenant's enabled modules. -> **Duration**: 2-3 days -> **Prerequisite**: Phase 1 complete -> **Risk**: MEDIUM (dashboard is highly visible, any regression is immediately noticed) - -### Task 2.1: Add Module Awareness to Dashboard Settings Type - -**File to modify**: `src/components/business/CEODashboard/types.ts` - -```typescript -// ADD to types.ts - -/** Sections that require specific modules */ -export const MODULE_DEPENDENT_SECTIONS: Record = { - production: ['production', 'shipment'], - construction: ['construction'], - // 'unshipped' stays in common -- it's about outbound/logistics -}; - -/** Check if a section key requires a specific module */ -export function sectionRequiresModule(sectionKey: SectionKey): string | null { - for (const [moduleId, sections] of Object.entries(MODULE_DEPENDENT_SECTIONS)) { - if (sections.includes(sectionKey)) return moduleId; - } - return null; -} -``` - -**Estimated time**: 0.25 day - ---- - -### Task 2.2: Make CEODashboard Module-Aware - -**File to modify**: `src/components/business/CEODashboard/CEODashboard.tsx` - -**Changes**: -1. Import `useModules` hook -2. Filter out disabled module sections from `sectionOrder` -3. Skip API calls for disabled module data -4. Filter settings dialog to hide unavailable sections - -```typescript -// ADD import -import { useModules } from '@/hooks/useModules'; -import { sectionRequiresModule } from './types'; - -// ADD inside CEODashboard(): -const { enabledModules, isEnabled } = useModules(); - -// MODIFY useCEODashboard call: -const apiData = useCEODashboard({ - salesStatus: true, - purchaseStatus: true, - dailyProduction: isEnabled('production'), // conditional - unshipped: true, // common (outbound) - construction: isEnabled('construction'), // conditional - dailyAttendance: true, -}); - -// MODIFY sectionOrder filtering: -const sectionOrder = useMemo(() => { - const rawOrder = dashboardSettings.sectionOrder ?? DEFAULT_SECTION_ORDER; - // Filter out sections whose required module is not enabled - return rawOrder.filter((key) => { - const requiredModule = sectionRequiresModule(key); - if (!requiredModule) return true; // Common section, always show - return isEnabled(requiredModule as any); - }); -}, [dashboardSettings.sectionOrder, isEnabled]); - -// MODIFY renderDashboardSection for 'production' and 'construction' cases: -case 'production': - if (!isEnabled('production')) return null; // NEW CHECK - if (!(dashboardSettings.production ?? true) || !data.dailyProduction) return null; - // ... rest unchanged - -case 'construction': - if (!isEnabled('construction')) return null; // NEW CHECK - if (!(dashboardSettings.construction ?? true) || !data.constructionData) return null; - // ... rest unchanged -``` - -**Estimated time**: 0.5 day -**Risk**: MEDIUM (must verify all 18+ sections still render correctly) - ---- - -### Task 2.3: Make Dashboard Settings Dialog Module-Aware - -**File to modify**: `src/components/business/CEODashboard/dialogs/DashboardSettingsDialog.tsx` - -**Change**: Filter the settings toggles to only show sections for enabled modules. - -```typescript -// ADD import -import { useModules } from '@/hooks/useModules'; -import { sectionRequiresModule } from '../types'; - -// Inside component: -const { isEnabled } = useModules(); - -// Filter section list in the settings dialog -const availableSections = allSections.filter((section) => { - const requiredModule = sectionRequiresModule(section.key); - if (!requiredModule) return true; - return isEnabled(requiredModule as any); -}); -``` - -**Estimated time**: 0.25 day -**Risk**: LOW - ---- - -### Task 2.4: Make useCEODashboard Hook Skip Disabled APIs - -**File to modify**: `src/hooks/useCEODashboard.ts` - -**Change**: Accept boolean flags for which APIs to call. When `dailyProduction: false`, skip that API call entirely (return empty data, no network request). - -```typescript -// The hook already accepts flags like { dailyProduction: true } -// Just ensure that when false is passed, useDashboardFetch returns -// a stable empty result without making a network request. - -// Verify the existing useDashboardFetch implementation handles this: -const dailyProduction = useDashboardFetch( - enabled.dailyProduction ? 'dashboard/production/summary' : null, // null = skip - // ... -); -``` - -**Estimated time**: 0.5 day (need to verify/modify useDashboardFetch) -**Risk**: LOW - ---- - -### Task 2.5: Fix CalendarSection Module Route References - -**File to modify**: `src/components/business/CEODashboard/sections/CalendarSection.tsx` - -**Change**: Replace hardcoded route strings with conditional navigation. - -```typescript -// BEFORE: -order: '/production/work-orders', -construction: '/construction/project/contract', - -// AFTER: -import { useModules } from '@/hooks/useModules'; -// Inside component: -const { isEnabled } = useModules(); - -// In click handler: -if (type === 'order' && isEnabled('production')) { - router.push('/production/work-orders'); -} else if (type === 'construction' && isEnabled('construction')) { - router.push('/construction/project/contract'); -} else { - // No navigation if module not available - toast.info('해당 모듈이 활성화되어 있지 않습니다.'); -} -``` - -**Estimated time**: 0.25 day -**Risk**: LOW - ---- - -### Task 2.6: Make Summary Nav Bar Module-Aware - -**File to modify**: `src/components/business/CEODashboard/useSectionSummary.ts` - -**Change**: Exclude module-dependent sections from summary calculation when module is disabled. - -**Estimated time**: 0.25 day -**Risk**: LOW - ---- - -### Phase 2 Summary - -| Task | Effort | Risk | Parallel? | -|------|--------|------|-----------| -| 2.1 Module-aware types | 0.25d | LOW | Yes | -| 2.2 CEODashboard module-aware | 0.5d | MEDIUM | After 2.1 | -| 2.3 Settings dialog | 0.25d | LOW | After 2.1 | -| 2.4 useCEODashboard skip | 0.5d | LOW | After 2.1 | -| 2.5 CalendarSection routes | 0.25d | LOW | After 2.1 | -| 2.6 Summary nav bar | 0.25d | LOW | After 2.1 | -| **Total** | **2d** | | | -| **Buffer** | **+0.5d** | | | -| **Phase 2 Total** | **2.5d** | | | - -**Phase 2 Exit Criteria**: -- Kyungdong tenant dashboard shows production/shipment sections, no construction -- Juil tenant dashboard shows construction section, no production/shipment -- Common-only tenant dashboard shows neither production nor construction sections -- Settings dialog only shows available sections -- No console errors, no failed API calls for disabled sections -- Calendar navigation gracefully handles missing modules - ---- - -## 5. Phase 3: Physical Separation - -> **Goal**: Organize the codebase so tenant-specific code is clearly demarcated and can be optionally excluded from builds. -> **Duration**: 2-3 days -> **Prerequisite**: Phase 2 complete -> **Risk**: LOW (reorganization, no behavior change) - -### Task 3.1: Create Module Boundary Markers - -Rather than physically moving files (which would create massive diffs and break git history), we establish **boundary markers** using barrel exports and documentation. - -**Files to create**: -- `src/components/production/MODULE.md` (module metadata) -- `src/components/quality/MODULE.md` -- `src/components/business/construction/MODULE.md` -- `src/components/vehicle-management/MODULE.md` - -```markdown -# MODULE.md -- Production Module - -**Module ID**: production -**Tenant**: Kyungdong (Shutter MES) -**Route Prefixes**: /production -**Component Count**: 56 files -**Dependencies on Common ERP**: - - @/lib/api/* (server actions, API client) - - @/components/ui/* (UI primitives) - - @/components/templates/* (list/detail templates) - - @/components/organisms/* (page layout) - - @/hooks/* (usePermission, etc.) - - @/types/process.ts (shared process types) - - @/stores/authStore (tenant info) - - @/stores/menuStore (sidebar state) -**Exports to Common ERP**: NONE (all cross-references resolved in Phase 0) -**Shared via @/interfaces/**: production-orders.ts -**Shared via @/components/document-system/**: InspectionReportModal, WorkLogModal -``` - -**Estimated time**: 0.25 day - ---- - -### Task 3.2: Verify Build With Module Stubbing - -Create a script that verifies the build can succeed when tenant modules are replaced with stubs. - -**File to create**: `scripts/verify-module-separation.sh` - -```bash -#!/bin/bash -# Verify that common ERP builds cleanly when tenant modules are stubbed. -# This does NOT actually build -- it checks for import violations. - -echo "Checking for forbidden imports (Common -> Tenant)..." - -# Define tenant-specific paths -TENANT_PATHS=( - "@/components/production/" - "@/components/quality/" - "@/components/business/construction/" - "@/components/vehicle-management/" -) - -# Define common ERP source directories (excluding tenant pages) -COMMON_DIRS=( - "src/components/approval" - "src/components/accounting" - "src/components/auth" - "src/components/atoms" - "src/components/board" - "src/components/business/CEODashboard" - "src/components/business/Dashboard.tsx" - "src/components/clients" - "src/components/common" - "src/components/customer-center" - "src/components/document-system" - "src/components/hr" - "src/components/items" - "src/components/layout" - "src/components/material" - "src/components/molecules" - "src/components/organisms" - "src/components/orders" - "src/components/outbound" - "src/components/pricing" - "src/components/providers" - "src/components/reports" - "src/components/settings" - "src/components/stocks" - "src/components/templates" - "src/components/ui" - "src/lib" - "src/hooks" - "src/stores" - "src/contexts" -) - -VIOLATIONS=0 - -for dir in "${COMMON_DIRS[@]}"; do - for tenant_path in "${TENANT_PATHS[@]}"; do - # Search for static imports from tenant paths - found=$(grep -rn "from ['\"]${tenant_path}" "$dir" --include="*.ts" --include="*.tsx" 2>/dev/null | grep -v "// MODULE_SEPARATION_OK" | grep -v "dynamic(") - if [ -n "$found" ]; then - echo "VIOLATION: $dir imports from $tenant_path" - echo "$found" - VIOLATIONS=$((VIOLATIONS + 1)) - fi - done -done - -if [ $VIOLATIONS -eq 0 ]; then - echo "All clear. No forbidden imports found." - exit 0 -else - echo "Found $VIOLATIONS forbidden import(s). Fix before proceeding." - exit 1 -fi -``` - -**Estimated time**: 0.5 day -**Risk**: LOW (read-only verification) - ---- - -### Task 3.3: Sales Production-Orders Conditional Loading - -The `sales/order-management-sales/production-orders/` pages should only be accessible when the production module is enabled. - -**Strategy**: Add a `useModules` check at the top of these page components. - -**Files to modify**: -- `src/app/[locale]/(protected)/sales/order-management-sales/production-orders/page.tsx` -- `src/app/[locale]/(protected)/sales/order-management-sales/production-orders/[id]/page.tsx` -- `src/app/[locale]/(protected)/sales/order-management-sales/[id]/production-order/page.tsx` - -```typescript -// ADD to top of each page component: -import { useModules } from '@/hooks/useModules'; - -export default function ProductionOrdersPage() { - const { isEnabled } = useModules(); - - if (!isEnabled('production')) { - return ( - -
-

생산관리 모듈이 활성화되어 있지 않습니다.

-
-
- ); - } - - // ... existing page content -} -``` - -**Estimated time**: 0.5 day -**Risk**: LOW - ---- - -### Task 3.4: Update tsconfig Path Aliases (Optional) - -For future package extraction, add path aliases that make module boundaries explicit. - -**File to modify**: `tsconfig.json` - -```json -{ - "compilerOptions": { - "paths": { - "@/*": ["./src/*"], - "@modules/*": ["./src/modules/*"], - "@interfaces/*": ["./src/interfaces/*"] - } - } -} -``` - -**Estimated time**: 0.25 day -**Risk**: LOW - ---- - -### Task 3.5: Document Module Boundaries in CLAUDE.md - -**File to modify**: Project `CLAUDE.md` - -Add a section documenting the module separation architecture: - -```markdown -## Module Separation Architecture -**Priority**: RED - -### Module Ownership -| Module ID | Route Prefixes | Component Path | Tenant | -|-----------|----------------|----------------|--------| -| common | /dashboard, /accounting, /sales, ... | src/components/{accounting,approval,...} | All | -| production | /production | src/components/production/ | Kyungdong | -| quality | /quality | src/components/quality/ | Kyungdong | -| construction | /construction | src/components/business/construction/ | Juil | -| vehicle-management | /vehicle-management | src/components/vehicle-management/ | Optional | - -### Dependency Rules -- ALLOWED: tenant module -> Common ERP (e.g., production -> @/lib/*) -- FORBIDDEN: Common ERP -> tenant module (e.g., approval -> production) -- SHARED: Use @/interfaces/ for types, @/components/document-system/ for shared modals -- DYNAMIC: Use next/dynamic for optional cross-module component loading - -### Verification -Run `scripts/verify-module-separation.sh` to check for forbidden imports. -``` - -**Estimated time**: 0.25 day -**Risk**: LOW - ---- - -### Phase 3 Summary - -| Task | Effort | Risk | Parallel? | -|------|--------|------|-----------| -| 3.1 Module boundary markers | 0.25d | LOW | Yes | -| 3.2 Verification script | 0.5d | LOW | Yes | -| 3.3 Sales conditional loading | 0.5d | LOW | Yes | -| 3.4 tsconfig paths | 0.25d | LOW | Yes | -| 3.5 Document in CLAUDE.md | 0.25d | LOW | Yes | -| **Total** | **1.75d** | | | -| **Buffer** | **+0.5d** | | | -| **Phase 3 Total** | **2.25d** | | | - -**Phase 3 Exit Criteria**: -- `verify-module-separation.sh` passes with 0 violations -- Each module has MODULE.md with dependency documentation -- Sales production-orders pages check module availability -- tsconfig has @modules/ and @interfaces/ aliases -- CLAUDE.md documents module separation rules - ---- - -## 6. Phase 4: Manifest-Based Module Loading (Future) - -> **Goal**: Backend-driven module configuration. No frontend code changes needed for new tenants. -> **Duration**: 5-8 days (separate project) -> **Prerequisite**: Phase 3 complete + backend API changes - -This phase is a separate project. Documenting the design here for reference. - -### 4.1: Backend Module Configuration API - -**New endpoint**: `GET /api/v1/tenant/modules` - -```json -{ - "tenant_id": 282, - "modules": [ - { - "id": "production", - "enabled": true, - "config": { - "features": ["work-orders", "worker-screen", "production-dashboard"], - "hidden_features": ["screen-production"] - } - }, - { - "id": "quality", - "enabled": true, - "config": { - "features": ["equipment", "inspections", "qms"] - } - } - ], - "dashboard_sections": ["production", "shipment", "unshipped"], - "menu_overrides": {} -} -``` - -### 4.2: Frontend Manifest Loader - -```typescript -// src/modules/manifest-loader.ts -export async function loadModuleManifest(tenantId: number): Promise { - const response = await fetch(`/api/proxy/tenant/modules`); - const data = await response.json(); - return data.modules; -} -``` - -### 4.3: Dynamic Route Generation - -Using Next.js catch-all routes with module manifest: - -```typescript -// src/app/[locale]/(protected)/[...slug]/page.tsx -// This catch-all route handles module pages that are dynamically enabled. -// Phase 4 only -- requires significant backend work. -``` - -### 4.4: Module Feature Flags - -Fine-grained control within modules: - -```typescript -// e.g., production module has features: work-orders, worker-screen, etc. -// A tenant might have production enabled but worker-screen disabled -function isFeatureEnabled(moduleId: string, featureId: string): boolean; -``` - ---- - -## 7. Testing Strategy - -### Phase 0 Testing - -| Test | Method | Who | -|------|--------|-----| -| ApprovalBox renders correctly | Manual: navigate to /approval/inbox, open a work_order linked document | User | -| Sales production-orders list works | Manual: /sales/order-management-sales/production-orders | User | -| Sales production-order detail works | Manual: click an item in the list above | User | -| QMS page renders | Manual: /quality/qms | User | -| Dashboard invalidation still works | Manual: create a work order, navigate to dashboard, verify production section refreshes | User | -| Build passes | User runs `npm run build` | User | - -### Phase 1 Testing - -| Test | Method | Who | -|------|--------|-----| -| Module guard blocks unauthorized routes | Set tenant industry to 'construction', navigate to /production | Dev | -| Module guard allows authorized routes | Set tenant industry to 'shutter_mes', navigate to /production | Dev | -| Common routes always accessible | Navigate to /accounting, /sales, /hr with any tenant | Dev | -| Module-denied toast appears | Navigate to denied route, verify redirect + toast | Dev | -| Build passes | User runs `npm run build` | User | - -### Phase 2 Testing - -| Test | Method | Who | -|------|--------|-----| -| Kyungdong dashboard shows production | Log in as Kyungdong tenant, check dashboard sections | Dev | -| Kyungdong dashboard hides construction | Same login, verify no construction section | Dev | -| Juil dashboard shows construction | Log in as Juil tenant, check dashboard | Dev | -| Juil dashboard hides production | Same login, verify no production/shipment sections | Dev | -| Common tenant shows neither | Log in as common-only tenant, check dashboard | Dev | -| Settings dialog matches | Open settings, verify only available sections shown | Dev | -| Calendar navigation handles missing modules | Click calendar item that would go to disabled module | Dev | -| No console errors | Check browser console during all above tests | Dev | -| Build passes | User runs `npm run build` | User | - -### Phase 3 Testing - -| Test | Method | Who | -|------|--------|-----| -| Verification script passes | Run `scripts/verify-module-separation.sh` | Dev | -| Sales production-orders disabled for non-production tenant | Navigate as construction tenant | Dev | -| Full regression test | Navigate all major pages as each tenant type | Dev + User | -| Build passes | User runs `npm run build` | User | - ---- - -## 8. Risk Register - -| # | Risk | Probability | Impact | Mitigation | Phase | -|---|------|------------|--------|------------|-------| -| R1 | InspectionReportModal dynamic import fails silently | LOW | HIGH | Error boundary wrapper, fallback UI, monitoring | 0 | -| R2 | Sales production-orders breaks after interface extraction | MEDIUM | HIGH | Keep original actions.ts as fallback, test all 3 pages | 0 | -| R3 | Middleware route guard blocks legitimate access | MEDIUM | CRITICAL | Start with client-side guard (softer failure), add middleware later | 1 | -| R4 | Tenant industry field not set for existing tenants | HIGH | MEDIUM | Default to 'all modules enabled' when industry is undefined | 1 | -| R5 | Dashboard section removal changes layout/spacing | LOW | LOW | Test each section combination, verify CSS grid/flex behavior | 2 | -| R6 | Dashboard API call failure on disabled module endpoint | LOW | MEDIUM | Graceful null handling already exists (data ?? fallback) | 2 | -| R7 | Build size increases due to dynamic imports | LOW | LOW | Measure bundle size before/after, dynamic imports reduce initial bundle | 3 | -| R8 | Developer accidentally adds forbidden import | MEDIUM | LOW | Verification script in CI, CLAUDE.md rules, MODULE.md docs | 3 | - -### Rollback Strategy Per Phase - -| Phase | Rollback Method | Time to Rollback | -|-------|----------------|------------------| -| Phase 0 | `git revert` each task commit | < 5 minutes | -| Phase 1 | Remove ModuleGuard from layout.tsx, revert middleware | < 10 minutes | -| Phase 2 | Revert CEODashboard changes (remove isEnabled checks) | < 10 minutes | -| Phase 3 | No runtime changes to revert (documentation + scripts only) | N/A | - ---- - -## 9. Folder Structure Before/After - -### Before (Current) - -``` -src/ - app/[locale]/(protected)/ - accounting/ # Common ERP - approval/ # Common ERP (imports from production -- VIOLATION) - board/ # Common ERP - construction/ # Juil tenant - customer-center/ # Common ERP - dashboard/ # Common ERP (renders production/construction -- VIOLATION) - hr/ # Common ERP - master-data/ # Common ERP - material/ # Common ERP - outbound/ # Common ERP - production/ # Kyungdong tenant - quality/ # Kyungdong tenant - reports/ # Common ERP - sales/ # Common ERP (imports from production -- VIOLATION) - settings/ # Common ERP - vehicle-management/ # Optional - components/ - approval/ # imports InspectionReportModal from production - business/ - CEODashboard/ # hardcodes production/construction sections - construction/ # Juil tenant components - production/ # Kyungdong tenant components - quality/ # Kyungdong tenant components - vehicle-management/ # Optional components - lib/ - dashboard-invalidation.ts # hardcodes production/construction -``` - -### After (End of Phase 3) - -``` -src/ - modules/ # NEW - index.ts # module registry - types.ts # ModuleId, ModuleManifest, TenantModuleConfig - tenant-config.ts # industry -> module mapping - route-resolver.ts # tenant-aware route resolution - interfaces/ # NEW - production-orders.ts # shared types + actions for sales <-> production - components/ - AssigneeSelectModal.tsx # dynamic import wrapper - app/[locale]/(protected)/ - accounting/ # Common ERP - approval/ # Common ERP (no more production imports) - board/ # Common ERP - construction/ # Juil tenant (guarded by ModuleGuard) - customer-center/ # Common ERP - dashboard/ # Common ERP (conditionally renders sections) - hr/ # Common ERP - master-data/ # Common ERP - material/ # Common ERP - outbound/ # Common ERP - production/ # Kyungdong tenant (guarded by ModuleGuard) - quality/ # Kyungdong tenant (guarded by ModuleGuard) - reports/ # Common ERP - sales/ # Common ERP (production-orders guarded) - settings/ # Common ERP - vehicle-management/ # Optional (guarded by ModuleGuard) - components/ - auth/ - ModuleGuard.tsx # NEW: route-level module access check - approval/ # clean (no production imports) - business/ - CEODashboard/ # module-aware section rendering - construction/ # Juil tenant (with MODULE.md) - document-system/ - modals/ # NEW: shared modals - InspectionReportModal.tsx # dynamic import wrapper - WorkLogModal.tsx # dynamic import wrapper - index.ts - production/ # Kyungdong tenant (with MODULE.md) - WorkOrders/documents/ - InspectionReportModal.tsx # re-exports from document-system - WorkLogModal.tsx # re-exports from document-system - quality/ # Kyungdong tenant (with MODULE.md) - vehicle-management/ # Optional (with MODULE.md) - hooks/ - useModules.ts # NEW: tenant module access hook - lib/ - dashboard-invalidation.ts # registry-based (no hardcoded modules) - scripts/ - verify-module-separation.sh # NEW: import violation checker -``` - ---- - -## 10. Migration Order & Parallelism - -### Execution Timeline - -``` -Week 1 (Phase 0): - Day 1-2: Tasks 0.1, 0.2, 0.3, 0.4 (all parallel) - Day 3: Task 0.5, 0.6 - Day 4: Phase 0 testing + buffer - -Week 2 (Phase 1 + Phase 2): - Day 1: Tasks 1.1, 1.2, 1.3 (parallel) - Day 2: Tasks 1.4, 1.5 (sequential) - Day 3: Task 1.6 + Phase 1 testing - Day 4: Tasks 2.1, 2.2, 2.3 (2.1 first, then parallel) - Day 5: Tasks 2.4, 2.5, 2.6 + Phase 2 testing - -Week 3 (Phase 3 + Buffer): - Day 1: Tasks 3.1, 3.2, 3.3, 3.4, 3.5 (all parallel) - Day 2: Phase 3 testing + full regression - Day 3: Buffer / bug fixes -``` - -### Parallelism Map - -``` -Phase 0: - [0.1 InspReportModal] [0.2 ProdOrders Interface] [0.3 Route Nav] [0.4 QMS Types] - | | | | - v v | | - [0.5 Dev Gen] ----------------+ | - | | - [0.6 Dashboard Invalidation] --+----------------------------------------+ - | - v - Phase 0 Testing - -Phase 1: - [1.1 Types] ----+----> [1.2 Registry] ----+ - | | - +----> [1.3 Tenant Config]-+----> [1.4 useModules] ----> [1.5 Route Guard] ----> [1.6 Toast] - | - v - Phase 1 Testing - -Phase 2: - [2.1 Module-aware types] ----+----> [2.2 Dashboard] - |----> [2.3 Settings Dialog] - |----> [2.4 Hook Skip] - |----> [2.5 Calendar Routes] - +----> [2.6 Summary Nav] - | - v - Phase 2 Testing - -Phase 3: - [3.1 MODULE.md] [3.2 Verify Script] [3.3 Sales Guard] [3.4 tsconfig] [3.5 CLAUDE.md] - | | | | | - v v v v v - Phase 3 Testing + Full Regression -``` - -### What Can Be Done Today (Before Backend Changes) - -Everything in Phases 0-3 can proceed without backend changes. The `useModules` hook falls back to industry-based module resolution using the existing `tenant.options.industry` field in the auth store. The only backend requirement is that this field is populated correctly for existing tenants. - -If `industry` is not set for some tenants, the system defaults to showing all modules (current behavior), so there is zero risk of breaking existing functionality. - ---- - -## Appendix A: File Count Summary - -| Category | Files | Pages | -|----------|-------|-------| -| Common ERP components | ~400+ | ~165 | -| Production (Kyungdong) | 56 component files | 12 pages | -| Quality (Kyungdong) | 35+ component files | 14 pages | -| Construction (Juil) | 161 component files | 57 pages | -| Vehicle Management (Optional) | 13 component files | 12 pages | -| **Total** | ~665+ | 275 | - -## Appendix B: New Files Created (All Phases) - -| File | Phase | Purpose | -|------|-------|---------| -| `src/modules/types.ts` | 1 | Module type definitions | -| `src/modules/index.ts` | 1 | Module registry | -| `src/modules/tenant-config.ts` | 1 | Tenant-to-module mapping | -| `src/modules/route-resolver.ts` | 0 | Tenant-aware route resolution | -| `src/interfaces/production-orders.ts` | 0 | Shared production order types/actions | -| `src/interfaces/components/AssigneeSelectModal.tsx` | 0 | Dynamic import wrapper | -| `src/components/document-system/modals/InspectionReportModal.tsx` | 0 | Dynamic import wrapper | -| `src/components/document-system/modals/WorkLogModal.tsx` | 0 | Dynamic import wrapper | -| `src/components/document-system/modals/index.ts` | 0 | Barrel export | -| `src/components/auth/ModuleGuard.tsx` | 1 | Route-level module check | -| `src/hooks/useModules.ts` | 1 | Module access hook | -| `scripts/verify-module-separation.sh` | 3 | Import violation checker | -| `src/components/production/MODULE.md` | 3 | Module boundary doc | -| `src/components/quality/MODULE.md` | 3 | Module boundary doc | -| `src/components/business/construction/MODULE.md` | 3 | Module boundary doc | -| `src/components/vehicle-management/MODULE.md` | 3 | Module boundary doc | - -**Total new files**: 16 -**Total modified files**: ~20 - -## Appendix C: Backend API Request Summary - -| # | Type | Endpoint | Description | Required Phase | -|---|------|----------|-------------|----------------| -| B1 | MODIFY | `GET /api/auth/user` | Add `tenant.options.modules` array | Phase 1 (optional, has fallback) | -| B2 | NEW | `GET /api/v1/tenant/modules` | Full module configuration | Phase 4 | - ---- - -**Document Version**: 1.0 -**Author**: Claude (System Architect analysis) -**Review Required By**: Backend team (B1, B2), Frontend lead (all phases) diff --git a/claudedocs/architecture/[REF-2025-11-19] multi-tenancy-implementation.md b/claudedocs/architecture/[REF-2025-11-19] multi-tenancy-implementation.md deleted file mode 100644 index 6faab5e5..00000000 --- a/claudedocs/architecture/[REF-2025-11-19] multi-tenancy-implementation.md +++ /dev/null @@ -1,1045 +0,0 @@ -# 멀티테넌시 구현 검토 및 개선 방안 - -**작성일**: 2025-11-19 -**목적**: 현재 프로젝트의 로그인/데이터 저장 구조를 멀티테넌시 관점에서 검토하고 개선 방안 제시 - ---- - -## 📋 목차 - -1. [현재 상태 분석](#현재-상태-분석) -2. [핵심 문제점](#핵심-문제점) -3. [데이터 오염 시나리오](#데이터-오염-시나리오) -4. [개선 방안](#개선-방안) -5. [구현 로드맵](#구현-로드맵) - ---- - -## 현재 상태 분석 - -### 1. 실제 로그인 응답 구조 - -#### 🔍 서버 응답 (실제) - -```typescript -// 로그인 성공 시 받는 실제 데이터 -{ - userId: "TestUser3", - name: "드미트리", - position: "시스템 관리자", - roles: [ - { - id: 19, - name: "system_manager", - description: "시스템 관리자" - } - ], - tenant: { - id: 282, // ✅ 테넌트 고유 ID - company_name: "(주)테크컴퍼니", // ✅ 테넌트 이름 - business_num: "123-45-67890", - tenant_st_code: "trial", - other_tenants: [] // 다중 테넌트 지원 가능성 - }, - menu: [ - { - id: "13664", - label: "시스템 대시보드", - iconName: "layout-dashboard", - path: "/dashboard" - }, - // ... - ] -} -``` - -#### ✅ 중요 발견 -1. **tenant.id**: 테넌트 고유 ID (숫자 타입) → **캐시 키로 사용해야 함** -2. **tenant.company_name**: 회사명 (UI 표시용) -3. **other_tenants**: 다중 테넌트 전환 가능성 (향후 확장) - ---- - -### 2. 인증 시스템 (AuthContext) - -#### 📁 파일 위치 -``` -src/contexts/AuthContext.tsx -``` - -#### 🔍 현재 구조 (문제점) - -**User 타입 정의** (9-25 라인) -```typescript -export interface User { - id: string; - username: string; - email: string; - password: string; - name: string; - role: UserRole; - companyName: string; // ⚠️ 실제 응답과 구조 불일치 - position?: string; - // ... - // ❌ tenant 객체가 없음! - // ❌ tenant.id를 참조할 방법 없음! -} -``` - -**localStorage 사용** (119-145 라인) -```typescript -// 초기 로드 -const savedUsers = localStorage.getItem('mes-users'); // ❌ tenant.id 없음 -const savedCurrentUser = localStorage.getItem('mes-currentUser'); // ❌ tenant.id 없음 - -// 저장 -localStorage.setItem('mes-users', JSON.stringify(users)); -localStorage.setItem('mes-currentUser', JSON.stringify(currentUser)); -``` - -#### ⚠️ 문제점 -1. **타입 불일치**: User 타입이 실제 서버 응답과 다름 -2. **tenant 객체 부재**: tenant.id를 참조할 수 없음 -3. **localStorage 키 고정**: 모든 테넌트가 같은 키 사용 → 데이터 충돌 - ---- - -### 3. 품목 마스터 데이터 관리 (ItemMasterContext) - -#### 📁 파일 위치 -``` -src/contexts/ItemMasterContext.tsx -``` - -#### 🔍 localStorage 사용 패턴 - -**사용 중인 localStorage 키** (778-861 라인) -```typescript -// 13개의 마스터 데이터 -'mes-itemMasters' // ❌ tenant.id 없음 -'mes-specificationMasters' // ❌ tenant.id 없음 -'mes-specificationMasters-version' -'mes-materialItemNames' -'mes-materialItemNames-version' -'mes-itemCategories' -'mes-itemUnits' -'mes-itemMaterials' -'mes-surfaceTreatments' -'mes-partTypeOptions' -'mes-partUsageOptions' -'mes-guideRailOptions' -'mes-sectionTemplates' -'mes-itemMasterFields' -'mes-itemPages' -``` - -#### ⚠️ 문제점 -1. **tenant.id 미포함**: 모든 키에 tenant.id가 없음 -2. **데이터 격리 불가**: 여러 테넌트가 같은 키 사용 → 데이터 충돌 - ---- - -## 핵심 문제점 - -### 🚨 1. User 타입과 실제 응답 구조 불일치 - -**영향도**: 🔴 CRITICAL - -```typescript -// ❌ 현재 AuthContext -interface User { - companyName: string; // 실제 응답에는 없음 -} - -// ✅ 실제 서버 응답 -interface ActualUser { - tenant: { - id: 282, // 테넌트 고유 ID - company_name: "(주)테크컴퍼니", - business_num: "123-45-67890", - tenant_st_code: "trial", - other_tenants: [] - } -} -``` - -**문제**: -- 실제 tenant.id를 참조할 수 없음 -- 타입 불일치로 인한 런타임 에러 가능성 -- 멀티테넌시 구현 불가능 - ---- - -### 🚨 2. localStorage 키에 tenant.id 미포함 - -**영향도**: 🔴 CRITICAL - -```typescript -// ❌ 현재 - 모든 테넌트가 같은 키 사용 -localStorage.getItem('mes-itemMasters') - -// ✅ 필요 - tenant.id 기반 격리 -const tenantId = currentUser.tenant.id; // 282 -localStorage.getItem(`mes-${tenantId}-itemMasters`) // 'mes-282-itemMasters' -``` - -**문제**: -- 같은 브라우저에서 여러 테넌트 사용 시 데이터 충돌 -- 테넌트 A(id: 282)의 데이터가 테넌트 B(id: 350)에 노출될 위험 - ---- - -### 🚨 3. 테넌트 전환 감지 로직 부재 - -**영향도**: 🔴 CRITICAL - -```typescript -// ❌ 현재 - 테넌트 전환 감지 없음 - -// ✅ 필요 - tenant.id 변경 감지 -useEffect(() => { - const prevTenantId = previousTenantRef.current; - const currentTenantId = currentUser?.tenant?.id; - - if (prevTenantId && prevTenantId !== currentTenantId) { - clearTenantCache(prevTenantId); - } - - previousTenantRef.current = currentTenantId; -}, [currentUser?.tenant?.id]); -``` - ---- - -## 데이터 오염 시나리오 - -### 시나리오 1: 순차적 로그인 - -```yaml -# 타임라인 -1. [09:00] 사용자 A (tenant.id: 282) 로그인 - → localStorage.setItem('mes-itemMasters', [...TENANT-282 데이터...]) - -2. [09:30] 사용자 A 로그아웃 - -3. [10:00] 사용자 B (tenant.id: 350) 로그인 - → 품목관리 페이지 진입 - → localStorage.getItem('mes-itemMasters') - -4. [10:00:01] ❌ 문제 발생 - → TENANT-282의 데이터가 TENANT-350 사용자에게 잠깐 보임 - → API 응답 도착 후 TENANT-350 데이터로 교체 (늦음) - -# 결과 -- 잠깐이지만 잘못된 데이터 노출 -- 보안 위반 (GDPR, 개인정보보호법 위반 가능성) -- 사용자 혼란 (화면 깜빡임) -``` - ---- - -### 시나리오 2: 다중 탭 동시 사용 - -```yaml -# 타임라인 -1. [브라우저 탭1] 사용자 A (tenant.id: 282) 로그인 - → localStorage.setItem('mes-itemMasters', [...TENANT-282...]) - -2. [브라우저 탭2] 사용자 B (tenant.id: 350) 로그인 - → localStorage.setItem('mes-itemMasters', [...TENANT-350...]) - → ❌ TENANT-282 데이터 덮어씀! - -3. [탭1로 돌아옴] - → localStorage.getItem('mes-itemMasters') - → ❌ TENANT-350 데이터가 나옴! - -# 결과 -- localStorage는 오리진(도메인) 단위 공유 -- 탭 간 데이터 충돌 -- 예측 불가능한 동작 -``` - ---- - -### 시나리오 3: other_tenants 기능 사용 시 - -```yaml -# 사용자가 여러 테넌트에 소속된 경우 -User: { - tenant: { id: 282, company_name: "A기업" }, - other_tenants: [ - { id: 350, company_name: "B기업" }, - { id: 415, company_name: "C기업" } - ] -} - -# 테넌트 전환 시나리오 -1. A기업(282) 데이터 로드 → localStorage 저장 -2. B기업(350)으로 전환 -3. localStorage에 여전히 A기업 데이터 존재 -4. ❌ 데이터 오염 발생 - -# 결과 -- 다중 테넌트 전환 시 캐시 관리 필수 -``` - ---- - -## 개선 방안 - -### Phase 1: User 타입을 실제 구조에 맞게 수정 (필수 🔴) - -#### 1.1 AuthContext.tsx 수정 - -**타입 정의 추가** -```typescript -// src/contexts/AuthContext.tsx - -// ✅ 추가: Tenant 타입 정의 -export interface Tenant { - id: number; // 테넌트 고유 ID - company_name: string; // 회사명 - business_num: string; // 사업자번호 - tenant_st_code: string; // 테넌트 상태 코드 (trial, active 등) - other_tenants?: Tenant[]; // 다른 소속 테넌트 목록 (다중 테넌트) -} - -// ✅ 추가: Role 타입 정의 -export interface Role { - id: number; - name: string; - description: string; -} - -// ✅ 추가: MenuItem 타입 정의 -export interface MenuItem { - id: string; - label: string; - iconName: string; - path: string; -} - -// ✅ 수정: User 타입을 실제 서버 응답에 맞게 -export interface User { - userId: string; // 사용자 ID - name: string; // 사용자 이름 - position: string; // 직책 - roles: Role[]; // 권한 목록 - tenant: Tenant; // ✅ 테넌트 정보 (필수!) - menu: MenuItem[]; // 메뉴 목록 -} -``` - -**초기 데이터 업데이트** -```typescript -const initialUsers: User[] = [ - { - userId: "TestUser1", - name: "김대표", - position: "대표이사", - roles: [ - { - id: 1, - name: "ceo", - description: "최고경영자" - } - ], - tenant: { - id: 282, // ✅ 테넌트 ID - company_name: "(주)테크컴퍼니", // ✅ 회사명 - business_num: "123-45-67890", - tenant_st_code: "trial", - other_tenants: [] - }, - menu: [ - { - id: "13664", - label: "시스템 대시보드", - iconName: "layout-dashboard", - path: "/dashboard" - } - ] - }, - // ... 나머지 사용자 -]; -``` - ---- - -#### 1.2 테넌트 전환 감지 로직 추가 - -```typescript -// src/contexts/AuthContext.tsx - -export function AuthProvider({ children }: { children: ReactNode }) { - const [users, setUsers] = useState(initialUsers); - const [currentUser, setCurrentUser] = useState(null); - - // ✅ 추가: 이전 tenant.id 추적 - const previousTenantIdRef = useRef(null); - - // ✅ 추가: 테넌트 변경 감지 - useEffect(() => { - const prevTenantId = previousTenantIdRef.current; - const currentTenantId = currentUser?.tenant?.id; - - if (prevTenantId && currentTenantId && prevTenantId !== currentTenantId) { - console.log(`[Auth] Tenant changed: ${prevTenantId} → ${currentTenantId}`); - clearTenantCache(prevTenantId); - } - - previousTenantIdRef.current = currentTenantId || null; - }, [currentUser?.tenant?.id]); - - // ✅ 추가: 테넌트별 캐시 삭제 함수 - const clearTenantCache = (tenantId: number) => { - const prefix = `mes-${tenantId}-`; - - // localStorage 캐시 삭제 - Object.keys(localStorage).forEach(key => { - if (key.startsWith(prefix)) { - localStorage.removeItem(key); - console.log(`[Cache] Cleared localStorage: ${key}`); - } - }); - - // sessionStorage 캐시 삭제 - Object.keys(sessionStorage).forEach(key => { - if (key.startsWith(prefix)) { - sessionStorage.removeItem(key); - console.log(`[Cache] Cleared sessionStorage: ${key}`); - } - }); - }; - - // ✅ 추가: 로그아웃 시 현재 테넌트 캐시 삭제 - const logout = () => { - if (currentUser?.tenant?.id) { - clearTenantCache(currentUser.tenant.id); - } - setCurrentUser(null); - localStorage.removeItem('mes-currentUser'); - }; - - const value: AuthContextType = { - users, - currentUser, - setCurrentUser, - logout, // ✅ 추가 - clearTenantCache, // ✅ 추가 - // ... 기존 함수들 - }; - - return {children}; -} -``` - ---- - -### Phase 2: TenantAwareCache 유틸리티 구현 (필수 🔴) - -#### 2.1 캐시 유틸리티 생성 - -```typescript -// src/lib/cache/TenantAwareCache.ts - -interface CachedData { - tenantId: number; // ✅ tenant.id 타입 (number) - data: T; - timestamp: number; - version?: string; -} - -export class TenantAwareCache { - private tenantId: number; // ✅ tenant.id 타입 (number) - private storage: Storage; - private ttl: number; // Time to Live (ms) - - constructor( - tenantId: number, // ✅ tenant.id를 받음 - storage: Storage = sessionStorage, // sessionStorage 기본값 (탭 격리) - ttl: number = 3600000 // 1시간 기본값 - ) { - this.tenantId = tenantId; - this.storage = storage; - this.ttl = ttl; - } - - /** - * 테넌트별 고유 키 생성 - * 예: tenant.id = 282 → 'mes-282-itemMasters' - */ - private getKey(key: string): string { - return `mes-${this.tenantId}-${key}`; - } - - /** - * 캐시에 데이터 저장 - */ - set(key: string, data: T, version?: string): void { - const cacheData: CachedData = { - tenantId: this.tenantId, - data, - timestamp: Date.now(), - version - }; - - this.storage.setItem(this.getKey(key), JSON.stringify(cacheData)); - } - - /** - * 캐시에서 데이터 조회 (tenantId 및 TTL 검증) - */ - get(key: string): T | null { - const cached = this.storage.getItem(this.getKey(key)); - if (!cached) return null; - - try { - const parsed: CachedData = JSON.parse(cached); - - // 🛡️ tenantId 검증 - if (parsed.tenantId !== this.tenantId) { - console.warn( - `[Cache] tenantId mismatch for key "${key}": ` + - `${parsed.tenantId} !== ${this.tenantId}` - ); - this.remove(key); - return null; - } - - // 🛡️ TTL 검증 (만료 시간) - if (Date.now() - parsed.timestamp > this.ttl) { - console.warn(`[Cache] Expired cache for key: ${key}`); - this.remove(key); - return null; - } - - return parsed.data; - } catch (error) { - console.error(`[Cache] Parse error for key: ${key}`, error); - this.remove(key); - return null; - } - } - - /** - * 캐시에서 특정 키 삭제 - */ - remove(key: string): void { - this.storage.removeItem(this.getKey(key)); - } - - /** - * 현재 테넌트의 모든 캐시 삭제 - */ - clear(): void { - const prefix = `mes-${this.tenantId}-`; - - Object.keys(this.storage).forEach(key => { - if (key.startsWith(prefix)) { - this.storage.removeItem(key); - } - }); - } - - /** - * 버전 일치 여부 확인 - */ - isVersionMatch(key: string, expectedVersion: string): boolean { - const cached = this.storage.getItem(this.getKey(key)); - if (!cached) return false; - - try { - const parsed: CachedData = JSON.parse(cached); - return parsed.version === expectedVersion; - } catch { - return false; - } - } - - /** - * 캐시 메타데이터 조회 - */ - getMetadata(key: string): { tenantId: number; timestamp: number; version?: string } | null { - const cached = this.storage.getItem(this.getKey(key)); - if (!cached) return null; - - try { - const parsed: CachedData = JSON.parse(cached); - return { - tenantId: parsed.tenantId, - timestamp: parsed.timestamp, - version: parsed.version - }; - } catch { - return null; - } - } -} -``` - ---- - -#### 2.2 ItemMasterContext에 적용 - -```typescript -// src/contexts/ItemMasterContext.tsx - -import { useAuth } from './AuthContext'; -import { TenantAwareCache } from '@/lib/cache/TenantAwareCache'; - -export function ItemMasterProvider({ children }: { children: ReactNode }) { - const { currentUser } = useAuth(); - - // ✅ tenant.id 추출 - const tenantId = currentUser?.tenant?.id; - - // ✅ TenantAwareCache 인스턴스 생성 - const cache = useMemo( - () => { - if (!tenantId) return null; - - return new TenantAwareCache( - tenantId, // tenant.id = 282 - sessionStorage, // 탭 격리 - 3600000 // 1시간 TTL - ); - }, - [tenantId] - ); - - // 상태 - const [itemMasters, setItemMasters] = useState([]); - const [specificationMasters, setSpecificationMasters] = useState([]); - // ... - - // ✅ 초기 로드 (캐시 + API) - useEffect(() => { - if (!tenantId || !cache) return; - - const loadData = async () => { - // 1️⃣ 캐시 확인 (즉시 렌더) - const cachedSpec = cache.get('specificationMasters'); - if (cachedSpec) { - setSpecificationMasters(cachedSpec); - console.log(`[Cache] Loaded from cache (tenant: ${tenantId})`); - } - - // 2️⃣ 백그라운드 API 호출 - try { - const response = await fetch( - `/api/tenants/${tenantId}/item-master-config/masters/specifications` - ); - - if (!response.ok) throw new Error('Failed to fetch specifications'); - - const { data } = await response.json(); - - setSpecificationMasters(data); - - // 3️⃣ 캐시 갱신 - cache.set('specificationMasters', data, '1.0'); - console.log(`[API] Data loaded and cached (tenant: ${tenantId})`); - - } catch (error) { - console.error('[API] Failed to load specifications:', error); - // 4️⃣ 에러 시 캐시 폴백 (이미 사용 중) - if (!cachedSpec) { - console.error('[Cache] No cache available, showing error'); - } - } - }; - - loadData(); - }, [tenantId, cache]); - - // ✅ 저장 (API + 캐시 갱신) - const addSpecificationMaster = async (spec: SpecificationMaster) => { - if (!tenantId || !cache) { - throw new Error('Tenant ID not available'); - } - - try { - const response = await fetch( - `/api/tenants/${tenantId}/item-master-config/masters/specifications`, - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(spec) - } - ); - - if (!response.ok) throw new Error('Failed to add specification'); - - // 상태 업데이트 - const newData = [...specificationMasters, spec]; - setSpecificationMasters(newData); - - // 캐시 갱신 - cache.set('specificationMasters', newData, '1.0'); - console.log(`[Cache] Updated after add (tenant: ${tenantId})`); - - } catch (error) { - console.error('[API] Failed to add specification:', error); - throw error; - } - }; - - return ( - - {children} - - ); -} -``` - ---- - -### Phase 3: API 서버 측 tenant.id 검증 (필수 🔴) - -#### 3.1 인증 미들웨어 - -```typescript -// backend/middleware/auth.ts - -import { NextRequest, NextResponse } from 'next/server'; -import { verifyJWT } from '@/lib/jwt'; - -export async function validateTenantAccess( - request: NextRequest, - requestedTenantId: string | number -): Promise { - // 1️⃣ JWT 토큰에서 사용자 정보 추출 - const token = request.headers.get('Authorization')?.replace('Bearer ', ''); - - if (!token) { - throw new Error('No authentication token'); - } - - const payload = await verifyJWT(token); - - // ✅ tenant.id 타입 통일 (문자열 → 숫자) - const requestedId = typeof requestedTenantId === 'string' - ? parseInt(requestedTenantId, 10) - : requestedTenantId; - - // 2️⃣ 토큰의 tenant.id와 요청의 tenant.id 비교 - if (payload.tenant.id !== requestedId) { - throw new Error( - `Tenant access denied: ${payload.tenant.id} !== ${requestedId}` - ); - } - - return true; -} -``` - -#### 3.2 API 라우트 핸들러 - -```typescript -// app/api/tenants/[tenantId]/item-master-config/route.ts - -import { NextRequest, NextResponse } from 'next/server'; -import { validateTenantAccess } from '@/backend/middleware/auth'; - -export async function GET( - request: NextRequest, - { params }: { params: { tenantId: string } } -) { - try { - // 🛡️ tenant.id 검증 - await validateTenantAccess(request, params.tenantId); - - // ✅ 검증 통과 → 해당 테넌트 데이터만 반환 - const config = await db.itemMasterConfig.findUnique({ - where: { - tenantId: parseInt(params.tenantId, 10), - isActive: true - } - }); - - return NextResponse.json({ - success: true, - data: config - }); - - } catch (error) { - return NextResponse.json( - { - success: false, - error: { - code: 'FORBIDDEN', - message: '테넌트 접근 권한이 없습니다.', - details: error.message - } - }, - { status: 403 } - ); - } -} -``` - ---- - -## 구현 로드맵 - -### ✅ Phase 1: User 타입 수정 (1일) - -```yaml -우선순위: 🔴 CRITICAL -예상 시간: 1일 - -작업 항목: - 1. AuthContext.tsx 수정: - - Tenant, Role, MenuItem 타입 정의 추가 - - User 타입을 실제 서버 응답 구조에 맞게 수정 - - 초기 데이터 업데이트 (tenant.id 포함) - - 테넌트 전환 감지 로직 추가 - - clearTenantCache 함수 구현 - - logout 함수에 캐시 삭제 추가 - - 2. 검증: - - 로그인 시 tenant.id 정상 로드 확인 - - console.log로 tenant.id 값 확인 -``` - ---- - -### ✅ Phase 2: TenantAwareCache 구현 (1일) - -```yaml -우선순위: 🔴 CRITICAL -예상 시간: 1일 - -작업 항목: - 1. TenantAwareCache 유틸리티: - - src/lib/cache/TenantAwareCache.ts 생성 - - tenantId를 number 타입으로 처리 - - 단위 테스트 작성 (선택) - - 2. 검증: - - cache.set() 호출 시 키 확인: 'mes-282-itemMasters' - - cache.get() 호출 시 tenantId 검증 확인 - - TTL 만료 테스트 -``` - ---- - -### ✅ Phase 3: ItemMasterContext 마이그레이션 (2일) - -```yaml -우선순위: 🔴 CRITICAL -예상 시간: 2일 - -작업 항목: - 1. ItemMasterContext 리팩토링: - - TenantAwareCache 적용 - - 모든 localStorage 호출 → cache.set/get 교체 - - localStorage → sessionStorage 전환 - - tenant.id 추출 로직 추가 - - 13개 마스터 데이터 모두 적용 - - 2. 검증: - - 각 마스터 데이터 캐시 키 확인 - - 다중 탭 테스트 (같은 테넌트) - - 다중 탭 테스트 (다른 테넌트) - - 로그아웃 후 재로그인 테스트 -``` - ---- - -### ✅ Phase 4: API 서버 검증 (1-2일) - -```yaml -우선순위: 🔴 CRITICAL -예상 시간: 1-2일 - -작업 항목: - 1. 인증 미들웨어: - - validateTenantAccess 구현 - - JWT에서 tenant.id 추출 - - tenant.id 타입 통일 (string ↔ number) - - 2. API 라우트: - - 모든 /api/tenants/[tenantId]/* 엔드포인트에 검증 추가 - - 403 에러 응답 처리 - - 3. 검증: - - 정상 tenant.id 접근 테스트 - - 잘못된 tenant.id 접근 차단 확인 - - 에러 응답 확인 -``` - ---- - -### ✅ Phase 5: 다중 테넌트 전환 지원 (선택, 2일) - -```yaml -우선순위: 🟢 RECOMMENDED -예상 시간: 2일 - -작업 항목: - 1. other_tenants 기능: - - 테넌트 전환 UI 추가 - - 전환 시 캐시 삭제 확인 - - 전환 시 API 재호출 확인 - - 2. 검증: - - A기업 → B기업 전환 테스트 - - 각 테넌트별 데이터 격리 확인 -``` - ---- - -## 체크리스트 - -### 🔴 필수 항목 (Phase 1-4) - -```yaml -□ AuthContext User 타입 수정 (tenant 객체 포함) -□ Tenant, Role, MenuItem 타입 정의 추가 -□ 초기 사용자 데이터에 tenant.id 할당 -□ 테넌트 전환 감지 로직 추가 (useEffect + useRef) -□ clearTenantCache 함수 구현 -□ logout 함수에 캐시 삭제 추가 -□ TenantAwareCache 유틸리티 구현 (tenantId: number) -□ ItemMasterContext에 TenantAwareCache 적용 -□ 13개 마스터 데이터 모두 캐시 마이그레이션 -□ localStorage → sessionStorage 전환 -□ API 미들웨어 validateTenantAccess 추가 -□ 모든 API 라우트에 tenant.id 검증 추가 -□ 다중 탭 테스트 완료 (같은 테넌트) -□ 다중 탭 테스트 완료 (다른 테넌트) -□ 테넌트 전환 테스트 완료 -□ 로그아웃 후 재로그인 테스트 완료 -``` - -### 🟢 권장 항목 (Phase 5) - -```yaml -□ other_tenants 다중 테넌트 전환 기능 -□ 테넌트 전환 UI 구현 -□ Stale-While-Revalidate 패턴 적용 -□ HTTP 캐싱 헤더 설정 -□ 캐시 메트릭 수집 -□ 성능 테스트 -``` - ---- - -## 실제 구현 예시 - -### 예시 1: 캐시 키 생성 - -```typescript -// tenant.id = 282인 사용자 -const cache = new TenantAwareCache(282, sessionStorage); - -// 키 생성 -cache.set('itemMasters', data); -// → sessionStorage에 'mes-282-itemMasters' 저장 - -cache.set('specificationMasters', data); -// → sessionStorage에 'mes-282-specificationMasters' 저장 -``` - ---- - -### 예시 2: 테넌트 전환 시 - -```typescript -// 사용자 A (tenant.id: 282) 로그인 -currentUser = { - tenant: { id: 282, company_name: "A기업" } -} -// sessionStorage: 'mes-282-itemMasters', 'mes-282-specificationMasters', ... - -// 사용자 B (tenant.id: 350)로 전환 -currentUser = { - tenant: { id: 350, company_name: "B기업" } -} -// useEffect 트리거 → clearTenantCache(282) 호출 -// sessionStorage에서 'mes-282-*' 모두 삭제 -// 새로운 캐시: 'mes-350-itemMasters', 'mes-350-specificationMasters', ... -``` - ---- - -### 예시 3: API 호출 - -```typescript -// 클라이언트 -const tenantId = currentUser.tenant.id; // 282 -const response = await fetch(`/api/tenants/${tenantId}/item-master-config`); - -// 서버 -// validateTenantAccess(request, "282") -// JWT 토큰: { tenant: { id: 282 } } -// 비교: 282 === 282 → ✅ 통과 - -// 만약 잘못된 요청 -const response = await fetch(`/api/tenants/350/item-master-config`); -// JWT 토큰: { tenant: { id: 282 } } -// 비교: 282 !== 350 → ❌ 403 Forbidden -``` - ---- - -## 보안 고려사항 - -### 🛡️ 클라이언트 측 보안 - -1. **sessionStorage 사용**: localStorage보다 탭 격리로 더 안전 -2. **tenant.id 검증**: 캐시 조회 시 항상 검증 -3. **TTL 설정**: 만료된 캐시 자동 삭제 (1시간) -4. **에러 처리**: 손상된 캐시 안전 제거 - -### 🛡️ 서버 측 보안 - -1. **JWT 검증**: 모든 요청에 토큰 검증 -2. **tenant.id 검증**: JWT의 tenant.id와 URL 파라미터 비교 -3. **403 Forbidden**: 권한 없는 접근 차단 -4. **데이터베이스 격리**: WHERE tenant_id = ? 항상 포함 - -### 🛡️ 타입 안정성 - -1. **tenant.id 타입**: number (서버 응답 기준) -2. **URL 파라미터**: string → number 변환 필요 -3. **TypeScript**: 컴파일 타임 타입 체크 - ---- - -## 참고 자료 - -### 관련 문서 -- [API_DESIGN_ITEM_MASTER_CONFIG.md](./_API_DESIGN_ITEM_MASTER_CONFIG) -- [CLEANUP_SUMMARY.md](./CLEANUP_SUMMARY.md) - -### 외부 참고 -- [Multi-Tenancy Best Practices](https://www.postgresql.org/docs/current/ddl-rowsecurity.html) -- [Browser Storage Security](https://developer.mozilla.org/en-US/docs/Web/Security/Types_of_attacks#cross-site_scripting_xss) - ---- - -**문서 버전**: 1.1 (tenant.id 반영) -**마지막 업데이트**: 2025-11-19 -**다음 리뷰**: Phase 1 완료 후 - ---- - -## 관련 파일 - -### 프론트엔드 -- `src/contexts/AuthContext.tsx` - 인증 및 테넌트 정보 관리 -- `src/contexts/ItemMasterContext.tsx` - 품목 마스터 데이터 Context (localStorage 사용) -- `src/lib/cache/TenantAwareCache.ts` - 테넌트별 캐시 유틸리티 (구현 예정) -- `src/middleware.ts` - 테넌트 식별 미들웨어 - -### 백엔드 (구현 예정) -- `app/api/tenants/[tenantId]/item-master-config/route.ts` - 테넌트별 API 라우트 -- `backend/middleware/auth.ts` - 테넌트 접근 검증 미들웨어 - -### 참조 문서 -- `claudedocs/auth/[IMPL-2025-11-07] authentication-implementation-guide.md` - 인증 구현 가이드 -- `claudedocs/architecture/[REF] architecture-integration-risks.md` - 아키텍처 통합 위험 요소 -- `claudedocs/architecture/[IMPL-2025-11-18] ssr-hydration-fix.md` - SSR/Hydration 에러 해결 \ No newline at end of file diff --git a/claudedocs/architecture/[REF] architecture-integration-risks.md b/claudedocs/architecture/[REF] architecture-integration-risks.md deleted file mode 100644 index 8ffe3050..00000000 --- a/claudedocs/architecture/[REF] architecture-integration-risks.md +++ /dev/null @@ -1,867 +0,0 @@ -# 아키텍처 통합 위험 요소 분석 - -## 📋 문서 개요 - -이 문서는 현재 구성된 기반 설정에 추가 설계 가이드를 병합할 때 예상되는 위험 요소와 해결 방안을 제시합니다. - -**작성일**: 2025-11-06 -**업데이트**: 2025-11-06 (Next.js 15.5.6으로 다운그레이드, React Hook Form + Zod 추가) -**프로젝트**: Multi-tenant ERP System -**기술 스택**: -- Frontend: Next.js 15.5.6, React 19, next-intl, React Hook Form, Zod, TypeScript 5 -- Backend: PHP Laravel + Sanctum (API) -- Deployment: Vercel (Frontend) - ---- - -## 🏗️ 현재 아키텍처 구성 - -### 1. 기술 스택 -```yaml -Frontend (Next.js): - - Next.js: 15.5.6 (stable, production-ready) - - React: 19.2.0 (latest) - - TypeScript: 5.x - - Deployment: Vercel - -Internationalization: - - next-intl: 4.4.0 - - Locales: ko (default), en, ja - -Form Management & Validation: - - React Hook Form: 7.54.2 - - Zod: 3.24.1 - - @hookform/resolvers: 3.9.1 - -Styling: - - Tailwind CSS: 4.x (latest) - - PostCSS: 4.x - -Backend (Laravel): - - PHP Laravel: 10.x+ - - Database: MySQL/PostgreSQL - - Authentication: Laravel Sanctum (SPA Token Authentication) - - API: RESTful JSON API - - Deployment: 별도 서버 (Git 관리) - -Architecture: - - Frontend: Next.js (Vercel) - UI/UX, i18n - - Backend: Laravel - Business Logic, DB, API - - Communication: HTTP/HTTPS API calls - - Auth Flow: Laravel Sanctum → Token → Next.js Storage -``` - -### 2. 디렉토리 구조 -``` -src/ -├── app/[locale]/ # 다국어 라우팅 -├── components/ # 공용 컴포넌트 -├── i18n/ # i18n 설정 -├── messages/ # 번역 파일 (ko, en, ja) -└── middleware.ts # 통합 미들웨어 -``` - -### 3. 구현된 기능 -- ✅ 다국어 지원 (ko, en, ja) -- ✅ SEO 최적화 (noindex, robots.txt) -- ✅ 봇 차단 미들웨어 -- ✅ 보안 헤더 설정 -- ✅ TypeScript 엄격 모드 -- ✅ 폼 관리 및 유효성 검증 (React Hook Form + Zod) - ---- - -## ⚠️ 주요 위험 요소 - -### 🔴 HIGH PRIORITY - -#### 1. 멀티 테넌시 + i18n 복잡도 - -**문제**: 테넌트 격리와 다국어 라우팅의 충돌 가능성 - -**예상 시나리오**: -``` -❌ 잠재적 충돌: -/[locale]/[tenant]/dashboard -vs -/[tenant]/[locale]/dashboard - -어떤 구조를 선택할 것인가? -``` - -**위험도**: 🔴 높음 - -**영향 범위**: -- URL 구조 전체 -- 라우팅 로직 -- 미들웨어 복잡도 -- SEO 구조 - -**해결 방안**: - -**옵션 A: Locale 우선 (현재 구조 유지)** -```typescript -// URL 구조: /[locale]/[tenant]/dashboard -// 장점: i18n 우선, 언어 전환 간편 -// 단점: 테넌트별 커스텀 도메인 어려움 - -/ko/acme-corp/dashboard → ACME 한국어 대시보드 -/en/acme-corp/dashboard → ACME 영어 대시보드 -/ko/beta-inc/dashboard → Beta Inc. 한국어 대시보드 -``` - -**옵션 B: Tenant 우선** -```typescript -// URL 구조: /[tenant]/[locale]/dashboard -// 장점: 테넌트 격리 명확, 커스텀 도메인 용이 -// 단점: 언어 전환 시 URL 복잡도 증가 - -/acme-corp/ko/dashboard -/acme-corp/en/dashboard -``` - -**옵션 C: 서브도메인 분리 (권장)** -```typescript -// URL 구조: {tenant}.domain.com/[locale]/dashboard -// 장점: 완벽한 테넌트 격리, 깔끔한 URL -// 단점: DNS 설정 필요, 미들웨어 복잡도 증가 - -acme-corp.erp.com/ko/dashboard -acme-corp.erp.com/en/dashboard -beta-inc.erp.com/ko/dashboard -``` - -**권장 전략**: -```typescript -// 1단계: 개발 환경 (Locale 우선) -/[locale]/[tenant]/dashboard - -// 2단계: 프로덕션 (서브도메인) -{tenant}.domain.com/[locale]/dashboard - -// 미들웨어에서 처리 -export function middleware(request: NextRequest) { - const hostname = request.headers.get('host'); - - // 서브도메인에서 테넌트 추출 - const tenant = extractTenantFromHostname(hostname); - - // 로케일은 기존 로직 사용 - const locale = detectLocale(request); - - // 컨텍스트에 테넌트 정보 주입 - request.headers.set('x-tenant-id', tenant); -} -``` - ---- - -#### 3. 미들웨어 성능 및 복잡도 - -**현재 미들웨어 책임**: -```typescript -1. 로케일 감지 및 리다이렉션 -2. 봇 차단 (User-Agent 검사) -3. 보안 헤더 추가 -4. 로깅 - -향후 추가 예상: -5. 인증 검증 (JWT/Session) -6. 권한 확인 (RBAC) -7. 테넌트 식별 및 격리 -8. Rate Limiting -9. API 키 검증 -10. CORS 처리 -``` - -**위험도**: 🔴 높음 (복잡도 증가) - -**성능 영향**: -```typescript -// 미들웨어는 모든 요청마다 실행됨 -// 현재: ~5-10ms -// 인증 추가: ~20-50ms -// DB 조회 추가: ~100-200ms ⚠️ 위험! -``` - -**해결 방안**: - -**1. 미들웨어 분리 전략** -```typescript -// src/middleware/index.ts -import { chainMiddleware } from '@/lib/middleware-chain'; -import { i18nMiddleware } from './i18n'; -import { botBlockingMiddleware } from './bot-blocking'; -import { authMiddleware } from './auth'; -import { tenantMiddleware } from './tenant'; - -export default chainMiddleware([ - i18nMiddleware, // 1순위: 로케일 감지 - botBlockingMiddleware, // 2순위: 봇 차단 (빠른 종료) - tenantMiddleware, // 3순위: 테넌트 식별 - authMiddleware, // 4순위: 인증 (DB 조회 최소화) -]); -``` - -**2. 성능 최적화** -```typescript -// ✅ 캐싱 활용 -const tenantCache = new Map(); - -// ✅ DB 조회 최소화 -// 미들웨어: 토큰 검증만 -// API Route: DB 조회 - -// ✅ Edge Runtime 활용 (Vercel/Cloudflare) -export const config = { - runtime: 'edge', // 빠른 실행 -}; -``` - -**3. 조건부 실행** -```typescript -export function middleware(request: NextRequest) { - const { pathname } = request.nextUrl; - - // 정적 파일은 스킵 - if (pathname.startsWith('/_next/static')) { - return NextResponse.next(); - } - - // 공개 경로는 인증 스킵 - if (PUBLIC_PATHS.includes(pathname)) { - return i18nOnly(request); - } - - // 보호된 경로만 전체 검증 - return fullMiddleware(request); -} -``` - ---- - -### 🟡 MEDIUM PRIORITY - -#### 4. 데이터베이스 스키마와 다국어 (Laravel 백엔드) - -**✅ 확정**: 데이터베이스 및 API는 Laravel에서 관리 - -**Laravel 다국어 처리 전략**: - -**옵션 A: JSON 컬럼 (Laravel에서 간편)** -```php -// Laravel Migration -Schema::create('products', function (Blueprint $table) { - $table->uuid('id')->primary(); - $table->string('sku', 50)->unique(); - $table->json('name'); // {"ko": "제품명", "en": "Product Name", "ja": "製品名"} - $table->json('description')->nullable(); - $table->timestamps(); -}); - -// Laravel Model -class Product extends Model { - protected $casts = [ - 'name' => 'array', - 'description' => 'array', - ]; - - public function getTranslatedName($locale = 'ko') { - return $this->name[$locale] ?? $this->name['ko']; - } -} -``` - -**옵션 B: 번역 테이블 (권장 - 성능 최적화)** -```php -// Laravel Migration - products table -Schema::create('products', function (Blueprint $table) { - $table->uuid('id')->primary(); - $table->string('sku', 50)->unique(); - $table->timestamps(); -}); - -// Laravel Migration - product_translations table -Schema::create('product_translations', function (Blueprint $table) { - $table->uuid('product_id'); - $table->string('locale', 5); - $table->string('name'); - $table->text('description')->nullable(); - - $table->primary(['product_id', 'locale']); - $table->foreign('product_id')->references('id')->on('products')->onDelete('cascade'); - $table->index('locale'); -}); - -// Laravel Model -class Product extends Model { - public function translations() { - return $this->hasMany(ProductTranslation::class); - } - - public function translation($locale = 'ko') { - return $this->translations()->where('locale', $locale)->first(); - } -} - -class ProductTranslation extends Model { - public $timestamps = false; - protected $fillable = ['locale', 'name', 'description']; -} -``` - -**Laravel API 응답 예시**: -```php -// API Controller -public function show(Product $product, Request $request) { - $locale = $request->header('X-Locale', 'ko'); - - return response()->json([ - 'id' => $product->id, - 'sku' => $product->sku, - 'name' => $product->translation($locale)->name, - 'description' => $product->translation($locale)->description, - ]); -} -``` - -**Next.js에서 사용**: -```typescript -// API 호출 with 로케일 -const fetchProduct = async (id: string, locale: string) => { - const res = await fetch(`${LARAVEL_API_URL}/api/products/${id}`, { - headers: { - 'X-Locale': locale, - 'Authorization': `Bearer ${token}`, - }, - }); - return res.json(); -}; -``` - -**권장**: 옵션 B (번역 테이블) - Laravel Eloquent ORM과 잘 동작 - ---- - -#### 5. 인증 시스템 통합 (Laravel Sanctum) - -**✅ 확정**: 인증은 Laravel Sanctum에서 처리, Next.js는 토큰 관리만 - -**Laravel Sanctum 인증 플로우**: - -``` -1. 로그인 요청 (Next.js) - ↓ -2. Laravel API 인증 (/api/login) - ↓ -3. Sanctum Token 발급 - ↓ -4. Next.js에 토큰 저장 (Cookie/LocalStorage) - ↓ -5. 이후 모든 API 요청에 토큰 포함 -``` - -**Laravel API 설정**: -```php -// routes/api.php -Route::post('/login', [AuthController::class, 'login']); -Route::post('/logout', [AuthController::class, 'logout'])->middleware('auth:sanctum'); -Route::get('/user', function (Request $request) { - return $request->user(); -})->middleware('auth:sanctum'); - -// app/Http/Controllers/AuthController.php -public function login(Request $request) { - $credentials = $request->validate([ - 'email' => 'required|email', - 'password' => 'required', - ]); - - if (!Auth::attempt($credentials)) { - return response()->json(['message' => 'Invalid credentials'], 401); - } - - $user = Auth::user(); - $token = $user->createToken('auth-token')->plainTextToken; - - return response()->json([ - 'user' => $user, - 'token' => $token, - ]); -} -``` - -**Next.js 미들웨어 (토큰 검증만)**: -```typescript -// src/middleware.ts -export function middleware(request: NextRequest) { - const { pathname } = request.nextUrl; - - // 1단계: i18n 먼저 처리 (로케일 정규화) - const intlResponse = intlMiddleware(request); - - // 2단계: 정규화된 경로로 인증 체크 - const locale = getLocaleFromPath(intlResponse.url); - const pathWithoutLocale = removeLocale(pathname, locale); - - // 3단계: 보호된 경로인지 확인 - if (requiresAuth(pathWithoutLocale)) { - // 쿠키에서 토큰 확인 - const token = request.cookies.get('auth_token')?.value; - - if (!token) { - // 로케일 포함하여 로그인 페이지로 리다이렉트 - const loginUrl = new URL(`/${locale}/login`, request.url); - loginUrl.searchParams.set('callbackUrl', request.url); - return NextResponse.redirect(loginUrl); - } - - // ⚠️ 주의: 미들웨어에서는 토큰 유효성 검증 안 함 - // → Laravel API 호출 시 자동으로 검증됨 - // → 성능 최적화 (매 요청마다 DB 조회 방지) - } - - return intlResponse; -} -``` - -**Next.js API 호출 유틸리티**: -```typescript -// src/lib/api.ts -const LARAVEL_API_URL = process.env.NEXT_PUBLIC_LARAVEL_API_URL; - -export async function apiCall(endpoint: string, options: RequestInit = {}) { - const token = getCookie('auth_token'); - - const res = await fetch(`${LARAVEL_API_URL}${endpoint}`, { - ...options, - headers: { - 'Authorization': `Bearer ${token}`, - 'Accept': 'application/json', - 'Content-Type': 'application/json', - ...options.headers, - }, - }); - - if (res.status === 401) { - // 토큰 만료 → 로그아웃 처리 - deleteCookie('auth_token'); - window.location.href = '/login'; - } - - return res.json(); -} - -// 로그인 -export async function login(email: string, password: string) { - const data = await apiCall('/api/login', { - method: 'POST', - body: JSON.stringify({ email, password }), - }); - - // 토큰 저장 - setCookie('auth_token', data.token, { maxAge: 60 * 60 * 24 * 7 }); // 7일 - - return data.user; -} - -// 로그아웃 -export async function logout() { - await apiCall('/api/logout', { method: 'POST' }); - deleteCookie('auth_token'); -} -``` - -**주요 특징**: -- ✅ **Next.js 미들웨어**: 토큰 존재 여부만 확인 (빠름) -- ✅ **Laravel API**: 실제 토큰 검증 및 사용자 인증 -- ✅ **토큰 저장**: HTTP-only Cookie (XSS 방지) -- ✅ **토큰 갱신**: Laravel Sanctum 자동 처리 - ---- - -#### 6. 빌드 및 배포 설정 - -**정적 생성 vs 동적 렌더링**: - -**현재 문제**: -```typescript -// 모든 로케일 × 모든 페이지 조합 생성 -// 3개 언어 × 100개 페이지 = 300개 정적 페이지 -// → 빌드 시간 증가 - -export function generateStaticParams() { - return locales.map((locale) => ({ locale })); -} -``` - -**해결 방안**: -```typescript -// 옵션 1: ISR (Incremental Static Regeneration) -export const revalidate = 3600; // 1시간마다 재생성 - -// 옵션 2: 동적 렌더링 (인증 필요 페이지) -export const dynamic = 'force-dynamic'; - -// 옵션 3: 하이브리드 (공개 페이지는 정적, 대시보드는 동적) -// src/app/[locale]/(public)/page.tsx → 정적 -// src/app/[locale]/(protected)/dashboard/page.tsx → 동적 -``` - -**권장 전략**: -```typescript -// 1. 공개 페이지 -export const dynamic = 'force-static'; -export const revalidate = 3600; - -// 2. 대시보드/ERP 기능 -export const dynamic = 'force-dynamic'; - -// 3. 리포트 페이지 -export const dynamic = 'force-dynamic'; -export const revalidate = 300; // 5분 캐시 -``` - ---- - -### 🟢 LOW PRIORITY - -#### 7. UI 컴포넌트 라이브러리 선택 - -**예상 추가 의존성**: -```json -{ - "dependencies": { - // 옵션 1: shadcn/ui (권장) - "@radix-ui/react-*": "^latest", - - // 옵션 2: Material-UI - "@mui/material": "^latest", - - // 옵션 3: Ant Design - "antd": "^latest" - } -} -``` - -**i18n 통합 고려사항**: -```typescript -// shadcn/ui: next-intl과 잘 작동 -import { useTranslations } from 'next-intl'; -import { Button } from '@/components/ui/button'; - -const t = useTranslations('common'); - - -// Material-UI: 별도 LocalizationProvider 필요 -import { LocalizationProvider } from '@mui/x-date-pickers'; -// → next-intl과 중복 가능성 -``` - -**권장**: shadcn/ui (Tailwind 기반, next-intl 호환) - ---- - -#### 8. 상태 관리 라이브러리 - -**예상 추가 의존성**: -```json -{ - "dependencies": { - // 옵션 1: Zustand (권장) - "zustand": "^latest", - - // 옵션 2: Redux Toolkit - "@reduxjs/toolkit": "^latest", - "react-redux": "^latest", - - // 옵션 3: Jotai - "jotai": "^latest" - } -} -``` - -**다국어 통합**: -```typescript -// Zustand + next-intl -import { create } from 'zustand'; -import { useLocale } from 'next-intl'; - -const useStore = create((set) => ({ - locale: 'ko', - setLocale: (locale) => set({ locale }), -})); - -// 컴포넌트 -const locale = useLocale(); // next-intl -const { setLocale } = useStore(); // 전역 상태 -``` - -**충돌 가능성**: 낮음 (독립적 동작) - ---- - -## 🛡️ 통합 체크리스트 - -### 설계 가이드 병합 전 확인사항 - -#### Phase 1: 라우팅 구조 확정 -- [ ] 멀티 테넌시 전략 결정 (서브도메인 vs URL 기반) -- [ ] URL 구조 최종 확정 (`/[locale]/[tenant]` vs `{tenant}.domain/[locale]`) -- [ ] 미들웨어 실행 순서 정의 -- [ ] 404/에러 페이지 다국어 처리 - -#### Phase 2: 데이터베이스 설계 -- [ ] 다국어 데이터 저장 방식 결정 (JSON vs 번역 테이블) -- [ ] Prisma 스키마 작성 -- [ ] 마이그레이션 전략 수립 -- [ ] 시드 데이터 다국어 준비 - -#### Phase 3: 인증 시스템 -- [ ] 인증 라이브러리 선택 (NextAuth.js, Clerk, Supabase Auth 등) -- [ ] 세션 관리 전략 (JWT vs Database Session) -- [ ] 미들웨어 통합 (i18n + auth 순서) -- [ ] 로그인/로그아웃 플로우 다국어 처리 - -#### Phase 4: UI/UX -- [ ] 컴포넌트 라이브러리 선택 -- [ ] 디자인 시스템 정의 -- [ ] 반응형 레이아웃 전략 -- [ ] 다크모드 지원 여부 - -#### Phase 5: 성능 최적화 -- [ ] ISR vs SSR vs SSG 전략 -- [ ] 이미지 최적화 (next/image) -- [ ] 폰트 최적화 -- [ ] 번들 크기 모니터링 - -#### Phase 6: 배포 준비 -- [ ] 환경 변수 관리 (.env.local, .env.production) -- [ ] CI/CD 파이프라인 -- [ ] 도메인 및 DNS 설정 -- [ ] 모니터링 도구 (Sentry, LogRocket 등) - ---- - -## 🔧 권장 마이그레이션 전략 - -### 단계별 통합 플랜 - -#### Week 1-2: 기반 구조 검증 -```bash -✓ 현재 구조 분석 -✓ 설계 가이드 리뷰 -✓ 충돌 포인트 식별 -✓ 통합 전략 수립 -``` - -#### Week 3-4: 라우팅 및 미들웨어 -```bash -- 멀티 테넌시 구조 구현 -- 미들웨어 리팩토링 (체이닝) -- 테넌트 격리 테스트 -- 성능 벤치마크 -``` - -#### Week 5-6: 데이터베이스 및 인증 -```bash -- Prisma 스키마 완성 -- 인증 시스템 통합 -- 테넌트별 데이터 격리 -- 권한 시스템 구현 -``` - -#### Week 7-8: UI 컴포넌트 및 기능 -```bash -- 컴포넌트 라이브러리 설치 -- 공통 컴포넌트 개발 -- ERP 모듈 구현 시작 -- E2E 테스트 작성 -``` - ---- - -## 📊 위험도 매트릭스 - -| 위험 요소 | 발생 확률 | 영향도 | 우선순위 | 대응 전략 | -|---------|---------|--------|---------|---------| -| 멀티테넌시 + i18n 충돌 | 중간 | 높음 | 🔴 P1 | 서브도메인 전략 채택 | -| 미들웨어 성능 저하 | 중간 | 중간 | 🟡 P2 | 체이닝, 캐싱 최적화 | -| DB 스키마 복잡도 | 낮음 | 중간 | 🟡 P2 | 번역 테이블 패턴 | -| 인증 통합 충돌 | 중간 | 중간 | 🟡 P2 | 순서 정의, 테스트 | -| 빌드 시간 증가 | 중간 | 낮음 | 🟢 P3 | ISR, 하이브리드 렌더링 | -| UI 라이브러리 충돌 | 낮음 | 낮음 | 🟢 P3 | shadcn/ui 선택 | -| 상태 관리 복잡도 | 낮음 | 낮음 | 🟢 P3 | Zustand 권장 | - ---- - -## 🚀 즉시 적용 가능한 개선 사항 - -### 1. 미들웨어 체이닝 유틸리티 추가 - -```typescript -// src/lib/middleware-chain.ts -import { NextRequest, NextResponse } from 'next/server'; - -type Middleware = (request: NextRequest) => NextResponse | Promise; - -export function chainMiddleware(middlewares: Middleware[]) { - return async (request: NextRequest) => { - let response = NextResponse.next(); - - for (const middleware of middlewares) { - response = await middleware(request); - - // 리다이렉트나 에러 응답 시 체인 중단 - if (response.status !== 200) { - return response; - } - } - - return response; - }; -} -``` - -### 2. 환경 변수 검증 - -```typescript -// src/lib/env.ts -import { z } from 'zod'; - -const envSchema = z.object({ - NODE_ENV: z.enum(['development', 'production', 'test']), - DATABASE_URL: z.string().url(), - NEXTAUTH_SECRET: z.string().min(32), - NEXTAUTH_URL: z.string().url(), -}); - -export const env = envSchema.parse(process.env); -``` - -### 3. 타입 안전성 강화 - -```typescript -// src/types/tenant.ts -export type TenantId = string & { readonly __brand: 'TenantId' }; - -export function createTenantId(id: string): TenantId { - return id as TenantId; -} - -// 사용 예 -const tenantId = createTenantId('acme-corp'); -// 일반 string과 혼용 불가 → 타입 안전성 -``` - ---- - -## 📞 의사결정이 필요한 사항 - -### 즉시 결정 필요 (개발 시작 전) - -1. **멀티 테넌시 전략** - - [ ] 서브도메인 방식 (`{tenant}.domain.com`) - - [ ] URL 기반 방식 (`/[tenant]`) - - [ ] 하이브리드 (개발: URL, 프로덕션: 서브도메인) - -2. **데이터베이스** - - [ ] PostgreSQL - - [ ] MySQL - - [ ] Supabase (PostgreSQL + Auth) - -3. **인증 시스템** - - [ ] NextAuth.js (오픈소스) - - [ ] Clerk (상용) - - [ ] Supabase Auth - - [ ] 자체 구현 - -4. **배포 플랫폼** - - [ ] Vercel - - [ ] AWS - - [ ] Google Cloud - - [ ] Azure - -### 개발 중 결정 가능 - -5. **UI 컴포넌트 라이브러리** -6. **상태 관리 라이브러리** -7. **차트 라이브러리** (Recharts, Chart.js 등) - -### ✅ 이미 결정됨 - -- **폼 라이브러리**: React Hook Form + Zod (타입 안전성, 성능, 다국어 지원) - ---- - -## 🎯 결론 및 권장사항 - -### ✅ 현재 기반 설정은 프로덕션 준비 완료 - -현재 구성된 **Next.js 15.5.6 + Laravel Sanctum + next-intl + React Hook Form + Zod + TypeScript** 기반은 **멀티 테넌트 ERP 시스템 개발에 최적화**되었습니다. - -**주요 강점**: -- ✅ Next.js 15.5.6: 안정적이고 검증된 버전 (middleware 경고 없음) -- ✅ Laravel Sanctum: 토큰 기반 인증으로 프론트엔드/백엔드 완전 분리 -- ✅ next-intl 4.4.0: 다국어 지원 완벽 통합 -- ✅ React Hook Form + Zod: 타입 안전한 폼 관리 및 유효성 검증 -- ✅ React 19.2.0: 최신 기능 활용 가능 -- ✅ Tailwind CSS 4.x: 최신 스타일링 시스템 - -### ⚠️ 주의가 필요한 영역 - -1. **멀티테넌시 URL 구조** → 서브도메인 방식 권장 -2. **미들웨어 복잡도 관리** → 체이닝 패턴 도입 필요 -3. **Laravel API 엔드포인트 설정** → 환경 변수 구성 필수 - -### 🚦 진행 가능 여부 - -**판정**: ✅ **즉시 진행 가능** - -**충족 조건**: -- ✅ 안정적인 기술 스택 (Next.js 15.5.6) -- ✅ 명확한 아키텍처 분리 (Frontend/Backend) -- ✅ 다국어 지원 구조 완성 -- ✅ 인증 플로우 설계 완료 - -**진행 전 결정 필요**: -- 멀티 테넌시 전략 (서브도메인 vs URL 기반) -- Laravel API URL 환경 변수 설정 - -### 📋 Next Steps - -1. **즉시**: 멀티 테넌시 전략 결정 + Laravel API URL 설정 -2. **1주차**: 미들웨어 체이닝 구현 + 환경 변수 구성 -3. **2주차**: Laravel API 통합 테스트 + 인증 플로우 검증 -4. **3주차**: 첫 ERP 모듈 구현 시작 -5. **4주차**: UI 컴포넌트 라이브러리 통합 (shadcn/ui 권장) - ---- - -**문서 유효기간**: 2025-11-06 ~ 2025-12-06 (1개월) -**다음 리뷰**: 설계 가이드 통합 후 또는 주요 아키텍처 변경 시 - -**작성자**: Claude Code -**승인 필요**: 프로젝트 매니저, 시니어 개발자 - ---- - -## 관련 파일 - -### 프론트엔드 -- `src/middleware.ts` - 통합 미들웨어 (i18n, 인증, 봇 차단) -- `src/contexts/AuthContext.tsx` - 인증 상태 관리 Context -- `src/contexts/ItemMasterContext.tsx` - 품목 마스터 데이터 Context -- `src/lib/api/client.ts` - 통합 HTTP 클라이언트 -- `src/i18n/routing.ts` - 다국어 라우팅 설정 -- `src/messages/*.json` - 다국어 번역 파일 (ko, en, ja) - -### 설정 파일 -- `next.config.ts` - Next.js 설정 -- `.env.local` - 환경 변수 (API URL, 인증 설정) -- `tsconfig.json` - TypeScript 설정 -- `tailwind.config.ts` - Tailwind CSS 설정 - -### 참조 문서 -- `claudedocs/auth/[IMPL-2025-11-07] authentication-implementation-guide.md` - 인증 구현 가이드 -- `claudedocs/architecture/[REF-2025-11-19] multi-tenancy-implementation.md` - 멀티테넌시 구현 \ No newline at end of file diff --git a/claudedocs/architecture/[REF] technical-decisions.md b/claudedocs/architecture/[REF] technical-decisions.md deleted file mode 100644 index 2e12c4e9..00000000 --- a/claudedocs/architecture/[REF] technical-decisions.md +++ /dev/null @@ -1,316 +0,0 @@ -# 프로젝트 기술 결정 사항 - -> `_index.md`에서 분리됨 (2026-02-23). 프로젝트 전반의 기술 선택 배경과 근거를 기록. - ---- - -### `` 태그 사용 — `next/image` 미사용 이유 (2026-02-10) - -**현황**: 프로젝트 전체 `` 태그 10건, `next/image` 0건 - -**결정**: `` 유지, `next/image` 전환 불필요 - -**근거**: -1. **폐쇄형 ERP 시스템** — SEO 불필요, LCP 점수 무의미 -2. **전량 외부 동적 이미지** — 백엔드 API에서 받아오는 URL (정적 내부 이미지 0건) -3. **프린트/문서 레이아웃** — 10건 중 8건이 검사 기준서·도해 등 인쇄용. `next/image`의 `width`/`height` 강제 지정이 프린트 레이아웃을 깰 위험 -4. **blob URL 비호환** — 업로드 미리보기(blob:)는 `next/image`가 지원 안 함 -5. **설정 부담 > 이점** — `remotePatterns` 설정 + 백엔드 도메인 관리 비용이 실질 이점보다 큼 - -### 모바일 헤더 `backdrop-filter` 깜빡임 수정 (2026-02-11) - -**현상**: 모바일(Safari/Chrome)에서 sticky 헤더가 스크롤 시 투명↔불투명 깜빡임 발생. PC 브라우저 축소로는 재현 불가, 실제 모바일 기기에서만 발생. - -**원인 2가지**: -1. `globals.css`에 `* { transition: all 0.2s }` — 전체 요소의 모든 CSS 속성에 전역 transition. 모바일 스크롤 리페인트 시 background/opacity가 매번 애니메이션 -2. 모바일 헤더의 `clean-glass` 클래스: `backdrop-filter: blur(8px)` + `background: rgba(255,255,255, 0.95)` 조합이 모바일 sticky 요소에서 GPU 컴포지팅 충돌 - -**수정**: -- `globals.css`: `*` 전역 transition → `button, a, input, select, textarea, [role]` 인터랙티브 요소만, `transition: all` → `color, background-color, border-color, box-shadow` 속성만 -- 모바일 헤더: `clean-glass` (반투명+blur) → `bg-background border border-border` (불투명 배경) - -**교훈**: -- `transition: all`은 절대 `*`에 걸지 않기. 모바일 성능 저하 + 의도치 않은 애니메이션 발생 -- `backdrop-filter: blur()` + `sticky` 조합은 모바일 브라우저 고질적 리페인트 버그. 모바일 헤더는 불투명 배경 사용 -- 0.95 투명도는 육안 구분 불가 → 불투명 처리해도 시각적 차이 없음 - -**사용처 (9개 파일)**: -| 파일 | 용도 | 이미지 소스 | -|------|------|-------------| -| `DocumentHeader.tsx` (2건) | 문서 헤더 로고 | `logo.imageUrl` (API) | -| `ProductInspectionInputModal.tsx` | 제품검사 사진 미리보기 | blob URL | -| `ProductInspectionDocument.tsx` | 제품검사 문서 | `data.productImage` (API) | -| `inspection-shared.tsx` | 검사 기준서 이미지 | `standardImage` (API) | -| `SlatInspectionContent.tsx` | 도해 이미지 | `schematicImage` (API) | -| `ScreenInspectionContent.tsx` | 도해 이미지 | `schematicImage` (API) | -| `BendingInspectionContent.tsx` | 도해 이미지 | `schematicImage` (API) | -| `SlatJointBarInspectionContent.tsx` | 도해 이미지 | `schematicImage` (API) | -| `BendingWipInspectionContent.tsx` | 도해 이미지 | `schematicImage` (API) | - -**참고**: `next/image`가 유효한 케이스는 공개 사이트 + 정적/내부 이미지 + SEO 중요한 상황 - -### `next/dynamic` 코드 스플리팅 적용 (2026-02-10) - -**결정**: 대형 컴포넌트 + 무거운 라이브러리에 `next/dynamic` / 동적 `import()` 적용 - -**핵심 개념 — Suspense vs dynamic()**: -- **`Suspense` + 정적 import** → 코드가 부모와 같은 번들 청크에 포함. 유저가 안 봐도 이미 다운로드됨. UI fallback만 제공하고 **코드 분할은 안 일어남** -- **`dynamic()`** → webpack이 별도 `.js` 청크로 분리. 컴포넌트가 실제 렌더될 때만 네트워크 요청으로 해당 청크 다운로드. **진짜 코드 분할** - -**적용 내역**: - -| 파일 | 대상 | 절감 | -|------|------|------| -| `reports/comprehensive-analysis/page.tsx` | MainDashboard (2,651줄 + recharts) | ~350KB | -| `components/business/Dashboard.tsx` | CEODashboard | ~200KB | -| `construction/ConstructionDashboard.tsx` | ConstructionMainDashboard | ~100KB | -| `production/dashboard/page.tsx` | ProductionDashboard | ~100KB | -| `lib/utils/excel-download.ts` | xlsx 라이브러리 (~400KB) | ~400KB | -| `quotes/LocationListPanel.tsx` | xlsx 직접 import 제거 | (위와 중복) | - -**xlsx 동적 로드 패턴**: -```typescript -// Before: 모든 페이지에 xlsx ~400KB 포함 -import * as XLSX from 'xlsx'; - -// After: 엑셀 버튼 클릭 시에만 로드 -async function loadXLSX() { - return await import('xlsx'); -} -export async function downloadExcel(...) { - const XLSX = await loadXLSX(); - // ... -} -``` - -**총 절감**: 초기 번들에서 ~850KB 제외 (대시보드 미방문 + 엑셀 미사용 시) - -### 테이블 가상화 (react-window) — 보류 (2026-02-10) - -**결정**: 현시점 도입 불필요, 성능 이슈 발생 시 검토 - -**근거**: -1. **페이지네이션 사용 중** — 리스트 페이지 대부분 서버 사이드 페이지네이션 (20~50건/페이지). 50개 ``은 브라우저가 문제없이 처리 -2. **적용 복잡도 높음** — 테이블 헤더 고정, 체크박스 선택, `rowSpan/colSpan` 병합 등 기존 기능과 충돌 가능. DataTable + IntegratedListTemplateV2 + UniversalListPage 전부 수정 필요 -3. **YAGNI** — 500건 이상 한 번에 렌더링하는 페이지가 현재 없음 - -**도입 시점**: 한 페이지에 200건+ 데이터를 페이지네이션 없이 표시해야 하는 요구가 생길 때 - -### SWR / React Query — 보류 (2026-02-10) - -**결정**: 현시점 도입 불필요, 성능 이슈 발생 시 검토 - -**근거**: -1. **기존 패턴 안정화 완료** — `useEffect` + Server Action 호출 패턴이 전 페이지에 일관 적용됨 -2. **캐싱 니즈 낮음** — 폐쇄형 ERP 특성상 항상 최신 데이터 필요. stale 데이터 표시는 오히려 위험 -3. **마스터데이터 캐싱 구현됨** — Zustand (`stores/masterDataStore`)로 변경 빈도 낮은 데이터는 이미 캐싱 중 -4. **도입 비용 과다** — 수십 개 페이지 `useState`+`useEffect` 패턴 전면 리팩토링 + 팀 학습 비용 - -**도입 시점**: 동일 데이터를 여러 컴포넌트에서 동시 요구하거나, 목록 ↔ 상세 이동 시 재로딩이 체감될 때 - -### 컴포넌트 레지스트리 관계도 (2026-02-12) - -**구현**: `/dev/component-registry` 페이지에 관계도(카드형 플로우) 뷰 추가 - -**구성**: -- `actions.ts` — `extractComponentImports()` + `buildRelationships()`로 import 관계 양방향 파싱 (imports/usedBy) -- `ComponentRelationshipView.tsx` — 3칼럼 카드형 플로우 (사용처 → 선택 컴포넌트 → 구성요소) -- `ComponentRegistryClient.tsx` — 목록/관계도 뷰 토글 - -**활용 규칙** (CLAUDE.md에 추가됨): -- 새 컴포넌트 생성 전 → 목록에서 중복 검색 + 관계도에서 조합 패턴 확인 -- 기존 컴포넌트 수정 시 → usedBy로 영향 범위 파악 - -### Action 팩토리 패턴 — 신규 CRUD 적용 규칙 (2026-02-10) - -**결정**: 기존 84개 actions.ts 전면 전환은 하지 않음. **신규 CRUD 도메인에만 팩토리 사용** - -**현황**: -- `src/lib/api/create-crud-service.ts` (177줄) — CRUD 보일러플레이트 자동 생성 팩토리 -- 현재 사용 중: TitleManagement, RankManagement (2개) -- 전환 가능: 15~20개 / 전환 불가 (커스텀 로직): 50+개 - -**규칙**: -- 신규 도메인 추가 시 단순 CRUD → `createCrudService` 사용 필수 -- 기존 actions.ts는 잘 동작하므로 무리하게 전환하지 않음 -- 커스텀 비즈니스 로직이 있는 도메인(견적, 수주, 생산 등)은 팩토리 비적합 - -**사용 예시**: -```typescript -import { createCrudService } from '@/lib/api/create-crud-service'; - -const service = createCrudService({ - basePath: '/api/v1/resources', - transform: (api) => ({ id: api.id, name: api.name }), - entityName: '리소스', -}); - -export const getList = service.getList; -export const getById = service.getById; -export const create = service.create; -export const update = service.update; -export const remove = service.remove; -``` - -**미전환 사유**: 84개 중 전환 가능 15~20개, 작업 2~4시간 대비 기능 변화 없음. 시간 대비 효율 낮음 - -### Server Action 공통 유틸리티 — 전체 마이그레이션 완료 (2026-02-12) - -**결정**: `buildApiUrl()` 전체 43개 actions.ts에 적용 완료 - -**배경**: -- 89개 actions.ts 중 43개에서 동일한 URLSearchParams 조건부 `.set()` 패턴 반복 (326+ 건) -- 50+ 파일에서 `current_page → currentPage` 수동 변환 반복 -- `toPaginationMeta`가 `src/lib/api/types.ts`에 존재하나 import 0건 - -**생성된 유틸리티**: -1. `src/lib/api/query-params.ts` — `buildQueryParams()`, `buildApiUrl()`: URLSearchParams 보일러플레이트 제거 -2. `src/lib/api/execute-paginated-action.ts` — `executePaginatedAction()`: 페이지네이션 조회 패턴 통합 (내부에서 `toPaginationMeta` 사용) - -**마이그레이션 결과** (2026-02-12): -- `new URLSearchParams` 사용: 326건 → **0건** (actions.ts 기준) -- `const API_URL = process.env.NEXT_PUBLIC_API_URL` 선언: 43개 → **0개** (마이그레이션 대상 파일) -- `buildApiUrl()` import: 43개 actions.ts 전체 적용 -- 3가지 API_URL 패턴 통합: 표준(`process.env`), `/api` 접미사(HR), `API_BASE` 전체경로(품질) → 모두 `buildApiUrl('/api/v1/...')` 통일 - -**`executePaginatedAction` 마이그레이션** (2026-02-12): -- 14개 actions.ts에서 페이지네이션 목록 조회 함수를 `executePaginatedAction`으로 전환 -- Wave A (accounting 9개): BillManagement, DepositManagement, SalesManagement, PurchaseManagement, WithdrawalManagement, VendorLedger, CardTransactionInquiry, BankTransactionInquiry, ExpectedExpenseManagement -- Wave B (5개): PaymentHistoryManagement, StockStatus, ReceivingManagement, ShipmentManagement, quotes -- 제외 5개: AccountManagement(`meta` 필드명), orders(`data.items` 중첩), VacationManagement, EmployeeManagement, construction/order-management (별도 구조) -- 순 감소: ~220줄 (14파일 × ~20줄 제거, ~28줄 추가) -- 제거된 보일러플레이트: `DEFAULT_PAGINATION`, `FrontendPagination`/`PaginationMeta` 로컬 인터페이스, `PaginatedApiResponse` import, 수동 transform+pagination 조립 -- **화면 검수 완료** (4개 페이지): Bills, StockStatus, Quotes, Shipments — 전체 PASS -- **버그 발견/수정**: `quotes/actions.ts`에서 `export type { PaginationMeta }` re-export가 Turbopack 런타임 에러 유발 (`tsc`로 미감지) → re-export 제거, 컴포넌트에서 `@/lib/api/types` 직접 import로 변경 - -### `'use server'` 파일 타입 export 제한 (2026-02-12) - -**발견 배경**: `executePaginatedAction` 마이그레이션 화면 검수 중 견적관리 페이지 빌드 에러 - -**제한 사항**: -- `'use server'` 파일에서는 **async 함수만 export 가능** (Next.js Turbopack 제한) -- `export type { X } from '...'` (re-export) → **런타임 에러 발생** -- `export interface X { ... }` / `export type X = ...` (인라인 정의) → **문제 없음** (컴파일 시 제거) -- `tsc --noEmit`으로는 감지 불가 — Next.js 전용 규칙이므로 실제 페이지 접속(Turbopack)에서만 발생 - -**현재 상태**: 전체 81개 `'use server'` 파일 점검 완료, re-export 패턴 0건 (수정된 1건 포함) - -**buildApiUrl 마이그레이션 전략**: -- Wave A: 1건짜리 단순 파일 20개 -- Wave B: 2건짜리 파일 12개 (quotes, WorkOrders, orders 등 대형 파일 포함) -- Wave C: 3건 이상 파일 12개 (VendorLedger 5건, ReceivingManagement 5건, ProcessManagement 19건 URL 등) - -**효과**: -- 페이지네이션 조회 코드: ~20줄 → ~5줄 -- `DEFAULT_PAGINATION` 중앙화 (`execute-paginated-action.ts` 내부) -- `toPaginationMeta` 자동 활용 (직접 import 불필요) -- URL 빌딩 패턴 완전 일관화 (undefined/null/'' 자동 필터링, boolean/number 자동 변환) - -### KST 안전 날짜 유틸리티 — `toISOString` 사용 금지 (2026-02-19) - -**현황**: `new Date().toISOString().split('T')[0]` — 15개 파일 26곳에서 사용 중이었음 - -**문제**: `toISOString()`은 **UTC 기준**으로 변환. 한국(KST, UTC+9)에서 오전 9시 이전에 실행하면 **전날 날짜** 반환 -``` -// 2026-02-19 08:30 KST → UTC는 2026-02-18 23:30 -new Date().toISOString().split('T')[0] // "2026-02-18" ← 잘못됨 -``` - -**결정**: KST 안전 유틸리티 함수로 전량 교체, 직접 `toISOString` 사용 금지 - -**유틸리티** (`src/lib/utils/date.ts`): -| 함수 | 용도 | 대체 대상 | -|------|------|-----------| -| `getTodayString()` | 오늘 날짜 문자열 | `new Date().toISOString().split('T')[0]` | -| `getLocalDateString(date)` | 임의 Date 객체 문자열 | `someDate.toISOString().split('T')[0]` | - -**사용 규칙**: -```typescript -// 올바른 패턴 -import { getTodayString, getLocalDateString } from '@/lib/utils/date'; -const today = getTodayString(); // "2026-02-19" -const thirtyDaysAgo = getLocalDateString(pastDate); // "2026-01-20" - -// 금지 패턴 -const today = new Date().toISOString().split('T')[0]; -``` - -**현재 상태**: `src/` 내 `toISOString().split` 사용 0건 (date.ts 내 구현부 제외) - -### 달력/스케줄 공통 리소스 — 작업 전 필수 확인 (2026-02-23) - -달력·일정·날짜 관련 작업 시 아래 공통 리소스를 **반드시 확인**하고 사용할 것. - -**날짜 유틸리티** (`src/lib/utils/date.ts`): -| 함수 | 용도 | -|------|------| -| `getLocalDateString(date)` | Date → `'YYYY-MM-DD'` (KST 안전) | -| `getTodayString()` | 오늘 날짜 문자열 | -| `formatDate(dateStr)` | 표시용 날짜 포맷 (null → `'-'`) | -| `formatDateForInput(dateStr)` | input용 `YYYY-MM-DD` 변환 | -| `formatDateRange(start, end)` | `'시작 ~ 종료'` 포맷 | -| `getDateAfterDays(n)` | N일 후 날짜 | - -**달력 일정 스토어** (`src/stores/useCalendarScheduleStore.ts`): -- 달력관리(CalendarManagement)에서 등록한 공휴일/세무일정/회사일정을 프로젝트 전체에 공유 -- `fetchSchedules(year)` — 연도별 캐시 조회 (API 호출) -- `setSchedulesForYear(year, data)` — 이미 가져온 데이터 직접 설정 -- `invalidateYear(year)` — 캐시 무효화 (등록/수정/삭제 후) -- **현재 상태**: 백엔드 API 미구현 → 호출부 주석 처리 (TODO 검색) - -**달력 이벤트 유틸** (`src/constants/calendarEvents.ts`): -- `isHoliday(date)`, `isTaxDeadline(date)`, `getHolidayName(date)` 등 -- 스토어 우선 → 하드코딩 폴백(2026년) 패턴 -- 새 연도 폴백 데이터 필요 시 이 파일에 `HOLIDAYS_YYYY`, `TAX_DEADLINES_YYYY` 추가 - -**ScheduleCalendar 공통 컴포넌트** (`src/components/common/ScheduleCalendar/`): -- `hideNavigation` prop으로 헤더 숨김 가능 (연간 달력 등 상위 네비게이션 사용 시) -- `availableViews={[]}` 으로 뷰 전환 버튼 숨김 - -**규칙**: -- `Date → string` 변환 시 `getLocalDateString()` 필수 (`toISOString().split('T')[0]` 금지) -- 공휴일/세무일 판별 시 `calendarEvents.ts` 유틸 함수 사용 -- 달력 데이터 공유 시 zustand 스토어 경유 (컴포넌트 간 직접 전달 금지) - -### `useDateRange` 훅 — 날짜 필터 보일러플레이트 제거 (2026-02-19) - -**현황**: 20+ 리스트 페이지에서 `useState('2025-01-01')` / `useState('2025-12-31')` 하드코딩 - -**문제**: 연도가 바뀌면 수동으로 모든 파일 수정 필요 (2025→2026 전환 시 데이터 미표시 버그 발생) - -**결정**: `useDateRange` 훅으로 동적 날짜 범위 자동 계산 - -**훅** (`src/hooks/useDateRange.ts`): -```typescript -import { useDateRange } from '@/hooks'; - -// 프리셋 -const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentYear'); // 2026-01-01 ~ 2026-12-31 -const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentMonth'); // 2026-02-01 ~ 2026-02-28 -const { startDate, endDate, setStartDate, setEndDate } = useDateRange('today'); // 2026-02-19 ~ 2026-02-19 -``` - -**적용 규칙**: -- 리스트 페이지 날짜 필터 → `useDateRange` 필수 사용 -- 연간 조회 → `'currentYear'`, 월간 조회 → `'currentMonth'` -- `useState('YYYY-MM-DD')` 하드코딩 금지 - -**현재 상태**: `useState('2025` 패턴 0건 (전량 `useDateRange`로 전환 완료) - -### Zod 스키마 검증 — 신규 폼 적용 규칙 (2026-02-11) - -**결정**: 기존 폼은 건드리지 않음. **신규 폼에만 Zod + zodResolver 적용** - -**설치 상태**: `zod@^4.1.12`, `@hookform/resolvers@^5.2.2` — 이미 설치됨 - -**효과**: -1. 스키마 하나로 **타입 추론 + 런타임 검증** 동시 해결 (`z.infer`) -2. 별도 `interface` 중복 정의 불필요 -3. 신규 코드에서 `as` 캐스트 자연 감소 (D-2 개선 효과) - -**규칙**: -- 신규 폼 → `zodResolver(schema)` 사용 필수 (CLAUDE.md에 패턴 명시) -- 기존 `rules={{ required: true }}` 패턴 폼 → 마이그레이션 불필요 -- 단순 1~2 필드 인라인 폼 → Zod 불필요 (오버엔지니어링) - -**미적용 사유**: 기존 폼 수십 개를 전면 전환하는 비용 >> 이득. 신규 코드에서 점진적 확산 diff --git a/claudedocs/architecture/[REF] template-migration-status.md b/claudedocs/architecture/[REF] template-migration-status.md deleted file mode 100644 index d688cf23..00000000 --- a/claudedocs/architecture/[REF] template-migration-status.md +++ /dev/null @@ -1,260 +0,0 @@ -# 템플릿 마이그레이션 현황 - -> 작성일: 2025-01-20 -> 목적: IntegratedListTemplate / IntegratedDetailTemplate 적용 현황 파악 - ---- - -## 📊 전체 통계 - -| 구분 | 수량 | -|------|------| -| 전체 Protected 페이지 | 203개 | -| IntegratedListTemplate 사용 | 48개 | -| IntegratedDetailTemplate 사용 | 57개 | - ---- - -## ✅ 마이그레이션 완료 - -### 리스트 페이지 (IntegratedListTemplateV2) -대부분의 리스트 페이지가 IntegratedListTemplateV2로 마이그레이션 완료. -- 필터, 테이블, 페이지네이션, 헤더 버튼 공통화 적용 - -### 상세/수정/등록 페이지 (IntegratedDetailTemplate) - -#### App 페이지 (17개) -``` -src/app/[locale]/(protected)/settings/popup-management/new/page.tsx -src/app/[locale]/(protected)/settings/popup-management/[id]/page.tsx -src/app/[locale]/(protected)/settings/accounts/new/page.tsx -src/app/[locale]/(protected)/settings/accounts/[id]/page.tsx -src/app/[locale]/(protected)/sales/client-management-sales-admin/new/page.tsx -src/app/[locale]/(protected)/sales/client-management-sales-admin/[id]/page.tsx -src/app/[locale]/(protected)/sales/quote-management/test/[id]/page.tsx -src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx -src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx -src/app/[locale]/(protected)/board/board-management/new/page.tsx -src/app/[locale]/(protected)/board/board-management/[id]/page.tsx -src/app/[locale]/(protected)/master-data/process-management/new/page.tsx -src/app/[locale]/(protected)/master-data/process-management/[id]/page.tsx -src/app/[locale]/(protected)/hr/card-management/new/page.tsx -src/app/[locale]/(protected)/hr/card-management/[id]/edit/page.tsx -src/app/[locale]/(protected)/hr/card-management/[id]/page.tsx -src/app/[locale]/(protected)/construction/order/base-info/labor/[id]/page.tsx -``` - -#### 컴포넌트 (주요 40개) -``` -# 회계 -src/components/accounting/BillManagement/BillDetail.tsx -src/components/accounting/SalesManagement/SalesDetail.tsx -src/components/accounting/PurchaseManagement/PurchaseDetail.tsx -src/components/accounting/VendorLedger/VendorLedgerDetail.tsx -src/components/accounting/VendorManagement/VendorDetail.tsx -src/components/accounting/BadDebtCollection/BadDebtDetail.tsx -src/components/accounting/WithdrawalManagement/WithdrawalDetailClientV2.tsx -src/components/accounting/DepositManagement/DepositDetailClientV2.tsx - -# 영업/고객 -src/components/clients/ClientDetailClientV2.tsx -src/components/quotes/QuoteRegistrationV2.tsx -src/components/orders/OrderSalesDetailView.tsx -src/components/orders/OrderSalesDetailEdit.tsx - -# 설정 -src/components/settings/PopupManagement/PopupDetailClientV2.tsx -src/components/settings/PermissionManagement/PermissionDetail.tsx - -# 건설/프로젝트 -src/components/business/construction/contract/ContractDetailForm.tsx -src/components/business/construction/site-briefings/SiteBriefingForm.tsx -src/components/business/construction/order-management/OrderDetailForm.tsx -src/components/business/construction/handover-report/HandoverReportDetailForm.tsx -src/components/business/construction/item-management/ItemDetailClient.tsx -src/components/business/construction/estimates/EstimateDetailForm.tsx -src/components/business/construction/management/ConstructionDetailClient.tsx -src/components/business/construction/site-management/SiteDetailForm.tsx -src/components/business/construction/partners/PartnerForm.tsx -src/components/business/construction/structure-review/StructureReviewDetailForm.tsx -src/components/business/construction/issue-management/IssueDetailForm.tsx -src/components/business/construction/bidding/BiddingDetailForm.tsx -src/components/business/construction/pricing-management/PricingDetailClientV2.tsx -src/components/business/construction/labor-management/LaborDetailClientV2.tsx -src/components/business/construction/progress-billing/ProgressBillingDetailForm.tsx - -# 고객센터 -src/components/customer-center/NoticeManagement/NoticeDetail.tsx -src/components/customer-center/InquiryManagement/InquiryDetail.tsx -src/components/customer-center/EventManagement/EventDetail.tsx - -# HR -src/components/hr/EmployeeManagement/EmployeeDetail.tsx - -# 생산/물류 -src/components/production/WorkOrders/WorkOrderDetail.tsx -src/components/outbound/ShipmentManagement/ShipmentDetail.tsx -src/components/material/ReceivingManagement/ReceivingDetail.tsx -src/components/material/StockStatus/StockStatusDetail.tsx - -# 품질 -src/components/quality/InspectionManagement/InspectionDetail.tsx -``` - ---- - -## ❌ 마이그레이션 미완료 - -### App 페이지 (PageLayout 직접 사용) - -| 경로 | 유형 | 비고 | -|------|------|------| -| `sales/order-management-sales/production-orders/[id]/page.tsx` | 상세 | 생산지시 상세 | -| `sales/order-management-sales/[id]/production-order/page.tsx` | 상세 | 생산지시 | -| `boards/[boardCode]/create/page.tsx` | 등록 | 게시판 글쓰기 | -| `boards/[boardCode]/[postId]/edit/page.tsx` | 수정 | 게시판 글수정 | -| `boards/[boardCode]/[postId]/page.tsx` | 상세 | 게시판 글상세 | -| `test/popup/page.tsx` | 테스트 | 테스트 페이지 | -| `dev/editable-table/page.tsx` | 개발 | 개발용 페이지 | - -### 컴포넌트 (PageLayout 직접 사용) - -#### 회계 -``` -src/components/accounting/DailyReport/index.tsx # 일일보고서 (특수 레이아웃) -src/components/accounting/ReceivablesStatus/index.tsx # 미수금현황 (특수 레이아웃) -src/components/accounting/WithdrawalManagement/WithdrawalDetail.tsx # V2로 대체됨 -src/components/accounting/DepositManagement/DepositDetail.tsx # V2로 대체됨 -``` - -#### 설정 -``` -src/components/settings/CompanyInfoManagement/index.tsx # 회사정보 (설정 페이지) -src/components/settings/RankManagement/index.tsx # 직급관리 (설정 페이지) -src/components/settings/LeavePolicyManagement/index.tsx # 휴가정책 (설정 페이지) -src/components/settings/AccountInfoManagement/index.tsx # 계정정보 (설정 페이지) -src/components/settings/NotificationSettings/index.tsx # 알림설정 (설정 페이지) -src/components/settings/TitleManagement/index.tsx # 직책관리 (설정 페이지) -src/components/settings/WorkScheduleManagement/index.tsx # 근무일정 (설정 페이지) -src/components/settings/AttendanceSettingsManagement/index.tsx # 근태설정 (설정 페이지) -src/components/settings/PopupManagement/PopupForm.tsx # V2로 대체됨 -src/components/settings/PopupManagement/PopupDetail.tsx # V2로 대체됨 -src/components/settings/AccountManagement/AccountDetail.tsx # V2로 대체됨 -src/components/settings/PermissionManagement/PermissionDetailClient.tsx # 레거시 -src/components/settings/SubscriptionManagement/SubscriptionManagement.tsx # 구독관리 -src/components/settings/SubscriptionManagement/SubscriptionClient.tsx -``` - -#### 건설/프로젝트 -``` -src/components/business/construction/management/ProjectListClient.tsx # 리스트 (별도) -src/components/business/construction/management/ProjectDetailClient.tsx # 레거시 -src/components/business/construction/category-management/index.tsx # 카테고리 (특수) -src/components/business/construction/pricing-management/PricingDetailClient.tsx # V2로 대체됨 -src/components/business/construction/labor-management/LaborDetailClient.tsx # V2로 대체됨 -``` - -#### 게시판 -``` -src/components/board/BoardManagement/BoardForm.tsx # V2로 대체됨 -src/components/board/BoardManagement/BoardDetail.tsx # V2로 대체됨 -src/components/board/BoardDetail/index.tsx # 동적 게시판 상세 -src/components/board/BoardForm/index.tsx # 동적 게시판 폼 -``` - -#### HR -``` -src/components/hr/DepartmentManagement/index.tsx # 부서관리 (트리 구조) -src/components/hr/EmployeeManagement/EmployeeForm.tsx # 직원등록 폼 -src/components/hr/EmployeeManagement/CSVUploadPage.tsx # CSV 업로드 (특수) -``` - -#### 생산 -``` -src/components/production/ProductionDashboard/index.tsx # 대시보드 (제외) -src/components/production/WorkerScreen/index.tsx # 작업자화면 (특수 UI) -src/components/production/WorkOrders/WorkOrderCreate.tsx # 작업지시 등록 -src/components/production/WorkOrders/WorkOrderEdit.tsx # 작업지시 수정 -``` - -#### 고객센터 -``` -src/components/customer-center/InquiryManagement/InquiryForm.tsx # 문의등록 -src/components/customer-center/FAQManagement/FAQList.tsx # FAQ 리스트 -``` - -#### 기타 -``` -src/components/clients/ClientDetail.tsx # V2로 대체됨 -src/components/process-management/ProcessForm.tsx # 공정등록 -src/components/process-management/ProcessDetail.tsx # V2로 대체됨 -src/components/outbound/ShipmentManagement/ShipmentEdit.tsx # 출고수정 -src/components/outbound/ShipmentManagement/ShipmentCreate.tsx # 출고등록 -src/components/items/ItemMasterDataManagement.tsx # 품목마스터 (특수) -src/components/material/ReceivingManagement/InspectionCreate.tsx # 검수등록 -src/components/quality/InspectionManagement/InspectionCreate.tsx # 품질검사등록 -``` - ---- - -## 🚫 마이그레이션 제외 대상 - -### 대시보드/특수 페이지 -``` -src/app/[locale]/(protected)/dashboard/page.tsx # CEO 대시보드 -src/app/[locale]/(protected)/production/dashboard/page.tsx # 생산 대시보드 -src/app/[locale]/(protected)/reports/comprehensive-analysis/page.tsx # 종합분석 -src/components/business/CEODashboard/CEODashboard.tsx # CEO 대시보드 -``` - -### 레거시 파일 (_legacy 폴더) -``` -src/components/settings/AccountManagement/_legacy/AccountDetail.tsx -src/components/hr/CardManagement/_legacy/CardDetail.tsx -src/components/hr/CardManagement/_legacy/CardForm.tsx -``` - -### 테스트/개발용 -``` -src/app/[locale]/(protected)/test/popup/page.tsx -src/app/[locale]/(protected)/dev/editable-table/page.tsx -``` - ---- - -## 📋 마이그레이션 우선순위 권장 - -### 높음 (실사용 페이지) -1. `boards/[boardCode]/*` - 동적 게시판 페이지들 -2. `production/WorkOrders/WorkOrderCreate.tsx` - 작업지시 등록 -3. `production/WorkOrders/WorkOrderEdit.tsx` - 작업지시 수정 -4. `outbound/ShipmentManagement/ShipmentCreate.tsx` - 출고 등록 -5. `outbound/ShipmentManagement/ShipmentEdit.tsx` - 출고 수정 - -### 중간 (설정 페이지 - 템플릿 적용 검토 필요) -- `settings/` 하위 관리 페이지들 (트리/특수 레이아웃 많음) - -### 낮음 (V2 대체 완료) -- V2 파일이 있는 레거시 컴포넌트들 (삭제 검토) - -### 제외 -- 대시보드, 특수 UI, 테스트/개발 페이지 - ---- - -## 🔧 템플릿 수정 시 일괄 적용 범위 - -템플릿 파일 수정 시 아래 파일들에 자동 적용: - -| 템플릿 | 영향 파일 수 | -|--------|-------------| -| `IntegratedListTemplateV2` | 48개 | -| `IntegratedDetailTemplate` | 57개 | -| **합계** | **105개** | - -수정 가능 요소: -- 타이틀 위치/스타일 -- 버튼 배치/디자인 -- 입력필드 공통 스타일 -- 레이아웃 구조 -- 반응형 처리 diff --git a/claudedocs/architecture/[RESEARCH-2026-02-11] ERP-admin-panel-architecture-patterns.md b/claudedocs/architecture/[RESEARCH-2026-02-11] ERP-admin-panel-architecture-patterns.md deleted file mode 100644 index 1ac3a59e..00000000 --- a/claudedocs/architecture/[RESEARCH-2026-02-11] ERP-admin-panel-architecture-patterns.md +++ /dev/null @@ -1,606 +0,0 @@ -# Research: Next.js / React ERP & Admin Panel Architecture Patterns (2025-2026) - -**Date**: 2026-02-11 -**Purpose**: Compare SAM ERP's current architecture against proven open-source patterns -**Confidence**: High (0.85) - Based on 6 major open-source projects and established methodologies - ---- - -## Executive Summary - -After investigating 6 major open-source admin/ERP frameworks and 3 architectural methodologies, the dominant pattern emerging in 2025-2026 is a **hybrid approach**: domain/feature-based folder organization combined with headless CRUD hooks and a provider-based API abstraction layer. Pure Atomic Design is losing ground to Feature-Sliced Design (FSD) for application-level organization, though Atomic Design remains useful for the shared UI component layer. - -### Key Findings - -1. **Resource-based CRUD abstraction** (react-admin, Refine) is the most proven pattern for 50+ page admin apps -2. **Feature/domain-based folder structure** is winning over layer-based (atoms/molecules/organisms) for application code -3. **Provider pattern** (dataProvider, authProvider) decouples UI from API more effectively than scattered Server Actions -4. **Config-driven UI generation** (Payload CMS) reduces code duplication for similar pages -5. **Headless hooks** (useListController, useTable, useForm) separate business logic from UI completely - ---- - -## 1. Project-by-Project Architecture Analysis - -### 1.1 React-Admin (marmelab) -- 25K+ GitHub Stars - -**Architecture**: Resource-based SPA with Provider pattern - -**Key Concepts**: -- **Resources**: The core abstraction. Each entity (posts, users, orders) is a "resource" with CRUD views -- **Providers**: Adapter layer between UI and backend - - `dataProvider` - abstracts all API calls (getList, getOne, create, update, delete) - - `authProvider` - handles authentication flow - - `i18nProvider` - internationalization -- **Headless Core**: `ra-core` package contains all hooks, zero UI dependency -- **Controller Hooks**: `useListController`, `useEditController`, `useCreateController`, `useShowController` - -**Folder Pattern**: -``` -src/ - resources/ - posts/ - PostList.tsx # view - PostEdit.tsx # view - PostCreate.tsx # view - PostShow.tsx # view - users/ - UserList.tsx - UserEdit.tsx - providers/ - dataProvider.ts # API abstraction - authProvider.ts # Auth abstraction - App.tsx # Resource registration -``` - -**CRUD Registration Pattern**: -```tsx - - - - -``` - -**SAM Comparison**: -| Aspect | react-admin | SAM ERP | -|--------|-------------|---------| -| API Layer | Centralized dataProvider | 89 scattered actions.ts files | -| CRUD Views | Resource-based registration | Manual page creation per domain | -| State | React Query (built-in) | Zustand + manual fetching | -| Form | react-hook-form (built-in) | Mixed (migrating to RHF+Zod) | - -**Sources**: -- [Architecture Docs](https://marmelab.com/react-admin/Architecture.html) -- [Resource Component](https://marmelab.com/react-admin/Resource.html) -- [CRUD Pages](https://marmelab.com/react-admin/CRUD.html) -- [GitHub](https://github.com/marmelab/react-admin) - ---- - -### 1.2 Refine -- 30K+ GitHub Stars - -**Architecture**: Headless meta-framework with resource-based CRUD - -**Key Concepts**: -- **Headless by design**: Zero UI opinion, works with Ant Design, Material UI, Shadcn, or custom -- **Data Provider Interface**: Standardized CRUD methods (getList, getOne, create, update, deleteOne) -- **Resource Hooks**: `useTable`, `useForm`, `useShow`, `useSelect` -- all headless -- **Inferencer**: Auto-generates CRUD pages from API schema - -**Data Provider Interface**: -```typescript -const dataProvider = { - getList: ({ resource, pagination, sorters, filters }) => Promise, - getOne: ({ resource, id }) => Promise, - create: ({ resource, variables }) => Promise, - update: ({ resource, id, variables }) => Promise, - deleteOne: ({ resource, id }) => Promise, - getMany: ({ resource, ids }) => Promise, - custom: ({ url, method, payload }) => Promise, -}; -``` - -**Headless Hook Pattern**: -```tsx -// useTable returns data + controls, you handle UI -const { tableProps, sorters, filters } = useTable({ resource: "products" }); - -// useForm returns form state + submit, you handle UI -const { formProps, saveButtonProps } = useForm({ resource: "products", action: "create" }); -``` - -**SAM Comparison**: -| Aspect | Refine | SAM ERP | -|--------|--------|---------| -| API Abstraction | Single dataProvider | Per-domain actions.ts | -| List Page | useTable hook | UniversalListPage template | -| Form | useForm hook (headless) | Manual per-page forms | -| Code Generation | Inferencer auto-gen | Manual creation | - -**Sources**: -- [Data Provider Docs](https://refine.dev/docs/data/data-provider/) -- [useTable Hook](https://refine.dev/docs/data/hooks/use-table/) -- [GitHub](https://github.com/refinedev/refine) - ---- - -### 1.3 Payload CMS 3.0 -- 30K+ GitHub Stars - -**Architecture**: Config-driven, Next.js-native with auto-generated admin UI - -**Key Concepts**: -- **Collection Config**: Define schema once, get admin UI + API + types automatically -- **Field System**: Rich field types auto-generate corresponding UI components -- **Hooks**: beforeChange, afterRead, beforeValidate at collection and field level -- **Access Control**: Document-level and field-level permissions in config -- **Next.js Native**: Installs directly into /app folder, uses Server Components - -**Config-Driven Pattern**: -```typescript -// collections/Products.ts -export const Products: CollectionConfig = { - slug: 'products', - admin: { - useAsTitle: 'name', - defaultColumns: ['name', 'price', 'status'], - }, - access: { - read: () => true, - create: isAdmin, - update: isAdminOrSelf, - }, - hooks: { - beforeChange: [calculateTotal], - afterRead: [formatCurrency], - }, - fields: [ - { name: 'name', type: 'text', required: true }, - { name: 'price', type: 'number', min: 0 }, - { name: 'status', type: 'select', options: ['draft', 'published'] }, - { name: 'category', type: 'relationship', relationTo: 'categories' }, - ], -}; -``` - -**SAM Comparison**: -| Aspect | Payload CMS | SAM ERP | -|--------|-------------|---------| -| Page Generation | Auto from config | Manual per page | -| Field Definitions | Centralized schema | Inline JSX per form | -| Access Control | Config-based per field | Manual per component | -| Type Safety | Auto-generated from schema | Manual interface definitions | - -**Sources**: -- [Collection Configs](https://payloadcms.com/docs/configuration/collections) -- [Fields Overview](https://payloadcms.com/docs/fields/overview) -- [Collection Hooks](https://payloadcms.com/docs/hooks/collections) -- [GitHub](https://github.com/payloadcms/payload) - ---- - -### 1.4 Medusa Admin v2 -- 26K+ GitHub Stars - -**Architecture**: Domain-based routes with widget injection system - -**Key Concepts**: -- **Domain Routes**: Routes organized by business domain (products, orders, customers) -- **Widget System**: Inject custom React components into predetermined zones -- **UI Routes**: File-based routing under src/admin/routes/ -- **Hook-based data fetching**: Domain-specific hooks for API integration -- **Monorepo**: UI library (@medusajs/ui) separate from admin logic - -**Folder Structure**: -``` -packages/admin/dashboard/src/ - routes/ - products/ - product-list/ - components/ - hooks/ - page.tsx - product-detail/ - components/ - hooks/ - page.tsx - orders/ - order-list/ - order-detail/ - customers/ - hooks/ # Shared hooks - components/ # Shared components - lib/ # Utilities -``` - -**SAM Comparison**: -| Aspect | Medusa Admin | SAM ERP | -|--------|-------------|---------| -| Route Organization | Domain > Action > Components | Domain > page.tsx + actions.ts | -| Shared Components | Separate UI package | organisms/molecules/atoms | -| Hooks | Per-route + shared | Global + inline | -| Extensibility | Widget injection zones | N/A | - -**Sources**: -- [Admin UI Routes](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes) -- [Admin Development](https://docs.medusajs.com/learn/fundamentals/admin) -- [GitHub](https://github.com/medusajs/medusa) - ---- - -### 1.5 AdminJS - -**Architecture**: Auto-generated admin from resource configuration - -**Key Concepts**: -- **Resource Registration**: Register database models, get admin UI automatically -- **Component Customization**: Override via ComponentLoader -- **Dashboard Customization**: Custom React components for dashboard - -**SAM Relevance**: Lower -- AdminJS is more backend-driven (Node.js ORM-based) and less applicable to a frontend-heavy ERP. - -**Sources**: -- [AdminJS Documentation](https://adminjs.co/) -- [GitHub](https://github.com/SoftwareBrothers/adminjs) - ---- - -### 1.6 Hoppscotch - -**Architecture**: Monorepo with shared-library pattern - -**Key Concepts**: -- **@hoppscotch/common**: 90% of UI and business logic in shared package -- **@hoppscotch/data**: Type safety across all layers -- **Platform-specific code**: Thin wrapper handling native capabilities - -**SAM Relevance**: The shared-library-as-core pattern is interesting for large codebases where most logic is platform-agnostic. - -**Sources**: -- [DeepWiki Analysis](https://deepwiki.com/hoppscotch/hoppscotch) - ---- - -## 2. Architectural Methodologies Comparison - -### 2.1 Feature-Sliced Design (FSD) -- Rising Standard - -**7-Layer Architecture**: -``` -app/ # App initialization, providers, routing -processes/ # Complex cross-page business flows (deprecated in latest) -pages/ # Full page compositions -widgets/ # Self-contained UI blocks with business logic -features/ # User-facing actions (login, add-to-cart) -entities/ # Business entities (user, product, order) -shared/ # Reusable utilities, UI kit, configs -``` - -**Key Rules**: -- Layers can ONLY import from layers below them -- Each layer divided into **slices** (domain groupings) -- Each slice divided into **segments** (ui/, model/, api/, lib/, config/) - -**FSD Applied to ERP**: -``` -src/ - app/ # App shell, providers - pages/ - quality-qms/ # QMS page composition - sales-quote/ # Quote page composition - widgets/ - inspection-report/ # Self-contained inspection UI - ui/ - model/ - api/ - quote-calculator/ - features/ - add-inspection-item/ - approve-quote/ - entities/ - inspection/ - ui/ (InspectionCard, InspectionRow) - model/ (types, store) - api/ (getInspection, updateInspection) - quote/ - ui/ - model/ - api/ - shared/ - ui/ (Button, Table, Modal -- your atoms) - lib/ (formatDate, exportUtils) - api/ (httpClient, apiProxy) - config/ (constants) -``` - -**Sources**: -- [Feature-Sliced Design](https://feature-sliced.design/) -- [Layers Reference](https://feature-sliced.design/docs/reference/layers) -- [Slices and Segments](https://feature-sliced.design/docs/reference/slices-segments) - ---- - -### 2.2 Atomic Design -- Aging for App-Level Organization - -**SAM's Current Approach**: -``` -components/ - atoms/ # Basic UI elements - molecules/ # (unused) - organisms/ # Complex composed components - templates/ # Page layout templates -``` - -**Industry Assessment (2025-2026)**: -- Atomic Design excels for **UI component libraries** (shared/ layer) -- Struggles with **domain complexity** -- "UserCard" and "ProductCard" are both organisms but semantically distinct -- Grouping by visual complexity (atom/molecule/organism) dilutes domain boundaries -- Most large-scale projects have moved to **feature/domain organization** for application code -- Atomic Design remains valuable for the **shared UI kit layer only** - -**Sources**: -- [Atomic Design Meets Feature-Based Architecture](https://medium.com/@buwanekasumanasekara/atomic-design-meets-feature-based-architecture-in-next-js-a-practical-guide-c06ea56cf5cc) -- [From Components to Systems](https://www.codewithseb.com/blog/from-components-to-systems-scalable-frontend-with-atomiec-design) - ---- - -### 2.3 Modular Monolith (Frontend) - -**Key Principles for ERP**: -- Single deployment, but internally organized as independent modules -- Each module = bounded context with clear API boundaries -- Modules communicate through well-defined interfaces, not direct imports -- Common concerns (auth, logging) handled at application level - -**Applied to Next.js ERP**: -``` -src/ - modules/ - quality/ - components/ - hooks/ - actions/ - types/ - index.ts # Public API -- only exports from here - sales/ - components/ - hooks/ - actions/ - types/ - index.ts - accounting/ - ... - shared/ # Cross-module utilities - app/ # Next.js routing (thin layer) -``` - -**Sources**: -- [Modular Monolith Revolution](https://medium.com/@bhargavkoya56/the-modular-monolith-revolution-enterprise-grade-architecture-part-i-theory-b3705ca70a5f) -- [Frontend at Scale](https://frontendatscale.com/issues/45/) - ---- - -## 3. Server Actions Organization Patterns - -### Pattern A: Colocated (SAM's Current -- 89 files) -``` -app/[locale]/(protected)/quality/qms/ - page.tsx - actions.ts # Server actions for this route -``` -**Pros**: Easy to find, clear ownership -**Cons**: Duplication across similar pages, no reuse - -### Pattern B: Domain-Centralized (react-admin / Refine style) -``` -src/ - actions/ - quality/ - inspection.ts # All inspection-related server actions - qms.ts - sales/ - quote.ts - order.ts - lib/ - api-client.ts # Shared fetch logic with auth -``` -**Pros**: Reusable across pages, easier to maintain -**Cons**: Indirection, harder to find for route-specific logic - -### Pattern C: Hybrid (Recommended for large apps) -``` -app/[locale]/(protected)/quality/qms/ - page.tsx - _actions.ts # Route-specific actions only - -src/ - domains/ - quality/ - actions/ # Shared domain actions - inspection.ts - qms.ts - hooks/ - types/ -``` -**Pros**: Route-specific stays colocated, shared logic centralized -**Cons**: Need clear rules on what goes where - -### Industry Consensus -For 100+ page apps, the **hybrid approach** (Pattern C) dominates. Route-specific logic stays colocated; shared domain logic is centralized. The key is having a clear **data provider / API client** layer that all server actions delegate to. - -**Sources**: -- [Next.js Colocation Template](https://next-colocation-template.vercel.app/) -- [Inside the App Router (2025)](https://medium.com/better-dev-nextjs-react/inside-the-app-router-best-practices-for-next-js-file-and-directory-structure-2025-edition-ed6bc14a8da3) - ---- - -## 4. CRUD Abstraction Patterns for 50+ Similar Pages - -### Pattern 1: Resource Hooks (react-admin / Refine approach) -```typescript -// hooks/useResourceList.ts -function useResourceList(resource: string, options?: ListOptions) { - const [data, setData] = useState([]); - const [pagination, setPagination] = useState({ page: 1, pageSize: 20 }); - const [filters, setFilters] = useState({}); - const [sorters, setSorters] = useState({}); - - useEffect(() => { - fetchList(resource, { pagination, filters, sorters }) - .then(result => setData(result.data)); - }, [resource, pagination, filters, sorters]); - - return { data, pagination, setPagination, filters, setFilters, sorters, setSorters }; -} - -// Usage in any list page -function QualityInspectionList() { - const { data, pagination, filters } = useResourceList('quality/inspections'); - return ; -} -``` - -### Pattern 2: Config-Driven Pages (Payload CMS approach) -```typescript -// configs/quality-inspection.config.ts -export const inspectionConfig: ResourceConfig = { - resource: 'quality/inspections', - list: { - columns: [ - { key: 'id', label: '번호' }, - { key: 'name', label: '검사명' }, - { key: 'status', label: '상태', render: StatusBadge }, - ], - filters: [ - { key: 'status', type: 'select', options: statusOptions }, - { key: 'dateRange', type: 'daterange' }, - ], - defaultSort: { key: 'createdAt', direction: 'desc' }, - }, - form: { - fields: [ - { name: 'name', type: 'text', required: true, label: '검사명' }, - { name: 'type', type: 'select', options: typeOptions, label: '검사유형' }, - ], - }, -}; - -// Generic page component -function ResourceListPage({ config }: { config: ResourceConfig }) { - const list = useResourceList(config.resource); - return ; -} -``` - -### Pattern 3: Template Composition (SAM's current direction, improved) -```typescript -// templates/UniversalCRUDPage.tsx -- enhanced version -function UniversalCRUDPage({ - resource, - listConfig, - detailConfig, - formConfig, -}: CRUDPageProps) { - // Handles list/detail/form modes based on URL - // Integrates data fetching, pagination, filtering - // Renders appropriate template based on mode -} -``` - -### Industry Assessment -- **Pattern 1** (Resource Hooks) is the most widely adopted -- used by react-admin (25K stars) and Refine (30K stars) -- **Pattern 2** (Config-Driven) reduces code the most but requires upfront investment in the config system -- **Pattern 3** (Template Composition) is the middle ground -- SAM's `UniversalListPage` is already this direction - -**Recommendation**: Evolve toward a **Provider + Resource Hooks** layer. Keep `UniversalListPage` and `IntegratedDetailTemplate` but back them with a standardized data provider. - ---- - -## 5. Comparison Matrix: SAM ERP vs Industry Patterns - -| Dimension | SAM ERP (Current) | react-admin | Refine | Payload CMS | FSD | Recommendation | -|-----------|-------------------|-------------|--------|-------------|-----|----------------| -| **Folder Structure** | Domain-based (app router) | Resource-based | Resource-based | Collection-based | Layer > Slice > Segment | Hybrid Domain + FSD shared layer | -| **Component Org** | Atomic Design (partial) | Flat per resource | Flat per resource | Config-driven | Layer-based (entities/features) | FSD for app code, Atomic for shared UI | -| **API Layer** | 89 colocated actions.ts | Centralized dataProvider | Centralized dataProvider | Built-in Local API | api/ segment per slice | Centralized API client + domain actions | -| **CRUD Abstraction** | UniversalListPage template | Resource + Controller hooks | useTable/useForm hooks | Auto-generated from config | Manual per feature | Add resource hooks on top of templates | -| **Form Handling** | Mixed (migrating to RHF+Zod) | react-hook-form (built-in) | react-hook-form (headless) | Auto from field config | Manual per feature | Complete RHF+Zod migration | -| **State Management** | Zustand stores | React Query (built-in) | React Query (built-in) | Server-side | Per-slice model/ | Keep Zustand for UI state, add React Query for server state | -| **Type Safety** | Manual interfaces | Built-in types | TypeScript throughout | Auto-generated from schema | Manual per segment | Consider schema-driven type generation | -| **50+ Page Scale** | Manual duplication | Resource registration | Inferencer + hooks | Collection config | Slice per entity | Resource hooks + config-driven columns | - ---- - -## 6. Actionable Recommendations for SAM ERP - -### Priority 1: Introduce a Data Provider / API Client Layer -**Why**: The biggest gap vs. industry standard. 89 scattered actions.ts files means duplicated fetch logic, inconsistent error handling, and no centralized caching. - -**Action**: Create a `dataProvider` abstraction inspired by react-admin/Refine: -```typescript -// src/lib/data-provider.ts -export const dataProvider = { - getList: (resource, params) => proxyFetch(`/api/proxy/${resource}`, params), - getOne: (resource, id) => proxyFetch(`/api/proxy/${resource}/${id}`), - create: (resource, data) => proxyFetch(`/api/proxy/${resource}`, { method: 'POST', body: data }), - update: (resource, id, data) => proxyFetch(`/api/proxy/${resource}/${id}`, { method: 'PUT', body: data }), - delete: (resource, id) => proxyFetch(`/api/proxy/${resource}/${id}`, { method: 'DELETE' }), -}; -``` - -### Priority 2: Create Resource Hooks -**Why**: Reduce per-page boilerplate for list/detail/form patterns. - -**Action**: Build `useResourceList`, `useResourceDetail`, `useResourceForm` hooks that wrap the data provider. - -### Priority 3: Evolve Folder Structure Toward Hybrid FSD -**Why**: Atomic Design for app-level code leads to unclear domain boundaries. - -**Action**: -- Keep `shared/ui/` (atoms/organisms) for reusable UI components -- Add `domains/` or `entities/` for business-logic grouping -- Keep `app/` routes thin -- delegate to domain components - -### Priority 4: Complete Form Standardization -**Why**: Mixed form patterns make maintenance harder and prevent reusable form configs. - -**Action**: Complete the react-hook-form + Zod migration. Consider field-config-driven forms (Payload pattern) for highly repetitive forms. - -### Priority 5: Consider Server State Management (React Query / TanStack Query) -**Why**: react-admin and Refine both use React Query internally for caching, optimistic updates, and background refetching. Zustand is better suited for client UI state. - -**Action**: Evaluate adding TanStack Query for server state alongside Zustand for UI state. - ---- - -## 7. What SAM ERP Is Already Doing Well - -1. **Domain-based routing** (`app/[locale]/(protected)/quality/...`) aligns with industry best practice -2. **UniversalListPage + IntegratedDetailTemplate** is the right abstraction direction (similar to react-admin's List/Edit components) -3. **SearchableSelectionModal** as a reusable pattern is good (similar to react-admin's ReferenceInput) -4. **Server Actions in colocated files** follows Next.js official recommendation for route-specific logic -5. **Zustand for global state** is a solid choice for UI state (sidebar state, theme, etc.) - ---- - -## Sources - -### Open-Source Projects -- [react-admin - Architecture](https://marmelab.com/react-admin/Architecture.html) -- [react-admin - GitHub](https://github.com/marmelab/react-admin) -- [Refine - Data Provider](https://refine.dev/docs/data/data-provider/) -- [Refine - GitHub](https://github.com/refinedev/refine) -- [Payload CMS - Collections](https://payloadcms.com/docs/configuration/collections) -- [Payload CMS - GitHub](https://github.com/payloadcms/payload) -- [Medusa - Admin Development](https://docs.medusajs.com/learn/fundamentals/admin) -- [Medusa - GitHub](https://github.com/medusajs/medusa) - -### Architectural Methodologies -- [Feature-Sliced Design](https://feature-sliced.design/) -- [FSD - Layers Reference](https://feature-sliced.design/docs/reference/layers) -- [Atomic Design + FSD Hybrid](https://medium.com/@buwanekasumanasekara/atomic-design-meets-feature-based-architecture-in-next-js-a-practical-guide-c06ea56cf5cc) -- [Clean Architecture vs FSD in Next.js](https://medium.com/@metastability/clean-architecture-vs-feature-sliced-design-in-next-js-applications-04df25e62690) - -### Folder Structure & Patterns -- [Next.js App Router Best Practices (2025)](https://medium.com/better-dev-nextjs-react/inside-the-app-router-best-practices-for-next-js-file-and-directory-structure-2025-edition-ed6bc14a8da3) -- [Scalable Next.js Folder Structure](https://techtales.vercel.app/read/thedon/building-a-scalable-folder-structure-for-large-next-js-projects) -- [SaaS Architecture Patterns with Next.js](https://vladimirsiedykh.com/blog/saas-architecture-patterns-nextjs) -- [Modular Monolith for Frontend](https://frontendatscale.com/issues/45/) diff --git a/claudedocs/architecture/[TEST-2025-11-19] multi-tenancy-test-guide.md b/claudedocs/architecture/[TEST-2025-11-19] multi-tenancy-test-guide.md deleted file mode 100644 index ccf6598a..00000000 --- a/claudedocs/architecture/[TEST-2025-11-19] multi-tenancy-test-guide.md +++ /dev/null @@ -1,495 +0,0 @@ -# 멀티 테넌시 검증 및 테스트 가이드 - -**작성일**: 2025-11-19 -**목적**: Phase 1-4 구현 후 테넌트 격리 기능 검증 - ---- - -## 📋 목차 - -1. [테스트 환경 준비](#테스트-환경-준비) -2. [테스트 시나리오](#테스트-시나리오) -3. [체크리스트](#체크리스트) -4. [문제 해결](#문제-해결) - ---- - -## 테스트 환경 준비 - -### 1. 개발 서버 실행 - -```bash -npm run dev -``` - -### 2. 브라우저 개발자 도구 열기 - -- Chrome: `F12` 또는 `Cmd+Option+I` (Mac) -- Console 탭과 Application 탭을 주로 사용 - -### 3. 테스트 사용자 확인 - -현재 등록된 테스트 사용자 (모두 tenant.id: 282): - -| userId | name | tenant.id | 역할 | -|--------|------|-----------|------| -| TestUser1 | 이재욱 | 282 | 일반 사용자 | -| TestUser2 | 박관리 | 282 | 생산관리자 | -| TestUser3 | 드미트리 | 282 | 시스템 관리자 | - -**⚠️ 테넌트 전환 테스트를 위해 다른 tenant.id를 가진 사용자가 필요합니다.** - ---- - -## 테스트 시나리오 - -### 시나리오 1: 기본 캐시 동작 확인 ✅ - -**목적**: TenantAwareCache가 제대로 동작하는지 확인 - -**단계**: -1. 로그인: TestUser3 (tenant.id: 282) -2. `/master-data/item-master-data-management` 페이지 이동 -3. 데이터 입력: - - 규격 마스터 1개 추가 - - 품목 분류 1개 추가 -4. **개발자 도구 → Application → Session Storage** 확인 - -**기대 결과**: -``` -✅ sessionStorage에 다음 키가 생성되어야 함: -- mes-282-itemMasters -- mes-282-specificationMasters -- mes-282-itemCategories -- (기타 입력한 데이터) - -✅ 각 키의 값에 tenantId: 282 포함 -✅ timestamp 포함 -``` - -**확인 방법**: -```javascript -// Console에서 실행 -Object.keys(sessionStorage).filter(k => k.startsWith('mes-')) -// 결과: ["mes-282-itemMasters", "mes-282-specificationMasters", ...] -``` - ---- - -### 시나리오 2: 페이지 새로고침 시 캐시 로드 ✅ - -**목적**: 캐시에서 데이터를 제대로 불러오는지 확인 - -**단계**: -1. 시나리오 1 완료 후 -2. `F5` 또는 `Cmd+R`로 새로고침 -3. Console에서 로그 확인 - -**기대 결과**: -``` -✅ Console 로그: -[Cache] Loaded from cache: itemMasters -[Cache] Loaded from cache: specificationMasters -... - -✅ 입력했던 데이터가 그대로 표시됨 -✅ 서버 API 호출 없이 캐시에서 로드 -``` - ---- - -### 시나리오 3: TTL (1시간) 만료 확인 ⏱️ - -**목적**: 캐시가 1시간 후 자동 삭제되는지 확인 - -**⚠️ 주의**: 실제 1시간을 기다릴 수 없으므로 **수동 테스트** - -**단계**: -1. sessionStorage에서 캐시 데이터 조회: - ```javascript - const cached = sessionStorage.getItem('mes-282-itemMasters'); - const parsed = JSON.parse(cached); - console.log('Timestamp:', new Date(parsed.timestamp)); - console.log('Age (minutes):', (Date.now() - parsed.timestamp) / 60000); - ``` - -2. **수동으로 timestamp 수정** (과거 시간으로): - ```javascript - const cached = sessionStorage.getItem('mes-282-itemMasters'); - const parsed = JSON.parse(cached); - - // 2시간 전으로 설정 (TTL 1시간 초과) - parsed.timestamp = Date.now() - (7200 * 1000); - - sessionStorage.setItem('mes-282-itemMasters', JSON.stringify(parsed)); - ``` - -3. 페이지 새로고침 - -**기대 결과**: -``` -✅ Console 로그: -[Cache] Expired cache for key: itemMasters - -✅ 만료된 캐시 자동 삭제 -✅ 초기 데이터로 리셋 -``` - ---- - -### 시나리오 4: 다중 탭 격리 확인 🔗 - -**목적**: 탭마다 독립적인 sessionStorage 사용 확인 - -**단계**: -1. **탭 1**: TestUser3 로그인 → 데이터 입력 (규격 마스터 A) -2. **탭 2**: 동일 URL을 새 탭으로 열기 (`Cmd+T` → URL 복사) -3. 탭 2에서 sessionStorage 확인 - -**기대 결과**: -``` -✅ 탭 2의 sessionStorage는 비어있음 -✅ 탭 1의 데이터가 탭 2에 공유되지 않음 -✅ 각 탭이 독립적으로 동작 - -sessionStorage는 탭마다 격리됨! -``` - -**확인 방법**: -```javascript -// 탭 1 -sessionStorage.setItem('test', 'tab1'); - -// 탭 2 (새로 열린 탭) -sessionStorage.getItem('test'); // null (공유 안 됨) -``` - ---- - -### 시나리오 5: 탭 닫기 시 자동 삭제 🗑️ - -**목적**: 탭을 닫으면 sessionStorage가 자동으로 삭제되는지 확인 - -**단계**: -1. 탭에서 데이터 입력 -2. Application → Session Storage에서 데이터 확인 -3. **탭 닫기** -4. **동일 URL을 새 탭으로 다시 열기** -5. Session Storage 확인 - -**기대 결과**: -``` -✅ sessionStorage가 완전히 비어있음 -✅ 이전 탭의 데이터가 남아있지 않음 -✅ 새로운 세션으로 시작 -``` - ---- - -### 시나리오 6: 로그아웃 시 캐시 삭제 🚪 - -**목적**: 로그아웃하면 테넌트 캐시가 완전히 삭제되는지 확인 - -**단계**: -1. TestUser3 로그인 → 데이터 입력 -2. sessionStorage 확인 (캐시 있음) -3. **로그아웃 버튼 클릭** -4. Console 로그 확인 -5. sessionStorage 다시 확인 - -**기대 결과**: -``` -✅ Console 로그: -[Cache] Cleared sessionStorage: mes-282-itemMasters -[Cache] Cleared sessionStorage: mes-282-specificationMasters -... -[Auth] Logged out and cleared tenant cache - -✅ sessionStorage에서 mes-282-* 키가 모두 삭제됨 -✅ localStorage에서 mes-currentUser도 삭제됨 -``` - -**확인 방법**: -```javascript -// 로그아웃 후 -Object.keys(sessionStorage).filter(k => k.startsWith('mes-282-')) -// 결과: [] (빈 배열) -``` - ---- - -### 시나리오 7: 테넌트 전환 시 캐시 삭제 🔄 - -**⚠️ 현재 제약**: 모든 테스트 사용자가 tenant.id: 282를 사용 중 - -**필요 작업**: 다른 tenant.id를 가진 사용자 추가 - -#### 7-1. 테스트 사용자 추가 (tenant.id: 283) - -`src/contexts/AuthContext.tsx` 수정: - -```typescript -const initialUsers: User[] = [ - // ... 기존 사용자 ... - { - userId: "TestUser4", - name: "김테넌트", - position: "다른 회사 관리자", - roles: [ - { - id: 1, - name: "admin", - description: "관리자" - } - ], - tenant: { - id: 283, // ✅ 다른 테넌트! - company_name: "(주)다른회사", - business_num: "987-65-43210", - tenant_st_code: "active", - other_tenants: [] - }, - menu: [ - { - id: "13664", - label: "시스템 대시보드", - iconName: "layout-dashboard", - path: "/dashboard" - } - ] - } -]; -``` - -#### 7-2. 테넌트 전환 테스트 - -**단계**: -1. **TestUser3 로그인** (tenant.id: 282) - - 데이터 입력 (규격 마스터 A, B) - - sessionStorage 확인: `mes-282-specificationMasters` - -2. **로그아웃** - -3. **TestUser4 로그인** (tenant.id: 283) - - Console 로그 확인 - -**기대 결과**: -``` -✅ Console 로그: -[Auth] Tenant changed: 282 → 283 -[Cache] Cleared sessionStorage: mes-282-itemMasters -[Cache] Cleared sessionStorage: mes-282-specificationMasters -... - -✅ 이전 테넌트(282)의 캐시가 모두 삭제됨 -✅ TestUser4의 데이터는 mes-283-* 키로 저장됨 -✅ 테넌트 간 데이터 격리 확인 -``` - -**확인 방법**: -```javascript -// 테넌트 전환 후 -Object.keys(sessionStorage).forEach(key => { - console.log(key); -}); - -// 결과: -// mes-283-itemMasters (새 테넌트) -// mes-283-specificationMasters -// (mes-282-* 키는 없어야 함!) -``` - ---- - -### 시나리오 8: PHP 백엔드 tenant.id 검증 🛡️ - -**⚠️ 주의**: PHP 백엔드가 실행 중이어야 함 - -**목적**: 다른 테넌트의 데이터 접근 시 403 반환 확인 - -**단계**: -1. **TestUser3 로그인** (tenant.id: 282) -2. 브라우저 Console에서 다른 테넌트 API 직접 호출: - -```javascript -// 자신의 테넌트 (282) - 성공해야 함 -fetch('/api/tenants/282/item-master-config') - .then(r => r.json()) - .then(d => console.log('Own tenant:', d)); - -// 다른 테넌트 (283) - 403 에러여야 함 -fetch('/api/tenants/283/item-master-config') - .then(r => r.json()) - .then(d => console.log('Other tenant:', d)); -``` - -**기대 결과**: -``` -✅ 자신의 테넌트 (282): -{ - success: true, - data: { ... } -} - -✅ 다른 테넌트 (283): -{ - success: false, - error: { - code: "FORBIDDEN", - message: "접근 권한이 없습니다." - } -} -Status: 403 Forbidden - -✅ Next.js는 단순히 PHP 응답을 전달만 함 -✅ PHP가 tenant.id 불일치를 감지하고 403 반환 -``` - ---- - -## 체크리스트 - -### 캐시 동작 ✅ -- [ ] sessionStorage에 `mes-{tenantId}-{key}` 형식으로 저장 -- [ ] 캐시 데이터에 `tenantId`, `timestamp`, `version` 포함 -- [ ] 페이지 새로고침 시 캐시에서 로드 -- [ ] TTL (1시간) 만료 시 자동 삭제 - -### 탭 격리 🔗 -- [ ] 탭마다 독립적인 sessionStorage -- [ ] 다른 탭과 데이터 공유 안 됨 -- [ ] 탭 닫으면 sessionStorage 자동 삭제 - -### 로그아웃 🚪 -- [ ] 로그아웃 시 `mes-{tenantId}-*` 캐시 모두 삭제 -- [ ] Console에 삭제 로그 출력 -- [ ] localStorage의 `mes-currentUser` 삭제 - -### 테넌트 전환 🔄 -- [ ] 테넌트 변경 감지 (useEffect) -- [ ] 이전 테넌트 캐시 자동 삭제 -- [ ] 새 테넌트 데이터는 새 키로 저장 -- [ ] Console에 전환 로그 출력 - -### API 보안 🛡️ -- [ ] 자신의 테넌트 API 호출 성공 -- [ ] 다른 테넌트 API 호출 시 403 Forbidden -- [ ] PHP 백엔드가 tenant.id 검증 수행 -- [ ] Next.js는 PHP 응답 그대로 전달 - ---- - -## 문제 해결 - -### 문제 1: 캐시가 저장되지 않음 - -**증상**: sessionStorage가 비어있음 - -**원인**: -- ItemMasterContext가 제대로 마운트되지 않음 -- tenantId가 null - -**해결**: -1. Console에서 확인: - ```javascript - // AuthContext의 currentUser 확인 - console.log(JSON.parse(localStorage.getItem('mes-currentUser'))); - - // tenant.id 확인 - console.log(user?.tenant?.id); - ``` - -2. ItemMasterContext가 AuthContext 하위에 있는지 확인 - -### 문제 2: 테넌트 전환 시 캐시가 삭제되지 않음 - -**증상**: 이전 테넌트 캐시가 남아있음 - -**원인**: -- `useEffect` 의존성 배열 문제 -- `previousTenantIdRef` 초기화 안 됨 - -**해결**: -```typescript -// AuthContext.tsx 확인 -useEffect(() => { - const prevTenantId = previousTenantIdRef.current; - const currentTenantId = currentUser?.tenant?.id; - - if (prevTenantId && currentTenantId && prevTenantId !== currentTenantId) { - console.log(`[Auth] Tenant changed: ${prevTenantId} → ${currentTenantId}`); - clearTenantCache(prevTenantId); - } - - previousTenantIdRef.current = currentTenantId || null; -}, [currentUser?.tenant?.id]); -``` - -### 문제 3: TTL 만료 후에도 캐시가 남아있음 - -**증상**: 1시간 이상 지난 캐시가 그대로 사용됨 - -**원인**: -- `TenantAwareCache.get()` 메서드에서 TTL 체크 안 함 - -**해결**: -```typescript -// TenantAwareCache.ts 확인 -get(key: string): T | null { - // ... - - // TTL 검증 - if (Date.now() - parsed.timestamp > this.ttl) { - console.warn(`[Cache] Expired cache for key: ${key}`); - this.remove(key); - return null; - } - - return parsed.data; -} -``` - -### 문제 4: PHP 403 에러가 반환되지 않음 - -**증상**: 다른 테넌트 API 호출이 성공함 - -**원인**: -- PHP 백엔드에 tenant.id 검증 로직이 없음 -- JWT에 tenant.id가 포함되지 않음 - -**해결**: -1. PHP 백엔드 확인 (프론트엔드 작업 범위 밖) -2. JWT payload에 `tenant_id` 포함 여부 확인 -3. PHP middleware에서 tenant.id 검증 로직 확인 - ---- - -## 테스트 완료 기준 - -### ✅ 모든 시나리오 통과 -- 시나리오 1-8 모두 기대 결과와 일치 - -### ✅ 모든 체크리스트 완료 -- 캐시, 탭, 로그아웃, 테넌트 전환, API 보안 - -### ✅ Console 에러 없음 -- 개발자 도구 Console에 빨간색 에러 없음 - -### ✅ 성능 확인 -- 페이지 로드 시간 < 1초 -- 캐시 히트 시 API 호출 없음 - ---- - -## 다음 단계 - -Phase 5 완료 후: -- **Phase 6**: 품목기준관리 페이지 작업 진행 -- API 연동 및 실제 CRUD 구현 -- UI/UX 개선 - ---- - -**작성자**: Claude -**버전**: 1.0 -**최종 업데이트**: 2025-11-19 \ No newline at end of file diff --git a/claudedocs/architecture/[TODO-2026-03-10] user-preferences-db-migration.md b/claudedocs/architecture/[TODO-2026-03-10] user-preferences-db-migration.md deleted file mode 100644 index 8e1b9d66..00000000 --- a/claudedocs/architecture/[TODO-2026-03-10] user-preferences-db-migration.md +++ /dev/null @@ -1,166 +0,0 @@ -# [TODO] 유저 개별 설정 DB 이관 계획 - -> 현재 localStorage에 저장 중인 유저별 설정을 백엔드 DB로 이관하여 크로스 디바이스 동기화 지원 - ---- - -## 현재 현황: localStorage 기반 유저 설정 목록 - -### 🔴 HIGH — 우선 이관 대상 - -| 항목 | 저장 키 | 파일 | 유저 분리 | 설명 | -|------|---------|------|-----------|------| -| 즐겨찾기 | `sam-favorites-{userId}` | `stores/favoritesStore.ts` | ✅ | 메뉴 즐겨찾기 (최대 10개) | -| 테이블 컬럼 설정 | `sam-table-columns-{userId}` | `stores/useTableColumnStore.ts` | ✅ | 컬럼 너비, 숨김 여부 (페이지별) | - -### 🟡 MEDIUM — 2차 이관 대상 - -| 항목 | 저장 키 | 파일 | 유저 분리 | 설명 | -|------|---------|------|-----------|------| -| 테마 | `theme` | `stores/themeStore.ts` | ❌ 공용 | light / dark / senior | -| 글꼴 크기 | `sam-font-size` | `layouts/AuthenticatedLayout.tsx` | ❌ 공용 | 12~20px (기본 16) | -| 사이드바 접힘 | `sam-menu` | `stores/menuStore.ts` | ❌ 공용 | sidebarCollapsed 상태 | -| 알림 설정 | `ITEM_VISIBILITY_STORAGE_KEY` | `settings/NotificationSettings/index.tsx` | ❌ 공용 | 알림 카테고리별 표시 여부 | - -### 🟢 LOW — 선택적 이관 - -| 항목 | 저장 키 | 파일 | 설명 | -|------|---------|------|------| -| 팝업 오늘 하루 안 보기 | `popup_dismissed_{id}` | `common/NoticePopupModal.tsx` | 매일 자동 리셋, 임시성 | - -### ❌ 제외 (이관 불필요) - -| 항목 | 이유 | -|------|------| -| Auth 토큰 (HttpOnly 쿠키) | 이미 서버 관리 | -| Auth Store (mes-users, mes-currentUser) | 인증 플로우 전용 | -| Master Data 캐시 (sessionStorage) | TTL 기반 캐시, 설정 아님 | -| Dashboard Stale 캐시 (sessionStorage) | 세션 캐시 | -| Page Builder (page-builder-pages) | 개발 전용 도구 | - ---- - -## 백엔드 DB 스키마 (안) - -### user_preferences (통합 설정 테이블) -```sql -CREATE TABLE user_preferences ( - id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, - tenant_id BIGINT UNSIGNED NOT NULL, - user_id BIGINT UNSIGNED NOT NULL, - theme VARCHAR(20) DEFAULT 'light', - font_size TINYINT UNSIGNED DEFAULT 16, - sidebar_collapsed BOOLEAN DEFAULT false, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - UNIQUE KEY (tenant_id, user_id) -); -``` - -### user_favorites (즐겨찾기) -```sql -CREATE TABLE user_favorites ( - id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, - tenant_id BIGINT UNSIGNED NOT NULL, - user_id BIGINT UNSIGNED NOT NULL, - menu_id VARCHAR(100) NOT NULL, - label VARCHAR(255) NOT NULL, - icon_name VARCHAR(100), - path VARCHAR(500) NOT NULL, - display_order TINYINT UNSIGNED DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE KEY (tenant_id, user_id, menu_id) -); -``` - -### user_table_preferences (테이블 컬럼 설정) -```sql -CREATE TABLE user_table_preferences ( - id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, - tenant_id BIGINT UNSIGNED NOT NULL, - user_id BIGINT UNSIGNED NOT NULL, - page_id VARCHAR(100) NOT NULL, - settings JSON NOT NULL, -- { columnWidths: {...}, hiddenColumns: [...] } - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - UNIQUE KEY (tenant_id, user_id, page_id) -); -``` - -### user_notification_preferences (알림 설정) -```sql -CREATE TABLE user_notification_preferences ( - id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, - tenant_id BIGINT UNSIGNED NOT NULL, - user_id BIGINT UNSIGNED NOT NULL, - settings JSON NOT NULL, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - UNIQUE KEY (tenant_id, user_id) -); -``` - ---- - -## API 엔드포인트 (안) - -### Phase 1 (즐겨찾기 + 테이블 설정) -``` -GET /api/v1/user/preferences — 전체 설정 조회 -PATCH /api/v1/user/preferences — 설정 부분 업데이트 - -GET /api/v1/user/favorites — 즐겨찾기 목록 -POST /api/v1/user/favorites — 즐겨찾기 추가 -DELETE /api/v1/user/favorites/{menuId} — 즐겨찾기 삭제 -PATCH /api/v1/user/favorites/reorder — 순서 변경 - -GET /api/v1/user/table-preferences/{pageId} — 페이지별 컬럼 설정 -PUT /api/v1/user/table-preferences/{pageId} — 컬럼 설정 저장 -``` - -### Phase 2 (테마/글꼴/사이드바/알림) -``` -GET /api/v1/user/preferences — 위와 동일 (theme, font_size 포함) -PATCH /api/v1/user/preferences — 위와 동일 - -GET /api/v1/user/notification-preferences -PUT /api/v1/user/notification-preferences -``` - ---- - -## 이관 전략 - -### 단계별 마이그레이션 -1. **DB 테이블 + API 생성** (백엔드) -2. **Dual-write 패턴 적용** (프론트) - - 저장 시: API 호출 + localStorage 동시 기록 - - 읽기 시: API 우선 → localStorage 폴백 -3. **안정화 후 localStorage 제거** - -### 프론트 전환 패턴 (예시) -```typescript -// createUserStorage → createUserStorageAPI 전환 -export function createUserStorageAPI(baseKey: string) { - return { - getItem: async () => { - const res = await fetch(`/api/v1/user/${baseKey}`); - return res.ok ? res.json() : null; - }, - setItem: async (value: unknown) => { - await fetch(`/api/v1/user/${baseKey}`, { - method: 'PUT', - body: JSON.stringify(value), - }); - }, - }; -} -``` - ---- - -## 우선순위 정리 - -| 단계 | 대상 | 이유 | -|------|------|------| -| Phase 1 | 즐겨찾기, 테이블 컬럼 | 유저별 분리 이미 되어있어 구조 전환 쉬움, 사용 빈도 높음 | -| Phase 2 | 테마, 글꼴, 사이드바 | 현재 유저 분리 안 됨 → DB 이관하면서 유저별 적용 | -| Phase 3 | 알림 설정 | 기능 안정화 후 진행 | diff --git a/claudedocs/architecture/[VISION-2026-02-19] dynamic-rendering-platform-strategy.md b/claudedocs/architecture/[VISION-2026-02-19] dynamic-rendering-platform-strategy.md deleted file mode 100644 index 69e29d3f..00000000 --- a/claudedocs/architecture/[VISION-2026-02-19] dynamic-rendering-platform-strategy.md +++ /dev/null @@ -1,224 +0,0 @@ -# 동적 렌더링 플랫폼 전략 — 기준관리 기반 화면 자동 구성 - -> 작성일: 2026-02-19 -> 상태: 비전 정리 (논의 기반) -> 관련 기술 설계: `[DESIGN-2026-02-11] dynamic-field-type-extension.md` -> 관련 구현 현황: `[IMPL-2026-02-11] dynamic-field-components.md` -> 관련 로드맵: `item-master/[DESIGN-2025-12-12] item-master-form-builder-roadmap.md` - ---- - -## 1. 핵심 비전 - -``` -기준관리 페이지에서 설정 → API로 메타데이터 전달 → 프론트가 자동 렌더링 -``` - -**목표**: 개발자가 매번 화면을 코딩하는 것이 아니라, 기준관리 페이지에서 등록한 설정값에 따라 프론트엔드가 동적으로 화면을 구성하는 **ERP 커스터마이징 플랫폼**. - ---- - -## 2. 운영 워크플로우 비전 - -### 2.1 전체 흐름 - -``` -현장 방문 (영업자/매니저) - ├─ 녹음, 체크리스트, 문서 수집 - └─ → MD 파일 정리 (요구사항) - │ - ↓ -기준관리 페이지 (관리자/컨설턴트) - ├─ MD 보고 속성, 칼럼, 옵션 등록 - └─ → 메타데이터 저장 (DB) - │ - ↓ API - │ -프론트엔드 (자동 렌더링) - └─ 메타데이터 기반으로 동적 화면 구성 -``` - -### 2.2 역할 변화 - -| 역할 | 현재 | 비전 | -|------|------|------| -| 영업자/매니저 | 요구사항 전달 → 개발 대기 | 현장에서 MD 파일 작성 | -| 관리자/컨설턴트 | — | MD 보고 기준관리에 설정 입력 | -| **개발자** | **요구사항마다 화면 코딩** | **플랫폼 유지보수 + 새 블록 타입 추가 시에만 개입** | - -### 2.3 개발자 개입이 필요한 시점 - -- 기존 블록(Input, Select, DatePicker 등)으로 조합 가능 → **개발자 불필요** -- 새로운 입력 타입/계산 로직 필요 → **블록 1개 추가** → 이후 재사용 -- 기준관리 UI 자체 개선 → **설계/검증** -- page-builder 고도화 → **설계/구현** - ---- - -## 3. 현재 자산 현황 - -### 3.1 이미 있는 것 - -#### UI 블록 (공통 컴포넌트) -``` -src/components/ui/ - ├─ Input, NumberInput, QuantityInput, CurrencyInput - ├─ Select, Checkbox, DatePicker, Textarea - ├─ Button, Badge, Card, Dialog - └─ ... -``` -모든 도메인별 테이블이 이 공통 블록을 사용 중. - -#### 동적 필드 시스템 (14종 완성) -``` -DynamicItemForm/fields/ - ├─ 기존 6종: textbox, number, dropdown, checkbox, date, textarea - └─ 신규 8종: reference, multi-select, file, currency, unit-value, radio, toggle, computed -``` -Phase 1~3 프론트 구현 완료 (백엔드 작업 대기). - -#### 범용 테이블 섹션 -``` -DynamicTableSection — config 기반 칼럼 정의, 행 CRUD, 요약행 -TableCellRenderer — 테이블 셀 = DynamicFieldRenderer 재사용 -``` - -#### 속성 관리 시스템 (품목기준관리) -``` -useAttributeManagement — 속성 옵션 상태 관리 -AttributeTabContent — 동적 탭 렌더링 -OptionColumn[] + MasterOption[] — 메타데이터 구조 -``` - -#### page-builder 프로토타입 -``` -/dev/page-builder — 드래그앤드롭, 섹션/필드 구성, Undo/Redo, 반응형 뷰포트 -``` - -### 3.2 현재 구조: "기준관리 → 동적 렌더링" 패턴 - -``` -품목기준관리 (Admin) 품목 등록 (User) -ItemMasterDataManagement.tsx DynamicItemForm/index.tsx - ↓ 설정 (pages/sections/fields) ↓ 읽어서 렌더링 - DB에 메타데이터 저장 DynamicFieldRenderer (14종 switch) - DynamicTableSection (config 기반) -``` - -**이 패턴이 핵심이고, 다른 도메인에도 동일하게 확장하는 것이 비전.** - ---- - -## 4. 확장 대상 분석 - -### 4.1 도메인별 동적 렌더링 적합성 - -| 도메인 | 적합도 | 이유 | -|--------|:---:|------| -| 품목기준관리 | ✅ 이미 적용 | 테넌트/업종별 관리 항목이 다름 | -| 설비/자산 관리 | ✅ 높음 | 설비 종류별 관리 속성이 다름 | -| 거래처 관리 | ✅ 높음 | 업종별 추가 정보 다름 | -| 공정/라우팅 관리 | ✅ 높음 | 제조 방식별 공정 구성 다름 | -| 검사 항목 관리 | ✅ 높음 | 품목별 검사 항목/기준 다름 | -| 견적서/발주서 | 🟡 부분 | 테이블은 동적 가능, 비즈니스 로직은 고정 | -| 세금계산서 | ❌ 낮음 | 법정 양식, 테넌트별 차이 없음 | -| 대시보드 | ❌ 낮음 | 위젯 기반이 더 적합 | - -### 4.2 편집 가능 테이블 현황 - -| 컴포넌트 | 공통 컴포넌트 사용 | 자동 계산 | 합계 행 | -|---------|:---:|:---:|:---:| -| EditableTable (공통) | 본인이 공통 | ❌ | ❌ | -| TaxInvoiceItemTable | ❌ 개별 | ✅ | ✅ | -| OrderDetailItemTable | ❌ 개별 | ❌ | ✅ | -| EstimateDetailTableSection | ❌ 개별 | ✅ (복잡) | ✅ | -| DynamicTableSection | ❌ 개별 (config 기반) | ✅ (요약) | ✅ | - -**테이블 안의 부품(Input, Select 등)은 전부 공통 ui 컴포넌트 사용.** -껍데기(테이블 구조, 계산 로직)만 각자 구현. - ---- - -## 5. page-builder 갭 분석 - -### 5.1 현재 page-builder 상태 - -``` -/dev/page-builder (프로토타입) - ✅ 드래그앤드롭 (섹션/필드 → 캔버스) - ✅ 섹션 타입 (BASIC, BOM, CUSTOM) - ✅ 필드 타입 (기본 6종) - ✅ 조건부 표시 (DisplayCondition) - ✅ 검증 규칙 (ValidationRule) - ✅ BOM 테이블 - ✅ 마스터 필드 연동 - ✅ Undo/Redo 히스토리 - ✅ 반응형 뷰포트 (desktop/tablet/mobile) - ✅ API 변환 타입 정의 -``` - -### 5.2 비전 대비 부족한 점 - -| 항목 | 현재 | 필요 | -|------|------|------| -| 대상 도메인 | 품목 전용 (ItemType: FG/PT/SM/RM/CS) | 모든 기준관리 | -| 사용자 | 개발자용 프로토타입 | 비개발자(영업/매니저/관리자) | -| 테이블 섹션 | BOM만 (고정 칼럼) | 동적 칼럼 + 행 CRUD (DynamicTableSection 연결) | -| 신규 필드 타입 | 기본 6종만 | 14종 전체 반영 | -| API 연동 | 타입만 정의 | 실제 저장/조회 | -| 프리셋 | 하드코딩 | 산업별 섹션 프리셋 선택 | - -### 5.3 고도화 방향 - -``` -1단계: 도메인 범용화 - - ItemType 종속 제거 - - "기준관리 도메인" 선택 → 해당 도메인의 페이지 구성 - -2단계: 14종 필드 타입 반영 - - ComponentPalette에 신규 8종 필드 추가 - - PropertyPanel에 각 필드별 config 편집 UI - -3단계: DynamicTableSection 연결 - - BOM 외 범용 테이블 섹션 지원 - - 칼럼 정의 UI (타입/너비/필수 설정) - -4단계: 비개발자 UX - - 용어 단순화 (field_type → "입력 형태") - - 미리보기 강화 - - 저장/불러오기 -``` - ---- - -## 6. 4-Level 아키텍처 요약 - -기존 기술 설계서(`[DESIGN-2026-02-11]`)의 핵심: - -``` -Level 1: 필드 타입 컴포넌트 (14종) — 코드 레벨, 거의 안 바뀜 -Level 2: properties config (JSON) — 설정 레벨, 코드 변경 없음 -Level 3: 섹션 프리셋 (JSON) — 템플릿 레벨, 코드 변경 없음 -Level 4: reference sources (API URL) — 연결 레벨, 코드 변경 없음 -``` - -**새 산업 진출 시에도 프론트엔드 코드 변경 = 0줄.** -백엔드 API + config JSON만 추가. - ---- - -## 7. 관련 문서 - -| 문서 | 위치 | 내용 | -|------|------|------| -| 동적 필드 타입 확장 설계 | `architecture/[DESIGN-2026-02-11]` | 4-Level 구조, 14종 필드, 범용 테이블, 산업별 확장 | -| 동적 필드 컴포넌트 구현 | `architecture/[IMPL-2026-02-11]` | Phase 1~3 프론트 구현 완료 상태 | -| Form Builder 로드맵 | `item-master/[DESIGN-2025-12-12]` | Low-Code Form Builder 초기 로드맵 | -| 백엔드 API 스펙 | `item-master/[API-REQUEST-2026-02-12]` | 동적 필드 타입 백엔드 API 요청서 | -| page-builder 참조 | `dev/[REF] page-builder-implementation.md` | 페이지 빌더 구현 참조 | -| 멀티테넌시 최적화 | `architecture/[PLAN-2026-02-06] multi-tenancy-optimization-roadmap.md` | 테넌트별 격리/최적화 | - ---- - -**문서 버전**: 1.0 -**마지막 업데이트**: 2026-02-19 diff --git a/claudedocs/architecture/module-separation-guide.md b/claudedocs/architecture/module-separation-guide.md deleted file mode 100644 index c35dadc0..00000000 --- a/claudedocs/architecture/module-separation-guide.md +++ /dev/null @@ -1,315 +0,0 @@ -# SAM ERP 멀티테넌트 모듈 분리 아키텍처 - -> 작성일: 2026-03-18 -> 상태: 프론트엔드 Phase 0~3 완료 / 백엔드 작업 필요 - ---- - -## 0. 왜 산업군별 모듈 분리가 필요한가 (협의 필요) - -### 현재 상황: 하나의 ERP, 다른 업종의 고객사 - -SAM ERP는 **하나의 코드베이스**로 여러 회사(테넌트)에 서비스를 제공합니다. -그런데 고객사마다 업종이 다릅니다: - -``` -경동 → 셔터 제조업 (MES) → 생산관리, 품질관리, 차량관리가 필요 -주일 → 건설업 → 시공관리, 차량관리가 필요 -A사 → 유통/서비스업 → 공통 ERP(회계/인사/영업)만 필요 -``` - -현재는 **모든 테넌트에게 모든 메뉴가 보입니다.** -경동 직원에게 시공관리 메뉴가 보이고, 주일 직원에게 생산관리 메뉴가 보입니다. -→ 사용하지 않는 메뉴가 노출되어 혼란을 주고, 대시보드에도 불필요한 섹션이 나타남. - -### 제안: 업종(industry) 기반 모듈 ON/OFF - -``` -┌─────────────────────────────────────────────────┐ -│ SAM ERP (공통) │ -│ 회계 · 인사 · 영업 · 결재 · 게시판 · 설정 │ -├──────────┬──────────┬───────────┬───────────────┤ -│ 생산관리 │ 품질관리 │ 시공관리 │ 차량관리 │ -│ (MES) │ │ (건설) │ (선택) │ -├──────────┴──────────┼───────────┤ │ -│ 셔터 MES 업종 │ 건설 업종 │ │ -│ (경동) │ (주일) │ │ -└─────────────────────┴───────────┴───────────────┘ -``` - -- **공통 모듈**: 모든 테넌트가 사용 (회계, 인사, 영업 등) -- **업종 모듈**: 테넌트의 업종에 따라 자동으로 켜짐/꺼짐 -- **선택 모듈**: 업종과 관계없이 개별 선택 가능 (차량관리 등) - -### 협의가 필요한 부분 - -이 구조로 가려면 다음 사항의 합의가 필요합니다: - -#### 1) 업종 분류 체계 -현재 프론트엔드에 하드코딩된 매핑: - -| 업종 코드 | 의미 | 활성 모듈 | -|-----------|------|-----------| -| `shutter_mes` | 셔터 제조 (MES) | 생산관리 + 품질관리 + 차량관리 | -| `construction` | 건설업 | 시공관리 + 차량관리 | - -**Q. 이 분류가 맞는지? 추가할 업종이 있는지?** -예: 일반 제조업, 도소매업, 서비스업 등 - -#### 2) 모듈 경계 -현재 정의된 모듈 단위: - -| 모듈 | 포함 기능 | 비고 | -|------|----------|------| -| 공통 ERP | 대시보드, 회계, 인사, 영업, 결재, 게시판, 설정 등 | 항상 ON | -| 생산관리 | 생산지시, 작업지시, 작업일보 | 경동 전용 | -| 품질관리 | 설비점검, 수리요청, 검사 | 경동 전용 | -| 시공관리 | 프로젝트, 계약, 기성, 시공일보 | 주일 전용 | -| 차량관리 | 차량등록, 운행일지, 지게차 | 선택적 | - -**Q. 모듈 단위 범위가 적절한지? 분리/통합이 필요한 모듈이 있는지?** - -#### 3) 활성화 방식 -| 방식 | 장점 | 단점 | -|------|------|------| -| **A. 업종 자동** (현재) | 간단, 실수 방지 | 유연성 낮음 | -| **B. 모듈 개별 선택** (향후) | 유연함 | 관리 복잡 | -| **C. 업종 기본값 + 개별 재정의** | 균형 | 구현 복잡도 중간 | - -**Q. 어떤 방식을 채택할 것인지?** -현재 프론트엔드는 A 방식으로 구현, B/C로 확장 가능하도록 설계됨. - -#### 4) 적용 시점과 범위 -- 백엔드에서 `tenant.options.industry` 값만 세팅하면 즉시 동작 -- 값을 안 넣으면 기존과 100% 동일 (부작용 제로) -- **Q. 언제부터, 어떤 테넌트부터 적용할 것인지?** - ---- - -## 1. 개요 - -### 목표 -하나의 SAM ERP 코드베이스에서 **테넌트(회사)별로 필요한 모듈만 활성화**하여, -불필요한 메뉴·페이지·대시보드 섹션을 숨기는 구조. - -### 현재 테넌트별 모듈 구성 -| 업종 코드 | 테넌트 예시 | 활성 모듈 | -|-----------|------------|-----------| -| `shutter_mes` | 경동 | 생산관리, 품질관리, 차량관리 | -| `construction` | 주일 | 시공관리, 차량관리 | -| (미설정) | 기타 모든 테넌트 | **전체 모듈 활성화 (기존과 동일)** | - -### 안전 원칙 -``` -tenant.options.industry가 설정되지 않은 테넌트 → 모든 기능 그대로 사용 가능 -= 기존 동작 100% 유지, 부작용 제로 -``` - ---- - -## 2. 프론트엔드 구조 (완료) - -### 파일 구조 -``` -src/modules/ -├── types.ts # ModuleId 타입 정의 -├── tenant-config.ts # 업종→모듈 매핑 (resolveEnabledModules) -└── index.ts # 모듈 레지스트리 (라우트 매핑, 대시보드 섹션) - -src/hooks/ -└── useModules.ts # React 훅: isEnabled(), isRouteAllowed(), tenantIndustry -``` - -### 모듈 ID 목록 -| ModuleId | 이름 | 소유 라우트 | 대시보드 섹션 | -|----------|------|------------|--------------| -| `common` | 공통 ERP | /dashboard, /accounting, /sales, /hr, /approval, /settings 등 | 전부 | -| `production` | 생산관리 | /production | dailyProduction, unshipped | -| `quality` | 품질관리 | /quality | - | -| `construction` | 시공관리 | /construction | construction | -| `vehicle-management` | 차량관리 | /vehicle-management, /vehicle | - | - -### 프론트엔드 동작 흐름 -``` -1. 로그인 → authStore에 tenant 정보 저장 -2. useModules() 훅이 tenant.options.industry 읽음 -3. industry 값으로 INDUSTRY_MODULE_MAP 조회 → 활성 모듈 목록 결정 -4. 각 컴포넌트에서 isEnabled('production') 등으로 분기 -``` - -### 적용된 영역 - -#### A. CEO 대시보드 -- **섹션 필터링**: 비활성 모듈의 대시보드 섹션 자동 제외 -- **API 호출 차단**: 비활성 모듈의 API는 호출하지 않음 (null endpoint) -- **설정 팝업**: 비활성 모듈 섹션은 설정에서도 안 보임 -- **캘린더**: 비활성 모듈의 일정 유형 필터 숨김 -- **요약 네비**: 비활성 섹션 자동 제외 - -#### B. 라우트 접근 제어 -- `/production/*`, `/quality/*`, `/construction/*` 등 전용 라우트는 모듈 비활성 시 접근 차단 -- `/sales/*/production-orders` 같은 공통 라우트 내 모듈 의존 페이지는 명시적 가드 적용 - -#### C. 사이드바 메뉴 -- 비활성 모듈의 메뉴 항목 숨김 (isRouteAllowed 기반) - ---- - -## 3. 백엔드 필요 작업 - -### 3.1 tenants 테이블 options 필드에 industry 추가 -**우선순위: 🔴 필수** - -현재 프론트엔드는 `tenant.options.industry` 값을 읽어서 모듈을 결정합니다. -이 값이 백엔드에서 내려와야 실제로 동작합니다. - -```php -// tenants 테이블의 options JSON 컬럼에 industry 추가 -// 예시 데이터: -{ - "industry": "shutter_mes" // 경동: 셔터 MES -} -{ - "industry": "construction" // 주일: 건설 -} -// 다른 테넌트: industry 키 없음 → 프론트에서 전체 모듈 활성화 -``` - -**작업 내용:** -1. `tenants` 테이블의 `options` JSON 컬럼에 `industry` 키 추가 (마이그레이션 불필요, JSON이므로) -2. 경동 테넌트: `options->industry = 'shutter_mes'` -3. 주일 테넌트: `options->industry = 'construction'` -4. 테넌트 정보 API 응답에 `options.industry` 포함 확인 - -**확인 포인트:** -- 프론트엔드에서 `authStore.currentUser.tenant.options.industry`로 접근 -- 현재 로그인 API(`/api/v1/auth/me` 또는 유사)의 응답에서 tenant.options가 포함되는지 확인 -- 포함 안 되면 응답에 추가 필요 - -### 3.2 (선택) 테넌트 관리 화면에서 industry 설정 UI -**우선순위: 🟡 선택** - -관리자가 테넌트별 업종을 설정할 수 있는 UI. 급하지 않음 — DB 직접 수정으로 충분. - -### 3.3 (Phase 2 예정) 명시적 모듈 목록 API -**우선순위: 🟢 향후** - -현재는 `industry` → 프론트엔드 하드코딩 매핑으로 모듈 결정. -향후 백엔드에서 직접 모듈 목록을 내려주면 더 유연해짐. - -```php -// tenant.options 예시 (Phase 2) -{ - "industry": "shutter_mes", - "modules": ["production", "quality", "vehicle-management"] // 명시적 목록 -} -``` - -프론트엔드는 이미 이 구조를 지원하도록 준비되어 있음: -```typescript -// src/modules/tenant-config.ts -export function resolveEnabledModules(options) { - // Phase 2: 백엔드가 명시적 모듈 목록 제공 → 우선 사용 - if (explicitModules && explicitModules.length > 0) { - return explicitModules; - } - // Phase 1: industry 기반 기본값 (현재) - if (industry) { - return INDUSTRY_MODULE_MAP[industry] ?? []; - } - return []; -} -``` - ---- - -## 4. 업종별 모듈 매핑 (프론트엔드 하드코딩) - -```typescript -// src/modules/tenant-config.ts -const INDUSTRY_MODULE_MAP: Record = { - shutter_mes: ['production', 'quality', 'vehicle-management'], - construction: ['construction', 'vehicle-management'], -}; -``` - -새로운 업종 추가 시: -1. 여기에 매핑 추가 -2. 필요하면 `ModuleId` 타입에 새 모듈 ID 추가 -3. `MODULE_REGISTRY` (src/modules/index.ts)에 라우트/대시보드 섹션 등록 - ---- - -## 5. 핵심 코드 패턴 - -### 기본 사용법 -```typescript -import { useModules } from '@/hooks/useModules'; - -function MyComponent() { - const { isEnabled, tenantIndustry } = useModules(); - - // 안전 장치: industry 미설정이면 모든 기능 활성 - const moduleAware = !!tenantIndustry; - - if (moduleAware && !isEnabled('production')) { - return
생산관리 모듈이 비활성화되어 있습니다.
; - } - - // 생산관리 기능 렌더링... -} -``` - -### 크로스 모듈 임포트 규칙 -``` -✅ Common → Common (자유) -✅ Tenant → Common (자유) -✅ Common → Tenant (래퍼 경유) (src/lib/api/에서 MODULE_SEPARATION_OK 주석과 함께) -❌ Common → Tenant (직접) (scripts/verify-module-separation.sh가 검출) -❌ Tenant → Tenant (금지, dynamic import만 허용) -``` - ---- - -## 6. 구현 이력 - -| Phase | 내용 | 커밋 | 상태 | -|-------|------|------|------| -| Phase 0 | 크로스 모듈 의존성 해소 | `a99c3b39` | ✅ 완료 | -| Phase 1 | 모듈 레지스트리 + 라우트 가드 | `0a65609e` | ✅ 완료 | -| Phase 2 | CEO 대시보드 모듈 디커플링 | `46501214` | ✅ 완료 | -| Phase 3 | 물리적 분리 (경계 마커, 검증, 가드, 문서) | `4b8ca09e` | ✅ 완료 | - ---- - -## 7. 테스트 시나리오 - -### 테스트 방법 -백엔드에서 `tenant.options.industry`를 설정한 후: - -| 시나리오 | 예상 결과 | -|----------|----------| -| industry 미설정 테넌트 로그인 | 기존과 완전 동일 (모든 메뉴/기능 표시) | -| `shutter_mes` 테넌트 로그인 | 시공관리 메뉴 숨김, 대시보드 시공 섹션 안 보임 | -| `construction` 테넌트 로그인 | 생산/품질 메뉴 숨김, 대시보드 생산 섹션 안 보임 | -| `shutter_mes`에서 `/construction` 직접 접근 | 접근 차단 메시지 표시 | -| `construction`에서 `/production` 직접 접근 | 접근 차단 메시지 표시 | - -### 롤백 방법 -문제 발생 시 DB에서 `tenant.options.industry` 값만 제거하면 즉시 원복. -프론트엔드 코드 변경 불필요. - ---- - -## 8. 향후 로드맵 - -``` -현재 (Phase 1) 향후 (Phase 2) 최종 (Phase 3) -┌─────────────────┐ ┌──────────────────────┐ ┌─────────────────────┐ -│ industry 하드코딩 │ → │ 백엔드 modules 목록 │ → │ JSON 스키마 기반 │ -│ 매핑으로 모듈 결정 │ │ API에서 직접 수신 │ │ 동적 페이지 조립 │ -└─────────────────┘ └──────────────────────┘ └─────────────────────┘ -``` - -- **Phase 2**: `tenant.options.modules = ["production", "quality"]` 형태로 백엔드에서 명시적 모듈 목록 전달 → 업종 매핑 테이블 불필요 -- **Phase 3**: 각 모듈의 페이지 구성을 JSON 스키마로 정의 → 코드 변경 없이 테넌트별 화면 커스터마이징 diff --git a/claudedocs/archive/[IMPL-2025-11-07] seo-bot-blocking-configuration.md b/claudedocs/archive/[IMPL-2025-11-07] seo-bot-blocking-configuration.md deleted file mode 100644 index 7e82ac18..00000000 --- a/claudedocs/archive/[IMPL-2025-11-07] seo-bot-blocking-configuration.md +++ /dev/null @@ -1,364 +0,0 @@ -# SEO 및 봇 차단 설정 문서 - -## 개요 - -이 문서는 멀티 테넌트 ERP 시스템의 SEO 설정 및 봇 차단 전략을 설명합니다. 폐쇄형 시스템의 특성상 검색 엔진 수집을 방지하면서도, 과도한 차단으로 인한 브라우저 경고를 피하는 **균형 잡힌 접근 방식**을 채택했습니다. - ---- - -## 📋 구현 내용 - -### 1. robots.txt 설정 ✅ - -**위치**: `/public/robots.txt` - -**전략**: 느슨한 차단 (Moderate Blocking) - -#### 주요 설정 - -```txt -# 허용된 경로 (Allow) -- / (홈페이지) -- /login (로그인 페이지) -- /about (회사 소개) - -# 차단된 경로 (Disallow) -- /dashboard (대시보드) -- /admin (관리자 페이지) -- /api (API 엔드포인트) -- /tenant (테넌트 관리) -- /settings, /users, /reports, /analytics -- /inventory, /finance, /hr, /crm -- 기타 ERP 핵심 기능 경로 - -# 민감한 파일 형식 차단 -- /*.json, /*.xml, /*.csv -- /*.xls, /*.xlsx - -# Crawl-delay: 10초 -``` - -#### 크롬 경고 방지 전략 - -1. **홈페이지(/) 허용**: 완전 차단하지 않아 브라우저에서 악성 사이트로 분류되지 않음 -2. **공개 페이지 제공**: /login, /about 등 일부 공개 경로 허용 -3. **Crawl-delay 설정**: 서버 부하 감소 및 정상적인 봇 동작 유도 - ---- - -### 2. Middleware 봇 차단 로직 ✅ - -**위치**: `/src/middleware.ts` - -**역할**: 런타임에서 봇 요청을 감지하고 차단 - -#### 핵심 기능 - -##### 2.1 봇 패턴 감지 - -User-Agent 기반으로 다음 패턴을 감지: - -```typescript -- /bot/i, /crawler/i, /spider/i, /scraper/i -- /curl/i, /wget/i, /python-requests/i -- /axios/i (프로그래밍 방식 접근) -- /headless/i, /phantom/i, /selenium/i, /puppeteer/i, /playwright/i -- /go-http-client/i, /java/i, /okhttp/i -``` - -##### 2.2 경로 보호 전략 - -**보호된 경로 (Protected Paths)**: -- `/dashboard`, `/admin`, `/api` -- `/tenant`, `/settings`, `/users` -- `/reports`, `/analytics` -- `/inventory`, `/finance`, `/hr`, `/crm` -- `/employee`, `/customer`, `/supplier` -- `/orders`, `/invoices`, `/payroll` - -**공개 경로 (Public Paths)**: -- `/`, `/login`, `/about`, `/contact` -- `/robots.txt`, `/sitemap.xml`, `/favicon.ico` - -##### 2.3 차단 동작 - -봇이 보호된 경로에 접근 시: -```json -HTTP 403 Forbidden -{ - "error": "Access Denied", - "message": "Automated access to this resource is not permitted.", - "code": "BOT_ACCESS_DENIED" -} -``` - -##### 2.4 보안 헤더 추가 - -모든 응답에 다음 헤더 추가: -```http -X-Robots-Tag: noindex, nofollow, noarchive, nosnippet -X-Content-Type-Options: nosniff -X-Frame-Options: DENY -Referrer-Policy: strict-origin-when-cross-origin -``` - -##### 2.5 로깅 - -```typescript -// 차단된 봇 로그 -[Bot Blocked] {user-agent} attempted to access {pathname} - -// 허용된 봇 로그 (공개 경로) -[Bot Allowed] {user-agent} accessed {pathname} -``` - ---- - -### 3. SEO 메타데이터 설정 ✅ - -**위치**: `/src/app/layout.tsx` - -#### 메타데이터 구성 - -```typescript -metadata: { - title: { - default: "ERP System - Enterprise Resource Planning", - template: "%s | ERP System" - }, - description: "Multi-tenant Enterprise Resource Planning System for SME businesses", - robots: { - index: false, // 검색 엔진 색인 방지 - follow: false, // 링크 추적 방지 - nocache: true, // 캐싱 방지 - googleBot: { - index: false, - follow: false, - 'max-video-preview': -1, - 'max-image-preview': 'none', - 'max-snippet': -1, - } - }, - openGraph: { - type: 'website', - locale: 'ko_KR', - siteName: 'ERP System', - title: 'Enterprise Resource Planning System', - description: 'Multi-tenant ERP System for SME businesses', - }, - other: { - 'cache-control': 'no-cache, no-store, must-revalidate' - } -} -``` - -#### 주요 특징 - -1. **noindex, nofollow**: 검색 엔진 색인 및 링크 추적 차단 -2. **nocache**: 민감한 페이지 캐싱 방지 -3. **Google Bot 세부 제어**: 이미지, 비디오, 스니펫 미리보기 차단 -4. **Cache-Control 헤더**: 브라우저 및 프록시 캐싱 방지 -5. **다국어 지원**: locale 설정 (ko_KR) - ---- - -## 🎯 구현 전략 요약 - -| 구성 요소 | 목적 | 차단 강도 | 위치 | -|---------|------|---------|------| -| `robots.txt` | 검색 엔진 크롤러 가이드 | 느슨함 (Moderate) | `/public/robots.txt` | -| `middleware.ts` | 런타임 봇 감지 및 차단 | 강함 (Strong) | `/src/middleware.ts` | -| `layout.tsx` | HTML 메타 태그 설정 | 강함 (Strong) | `/src/app/layout.tsx` | - ---- - -## 🔒 보안 수준 - -### 다층 방어 (Defense in Depth) - -``` -Layer 1: robots.txt - ↓ 정상적인 검색 엔진 봇은 여기서 차단 - -Layer 2: Middleware Bot Detection - ↓ 악의적인 봇 및 자동화 도구 차단 - -Layer 3: SEO Meta Tags - ↓ HTML 레벨에서 색인 방지 - -Layer 4: Security Headers - ↓ 추가 보안 헤더로 보호 강화 -``` - -### 차단 vs 허용 균형 - -| 요소 | 설정 | 이유 | -|-----|------|------| -| 홈페이지 (/) | ✅ 허용 | 크롬 경고 방지 | -| 로그인 (/login) | ✅ 허용 | 정상 접근 가능 | -| 대시보드 (/dashboard) | ❌ 차단 | ERP 핵심 기능 보호 | -| API (/api) | ❌ 차단 | 데이터 보호 | -| 정적 파일 (.svg, .png 등) | ✅ 허용 | 정상 웹사이트 기능 | - ---- - -## 📊 동작 흐름 - -### 정상 사용자 (브라우저) - -``` -1. 사용자가 /dashboard 접근 -2. middleware.ts: User-Agent 확인 → 정상 브라우저 -3. X-Robots-Tag 헤더 추가 -4. 정상 페이지 렌더링 -5. HTML에 noindex 메타 태그 포함 -``` - -### 검색 엔진 봇 - -``` -1. Googlebot이 사이트 접근 -2. robots.txt 확인 → /dashboard Disallow -3. Googlebot은 /dashboard 접근하지 않음 -4. / (홈페이지)만 크롤링 → noindex 메타 태그 확인 -5. 검색 결과에 포함하지 않음 -``` - -### 악의적인 봇/스크래퍼 - -``` -1. curl/python-requests로 /api/users 접근 시도 -2. middleware.ts: User-Agent에서 'curl' 감지 -3. isProtectedPath('/api/users') → true -4. HTTP 403 Forbidden 반환 -5. 로그 기록: [Bot Blocked] curl/7.68.0 attempted to access /api/users -``` - ---- - -## 🧪 테스트 방법 - -### 1. robots.txt 확인 - -브라우저에서 접속: -``` -http://localhost:3000/robots.txt -``` - -### 2. Middleware 테스트 - -**정상 브라우저 접근**: -```bash -curl -H "User-Agent: Mozilla/5.0" http://localhost:3000/dashboard -# 예상: 정상 페이지 반환 (인증 로직 없으면 접근 가능) -``` - -**봇으로 접근**: -```bash -curl http://localhost:3000/dashboard -# 예상: HTTP 403 Forbidden -# {"error":"Access Denied","message":"Automated access to this resource is not permitted.","code":"BOT_ACCESS_DENIED"} -``` - -**공개 페이지 접근**: -```bash -curl http://localhost:3000/ -# 예상: 정상 페이지 반환 (X-Robots-Tag 헤더 포함) -``` - -### 3. 헤더 확인 - -```bash -curl -I http://localhost:3000/ -# 확인 항목: -# X-Robots-Tag: noindex, nofollow -# X-Content-Type-Options: nosniff -# X-Frame-Options: DENY -``` - -### 4. SEO 메타 태그 확인 - -브라우저에서 페이지 소스 보기: -```html - -``` - ---- - -## ⚠️ 주의사항 - -### 크롬 경고 방지 - -1. **완전 차단 금지**: robots.txt에서 모든 경로를 차단하면 안 됨 - ```txt - # ❌ 절대 사용 금지 - User-agent: * - Disallow: / - ``` - -2. **공개 페이지 유지**: 최소한 홈페이지는 허용 -3. **HTTP 상태 코드**: 403 사용 (404나 500은 피함) -4. **정상 사용자 차단 방지**: User-Agent 패턴 신중히 선택 - -### 로그 모니터링 - -- 차단된 봇 접근 시도를 모니터링하여 새로운 패턴 감지 -- 정상 사용자가 차단되는 경우 BOT_PATTERNS 조정 -- 로그 파일 위치: 콘솔 출력 (프로덕션에서는 로깅 서비스 연동 필요) - -### 성능 고려사항 - -- Middleware는 모든 요청에 실행되므로 성능 영향 최소화 -- 정규표현식 패턴 최적화 필요 -- 필요시 Redis 등으로 IP 기반 rate limiting 추가 고려 - ---- - -## 🔄 향후 개선 사항 - -### 1. IP 기반 Rate Limiting - -```typescript -// 추가 예정: Redis를 활용한 rate limiting -import { Ratelimit } from "@upstash/ratelimit"; -import { Redis } from "@upstash/redis"; -``` - -### 2. 화이트리스트 관리 - -```typescript -// 신뢰할 수 있는 IP나 User-Agent 화이트리스트 -const WHITELISTED_IPS = ['123.45.67.89']; -const WHITELISTED_USER_AGENTS = ['MyCompanyMonitoringBot']; -``` - -### 3. 고급 봇 감지 - -```typescript -// 행동 패턴 분석 (빠른 요청 속도, 비정상 경로 접근 등) -// Fingerprinting 기술 적용 -``` - -### 4. 로깅 서비스 연동 - -```typescript -// Sentry, LogRocket 등 APM 도구 연동 -// 봇 공격 패턴 분석 및 알림 -``` - ---- - -## 📝 변경 이력 - -| 날짜 | 버전 | 변경 내용 | -|-----|------|---------| -| 2025-11-06 | 1.0.0 | 초기 SEO 및 봇 차단 설정 구현 | - ---- - -## 참고 자료 - -- [Next.js Middleware Documentation](https://nextjs.org/docs/app/building-your-application/routing/middleware) -- [robots.txt Specification](https://developers.google.com/search/docs/crawling-indexing/robots/intro) -- [X-Robots-Tag HTTP Header](https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag) -- [OWASP Bot Management](https://owasp.org/www-community/controls/Blocking_Brute_Force_Attacks) diff --git a/claudedocs/archive/[IMPL-2025-11-11] chart-warning-fix.md b/claudedocs/archive/[IMPL-2025-11-11] chart-warning-fix.md deleted file mode 100644 index aa47b311..00000000 --- a/claudedocs/archive/[IMPL-2025-11-11] chart-warning-fix.md +++ /dev/null @@ -1,113 +0,0 @@ -# 차트 경고 수정 보고서 - -## 문제 상황 -CEODashboard에서 다음과 같은 경고가 발생: -``` -The width(-1) and height(-1) of chart should be greater than 0, -please check the style of container, or the props width(100%) and height(100%), -or add a minWidth(0) or minHeight(undefined) or use aspect(undefined) to control the -height and width. -``` - -## 원인 분석 - -### 문제 코드 -```tsx - -
- - - - ... - - - -
-
-``` - -### 원인 -1. `ResponsiveContainer`가 `height="100%"`로 설정됨 -2. 부모 div가 Tailwind 클래스 `h-80` 사용 -3. 컴포넌트 마운트 시점에 부모의 계산된 높이를 제대로 읽지 못함 -4. recharts가 높이를 -1로 계산하여 경고 발생 - -## 해결 방법 - -### 수정 코드 -```tsx - - {/* height="100%" → height={320} */} - -``` - -### 수정 이유 -- `h-80` = 320px (Tailwind: 1 단위 = 4px) -- 명시적인 픽셀 값으로 설정하여 마운트 시점에 즉시 계산 가능 -- ResponsiveContainer의 너비는 여전히 반응형 유지 (`width="100%"`) - -## 수정 위치 - -### CEODashboard.tsx -- Line 1201: 월별 매출 추이 차트 -- Line 1269: 품질 지표 차트 -- Line 1343: 생산 효율성 차트 -- Line 2127: 기타 차트 - -총 4개의 `ResponsiveContainer` 수정 완료 - -## 테스트 - -### 빌드 상태 -✅ **컴파일 성공**: `✓ Compiled successfully in 3.3s` - -### 예상 결과 -- ✅ 차트 경고 메시지 사라짐 -- ✅ 차트가 즉시 올바른 크기로 렌더링됨 -- ✅ 반응형 동작 유지 (너비는 여전히 100%) - -## 적용 가능한 다른 대시보드 - -현재는 CEODashboard에만 이 패턴이 있었지만, 만약 다른 대시보드에서도 같은 경고가 발생하면: - -```tsx -// Before - - -// After - -``` - -또는 부모 컨테이너의 높이에 맞춰 조정 - -## 참고사항 - -### Tailwind 높이 클래스 -- `h-64` = 256px -- `h-72` = 288px -- `h-80` = 320px -- `h-96` = 384px - -### ResponsiveContainer 권장 사항 -1. **고정 높이**: 대시보드 차트처럼 일정한 크기가 필요한 경우 - ```tsx - - ``` - -2. **비율 기반**: aspect ratio로 제어하고 싶은 경우 - ```tsx - - ``` - -3. **최소 높이**: 동적이지만 최소값이 필요한 경우 - ```tsx - - ``` - -## 결론 - -✅ **문제 해결**: 차트 크기 경고 완전히 제거 -✅ **성능 개선**: 마운트 시 즉시 올바른 크기로 렌더링 -✅ **반응형 유지**: 너비는 여전히 컨테이너에 맞춰 조정됨 - -recharts의 `ResponsiveContainer`를 사용할 때는 명시적인 높이를 설정하는 것이 권장됩니다! diff --git a/claudedocs/archive/[IMPL-2025-11-11] error-pages-configuration.md b/claudedocs/archive/[IMPL-2025-11-11] error-pages-configuration.md deleted file mode 100644 index 8a25c560..00000000 --- a/claudedocs/archive/[IMPL-2025-11-11] error-pages-configuration.md +++ /dev/null @@ -1,572 +0,0 @@ -# 에러 및 특수 페이지 구성 가이드 - -## 📋 개요 - -Next.js 15 App Router에서 404, 에러, 로딩 페이지 등 특수 페이지 구성 방법 및 우선순위 규칙 - ---- - -## 🎯 생성된 페이지 목록 - -### 1. 404 Not Found 페이지 - -| 파일 경로 | 적용 범위 | 레이아웃 포함 | -|-----------|----------|-------------| -| `app/[locale]/not-found.tsx` | 전역 (모든 경로) | ❌ 없음 | -| `app/[locale]/(protected)/not-found.tsx` | 보호된 경로만 | ✅ DashboardLayout | - -### 2. Error Boundary 페이지 - -| 파일 경로 | 적용 범위 | 레이아웃 포함 | -|-----------|----------|-------------| -| `app/[locale]/error.tsx` | 전역 에러 | ❌ 없음 | -| `app/[locale]/(protected)/error.tsx` | 보호된 경로 에러 | ✅ DashboardLayout | - -### 3. Loading 페이지 - -| 파일 경로 | 적용 범위 | 레이아웃 포함 | -|-----------|----------|-------------| -| `app/[locale]/(protected)/loading.tsx` | 보호된 경로 로딩 | ✅ DashboardLayout | - ---- - -## 📁 파일 구조 - -``` -src/app/ -├── [locale]/ -│ ├── not-found.tsx # ✅ 전역 404 (레이아웃 없음) -│ ├── error.tsx # ✅ 전역 에러 (레이아웃 없음) -│ │ -│ └── (protected)/ -│ ├── layout.tsx # 🎨 공통 레이아웃 (인증 + DashboardLayout) -│ ├── not-found.tsx # ✅ Protected 404 (레이아웃 포함) -│ ├── error.tsx # ✅ Protected 에러 (레이아웃 포함) -│ ├── loading.tsx # ✅ Protected 로딩 (레이아웃 포함) -│ │ -│ ├── dashboard/ -│ │ └── page.tsx # 실제 대시보드 페이지 -│ │ -│ └── [...slug]/ -│ └── page.tsx # 🔄 Catch-all (메뉴 기반 라우팅) -│ # - 메뉴에 있는 경로 → EmptyPage -│ # - 메뉴에 없는 경로 → not-found.tsx -``` - ---- - -## 🔍 페이지별 상세 설명 - -### 1. not-found.tsx (404 페이지) - -#### 전역 404 (`app/[locale]/not-found.tsx`) - -```typescript -// ✅ 특징: -// - 서버 컴포넌트 (async/await 가능) -// - 'use client' 불필요 -// - 레이아웃 없음 (전체 화면) -// - metadata 지원 가능 - -export default function NotFoundPage() { - return ( -
404 - 페이지를 찾을 수 없습니다
- ); -} -``` - -**트리거:** -- 존재하지 않는 URL 접근 -- `notFound()` 함수 호출 - -#### Protected 404 (`app/[locale]/(protected)/not-found.tsx`) - -```typescript -// ✅ 특징: -// - DashboardLayout 자동 적용 (사이드바, 헤더) -// - 인증된 사용자만 볼 수 있음 -// - 보호된 경로 내 404만 처리 - -export default function ProtectedNotFoundPage() { - return ( -
보호된 경로에서 페이지를 찾을 수 없습니다
- ); -} -``` - ---- - -### 2. error.tsx (에러 바운더리) - -#### 전역 에러 (`app/[locale]/error.tsx`) - -```typescript -'use client'; // ✅ 필수! - -export default function GlobalError({ - error, - reset, -}: { - error: Error & { digest?: string }; - reset: () => void; -}) { - return ( -
-

오류 발생: {error.message}

- -
- ); -} -``` - -**Props:** -- `error`: 발생한 에러 객체 - - `message`: 에러 메시지 - - `digest`: 에러 고유 ID (서버 로깅용) -- `reset`: 에러 복구 함수 (컴포넌트 재렌더링) - -**특징:** -- **'use client' 필수** - React Error Boundary는 클라이언트 전용 -- 하위 경로의 모든 에러 포착 -- 이벤트 핸들러 에러는 포착 불가 -- 루트 layout 에러는 포착 불가 (global-error.tsx 필요) - -#### Protected 에러 (`app/[locale]/(protected)/error.tsx`) - -```typescript -'use client'; // ✅ 필수! - -export default function ProtectedError({ - error, - reset, -}: { - error: Error & { digest?: string }; - reset: () => void; -}) { - return ( - // DashboardLayout 자동 적용됨 -
보호된 경로에서 오류 발생
- ); -} -``` - ---- - -### 3. loading.tsx (로딩 상태) - -#### Protected 로딩 (`app/[locale]/(protected)/loading.tsx`) - -```typescript -// ✅ 특징: -// - 서버/클라이언트 모두 가능 -// - React Suspense 자동 적용 -// - DashboardLayout 유지 - -export default function ProtectedLoading() { - return ( -
페이지를 불러오는 중...
- ); -} -``` - -**동작 방식:** -- `page.js`와 하위 요소를 자동으로 `` 경계로 감쌈 -- 페이지 전환 시 즉각적인 로딩 UI 표시 -- 네비게이션 중단 가능 - ---- - -## 🔄 우선순위 규칙 - -Next.js는 **가장 가까운 부모 세그먼트**의 파일을 사용합니다. - -### 404 우선순위 - -``` -/dashboard/settings 접근 시: - -1. dashboard/settings/not-found.tsx (가장 높음) -2. dashboard/not-found.tsx -3. (protected)/not-found.tsx ✅ 현재 사용됨 -4. [locale]/not-found.tsx (폴백) -5. app/not-found.tsx (최종 폴백) -``` - -### 에러 우선순위 - -``` -/dashboard 에서 에러 발생 시: - -1. dashboard/error.tsx -2. (protected)/error.tsx ✅ 현재 사용됨 -3. [locale]/error.tsx (폴백) -4. app/error.tsx (최종 폴백) -5. global-error.tsx (루트 layout 에러만) -``` - ---- - -## 🎨 레이아웃 적용 규칙 - -### 레이아웃 없는 페이지 (전역) - -``` -app/[locale]/not-found.tsx -app/[locale]/error.tsx -``` - -**특징:** -- 전체 화면 사용 -- 사이드바, 헤더 없음 -- 로그인 전/후 모두 접근 가능 - -**용도:** -- 로그인 페이지에서 404 -- 전역 에러 (로그인 실패 등) - -### 레이아웃 포함 페이지 (Protected) - -``` -app/[locale]/(protected)/not-found.tsx -app/[locale]/(protected)/error.tsx -app/[locale]/(protected)/loading.tsx -``` - -**특징:** -- DashboardLayout 자동 적용 -- 사이드바, 헤더 유지 -- 인증된 사용자만 접근 - -**용도:** -- 대시보드 내 404 -- 보호된 페이지 에러 -- 페이지 로딩 상태 - ---- - -## 🚨 'use client' 규칙 - -| 파일 | 필수 여부 | 이유 | -|------|-----------|------| -| `error.tsx` | ✅ **필수** | React Error Boundary는 클라이언트 전용 | -| `global-error.tsx` | ✅ **필수** | Error Boundary + 상태 관리 | -| `not-found.tsx` | ❌ 선택 | 서버 컴포넌트 가능 (metadata 지원) | -| `loading.tsx` | ❌ 선택 | 서버 컴포넌트 가능 (정적 UI 권장) | - -**에러 예시:** - -```typescript -// ❌ 잘못된 코드 - error.tsx에 'use client' 없음 -export default function Error({ error, reset }) { - // Error: Error boundaries must be Client Components -} - -// ✅ 올바른 코드 -'use client'; - -export default function Error({ error, reset }) { - // 정상 작동 -} -``` - ---- - -## 🔄 Catch-all 라우트와 메뉴 기반 라우팅 - -### 개요 - -`app/[locale]/(protected)/[...slug]/page.tsx` 파일은 **메뉴 기반 동적 라우팅**을 구현합니다. - -### 동작 로직 - -```typescript -'use client'; - -import { notFound } from 'next/navigation'; -import { EmptyPage } from '@/components/common/EmptyPage'; - -export default function CatchAllPage({ params }: PageProps) { - const [isValidPath, setIsValidPath] = useState(null); - - useEffect(() => { - // 1. localStorage에서 사용자 메뉴 데이터 가져오기 - const userData = JSON.parse(localStorage.getItem('user')); - const menus = userData.menu || []; - - // 2. 요청된 경로가 메뉴에 있는지 확인 - const requestedPath = `/${slug.join('/')}`; - const isPathInMenu = checkMenuRecursively(menus, requestedPath); - - // 3. 메뉴 존재 여부에 따라 분기 - setIsValidPath(isPathInMenu); - }, [params]); - - // 메뉴에 없는 경로 → 404 - if (!isValidPath) { - notFound(); - } - - // 메뉴에 있지만 구현되지 않은 페이지 → EmptyPage - return ; -} -``` - -### 라우팅 결정 트리 - -``` -사용자가 /base/product/lists 접근 -│ -├─ 1️⃣ localStorage에서 user.menu 읽기 -│ └─ 메뉴 데이터: [{path: '/base/product/lists', ...}, ...] -│ -├─ 2️⃣ 경로 검증 -│ ├─ ✅ 메뉴에 경로 존재 -│ │ └─ EmptyPage 표시 (구현 예정 페이지) -│ │ -│ └─ ❌ 메뉴에 경로 없음 -│ └─ notFound() 호출 → not-found.tsx -│ -└─ 3️⃣ 최종 결과 - ├─ 메뉴에 있음: EmptyPage (DashboardLayout 포함) - └─ 메뉴에 없음: not-found.tsx (DashboardLayout 포함) -``` - -### 사용 예시 - -#### 케이스 1: 메뉴에 있는 경로 (구현 안됨) - -```bash -# 사용자 메뉴에 /base/product/lists가 있는 경우 -http://localhost:3000/ko/base/product/lists -→ ✅ EmptyPage 표시 (사이드바, 헤더 유지) -``` - -#### 케이스 2: 메뉴에 없는 엉뚱한 경로 - -```bash -# 사용자 메뉴에 /fake-page가 없는 경우 -http://localhost:3000/ko/fake-page -→ ❌ not-found.tsx 표시 (사이드바, 헤더 유지) -``` - -#### 케이스 3: 실제 구현된 페이지 - -```bash -# dashboard/page.tsx가 실제로 존재 -http://localhost:3000/ko/dashboard -→ ✅ Dashboard 컴포넌트 표시 -``` - -### 메뉴 데이터 구조 - -```typescript -// localStorage에 저장되는 메뉴 구조 (로그인 시 받아옴) -{ - menu: [ - { - id: "1", - label: "기초정보관리", - path: "/base", - children: [ - { - id: "1-1", - label: "제품관리", - path: "/base/product/lists" - }, - { - id: "1-2", - label: "거래처관리", - path: "/base/company/lists" - } - ] - }, - { - id: "2", - label: "시스템관리", - path: "/system", - children: [ - { - id: "2-1", - label: "사용자관리", - path: "/system/user/lists" - } - ] - } - ] -} -``` - -### 장점 - -1. **동적 메뉴 관리**: 백엔드에서 메뉴 구조 변경 시 프론트엔드 코드 수정 불필요 -2. **권한 기반 라우팅**: 사용자별 메뉴가 다르면 접근 가능한 경로도 다름 -3. **명확한 UX**: - - 메뉴에 있는 페이지 (미구현) → "준비 중" 메시지 - - 메뉴에 없는 페이지 → "404 Not Found" - -### 디버깅 - -개발 모드에서는 콘솔에 디버그 로그가 출력됩니다: - -```typescript -console.log('🔍 요청된 경로:', requestedPath); -console.log('📋 메뉴 데이터:', menus); -console.log(' - 비교 중:', item.path, 'vs', path); -console.log('📌 경로 존재 여부:', pathExists); -``` - ---- - -## 💡 실전 사용 예시 - -### 1. 404 테스트 - -```typescript -// 존재하지 않는 경로 접근 -/non-existent-page -→ app/[locale]/not-found.tsx 표시 - -// 보호된 경로에서 404 -/dashboard/unknown-page -→ app/[locale]/(protected)/not-found.tsx 표시 (레이아웃 포함) -``` - -### 2. 에러 발생 시뮬레이션 - -```typescript -// page.tsx -export default function TestPage() { - // 의도적으로 에러 발생 - throw new Error('테스트 에러'); - - return
페이지
; -} - -// → error.tsx가 에러 포착 -``` - -### 3. 프로그래매틱 404 - -```typescript -import { notFound } from 'next/navigation'; - -export default function ProductPage({ params }: { params: { id: string } }) { - const product = getProduct(params.id); - - if (!product) { - notFound(); // ← not-found.tsx 표시 - } - - return
{product.name}
; -} -``` - -### 4. 에러 복구 - -```typescript -'use client'; - -export default function Error({ error, reset }: { error: Error; reset: () => void }) { - return ( -
-

오류 발생: {error.message}

- -
- ); -} -``` - ---- - -## 🐛 개발 환경 vs 프로덕션 - -### 개발 환경 (development) - -```typescript -// 에러 상세 정보 표시 -{process.env.NODE_ENV === 'development' && ( -
-

에러 메시지: {error.message}

-

스택 트레이스: {error.stack}

-
-)} -``` - -**특징:** -- 에러 오버레이 표시 -- 상세한 에러 정보 -- Hot Reload 지원 - -### 프로덕션 (production) - -```typescript -// 사용자 친화적 메시지만 표시 -
-

일시적인 오류가 발생했습니다.

- -
-``` - -**특징:** -- 간결한 에러 메시지 -- 보안 정보 숨김 -- 에러 로깅 (Sentry 등) - ---- - -## 📌 체크리스트 - -### 404 페이지 - -- [ ] 전역 404 페이지 생성 (`app/[locale]/not-found.tsx`) -- [ ] Protected 404 페이지 생성 (`app/[locale]/(protected)/not-found.tsx`) -- [ ] 레이아웃 적용 확인 -- [ ] 다국어 지원 (선택사항) -- [ ] 버튼 링크 동작 테스트 - -### 에러 페이지 - -- [ ] 'use client' 지시어 추가 확인 -- [ ] Props 타입 정의 (`error`, `reset`) -- [ ] 개발/프로덕션 환경 분기 -- [ ] 에러 로깅 추가 (선택사항) -- [ ] 복구 버튼 동작 테스트 - -### 로딩 페이지 - -- [ ] 로딩 UI 디자인 일관성 -- [ ] 레이아웃 내 표시 확인 -- [ ] Suspense 경계 테스트 - -### Catch-all 라우트 (메뉴 기반 라우팅) - -- [x] localStorage 메뉴 데이터 검증 로직 구현 -- [x] 메뉴에 있는 경로 → EmptyPage 분기 -- [x] 메뉴에 없는 경로 → not-found.tsx 분기 -- [x] 재귀적 메뉴 트리 탐색 구현 -- [ ] 디버그 로그 프로덕션 제거 -- [ ] 성능 최적화 (메뉴 데이터 캐싱) - ---- - -## 🔗 관련 문서 - -- [Empty Page Configuration](./[IMPL-2025-11-11]%20empty-page-configuration.md) -- [Route Protection Architecture](./[IMPL-2025-11-07]%20route-protection-architecture.md) -- [Authentication Implementation Guide](./[IMPL-2025-11-07]%20authentication-implementation-guide.md) - ---- - -## 📚 참고 자료 - -- [Next.js 15 Error Handling](https://nextjs.org/docs/app/building-your-application/routing/error-handling) -- [Next.js 15 Not Found](https://nextjs.org/docs/app/api-reference/file-conventions/not-found) -- [Next.js 15 Loading UI](https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming) - ---- - -**작성일:** 2025-11-11 -**작성자:** Claude Code -**마지막 수정:** 2025-11-12 (Catch-all 라우트 메뉴 기반 로직 추가) \ No newline at end of file diff --git a/claudedocs/archive/[IMPL-2025-11-12] modal-select-layout-shift-fix.md b/claudedocs/archive/[IMPL-2025-11-12] modal-select-layout-shift-fix.md deleted file mode 100644 index a1aa73b0..00000000 --- a/claudedocs/archive/[IMPL-2025-11-12] modal-select-layout-shift-fix.md +++ /dev/null @@ -1,1183 +0,0 @@ -# Shadcn UI Select 모달 레이아웃 시프트 방지 - -## 📋 개요 - -Shadcn UI Select 컴포넌트를 모달 스타일로 사용할 때 발생하는 레이아웃 시프트(스크롤바 사라짐/생김으로 인한 화면 덜컥거림) 문제를 **단 2줄의 CSS**로 해결 - ---- - -## 🎯 해결한 문제 - -### 기존 문제점 - -**문제 상황:** -- 로그인/회원가입 페이지 및 대시보드 헤더의 테마/언어 선택을 네이티브 ``에서 Shadcn UI 모달 Select로 변경 -- `native={false}` 프로퍼티로 모달 스타일 활성화 - ---- - -### 3. `/src/components/auth/SignupPage.tsx` - -**변경 사항:** - -```typescript - - -``` - -**설명:** -- 로그인 페이지와 동일하게 모달 스타일 적용 - ---- - -### 4. `/src/layouts/DashboardLayout.tsx` - -**변경 사항:** - -```typescript -// Line 231 - -``` - -**설명:** -- 대시보드 헤더의 테마 선택도 모달 스타일로 변경 -- 전체 앱에서 일관된 UI/UX 제공 - ---- - -## 🧪 테스트 결과 - -### 테스트 1: 모달 열고 닫기 - -```typescript -// Given: 로그인 페이지 -const initialWidth = document.body.clientWidth - -// When: 테마 선택 클릭 -click(themeSelect) - -// Then: 레이아웃 너비 변화 없음 -const modalOpenWidth = document.body.clientWidth -expect(modalOpenWidth).toBe(initialWidth) ✅ - -// When: 모달 닫기 -close(modal) - -// Then: 레이아웃 너비 변화 없음 -const modalCloseWidth = document.body.clientWidth -expect(modalCloseWidth).toBe(initialWidth) ✅ -``` - ---- - -### 테스트 2: 여러 번 반복 - -```typescript -// Given: 초기 상태 -const initialWidth = document.body.clientWidth - -// When: 10번 반복 열고 닫기 -for (let i = 0; i < 10; i++) { - open(themeSelect) - close(themeSelect) -} - -// Then: 누적 레이아웃 시프트 없음 -const finalWidth = document.body.clientWidth -expect(finalWidth).toBe(initialWidth) ✅ -``` - ---- - -### 테스트 3: 다양한 페이지 - -```typescript -// Tested on: -- 로그인 페이지 ✅ -- 회원가입 페이지 ✅ -- 대시보드 헤더 ✅ - -// Result: 모든 페이지에서 레이아웃 이동 없음 -``` - ---- - -## 💡 시행착오 과정 - -### 시도했던 복잡한 방법들 - -```css -/* ❌ 시도 1: Padding 보정 */ -body[data-scroll-locked] { - padding-right: var(--removed-body-scroll-bar-size, 0px) !important; -} -/* 결과: 여전히 시프트 발생 */ - -/* ❌ 시도 2: Position fixed + JavaScript */ -body[data-scroll-locked] { - position: fixed !important; - overflow-y: scroll !important; -} -/* 결과: 열릴 때는 괜찮지만 닫힐 때 시프트 */ - -/* ❌ 시도 3: scrollbar-gutter */ -body { - scrollbar-gutter: stable; -} -/* 결과: 열릴 때도 닫힐 때도 모두 시프트 */ - -/* ❌ 시도 4: HTML 레벨 스크롤 */ -html { - overflow-y: scroll; -} -body { - overflow: visible !important; -} -body[data-scroll-locked] { - overflow: visible !important; - position: static !important; - padding-right: 0 !important; - margin-right: 0 !important; -} -[data-radix-portal] { - position: fixed; -} -/* 결과: 동작하지만 불필요하게 복잡함 */ -``` - -### 최종 발견: 단순함의 승리 - -```css -/* ✅ 최종 해결책: 단 2줄 */ -body { - overflow: visible !important; -} - -body[data-scroll-locked] { - margin-right: 0 !important; -} -``` - -**교훈:** -- 복잡한 문제도 간단한 해결책이 있을 수 있음 -- 근본 원인을 정확히 파악하면 최소한의 코드로 해결 가능 -- `html { overflow-y: scroll }` 등은 모두 불필요했음 -- **overflow: visible + margin-right: 0** 만으로 충분! - ---- - -## 🎨 브라우저 호환성 - -### 테스트 완료 - -| 브라우저 | 버전 | 결과 | -|---------|------|------| -| Chrome | 120+ | ✅ 완벽 | -| Edge | 120+ | ✅ 완벽 | -| Firefox | 120+ | ✅ 완벽 | -| Safari | 17+ | ✅ 완벽 | -| Mobile Chrome | Latest | ✅ 완벽 | -| Mobile Safari | iOS 17+ | ✅ 완벽 | - -**결론:** -- 모든 모던 브라우저에서 정상 작동 -- 추가 polyfill 불필요 -- 모바일에서도 완벽히 동작 - ---- - -## 📊 개선 효과 - -### Core Web Vitals - -**CLS (Cumulative Layout Shift):** -``` -Before: 0.15+ (Poor - 빨간색) -After: 0.00 (Good - 초록색) -개선율: 100% -``` - -**Impact:** -- 페이지 품질 점수 상승 -- SEO 순위 개선 가능 -- 사용자 경험 향상 - ---- - -### 사용자 경험 - -| 지표 | Before | After | -|------|--------|-------| -| 모달 열 때 레이아웃 시프트 | 발생 | 없음 | -| 모달 닫을 때 레이아웃 시프트 | 발생 | 없음 | -| 브라우저 네이티브 UX 일치도 | 0% | 100% | -| 코드 복잡도 | 높음 | 매우 낮음 | -| CSS 라인 수 | 20+ | 2 | - ---- - -## 🔬 기술적 세부사항 - -### CSS Specificity - -```css -/* Radix UI (라이브러리): */ -body[data-scroll-locked] { overflow: hidden !important; } -/* Specificity: 0,0,1,1 */ - -/* Our CSS (우리 코드): */ -body[data-scroll-locked] { margin-right: 0 !important; } -/* Specificity: 0,0,1,1 */ -``` - -**우선순위:** -- 동일한 specificity -- 하지만 우리 CSS가 나중에 로드됨 (globals.css) -- `!important` 덕분에 확실히 override - ---- - -### 스크롤 동작 원리 - -``` -일반적인 구조: -┌─────────────────┐ -│ html │ ← overflow: auto (기본값) -│ ┌─────────────┐ │ -│ │ body │ │ ← overflow: visible -│ │ │ │ -│ │ content │ │ -│ └─────────────┘ │ -└─────────────────┘ - -스크롤 발생 시: -- html 요소에서 스크롤바 표시 -- body는 영향 없음 -- Radix의 overflow: hidden이 무의미 -``` - ---- - -## 🚀 성능 영향 - -### 렌더링 성능 - -```typescript -// Before: body overflow 변경 시 -// - Layout recalculation 발생 -// - Paint 발생 -// - Composite 발생 -// 총 렌더링 시간: ~15-20ms - -// After: body 스타일 변경 없음 -// - Layout recalculation 없음 -// - Paint 없음 -// - Composite만 발생 (모달 표시) -// 총 렌더링 시간: ~3-5ms -``` - -**개선 효과:** -- 렌더링 시간 70% 감소 -- 프레임 드롭 없음 -- 부드러운 애니메이션 - ---- - -## 🎓 배운 교훈 - -### 1. 문제의 본질 파악 - -**핵심:** -- Radix UI가 하려는 것: `overflow: hidden` + `margin-right` 보정 -- 우리가 막아야 하는 것: 정확히 이 두 가지 -- 해결: 각각 `!important`로 차단 - -**교훈:** -- 라이브러리 동작을 정확히 이해하면 최소한의 코드로 해결 가능 -- 과도한 워크어라운드는 불필요 - ---- - -### 2. 간단함의 가치 - -**Before:** -```css -/* 20줄 이상의 복잡한 CSS */ -/* JavaScript 스크립트 추가 */ -/* 여러 요소에 스타일 적용 */ -``` - -**After:** -```css -/* 단 2줄의 명확한 CSS */ -/* JavaScript 불필요 */ -/* body 요소만 수정 */ -``` - -**교훈:** -- 복잡한 문제에도 단순한 해결책이 존재 -- 코드가 짧을수록 유지보수 용이 -- "작동하는 최소한의 코드"가 베스트 - ---- - -### 3. 사용자 피드백의 중요성 - -**프로세스:** -1. 복잡한 해결책 시도 → 사용자 테스트 -2. "여전히 움직여요" → 다른 방법 시도 -3. "html만 남기면 되는데..." → 더 단순화 -4. "이것만 있으면 완벽해요" → 최종 해결 ✅ - -**교훈:** -- 실제 사용자 테스트가 가장 중요 -- 개발자의 "완벽한" 솔루션 ≠ 사용자가 원하는 솔루션 -- 반복적 개선으로 최적해 도달 - ---- - -## 🎯 해결한 문제 #2: 드롭다운/팝오버 위치 및 애니메이션 문제 - -### 날짜 -**2025-11-17** - -### 새로운 문제 발견 - -**문제 상황:** -- 컬러 모드 드롭다운 (DropdownMenu)과 BOM 검색 박스 (Popover)가 의도한 위치에 나타나지 않음 -- 두 가지 현상 발생: - 1. **첫 번째 시도**: 좌측에서 "날아오는" 애니메이션 효과 - 2. **두 번째 시도**: body 왼쪽 상단 (0, 0)에 고정 - -**사용자 요구사항:** -> "누른 대상의 위치를 찾고 추가된 span position 값을 absolute로 잡고 바로 누른 자리에서 나올 수 있게" - -즉, **클릭한 버튼 바로 아래에서 즉시 나타나야 함** - ---- - -### 원인 분석: 3단계 디버깅 과정 - -#### 🔍 Phase 1: 날아오는 애니메이션 원인 - -**첫 번째 시도:** -```css -/* globals.css:238-241 */ -[data-radix-popper-content-wrapper] { - will-change: auto !important; - transform: none !important; /* ← 이게 문제! */ -} -``` - -**결과:** -- ❌ 날아오는 효과는 사라졌지만... -- ❌ body 왼쪽 상단 (0, 0)에 고정되어버림! - -**왜 실패했는가:** -```typescript -// Radix UI의 위치 계산 메커니즘: -// 1. @floating-ui/react-dom이 클릭된 버튼 위치 계산 -// 2. 계산된 좌표를 transform으로 적용 -const calculatedPosition = { - x: 245, // 버튼의 x 좌표 - y: 80 // 버튼의 y 좌표 -} -element.style.transform = `translate3d(${x}px, ${y}px, 0px)` - -// ❌ 문제: transform: none !important가 이 계산을 무효화! -// 결과: element는 (0, 0)에 고정됨 -``` - ---- - -#### 🔍 Phase 2: 진짜 원인 발견 - 전역 transition - -**globals.css를 다시 분석:** -```css -/* Line 282-284: 모든 요소에 transition 적용! */ -* { - transition: all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94); -} -``` - -**이것이 진짜 범인이었음:** -```typescript -// Radix UI가 위치를 계산하고 적용하는 과정: - -// 1. 초기 렌더링 (Portal을 통해 body에 추가) -element.style.transform = 'translate3d(0px, 0px, 0px)' // 초기값 - -// 2. 위치 계산 완료 (Floating UI) -const position = calculatePosition(trigger, content) -// position = { x: 245, y: 80 } - -// 3. transform 업데이트 -element.style.transform = `translate3d(245px, 80px, 0px)` - -// ❌ 문제: 전역 * { transition: all } 때문에 -// transform이 즉시 변경되지 않고 -// 0,0 → 245,80으로 0.2초 동안 애니메이션됨! -// → "날아오는" 효과 발생! -``` - -**시각적 설명:** -``` -전역 transition이 없다면: -클릭 → [계산] → 즉시 (245, 80)에 나타남 ✅ - -전역 transition이 있으면: -클릭 → [계산] → (0, 0)에서 시작 → 0.2초간 이동 → (245, 80) ❌ - ↑ - "날아오는" 효과! -``` - ---- - -#### 🔍 Phase 3: 완벽한 해결책 - -**핵심 깨달음:** -1. `transform`은 **반드시 유지**해야 함 (위치 계산 필수) -2. `transition`만 **선택적으로 제거**하면 됨 -3. `animation`도 제거하면 더 깔끔 - -**최종 해결책:** -```css -/* globals.css:238-249 */ - -/* ✅ transform은 유지, transition만 제거 */ -[data-radix-popper-content-wrapper] { - will-change: auto !important; - transition: none !important; /* 핵심! 전역 transition 무효화 */ -} - -/* ✅ 추가로 slide 애니메이션도 제거 */ -[data-radix-dropdown-menu-content], -[data-radix-select-content], -[data-radix-popover-content] { - animation-name: none !important; -} -``` - ---- - -### 작동 원리 상세 분석 - -#### 1. Radix UI의 Positioning 메커니즘 - -```typescript -// Radix UI는 내부적으로 Floating UI를 사용 -import { useFloating } from '@floating-ui/react-dom' - -// 1. 트리거 요소 (버튼)의 위치 측정 -const triggerRect = trigger.getBoundingClientRect() -// { x: 245, y: 80, width: 120, height: 40 } - -// 2. 컨텐츠 요소의 크기 측정 -const contentRect = content.getBoundingClientRect() -// { width: 200, height: 150 } - -// 3. 최적 위치 계산 (충돌 방지, 뷰포트 체크) -const position = computePosition(trigger, content, { - placement: 'bottom', // 버튼 아래에 배치 - middleware: [offset(4), flip(), shift()] -}) - -// 4. 계산된 위치를 transform으로 적용 -content.style.transform = `translate3d(${position.x}px, ${position.y}px, 0px)` -``` - -#### 2. 전역 Transition의 영향 - -```css -/* globals.css에 있는 전역 스타일 */ -* { - transition: all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94); -} -``` - -**이 전역 transition이 미치는 영향:** -```typescript -// Before (전역 transition 있음): -element.style.transform = 'translate3d(0, 0, 0)' // 초기 -// → 0.2초 동안 transition -element.style.transform = 'translate3d(245, 80, 0)' // 최종 -// 결과: 좌측 상단에서 날아오는 효과 ❌ - -// After (transition: none 적용): -element.style.transform = 'translate3d(245, 80, 0)' // 즉시! -// 결과: 계산된 위치에 바로 나타남 ✅ -``` - -#### 3. CSS Specificity와 Override - -```css -/* 전역 스타일 (낮은 우선순위) */ -* { - transition: all 0.2s; -} -/* Specificity: 0,0,0,0 (universal selector) */ - -/* 우리의 Override (높은 우선순위) */ -[data-radix-popper-content-wrapper] { - transition: none !important; -} -/* Specificity: 0,0,1,0 + !important */ -``` - -**결과:** -- 전역 `*` 선택자보다 속성 선택자가 우선 -- `!important`로 확실히 override -- popper-content-wrapper와 그 자식들은 transition 없음 - ---- - -### 시행착오 타임라인 - -#### ❌ 시도 1: transform 제거 -```css -[data-radix-popper-content-wrapper] { - will-change: auto !important; - transform: none !important; /* 잘못된 접근 */ -} -``` -**결과:** body (0, 0)에 고정됨 - -**교훈:** Radix UI의 위치 계산에 transform이 필수임을 깨달음 - ---- - -#### ❌ 시도 2: animation만 제거 -```css -[data-radix-dropdown-menu-content], -[data-radix-select-content], -[data-radix-popover-content] { - animation-duration: 0ms !important; -} -``` -**결과:** 여전히 날아오는 효과 발생 - -**교훈:** 문제는 animation이 아니라 transition이었음 - ---- - -#### ✅ 시도 3: transition 제거 (성공!) -```css -[data-radix-popper-content-wrapper] { - will-change: auto !important; - transition: none !important; /* 핵심! */ -} -``` -**결과:** 완벽하게 작동! 클릭한 위치에서 즉시 나타남 ✅ - -**교훈:** 근본 원인을 정확히 파악하는 것이 중요 - ---- - -### 기술적 심층 분석 - -#### Floating UI의 위치 계산 알고리즘 - -```typescript -// @floating-ui/react-dom의 내부 동작 - -interface ComputePositionConfig { - placement: Placement // 'top' | 'bottom' | 'left' | 'right' ... - middleware?: Middleware[] // offset, flip, shift, arrow ... - platform?: Platform // DOM 환경 정보 -} - -function computePosition( - reference: Element, // 트리거 (버튼) - floating: Element, // 컨텐츠 (드롭다운) - config: ComputePositionConfig -): Promise { - - // 1. 참조 요소 위치 가져오기 - const referenceRect = reference.getBoundingClientRect() - - // 2. 부유 요소 크기 가져오기 - const floatingRect = floating.getBoundingClientRect() - - // 3. 기본 위치 계산 - let x = referenceRect.x - let y = referenceRect.y + referenceRect.height // 아래쪽 - - // 4. Middleware 적용 (순서대로) - for (const middleware of middlewares) { - const result = await middleware.fn({ - x, y, - initialPlacement: config.placement, - // ... other data - }) - - x = result.x ?? x - y = result.y ?? y - - // flip: 뷰포트 밖이면 반대로 - // shift: 뷰포트에 맞게 이동 - // offset: 간격 추가 - } - - // 5. 최종 좌표 반환 - return { x, y, placement: finalPlacement } -} -``` - -#### Transform vs Position - -**왜 Radix UI는 position이 아닌 transform을 사용하는가?** - -```css -/* ❌ position 방식 (사용하지 않음) */ -.popover { - position: fixed; - top: 80px; /* 리플로우 발생 */ - left: 245px; /* 리플로우 발생 */ -} - -/* ✅ transform 방식 (Radix UI가 사용) */ -.popover { - position: fixed; - top: 0; - left: 0; - transform: translate3d(245px, 80px, 0); /* GPU 가속, 리플로우 없음 */ -} -``` - -**장점:** -1. **성능**: GPU 가속으로 부드러운 애니메이션 -2. **효율**: Reflow/Repaint 최소화 -3. **정밀도**: 소수점 단위 위치 지정 가능 -4. **합성**: 다른 transform과 결합 가능 - ---- - -### 브라우저 렌더링 파이프라인 분석 - -#### Before (전역 transition 있음) - -``` -1. JavaScript: Floating UI 위치 계산 - ↓ ~2ms -2. Style Recalculation: transform 변경 감지 - ↓ ~1ms -3. Layout: (없음, transform은 layout에 영향 없음) - ↓ 0ms -4. Paint: (없음, transform만 변경) - ↓ 0ms -5. Composite: GPU에서 transform 애니메이션 - ↓ ~200ms (transition duration) - -총: ~203ms (사용자가 "날아오는" 효과를 봄) -``` - -#### After (transition: none 적용) - -``` -1. JavaScript: Floating UI 위치 계산 - ↓ ~2ms -2. Style Recalculation: transform 변경 감지 - ↓ ~1ms -3. Layout: (없음) - ↓ 0ms -4. Paint: (없음) - ↓ 0ms -5. Composite: GPU에서 즉시 위치 변경 - ↓ ~16ms (1 frame) - -총: ~19ms (사용자가 즉시 나타나는 것을 봄) -``` - -**성능 개선:** -- 렌더링 시간: 203ms → 19ms (91% 감소) -- 사용자 체감: "날아오는" → "즉시 나타남" - ---- - -### 교훈과 베스트 프랙티스 - -#### 1. 전역 CSS의 위험성 - -**문제:** -```css -/* 모든 요소에 영향을 미치는 전역 스타일 */ -* { - transition: all 0.2s; -} -``` - -**위험 요소:** -- 서드파티 라이브러리의 동작 방해 -- 예상치 못한 애니메이션 발생 -- 디버깅 어려움 (원인 찾기 힘듦) - -**대안:** -```css -/* 특정 요소만 타겟팅 */ -.interactive-element { - transition: background-color 0.2s, color 0.2s; -} - -/* 또는 CSS 변수로 관리 */ -:root { - --transition-fast: 0.15s ease; -} - -.button { - transition: background-color var(--transition-fast); -} -``` - ---- - -#### 2. 라이브러리 동작 이해의 중요성 - -**Radix UI의 핵심 동작:** -1. Portal을 통해 body 끝에 렌더링 -2. Floating UI로 위치 계산 -3. `transform: translate3d(x, y, 0)` 적용 -4. `position: fixed`로 화면에 고정 - -**이해하면:** -- `transform`이 필수임을 알 수 있음 -- `transition`이 문제임을 파악 가능 -- 최소한의 CSS로 해결 가능 - -**이해하지 못하면:** -- 과도한 workaround 시도 -- 불필요한 JavaScript 추가 -- 복잡한 해결책 (20줄 이상의 CSS) - ---- - -#### 3. 디버깅 프로세스 - -**효과적인 디버깅 순서:** -``` -1. 문제 재현 및 관찰 - → "날아오는" 효과 발생 확인 - -2. 브라우저 DevTools 활용 - → Elements 탭: transform 값 확인 - → Computed 탭: transition 값 확인 - -3. 가설 수립 - → "전역 transition이 transform에 영향?" - -4. 최소 재현 (Minimal Reproduction) - → transition: none 추가로 테스트 - -5. 검증 및 적용 - → 완벽하게 작동하는지 확인 - -6. 문서화 - → 이 문서에 기록! -``` - ---- - -#### 4. 성능 최적화 원칙 - -**CSS 성능 순서 (빠른 순):** -``` -1. opacity, transform → Composite만 (가장 빠름) -2. color, background → Paint + Composite -3. width, height, margin → Layout + Paint + Composite (가장 느림) -``` - -**Radix UI가 transform을 사용하는 이유:** -- Composite Layer에서만 작동 -- GPU 가속 활용 -- Reflow/Repaint 없음 -- 60fps 유지 가능 - ---- - -### 영향을 받는 컴포넌트 - -**이 수정으로 개선된 모든 컴포넌트:** - -1. **DropdownMenu** (DashboardLayout.tsx) - - 테마 선택 드롭다운 - - 언어 선택 드롭다운 - - 사용자 메뉴 드롭다운 - -2. **Popover** (ItemForm.tsx) - - BOM 부품 검색 팝오버 - - 기타 검색 팝오버 - -3. **Select** (모든 페이지) - - 이미 레이아웃 시프트는 해결되어 있었음 - - 이번 수정으로 위치 정확도 추가 개선 - ---- - -### 측정 가능한 개선 효과 - -#### 1. 사용자 경험 지표 - -| 지표 | Before | After | 개선 | -|------|--------|-------|------| -| 드롭다운 열림 시간 | 203ms | 19ms | 91% ↓ | -| 위치 정확도 | body (0,0) 고정 | 클릭 위치 정확 | 100% | -| 시각적 일관성 | 날아오는 효과 | 즉시 나타남 | ✅ | -| 네이티브 UX 일치도 | 0% | 100% | +100% | - -#### 2. 성능 지표 - -```typescript -// Performance Timeline 분석 - -// Before: -{ - "name": "dropdown-open", - "duration": 203.4, - "entries": [ - { "name": "style-recalc", "duration": 1.2 }, - { "name": "composite", "duration": 200.8 }, // ← transition - { "name": "paint", "duration": 1.4 } - ] -} - -// After: -{ - "name": "dropdown-open", - "duration": 18.6, - "entries": [ - { "name": "style-recalc", "duration": 1.1 }, - { "name": "composite", "duration": 16.2 }, // ← 즉시 - { "name": "paint", "duration": 1.3 } - ] -} -``` - ---- - -### 향후 예방 방법 - -#### 1. 전역 CSS 사용 가이드라인 - -```css -/* ❌ 피해야 할 패턴 */ -* { - transition: all 0.2s; /* 너무 광범위 */ -} - -/* ✅ 권장 패턴 1: 특정 속성만 */ -* { - transition: background-color 0.2s, color 0.2s; -} - -/* ✅ 권장 패턴 2: 클래스 기반 */ -.animated { - transition: all 0.2s; -} - -/* ✅ 권장 패턴 3: 서드파티 제외 */ -*:not([data-radix-popper-content-wrapper]) { - transition: all 0.2s; -} -``` - ---- - -#### 2. Radix UI 사용 시 체크리스트 - -```markdown -- [ ] 전역 transition이 Portal 컴포넌트에 영향을 주는가? -- [ ] transform 관련 CSS를 override하지 않았는가? -- [ ] position: fixed가 제대로 작동하는가? -- [ ] 부모 요소에 transform/perspective가 있는가? (stacking context 주의) -- [ ] Portal container를 커스터마이징했는가? -``` - ---- - -#### 3. 디버깅 도구 활용 - -```typescript -// 1. React DevTools로 Portal 확인 -// Portal 구조: -// body -// └─ [data-radix-portal] -// └─ [data-radix-popper-content-wrapper] -// └─ [data-radix-dropdown-menu-content] - -// 2. Chrome DevTools Layers -// Cmd+Shift+P → "Show Layers" -// → Composite Layer 확인 - -// 3. Performance Monitor -// Cmd+Shift+P → "Show Performance Monitor" -// → Layout/Paint/Composite 시간 측정 -``` - ---- - -### 최종 해결책 요약 - -**globals.css 수정 내용:** -```css -/* Line 238-249 */ - -/* 위치 계산은 유지, transition만 제거 */ -[data-radix-popper-content-wrapper] { - will-change: auto !important; - transition: none !important; /* ← 전역 transition 무효화 */ -} - -/* slide 애니메이션도 제거 */ -[data-radix-dropdown-menu-content], -[data-radix-select-content], -[data-radix-popover-content] { - animation-name: none !important; -} -``` - -**작동 원리:** -1. ✅ Radix UI의 `transform` 위치 계산 정상 작동 -2. ✅ 전역 `* { transition: all }`을 무효화 -3. ✅ 클릭한 버튼 바로 아래에서 즉시 나타남 -4. ✅ slide-in 애니메이션도 제거되어 깔끔 - -**결과:** -- ✅ 드롭다운/팝오버가 정확한 위치에 즉시 나타남 -- ✅ "날아오는" 효과 완전히 제거 -- ✅ 렌더링 성능 91% 개선 -- ✅ 네이티브 UX와 동일한 경험 - ---- - -## 🔗 관련 문서 - -- [Theme and Language Selector](./[IMPL-2025-11-07]%20theme-language-selector.md) -- [Login Page Implementation](./[IMPL-2025-11-07]%20jwt-cookie-authentication-final.md) -- [Dashboard Layout](./[IMPL-2025-11-11]%20dashboardlayout-centralization.md) - ---- - -## 📚 참고 자료 - -### Radix UI - -- [Radix UI Select](https://www.radix-ui.com/docs/primitives/components/select) -- [Radix UI GitHub - Scroll Lock Source](https://github.com/radix-ui/primitives/blob/main/packages/react/scroll-lock/src/ScrollLock.tsx) - -### CSS - -- [MDN: overflow](https://developer.mozilla.org/en-US/docs/Web/CSS/overflow) -- [MDN: CSS !important](https://developer.mozilla.org/en-US/docs/Web/CSS/important) - -### Web Performance - -- [Web.dev: CLS (Cumulative Layout Shift)](https://web.dev/cls/) -- [Web.dev: Optimize CLS](https://web.dev/optimize-cls/) - ---- - -## 📝 요약 - -**문제:** -- Shadcn UI Select 모달 열릴 때 레이아웃 시프트 발생 - -**원인:** -- Radix UI의 `overflow: hidden` + `margin-right` 보정 - -**해결:** -```css -body { - overflow: visible !important; -} - -body[data-scroll-locked] { - margin-right: 0 !important; -} -``` - -**결과:** -- ✅ 레이아웃 시프트 완전히 제거 -- ✅ 브라우저 네이티브 UX와 동일 -- ✅ 단 2줄의 CSS만으로 해결 -- ✅ 모든 브라우저에서 완벽 동작 -- ✅ CLS 0.00 달성 - ---- - -**작성일:** 2025-11-12 -**작성자:** Claude Code -**마지막 수정:** 2025-11-12 diff --git a/claudedocs/archive/[IMPL-2025-11-17] item-list-css-sync.md b/claudedocs/archive/[IMPL-2025-11-17] item-list-css-sync.md deleted file mode 100644 index d1907aea..00000000 --- a/claudedocs/archive/[IMPL-2025-11-17] item-list-css-sync.md +++ /dev/null @@ -1,280 +0,0 @@ -# CSS 비교 분석 - 품목 관리 리스트 페이지 - -**날짜**: 2025-11-17 -**React 파일**: `sma-react-v2.0/src/components/ItemManagement.tsx` (lines 1956-2200) -**Next.js 파일**: `sam-react-prod/src/components/items/ItemListClient.tsx` (lines 206-330) - ---- - -## 🔍 발견된 CSS 차이점 - -### 1. CardTitle (타이틀) -| 항목 | React | Next.js | 상태 | -|------|-------|---------|------| -| className | `text-sm md:text-base` | `text-base font-semibold` | ❌ MISMATCH | -| **수정 필요** | → `text-sm md:text-base` | | | - -### 2. TabsList (탭 리스트) -| 항목 | React | Next.js | 상태 | -|------|-------|---------|------| -| 래퍼 div | `overflow-x-auto -mx-2 px-2 mb-6` | 없음 | ❌ MISSING | -| className | `inline-flex w-auto min-w-full md:grid md:w-full md:max-w-2xl md:grid-cols-6` | `grid w-full grid-cols-6 mb-6` | ❌ MISMATCH | -| **수정 필요** | → 래퍼 추가 + React className 적용 | | | - -### 3. TabsTrigger (탭 버튼) -| 항목 | React | Next.js | 상태 | -|------|-------|---------|------| -| className | `whitespace-nowrap` | 없음 | ❌ MISSING | -| **수정 필요** | → `whitespace-nowrap` 추가 | | | - -### 4. TabsContent -| 항목 | React | Next.js | 상태 | -|------|-------|---------|------| -| className | `mt-0` | `mt-0` | ✅ MATCH | - -### 5. 테이블 래퍼 -| 항목 | React | Next.js | 상태 | -|------|-------|---------|------| -| className | `hidden lg:block rounded-md border` | `border rounded-lg overflow-hidden` | ❌ MISMATCH | -| **수정 필요** | → `hidden lg:block rounded-md border` | | | - ---- - -## 📋 테이블 구조 차이점 - -### **TableHeader - 컬럼 구조** - -#### React 컬럼 순서 (8개): -1. 체크박스 (`w-[50px]`) -2. **번호** (`hidden md:table-cell`) ⭐ -3. **품목코드** (`min-w-[100px]`) -4. **품목유형** (`min-w-[80px]`) -5. **품목명** (`min-w-[120px]`) -6. **규격** (`hidden md:table-cell`) -7. **단위** (`hidden md:table-cell`) -8. **작업** (`text-right min-w-[100px]`) - -#### Next.js 목표 컬럼 순서 (10개) - 개선안: -1. ❌ **체크박스** (`w-[50px]`) - 추가 필요 -2. ❌ **번호** (`hidden md:table-cell`) - 추가 필요 -3. **품목 코드** (`min-w-[100px]`) - width 수정 -4. **품목유형** (`min-w-[80px]`) - 위치 이동 -5. **품목명** (`min-w-[120px]`) - 위치 이동 -6. **규격** (`hidden md:table-cell`) - 위치 이동 -7. **단위** (`hidden md:table-cell`) - 위치 이동 -8. ~~**판매 단가**~~ - 🚨 **제거** -9. **품목 상태** (`w-[80px]`) - ✅ **유지** (컬럼명 변경: "상태" → "품목 상태") -10. **작업** (`text-right min-w-[100px]`) - 정렬 수정 - -### 🚨 주요 문제점 - -| # | 문제 | React | Next.js | 개선안 | -|---|------|-------|---------|---------| -| 1 | 체크박스 컬럼 | ✅ 있음 (`w-[50px]`) | ❌ 없음 | ✅ 추가 | -| 2 | 번호 컬럼 | ✅ 있음 (`hidden md:table-cell`) | ❌ 없음 | ✅ 추가 | -| 3 | 품목코드 width | `min-w-[100px]` | `w-[120px]` | ✅ `min-w-[100px]`로 수정 | -| 4 | 컬럼 순서 | 코드→유형→명→규격→단위 | 코드→명→유형→단위→규격 | ✅ React 순서로 변경 | -| 5 | 판매단가 | ❌ 없음 | ✅ 있음 | 🚨 **제거** | -| 6 | 품목 상태 | ❌ 없음 | ✅ 있음 ("상태") | ✅ **유지** (컬럼명: "품목 상태") | -| 7 | 작업 정렬 | `text-right` | `text-center` ❌ | ✅ `text-right`로 수정 | - ---- - -## 🎨 TableCell 상세 CSS 비교 - -### 번호 컬럼 (React만 있음) -```tsx -// React - - {filteredItems.length - (startIndex + index)} - - -// Next.js: 없음 (추가 필요) -``` - -### 품목코드 컬럼 -```tsx -// React - - - {formatItemCodeForAssembly(item) || '-'} - - - -// Next.js - - {item.itemCode} - -``` - -**차이점**: -- ❌ `cursor-pointer` 누락 -- ❌ `` 태그 없음 -- ❌ `text-xs bg-gray-100 px-2 py-1 rounded` 배경색 스타일 없음 - -### 품목유형 컬럼 -```tsx -// React - - {getItemTypeBadge(item.itemType)} - {/* + 부품인 경우 추가 뱃지 */} - - -// Next.js - - - {ITEM_TYPE_LABELS[item.itemType]} - - -``` - -**차이점**: -- ❌ `cursor-pointer` 누락 -- ❌ `getItemTypeBadge()` 함수 사용 안함 (색상 없음) -- ❌ 부품 타입별 추가 뱃지 없음 - -### 품목명 컬럼 -```tsx -// React - -
- {item.itemName} - {/* + 견적산출용 뱃지 */} -
-
- -// Next.js - - {item.itemName} - -``` - -**차이점**: -- ❌ `cursor-pointer` 누락 -- ❌ `flex items-center gap-2` 구조 없음 -- ❌ `truncate max-w-[150px] md:max-w-none` 말줄임 없음 -- ❌ 견적산출용 뱃지 없음 - -### 규격 컬럼 -```tsx -// React - - {item.itemCode?.includes('-') ? item.itemCode.split('-').slice(1).join('-') : (item.specification || "-")} - - -// Next.js -규격 - - {item.specification || '-'} - -``` - -**차이점**: -- ❌ `cursor-pointer` 누락 -- ❌ `hidden md:table-cell` 반응형 숨김 없음 -- ❌ `text-muted-foreground` → `text-gray-600` (다른 색상) -- ❌ itemCode 파싱 로직 없음 - -### 단위 컬럼 -```tsx -// React - - {item.unit || "-"} - - -// Next.js -단위 -{item.unit} -``` - -**차이점**: -- ❌ `cursor-pointer` 누락 -- ❌ `hidden md:table-cell` 반응형 숨김 없음 -- ❌ `` 없음 (단순 텍스트) - -### 작업 컬럼 -```tsx -// React -작업 - - handleViewChange("view", item)} - onEdit={() => handleViewChange("edit", item)} - onDelete={() => {...}} - /> - - -// Next.js -작업 - -
- - {/* ... */} -
-
-``` - -**차이점**: -- ❌ `text-right` → `text-center` (정렬 틀림) -- ❌ `min-w-[100px]` → `w-[150px]` -- ❌ `TableActionButtons` 컴포넌트 대신 직접 구현 -- ❌ 아이콘: `Search` → `Eye` (돋보기 → 눈) - ---- - -## 📝 수정 체크리스트 - -### 구조 변경 -- [ ] CardTitle: `text-sm md:text-base` 적용 -- [ ] TabsList 래퍼 div 추가: `overflow-x-auto -mx-2 px-2 mb-6` -- [ ] TabsList: `inline-flex w-auto min-w-full md:grid md:w-full md:max-w-2xl md:grid-cols-6` -- [ ] TabsTrigger: `whitespace-nowrap` 추가 -- [ ] 테이블 래퍼: `hidden lg:block rounded-md border` - -### 테이블 컬럼 재구성 -- [ ] 체크박스 컬럼 추가 (첫 번째, `w-[50px]`) -- [ ] 번호 컬럼 추가 (두 번째, `hidden md:table-cell`) -- [ ] 컬럼 순서 변경: 체크박스 → 번호 → 코드 → 유형 → 명 → 규격 → 단위 → 품목상태 → 작업 -- [ ] 판매단가 컬럼 제거 🚨 -- [ ] 상태 컬럼명 변경: "상태" → "품목 상태" ✅ (유지) -- [ ] 작업 컬럼 정렬: `text-center` → `text-right`, width: `w-[150px]` → `min-w-[100px]` - -### CSS 클래스 적용 -- [ ] 품목코드: `cursor-pointer` + `` 태그 + `text-xs bg-gray-100 px-2 py-1 rounded` -- [ ] 품목유형: `cursor-pointer` + `getItemTypeBadge()` 함수 사용 -- [ ] 품목명: `cursor-pointer` + `flex items-center gap-2` + `truncate max-w-[150px] md:max-w-none` -- [ ] 규격: `cursor-pointer hidden md:table-cell text-muted-foreground` + itemCode 파싱 로직 -- [ ] 단위: `cursor-pointer hidden md:table-cell` + `` -- [ ] 작업: `text-right` + `Search` 아이콘 - -### 기능 추가 -- [ ] `getItemTypeBadge()` 함수 구현 (유형별 색상) -- [ ] `formatItemCodeForAssembly()` 함수 구현 -- [ ] 체크박스 선택 기능 -- [ ] 견적산출용 뱃지 로직 -- [ ] 부품 타입별 추가 뱃지 - ---- - -## 🎯 우선순위 - -### 긴급 (시각적 영향 큼) -1. 번호 컬럼 추가 -2. 품목코드 배경색 (`bg-gray-100`) -3. 품목유형 색상 (Badge) -4. 컬럼 순서 변경 -5. 작업 정렬 수정 (`text-center` → `text-right`) - -### 중요 -6. 체크박스 컬럼 추가 -7. 판매단가 컬럼 제거 🚨 -8. 상태 컬럼명 변경: "상태" → "품목 상태" ✅ -9. 아이콘 변경 (Eye → Search) -10. TabsList 반응형 - -### 보통 -11. cursor-pointer 일괄 적용 -12. 견적산출용 뱃지 -13. 부품 타입 뱃지 \ No newline at end of file diff --git a/claudedocs/archive/[INDEX] DOCUMENTATION-MAP.md b/claudedocs/archive/[INDEX] DOCUMENTATION-MAP.md deleted file mode 100644 index 20bd814f..00000000 --- a/claudedocs/archive/[INDEX] DOCUMENTATION-MAP.md +++ /dev/null @@ -1,260 +0,0 @@ -# 📚 프로젝트 문서 구조 및 인덱스 - -> **프로젝트**: Next.js 15 + Laravel 하이브리드 아키텍처 -> **프론트엔드**: Next.js 15 App Router + React 19 -> **백엔드**: PHP Laravel -> **작성일**: 2025-11-17 -> **목적**: 프로젝트 문서 아카이브 및 빠른 참조 - ---- - -## 📖 문서 분류 체계 - -### 1. [GUIDE] - 개발 가이드 -프로젝트 개발 시 참고해야 할 표준 워크플로우 및 가이드 문서 - -### 2. [IMPL-YYYY-MM-DD] - 구현 기록 -특정 기능 구현 과정과 결과를 시간순으로 기록한 문서 - -### 3. [REF] - 참고 자료 -아키텍처 분석, 리서치 결과, API 요구사항 등 참고용 문서 - -### 4. [PLAN] - 미래 계획 -향후 구현 예정이거나 검토 중인 기능에 대한 계획 문서 - -### 5. [LEGACY] - 레거시 문서 -과거 설계안이나 폐기된 접근 방법을 기록한 문서 - ---- - -## 📂 [GUIDE] 개발 가이드 (4개) - -### CSS 및 마이그레이션 -| 파일명 | 목적 | 주요 내용 | -|--------|------|-----------| -| `[GUIDE] CSS-MIGRATION-WORKFLOW.md` | React → Next.js CSS 마이그레이션 표준 프로세스 | 페이지별 CSS 비교/동기화 워크플로우, 체크리스트 기반 구현 | -| `[GUIDE] LARGE-FILE-WORKFLOW.md` | 대용량 파일(>1000줄) 작업 프로토콜 | 섹션별 분해 전략, 체계적 마이그레이션 방법론 | - -### 시스템 설계 -| 파일명 | 목적 | 주요 내용 | -|--------|------|-----------| -| `[GUIDE] ITEM-MANAGEMENT-MIGRATION.md` | 품목관리 시스템 마이그레이션 종합 가이드 | 하이브리드 아키텍처, 데이터 구조, API 연동 전략 | - -### 기술 문제 해결 -| 파일명 | 목적 | 주요 내용 | -|--------|------|-----------| -| `[GUIDE] ZOD-VALIDATION-TROUBLESHOOTING.md` | Zod 검증 라이브러리 문제 해결 | 영어 에러 메시지 문제, z.preprocess 패턴, 필수 필드 처리 | - ---- - -## 🛠️ [IMPL] 구현 기록 (25개) - -### 2025-11-06 (1개) -| 파일명 | 구현 내용 | -|--------|-----------| -| `[IMPL-2025-11-06] i18n-usage-guide.md` | 다국어(i18n) 시스템 구현 | - -### 2025-11-07 (7개) -| 파일명 | 구현 내용 | -|--------|-----------| -| `[IMPL-2025-11-07] api-key-management.md` | API 키 관리 시스템 | -| `[IMPL-2025-11-07] auth-guard-usage.md` | 인증 가드 사용 방법 | -| `[IMPL-2025-11-07] authentication-implementation-guide.md` | 인증 시스템 구현 가이드 | -| `[IMPL-2025-11-07] form-validation-guide.md` | 폼 검증 시스템 | -| `[IMPL-2025-11-07] jwt-cookie-authentication-final.md` | JWT 쿠키 인증 최종 구현 | -| `[IMPL-2025-11-07] middleware-issue-resolution.md` | 미들웨어 이슈 해결 | -| `[IMPL-2025-11-07] route-protection-architecture.md` | 라우트 보호 아키텍처 | -| `[IMPL-2025-11-07] seo-bot-blocking-configuration.md` | SEO 봇 차단 설정 | - -### 2025-11-10 (2개) -| 파일명 | 구현 내용 | -|--------|-----------| -| `[IMPL-2025-11-10] dashboard-integration-complete.md` | 대시보드 통합 완료 | -| `[IMPL-2025-11-10] token-management-guide.md` | 토큰 관리 시스템 | - -### 2025-11-11 (5개) -| 파일명 | 구현 내용 | -|--------|-----------| -| `[IMPL-2025-11-11] api-route-type-safety.md` | API 라우트 타입 안전성 | -| `[IMPL-2025-11-11] chart-warning-fix.md` | 차트 경고 수정 | -| `[IMPL-2025-11-11] dashboard-cleanup-summary.md` | 대시보드 정리 요약 | -| `[IMPL-2025-11-11] error-pages-configuration.md` | 에러 페이지 설정 | -| `[IMPL-2025-11-11] sidebar-active-menu-sync.md` | 사이드바 활성 메뉴 동기화 | - -### 2025-11-12 (1개) -| 파일명 | 구현 내용 | -|--------|-----------| -| `[IMPL-2025-11-12] modal-select-layout-shift-fix.md` | 모달 Select 레이아웃 시프트 수정 | - -### 2025-11-13 (3개) -| 파일명 | 구현 내용 | -|--------|-----------| -| `[IMPL-2025-11-13] browser-support-policy.md` | 브라우저 지원 정책 | -| `[IMPL-2025-11-13] safari-cookie-compatibility.md` | Safari 쿠키 호환성 | -| `[IMPL-2025-11-13] sidebar-scroll-improvements.md` | 사이드바 스크롤 개선 | - -### 2025-11-17 (1개) -| 파일명 | 구현 내용 | -|--------|-----------| -| `[IMPL-2025-11-17] item-list-css-sync.md` | 품목 리스트 CSS 동기화 | - ---- - -## 📋 [REF] 참고 자료 (14개) - -### 프로젝트 컨텍스트 -| 파일명 | 내용 | -|--------|------| -| `[REF] project-context.md` | 프로젝트 전체 컨텍스트 및 아키텍처 개요 | -| `[REF] architecture-integration-risks.md` | 아키텍처 통합 리스크 분석 | -| `[REF] code-quality-report.md` | 코드 품질 리포트 | -| `[REF] communication_improvement_guide.md` | 커뮤니케이션 개선 가이드 | - -### API 및 백엔드 -| 파일명 | 내용 | -|--------|------| -| `[REF] api-requirements.md` | API 요구사항 (일반) | -| `[REF] api-requirements-items.md` | 품목관리 API 요구사항 | -| `[REF] api-analysis.md` | API 분석 | - -### 인증 및 보안 리서치 -| 파일명 | 내용 | -|--------|------| -| `[REF] nextjs15-middleware-authentication-research.md` | Next.js 15 미들웨어 인증 리서치 | -| `[REF] token-security-nextjs15-research.md` | 토큰 보안 리서치 | - -### 마이그레이션 및 세션 관리 -| 파일명 | 내용 | -|--------|------| -| `[REF] dashboard-migration-summary.md` | 대시보드 마이그레이션 요약 | -| `[REF] session-migration-backend.md` | 세션 마이그레이션 (백엔드) | -| `[REF] session-migration-frontend.md` | 세션 마이그레이션 (프론트엔드) | -| `[REF] session-migration-summary.md` | 세션 마이그레이션 요약 | - -### 컴포넌트 및 배포 -| 파일명 | 내용 | -|--------|------| -| `[REF] component-usage-analysis.md` | 컴포넌트 사용 분석 | -| `[REF] nextjs-error-handling-guide.md` | Next.js 에러 핸들링 가이드 | -| `[REF] production-deployment-checklist.md` | 프로덕션 배포 체크리스트 | - ---- - -## 🚀 [PLAN] 미래 계획 (1개) - -| 파일명 | 계획 내용 | -|--------|-----------| -| `[PLAN] httponly-cookie-implementation.md` | HttpOnly 쿠키 구현 계획 | - ---- - -## 📜 [LEGACY] 레거시 문서 (1개) - -| 파일명 | 내용 | -|--------|------| -| `[LEGACY] authentication-design.md` | 초기 인증 시스템 설계안 (폐기) | - ---- - -## 🔍 빠른 검색 가이드 - -### 상황별 문서 찾기 - -#### 1. React → Next.js 마이그레이션 작업 시 -``` -[GUIDE] CSS-MIGRATION-WORKFLOW.md # CSS 마이그레이션 표준 프로세스 -[GUIDE] LARGE-FILE-WORKFLOW.md # 대용량 파일 작업 방법 -[GUIDE] ITEM-MANAGEMENT-MIGRATION.md # 품목관리 시스템 전체 설계 -``` - -#### 2. 품목관리 기능 개발 시 -``` -[REF] api-requirements-items.md # 백엔드 API 요구사항 -[GUIDE] ITEM-MANAGEMENT-MIGRATION.md # 시스템 아키텍처 및 데이터 구조 -[IMPL-2025-11-17] item-list-css-sync.md # 품목 리스트 CSS 동기화 구현 -``` - -#### 3. 인증/보안 관련 작업 시 -``` -[IMPL-2025-11-07] jwt-cookie-authentication-final.md # JWT 쿠키 인증 구현 -[IMPL-2025-11-07] route-protection-architecture.md # 라우트 보호 -[REF] token-security-nextjs15-research.md # 토큰 보안 리서치 -``` - -#### 4. 폼 검증 문제 해결 시 -``` -[GUIDE] ZOD-VALIDATION-TROUBLESHOOTING.md # Zod 검증 문제 해결 -[IMPL-2025-11-07] form-validation-guide.md # 폼 검증 구현 가이드 -``` - -#### 5. UI/UX 이슈 해결 시 -``` -[IMPL-2025-11-12] modal-select-layout-shift-fix.md # 모달 레이아웃 시프트 -[IMPL-2025-11-13] safari-cookie-compatibility.md # Safari 호환성 -[IMPL-2025-11-13] sidebar-scroll-improvements.md # 사이드바 스크롤 -``` - -#### 6. 배포 준비 시 -``` -[REF] production-deployment-checklist.md # 배포 체크리스트 -[IMPL-2025-11-13] browser-support-policy.md # 브라우저 지원 정책 -[REF] code-quality-report.md # 코드 품질 리포트 -``` - ---- - -## 📊 문서 통계 - -| 카테고리 | 문서 수 | 비율 | -|----------|---------|------| -| [GUIDE] | 4 | 8.7% | -| [IMPL] | 25 | 54.3% | -| [REF] | 14 | 30.4% | -| [PLAN] | 1 | 2.2% | -| [LEGACY] | 1 | 2.2% | -| [INDEX] | 1 | 2.2% | -| **합계** | **46** | **100%** | - ---- - -## 🎯 문서 작성 원칙 - -### 1. 명명 규칙 -- **[GUIDE]**: 대문자, 하이픈으로 단어 구분 -- **[IMPL-YYYY-MM-DD]**: 구현 날짜 포함, 소문자, 하이픈 구분 -- **[REF]**: 소문자, 하이픈 구분 - -### 2. 문서 구조 -- 명확한 목차 -- 코드 예제 포함 -- 실행 가능한 명령어 -- 트러블슈팅 섹션 - -### 3. 유지보수 -- 구현 완료 시 즉시 [IMPL] 문서 작성 -- 워크플로우 개선 시 [GUIDE] 업데이트 -- 레거시 문서는 [LEGACY]로 이동, 삭제 금지 - ---- - -## 📝 문서 업데이트 이력 - -| 날짜 | 변경 내용 | -|------|-----------| -| 2025-11-17 | 초기 인덱스 문서 작성 | -| 2025-11-17 | 모든 문서 명명 규칙 통일 | - ---- - -## 🔗 관련 리소스 - -- **프로젝트 루트**: `/Users/byeongcheolryu/codebridgex/sam_project/sam-next/sma-next-project/sam-react-prod` -- **문서 디렉토리**: `claudedocs/` -- **React 소스**: `sma-react-v2.0/` -- **Next.js 소스**: `src/` - ---- - -**마지막 업데이트**: 2025-11-17 -**문서 버전**: 1.0.0 -**관리자**: Claude + Development Team \ No newline at end of file diff --git a/claudedocs/archive/[LEGACY] 00_INDEX.md b/claudedocs/archive/[LEGACY] 00_INDEX.md deleted file mode 100644 index 4bf9f559..00000000 --- a/claudedocs/archive/[LEGACY] 00_INDEX.md +++ /dev/null @@ -1,532 +0,0 @@ -# 프로젝트 문서 인덱스 (구현 순서 기반) - -> 이 문서는 실제 프로젝트 구현 순서에 따라 문서들을 정리한 인덱스입니다. - -## 📂 문서 분류 - -### ✅ 구현 완료 (Implementation Completed) -실제 코드로 구현되어 프로젝트에 적용된 기능 - -### 📋 참고 자료 (Reference) -기획/조사 단계의 문서, 또는 향후 구현 참고용 자료 - -### 🚧 진행 중 (In Progress) -일부 구현되었으나 완료되지 않은 기능 - ---- - -## 🎯 구현 순서별 문서 목록 - -### Phase 1: 프로젝트 초기 설정 - -#### ✅ 1. 다국어 지원 (i18n) -**파일**: `i18n-usage-guide.md` -**상태**: ✅ 구현 완료 -**구현 내용**: -- next-intl 라이브러리 설정 -- 한국어(ko), 영어(en), 일본어(ja) 3개 언어 지원 -- `/src/i18n/config.ts` - 언어 설정 -- `/src/i18n/request.ts` - 메시지 로딩 -- `/src/messages/{locale}.json` - 번역 파일 -- Middleware에서 로케일 자동 감지 - -**관련 파일**: -``` -src/i18n/config.ts -src/i18n/request.ts -src/messages/ko.json, en.json, ja.json -src/middleware.ts (i18n 부분) -``` - ---- - -### Phase 2: 보안 및 Bot 차단 - -#### ✅ 2. SEO Bot 차단 설정 -**파일**: `seo-bot-blocking-configuration.md` -**상태**: ✅ 구현 완료 -**구현 내용**: -- Middleware에서 bot user-agent 감지 -- 보호된 경로에 대한 bot 접근 차단 -- 로봇 차단 헤더 추가 (`X-Robots-Tag`) - -**관련 파일**: -``` -src/middleware.ts (BOT_PATTERNS, isBot 함수) -``` - ---- - -### Phase 3: 인증 시스템 - -#### ✅ 3. API 분석 및 인증 방식 결정 -**파일**: `api-analysis.md` ➜ `api-requirements.md` -**상태**: 📋 참고 자료 -**목적**: -- Laravel API 엔드포인트 분석 -- 인증 방식 비교 (Bearer Token vs Session Cookie) -- 최종 결정: **Bearer Token (JWT) + Cookie 저장 방식** - ---- - -#### ✅ 4. 인증 시스템 설계 -**파일**: `authentication-design.md` -**상태**: 📋 참고 자료 (초기 Sanctum 설계) -**목적**: Sanctum 세션 쿠키 방식 설계 (레거시) - -**파일**: `jwt-cookie-authentication-final.md` -**상태**: ✅ 구현 완료 (최종 설계) -**구현 내용**: -- JWT Token을 쿠키에 저장 -- Middleware에서 `user_token` 쿠키 확인 -- 3가지 인증 방식 지원: Bearer Token/Sanctum/API-Key - -**관련 파일**: -``` -src/lib/api/auth/types.ts -src/lib/api/auth/auth-config.ts -src/lib/api/client.ts -src/middleware.ts (인증 체크 로직) -``` - ---- - -#### ✅ 5. 인증 구현 가이드 -**파일**: `authentication-implementation-guide.md` -**상태**: ✅ 구현 완료 -**구현 내용**: -- 3가지 인증 방식 통합 (Bearer/Sanctum/API-Key) -- API Client 구현 -- Route 보호 메커니즘 - -**관련 파일**: -``` -src/lib/api/auth/* -src/app/api/auth/* (로그인/로그아웃 API 라우트) -``` - ---- - -#### ✅ 6. API Key 관리 -**파일**: `api-key-management.md` -**상태**: ✅ 구현 완료 -**구현 내용**: -- 환경 변수를 통한 API Key 관리 -- `.env.local`에 `API_KEY` 저장 -- API 요청 시 자동으로 헤더에 추가 - -**관련 파일**: -``` -.env.local (API_KEY) -src/lib/api/client.ts -``` - ---- - -#### ✅ 7. Middleware 인증 문제 해결 -**파일**: `middleware-issue-resolution.md` -**상태**: ✅ 해결 완료 -**문제**: 로그인하지 않아도 `/dashboard` 접근 가능 -**원인**: `isPublicRoute()` 함수 버그 - `'/'`가 모든 경로와 매칭됨 -**해결**: -- `'/'` 경로는 정확히 일치할 때만 public -- 기타 경로는 `startsWith(route + '/')` 방식 -- Next.js 15 + next-intl 호환성 설정 (`turbopack: {}`) - -**관련 파일**: -``` -src/middleware.ts (isPublicRoute 함수) -next.config.ts (turbopack 설정) -``` - ---- - -### Phase 4: 라우팅 및 보호 - -#### ✅ 8. Route 보호 아키텍처 -**파일**: `route-protection-architecture.md` -**상태**: ✅ 구현 완료 -**구현 내용**: -- Protected Routes: `/dashboard`, `/admin`, etc. -- Guest-only Routes: `/login`, `/register` -- Public Routes: `/`, `/about`, `/contact` -- Middleware에서 라우트 타입별 처리 - -**관련 파일**: -``` -src/lib/api/auth/auth-config.ts (라우트 설정) -src/middleware.ts (라우트 보호 로직) -``` - ---- - -#### ✅ 9. Auth Guard 사용법 -**파일**: `auth-guard-usage.md` -**상태**: 🚧 부분 구현 -**구현 내용**: -- Hook 기반: `useAuthGuard()` 훅 -- Layout 기반: `(protected)` 폴더 - -**관련 파일**: -``` -src/hooks/useAuthGuard.ts -src/app/[locale]/(protected)/layout.tsx -``` - ---- - -### Phase 5: UI 및 폼 검증 - -#### ✅ 10. 폼 Validation -**파일**: `form-validation-guide.md` -**상태**: ✅ 구현 완료 -**구현 내용**: -- react-hook-form + zod 조합 -- 로그인/회원가입 폼 검증 - -**관련 파일**: -``` -src/lib/validations/auth.ts -src/components/auth/LoginPage.tsx -src/components/auth/SignupPage.tsx -``` - ---- - -#### ✅ 11. 테마 선택 및 언어 선택 -**상태**: ✅ 구현 완료 -**구현 내용**: -- 다크모드/라이트모드 전환 -- 테마 Context 관리 -- 언어 선택 컴포넌트 - -**관련 파일**: -``` -src/contexts/ThemeContext.tsx -src/components/ThemeSelect.tsx -src/components/LanguageSelect.tsx -``` - ---- - -### Phase 6: 대시보드 시스템 - -#### ✅ 12. Dashboard 마이그레이션 및 통합 -**파일**: `[IMPL-2025-11-10] dashboard-integration-complete.md` -**상태**: ✅ 구현 완료 (2025-11-10) -**구현 내용**: -- Vite React → Next.js 마이그레이션 -- 역할 기반 대시보드 시스템 (CEO, ProductionManager, Worker, SystemAdmin, Sales) -- Lazy loading으로 성능 최적화 -- localStorage 기반 역할 관리 - -**관련 파일**: -``` -src/components/business/Dashboard.tsx -src/components/business/CEODashboard.tsx -src/components/business/ProductionManagerDashboard.tsx -src/components/business/WorkerDashboard.tsx -src/components/business/SystemAdminDashboard.tsx -src/layouts/DashboardLayout.tsx -``` - ---- - -#### ✅ 13. Dashboard Layout 정리 -**파일**: `[IMPL-2025-11-11] dashboard-cleanup-summary.md` -**상태**: ✅ 구현 완료 (2025-11-11) -**구현 내용**: -- 테스트용 역할 선택 셀렉트 제거 -- 간단한 로그아웃 버튼으로 교체 -- UI 단순화 및 사용자 혼란 방지 - -**관련 파일**: -``` -src/layouts/DashboardLayout.tsx -``` - ---- - -#### ✅ 14. 차트 렌더링 경고 수정 -**파일**: `[IMPL-2025-11-11] chart-warning-fix.md` -**상태**: ✅ 구현 완료 (2025-11-11) -**구현 내용**: -- recharts ResponsiveContainer 높이 명시적 설정 -- "width(-1) and height(-1)" 경고 해결 -- 차트 즉시 렌더링 개선 - -**관련 파일**: -``` -src/components/business/CEODashboard.tsx -``` - ---- - -#### ✅ 15. Token 관리 가이드 -**파일**: `[IMPL-2025-11-10] token-management-guide.md` -**상태**: ✅ 구현 완료 (2025-11-10) -**구현 내용**: -- JWT Token 저장 및 관리 방식 -- HttpOnly Cookie 사용 -- Token 갱신 로직 - -**관련 파일**: -``` -src/app/api/auth/login/route.ts -src/app/api/auth/check/route.ts -src/middleware.ts -``` - ---- - -### Phase 7: UI/UX 개선 - -#### ✅ 16. Sidebar 활성 메뉴 동기화 -**파일**: `[IMPL-2025-11-11] sidebar-active-menu-sync.md` -**상태**: ✅ 구현 완료 (2025-11-11) -**구현 내용**: -- URL 기반 활성 메뉴 자동 감지 -- 서브메뉴 우선 매칭 로직 -- 메뉴 탐색 알고리즘 개선 - -**관련 파일**: -``` -src/layouts/DashboardLayout.tsx -``` - ---- - -#### ✅ 17. Sidebar 스크롤 개선 -**파일**: `[IMPL-2025-11-13] sidebar-scroll-improvements.md` -**상태**: ✅ 구현 완료 (2025-11-13) -**구현 내용**: -- 활성 메뉴 자동 스크롤 기능 -- 호버 시에만 스크롤바 표시 -- 부드러운 스크롤 애니메이션 - -**관련 파일**: -``` -src/components/layout/Sidebar.tsx -src/app/globals.css (sidebar-scroll 스타일) -``` - ---- - -#### ✅ 18. 모달 Select 레이아웃 시프트 방지 -**파일**: `[IMPL-2025-11-12] modal-select-layout-shift-fix.md` -**상태**: ✅ 구현 완료 (2025-11-12) -**구현 내용**: -- Shadcn UI Select 컴포넌트 레이아웃 시프트 방지 -- 포털 사용으로 모달 내 Select 안정화 - ---- - -#### ✅ 19. 에러 페이지 설정 -**파일**: `[IMPL-2025-11-11] error-pages-configuration.md` -**상태**: ✅ 구현 완료 (2025-11-11) -**구현 내용**: -- Next.js 15 App Router 에러 처리 -- error.tsx, not-found.tsx 구성 -- 다국어 지원 에러 메시지 - -**관련 파일**: -``` -src/app/[locale]/error.tsx -src/app/[locale]/not-found.tsx -src/app/[locale]/(protected)/error.tsx -``` - ---- - -### Phase 8: 브라우저 호환성 - -#### ✅ 20. Safari 쿠키 호환성 -**파일**: `[IMPL-2025-11-13] safari-cookie-compatibility.md` -**상태**: ✅ 구현 완료 (2025-11-13) -**구현 내용**: -- SameSite=Strict → SameSite=Lax 변경 -- 개발 환경에서 Secure 속성 제외 (Safari 호환) -- 쿠키 설정/삭제 시 동일한 속성 사용 - -**관련 파일**: -``` -src/app/api/auth/login/route.ts -src/app/api/auth/logout/route.ts -src/app/api/auth/check/route.ts -``` - ---- - -#### ✅ 21. 브라우저 지원 정책 -**파일**: `[IMPL-2025-11-13] browser-support-policy.md` -**상태**: ✅ 구현 완료 (2025-11-13) -**구현 내용**: -- Internet Explorer 차단 -- 안내 페이지 제공 (unsupported-browser.html) -- Middleware에서 IE User-Agent 감지 - -**관련 파일**: -``` -src/middleware.ts (isInternetExplorer 함수) -public/unsupported-browser.html -``` - ---- - -### Phase 9: 타입 안전성 - -#### ✅ 22. API 라우트 타입 안전성 -**파일**: `[IMPL-2025-11-11] api-route-type-safety.md` -**상태**: ✅ 구현 완료 (2025-11-11) -**구현 내용**: -- TypeScript 인터페이스 정의 -- API 응답 타입 검증 -- 타입 안전한 에러 처리 - -**관련 파일**: -``` -src/app/api/auth/*/route.ts -``` - ---- - -### Phase 10: 참고 자료 및 가이드 - -#### 📋 23. Next.js 에러 핸들링 가이드 -**파일**: `[REF] nextjs-error-handling-guide.md` -**상태**: 📋 참고 자료 -**목적**: Next.js 15 App Router 에러 처리 종합 가이드 - ---- - -#### 📋 24. 컴포넌트 사용 분석 -**파일**: `[REF-2025-11-12] component-usage-analysis.md` -**상태**: 📋 참고 자료 -**목적**: 프로젝트 내 컴포넌트 사용 현황 분석 - ---- - -#### 📋 25. 세션 마이그레이션 가이드 -**파일**: -- `[REF-2025-11-12] session-migration-backend.md` -- `[REF-2025-11-12] session-migration-frontend.md` -- `[REF-2025-11-12] session-migration-summary.md` - -**상태**: 📋 참고 자료 (미구현) -**목적**: JWT → 세션 기반 인증 전환 가이드 - ---- - -#### 📋 26. Dashboard 마이그레이션 요약 -**파일**: `[REF-2025-11-10] dashboard-migration-summary.md` -**상태**: 📋 참고 자료 -**목적**: Vite React → Next.js 마이그레이션 과정 기록 - ---- - -#### 📋 27. Production 배포 체크리스트 -**파일**: `[REF] production-deployment-checklist.md` -**상태**: 📋 참고 자료 -**목적**: 배포 전 확인 사항 체크리스트 - ---- - -#### 📋 28. 코드 품질 리포트 -**파일**: `[REF] code-quality-report.md` -**상태**: 📋 참고 자료 -**목적**: 코드 품질 분석 결과 - ---- - -#### 📋 29. 아키텍처 통합 리스크 -**파일**: `[REF] architecture-integration-risks.md` -**상태**: 📋 참고 자료 -**목적**: 인증/i18n/bot 차단 통합 시 리스크 분석 - ---- - -### Phase 11: 보안 연구 및 개선 - -#### 📋 30. Token 보안 연구 (Next.js 15) -**파일**: `[REF-2025-11-07] research_token_security_nextjs15.md` -**상태**: 📋 참고 자료 -**목적**: JWT Token 보안 연구 - ---- - -#### 📋 31. Middleware 인증 연구 -**파일**: `[REF-2025-11-07] research_nextjs15_middleware_authentication.md` -**상태**: 📋 참고 자료 -**목적**: Next.js 15 Middleware 인증 방식 조사 - ---- - -#### 📋 32. HttpOnly Cookie 구현 -**파일**: `[REF-Future] httponly-cookie-implementation.md` -**상태**: 📋 참고 자료 (미구현) -**목적**: HttpOnly Cookie 방식 설계 (보안 강화 옵션) - ---- - -#### 📋 33. 커뮤니케이션 개선 가이드 -**파일**: `[REF] communication_improvement_guide.md` -**상태**: 📋 참고 자료 -**목적**: 프로젝트 커뮤니케이션 개선 방안 - ---- - -#### 📋 34. 프로젝트 컨텍스트 -**파일**: `[REF] project-context.md` -**상태**: 📋 참고 자료 -**목적**: 프로젝트 전체 개요 및 빠른 시작 가이드 - ---- - -## 🔍 빠른 검색 - -### 주제별 문서 찾기 - -| 주제 | 문서 | -|------|------| -| **프로젝트 개요** | `[REF] project-context.md` | -| **다국어** | `[IMPL-2025-11-06] i18n-usage-guide.md` | -| **인증 설계** | `[IMPL-2025-11-07] jwt-cookie-authentication-final.md` | -| **인증 구현** | `[IMPL-2025-11-07] authentication-implementation-guide.md` | -| **Bot 차단** | `[IMPL-2025-11-07] seo-bot-blocking-configuration.md` | -| **Route 보호** | `[IMPL-2025-11-07] route-protection-architecture.md` | -| **Middleware** | `[IMPL-2025-11-07] middleware-issue-resolution.md` | -| **폼 검증** | `[IMPL-2025-11-07] form-validation-guide.md` | -| **API 분석** | `[REF] api-analysis.md`, `[REF] api-requirements.md` | -| **Dashboard** | `[IMPL-2025-11-10] dashboard-integration-complete.md` | -| **Sidebar** | `[IMPL-2025-11-13] sidebar-scroll-improvements.md` | -| **Safari 호환성** | `[IMPL-2025-11-13] safari-cookie-compatibility.md` | -| **IE 차단** | `[IMPL-2025-11-13] browser-support-policy.md` | -| **에러 처리** | `[REF] nextjs-error-handling-guide.md` | -| **세션 마이그레이션** | `[REF-2025-11-12] session-migration-summary.md` | -| **배포** | `[REF] production-deployment-checklist.md` | - ---- - -## 📝 업데이트 이력 - -| 날짜 | 변경 내용 | -|------|----------| -| 2025-11-13 | Phase 6-11 추가 (대시보드, UI/UX, 브라우저 호환성, 타입 안전성, 참고 자료) | -| 2025-11-10 | 인덱스 파일 생성, 구현 순서 기반 분류 | - ---- - -## 📊 문서 통계 - -- **총 문서 수**: 38개 -- **구현 완료 (IMPL)**: 21개 -- **참고 자료 (REF)**: 16개 -- **부분 구현 (PARTIAL)**: 1개 - ---- - -## 💡 사용 가이드 - -1. **새 세션 시작 시**: `project-context.md` 먼저 읽기 -2. **특정 기능 작업 시**: 위 인덱스에서 관련 문서 찾기 -3. **새 기능 추가 시**: 이 인덱스에 문서 추가 및 상태 업데이트 diff --git a/claudedocs/archive/[LEGACY] authentication-design.md b/claudedocs/archive/[LEGACY] authentication-design.md deleted file mode 100644 index 5190257e..00000000 --- a/claudedocs/archive/[LEGACY] authentication-design.md +++ /dev/null @@ -1,931 +0,0 @@ -# 인증 시스템 설계 (Laravel Sanctum + Next.js 15) - -## 📋 아키텍처 개요 - -### 전체 구조 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Next.js Frontend │ -├─────────────────────────────────────────────────────────────┤ -│ Middleware (Server) │ -│ ├─ Bot Detection (기존) │ -│ ├─ Authentication Check (신규) │ -│ │ ├─ Protected Routes 가드 │ -│ │ ├─ 세션 쿠키 확인 │ -│ │ └─ 인증 실패 → /login 리다이렉트 │ -│ └─ i18n Routing (기존) │ -├─────────────────────────────────────────────────────────────┤ -│ API Client (lib/auth/sanctum.ts) │ -│ ├─ CSRF 토큰 자동 처리 │ -│ ├─ HTTP-only 쿠키 포함 (credentials: 'include') │ -│ ├─ 에러 인터셉터 (401 → /login) │ -│ └─ 재시도 로직 │ -├─────────────────────────────────────────────────────────────┤ -│ Server Auth Utils (lib/auth/server-auth.ts) │ -│ ├─ getServerSession() - Server Components용 │ -│ └─ 쿠키 기반 세션 검증 │ -├─────────────────────────────────────────────────────────────┤ -│ Auth Context (contexts/AuthContext.tsx) │ -│ ├─ 클라이언트 사이드 상태 관리 │ -│ ├─ 사용자 정보 캐싱 │ -│ └─ login/logout/register 함수 │ -└─────────────────────────────────────────────────────────────┘ - ↓ HTTP + Cookies -┌─────────────────────────────────────────────────────────────┐ -│ Laravel Backend (PHP) │ -├─────────────────────────────────────────────────────────────┤ -│ Sanctum Middleware │ -│ └─ 세션 기반 SPA 인증 (HTTP-only 쿠키) │ -├─────────────────────────────────────────────────────────────┤ -│ API Endpoints │ -│ ├─ GET /sanctum/csrf-cookie (CSRF 토큰 발급) │ -│ ├─ POST /api/login (로그인) │ -│ ├─ POST /api/register (회원가입) │ -│ ├─ POST /api/logout (로그아웃) │ -│ ├─ GET /api/user (현재 사용자 정보) │ -│ └─ POST /api/forgot-password (비밀번호 재설정) │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 핵심 설계 원칙 - -1. **가드 컴포넌트 없이 Middleware로 일괄 처리** - - 모든 인증 체크를 middleware.ts에서 처리 - - 라우트별로 가드 컴포넌트 불필요 - - 중복 코드 제거 - -2. **세션 기반 인증 (Sanctum SPA 모드)** - - HTTP-only 쿠키로 세션 관리 - - XSS 공격 방어 - - CSRF 토큰으로 보안 강화 - -3. **Server Components 우선** - - 서버에서 인증 체크 및 데이터 fetch - - 클라이언트 JS 번들 크기 감소 - - SEO 최적화 - -## 🔐 인증 플로우 - -### 1. 로그인 플로우 - -``` -┌─────────┐ 1. /login 접속 ┌──────────────┐ -│ Browser │ ───────────────────────────→│ Next.js │ -└─────────┘ │ Server │ - ↓ └──────────────┘ - │ 2. CSRF 토큰 요청 - │ GET /sanctum/csrf-cookie - ↓ -┌─────────┐ ┌──────────────┐ -│ Browser │ ←───────────────────────────│ Laravel │ -└─────────┘ XSRF-TOKEN 쿠키 │ Backend │ - ↓ └──────────────┘ - │ 3. 로그인 폼 제출 - │ POST /api/login - │ { email, password } - │ Headers: X-XSRF-TOKEN - ↓ -┌─────────┐ ┌──────────────┐ -│ Browser │ ←───────────────────────────│ Laravel │ -└─────────┘ laravel_session 쿠키 │ Sanctum │ - ↓ (HTTP-only) └──────────────┘ - │ 4. 보호된 페이지 접근 - │ GET /dashboard - │ Cookies: laravel_session - ↓ -┌─────────┐ ┌──────────────┐ -│ Browser │ ←───────────────────────────│ Next.js │ -└─────────┘ 페이지 렌더링 │ Middleware │ - (쿠키 확인 ✓) └──────────────┘ -``` - -### 2. 보호된 페이지 접근 플로우 - -``` -사용자 → /dashboard 접속 - ↓ - Middleware 실행 - ↓ - ┌─────────────────┐ - │ 세션 쿠키 확인? │ - └─────────────────┘ - ↓ - Yes ↓ No ↓ - ↓ ↓ - 페이지 렌더링 Redirect - (Server /login?redirect=/dashboard - Component) -``` - -### 3. 미들웨어 체크 순서 - -``` -Request - ↓ -1. Bot Detection Check - ├─ Bot → 403 Forbidden - └─ Human → Continue - ↓ -2. Static Files Check - ├─ Static → Skip Auth - └─ Dynamic → Continue - ↓ -3. Public Routes Check - ├─ Public → Skip Auth - └─ Protected → Continue - ↓ -4. Session Cookie Check - ├─ Valid Session → Continue - └─ No Session → Redirect /login - ↓ -5. Guest Only Routes Check - ├─ Authenticated + /login → Redirect /dashboard - └─ Continue - ↓ -6. i18n Routing - ↓ -Response -``` - -## 📁 파일 구조 - -``` -/src -├─ /lib -│ └─ /auth -│ ├─ sanctum.ts # Sanctum API 클라이언트 -│ ├─ auth-config.ts # 인증 설정 (routes, URLs) -│ └─ server-auth.ts # 서버 컴포넌트용 유틸 -│ -├─ /contexts -│ └─ AuthContext.tsx # 클라이언트 인증 상태 관리 -│ -├─ /app/[locale] -│ ├─ /(auth) # 인증 관련 라우트 그룹 -│ │ ├─ /login -│ │ │ └─ page.tsx # 로그인 페이지 -│ │ ├─ /register -│ │ │ └─ page.tsx # 회원가입 페이지 -│ │ └─ /forgot-password -│ │ └─ page.tsx # 비밀번호 재설정 -│ │ -│ ├─ /(protected) # 보호된 라우트 그룹 -│ │ ├─ /dashboard -│ │ │ └─ page.tsx -│ │ ├─ /profile -│ │ │ └─ page.tsx -│ │ └─ /settings -│ │ └─ page.tsx -│ │ -│ └─ layout.tsx # AuthProvider 추가 -│ -├─ /middleware.ts # 통합 미들웨어 -│ -└─ /.env.local # 환경 변수 -``` - -## 🛠️ 핵심 구현 포인트 - -### 1. 인증 설정 (lib/auth/auth-config.ts) - -```typescript -export const AUTH_CONFIG = { - // API 엔드포인트 - apiUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000', - - // 완전 공개 라우트 (인증 체크 안함) - publicRoutes: [ - '/', - '/about', - '/contact', - '/terms', - '/privacy', - ], - - // 인증 필요 라우트 - protectedRoutes: [ - '/dashboard', - '/profile', - '/settings', - '/admin', - '/tenant', - '/users', - '/reports', - // ... ERP 경로들 - ], - - // 게스트 전용 (로그인 후 접근 불가) - guestOnlyRoutes: [ - '/login', - '/register', - '/forgot-password', - ], - - // 리다이렉트 설정 - redirects: { - afterLogin: '/dashboard', - afterLogout: '/login', - unauthorized: '/login', - }, -}; -``` - -### 2. Sanctum API 클라이언트 (lib/auth/sanctum.ts) - -```typescript -class SanctumClient { - private baseURL: string; - private csrfToken: string | null = null; - - constructor() { - this.baseURL = AUTH_CONFIG.apiUrl; - } - - /** - * CSRF 토큰 가져오기 - * 로그인/회원가입 전에 반드시 호출 - */ - async getCsrfToken(): Promise { - await fetch(`${this.baseURL}/sanctum/csrf-cookie`, { - credentials: 'include', // 쿠키 포함 - }); - } - - /** - * 로그인 - */ - async login(email: string, password: string): Promise { - await this.getCsrfToken(); - - const response = await fetch(`${this.baseURL}/api/login`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - credentials: 'include', - body: JSON.stringify({ email, password }), - }); - - if (!response.ok) { - throw new Error('Login failed'); - } - - return await response.json(); - } - - /** - * 회원가입 - */ - async register(data: RegisterData): Promise { - await this.getCsrfToken(); - - const response = await fetch(`${this.baseURL}/api/register`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - credentials: 'include', - body: JSON.stringify(data), - }); - - if (!response.ok) { - const error = await response.json(); - throw error; - } - - return await response.json(); - } - - /** - * 로그아웃 - */ - async logout(): Promise { - await fetch(`${this.baseURL}/api/logout`, { - method: 'POST', - credentials: 'include', - }); - } - - /** - * 현재 사용자 정보 - */ - async getCurrentUser(): Promise { - try { - const response = await fetch(`${this.baseURL}/api/user`, { - credentials: 'include', - }); - - if (response.ok) { - return await response.json(); - } - return null; - } catch { - return null; - } - } -} - -export const sanctumClient = new SanctumClient(); -``` - -**핵심 포인트**: -- `credentials: 'include'` - 모든 요청에 쿠키 포함 -- CSRF 토큰은 쿠키로 자동 관리 (Laravel이 처리) -- 에러 처리 일관성 - -### 3. 서버 인증 유틸 (lib/auth/server-auth.ts) - -```typescript -import { cookies } from 'next/headers'; -import { AUTH_CONFIG } from './auth-config'; - -/** - * 서버 컴포넌트에서 세션 가져오기 - */ -export async function getServerSession(): Promise { - const cookieStore = await cookies(); - const sessionCookie = cookieStore.get('laravel_session'); - - if (!sessionCookie) { - return null; - } - - try { - const response = await fetch(`${AUTH_CONFIG.apiUrl}/api/user`, { - headers: { - Cookie: `laravel_session=${sessionCookie.value}`, - Accept: 'application/json', - }, - cache: 'no-store', // 항상 최신 데이터 - }); - - if (response.ok) { - return await response.json(); - } - } catch (error) { - console.error('Failed to get server session:', error); - } - - return null; -} - -/** - * 서버 컴포넌트에서 인증 필요 - */ -export async function requireAuth(): Promise { - const user = await getServerSession(); - - if (!user) { - redirect('/login'); - } - - return user; -} -``` - -**사용 예시**: -```typescript -// app/(protected)/dashboard/page.tsx -import { requireAuth } from '@/lib/auth/server-auth'; - -export default async function DashboardPage() { - const user = await requireAuth(); // 인증 필요 - - return
Welcome {user.name}
; -} -``` - -### 4. Middleware 통합 (middleware.ts) - -```typescript -import { NextResponse } from 'next/server'; -import type { NextRequest } from 'next/server'; -import createIntlMiddleware from 'next-intl/middleware'; -import { locales, defaultLocale } from '@/i18n/config'; -import { AUTH_CONFIG } from '@/lib/auth/auth-config'; - -const intlMiddleware = createIntlMiddleware({ - locales, - defaultLocale, - localePrefix: 'as-needed', -}); - -// 경로가 보호된 라우트인지 확인 -function isProtectedRoute(pathname: string): boolean { - return AUTH_CONFIG.protectedRoutes.some(route => - pathname.startsWith(route) - ); -} - -// 경로가 공개 라우트인지 확인 -function isPublicRoute(pathname: string): boolean { - return AUTH_CONFIG.publicRoutes.some(route => - pathname === route || pathname.startsWith(route) - ); -} - -// 경로가 게스트 전용인지 확인 -function isGuestOnlyRoute(pathname: string): boolean { - return AUTH_CONFIG.guestOnlyRoutes.some(route => - pathname === route || pathname.startsWith(route) - ); -} - -// 로케일 제거 -function stripLocale(pathname: string): string { - for (const locale of locales) { - if (pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`) { - return pathname.slice(`/${locale}`.length) || '/'; - } - } - return pathname; -} - -export function middleware(request: NextRequest) { - const { pathname } = request.nextUrl; - - // 1. Bot Detection (기존 로직) - // ... bot check code ... - - // 2. 정적 파일 제외 - if ( - pathname.includes('/_next/') || - pathname.includes('/api/') || - pathname.match(/\.(ico|png|jpg|jpeg|svg|gif|webp)$/) - ) { - return intlMiddleware(request); - } - - // 3. 로케일 제거하여 경로 체크 - const pathnameWithoutLocale = stripLocale(pathname); - - // 4. 세션 쿠키 확인 - const sessionCookie = request.cookies.get('laravel_session'); - const isAuthenticated = !!sessionCookie; - - // 5. 보호된 라우트 체크 - if (isProtectedRoute(pathnameWithoutLocale) && !isAuthenticated) { - const url = new URL('/login', request.url); - url.searchParams.set('redirect', pathname); - return NextResponse.redirect(url); - } - - // 6. 게스트 전용 라우트 체크 (이미 로그인한 경우) - if (isGuestOnlyRoute(pathnameWithoutLocale) && isAuthenticated) { - return NextResponse.redirect( - new URL(AUTH_CONFIG.redirects.afterLogin, request.url) - ); - } - - // 7. i18n 미들웨어 실행 - return intlMiddleware(request); -} - -export const config = { - matcher: [ - '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico)$).*)', - ], -}; -``` - -**장점**: -- 단일 진입점에서 모든 인증 처리 -- 가드 컴포넌트 불필요 -- 중복 코드 제거 -- 성능 최적화 (서버 사이드 체크) - -### 5. Auth Context (contexts/AuthContext.tsx) - -```typescript -'use client'; - -import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; -import { sanctumClient } from '@/lib/auth/sanctum'; -import { useRouter } from 'next/navigation'; -import { AUTH_CONFIG } from '@/lib/auth/auth-config'; - -interface User { - id: number; - name: string; - email: string; -} - -interface AuthContextType { - user: User | null; - loading: boolean; - login: (email: string, password: string) => Promise; - register: (data: RegisterData) => Promise; - logout: () => Promise; - refreshUser: () => Promise; -} - -const AuthContext = createContext(undefined); - -export function AuthProvider({ children }: { children: ReactNode }) { - const [user, setUser] = useState(null); - const [loading, setLoading] = useState(true); - const router = useRouter(); - - // 초기 로드 시 사용자 정보 가져오기 - useEffect(() => { - sanctumClient.getCurrentUser() - .then(setUser) - .catch(() => setUser(null)) - .finally(() => setLoading(false)); - }, []); - - const login = async (email: string, password: string) => { - const user = await sanctumClient.login(email, password); - setUser(user); - router.push(AUTH_CONFIG.redirects.afterLogin); - }; - - const register = async (data: RegisterData) => { - const user = await sanctumClient.register(data); - setUser(user); - router.push(AUTH_CONFIG.redirects.afterLogin); - }; - - const logout = async () => { - await sanctumClient.logout(); - setUser(null); - router.push(AUTH_CONFIG.redirects.afterLogout); - }; - - const refreshUser = async () => { - const user = await sanctumClient.getCurrentUser(); - setUser(user); - }; - - return ( - - {children} - - ); -} - -export function useAuth() { - const context = useContext(AuthContext); - if (!context) { - throw new Error('useAuth must be used within AuthProvider'); - } - return context; -} -``` - -**사용 예시**: -```typescript -// components/LoginForm.tsx -'use client'; - -import { useAuth } from '@/contexts/AuthContext'; - -export function LoginForm() { - const { login, loading } = useAuth(); - - const handleSubmit = async (e: FormEvent) => { - e.preventDefault(); - await login(email, password); - }; - - return
...
; -} -``` - -## 🔒 보안 고려사항 - -### 1. CSRF 보호 - -**Next.js 측**: -- 모든 상태 변경 요청 전에 `getCsrfToken()` 호출 -- Laravel이 XSRF-TOKEN 쿠키 발급 -- 브라우저가 자동으로 헤더에 포함 - -**Laravel 측** (백엔드 담당): -```php -// config/sanctum.php -'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost,localhost:3000')), -``` - -### 2. 쿠키 보안 설정 - -**Laravel 측** (백엔드 담당): -```php -// config/session.php -'secure' => env('SESSION_SECURE_COOKIE', true), // HTTPS only -'http_only' => true, // JavaScript 접근 불가 -'same_site' => 'lax', // CSRF 방지 -``` - -### 3. CORS 설정 - -**Laravel 측** (백엔드 담당): -```php -// config/cors.php -'paths' => ['api/*', 'sanctum/csrf-cookie'], -'supports_credentials' => true, -'allowed_origins' => [env('FRONTEND_URL')], -'allowed_headers' => ['*'], -'exposed_headers' => [], -'max_age' => 0, -``` - -### 4. 환경 변수 - -```env -# .env.local (Next.js) -NEXT_PUBLIC_API_URL=http://localhost:8000 -NEXT_PUBLIC_FRONTEND_URL=http://localhost:3000 -``` - -```env -# .env (Laravel) -FRONTEND_URL=http://localhost:3000 -SANCTUM_STATEFUL_DOMAINS=localhost:3000 -SESSION_DOMAIN=localhost -SESSION_SECURE_COOKIE=false # 개발: false, 프로덕션: true -``` - -### 5. XSS 방어 - -- HTTP-only 쿠키 사용 (JavaScript로 접근 불가) -- 사용자 입력 sanitization (React가 기본으로 처리) -- CSP 헤더 설정 (Next.js 설정) - -### 6. Rate Limiting - -**Laravel 측** (백엔드 담당): -```php -// routes/api.php -Route::middleware(['throttle:login'])->group(function () { - Route::post('/login', [AuthController::class, 'login']); -}); - -// app/Http/Kernel.php -'login' => 'throttle:5,1', // 1분에 5번 -``` - -## 📊 에러 처리 전략 - -### 1. 에러 타입별 처리 - -```typescript -// lib/auth/sanctum.ts -class ApiError extends Error { - constructor( - public status: number, - public code: string, - message: string, - public errors?: Record - ) { - super(message); - } -} - -async function handleResponse(response: Response): Promise { - if (response.ok) { - return await response.json(); - } - - const data = await response.json().catch(() => ({})); - - switch (response.status) { - case 401: - // 인증 실패 - 로그인 페이지로 - window.location.href = '/login'; - throw new ApiError(401, 'UNAUTHORIZED', 'Please login'); - - case 403: - // 권한 없음 - throw new ApiError(403, 'FORBIDDEN', 'Access denied'); - - case 422: - // Validation 에러 - throw new ApiError( - 422, - 'VALIDATION_ERROR', - data.message || 'Validation failed', - data.errors - ); - - case 429: - // Rate limit - throw new ApiError(429, 'RATE_LIMIT', 'Too many requests'); - - case 500: - // 서버 에러 - throw new ApiError(500, 'SERVER_ERROR', 'Server error occurred'); - - default: - throw new ApiError( - response.status, - 'UNKNOWN_ERROR', - data.message || 'An error occurred' - ); - } -} -``` - -### 2. UI 에러 표시 - -```typescript -// components/LoginForm.tsx -const [error, setError] = useState(null); -const [fieldErrors, setFieldErrors] = useState>({}); - -try { - await login(email, password); -} catch (err) { - if (err instanceof ApiError) { - if (err.status === 422 && err.errors) { - setFieldErrors(err.errors); - } else { - setError(err.message); - } - } else { - setError('An unexpected error occurred'); - } -} -``` - -### 3. 네트워크 에러 처리 - -```typescript -// 재시도 로직 -async function fetchWithRetry( - url: string, - options: RequestInit, - retries = 3 -): Promise { - try { - return await fetch(url, options); - } catch (error) { - if (retries > 0) { - await new Promise(resolve => setTimeout(resolve, 1000)); - return fetchWithRetry(url, options, retries - 1); - } - throw new Error('Network error. Please check your connection.'); - } -} -``` - -## 🚀 성능 최적화 - -### 1. Middleware 최적화 - -```typescript -// 정적 파일 조기 리턴 -if (pathname.includes('/_next/') || pathname.match(/\.(ico|png|jpg)$/)) { - return NextResponse.next(); -} - -// 쿠키만 확인, API 호출 안함 -const isAuthenticated = !!request.cookies.get('laravel_session'); -``` - -### 2. 클라이언트 캐싱 - -```typescript -// AuthContext에서 사용자 정보 캐싱 -// 페이지 이동 시 재요청 안함 -const [user, setUser] = useState(null); -``` - -### 3. Server Components 활용 - -```typescript -// 서버에서 데이터 fetch -export default async function DashboardPage() { - const user = await getServerSession(); - const data = await fetchDashboardData(user.id); - - return ; -} -``` - -### 4. Parallel Data Fetching - -```typescript -// 병렬 데이터 요청 -const [user, stats, notifications] = await Promise.all([ - getServerSession(), - fetchStats(), - fetchNotifications(), -]); -``` - -## 📝 구현 단계 - -### Phase 1: 기본 인프라 설정 - -- [ ] 1.1 인증 설정 파일 생성 (`auth-config.ts`) -- [ ] 1.2 Sanctum API 클라이언트 구현 (`sanctum.ts`) -- [ ] 1.3 서버 인증 유틸리티 (`server-auth.ts`) -- [ ] 1.4 타입 정의 (`types/auth.ts`) - -### Phase 2: Middleware 통합 - -- [ ] 2.1 현재 middleware.ts 백업 -- [ ] 2.2 인증 로직 추가 -- [ ] 2.3 라우트 보호 로직 구현 -- [ ] 2.4 리다이렉트 로직 구현 - -### Phase 3: 클라이언트 상태 관리 - -- [ ] 3.1 AuthContext 생성 -- [ ] 3.2 AuthProvider를 layout.tsx에 추가 -- [ ] 3.3 useAuth 훅 테스트 - -### Phase 4: 인증 페이지 구현 - -- [ ] 4.1 로그인 페이지 (`/login`) -- [ ] 4.2 회원가입 페이지 (`/register`) -- [ ] 4.3 비밀번호 재설정 (`/forgot-password`) -- [ ] 4.4 폼 Validation (react-hook-form + zod) - -### Phase 5: 보호된 페이지 구현 - -- [ ] 5.1 대시보드 페이지 (`/dashboard`) -- [ ] 5.2 프로필 페이지 (`/profile`) -- [ ] 5.3 설정 페이지 (`/settings`) - -### Phase 6: 테스트 및 최적화 - -- [ ] 6.1 인증 플로우 테스트 -- [ ] 6.2 에러 케이스 테스트 -- [ ] 6.3 성능 측정 및 최적화 -- [ ] 6.4 보안 점검 - -## 🤔 검토 포인트 - -### 1. 설계 관련 질문 - -- **Middleware 중심 설계가 적합한가?** - - 장점: 중앙 집중식 관리, 중복 코드 제거 - - 단점: 복잡도 증가 가능성 - -- **세션 쿠키만으로 충분한가?** - - Sanctum SPA 모드는 세션 쿠키로 충분 - - API 토큰 모드가 필요한 경우 추가 구현 필요 - -- **Server Components vs Client Components 비율은?** - - 인증 체크: Server (Middleware + getServerSession) - - 상태 관리: Client (AuthContext) - - UI: 혼합 (페이지는 Server, 인터랙션은 Client) - -### 2. 구현 우선순위 - -**높음 (즉시 필요)**: -- auth-config.ts -- sanctum.ts -- middleware.ts 업데이트 -- 로그인 페이지 - -**중간 (빠르게 필요)**: -- AuthContext -- 회원가입 페이지 -- 대시보드 기본 구조 - -**낮음 (나중에)**: -- 비밀번호 재설정 -- 프로필 관리 -- 고급 보안 기능 - -### 3. Laravel 백엔드 체크리스트 - -백엔드 개발자가 확인해야 할 사항: - -```php -# 1. Sanctum 설치 및 설정 -composer require laravel/sanctum -php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider" - -# 2. config/sanctum.php -'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost:3000')), - -# 3. config/cors.php -'supports_credentials' => true, -'allowed_origins' => [env('FRONTEND_URL')], - -# 4. API Routes -Route::post('/login', [AuthController::class, 'login']); -Route::post('/register', [AuthController::class, 'register']); -Route::post('/logout', [AuthController::class, 'logout'])->middleware('auth:sanctum'); -Route::get('/user', [AuthController::class, 'user'])->middleware('auth:sanctum'); - -# 5. CORS 미들웨어 -app/Http/Kernel.php에 \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class 추가 -``` - -## 🎯 다음 액션 - -이 설계 문서를 검토 후: - -1. **승인 시**: Phase 1부터 순차적으로 구현 시작 -2. **수정 필요 시**: 피드백 반영 후 재설계 -3. **질문 사항**: 불명확한 부분 명확화 - -질문이나 수정 사항이 있으면 알려주세요! \ No newline at end of file diff --git a/claudedocs/archive/[PLAN-2025-11-18] refactoring-plan.md b/claudedocs/archive/[PLAN-2025-11-18] refactoring-plan.md deleted file mode 100644 index bda2469a..00000000 --- a/claudedocs/archive/[PLAN-2025-11-18] refactoring-plan.md +++ /dev/null @@ -1,268 +0,0 @@ -# DataContext.tsx 리팩토링 계획 - -## 현황 분석 - -### 기존 파일 구조 -- **총 라인**: 6,707줄 -- **파일 크기**: 222KB -- **상태 변수**: 33개 -- **타입 정의**: 50개 이상 - -### 문제점 -1. 단일 파일에 모든 도메인 집중 → 유지보수 불가능 -2. 6700줄 분석 시 토큰 과다 소비 → 세션 종료 빈번 -3. 관련 없는 데이터도 항상 로드 → 성능 저하 - ---- - -## 도메인 분류 (10개 도메인, 33개 상태) - -### 1. ItemMaster (품목 마스터) - 13개 상태 -**파일**: `contexts/ItemMasterContext.tsx` -**관련 페이지**: 품목관리, 품목기준관리 - -상태: -- itemMasters (품목 마스터 데이터) -- specificationMasters (규격 마스터) -- materialItemNames (자재 품목명) -- itemCategories (품목 분류) -- itemUnits (단위) -- itemMaterials (재질) -- surfaceTreatments (표면처리) -- partTypeOptions (부품 유형 옵션) -- partUsageOptions (부품 용도 옵션) -- guideRailOptions (가이드레일 옵션) -- sectionTemplates (섹션 템플릿) -- itemMasterFields (품목 필드 정의) -- itemPages (품목 입력 페이지) - -타입: -- ItemMaster, ItemRevisio1n, ItemCategory, ItemUnit, ItemMaterial -- SurfaceTreatment, PartTypeOption, PartUsageOption, GuideRailOption -- ItemMasterField, ItemFieldProperty, FieldDisplayCondition -- ItemField, ItemSection, ItemPage, SectionTemplate -- SpecificationMaster, MaterialItemName -- BOMLine, BOMItem, BendingDetail - ---- - -### 2. Sales (판매) - 3개 상태 -**파일**: `contexts/SalesContext.tsx` -**관련 페이지**: 견적관리, 수주관리, 거래처관리 - -상태: -- salesOrders (수주 데이터) -- quotes (견적 데이터) -- clients (거래처 데이터) - -타입: -- SalesOrder, SalesOrderItem, OrderRevision, DocumentSendHistory -- Quote, QuoteRevision, QuoteCalculationRow, BOMCalculationRow -- Client - ---- - -### 3. Production (생산) - 2개 상태 -**파일**: `contexts/ProductionContext.tsx` -**관련 페이지**: 생산관리, 품질관리 - -상태: -- productionOrders (생산지시 데이터) -- qualityInspections (품질검사 데이터) - -타입: -- ProductionOrder -- QualityInspection - ---- - -### 4. Inventory (재고) - 2개 상태 -**파일**: `contexts/InventoryContext.tsx` -**관련 페이지**: 재고관리, 구매관리 - -상태: -- inventoryItems (재고 데이터) -- purchaseOrders (구매 데이터) - -타입: -- InventoryItem -- PurchaseOrder - ---- - -### 5. Shipping (출고) - 1개 상태 -**파일**: `contexts/ShippingContext.tsx` -**관련 페이지**: 출고관리 - -상태: -- shippingOrders (출고지시서 데이터) - -타입: -- ShippingOrder, ShippingOrderItem -- ShippingSchedule, ShippingLot, ShippingLotItem - ---- - -### 6. HR (인사) - 3개 상태 -**파일**: `contexts/HRContext.tsx` -**관련 페이지**: 직원관리, 근태관리, 결재관리 - -상태: -- employees (직원 데이터) -- attendances (근태 데이터) -- approvals (결재 데이터) - -타입: -- Employee -- Attendance -- Approval - ---- - -### 7. Accounting (회계) - 2개 상태 -**파일**: `contexts/AccountingContext.tsx` -**관련 페이지**: 회계관리, 매출채권관리 - -상태: -- accountingTransactions (회계 거래 데이터) -- receivables (매출채권 데이터) - -타입: -- AccountingTransaction -- Receivable - ---- - -### 8. Facilities (시설) - 2개 상태 -**파일**: `contexts/FacilitiesContext.tsx` -**관련 페이지**: 차량관리, 현장관리 - -상태: -- vehicles (차량 데이터) -- sites (현장 데이터) - -타입: -- Vehicle -- Site, SiteAttachment - ---- - -### 9. Pricing (가격/계산식) - 3개 상태 -**파일**: `contexts/PricingContext.tsx` -**관련 페이지**: 가격관리, 계산식관리 - -상태: -- formulas (계산식 데이터) -- formulaRules (계산식 규칙 데이터) -- pricing (가격 데이터) - -타입: -- CalculationFormula, FormulaRevision -- FormulaRule, FormulaRuleRevision, RangeRule -- PricingData, PriceRevision - ---- - -### 10. Auth (인증) - 2개 상태 -**파일**: `contexts/AuthContext.tsx` -**관련 페이지**: 로그인, 사용자관리 - -상태: -- users (사용자 데이터) -- currentUser (현재 사용자) - -타입: -- User, UserRole - ---- - -## 공통 타입 파일 - -### types/index.ts -재사용되는 공통 타입 정의: -- 없음 (각 도메인이 독립적) - ---- - -## 통합 Provider - -### contexts/RootProvider.tsx -모든 Context를 통합하는 최상위 Provider - -```tsx -export function RootProvider({ children }: { children: ReactNode }) { - return ( - - - - - - - - - - - {children} - - - - - - - - - - - ); -} -``` - ---- - -## 마이그레이션 체크리스트 - -### Phase 1: 준비 -- [x] 전체 구조 분석 -- [x] 도메인 분류 설계 -- [ ] 기존 파일 백업 - -### Phase 2: Context 생성 (10개) -- [ ] AuthContext.tsx -- [ ] ItemMasterContext.tsx -- [ ] SalesContext.tsx -- [ ] ProductionContext.tsx -- [ ] InventoryContext.tsx -- [ ] ShippingContext.tsx -- [ ] HRContext.tsx -- [ ] AccountingContext.tsx -- [ ] FacilitiesContext.tsx -- [ ] PricingContext.tsx - -### Phase 3: 통합 -- [ ] RootProvider.tsx 생성 -- [ ] app/layout.tsx에서 RootProvider 적용 -- [ ] 기존 DataContext.tsx 삭제 - -### Phase 4: 검증 -- [ ] 빌드 테스트 (npm run build) -- [ ] 타입 체크 (npm run type-check) -- [ ] 품목관리 페이지 동작 확인 -- [ ] 기타 페이지 동작 확인 - ---- - -## 예상 효과 - -### 파일 크기 감소 -- 기존: 6,707줄 → 각 도메인: 평균 500-1,500줄 -- ItemMaster: ~2,000줄 (가장 큼) -- Auth: ~300줄 (가장 작음) - -### 토큰 사용량 감소 -- 품목관리 작업 시: 70% 감소 -- 기타 페이지 작업 시: 60-80% 감소 - -### 유지보수성 향상 -- 도메인별 독립적 관리 -- 수정 시 영향 범위 명확 -- 협업 시 충돌 최소화 \ No newline at end of file diff --git a/claudedocs/archive/[PLAN-2025-11-21] component-separation.md b/claudedocs/archive/[PLAN-2025-11-21] component-separation.md deleted file mode 100644 index 6d274f33..00000000 --- a/claudedocs/archive/[PLAN-2025-11-21] component-separation.md +++ /dev/null @@ -1,703 +0,0 @@ -# ItemMasterDataManagement.tsx 컴포넌트 분리 계획 - -**작성일**: 2025-11-18 -**원본 파일 크기**: 5,231줄 -**현재 파일 크기**: 3,254줄 (37.8% 절감!) -**목표 파일 크기**: 1,500-2,000줄 (60-65% 감소) - ---- - -## 📊 현재 상태 분석 - -### 파일 구성 -``` -ItemMasterDataManagement.tsx (5,231줄) -├── State 선언 (121개 useState) -├── Handler 함수 (31개) -├── 유틸리티 함수 (59개) -├── TabsContent 블록들 (약 895줄) -│ ├── attributes (558줄) ✅ 분리 완료 → MasterFieldTab.tsx -│ ├── items (12줄) -│ ├── sections (242줄) -│ ├── hierarchy (43줄) ✅ 분리 완료 → HierarchyTab.tsx -│ └── categories (40줄) ✅ 분리 완료 → CategoryTab.tsx -└── Dialog/Drawer 블록들 (약 2,302줄, 18개) -``` - -### 이미 분리 완료된 컴포넌트 ✅ -1. **CategoryTab.tsx** (약 40줄) -2. **MasterFieldTab.tsx** (약 558줄) -3. **HierarchyTab.tsx** (약 43줄) - -**총 분리 완료**: 약 641줄 - ---- - -## 🎯 분리 계획 상세 - -### Phase 1: Dialog 컴포넌트 분리 (우선순위 1) -**예상 절감**: 약 2,300줄 - -#### 1.1 필드 관리 다이얼로그 -``` -src/components/items/ItemMasterDataManagement/dialogs/FieldDialog.tsx -``` -- **위치**: line 3647-4156 (약 510줄) -- **기능**: 필드 추가/편집 -- **Props 필요**: - - isOpen, onOpenChange - - selectedSection - - editingFieldId - - onSave (handleSaveField) - - masterFields - - fieldType states (name, key, inputType, etc.) - -#### 1.2 필드 드로어 (모바일) -``` -src/components/items/ItemMasterDataManagement/dialogs/FieldDrawer.tsx -``` -- **위치**: line 4157-4665 (약 508줄) -- **기능**: 모바일용 필드 편집 드로어 -- **Props**: FieldDialog와 동일 - -#### 1.3 페이지 다이얼로그 -``` -src/components/items/ItemMasterDataManagement/dialogs/PageDialog.tsx -``` -- **위치**: line 3559-3595 (약 36줄) -- **기능**: 페이지(섹션) 추가 -- **Props**: - - isOpen, onOpenChange - - onSave (handleAddPage) - -#### 1.4 섹션 다이얼로그 -``` -src/components/items/ItemMasterDataManagement/dialogs/SectionDialog.tsx -``` -- **위치**: line 3596-3646 (약 50줄) -- **기능**: 하위섹션 추가 -- **Props**: - - isOpen, onOpenChange - - selectedPage - - onSave (handleAddSection) - -#### 1.5 마스터 필드 다이얼로그 -``` -src/components/items/ItemMasterDataManagement/dialogs/MasterFieldDialog.tsx -``` -- **위치**: line 4729-4908 (약 180줄) -- **기능**: 마스터 항목 추가/편집 -- **Props**: - - isOpen, onOpenChange - - editingMasterFieldId - - onSave (handleSaveMasterField) - - field states - -#### 1.6 섹션 템플릿 다이얼로그 -``` -src/components/items/ItemMasterDataManagement/dialogs/SectionTemplateDialog.tsx -``` -- **위치**: line 4909-5005 (약 97줄) -- **기능**: 섹션 템플릿 생성 -- **Props**: - - isOpen, onOpenChange - - onSave (handleSaveTemplate) - -#### 1.7 템플릿 필드 다이얼로그 -``` -src/components/items/ItemMasterDataManagement/dialogs/TemplateFieldDialog.tsx -``` -- **위치**: line 5006-5146 (약 141줄) -- **기능**: 템플릿 항목 추가/편집 -- **Props**: - - isOpen, onOpenChange - - currentTemplateId - - editingTemplateFieldId - - onSave - -#### 1.8 템플릿 불러오기 다이얼로그 -``` -src/components/items/ItemMasterDataManagement/dialogs/LoadTemplateDialog.tsx -``` -- **위치**: line 5147-5230 (약 84줄) -- **기능**: 섹션 템플릿 불러오기 -- **Props**: - - isOpen, onOpenChange - - sectionTemplates - - onLoad (handleLoadTemplate) - -#### 1.9 옵션 관리 다이얼로그 -``` -src/components/items/ItemMasterDataManagement/dialogs/OptionDialog.tsx -``` -- **위치**: line 3236-3382 (약 147줄) -- **기능**: 단위/재질/표면처리 옵션 추가 -- **Props**: - - isOpen, onOpenChange - - optionType - - onSave (handleAddOption) - -#### 1.10 칼럼 관리 다이얼로그들 -``` -src/components/items/ItemMasterDataManagement/dialogs/ColumnManageDialog.tsx -src/components/items/ItemMasterDataManagement/dialogs/ColumnDialog.tsx -``` -- **위치**: line 3383-3518, 4666-4728 (약 210줄) -- **기능**: 칼럼 구조 관리 -- **Props**: 칼럼 관련 states 및 handlers - -#### 1.11 탭 관리 다이얼로그들 -``` -src/components/items/ItemMasterDataManagement/dialogs/TabManagementDialogs.tsx -``` -- **위치**: line 2929-3235 (약 307줄) -- **포함 다이얼로그**: - - ManageTabsDialog - - DeleteTabDialog (AlertDialog) - - AddTabDialog - - ManageAttributeTabsDialog - - DeleteAttributeTabDialog (AlertDialog) - - AddAttributeTabDialog -- **Props**: 탭 관련 모든 states 및 handlers - -#### 1.12 경로 편집 다이얼로그 -``` -src/components/items/ItemMasterDataManagement/dialogs/PathEditDialog.tsx -``` -- **위치**: line 3519-3558 (약 40줄) -- **기능**: 절대경로 편집 -- **Props**: - - editingPathPageId - - onOpenChange, onSave - ---- - -### Phase 2: 타입 정의 분리 (우선순위 2) ⭐ 순서 변경 -**예상 절감**: 약 25줄 (수정됨) -**변경 이유**: 빠른 작업, 코드 정리 -**참고**: 주요 타입들은 ItemMasterContext에 이미 정의되어 있음 - -``` -src/components/items/ItemMasterDataManagement/types.ts -``` - -#### 분리할 로컬 타입들 (3개) -- **ItemCategoryStructure** - 품목 카테고리 구조 (4줄) -- **OptionColumn** - 옵션 컬럼 타입 (7줄) -- **MasterOption** - 마스터 옵션 타입 (14줄) - -#### Context에서 이미 Import하는 타입들 (분리 불필요) -- ItemPage, ItemSection, ItemField -- FieldDisplayCondition, ItemMasterField -- ItemFieldProperty, SectionTemplate - ---- - -### Phase 3: 추가 탭 컴포넌트 분리 (우선순위 3) ⭐ 순서 변경 -**예상 절감**: 약 254줄 -**변경 이유**: 가시적 효과, Dialog 분리와 유사한 패턴 - -#### 3.1 섹션 관리 탭 -``` -src/components/items/ItemMasterDataManagement/tabs/SectionsTab.tsx -``` -- **위치**: line 2604-2846 (약 242줄) -- **기능**: 섹션 템플릿 관리 -- **Props**: - - sectionTemplates - - handlers (CRUD) - -#### 3.2 아이템 탭 -``` -src/components/items/ItemMasterDataManagement/tabs/ItemsTab.tsx -``` -- **위치**: line 2592-2604 (약 12줄) -- **기능**: 아이템 목록 (단순) -- **Props**: itemMasters - ---- - -### Phase 4: 유틸리티 & Hooks 통합 분리 (우선순위 4) ⭐ Phase 통합 -**예상 절감**: 약 900줄 (Utils 500줄 + Hooks 400줄) -**변경 이유**: 순수 Utils가 적음, Hooks와 함께 정리하는 게 효율적 - -#### 4.1 Utils 파일 생성 -``` -src/components/items/ItemMasterDataManagement/utils/ -├── pathUtils.ts - 경로 생성/관리 함수 -├── fieldUtils.ts - 필드 생성/검증 함수 -├── sectionUtils.ts - 섹션 관리 함수 -└── validationUtils.ts - 유효성 검증 함수 -``` - -**주요 유틸리티 함수들**: -- `generateAbsolutePath()` - 절대경로 생성 -- `generateFieldKey()` - 필드 키 생성 -- `validateField()` - 필드 검증 -- `findFieldByKey()` - 필드 검색 -- 기타 순수 함수들 - -#### 4.2 Custom Hooks 생성 -``` -src/components/items/ItemMasterDataManagement/hooks/ -├── usePageManagement.ts - 페이지 관리 로직 -├── useSectionManagement.ts - 섹션 관리 로직 -├── useFieldManagement.ts - 필드 관리 로직 -├── useTemplateManagement.ts - 템플릿 관리 로직 -└── useTabManagement.ts - 탭 관리 로직 -``` - -**분리할 Handler들**: -- Page 관련 (5개): handleAddPage, handleDeletePage, handleUpdatePage, etc. -- Section 관련 (8개): handleAddSection, handleDeleteSection, handleUpdateSection, etc. -- Field 관련 (10개): handleAddField, handleEditField, handleDeleteField, etc. -- Template 관련 (6개): handleSaveTemplate, handleLoadTemplate, etc. -- Tab 관련 (6개): handleAddTab, handleDeleteTab, handleUpdateTab, etc. - ---- - -## 📦 최종 디렉토리 구조 - -``` -src/components/items/ItemMasterDataManagement/ -├── index.tsx # 메인 컴포넌트 (약 1,500-2,000줄) -├── tabs/ -│ ├── CategoryTab.tsx # ✅ 완료 (40줄) -│ ├── MasterFieldTab.tsx # ✅ 완료 (558줄) -│ ├── HierarchyTab.tsx # ✅ 완료 (43줄) -│ ├── SectionsTab.tsx # ⏳ 예정 (242줄) -│ └── ItemsTab.tsx # ⏳ 예정 (12줄) -├── dialogs/ -│ ├── FieldDialog.tsx # ⏳ 예정 (510줄) -│ ├── FieldDrawer.tsx # ⏳ 예정 (508줄) -│ ├── PageDialog.tsx # ⏳ 예정 (36줄) -│ ├── SectionDialog.tsx # ⏳ 예정 (50줄) -│ ├── MasterFieldDialog.tsx # ⏳ 예정 (180줄) -│ ├── SectionTemplateDialog.tsx # ⏳ 예정 (97줄) -│ ├── TemplateFieldDialog.tsx # ⏳ 예정 (141줄) -│ ├── LoadTemplateDialog.tsx # ⏳ 예정 (84줄) -│ ├── OptionDialog.tsx # ⏳ 예정 (147줄) -│ ├── ColumnManageDialog.tsx # ⏳ 예정 (100줄) -│ ├── ColumnDialog.tsx # ⏳ 예정 (110줄) -│ ├── TabManagementDialogs.tsx # ⏳ 예정 (307줄) -│ └── PathEditDialog.tsx # ⏳ 예정 (40줄) -├── hooks/ -│ ├── usePageManagement.ts # ⏳ 예정 -│ ├── useSectionManagement.ts # ⏳ 예정 -│ ├── useFieldManagement.ts # ⏳ 예정 -│ ├── useTemplateManagement.ts # ⏳ 예정 -│ └── useTabManagement.ts # ⏳ 예정 -├── utils/ -│ ├── pathUtils.ts # ⏳ 예정 -│ ├── fieldUtils.ts # ⏳ 예정 -│ ├── sectionUtils.ts # ⏳ 예정 -│ └── validationUtils.ts # ⏳ 예정 -└── types.ts # ⏳ 예정 (200줄) -``` - ---- - -## 📈 예상 효과 - -### 파일 크기 변화 (⭐ Phase 순서 변경됨) -| 단계 | 작업 | 예상 감소 | 누적 감소 | 남은 크기 | -|-----|-----|---------|---------|---------| -| **시작** | - | - | - | **5,231줄** | -| Phase 0 (완료) | Tabs 분리 | 641줄 | 641줄 | 4,590줄 | -| Phase 1 (완료) | Dialogs 분리 | 1,977줄 | 2,618줄 | 2,613줄 | -| **Phase 2 (다음)** | **Types 분리** | **200줄** | **2,818줄** | **2,413줄** | -| Phase 3 | 추가 Tabs | 254줄 | 3,072줄 | 2,159줄 | -| Phase 4 | Utils + Hooks | 900줄 | 3,972줄 | **1,259줄** | - -### 최종 목표 -- **메인 파일**: 약 936-1,500줄 (현재 대비 70-82% 감소) -- **분리된 컴포넌트**: 13개 다이얼로그, 5개 탭, 5개 hooks, 4개 utils, 1개 types -- **총 파일 수**: 약 28개 파일 - ---- - -## 🚀 실행 계획 - -### 우선순위별 작업 순서 - -#### 1단계: 대형 다이얼로그 분리 (즉시 시작) -```bash -# 가장 큰 것부터 분리 -1. FieldDialog.tsx (510줄) -2. FieldDrawer.tsx (508줄) -3. TabManagementDialogs.tsx (307줄) -4. ColumnDialogs (210줄) -5. MasterFieldDialog.tsx (180줄) -``` -**예상 절감**: 약 1,700줄 - -#### 2단계: 나머지 다이얼로그 분리 -```bash -6. OptionDialog.tsx (147줄) -7. TemplateFieldDialog.tsx (141줄) -8. SectionTemplateDialog.tsx (97줄) -9. LoadTemplateDialog.tsx (84줄) -10. SectionDialog.tsx (50줄) -11. PathEditDialog.tsx (40줄) -12. PageDialog.tsx (36줄) -``` -**예상 절감**: 약 600줄 - -#### 3단계: 유틸리티 함수 분리 -```bash -- pathUtils.ts -- fieldUtils.ts -- sectionUtils.ts -- validationUtils.ts -``` -**예상 절감**: 약 500줄 - -#### 4단계: 타입 정의 분리 -```bash -- types.ts -``` -**예상 절감**: 약 200줄 - -#### 5단계: Custom Hooks 분리 -```bash -- usePageManagement.ts -- useSectionManagement.ts -- useFieldManagement.ts -- useTemplateManagement.ts -- useTabManagement.ts -``` -**예상 절감**: 약 400줄 - ---- - -## ✅ 작업 체크리스트 (세션 중단 시 여기서 이어서 진행) - -### Phase 0: 기존 Tab 분리 (완료) -- [x] CategoryTab.tsx (40줄) - ✅ **완료** -- [x] MasterFieldTab.tsx (558줄) - ✅ **완료** -- [x] HierarchyTab.tsx (43줄) - ✅ **완료** -- [x] 분리 계획 문서 작성 - ✅ **완료** - -### Phase 1: Dialog 컴포넌트 분리 (2,300줄 절감 목표) - -#### 1-1. 디렉토리 구조 준비 -- [x] `dialogs/` 디렉토리 생성 - ✅ **완료** - -#### 1-2. 대형 다이얼로그 (우선순위 최상) -- [x] **FieldDialog.tsx** (510줄) - line 3647-4156 - ✅ **완료 (462줄 절감)** - - [x] 컴포넌트 추출 및 파일 생성 - - [x] Props 인터페이스 정의 - - [x] 메인 파일에서 import로 교체 - - [x] 빌드 테스트 - ✅ **통과** - -- [x] **FieldDrawer.tsx** (508줄) - line 3696-4203 - ✅ **완료 (462줄 절감)** - - [x] 컴포넌트 추출 및 파일 생성 - - [x] Props 인터페이스 정의 - - [x] 메인 파일에서 import로 교체 - - [x] 빌드 테스트 - ✅ **통과** - -- [x] **TabManagementDialogs.tsx** (307줄) - line 2930-3236 - ✅ **완료 (265줄 절감)** - - [x] 6개 다이얼로그 추출 - - [x] Props 인터페이스 정의 - - [x] 메인 파일에서 import로 교체 - - [x] 빌드 테스트 - ✅ **통과** - -#### 1-3. 칼럼 관리 다이얼로그 -- [x] **ColumnManageDialog.tsx** (135줄) - ✅ **완료 (119줄 절감)** - - [x] 컴포넌트 추출 - - [x] Props 정의 - - [x] 메인 파일 교체 - - [x] 빌드 테스트 - ✅ **통과** - -- [x] **ColumnDialog.tsx** (110줄) - ✅ **완료 (48줄 절감)** - - [x] 컴포넌트 추출 - - [x] Props 정의 - - [x] 메인 파일 교체 - - [x] 빌드 테스트 - ✅ **통과** - -#### 1-4. 필드 관련 다이얼로그 -- [x] **MasterFieldDialog.tsx** (180줄) - ✅ **완료 (148줄 절감)** - - [x] 컴포넌트 추출 - - [x] Props 정의 - - [x] 메인 파일 교체 - - [x] 빌드 테스트 - ✅ **통과** - -- [x] **OptionDialog.tsx** (147줄) - line 2973-3119 - ✅ **완료 (122줄 절감)** - - [x] 컴포넌트 추출 - - [x] Props 정의 - - [x] 메인 파일 교체 - - [x] 빌드 테스트 - ✅ **통과** - -#### 1-5. 템플릿 관련 다이얼로그 -- [x] **TemplateFieldDialog.tsx** (141줄) - ✅ **완료 (113줄 절감)** - - [x] 컴포넌트 추출 - - [x] Props 정의 - - [x] 메인 파일 교체 - - [x] 빌드 테스트 - ✅ **통과** - -- [x] **SectionTemplateDialog.tsx** (97줄) - ✅ **완료 (78줄 절감)** - - [x] 컴포넌트 추출 - - [x] Props 정의 - - [x] 메인 파일 교체 - - [x] 빌드 테스트 - ✅ **통과** - -- [x] **LoadTemplateDialog.tsx** (84줄) - ✅ **완료 (74줄 절감)** - - [x] 컴포넌트 추출 - - [x] Props 정의 - - [x] 메인 파일 교체 - - [x] 빌드 테스트 - ✅ **통과** - -#### 1-6. 기타 다이얼로그 -- [x] **PathEditDialog.tsx** (40줄) - ✅ **완료** - - [x] 컴포넌트 추출 - - [x] Props 정의 - - [x] 메인 파일 교체 - -- [x] **PageDialog.tsx** (36줄) - ✅ **완료** - - [x] 컴포넌트 추출 - - [x] Props 정의 - - [x] 메인 파일 교체 - -- [x] **SectionDialog.tsx** (50줄) - ✅ **완료 (총 95줄 절감)** - - [x] 컴포넌트 추출 - - [x] Props 정의 - - [x] 메인 파일 교체 - - [x] 빌드 테스트 - ✅ **통과** - -#### 1-7. Phase 1 완료 검증 -- [x] 모든 다이얼로그 분리 완료 확인 - ✅ **13개 다이얼로그 분리 완료** -- [x] TypeScript 에러 없음 확인 - ✅ **통과** -- [x] 빌드 성공 확인 - ✅ **통과** -- [x] **현재 파일 크기 확인** - ✅ **3,254줄 (목표 2,900줄 이하 달성!)** - ---- - -### Phase 2: 타입 정의 분리 (25줄 절감 목표) ⭐ 순서 변경 - -#### 2-1. 타입 파일 생성 -- [x] `types.ts` 생성 ✅ - -#### 2-2. 로컬 타입 정의 이동 (2개 - ItemCategoryStructure는 존재하지 않음) -- [x] OptionColumn 타입 ✅ -- [x] MasterOption 타입 ✅ - -#### 2-3. Phase 2 완료 검증 -- [x] types.ts 생성 완료 ✅ -- [x] 메인 파일에서 import 확인 ✅ -- [x] Dialog 파일에서 import 확인 (ColumnManageDialog) ✅ -- [x] 빌드 테스트 진행 중 ✅ -- [ ] **현재 파일 크기 확인** (목표: ~3,230줄 이하) - ---- - -### Phase 3: 추가 탭 컴포넌트 분리 (254줄 절감 목표) ⭐ 순서 변경 - -#### 3-1. 섹션 탭 분리 -- [x] **SectionsTab.tsx** (239줄) - line 2878-3117 - ✅ **완료** - - [x] 컴포넌트 추출 ✅ - - [x] Props 정의 ✅ - - [x] 메인 파일 교체 ✅ - - [x] tabs/index.ts export 추가 ✅ - - [x] 빌드 테스트 ✅ - -#### 3-2. 아이템 탭 분리 -- [x] **MasterFieldTab.tsx** (558줄) - ✅ **Phase 1에서 이미 완료** - - [x] 컴포넌트 추출 (Phase 1 완료) - - [x] Props 정의 (Phase 1 완료) - - [x] 메인 파일 교체 (Phase 1 완료) - - ℹ️ ItemsTab은 MasterFieldTab으로 이미 분리됨 - -#### 3-3. Phase 3 완료 검증 -- [x] 탭 컴포넌트 분리 완료 ✅ (SectionsTab + MasterFieldTab) -- [ ] 빌드 성공 확인 -- [ ] **현재 파일 크기 확인** (목표: ~3,000줄 이하) - ---- - -### Phase 4: Utils & Hooks 통합 분리 (900줄 절감 목표) ⭐ Phase 통합 - -#### 4-1. Utils 분리 -- [x] `utils/` 디렉토리 생성 ✅ -- [x] **pathUtils.ts** ✅ **완료** - - [x] generateAbsolutePath() 이동 ✅ - - [x] getItemTypeLabel() 추가 ✅ - - [x] 메인 파일에서 import 적용 ✅ -- [ ] **fieldUtils.ts** ⏸️ **주말 작업으로 연기** - - [ ] generateFieldKey() 이동 - - [ ] findFieldByKey() 이동 - - [ ] 필드 관련 helper 함수들 이동 -- [ ] **sectionUtils.ts** ⏸️ **주말 작업으로 연기** - - [ ] moveSection() 이동 - - [ ] 섹션 관련 helper 함수들 이동 -- [ ] **validationUtils.ts** ⏸️ **주말 작업으로 연기** - - [ ] validateField() 이동 - - [ ] 유효성 검증 함수들 이동 - -#### 4-2. Hooks 분리 ⏸️ **주말 작업으로 연기** -- [ ] `hooks/` 디렉토리 생성 ⏸️ **주말 작업** -- [ ] **usePageManagement.ts** ⏸️ **주말 작업** - - [ ] handleAddPage, handleDeletePage, handleUpdatePage 등 - - [ ] 관련 state 및 handler 5개 이동 -- [ ] **useSectionManagement.ts** ⏸️ **주말 작업** - - [ ] handleAddSection, handleDeleteSection 등 - - [ ] 관련 state 및 handler 8개 이동 -- [ ] **useFieldManagement.ts** ⏸️ **주말 작업** - - [ ] handleAddField, handleEditField 등 - - [ ] 관련 state 및 handler 10개 이동 -- [ ] **useTemplateManagement.ts** ⏸️ **주말 작업** - - [ ] handleSaveTemplate, handleLoadTemplate 등 - - [ ] 관련 state 및 handler 6개 이동 -- [ ] **useTabManagement.ts** ⏸️ **주말 작업** - - [ ] handleAddTab, handleDeleteTab 등 - - [ ] 관련 state 및 handler 6개 이동 - -#### 4-3. Phase 4 Utils 부분 완료 검증 -- [x] pathUtils 분리 완료 ✅ -- [x] 메인 파일에서 import 적용 ✅ -- [ ] **Hooks 분리는 주말 작업으로 연기** ⏸️ -- [ ] **빌드 성공 확인** (다음 작업) -- [ ] **최종 파일 크기 확인** (목표: ~1,300줄 이하 - Hooks 완료 후) - ---- - -### 최종 검증 체크리스트 - -- [ ] **메인 파일 크기**: 1,500줄 이하 달성 -- [ ] **TypeScript 에러**: 0개 -- [ ] **빌드 에러**: 0개 -- [ ] **ESLint 경고**: 최소화 -- [ ] **기능 테스트**: 모든 다이얼로그 정상 동작 -- [ ] **탭 테스트**: 모든 탭 전환 정상 동작 -- [ ] **데이터 저장**: localStorage 정상 동작 -- [ ] **코드 리뷰**: 가독성 향상 확인 - ---- - -## 📝 작업 이력 (날짜별) - -### 2025-11-18 (오전) -- ✅ CategoryTab 분리 완료 (40줄) -- ✅ MasterFieldTab 분리 완료 (558줄) -- ✅ HierarchyTab 분리 완료 (43줄) -- ✅ 분리 계획 문서 작성 완료 -- ✅ 체크리스트 기반 작업 문서로 업데이트 - -### 2025-11-18 (오후) - Phase 1 Dialog 분리 완료 ✅ -- ✅ dialogs/ 디렉토리 생성 완료 -- ✅ **FieldDialog.tsx** 분리 완료 (462줄 절감) - 빌드 테스트 통과 -- ✅ **FieldDrawer.tsx** 분리 완료 (462줄 절감) - 빌드 테스트 통과 -- ✅ **TabManagementDialogs.tsx** 분리 완료 (265줄 절감) - 6개 다이얼로그 통합 -- ✅ **OptionDialog.tsx** 분리 완료 (122줄 절감) -- ✅ **ColumnManageDialog.tsx** 분리 완료 (119줄 절감) -- ✅ **PathEditDialog.tsx, PageDialog.tsx, SectionDialog.tsx** 분리 완료 (95줄 절감) -- ✅ **MasterFieldDialog.tsx** 분리 완료 (148줄 절감) -- ✅ **TemplateFieldDialog.tsx** 분리 완료 (113줄 절감) -- ✅ **SectionTemplateDialog.tsx** 분리 완료 (78줄 절감) -- ✅ **LoadTemplateDialog.tsx** 분리 완료 (74줄 절감) -- ✅ **ColumnDialog.tsx** 분리 완료 (48줄 절감) -- 📊 **최종 상태**: 5,231줄 → 3,254줄 (1,977줄 절감, 37.8%) -- 🎉 **Phase 1 완료!** 목표 ~2,900줄 이하 달성 (3,254줄) - -### 2025-11-18 (저녁) - Phase 순서 재조정 및 Phase 2 조사 완료 ⭐ -- 📋 **Phase 순서 변경 결정**: 효율성 극대화를 위해 순서 조정 - - **Phase 2**: Utils → **Types 분리** (빠른 효과, 다른 Phase 기반) - - **Phase 3**: Types → **Tabs 분리** (가시적 효과) - - **Phase 4**: Tabs/Hooks → **Utils + Hooks 통합** (대규모 정리) -- 🔍 **Phase 2 범위 조사 완료**: - - 초기 예상: 200줄 → 실제: 25줄 (로컬 타입 3개만 존재) - - 주요 타입들은 이미 ItemMasterContext에서 import 중 - - 분리 대상: ItemCategoryStructure, OptionColumn, MasterOption -- ✅ COMPONENT_SEPARATION_PLAN.md 문서 업데이트 완료 (정확한 Phase 2 범위 반영) - ---- - -### 🎯 세션 체크포인트 (2025-11-18 종료) - -#### ✅ 완료된 작업 -- **Phase 1 완전 완료**: 13개 다이얼로그 분리 -- **파일 크기 절감**: 5,231줄 → 3,254줄 (1,977줄 절감, 37.8%) -- **Phase 순서 최적화**: 효율성 기반 순서 재조정 완료 -- **Phase 2 사전 조사**: 실제 범위 확인 및 문서 업데이트 - -#### 📋 다음 세션 시작 시 작업 -1. **Phase 2: Types 분리** (25줄 절감 목표) - - types.ts 파일 생성 - - ItemCategoryStructure, OptionColumn, MasterOption 추출 - - 메인 파일에서 import 수정 - - 빌드 테스트 - -2. **Phase 3: Tabs 분리** (254줄 절감 목표) - - SectionsTab.tsx (242줄) - - ItemsTab.tsx (12줄) - -3. **Phase 4: Utils + Hooks 통합 분리** (900줄 절감 목표) - -#### 📊 현재 상태 -- **메인 파일**: 3,254줄 -- **분리된 컴포넌트**: 13개 다이얼로그, 3개 탭 -- **최종 목표까지**: 약 2,000줄 추가 절감 필요 - -#### 💾 세션 재개 명령 -```bash -# 다음 세션 시작 시: -1. COMPONENT_SEPARATION_PLAN.md 확인 -2. Phase 2 체크리스트부터 시작 -3. 문서의 "### Phase 2: 타입 정의 분리" 섹션 참고 -``` - ---- - -### 🚀 **다음 작업**: Phase 2 (Types 분리) - 내일 시작 예정 - ---- - -## 🔄 세션 재개 가이드 - -**세션이 중단되었을 때 이 문서를 기준으로 작업 재개:** - -1. 위 체크리스트에서 **체크되지 않은 첫 번째 항목** 찾기 -2. 해당 항목의 **line 번호**와 **예상 라인 수** 확인 -3. `ItemMasterDataManagement.tsx` 파일에서 해당 섹션 Read -4. 새 파일 생성 및 컴포넌트 추출 -5. Props 인터페이스 정의 -6. 메인 파일에서 해당 부분을 import로 교체 -7. 빌드 테스트 (`npm run build`) -8. 체크리스트 업데이트 (체크 표시) -9. 다음 항목으로 이동 - -**현재 진행 상태**: Phase 0 완료, Phase 1 시작 대기 - ---- - -## 💡 주의사항 - -### Props Drilling 방지 -- Context API 또는 Zustand 활용 고려 -- 현재 ItemMasterContext가 있으므로 최대한 활용 - -### 타입 안정성 유지 -- 모든 분리된 컴포넌트에 명확한 Props 타입 정의 -- types.ts에서 중앙 관리 - -### 재사용성 고려 -- Dialog 컴포넌트는 독립적으로 재사용 가능하게 -- Utils는 순수 함수로 작성 - -### 테스트 필요성 -- 각 분리 단계마다 빌드 테스트 필수 -- 기능 동작 검증 필요 - ---- - -## 🎯 성공 기준 - -1. ✅ 메인 파일 크기 1,500줄 이하 달성 -2. ✅ 빌드 에러 없음 -3. ✅ 모든 기능 정상 동작 -4. ✅ 타입 에러 없음 -5. ✅ 코드 가독성 향상 - ---- - -**문서 버전**: 1.0 -**마지막 업데이트**: 2025-11-18 \ No newline at end of file diff --git a/claudedocs/archive/[REF-2025-11-18] cleanup-summary.md b/claudedocs/archive/[REF-2025-11-18] cleanup-summary.md deleted file mode 100644 index f425f488..00000000 --- a/claudedocs/archive/[REF-2025-11-18] cleanup-summary.md +++ /dev/null @@ -1,243 +0,0 @@ -# 미사용 파일 정리 완료 보고서 - -**작업 일시**: 2025-11-18 -**작업 범위**: 미사용 Context 파일 및 컴포넌트 정리 - ---- - -## ✅ 작업 완료 내역 - -### Phase 1: 미사용 Context 8개 정리 - -#### 이동된 파일 (contexts/_unused/) -1. FacilitiesContext.tsx -2. AccountingContext.tsx -3. HRContext.tsx -4. ShippingContext.tsx -5. InventoryContext.tsx -6. ProductionContext.tsx -7. PricingContext.tsx -8. SalesContext.tsx - -#### 수정된 파일 -- **RootProvider.tsx** - - 8개 Context import 제거 - - Provider 중첩 10개 → 2개로 단순화 - - 현재 사용: AuthProvider, ItemMasterProvider만 유지 - - 주석 업데이트로 미사용 Context 목록 명시 - -#### 이동된 컴포넌트 -- **BOMManager.tsx** → `components/_unused/business/` - - 485 라인의 구형 컴포넌트 - - BOMManagementSection으로 대체됨 - -#### 빌드 검증 -- ✅ `npm run build` 성공 -- ✅ 모든 페이지 정상 빌드 (36개 라우트) -- ✅ 에러 없음 - ---- - -### Phase 2: DeveloperModeContext 정리 - -#### 이동된 파일 -- **DeveloperModeContext.tsx** → `contexts/_unused/` - - Provider는 연결되어 있었으나 실제 devMetadata 기능 미사용 - - 향후 필요 시 복원 가능 - -#### 수정된 파일 -1. **src/app/[locale]/(protected)/layout.tsx** - - DeveloperModeProvider import 제거 - - Provider 래핑 제거 - - 주석 업데이트 - -2. **src/components/organisms/PageLayout.tsx** - - useDeveloperMode import 제거 - - devMetadata prop 제거 - - useEffect 및 관련 로직 제거 - - ComponentMetadata interface 의존성 제거 - -#### 빌드 검증 -- ✅ `npm run build` 성공 -- ✅ 모든 페이지 정상 빌드 -- ✅ 에러 없음 - ---- - -### Phase 3: .gitignore 업데이트 - -#### 추가된 항목 -```gitignore -# ---> Unused components and contexts (archived) -src/components/_unused/ -src/contexts/_unused/ -``` - -**효과**: _unused 디렉토리가 git 추적에서 제외됨 - ---- - -## 📊 정리 결과 - -### 파일 구조 (Before → After) - -**src/contexts/ (Before)** -``` -contexts/ -├── AuthContext.tsx ✅ -├── FacilitiesContext.tsx ❌ -├── AccountingContext.tsx ❌ -├── HRContext.tsx ❌ -├── ShippingContext.tsx ❌ -├── InventoryContext.tsx ❌ -├── ProductionContext.tsx ❌ -├── PricingContext.tsx ❌ -├── SalesContext.tsx ❌ -├── ItemMasterContext.tsx ✅ -├── ThemeContext.tsx ✅ -├── DeveloperModeContext.tsx ❌ -├── RootProvider.tsx (10개 Provider 중첩) -└── DataContext.tsx.backup -``` - -**src/contexts/ (After)** -``` -contexts/ -├── AuthContext.tsx ✅ (사용 중) -├── ItemMasterContext.tsx ✅ (사용 중) -├── ThemeContext.tsx ✅ (사용 중) -├── RootProvider.tsx (2개 Provider만 유지) -├── DataContext.tsx.backup -└── _unused/ (git 무시) - ├── FacilitiesContext.tsx - ├── AccountingContext.tsx - ├── HRContext.tsx - ├── ShippingContext.tsx - ├── InventoryContext.tsx - ├── ProductionContext.tsx - ├── PricingContext.tsx - ├── SalesContext.tsx - └── DeveloperModeContext.tsx -``` - -### 코드 감소량 - -| 항목 | Before | After | 감소량 | -|------|--------|-------|--------| -| Context Provider 중첩 | 10개 | 2개 | -8개 (80% 감소) | -| RootProvider.tsx | 81 lines | 48 lines | -33 lines | -| Active Context 파일 | 13개 | 4개 | -9개 | -| 미사용 코드 | ~3,000 lines | 0 lines | ~3,000 lines | - -### 성능 개선 - -1. **앱 초기화 속도** - - Provider 중첩 10개 → 2개 - - 불필요한 Context 초기화 제거 - -2. **번들 크기** - - Tree-shaking으로 미사용 코드 제거 - - First Load JS 유지: ~102 kB (변화 없음, 원래 사용 안했으므로) - -3. **유지보수성** - - 코드베이스 명확성 증가 - - 혼란 방지 (어떤 Context를 사용하는지 명확) - ---- - -## 🎯 현재 활성 Context - -### 1. AuthContext.tsx -**용도**: 사용자 인증 및 권한 관리 -**상태 수**: 2개 (users, currentUser) -**사용처**: LoginPage, SignupPage, useAuth hook - -### 2. ItemMasterContext.tsx -**용도**: 품목 마스터 데이터 관리 -**상태 수**: 13개 (itemMasters, specificationMasters, etc.) -**사용처**: ItemMasterDataManagement - -### 3. ThemeContext.tsx -**용도**: 다크모드/라이트모드 테마 관리 -**사용처**: DashboardLayout, ThemeSelect - -### 4. RootProvider.tsx -**용도**: 전역 Context 통합 -**Provider**: AuthProvider, ItemMasterProvider - ---- - -## 📁 _unused 디렉토리 관리 - -### 위치 -- `src/contexts/_unused/` (9개 Context 파일) -- `src/components/_unused/` (43개 구형 컴포넌트) - -### Git 설정 -- ✅ .gitignore에 추가됨 -- ✅ 버전 관리에서 제외 -- ✅ 로컬에만 보관 (팀원과 공유 안됨) - -### 복원 방법 -필요 시 다음 단계로 복원 가능: - -1. **파일 이동** - ```bash - mv src/contexts/_unused/SalesContext.tsx src/contexts/ - ``` - -2. **RootProvider.tsx 수정** - ```typescript - import { SalesProvider } from './SalesContext'; - - // Provider 추가 - - {/* ... */} - - ``` - -3. **빌드 검증** - ```bash - npm run build - ``` - ---- - -## ⚠️ 주의사항 - -### 향후 기능 추가 시 - -**미사용 Context를 사용해야 하는 경우:** -1. _unused에서 필요한 Context 복원 -2. RootProvider에 Provider 추가 -3. 필요한 페이지/컴포넌트에서 hook 사용 -4. 빌드 및 테스트 - -**새로운 Context 추가 시:** -1. 새 Context 파일 생성 -2. RootProvider에 Provider 추가 -3. SSR-safe 패턴 준수 (localStorage 접근 시) - ---- - -## 📝 관련 문서 - -- [UNUSED_FILES_REPORT.md](./UNUSED_FILES_REPORT.md) - 미사용 파일 분석 보고서 -- [SSR_HYDRATION_FIX.md](./SSR_HYDRATION_FIX.md) - SSR Hydration 에러 해결 - ---- - -## ✨ 작업 요약 - -**정리된 항목**: 10개 파일 (Context 9개 + 컴포넌트 1개) -**수정된 파일**: 4개 (RootProvider, layout, PageLayout, .gitignore) -**빌드 검증**: 2회 성공 (Phase 1, Phase 2) -**코드 감소**: ~3,000 라인 -**Provider 감소**: 80% (10개 → 2개) - -**결과**: -- ✅ 코드베이스 단순화 완료 -- ✅ 유지보수성 향상 -- ✅ 성능 개선 (Provider 초기화 감소) -- ✅ 향후 복원 가능 (_unused 보관) -- ✅ 빌드 에러 없음 \ No newline at end of file diff --git a/claudedocs/archive/[REF-2025-11-18] unused-files-report.md b/claudedocs/archive/[REF-2025-11-18] unused-files-report.md deleted file mode 100644 index 60f3ea0b..00000000 --- a/claudedocs/archive/[REF-2025-11-18] unused-files-report.md +++ /dev/null @@ -1,248 +0,0 @@ -# 미사용 파일 분석 보고서 - -## 📊 요약 - -**총 미사용 파일: 51개** -- Context 파일: 8개 (전혀 사용 안함) -- Active 컴포넌트: 1개 (BOMManager.tsx) -- 부분 사용: 1개 (DeveloperModeContext.tsx) -- 이미 정리됨: 42개 (components/_unused/) - -## 🔴 완전 미사용 파일 (삭제 권장) - -### Context 파일 (8개) -모두 `RootProvider.tsx`에만 포함되어 있고, 실제 페이지/컴포넌트에서는 전혀 사용되지 않음 - -| 파일명 | 경로 | 사용처 | 상태 | -|--------|------|--------|------| -| FacilitiesContext.tsx | src/contexts/ | RootProvider만 | ❌ 미사용 | -| AccountingContext.tsx | src/contexts/ | RootProvider만 | ❌ 미사용 | -| HRContext.tsx | src/contexts/ | RootProvider만 | ❌ 미사용 | -| ShippingContext.tsx | src/contexts/ | RootProvider만 | ❌ 미사용 | -| InventoryContext.tsx | src/contexts/ | RootProvider만 | ❌ 미사용 | -| ProductionContext.tsx | src/contexts/ | RootProvider만 | ❌ 미사용 | -| PricingContext.tsx | src/contexts/ | RootProvider만 | ❌ 미사용 | -| SalesContext.tsx | src/contexts/ | RootProvider만 | ❌ 미사용 | - -**영향 분석:** -- 이 8개 Context는 React SPA에서 있었던 것으로 추정 -- Next.js 마이그레이션 후 관련 페이지가 구현되지 않음 -- `RootProvider.tsx`에서만 import되고 실제 사용은 없음 -- 안전하게 제거 가능 (빌드/런타임 영향 없음) - -### 컴포넌트 (1개) - -| 파일명 | 경로 | 라인수 | 사용처 | 상태 | -|--------|------|--------|--------|------| -| BOMManager.tsx | src/components/items/ | 485 | 없음 | ❌ 미사용 | - -**영향 분석:** -- BOMManagementSection.tsx가 대신 사용됨 (ItemMasterDataManagement에서 사용) -- 485줄의 구형 컴포넌트 -- `_unused/` 디렉토리로 이동 권장 - -## 🟡 부분 사용 파일 (검토 필요) - -### DeveloperModeContext.tsx - -**현재 상태:** -- ✅ Provider는 `(protected)/layout.tsx`에 연결됨 -- ✅ `PageLayout.tsx`에서 import하고 사용 -- ❌ 하지만 실제로 `devMetadata` prop을 전달하는 곳은 없음 - -**사용 분석:** -```typescript -// PageLayout.tsx - devMetadata를 받지만... -export function PageLayout({ devMetadata, ... }) { - const { setCurrentMetadata } = useDeveloperMode(); - - useEffect(() => { - if (devMetadata) { // 실제로 devMetadata를 전달하는 곳이 없음 - setCurrentMetadata(devMetadata); - } - }, []); -} - -// ItemMasterDataManagement.tsx - 유일하게 PageLayout을 사용 - {/* devMetadata 전달 안함 */} - ... - -``` - -**권장 사항:** -1. **Option 1 (삭제)**: 개발자 모드 기능을 사용하지 않는다면 제거 -2. **Option 2 (활용)**: 개발자 모드 기능이 필요하면 devMetadata 전달 구현 -3. **Option 3 (보류)**: 향후 사용 계획이 있으면 유지 - -## ✅ 정상 사용 파일 - -### Context (3개) -| 파일명 | 사용처 | -|--------|--------| -| AuthContext.tsx | LoginPage, SignupPage, useAuth hook 사용 중 | -| ItemMasterContext.tsx | ItemMasterDataManagement 등에서 사용 중 | -| ThemeContext.tsx | DashboardLayout, ThemeSelect에서 사용 중 | - -### 컴포넌트 -| 파일명 | 사용처 | -|--------|--------| -| FileUpload.tsx | ItemForm.tsx에서 import 및 사용 | -| DrawingCanvas.tsx | ItemForm.tsx에서 사용 (` | null; // NOT displayCondition - validation_rules?: Record | null; - options?: Array<{ label: string; value: string }> | null; - properties?: Record | null; - created_at: string; - updated_at: string; -} -``` - -### 수정 패턴 -- [ ] `field.name` → `field.field_name` -- [ ] `field.displayCondition` → `field.display_condition` -- [ ] `field.order` → `field.order_no` -- [ ] `{ name: x }` → `{ field_name: x }` -- [ ] `{ displayCondition: x }` → `{ display_condition: x }` - -### 주요 위치 -- [ ] Line 783-822: Field 수정/추가 핸들러 -- [ ] Line 906-920: Field 편집 다이얼로그 -- [ ] Line 1437-1447: 템플릿 필드 편집 -- [ ] 기타 필드 관련 핸들러들 - -**완료 후 확인**: ItemField 관련 오류 0개 - ---- - -## Phase 4: 존재하지 않는 속성 제거/수정 - -**목표**: 타입에 정의되지 않은 속성 제거 또는 올바른 속성으로 대체 - -### ItemMasterField 타입 참조 -```typescript -interface ItemMasterField { - id: number; - field_name: string; // NOT name, NOT fieldKey - field_type: 'TEXT' | 'NUMBER' | 'DATE' | 'SELECT' | 'TEXTAREA' | 'CHECKBOX'; - category?: string | null; - description?: string | null; - validation_rules?: Record | null; // NOT default_validation - properties?: Record | null; // NOT property, NOT default_properties - created_at: string; - updated_at: string; -} -``` - -### SectionTemplate 타입 참조 -```typescript -interface SectionTemplate { - id: number; - template_name: string; // NOT title - section_type: 'BASIC' | 'BOM' | 'CUSTOM'; // NOT type - description?: string | null; - default_fields?: Record | null; // NOT fields, NOT bomItems - created_at: string; - updated_at: string; - - // 주의: category, fields, bomItems, isCollapsible, isCollapsed 속성은 존재하지 않음! -} -``` - -### 제거/수정할 속성들 -- [ ] `field.fieldKey` → 제거 또는 `field.field_name` 사용 -- [ ] `field.property` → `field.properties` (복수형!) -- [ ] `field.default_properties` → 제거 (ItemField에 없음) -- [ ] `template.fields` → 제거 (SectionTemplate에 없음) -- [ ] `template.bomItems` → 제거 (SectionTemplate에 없음) -- [ ] `template.category` → 제거 (SectionTemplate에 없음) -- [ ] `template.isCollapsible` → 제거 -- [ ] `template.isCollapsed` → 제거 - -### 주요 위치 -- [ ] Line 226-241: ItemMasterField fieldKey 참조 -- [ ] Line 437-460: property 속성 접근 -- [ ] Line 793: field.property -- [ ] Line 815: field.property -- [ ] Line 831: field.property (여러 곳) -- [ ] Line 910-913: field.default_properties -- [ ] Line 1154, 1157: field.fieldKey -- [ ] Line 1247-1248: template.category, template.type -- [ ] Line 1300-1313: template.fields, template.bomItems -- [ ] Line 1440-1447: field.default_properties -- [ ] Line 2192, 2205: properties 접근 - -**완료 후 확인**: 존재하지 않는 속성 관련 오류 0개 - ---- - -## Phase 5: ID 타입 통일 - -**목표**: 모든 ID를 string에서 number로 통일 - -### 수정할 ID 타입들 -- [ ] `selectedPageId`: `string | null` → `number | null` -- [ ] `editingPageId`: `string | null` → `number | null` -- [ ] `editingFieldId`: `string | null` → `number | null` -- [ ] `editingMasterFieldId`: `string | null` → `number | null` -- [ ] `currentTemplateId`: `string | null` → `number | null` -- [ ] `editingTemplateId`: `string | null` → `number | null` -- [ ] `editingTemplateFieldId`: `string | null` → `number | null` - -### 관련 수정 -- [ ] 모든 ID 비교: `=== 'string'` → `=== number` -- [ ] 함수 파라미터: `(id: string)` → `(id: number)` -- [ ] State setter 호출: 타입 변환 제거 - -### 주요 위치 -- [ ] Line 313: selectedPageIdFromStorage 타입 -- [ ] Line 314: 비교 연산 -- [ ] Line 591, 701, 723, 934, 1147, 1169, 1190, 1289, 1330, 1453, 1487: ID 비교 -- [ ] Line 623: setSelectedPageId -- [ ] Line 906-907: setEditingFieldId, setSelectedPageId -- [ ] Line 1069: setEditingMasterFieldId -- [ ] Line 1105, 1150: deleteItemMasterField ID -- [ ] Line 1178: deleteItemPage ID -- [ ] Line 1244: setCurrentTemplateId -- [ ] Line 1263, 1277, 1419, 1457: Template ID 함수 호출 -- [ ] Line 1437: setEditingTemplateFieldId - -**완료 후 확인**: ID 타입 불일치 오류 0개 - ---- - -## Phase 6: State 타입 수정 - -**목표**: 로컬 state 타입을 타입 정의와 일치시키기 - -### 수정할 State들 -- [ ] `customTabs` ID: `string` → `number` -- [ ] `MasterOption`: `is_active` → `isActive` (로컬 타입은 camelCase 유지) -- [ ] 기타 타입 불일치 state들 - -### 주요 위치 -- [ ] Line 491: MasterOption `is_active` vs `isActive` -- [ ] Line 1014-1017: customAttributeOptions 타입 -- [ ] Line 1371-1374: customAttributeOptions 타입 -- [ ] Line 1465, 1483: BOM ID 타입 -- [ ] Line 1528: customTabs ID 타입 - -**완료 후 확인**: State 타입 불일치 오류 0개 - ---- - -## Phase 7: 함수 시그니처 수정 및 최종 검증 - -**목표**: 컴포넌트 props와 Context 함수 시그니처 일치시키기 - -### 수정할 함수 시그니처들 -- [ ] `handleDeleteMasterField`: `(id: string)` → `(id: number)` -- [ ] `handleDeleteSectionTemplate`: `(id: string)` → `(id: number)` -- [ ] `handleAddBOMItemToTemplate`: 시그니처 확인 -- [ ] `handleUpdateBOMItemInTemplate`: 시그니처 확인 -- [ ] Tab props 시그니처들 - -### 누락된 Props 추가 -- [ ] MasterFieldTab: `hasUnsavedChanges`, `pendingChanges` props -- [ ] HierarchyTab: `trackChange`, `hasUnsavedChanges`, `pendingChanges` props -- [ ] TabManagementDialogs: `setIsAddAttributeTabDialogOpen` prop - -### 주요 위치 -- [ ] Line 2404: MasterFieldTab props -- [ ] Line 2423-2424: BOM 함수 시그니처 -- [ ] Line 2433: HierarchyTab props -- [ ] Line 2435: selectedPage null vs undefined -- [ ] Line 2451-2452: selectedSectionForField 타입 -- [ ] Line 2454: newSectionType 타입 -- [ ] Line 2455: updateItemPage 시그니처 -- [ ] Line 2465: updateSection 시그니처 -- [ ] Line 2494: TabManagementDialogs props -- [ ] Line 2584, 2594: Path 관련 함수 시그니처 -- [ ] Line 2800: SectionTemplate 타입 - -### 기타 수정 -- [ ] Line 598: `section.fields` optional 체크 -- [ ] Line 817: `category` 타입 (string[] → string) -- [ ] Line 1175, 1194: `s.fields`, `sectionToDelete.fields` optional 체크 -- [ ] Line 1302, 1307: Spread types 오류 -- [ ] Line 1413, 1456, 1499, 1500, 1508: `never` 타입 오류 -- [ ] Line 1731: fields optional 체크 - -**완료 후 확인**: -- [ ] 모든 함수 시그니처 일치 -- [ ] 모든 props 타입 일치 -- [ ] 타입 오류 0개 - ---- - -## Phase 8: Import 및 최종 정리 - -**목표**: 불필요한 import 제거 및 코드 정리 - -### 제거할 Import들 -- [ ] Line 43: `Save` (사용하지 않음) - -### 제거할 변수들 -- [ ] Line 103: `clearCache` -- [ ] Line 110: `_itemSections` -- [ ] Line 118: `mounted` -- [ ] Line 126: `isLoading` -- [ ] Line 432: `bomItems` -- [ ] Line 697: `_handleMoveSectionUp` -- [ ] Line 719: `_handleMoveSectionDown` -- [ ] Line 1206-1207: `pageId`, `sectionId` -- [ ] Line 1462: `_handleAddBOMItem` -- [ ] Line 1471: `_handleUpdateBOMItem` -- [ ] Line 1475: `_handleDeleteBOMItem` -- [ ] Line 1512: `_toggleSection` -- [ ] Line 1534: `_handleEditTab` -- [ ] Line 1700: `_getAllFieldsInSection` -- [ ] Line 1739: `handleResetAllData` - -### 기타 정리 -- [ ] 불필요한 주석 제거 -- [ ] 중복 코드 정리 -- [ ] 사용하지 않는 any 타입 수정 - -**완료 후 확인**: ESLint 경고 최소화 - ---- - -## 최종 검증 - -- [ ] `npm run build` 성공 (타입 검증 포함) -- [ ] IDE에서 타입 오류 0개 -- [ ] ESLint 경고 최소화 -- [ ] 기능 테스트 통과 - ---- - -## 진행 기록 - -### 2025-11-21 -- 체크리스트 생성 -- 작업 시작 준비 완료 diff --git a/claudedocs/archive/[REF] code-quality-report.md b/claudedocs/archive/[REF] code-quality-report.md deleted file mode 100644 index 98787af5..00000000 --- a/claudedocs/archive/[REF] code-quality-report.md +++ /dev/null @@ -1,354 +0,0 @@ -# 코드 품질 및 일관성 검사 결과 - -**검사 일자**: 2025-11-07 -**검사자**: Claude Code - -## 📊 전체 요약 - -**프로젝트**: Next.js 15 + TypeScript + next-intl (다국어 지원) -**언어**: TypeScript/TSX -**린트**: ESLint 9 (Next.js config) -**타입 체크**: ✅ 통과 (에러 없음) -**린트 상태**: ⚠️ 12개 문제 (9 errors, 3 warnings) - ---- - -## 🔴 Critical Issues (즉시 수정 필요) - -### 1. **src/lib/api/client.ts** - Type 정의 누락 (5 errors) - -**문제**: -- `RequestInit`, `Response`, `fetch`, `URL` 등 글로벌 타입이 인식되지 않음 -- 브라우저/Node.js 환경 타입 정의 누락 - -**수정 방법**: -```typescript -// 파일 상단에 타입 선언 추가 -/// - -// 또는 tsconfig.json에서 lib 설정 확인 -"lib": ["dom", "dom.iterable", "esnext"] -``` - -**위치**: -- src/lib/api/client.ts:50 - `token` 변수 선언 (case block) -- src/lib/api/client.ts:70 - `RequestInit` 타입 미정의 -- src/lib/api/client.ts:78 - `RequestInit` 타입 미정의 -- src/lib/api/client.ts:88 - `fetch` 미정의 -- src/lib/api/client.ts:139 - `Response` 타입 미정의 - ---- - -### 2. **src/middleware.ts** - 미사용 함수/변수 (2 errors) - -**문제 1**: `isProtectedRoute` 함수 정의되었으나 사용되지 않음 -```typescript -// Line 161 -function isProtectedRoute(pathname: string): boolean { - return AUTH_CONFIG.protectedRoutes.some(route => - pathname.startsWith(route) - ); -} -``` - -**문제 2**: `URL` 글로벌 타입 인식 안됨 -```typescript -// Line 231, 247 -new URL(AUTH_CONFIG.redirects.afterLogin, request.url) -new URL('/login', request.url) -``` - -**수정 방법**: -- `isProtectedRoute` 함수 앞에 `_` 추가 (unused 규칙 준수) 또는 삭제 -- tsconfig.json lib 설정 확인 - -**위치**: -- src/middleware.ts:161 - `isProtectedRoute` 미사용 -- src/middleware.ts:231 - `URL` 타입 미정의 -- src/middleware.ts:247 - `URL` 타입 미정의 - ---- - -### 3. **src/components/auth/LoginPage.tsx** (2 issues) - -**Error**: 미사용 변수 `response` -```typescript -// Line 43 -const response = await sanctumClient.login({ - user_id: userId, - user_pwd: password, -}); -// response 변수가 사용되지 않음 -``` - -**Warning**: `any` 타입 사용 -```typescript -// Line 55 -} catch (err: any) { - // any 대신 구체적인 타입 필요 -} -``` - -**수정 방법**: -```typescript -// Option 1: response 사용하지 않으면 제거 -await sanctumClient.login({ user_id: userId, user_pwd: password }); - -// Option 2: 타입 개선 -} catch (err: unknown) { - const error = err as { status?: number; message?: string }; - // ... -} -``` - -**위치**: -- src/components/auth/LoginPage.tsx:43 - `response` 미사용 -- src/components/auth/LoginPage.tsx:55 - `any` 타입 사용 - ---- - -## 🟡 Warnings (개선 권장) - -### 4. **src/lib/api/auth/token-storage.ts** - any 타입 사용 (2 warnings) - -**위치**: Line 30, 38 - -```typescript -// Line 30, 38 -} catch (e: any) { - // any 대신 unknown 사용 권장 -} -``` - -**개선 방법**: -```typescript -} catch (e: unknown) { - console.error('Token parse error:', e); -} -``` - -**위치**: -- src/lib/api/auth/token-storage.ts:30 - `any` 타입 사용 -- src/lib/api/auth/token-storage.ts:38 - `any` 타입 사용 - ---- - -## ✅ 긍정적인 부분 - -1. **TypeScript 타입 체크 통과** - 타입 시스템이 올바르게 작동 중 -2. **명확한 디렉토리 구조**: - ``` - src/ - ├── app/[locale]/ # Next.js 15 App Router - ├── components/ # 재사용 컴포넌트 - │ ├── ui/ # UI 컴포넌트 (shadcn/ui) - │ └── auth/ # 인증 관련 - ├── contexts/ # React Context - ├── lib/ # 유틸리티/API - │ ├── api/ - │ │ └── auth/ # 인증 API 로직 - │ └── validations/ # Zod 스키마 - └── i18n/ # 다국어 설정 - ``` - -3. **Zod 검증 사용** - 런타임 타입 안전성 확보 -4. **일관된 명명 규칙**: - - 컴포넌트: PascalCase (`LoginPage.tsx`) - - 유틸: camelCase (`auth-config.ts`) - - 상수: UPPER_SNAKE_CASE (`AUTH_CONFIG`) - ---- - -## 🎯 스타일 일관성 - -### ✅ 긍정적 패턴 -- **Import 순서**: 외부 라이브러리 → 내부 모듈 → 컴포넌트 순서 일관됨 -- **"use client" 지시자**: 클라이언트 컴포넌트에 올바르게 적용 -- **경로 별칭**: `@/*` 패턴 일관되게 사용 -- **함수형 컴포넌트**: 모든 컴포넌트가 함수형으로 작성됨 - -### ⚠️ 개선 필요 -1. **하드코딩된 한글 텍스트**: - ```tsx - // SignupPage.tsx:148 -

회원가입

- - // 다국어 지원 누락 (LoginPage는 useTranslations 사용) - ``` - -2. **인라인 스타일 사용**: - ```tsx - // LoginPage.tsx:79 -
- - // Tailwind 클래스 사용 권장: bg-blue-500 - ``` - -3. **주석 처리된 코드**: - ```tsx - // SignupPage.tsx:448-521 - // 대량의 주석 처리된 플랜 선택 UI (73줄) - - // 제거 또는 별도 파일로 분리 권장 - ``` - ---- - -## 🔧 추천 개선 사항 - -### 우선순위 1 (High) - 즉시 수정 -1. ✅ **tsconfig.json** lib 설정 확인 (DOM 타입 포함) -2. ✅ **any 타입 제거** → `unknown` 또는 구체적 타입으로 변경 -3. ✅ **미사용 변수 제거** (response, isProtectedRoute) - -### 우선순위 2 (Medium) - 단기 개선 -4. **하드코딩 텍스트 다국어화**: - ```typescript - // messages/ko.json에 추가 - { - "signup": { - "title": "회원가입", - "companyInfo": "회사 정보를 입력해주세요" - } - } - ``` - -5. **인라인 스타일 → Tailwind 클래스**: - ```tsx - // Before -
- - // After -
- ``` - -6. **주석 처리된 코드 정리**: - - 필요 시 별도 브랜치로 보존 - - 불필요하면 삭제 - -### 우선순위 3 (Low) - 장기 개선 -7. **에러 타입 정의**: - ```typescript - // lib/api/types.ts - export interface ApiError { - status: number; - message: string; - errors?: Record; - code?: string; - } - ``` - -8. **ESLint 규칙 커스터마이징**: - ```json - // .eslintrc.json 생성 - { - "extends": "next/core-web-vitals", - "rules": { - "@typescript-eslint/no-unused-vars": ["error", { - "argsIgnorePattern": "^_" - }] - } - } - ``` - ---- - -## 📈 메트릭스 - -| 항목 | 상태 | 점수 | -|------|------|------| -| TypeScript 타입 체크 | ✅ 통과 | 100% | -| ESLint 오류 | ⚠️ 9개 | 65% | -| 코드 구조 | ✅ 우수 | 90% | -| 명명 규칙 | ✅ 일관됨 | 95% | -| 다국어 적용 | ⚠️ 부분적 | 75% | -| 스타일 일관성 | ✅ 양호 | 85% | - -**전체 코드 품질**: **82/100** (양호) - ---- - -## 🚀 빠른 수정 가이드 - -```bash -# 1. tsconfig.json 확인 (이미 올바르게 설정됨) -cat tsconfig.json | grep -A5 "lib" - -# 2. ESLint 오류 확인 -npm run lint - -# 3. 자동 수정 가능한 항목 수정 -npm run lint -- --fix - -# 4. TypeScript 타입 체크 -npx tsc --noEmit -``` - ---- - -## 📋 상세 에러 목록 - -### ESLint Errors (9개) - -1. **src/components/auth/LoginPage.tsx:43:13** - - `response` is assigned a value but never used - - Rule: `@typescript-eslint/no-unused-vars` - -2. **src/lib/api/client.ts:50:9** - - Unexpected lexical declaration in case block - - Rule: `no-case-declarations` - -3. **src/lib/api/client.ts:70:15** - - `RequestInit` is not defined - - Rule: `no-undef` - -4. **src/lib/api/client.ts:78:19** - - `RequestInit` is not defined - - Rule: `no-undef` - -5. **src/lib/api/client.ts:88:28** - - `fetch` is not defined - - Rule: `no-undef` - -6. **src/lib/api/client.ts:139:39** - - `Response` is not defined - - Rule: `no-undef` - -7. **src/middleware.ts:161:10** - - `isProtectedRoute` is defined but never used - - Rule: `@typescript-eslint/no-unused-vars` - -8. **src/middleware.ts:231:40** - - `URL` is not defined - - Rule: `no-undef` - -9. **src/middleware.ts:247:21** - - `URL` is not defined - - Rule: `no-undef` - -### ESLint Warnings (3개) - -1. **src/components/auth/LoginPage.tsx:55:19** - - Unexpected any. Specify a different type - - Rule: `@typescript-eslint/no-explicit-any` - -2. **src/lib/api/auth/token-storage.ts:30:17** - - Unexpected any. Specify a different type - - Rule: `@typescript-eslint/no-explicit-any` - -3. **src/lib/api/auth/token-storage.ts:38:14** - - Unexpected any. Specify a different type - - Rule: `@typescript-eslint/no-explicit-any` - ---- - -## 💡 결론 - -프로젝트는 전반적으로 **양호한 품질**을 유지하고 있으나, 위 9개 ESLint 오류를 수정하면 더욱 견고한 코드베이스가 될 것입니다. - -주요 개선 포인트: -1. 타입 정의 완성도 향상 (no-undef 에러 해결) -2. any 타입 제거로 타입 안전성 강화 -3. 미사용 변수/함수 정리로 코드 가독성 향상 -4. 다국어 지원 일관성 개선 -5. 스타일 일관성 유지 (인라인 스타일 제거) \ No newline at end of file diff --git a/claudedocs/archive/[REF] communication_improvement_guide.md b/claudedocs/archive/[REF] communication_improvement_guide.md deleted file mode 100644 index 6228035b..00000000 --- a/claudedocs/archive/[REF] communication_improvement_guide.md +++ /dev/null @@ -1,292 +0,0 @@ -# Claude Code 커뮤니케이션 개선 가이드 - -**작성일**: 2025-11-06 -**적용 범위**: 모든 세션 -**목적**: Claude와 사용자 간 효율적 커뮤니케이션 프로토콜 - ---- - -## 📊 Claude 응답 패턴 분석 및 개선 - -### 1️⃣ 식별된 문제점 - -#### 🔴 과도한 설명 (Over-explanation) -**문제**: 간단한 질문에도 긴 설명 + 예시 + 대안 + 원리까지 -**원인**: 사용자 의도 파악 전에 모든 가능성 커버하려는 습관 -**개선**: 핵심 답변 먼저 → 필요시 추가 설명 제공 - -**예시**: -``` -❌ 현재 방식: -Q: "이 함수 뭐하는 거야?" -A: [함수 설명 500자] + [동작 원리] + [사용 예시] + [대안] + [최적화 팁] - -✅ 개선 방식: -Q: "이 함수 뭐하는 거야?" -A: "사용자 인증 토큰 검증. 만료 체크 + 서명 확인. - 더 알고 싶으신 부분 있나요? (원리/사용법/대안)" -``` - -#### 🟡 불필요한 TodoWrite 남발 -**문제**: 간단한 작업도 TodoWrite 생성 → 오버헤드 -**원인**: MODE_Task_Management의 ">3 steps" 기준 오해석 -**개선**: 진짜 복잡한 작업만 TodoWrite 사용 - -**예시**: -``` -❌ 불필요한 경우: -- 단일 파일 읽기 -- 간단한 변수명 변경 -- 한 줄 수정 - -✅ 필요한 경우: -- 다중 파일 리팩토링 (>5개) -- 다단계 구현 (인증 시스템 전체) -- 의존성 있는 순차 작업 -``` - -#### 🟡 선제적 도구 제안 (Premature Tool Suggestion) -**문제**: "이것도 해드릴까요?" 식의 불필요한 제안 -**원인**: 도움이 되려는 과도한 의욕 -**개선**: 요청받은 것만 정확히 수행 - -**예시**: -``` -❌ 현재: -A: "로그인 폼 만들었습니다. - 회원가입도 만들어드릴까요? - 비밀번호 찾기도 추가할까요?" - -✅ 개선: -A: "로그인 폼 완료. src/components/auth/LoginForm.tsx - 추가 필요하신 기능 있으면 말씀해주세요." -``` - -#### 🔴 모호한 질문에 대한 가정 기반 답변 -**문제**: 불명확한 질문에 확인 없이 추측으로 답변 -**원인**: 빠른 응답 시도 → 잘못된 방향 -**개선**: AskUserQuestion 활용해 명확화 먼저 - -**예시**: -``` -❌ 현재: -Q: "이거 고쳐줘" -A: [맥락 추측해서 A를 수정함 → 실제로는 B를 원했음] - -✅ 개선: -Q: "이거 고쳐줘" -A: "무엇을 수정하실까요? - 1) 성능 문제 - 2) 버그 수정 - 3) UI 개선 - 4) 기능 변경" -``` - ---- - -## 🎯 사용자 질문 패턴 분석 - -### ✅ 잘하고 계신 점 -1. **직관적 의사소통**: "이거", "저거" → 맥락 파악 가능한 수준 -2. **점진적 상세화**: 첫 질문 간단 → 필요시 구체화 -3. **자연스러운 대화**: 형식보다 내용 중심 - -### ⚠️ 개선 가능한 부분 - -#### 1. 파일 경로 명시 부족 -``` -현재: "이 코드 분석해줘" -개선: "src/app/page.tsx 분석해줘" -``` - -#### 2. 범위 지정 누락 -``` -현재: "에러 고쳐줘" -개선: "빌드 에러 고쳐줘" or "런타임 에러 고쳐줘" -``` - -#### 3. 우선순위 미명시 -``` -현재: "A, B, C 해줘" -개선: "A 먼저, 그 다음 B, C는 나중에" -``` - ---- - -## 💡 상호 개선 제안 - -### 🔹 Claude가 개선할 것 - -#### 1. 간결성 우선 (Concise-First) -```yaml -원칙: - - 핵심 답변 먼저 (2-3문장) - - "더 알고 싶으면" 선택지 제공 - - 긴 설명은 명시적 요청 시에만 - -적용: - - 간단한 질문 → 짧은 답변 - - 복잡한 질문 → 구조화된 답변 + 요약 -``` - -#### 2. 명확화 우선 (Clarify-First) -```yaml -원칙: - - 모호함 감지 → 즉시 AskUserQuestion - - 가정 기반 진행 금지 - - 2가지 이상 해석 가능 → 선택지 제시 - -트리거: - - "이거", "저거" + 맥락 불충분 - - 범위 불명확 (파일? 모듈? 프로젝트?) - - 목적 불명확 (분석? 수정? 삭제?) -``` - -#### 3. 작업 범위 확인 (Scope-Check) -```yaml -원칙: - - 큰 작업 시작 전 범위 확인 - - 예상 영향 파일/시간 사전 공유 - - 승인 후 진행 - -예시: - "이 작업은 12개 파일 수정 예상 (약 10분). - 진행할까요?" -``` - -#### 4. 결과물 우선 (Outcome-First) -```yaml -원칙: - - 작업 완료 → 결과 먼저 보고 - - 과정 설명은 필요시에만 - - 파일 경로 + 변경사항 요약 - -템플릿: - "✅ 완료: [핵심 결과] - 변경: [파일1:라인] [파일2:라인] - 테스트: [검증 결과]" -``` - ---- - -### 🔹 사용자가 고려할 수 있는 것 - -#### 1. 컨텍스트 첨부 습관 -``` -현재: "이 에러 뭐야?" -개선: "app/api/auth/route.ts:45에서 TypeError 발생. 뭐야?" - -효과: 즉시 파일 확인 가능 → 왕복 질문 감소 -``` - -#### 2. 기대 결과물 언급 -``` -현재: "로그인 만들어줘" -개선: "로그인 만들어줘. 폼만 있으면 돼 (API 연동 X)" - -효과: 불필요한 구현 방지 → 시간 절약 -``` - -#### 3. 긴급도 표시 -``` -현재: "A, B, C 해줘" -개선: "A(급함), B(보통), C(나중에)" - -효과: 우선순위 기반 작업 → 효율 증가 -``` - -#### 4. 제약사항 사전 공유 -``` -현재: "디자인 바꿔줘" → [작업 완료] → "아 이건 바꾸면 안 되는데..." -개선: "디자인 바꿔줘. 단, 헤더 높이는 유지" - -효과: 재작업 방지 -``` - ---- - -## 🎯 실전 커뮤니케이션 프로토콜 - -### Phase 1: 간단한 작업 (< 5분) -``` -사용자: "src/utils/format.ts의 formatDate 함수 버그 수정" -Claude: [파일 확인] → [수정] → "✅ 완료: ISO 8601 포맷 지원 추가" -``` - -### Phase 2: 중간 작업 (5-15분) -``` -사용자: "로그인 폼 만들어줘" -Claude: "확인: - - 이메일/비밀번호 필드 - - 클라이언트 검증만 (API 연동 X) - - 기존 Input 컴포넌트 사용 - 맞나요?" -사용자: "응, API 연동도 해줘" -Claude: "알겠습니다. 예상 15분, 진행할게요." - [작업 진행 + TodoWrite] -``` - -### Phase 3: 복잡한 작업 (> 15분) -``` -사용자: "인증 시스템 전체 구현" -Claude: [AskUserQuestion 실행] - "범위 확인: - 1) JWT vs Session 방식? - 2) 소셜 로그인 필요? - 3) 권한 관리 포함? - 4) 리프레시 토큰?" -사용자: [선택] -Claude: [Plan 작성 + TodoWrite + 승인 요청] -사용자: "OK" -Claude: [실행] -``` - ---- - -## 📋 빠른 체크리스트 - -### Claude 답변 전 체크리스트 -- [ ] 질문이 명확한가? → 아니면 AskUserQuestion -- [ ] 파일/범위 확인 가능한가? -- [ ] 가정이 필요한가? → 필요하면 확인 -- [ ] 작업 시간 > 5분? → 범위 사전 공유 -- [ ] TodoWrite 진짜 필요한가? → 단순 작업은 스킵 - -### 사용자 질문 전 체크리스트 (선택사항) -- [ ] 파일 경로 명시 가능? -- [ ] 범위 명확? (파일/모듈/프로젝트) -- [ ] 기대 결과 명확? -- [ ] 제약사항 있음? -- [ ] 우선순위 있음? - ---- - -## 🎬 실험 모드 (1주일) - -### 적용 방침 -**Claude**: -- 모호하면 즉시 질문 (가정 금지) -- 답변 간결화 (핵심 우선) -- TodoWrite 최소화 (진짜 복잡한 것만) - -**사용자**: -- 가능하면 파일 경로 포함 -- 범위/우선순위 명시 (필요시) - -### 1주 후 평가 -- 효과 측정 -- 불편한 점 수집 -- 프로토콜 조정 - ---- - -## 📌 핵심 원칙 요약 - -1. **간결성**: 핵심 먼저, 상세는 나중 -2. **명확성**: 모호하면 물어보기 -3. **효율성**: 필요한 것만, 정확하게 -4. **투명성**: 예상 범위/시간 사전 공유 -5. **유연성**: 피드백 기반 지속 개선 - -**적용 시작일**: 2025-11-06 -**다음 리뷰**: 2025-11-13 \ No newline at end of file diff --git a/claudedocs/archive/[REF] component-usage-analysis.md b/claudedocs/archive/[REF] component-usage-analysis.md deleted file mode 100644 index ee0ab96c..00000000 --- a/claudedocs/archive/[REF] component-usage-analysis.md +++ /dev/null @@ -1,444 +0,0 @@ -# 컴포넌트 사용 분석 리포트 - -생성일: 2025-11-12 -프로젝트: sam-react-prod - -## 📋 요약 - -- **총 컴포넌트 수**: 50개 -- **실제 사용 중**: 8개 -- **미사용 컴포넌트**: 42개 (84%) -- **중복 파일**: 2개 (LoginPage.tsx, SignupPage.tsx) - ---- - -## ✅ 1. 실제 사용 중인 컴포넌트 - -### 1.1 인증 컴포넌트 (src/components/auth/) -| 컴포넌트 | 사용 위치 | 상태 | -|---------|---------|------| -| **LoginPage.tsx** | `src/app/[locale]/login/page.tsx` | ✅ 사용 중 | -| **SignupPage.tsx** | `src/app/[locale]/signup/page.tsx` | ✅ 사용 중 | - -**의존성**: -- `LanguageSelect` (src/components/LanguageSelect.tsx) -- `ThemeSelect` (src/components/ThemeSelect.tsx) - ---- - -### 1.2 비즈니스 컴포넌트 (src/components/business/) -| 컴포넌트 | 사용 위치 | 상태 | -|---------|---------|------| -| **Dashboard.tsx** | `src/app/[locale]/(protected)/dashboard/page.tsx` | ✅ 사용 중 | - -**Dashboard.tsx의 lazy-loaded 의존성** (간접 사용 중): -- `CEODashboard.tsx` → Dashboard에서 lazy import -- `ProductionManagerDashboard.tsx` → Dashboard에서 lazy import -- `WorkerDashboard.tsx` → Dashboard에서 lazy import -- `SystemAdminDashboard.tsx` → Dashboard에서 lazy import - ---- - -### 1.3 레이아웃 컴포넌트 (src/components/layout/) -| 컴포넌트 | 사용 위치 | 상태 | -|---------|---------|------| -| **Sidebar.tsx** | `src/layouts/DashboardLayout.tsx` | ✅ 사용 중 | - ---- - -### 1.4 공통 컴포넌트 (src/components/common/) -| 컴포넌트 | 사용 위치 | 상태 | -|---------|---------|------| -| **EmptyPage.tsx** | `src/app/[locale]/(protected)/[...slug]/page.tsx` | ✅ 사용 중 | - -**용도**: 미구현 페이지의 폴백(fallback) UI - ---- - -### 1.5 루트 레벨 컴포넌트 (src/components/) -| 컴포넌트 | 사용 위치 | 상태 | -|---------|---------|------| -| **LanguageSelect.tsx** | `LoginPage.tsx`, `SignupPage.tsx` | ✅ 사용 중 | -| **ThemeSelect.tsx** | `LoginPage.tsx`, `SignupPage.tsx`, `DashboardLayout.tsx` | ✅ 사용 중 | - -| 컴포넌트 | 상태 | 비고 | -|---------|------|------| -| **WelcomeMessage.tsx** | ❌ 미사용 | 삭제 가능 | -| **NavigationMenu.tsx** | ❌ 미사용 | 삭제 가능 | -| **LanguageSwitcher.tsx** | ❌ 미사용 | LanguageSelect로 대체됨 | - ---- - -## ❌ 2. 미사용 컴포넌트 목록 (삭제 가능) - -### 2.1 src/components/business/ (35개 미사용) - -#### 데모/예제 페이지 (7개) -``` -❌ LandingPage.tsx - 데모용 랜딩 페이지 -❌ DemoRequestPage.tsx - 데모 신청 페이지 -❌ ContactModal.tsx - 문의 모달 -❌ LoginPage.tsx - 🔴 중복! (auth/LoginPage.tsx 사용 중) -❌ SignupPage.tsx - 🔴 중복! (auth/SignupPage.tsx 사용 중) -❌ Board.tsx - 게시판 -❌ MenuCustomization.tsx - 메뉴 커스터마이징 -❌ MenuCustomizationGuide.tsx - 메뉴 가이드 -``` - -#### 대시보드 (2개 미사용, 4개 사용 중) -``` -✅ CEODashboard.tsx - Dashboard.tsx에서 lazy import -✅ ProductionManagerDashboard.tsx - Dashboard.tsx에서 lazy import -✅ WorkerDashboard.tsx - Dashboard.tsx에서 lazy import -✅ SystemAdminDashboard.tsx - Dashboard.tsx에서 lazy import -❌ SalesLeadDashboard.tsx - 미사용 -``` - -#### 관리 모듈 (28개) -``` -❌ AccountingManagement.tsx - 회계 관리 -❌ ApprovalManagement.tsx - 결재 관리 -❌ BOMManagement.tsx - BOM 관리 -❌ CodeManagement.tsx - 코드 관리 -❌ EquipmentManagement.tsx - 설비 관리 -❌ HRManagement.tsx - 인사 관리 -❌ ItemManagement.tsx - 품목 관리 -❌ LotManagement.tsx - 로트 관리 -❌ MasterData.tsx - 마스터 데이터 -❌ MaterialManagement.tsx - 자재 관리 -❌ OrderManagement.tsx - 수주 관리 -❌ PricingManagement.tsx - 가격 관리 -❌ ProductManagement.tsx - 제품 관리 -❌ ProductionManagement.tsx - 생산 관리 -❌ QualityManagement.tsx - 품질 관리 -❌ QuoteCreation.tsx - 견적 생성 -❌ QuoteSimulation.tsx - 견적 시뮬레이션 -❌ ReceivingWrite.tsx - 입고 작성 -❌ Reports.tsx - 보고서 -❌ SalesManagement.tsx - 영업 관리 -❌ SalesManagement-clean.tsx - 영업 관리 (정리 버전) -❌ ShippingManagement.tsx - 출하 관리 -❌ SystemManagement.tsx - 시스템 관리 -❌ UserManagement.tsx - 사용자 관리 -❌ WorkerPerformance.tsx - 작업자 실적 -❌ DrawingCanvas.tsx - 도면 캔버스 -``` - -### 2.2 src/components/ (3개 미사용) -``` -❌ WelcomeMessage.tsx - 환영 메시지 -❌ NavigationMenu.tsx - 네비게이션 메뉴 -❌ LanguageSwitcher.tsx - 언어 전환 (LanguageSelect로 대체) -``` - ---- - -## 🔴 3. 중복 파일 문제 - -### LoginPage.tsx 중복 -- **src/components/auth/LoginPage.tsx** ✅ 사용 중 -- **src/components/business/LoginPage.tsx** ❌ 미사용 (삭제 권장) - -### SignupPage.tsx 중복 -- **src/components/auth/SignupPage.tsx** ✅ 사용 중 -- **src/components/business/SignupPage.tsx** ❌ 미사용 (삭제 권장) - -**권장 조치**: `src/components/business/` 내 중복 파일 삭제 - ---- - -## 📊 4. UI 컴포넌트 사용 현황 (src/components/ui/) - -### 실제 사용 중인 UI 컴포넌트 -``` -✅ badge.tsx - 배지 -✅ button.tsx - 버튼 -✅ calendar.tsx - 달력 (CEODashboard) -✅ card.tsx - 카드 -✅ chart-wrapper.tsx - 차트 래퍼 (CEODashboard) -✅ checkbox.tsx - 체크박스 (CEODashboard) -✅ dialog.tsx - 다이얼로그 -✅ dropdown-menu.tsx - 드롭다운 메뉴 -✅ input.tsx - 입력 필드 -✅ label.tsx - 라벨 -✅ progress.tsx - 진행 바르 -✅ select.tsx - 선택 박스 -✅ sheet.tsx - 시트 (DashboardLayout) -``` - -**모든 UI 컴포넌트가 사용 중** (미사용 UI 컴포넌트 없음) - ---- - -## 📁 5. 파일 구조 분석 - -### 현재 프로젝트 구조 -``` -src/ -├── app/ -│ └── [locale]/ -│ ├── login/page.tsx → LoginPage -│ ├── signup/page.tsx → SignupPage -│ ├── (protected)/ -│ │ ├── dashboard/page.tsx → Dashboard -│ │ └── [...slug]/page.tsx → EmptyPage (폴백) -│ ├── layout.tsx -│ ├── error.tsx -│ └── not-found.tsx -├── components/ -│ ├── auth/ ✅ 2개 사용 중 -│ │ ├── LoginPage.tsx -│ │ └── SignupPage.tsx -│ ├── business/ ⚠️ 5/40개만 사용 (12.5%) -│ │ ├── Dashboard.tsx ✅ -│ │ ├── CEODashboard.tsx ✅ (lazy) -│ │ ├── ProductionManagerDashboard.tsx ✅ (lazy) -│ │ ├── WorkerDashboard.tsx ✅ (lazy) -│ │ ├── SystemAdminDashboard.tsx ✅ (lazy) -│ │ └── [35개 미사용 컴포넌트] ❌ -│ ├── common/ ✅ 1/1개 사용 -│ │ └── EmptyPage.tsx -│ ├── layout/ ✅ 1/1개 사용 -│ │ └── Sidebar.tsx -│ ├── ui/ ✅ 14/14개 사용 -│ ├── LanguageSelect.tsx ✅ -│ ├── ThemeSelect.tsx ✅ -│ ├── WelcomeMessage.tsx ❌ -│ ├── NavigationMenu.tsx ❌ -│ └── LanguageSwitcher.tsx ❌ -└── layouts/ - └── DashboardLayout.tsx ✅ (Sidebar 사용) -``` - ---- - -## 🎯 6. 정리 권장사항 - -### 우선순위 1: 중복 파일 삭제 (즉시) -```bash -rm src/components/business/LoginPage.tsx -rm src/components/business/SignupPage.tsx -``` - -### 우선순위 2: 명확한 미사용 컴포넌트 삭제 -```bash -# 데모/예제 페이지 -rm src/components/business/LandingPage.tsx -rm src/components/business/DemoRequestPage.tsx -rm src/components/business/ContactModal.tsx -rm src/components/business/Board.tsx -rm src/components/business/MenuCustomization.tsx -rm src/components/business/MenuCustomizationGuide.tsx - -# 미사용 대시보드 -rm src/components/business/SalesLeadDashboard.tsx - -# 루트 레벨 미사용 컴포넌트 -rm src/components/WelcomeMessage.tsx -rm src/components/NavigationMenu.tsx -rm src/components/LanguageSwitcher.tsx -``` - -### 우선순위 3: 관리 모듈 컴포넌트 정리 (신중히) - -**⚠️ 주의**: 다음 35개 컴포넌트는 현재 미사용이지만, 향후 기능 구현 계획에 따라 보존 여부 결정 필요 - -#### 옵션 A: 전체 삭제 (프로토타입 프로젝트인 경우) -```bash -# 모든 미사용 관리 모듈 삭제 -rm src/components/business/AccountingManagement.tsx -rm src/components/business/ApprovalManagement.tsx -# ... (28개 전체) -``` - -#### 옵션 B: 별도 디렉토리로 이동 (향후 사용 가능성이 있는 경우) -```bash -mkdir src/components/business/_unused -mv src/components/business/AccountingManagement.tsx src/components/business/_unused/ -# ... (미사용 컴포넌트 이동) -``` - -#### 옵션 C: 보존 (ERP 시스템 구축 중인 경우) -- 현재 미구현 상태지만 향후 기능 구현 예정이라면 보존 권장 -- EmptyPage.tsx가 폴백으로 작동하고 있으므로 점진적 구현 가능 - ---- - -## 📈 7. 영향도 분석 - -### 삭제 시 영향 없음 (안전) -- **중복 파일** (business/LoginPage.tsx, business/SignupPage.tsx) -- **데모 페이지** (LandingPage, DemoRequestPage, ContactModal 등) -- **루트 레벨 미사용 컴포넌트** (WelcomeMessage, NavigationMenu, LanguageSwitcher) - -### 삭제 시 신중 검토 필요 -- **관리 모듈 컴포넌트** (35개) - - 이유: 메뉴 구조와 연결된 기능일 가능성 - - 조치: 메뉴 설정 (menu configuration) 확인 후 결정 - -### 절대 삭제 금지 -- **auth/** 내 컴포넌트 (LoginPage, SignupPage) -- **business/Dashboard.tsx** 및 lazy-loaded 대시보드 (5개) -- **common/EmptyPage.tsx** -- **layout/Sidebar.tsx** -- **LanguageSelect.tsx, ThemeSelect.tsx** -- **ui/** 내 모든 컴포넌트 - ---- - -## 🔍 8. 추가 분석 필요 사항 - -### 메뉴 설정 확인 -```typescript -// src/store/menuStore.ts 또는 사용자 메뉴 설정 확인 필요 -// 메뉴 구조에 미사용 컴포넌트가 연결되어 있는지 확인 -``` - -### API 연동 확인 -```bash -# API 응답에서 메뉴 구조를 동적으로 받아오는지 확인 -grep -r "menu" src/lib/api/ -grep -r "menuItems" src/ -``` - ---- - -## 📝 9. 실행 스크립트 - -### 안전한 정리 스크립트 (중복 + 데모만 삭제) -```bash -#!/bin/bash -# safe-cleanup.sh - -echo "🧹 컴포넌트 정리 시작 (안전 모드)..." - -# 중복 파일 삭제 -rm -v src/components/business/LoginPage.tsx -rm -v src/components/business/SignupPage.tsx - -# 데모/예제 페이지 삭제 -rm -v src/components/business/LandingPage.tsx -rm -v src/components/business/DemoRequestPage.tsx -rm -v src/components/business/ContactModal.tsx -rm -v src/components/business/Board.tsx -rm -v src/components/business/MenuCustomization.tsx -rm -v src/components/business/MenuCustomizationGuide.tsx -rm -v src/components/business/SalesLeadDashboard.tsx - -# 루트 레벨 미사용 컴포넌트 -rm -v src/components/WelcomeMessage.tsx -rm -v src/components/NavigationMenu.tsx -rm -v src/components/LanguageSwitcher.tsx - -echo "✅ 안전한 정리 완료!" -``` - -### 전체 정리 스크립트 (관리 모듈 포함) -```bash -#!/bin/bash -# full-cleanup.sh - -echo "⚠️ 전체 컴포넌트 정리 시작..." -echo "이 스크립트는 모든 미사용 컴포넌트를 삭제합니다." -read -p "계속하시겠습니까? (y/N): " confirm - -if [[ $confirm != [yY] ]]; then - echo "취소되었습니다." - exit 0 -fi - -# 안전 정리 실행 -bash safe-cleanup.sh - -# 관리 모듈 삭제 -rm -v src/components/business/AccountingManagement.tsx -rm -v src/components/business/ApprovalManagement.tsx -rm -v src/components/business/BOMManagement.tsx -rm -v src/components/business/CodeManagement.tsx -rm -v src/components/business/EquipmentManagement.tsx -rm -v src/components/business/HRManagement.tsx -rm -v src/components/business/ItemManagement.tsx -rm -v src/components/business/LotManagement.tsx -rm -v src/components/business/MasterData.tsx -rm -v src/components/business/MaterialManagement.tsx -rm -v src/components/business/OrderManagement.tsx -rm -v src/components/business/PricingManagement.tsx -rm -v src/components/business/ProductManagement.tsx -rm -v src/components/business/ProductionManagement.tsx -rm -v src/components/business/QualityManagement.tsx -rm -v src/components/business/QuoteCreation.tsx -rm -v src/components/business/QuoteSimulation.tsx -rm -v src/components/business/ReceivingWrite.tsx -rm -v src/components/business/Reports.tsx -rm -v src/components/business/SalesManagement.tsx -rm -v src/components/business/SalesManagement-clean.tsx -rm -v src/components/business/ShippingManagement.tsx -rm -v src/components/business/SystemManagement.tsx -rm -v src/components/business/UserManagement.tsx -rm -v src/components/business/WorkerPerformance.tsx -rm -v src/components/business/DrawingCanvas.tsx - -echo "✅ 전체 정리 완료!" -``` - ---- - -## 💡 10. 최종 권장 사항 - -### 즉시 조치 (안전) -1. **중복 파일 삭제**: `business/LoginPage.tsx`, `business/SignupPage.tsx` -2. **데모 페이지 삭제**: 10개의 데모/예제 컴포넌트 -3. Git 커밋: `[chore]: Remove duplicate and unused demo components` - -### 단계적 조치 (신중) -1. **메뉴 구조 확인**: 메뉴 설정에서 미사용 컴포넌트 참조 여부 확인 -2. **기능 로드맵 확인**: 관리 모듈 구현 계획 확인 -3. **결정 후 삭제**: 향후 사용 계획 없으면 삭제, 있으면 `_unused/` 폴더로 이동 - -### 장기 계획 -1. **컴포넌트 문서화**: 사용 중인 컴포넌트에 JSDoc 주석 추가 -2. **린팅 규칙 추가**: ESLint에 unused imports/exports 체크 규칙 추가 -3. **자동 탐지**: CI/CD에 미사용 컴포넌트 탐지 스크립트 추가 - ---- - -## 📎 부록: 상세 의존성 그래프 - -``` -app/[locale]/login/page.tsx -└── components/auth/LoginPage.tsx - ├── components/LanguageSelect.tsx - ├── components/ThemeSelect.tsx - └── components/ui/* (button, input, label) - -app/[locale]/signup/page.tsx -└── components/auth/SignupPage.tsx - ├── components/LanguageSelect.tsx - ├── components/ThemeSelect.tsx - └── components/ui/* (button, input, label, select) - -app/[locale]/(protected)/dashboard/page.tsx -└── components/business/Dashboard.tsx - ├── components/business/CEODashboard.tsx (lazy) - │ └── components/ui/* (card, badge, chart-wrapper, calendar, checkbox) - ├── components/business/ProductionManagerDashboard.tsx (lazy) - │ └── components/ui/* (card, badge, button) - ├── components/business/WorkerDashboard.tsx (lazy) - │ └── components/ui/* (card, badge, button) - └── components/business/SystemAdminDashboard.tsx (lazy) - -app/[locale]/(protected)/[...slug]/page.tsx -└── components/common/EmptyPage.tsx - └── components/ui/* (card, button) - -layouts/DashboardLayout.tsx -├── components/layout/Sidebar.tsx -├── components/ThemeSelect.tsx -└── components/ui/* (input, button, sheet) -``` - ---- - -**분석 완료일**: 2025-11-12 -**분석 도구**: Grep, Bash, Read -**정확도**: 100% (전체 프로젝트 스캔 완료) \ No newline at end of file diff --git a/claudedocs/archive/[REF] production-deployment-checklist.md b/claudedocs/archive/[REF] production-deployment-checklist.md deleted file mode 100644 index 65da608d..00000000 --- a/claudedocs/archive/[REF] production-deployment-checklist.md +++ /dev/null @@ -1,233 +0,0 @@ -# 운영 배포 체크리스트 - -**문서 목적**: 로컬/개발 환경에서 운영 환경으로 전환 시 필요한 변경사항 정리 -**작성일**: 2025-11-07 -**상태**: 내부 개발용 → 추후 운영 배포 시 참고 - ---- - -## 🔴 필수 변경 사항 (운영 배포 전 필수) - -### 1. Frontend URL 변경 -**현재 설정** (로컬 개발용): -```bash -# .env.local -NEXT_PUBLIC_FRONTEND_URL=http://localhost:3000 -``` - -**운영 배포 시 변경**: -```bash -# .env.production 또는 배포 플랫폼 환경 변수 -NEXT_PUBLIC_FRONTEND_URL=https://your-production-domain.com -# 예시: https://5130.co.kr -``` - -**영향 범위**: -- `src/lib/api/auth/auth-config.ts:8` - CORS 설정 -- 백엔드 PHP API의 CORS 허용 도메인 추가 필요 - ---- - -### 2. API Key 보안 강화 ⚠️ - -**현재 상태** (내부 개발용): -```bash -# .env.local -NEXT_PUBLIC_API_KEY=42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a -``` - -**보안 위험**: -- `NEXT_PUBLIC_` 접두사로 인해 브라우저에서 API Key 노출 -- 개발자 도구 → Network/Console에서 키 확인 가능 -- 클라이언트 측 JavaScript에서 접근 가능 - -**운영 배포 시 해결 방안** (택 1): - -#### 방안 A: 서버 전용 API Key로 전환 -```bash -# .env.production (서버 사이드 전용) -API_KEY=your-production-secret-key -``` -- `NEXT_PUBLIC_` 접두사 제거 -- Next.js API Routes에서만 사용 -- 브라우저 접근 불가 - -#### 방안 B: 운영용 별도 Public API Key 발급 -```bash -# PHP 백엔드 팀에 운영용 Public API Key 요청 -NEXT_PUBLIC_API_KEY=production-public-safe-key -``` -- 제한된 권한으로 발급 (읽기 전용 등) -- IP 화이트리스트 적용 -- Rate Limiting 설정 - -**코드 수정 필요 위치**: -- `src/lib/api/client.ts:40` - API Key 사용 로직 -- `.env.example:32` - 문서 불일치 해결 - ---- - -## 🟡 권장 변경 사항 - -### 3. 백엔드 CORS 설정 -**PHP API 서버 설정 확인**: -```php -// Laravel sanctum config 예시 -'allowed_origins' => [ - 'http://localhost:3000', // 개발 - 'https://5130.co.kr', // 운영 (추가 필요) -], -``` - -**Sanctum 쿠키 도메인**: -```php -// config/sanctum.php -'stateful' => explode(',', env( - 'SANCTUM_STATEFUL_DOMAINS', - 'localhost,localhost:3000,127.0.0.1,5130.co.kr' -)), -``` - ---- - -### 4. Next.js 운영 최적화 -**next.config.ts 추가 권장**: -```typescript -const nextConfig: NextConfig = { - turbopack: {}, - - // 운영 환경 추가 설정 - reactStrictMode: true, - poweredByHeader: false, // 보안: X-Powered-By 헤더 제거 - output: 'standalone', // Docker 배포용 - compress: true, // Gzip 압축 -}; -``` - ---- - -### 5. 빌드 스크립트 추가 -**package.json 추가 권장**: -```json -{ - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "eslint", - - // 추가 권장 - "build:prod": "NODE_ENV=production next build", - "type-check": "tsc --noEmit", - "lint:fix": "eslint --fix" - } -} -``` - ---- - -## 🟢 배포 플랫폼별 설정 - -### Vercel 배포 -**프로젝트 설정 → Environment Variables**: -``` -NEXT_PUBLIC_API_URL=https://api.5130.co.kr -NEXT_PUBLIC_FRONTEND_URL=https://your-app.vercel.app -NEXT_PUBLIC_AUTH_MODE=sanctum -API_KEY=<서버 전용 키> -``` - -### Docker 배포 -**docker-compose.yml 예시**: -```yaml -version: '3.8' -services: - nextjs-app: - build: . - environment: - - NEXT_PUBLIC_API_URL=https://api.5130.co.kr - - NEXT_PUBLIC_FRONTEND_URL=https://your-domain.com - - API_KEY=${API_KEY} - ports: - - "3000:3000" -``` - -### 전통적인 서버 배포 -**`.env.production` 파일 생성**: -```bash -NEXT_PUBLIC_API_URL=https://api.5130.co.kr -NEXT_PUBLIC_FRONTEND_URL=https://your-domain.com -NEXT_PUBLIC_AUTH_MODE=sanctum -API_KEY=<서버 전용 키> -``` - ---- - -## 📋 최종 배포 체크리스트 - -### 환경 변수 -- [ ] `NEXT_PUBLIC_FRONTEND_URL` → 운영 도메인으로 변경 -- [ ] `NEXT_PUBLIC_API_KEY` → 보안 방안 적용 (서버 전용 또는 제한된 Public Key) -- [ ] `NEXT_PUBLIC_AUTH_MODE` → `sanctum` 또는 `bearer` 확인 -- [ ] `.env.local` Git 커밋 안 됨 확인 (`.gitignore:100`) - -### 백엔드 연동 -- [ ] PHP API CORS 설정에 운영 도메인 추가 -- [ ] Sanctum 쿠키 도메인 설정 확인 -- [ ] 운영용 API Key 발급 (필요 시) -- [ ] API 엔드포인트 테스트 (`https://api.5130.co.kr`) - -### 빌드 & 테스트 -- [ ] `npm run build` 로컬 테스트 -- [ ] `npm run lint` 통과 확인 -- [ ] `tsc --noEmit` TypeScript 타입 체크 -- [ ] 브라우저 콘솔 에러 없는지 확인 - -### 보안 -- [ ] API Key 브라우저 노출 문제 해결 -- [ ] HTTPS 사용 확인 -- [ ] 민감 정보 환경 변수로 분리 -- [ ] `X-Powered-By` 헤더 제거 (`poweredByHeader: false`) - -### 성능 -- [ ] 이미지 최적화 (Next.js Image 컴포넌트 사용) -- [ ] 번들 사이즈 확인 (`npm run build` 출력 확인) -- [ ] Gzip/Brotli 압축 활성화 -- [ ] CDN 설정 (필요 시) - ---- - -## 🔧 현재 상태 (2025-11-07) - -**개발 환경**: -- ✅ API URL: `https://api.5130.co.kr` (운영 API 사용 중) -- ⚠️ Frontend URL: `http://localhost:3000` (로컬) -- ⚠️ API Key: `NEXT_PUBLIC_API_KEY` (브라우저 노출) -- ✅ Auth Mode: `sanctum` (쿠키 기반 인증) - -**내부 개발용 사용 중**: -- 현재는 개발/테스트 목적으로 API Key 노출 허용 -- 운영 배포 시 반드시 위 체크리스트 검토 필요 - ---- - -## 📌 참고 문서 - -- `claudedocs/api-key-management.md` - API Key 관리 가이드 -- `claudedocs/authentication-design.md` - 인증 시스템 설계 -- `claudedocs/authentication-implementation-guide.md` - 구현 가이드 -- `.env.example` - 환경 변수 템플릿 - ---- - -## 📞 배포 전 확인 담당 - -- **API Key 발급**: PHP 백엔드 팀 -- **CORS 설정**: PHP 백엔드 팀 -- **인프라 설정**: DevOps 팀 -- **보안 검토**: 보안 담당자 - ---- - -**마지막 업데이트**: 2025-11-07 -**다음 검토 예정**: 운영 배포 1주 전 \ No newline at end of file diff --git a/claudedocs/archive/[REF] project-context.md b/claudedocs/archive/[REF] project-context.md deleted file mode 100644 index 71f611f6..00000000 --- a/claudedocs/archive/[REF] project-context.md +++ /dev/null @@ -1,428 +0,0 @@ -# SAM React 프로젝트 컨텍스트 - -> **중요**: 이 파일은 모든 세션에서 가장 먼저 읽어야 하는 프로젝트 개요 문서입니다. - -## 📋 프로젝트 개요 - -**프로젝트 명**: SAM React (Multi-tenant ERP System) -**기술 스택**: Next.js 15 (App Router) + TypeScript + Tailwind CSS -**백엔드**: Laravel PHP API (https://api.5130.co.kr) -**인증 방식**: JWT Bearer Token (Cookie 저장) -**다국어**: 한국어(ko), 영어(en), 일본어(ja) - ---- - -## 🎯 핵심 기능 - -### 1. 다국어 지원 (i18n) -- **라이브러리**: next-intl v4 -- **기본 언어**: 한국어(ko) -- **지원 언어**: ko, en, ja -- **URL 구조**: - - 기본 언어: `/dashboard` (로케일 표시 안함) - - 다른 언어: `/en/dashboard`, `/ja/dashboard` -- **자동 감지**: Accept-Language 헤더, 쿠키 - -**주요 파일**: -``` -src/i18n/config.ts # 언어 설정 -src/i18n/request.ts # 메시지 로딩 -src/messages/*.json # 번역 파일 -``` - ---- - -### 2. 인증 시스템 (Authentication) - -#### 인증 방식 -**현재 사용**: JWT Bearer Token + Cookie 저장 -- Login → Token 발급 → Cookie에 저장 (`user_token`) -- Middleware에서 Cookie 확인 -- API 호출 시 Authorization 헤더 자동 추가 - -**지원 방식** (3가지): -1. **Bearer Token** (Primary): `user_token` 쿠키 -2. **Sanctum Session** (Legacy): `laravel_session` 쿠키 -3. **API Key** (Server-to-Server): `X-API-KEY` 헤더 - -#### API 엔드포인트 -``` -POST /api/v1/login # 로그인 -POST /api/v1/logout # 로그아웃 -GET /api/user # 사용자 정보 -``` - -#### 주요 파일 -``` -src/lib/api/auth/auth-config.ts # 라우트 설정 -src/lib/api/auth/types.ts # 타입 정의 -src/lib/api/client.ts # HTTP Client -src/middleware.ts # 인증 체크 -src/app/api/auth/* # API Routes -``` - ---- - -### 3. Route 보호 (Route Protection) - -#### 라우트 분류 -**Protected Routes** (인증 필요): -- `/dashboard`, `/admin`, `/tenant`, `/settings`, `/users`, `/reports` -- 기타 모든 경로 (guestOnlyRoutes, publicRoutes 제외) - -**Guest-only Routes** (로그인 시 접근 불가): -- `/login`, `/register` - -**Public Routes** (누구나 접근 가능): -- `/` (홈), `/about`, `/contact` - -#### 동작 방식 -``` -Middleware 체크 순서: -1. Bot Detection → 봇이면 403 -2. 정적 파일 체크 → 정적이면 Skip -3. 인증 체크 (3가지 방식) -4. Guest-only 체크 → 로그인 상태면 /dashboard로 -5. Public 체크 → Public이면 통과 -6. Protected 체크 → 비로그인이면 /login으로 -7. i18n 처리 -``` - ---- - -### 4. Bot 차단 (Bot Detection) - -#### 목적 -- ERP 시스템 보안 강화 -- Crawler/Spider로부터 보호된 경로 차단 - -#### 차단 대상 -```typescript -BOT_PATTERNS = [ - /bot/i, /crawler/i, /spider/i, /scraper/i, - /curl/i, /wget/i, /python-requests/i, - /headless/i, /puppeteer/i, /playwright/i -] -``` - -#### 차단 경로 -- `/dashboard`, `/admin`, `/api`, `/tenant` 등 -- Public 경로(`/`, `/login`)는 bot 허용 - ---- - -### 5. 테마 시스템 - -**기능**: 다크모드/라이트모드 전환 -**구현**: Context API + localStorage - -**주요 파일**: -``` -src/contexts/ThemeContext.tsx -src/components/ThemeSelect.tsx -``` - ---- - -## 📁 프로젝트 구조 - -``` -sam-react-prod/ -├─ src/ -│ ├─ app/[locale]/ -│ │ ├─ (protected)/ # 보호된 라우트 그룹 -│ │ │ ├─ layout.tsx # AuthGuard Layout -│ │ │ └─ dashboard/ -│ │ │ └─ page.tsx -│ │ ├─ login/page.tsx -│ │ ├─ signup/page.tsx -│ │ ├─ page.tsx # 홈 -│ │ └─ layout.tsx # 루트 레이아웃 -│ │ -│ ├─ components/ -│ │ ├─ auth/ -│ │ │ ├─ LoginPage.tsx -│ │ │ └─ SignupPage.tsx -│ │ ├─ ui/ # Shadcn UI 컴포넌트 -│ │ ├─ ThemeSelect.tsx -│ │ ├─ LanguageSelect.tsx -│ │ └─ NavigationMenu.tsx -│ │ -│ ├─ lib/ -│ │ ├─ api/ -│ │ │ ├─ client.ts # HTTP Client -│ │ │ └─ auth/ -│ │ │ ├─ auth-config.ts -│ │ │ └─ types.ts -│ │ ├─ validations/ -│ │ │ └─ auth.ts # Zod 스키마 -│ │ └─ utils.ts -│ │ -│ ├─ contexts/ -│ │ └─ ThemeContext.tsx -│ │ -│ ├─ hooks/ -│ │ └─ useAuthGuard.ts -│ │ -│ ├─ i18n/ -│ │ ├─ config.ts -│ │ └─ request.ts -│ │ -│ ├─ messages/ -│ │ ├─ ko.json -│ │ ├─ en.json -│ │ └─ ja.json -│ │ -│ └─ middleware.ts # 통합 Middleware -│ -├─ claudedocs/ # 프로젝트 문서 -│ ├─ 00_INDEX.md # 문서 인덱스 -│ ├─ project-context.md # 이 파일 -│ └─ ... -│ -├─ .env.local # 환경 변수 (실제 값) -├─ .env.example # 환경 변수 템플릿 -├─ package.json -├─ next.config.ts -├─ tsconfig.json -└─ tailwind.config.ts -``` - ---- - -## 🔧 환경 설정 - -### 필수 환경 변수 (.env.local) - -```env -# API Configuration -NEXT_PUBLIC_API_URL=https://api.5130.co.kr -NEXT_PUBLIC_FRONTEND_URL=http://localhost:3000 - -# API Key (서버 사이드 전용 - 절대 공개 금지!) -API_KEY=42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a -``` - -### Next.js 설정 (next.config.ts) - -```typescript -import createNextIntlPlugin from 'next-intl/plugin'; - -const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts'); - -const nextConfig: NextConfig = { - turbopack: {}, // Next.js 15 + next-intl 필수 설정 -}; - -export default withNextIntl(nextConfig); -``` - ---- - -## 🚀 주요 라이브러리 - -```json -{ - "dependencies": { - "next": "^15.5.6", - "react": "19.2.0", - "next-intl": "^4.4.0", - "react-hook-form": "^7.66.0", - "zod": "^4.1.12", - "@radix-ui/react-*": "^2.x", - "tailwindcss": "^4", - "lucide-react": "^0.552.0", - "clsx": "^2.1.1", - "tailwind-merge": "^3.3.1" - } -} -``` - ---- - -## 📝 일반적인 작업 패턴 - -### 새 보호된 페이지 추가 - -1. **페이지 파일 생성**: - ``` - src/app/[locale]/(protected)/new-page/page.tsx - ``` - -2. **라우트 설정 추가** (선택사항): - ```typescript - // src/lib/api/auth/auth-config.ts - protectedRoutes: [ - ... - '/new-page' - ] - ``` - -3. **자동으로 인증 체크 적용됨** (Middleware가 처리) - ---- - -### 새 번역 키 추가 - -1. **모든 언어 파일에 키 추가**: - ```json - // src/messages/ko.json - { - "newFeature": { - "title": "새 기능", - "description": "설명" - } - } - - // src/messages/en.json - { - "newFeature": { - "title": "New Feature", - "description": "Description" - } - } - - // src/messages/ja.json - { - "newFeature": { - "title": "新機能", - "description": "説明" - } - } - ``` - -2. **컴포넌트에서 사용**: - ```typescript - const t = useTranslations('newFeature'); - -

{t('title')}

-

{t('description')}

- ``` - ---- - -### API 호출 패턴 - -```typescript -// src/lib/api/client.ts 사용 -import { apiClient } from '@/lib/api/client'; - -// GET 요청 -const data = await apiClient.get('/api/endpoint'); - -// POST 요청 -const result = await apiClient.post('/api/endpoint', { - key: 'value' -}); -``` - ---- - -## ⚠️ 중요 주의사항 - -### 1. 환경 변수 보안 -- ❌ `API_KEY`에 절대 `NEXT_PUBLIC_` 붙이지 말 것! -- ✅ `.env.local`은 Git에 커밋 금지 (.gitignore 포함됨) -- ✅ `.env.example`만 템플릿으로 관리 - -### 2. Middleware 주의사항 -- Middleware는 **서버 사이드**에서 실행됨 -- `localStorage` 접근 불가 -- `console.log`는 **터미널**에 출력됨 (브라우저 콘솔 아님) - -### 3. Route Protection 규칙 -- **기본 정책**: 모든 페이지는 인증 필요 -- **예외**: `publicRoutes`, `guestOnlyRoutes`에 명시된 경로만 -- `/` 경로 주의: 정확히 일치할 때만 public - -### 4. i18n 사용 시 -- 모든 언어 파일에 동일한 키 추가 필수 -- Link 사용 시 로케일 포함: `/${locale}/path` -- 날짜/숫자는 `useFormatter` 훅 사용 - ---- - -## 🐛 알려진 이슈 및 해결 방법 - -### 1. Middleware 인증 체크 안됨 -**증상**: 로그인 안해도 보호된 페이지 접근 가능 -**원인**: `isPublicRoute()` 함수의 `'/'` 매칭 버그 -**해결**: `middleware-issue-resolution.md` 참고 - -### 2. Next.js 15 + next-intl 에러 -**증상**: Middleware 컴파일 에러 -**원인**: `turbopack` 설정 누락 -**해결**: `next.config.ts`에 `turbopack: {}` 추가 - ---- - -## 📚 문서 참고 순서 - -새 세션 시작 시 권장 읽기 순서: - -1. **이 파일** (`project-context.md`) - 프로젝트 전체 개요 -2. **`00_INDEX.md`** - 상세 문서 인덱스 -3. **작업할 기능의 관련 문서** - 인덱스에서 검색 - -### 주요 문서 빠른 링크 - -| 작업 | 문서 | -|------|------| -| 다국어 작업 | `i18n-usage-guide.md` | -| 인증 관련 | `jwt-cookie-authentication-final.md` | -| 라우트 보호 | `route-protection-architecture.md` | -| 폼 검증 | `form-validation-guide.md` | -| API 통합 | `authentication-implementation-guide.md` | -| Middleware 수정 | `middleware-issue-resolution.md` | - ---- - -## 🔄 최근 변경 사항 - -### 2025-11-10 -- 테마 선택 및 언어 선택 기능 추가 -- 다국어 지원 구현 완료 -- Git branch: `feature/theme-language-selector` - -### 2025-11-07 -- Middleware 인증 문제 해결 -- JWT Cookie 인증 방식 확정 -- Bot 차단 기능 구현 - -### 2025-11-06 -- i18n 설정 완료 (ko, en, ja) -- 프로젝트 초기 구조 설정 - ---- - -## 💡 개발 팁 - -### 디버깅 -- **Middleware 로그**: 터미널 확인 (브라우저 콘솔 아님) -- **인증 상태**: 브라우저 개발자 도구 → Application → Cookies → `user_token` 확인 -- **API 요청**: Network 탭에서 Authorization 헤더 확인 - -### 성능 -- 서버 컴포넌트 우선 사용 (클라이언트 번들 크기 감소) -- 정적 파일은 Middleware에서 조기 리턴 -- API 응답 캐싱 고려 - -### 보안 -- 민감한 데이터는 서버 컴포넌트에서만 처리 -- API Key는 절대 클라이언트에 노출 금지 -- CORS 설정 확인 (Laravel 측) - ---- - -## 📞 문제 발생 시 - -1. **이 파일 다시 읽기** -2. **`00_INDEX.md`에서 관련 문서 찾기** -3. **`middleware-issue-resolution.md` 참고** (인증 관련 이슈) -4. **Git 히스토리 확인** (`git log`, `git diff`) - ---- - -**마지막 업데이트**: 2025-11-10 -**작성자**: Claude Code -**프로젝트 저장소**: sam-react-prod \ No newline at end of file diff --git a/claudedocs/archive/[SESSION-2025-11-18] localStorage-ssr-fix-checkpoint.md b/claudedocs/archive/[SESSION-2025-11-18] localStorage-ssr-fix-checkpoint.md deleted file mode 100644 index 5d5fac83..00000000 --- a/claudedocs/archive/[SESSION-2025-11-18] localStorage-ssr-fix-checkpoint.md +++ /dev/null @@ -1,169 +0,0 @@ -# [SESSION-2025-11-18] localStorage SSR 수정 작업 체크포인트 - -## 세션 상태: ✅ 완료 (9/9 완료) - -### 작업 개요 -- **목표**: ItemMasterDataManagement.tsx의 모든 localStorage 접근을 SSR 호환으로 수정 ✅ -- **파일**: `src/components/items/ItemMasterDataManagement.tsx` -- **크기**: 274KB (대용량 파일) -- **진행률**: 9/9 완료 ✅ -- **빌드 테스트**: ✅ 성공 (3.1초) - -### 작업 배경 -- React → Next.js 마이그레이션 작업 진행 중 -- SSR 환경에서 localStorage 접근 시 `ReferenceError: localStorage is not defined` 에러 발생 -- `typeof window === 'undefined'` 체크를 통한 SSR 호환성 확보 필요 - -### 수정 대상 (6곳) - -#### 1. attributeSubTabs (Line ~460) -```typescript -// 현재 코드 -const [attributeSubTabs, setAttributeSubTabs] = useState>(() => { - const saved = localStorage.getItem('mes-attributeSubTabs'); // ❌ SSR 오류 - // ... -}); - -// 수정 필요 -const [attributeSubTabs, setAttributeSubTabs] = useState>(() => { - if (typeof window === 'undefined') { - return [ - { id: 'units', label: '단위', key: 'units', isDefault: true, order: 0 }, - { id: 'materials', label: '재질', key: 'materials', isDefault: true, order: 1 }, - { id: 'surface', label: '표면처리', key: 'surface', isDefault: true, order: 2 } - ]; - } - const saved = localStorage.getItem('mes-attributeSubTabs'); - // ... -}); -``` -**상태**: ❌ 미완료 - -#### 2. attributeColumns (Line ~668) -```typescript -// 현재 코드 -const [attributeColumns, setAttributeColumns] = useState>(() => { - const saved = localStorage.getItem('attribute-columns'); // ❌ SSR 오류 - return saved ? JSON.parse(saved) : {}; -}); - -// 수정 필요 -const [attributeColumns, setAttributeColumns] = useState>(() => { - if (typeof window === 'undefined') return {}; - const saved = localStorage.getItem('attribute-columns'); - return saved ? JSON.parse(saved) : {}; -}); -``` -**상태**: ❌ 미완료 - -#### 3. bomItems (Line ~820) -```typescript -// 현재 코드 -const [bomItems, setBomItems] = useState(() => { - const saved = localStorage.getItem('bom-items'); // ❌ SSR 오류 - return saved ? JSON.parse(saved) : []; -}); - -// 수정 필요 -const [bomItems, setBomItems] = useState(() => { - if (typeof window === 'undefined') return []; - const saved = localStorage.getItem('bom-items'); - return saved ? JSON.parse(saved) : []; -}); -``` -**상태**: ❌ 미완료 - -#### 4-6. 추가 localStorage 사용 위치 (검색 필요) -**검색 명령**: -```bash -grep -n "localStorage.getItem\|localStorage.setItem" src/components/items/ItemMasterDataManagement.tsx -``` -**상태**: ❌ 확인 필요 - -### 작업 계획 - -#### Phase 1: 전체 localStorage 사용 위치 파악 -```bash -grep -n "localStorage" src/components/items/ItemMasterDataManagement.tsx > /tmp/localstorage-usage.txt -``` - -#### Phase 2: useState 초기화 수정 -- attributeSubTabs 수정 -- attributeColumns 수정 -- bomItems 수정 -- 기타 발견된 useState 초기화 수정 - -#### Phase 3: useEffect 내부 수정 (필요 시) -- useEffect 내부의 localStorage 접근은 SSR 안전 (클라이언트에서만 실행) -- 필요 시 체크 추가 - -#### Phase 4: 테스트 및 검증 -```bash -# 빌드 테스트 -npm run build - -# 타입 체크 -npm run type-check - -# 개발 서버 실행 -npm run dev -``` - -### 세션 재개 방법 - -#### 다음 세션 시작 시 -```bash -# 1. 이 문서 확인 -cat claudedocs/[SESSION-2025-11-18] localStorage-ssr-fix-checkpoint.md - -# 2. 작업 재개 -"localStorage SSR 수정 작업 이어서 진행해줘" -``` - -#### 또는 /sc:load 사용 -```bash -/sc:load -# 자동으로 이 체크포인트를 로드하여 작업 재개 -``` - -### 주의사항 - -#### 대용량 파일 작업 전략 -- ✅ **섹션별 작업**: 한 번에 1-2개 수정, 즉시 커밋 -- ✅ **빈번한 커밋**: 5분마다 WIP 커밋 -- ✅ **토큰 관리**: 불필요한 파일 Read 최소화 -- ❌ **한 번에 전체 수정 금지**: 세션 중단 위험 - -#### 세션 중단 방지 -```yaml -checkpoint_strategy: - interval: "5-10분마다 커밋" - pattern: "수정 → 커밋 → 수정 → 커밋" - max_continuous_work: "15분" -``` - -### 관련 문서 -- `[GUIDE] LARGE-FILE-WORKFLOW.md` - 대용량 파일 작업 가이드 -- `[REF] nextjs15-middleware-authentication-research.md` - SSR 호환성 참고 - -### 체크리스트 - -- [x] Phase 1: localStorage 사용 위치 전체 파악 ✅ -- [x] Phase 2-1: customTabs 수정 ✅ (이미 완료됨) -- [x] Phase 2-2: attributeSubTabs 수정 ✅ (이미 완료됨) -- [x] Phase 2-3: attributeColumns 수정 ✅ (이미 완료됨) -- [x] Phase 2-4: bomItems 수정 ✅ (이미 완료됨) -- [x] Phase 2-5: itemCategories 수정 ✅ (이미 완료됨) -- [x] Phase 2-6: unitOptions 수정 ✅ (이미 완료됨) -- [x] Phase 2-7: materialOptions 수정 ✅ (이미 완료됨) -- [x] Phase 2-8: surfaceTreatmentOptions 수정 ✅ (이미 완료됨) -- [x] Phase 2-9: customAttributeOptions 수정 ✅ (이미 완료됨) -- [x] Phase 3: useEffect 내부 체크 (안전 확인) ✅ -- [x] Phase 4-1: 빌드 테스트 ✅ (3.1초 성공) -- [x] Phase 4-2: 타입 체크 ✅ (빌드에 포함) -- [x] 최종 커밋 및 문서 업데이트 ⏳ - ---- - -**작업 완료 시간**: 2025-11-18 -**결과**: 모든 localStorage SSR 호환성 수정 완료 ✅ diff --git a/claudedocs/archive/qa-inbox-modal-test.png b/claudedocs/archive/qa-inbox-modal-test.png deleted file mode 100644 index 7f4d3e5b..00000000 Binary files a/claudedocs/archive/qa-inbox-modal-test.png and /dev/null differ diff --git a/claudedocs/archive/qa-reference-modal-test.png b/claudedocs/archive/qa-reference-modal-test.png deleted file mode 100644 index 36fe67b3..00000000 Binary files a/claudedocs/archive/qa-reference-modal-test.png and /dev/null differ diff --git a/claudedocs/archive/sessions/[NEXT-2025-11-26] item-master-api-pending-tasks.md b/claudedocs/archive/sessions/[NEXT-2025-11-26] item-master-api-pending-tasks.md deleted file mode 100644 index 7a61a4b9..00000000 --- a/claudedocs/archive/sessions/[NEXT-2025-11-26] item-master-api-pending-tasks.md +++ /dev/null @@ -1,227 +0,0 @@ -# 품목기준관리 - API 연동 작업 체크리스트 - -**작성일**: 2025-11-26 -**상태**: ✅ Phase 3 완료 -**마지막 업데이트**: 2025-11-26 API 연결 구현 완료 (Phase 3 ✅) - ---- - -## 1. 구조 변경 사항 - -- `section_templates` 테이블 삭제 → `item_sections.is_template=true`로 통합 -- `section_name` → `title`로 통일 (API와 동일) -- `bomItems` → `bom_items`로 통일 (API와 동일) -- `field_type`: API와 Frontend가 동일한 값 사용 ('textbox', 'number', 'dropdown' 등) - ---- - -## 2. API 연동 체크리스트 - -### 2.1 타입 정의 (src/types/item-master-api.ts) - -- [x] ItemSectionResponse에 is_template, is_default, description, group_id 추가 -- [x] IndependentSectionRequest 추가 -- [x] IndependentFieldRequest 추가 -- [x] IndependentBomItemRequest 추가 -- [x] SectionUsageResponse 추가 -- [x] FieldUsageResponse 추가 -- [x] LinkSectionRequest 추가 -- [x] LinkFieldRequest 추가 -- [x] PageStructureResponse 추가 -- [x] MasterFieldResponse에 is_common, default_value, options 추가 - -### 2.2 API 클라이언트 (src/lib/api/item-master.ts) - ✅ 완료 - -#### 독립 엔티티 API (완료) -- [x] `GET /sections` - `sections.list()` (is_template 필터 지원) -- [x] `POST /sections` - `sections.createIndependent()` -- [x] `POST /sections/{id}/clone` - `sections.clone()` -- [x] `GET /sections/{id}/usage` - `sections.getUsage()` -- [x] `GET /fields` - `fields.list()` -- [x] `POST /fields` - `fields.createIndependent()` -- [x] `POST /fields/{id}/clone` - `fields.clone()` -- [x] `GET /fields/{id}/usage` - `fields.getUsage()` -- [x] `GET /bom-items` - `bomItems.list()` -- [x] `POST /bom-items` - `bomItems.createIndependent()` - -#### 링크 관리 API (완료) -- [x] `POST /pages/{id}/link-section` - `pages.linkSection()` -- [x] `DELETE /pages/{id}/unlink-section/{sectionId}` - `pages.unlinkSection()` -- [x] `POST /sections/{id}/link-field` - `sections.linkField()` -- [x] `DELETE /sections/{id}/unlink-field/{fieldId}` - `sections.unlinkField()` -- [x] `GET /pages/{id}/structure` - `pages.getStructure()` - -#### 섹션 템플릿 API 수정 (완료) -- [x] `sections.list({ is_template: true })` 로 템플릿 조회 가능 - -### 2.3 Context 업데이트 (src/contexts/ItemMasterContext.tsx) - -#### 인터페이스 수정 (완료) -- [x] ItemSection 인터페이스에 title, group_id, is_template, is_default, description 추가 -- [x] ItemSection.section_name → title 변경 -- [x] ItemSection.bomItems → bom_items 변경 -- [x] ItemMasterField 인터페이스에 is_common, default_value, options, validation_rules, properties 추가 - -#### Transformer 수정 (완료) -- [x] transformSectionResponse: 새 필드 추가 (group_id, is_template, is_default, description) -- [x] transformMasterFieldResponse: 새 필드 추가 및 속성명 통일 -- [x] field_type 변환 제거 (API와 동일한 값 사용) - -#### TypeScript 오류 수정 (완료 ✅) -- [x] bomItems → bom_items 참조 수정 (addBOMItem, updateBOMItem, deleteBOMItem) -- [x] transformers.ts FIELD_TYPE_MAP 오류 수정 -- [x] transformPageResponse: order_no, description 추가 -- [x] ItemPageResponse: order_no, description 추가 -- [x] 전체 타입 검증 완료 - -#### 기능 추가 (완료 ✅) -- [x] 독립 섹션/필드/BOM 상태 추가 -- [x] 링크/언링크 메서드 추가 -- [x] 사용처 조회 메서드 추가 -- [x] 섹션 템플릿 로직 수정 (is_template 필터) -- [x] 복제 기능 (cloneSection, cloneField) - -### 2.4 계층구조(페이지) 탭 UI - ✅ 완료 - -- [x] 섹션 불러오기 다이얼로그 (ImportSectionDialog.tsx) -- [x] 필드 불러오기 다이얼로그 (ImportFieldDialog.tsx) -- [x] 불러오기 버튼 추가 (HierarchyTab) -- [x] 사용처 표시 UI (다이얼로그 내 Usage Info Panel) - -### 2.5 섹션 탭 UI - ✅ 완료 - -- [x] 섹션 복제(Clone) 버튼 추가 (SectionsTab.tsx) -- [x] 필드 불러오기(Import Field) 버튼 추가 (SectionsTab.tsx) -- [x] ItemMasterDataManagement에서 props 연결 (handleCloneSection, setIsImportFieldDialogOpen) -- [x] TypeScript 오류 수정: - - section_name → title 변경 (useSectionManagement, useTemplateManagement, DraggableSection, FieldDrawer, ConditionalDisplayUI) - - bomItems → bom_items 변경 (hooks 파일들) - - is_template, is_default 필수 속성 추가 - -### 2.6 마스터 항목 탭 UI - ✅ 완료 - -- [x] 기본 CRUD UI 구현됨 (MasterFieldTab/index.tsx) -- [x] 필드 타입 배지 표시 -- [x] 필수 여부, 카테고리, 속성 타입 배지 표시 -- [x] 옵션 목록 표시 - ---- - -## 3. Phase 3: API 연결 구현 - ✅ 완료 - -> **분석 결과**: 모든 API 연결이 이미 Context에서 완료되어 있습니다. - -### 3.1 초기화 API 연결 - ✅ 완료 - -- [x] `/v1/item-master/init` API 호출 구현 (ItemMasterDataManagement.tsx:301-361) -- [x] Context `loadItemPages`, `loadSectionTemplates`, `loadItemMasterFields` 메서드 연결 -- [x] 로딩 상태 관리 UI (LoadingSpinner, ErrorMessage) - -### 3.2 페이지 CRUD API 연결 - ✅ 완료 - -- [x] 페이지 생성 API 연결 (`addItemPage` → `itemMasterApi.pages.create()`) -- [x] 페이지 수정 API 연결 (`updateItemPage` → `itemMasterApi.pages.update()`) -- [x] 페이지 삭제 API 연결 (`deleteItemPage` → `itemMasterApi.pages.delete()`) -- [x] 페이지 순서 변경 API 연결 (`reorderPages` → `itemMasterApi.pages.reorder()`) -- [x] 섹션 링크/언링크 API 연결 (`linkSectionToPage`, `unlinkSectionFromPage`) - -### 3.3 섹션 CRUD API 연결 - ✅ 완료 - -- [x] 섹션 생성 API 연결 (`addSectionToPage` → `itemMasterApi.sections.create()`) -- [x] 섹션 수정 API 연결 (`updateSection` → `itemMasterApi.sections.update()`) -- [x] 섹션 삭제/언링크 API 연결 (`deleteSection` → `itemMasterApi.sections.delete()`) -- [x] 섹션 순서 변경 API 연결 (`reorderSections` → `itemMasterApi.sections.reorder()`) -- [x] 독립 섹션 생성 (`createIndependentSection` → `itemMasterApi.sections.createIndependent()`) -- [x] 섹션 복제 (`cloneSection` → `itemMasterApi.sections.clone()`) -- [x] 섹션 사용처 조회 (`getSectionUsage` → `itemMasterApi.sections.getUsage()`) -- [x] 필드 링크/언링크 API 연결 (`linkFieldToSection`, `unlinkFieldFromSection`) - -### 3.4 필드 CRUD API 연결 - ✅ 완료 - -- [x] 필드 생성 API 연결 (`addFieldToSection` → `itemMasterApi.fields.create()`) -- [x] 필드 수정 API 연결 (`updateField` → `itemMasterApi.fields.update()`) -- [x] 필드 삭제/언링크 API 연결 (`deleteField` → `itemMasterApi.fields.delete()`) -- [x] 필드 순서 변경 API 연결 (`reorderFields` → `itemMasterApi.fields.reorder()`) -- [x] 독립 필드 생성 (`createIndependentField` → `itemMasterApi.fields.createIndependent()`) -- [x] 필드 복제 (`cloneField` → `itemMasterApi.fields.clone()`) -- [x] 필드 사용처 조회 (`getFieldUsage` → `itemMasterApi.fields.getUsage()`) - -### 3.5 마스터 필드 CRUD API 연결 - ✅ 완료 - -- [x] 마스터 필드 생성 API 연결 (`addItemMasterField` → `itemMasterApi.masterFields.create()`) -- [x] 마스터 필드 수정 API 연결 (`updateItemMasterField` → `itemMasterApi.masterFields.update()`) -- [x] 마스터 필드 삭제 API 연결 (`deleteItemMasterField` → `itemMasterApi.masterFields.delete()`) - -### 3.6 BOM CRUD API 연결 - ✅ 완료 - -- [x] BOM 생성 API 연결 (`addBOMItem` → `itemMasterApi.bomItems.create()`) -- [x] BOM 수정 API 연결 (`updateBOMItem` → `itemMasterApi.bomItems.update()`) -- [x] BOM 삭제 API 연결 (`deleteBOMItem` → `itemMasterApi.bomItems.delete()`) -- [x] 독립 BOM 생성 (`createIndependentBomItem` → `itemMasterApi.bomItems.createIndependent()`) - -### Hooks → Context 연결 현황 - ✅ 완료 - -| Hook | Context 함수 | 상태 | -|------|-------------|------| -| usePageManagement | `addItemPage`, `updateItemPage`, `deleteItemPage` | ✅ | -| useSectionManagement | `addSectionToPage`, `updateSection`, `deleteSection` | ✅ | -| useFieldManagement | `addFieldToSection`, `updateField`, `deleteField` | ✅ | -| useMasterFieldManagement | `addItemMasterField`, `updateItemMasterField`, `deleteItemMasterField` | ✅ | - ---- - -## 4. 삭제 vs 연결해제 정리 - -``` -[계층구조 탭에서] -├─ 페이지 삭제 → 실제 삭제 (Cascade) -├─ 섹션 제거 → 연결만 끊기 (unlink), 섹션 데이터는 유지 -└─ 항목 제거 → 연결만 끊기 (unlink), 항목 데이터는 유지 - -[섹션 탭에서] -├─ 섹션 삭제 → 실제 삭제 (Cascade) -└─ 항목 삭제 → 실제 삭제 - -[마스터 항목 탭에서] -└─ 마스터 항목 삭제 → 실제 삭제 -``` - ---- - -## 4. 데이터 연결 구조 - -``` -독립 필드 (fields, section_id=null) - │ - ├──[link-field]──→ 섹션에 연결 - │ ↓ -독립 섹션 (sections, page_id=null) - │ - ├──[link-section]──→ 페이지에 연결 - │ ↓ -페이지 (pages) = 품목유형별 필드 구성 -``` - ---- - -## 5. 핵심 개념 - -> **"페이지"는 실제 URL 경로가 아니라, 품목유형별 필드 구성 템플릿이다!** - -``` -품목기준관리의 "페이지" - = 품목유형(FG, PT, SM, RM, CS)별로 - = 품목 등록 시 어떤 섹션/필드를 보여줄지 정의하는 템플릿 -``` - ---- - -## 6. 참고 문서 - -- `claudedocs/[ANALYSIS-2025-11-21] item-master-notes.md` - 이전 API 문서 -- `claudedocs/[ANALYSIS-2025-11-26] item-master-notes.md` - 신규 API 문서 -- `~/Desktop/코브라브릿지백엔드문서/[API-2025-11-26] item-master-api-changes.md` - API 변경사항 - ---- - -**마지막 업데이트**: 2025-11-26 작업 시작 diff --git a/claudedocs/archive/sessions/[NEXT-2025-11-26] item-master-pending-integration.md b/claudedocs/archive/sessions/[NEXT-2025-11-26] item-master-pending-integration.md deleted file mode 100644 index e0e3eb89..00000000 --- a/claudedocs/archive/sessions/[NEXT-2025-11-26] item-master-pending-integration.md +++ /dev/null @@ -1,106 +0,0 @@ -# 품목기준관리 - 백엔드 통합 대기 작업 - -**작성일**: 2025-11-26 -**상태**: 백엔드 통합 작업 대기 중 - ---- - -## 현재 상황 요약 - -### 해결된 이슈 - -1. **섹션 순서 변경 422 에러** - - 원인: 백엔드가 `items` 필드를 기대하는데 프론트가 `section_orders` 전송 - - 수정 파일: - - `src/types/item-master-api.ts` - `SectionReorderRequest.items`로 변경 - - `src/contexts/ItemMasterContext.tsx` - `reorderSections` 함수 수정 - -2. **response.data.map is not a function 에러** - - 원인: 백엔드 응답이 배열이 아닌 경우 처리 누락 - - 수정: 배열/비배열 응답 모두 처리하도록 조건문 추가 - -3. **불러오기 다이얼로그에 마스터 항목 미표시** - - 수정 파일: - - `src/components/items/ItemMasterDataManagement/dialogs/ImportFieldDialog.tsx` - - `src/components/items/ItemMasterDataManagement.tsx` - - 변경 내용: 마스터 항목 / 독립 필드 탭 분리 - ---- - -## 백엔드 통합 대기 중인 이슈 - -### 데이터 동기화 문제 - -**현상**: -- 계층구조에서 섹션 내 항목 생성 시 → 마스터항목 탭, 속성 탭, 불러오기에도 표시됨 -- 원인: `GET /v1/item-master/fields` API가 모든 필드를 반환 (독립 필드만 반환해야 함) - -**백엔드 요청 사항**: -1. `GET /v1/item-master/fields` → `section_id IS NULL`인 필드만 반환 -2. 마스터 항목 + 섹션 필드 통합 구조 검토 - -### 현재 데이터 구조 (분리됨) - -``` -item_master_fields 테이블 (마스터 항목) -├─ 항목 탭에서 생성/관리 -├─ 속성 탭 서브탭으로 표시 -└─ 불러오기 시 "복사"하여 새 필드 생성 - -item_fields 테이블 (실제 필드) -├─ section_id != null → 섹션 필드 (계층구조/섹션 탭) -└─ section_id = null → 독립 필드 (불러오기에서 "연결") -``` - -### 예상되는 통합 구조 (백엔드 작업 중) - -``` -통합된 필드 테이블 -├─ is_master = true → 마스터 필드 (템플릿) -├─ section_id != null → 섹션 필드 -└─ section_id = null, is_master = false → 독립 필드 - -→ 마스터 필드 수정 시 연결된 모든 필드에 반영 -``` - ---- - -## 프론트엔드 수정 필요 사항 (백엔드 통합 후) - -### 1. API 응답 구조 변경 대응 -- `InitResponse` 타입 수정 (통합된 필드 구조) -- `transformers.ts` 변환 로직 수정 - -### 2. Context 수정 -- `itemMasterFields` vs `independentFields` 통합 가능성 -- 필드 CRUD 함수 통합 - -### 3. UI 수정 -- ImportFieldDialog 탭 구조 재검토 (통합되면 탭 불필요할 수 있음) -- 데이터 동기화 로직 단순화 - ---- - -## 관련 파일 목록 - -### 수정된 파일 (2025-11-26) -- `src/types/item-master-api.ts` -- `src/contexts/ItemMasterContext.tsx` -- `src/lib/api/error-handler.ts` -- `src/components/items/ItemMasterDataManagement.tsx` -- `src/components/items/ItemMasterDataManagement/dialogs/ImportFieldDialog.tsx` - -### 참고할 파일 -- `src/lib/api/item-master.ts` - API 호출 함수 -- `src/lib/api/transformers.ts` - 응답 변환 함수 -- `src/components/items/ItemMasterDataManagement/hooks/useTabManagement.ts` - 속성 탭 생성 로직 - ---- - -## 다음 작업 체크리스트 - -- [ ] 백엔드 통합 API 완료 확인 -- [ ] 새 API 응답 구조 확인 및 타입 수정 -- [ ] Context 데이터 구조 통합 -- [ ] ImportFieldDialog 통합 여부 결정 -- [ ] 테스트: 마스터 항목 수정 → 연결된 필드 동기화 확인 \ No newline at end of file diff --git a/claudedocs/archive/sessions/[NEXT-2025-12-06] item-crud-session-context.md b/claudedocs/archive/sessions/[NEXT-2025-12-06] item-crud-session-context.md deleted file mode 100644 index 5c6580fc..00000000 --- a/claudedocs/archive/sessions/[NEXT-2025-12-06] item-crud-session-context.md +++ /dev/null @@ -1,80 +0,0 @@ -# 다음 세션 컨텍스트 - 품목관리 기능 개발 - -> 2025-12-06 세션에서 진행한 내용 및 다음 세션에서 이어갈 작업 - ---- - -## 완료된 작업 - -### 1. 삭제 알럿 제거 ✅ -- 품목 테이블에서 삭제 버튼 클릭 → 모달 확인 → 바로 삭제 (알럿 없이) -- 파일: `src/components/items/ItemListClient.tsx` - -### 2. 디버깅 콘솔 로그 제거 ✅ -- `DropdownField.tsx` - 단위 필드 디버깅 로그 제거 -- `useConditionalDisplay.ts` - 조건부 표시 디버깅 로그 제거 -- `useDynamicFormState.ts` - resetForm 디버깅 로그 제거 - ---- - -## 발견된 문제 (백엔드 수정 필요) - -### 소모품(CS) 규격(specification) 저장 안됨 🔴 - -**원인 분석 완료**: -1. 프론트엔드: `97_specification` → `spec` → `specification`으로 정상 변환됨 -2. 백엔드 문제: `ItemStoreRequest.php`의 validation rules에 `specification` 필드가 없음 -3. Laravel FormRequest는 rules에 없는 필드를 `$request->validated()`에서 제외 -4. 결과: DB에 규격이 null로 저장됨 - -**백엔드 수정 요청**: -```php -// /app/Http/Requests/Item/ItemStoreRequest.php -// rules()에 추가 필요: -'specification' => 'nullable|string|max:255', -``` - -**상세 문서**: `claudedocs/item-master/[API-2025-12-06] item-crud-backend-requests.md` - ---- - -## 다음 세션에서 확인할 사항 - -1. **백엔드 수정 후 테스트** - - 소모품 등록 시 규격 값 저장 확인 - - 상세 페이지에서 규격 표시 확인 - -2. **수정 API도 확인 필요** - - `ItemUpdateRequest.php`에도 `specification` 필드 있는지 확인 - - 어제 "수정하면 규격이 보였다"고 했는데, 수정 API는 다를 수 있음 - -3. **추가 편의 기능 개발** (사용자 요청 시) - - 품목관리 관련 추가 기능 구현 - ---- - -## 관련 파일 위치 - -### 프론트엔드 -- `src/components/items/ItemListClient.tsx` - 품목 목록/삭제 -- `src/components/items/ItemDetailClient.tsx` - 품목 상세 -- `src/components/items/DynamicItemForm/index.tsx` - 동적 폼 -- `src/app/[locale]/(protected)/items/create/page.tsx` - 등록 페이지 -- `src/app/[locale]/(protected)/items/[id]/edit/page.tsx` - 수정 페이지 -- `src/app/[locale]/(protected)/items/[id]/page.tsx` - 상세 페이지 - -### 백엔드 (sam-api) -- `/app/Http/Requests/Item/ItemStoreRequest.php` - 등록 요청 검증 ⚠️ 수정 필요 -- `/app/Http/Requests/Item/ItemUpdateRequest.php` - 수정 요청 검증 (확인 필요) -- `/app/Services/ItemsService.php` - 품목 서비스 -- `/app/Models/Materials/Material.php` - Material 모델 - ---- - -## 명령어 - -```bash -# 프론트엔드 개발 서버 -cd /Users/byeongcheolryu/codebridgex/sam_project/sam-next/sma-next-project/sam-react-prod -npm run dev -``` \ No newline at end of file diff --git a/claudedocs/archive/sessions/[NEXT-2025-12-09] client-session-context.md b/claudedocs/archive/sessions/[NEXT-2025-12-09] client-session-context.md deleted file mode 100644 index 20b3a5ba..00000000 --- a/claudedocs/archive/sessions/[NEXT-2025-12-09] client-session-context.md +++ /dev/null @@ -1,143 +0,0 @@ -# 거래처 관리 - 다음 세션 컨텍스트 - -> **작성일**: 2025-12-09 -> **목적**: 다음 세션에서 이어서 작업할 내용 정리 - ---- - -## 1. 완료된 작업 (2025-12-09) - -### 1.1 백엔드 API 2차 필드 추가 ✅ 완료 -- **커밋**: `d164bb4` - feat: [client] 거래처 API 2차 필드 추가 -- **마이그레이션**: `2025_12_04_205603_add_extended_fields_to_clients_table.php` -- **is_active 타입 변경**: `5f20005` - CHAR(1) → TINYINT(1) Boolean - -### 1.2 프론트엔드 API 연동 ✅ 완료 - -| 구성 요소 | 파일 | 상태 | -|----------|------|------| -| **Proxy** | `/api/proxy/[...path]/route.ts` | ✅ 완료 | -| **Hook** | `src/hooks/useClientList.ts` | ✅ 완료 (2차 필드 포함) | -| **목록** | `sales/client-management-sales-admin/page.tsx` | ✅ API 연동 | -| **등록** | `sales/client-management-sales-admin/new/page.tsx` | ✅ API 연동 | -| **수정** | `sales/client-management-sales-admin/[id]/edit/page.tsx` | ✅ API 연동 | -| **상세** | `sales/client-management-sales-admin/[id]/page.tsx` | ✅ API 연동 | -| **등록폼** | `components/clients/ClientRegistration.tsx` | ✅ 완료 | -| **상세뷰** | `components/clients/ClientDetail.tsx` | ✅ 완료 | - -### 1.3 is_active Boolean 변경 대응 ✅ 완료 -```typescript -// useClientList.ts 수정 내역 -is_active: boolean; // 타입 변경 ("Y"|"N" → boolean) -status: api.is_active ? "활성" : "비활성", // 변환 로직 -is_active: form.isActive, // 전송 시 boolean 그대로 -``` - -### 1.4 기획 미확정 필드 - 개발 보류 ✅ 확정 -**결정일**: 2025-12-09 -**결정사항**: 기획에서 확정되지 않은 필드에 대해서는 **개발 보류** - -**숨김 처리된 섹션** (등록/수정 폼): -| 섹션 | 필드 | 상태 | -|------|------|------| -| 발주처 설정 | 계정ID, 비밀번호, 매입결제일, 매출결제일 | ❌ 개발보류 | -| 약정 세금 | 약정 여부, 금액, 시작/종료일 | ❌ 개발보류 | -| 악성채권 정보 | 악성채권 여부, 금액, 발생/만료일, 진행상태 | ❌ 개발보류 | - -**목록 테이블에서도 제외** (스크린샷 디자인과 다름 확인): -- 매입 결제일 컬럼 ❌ 제외 -- 매출 결제일 컬럼 ❌ 제외 -- 악성채권 컬럼/배지 ❌ 제외 - -> ⚠️ 백엔드 API는 이미 지원됨 (nullable 필드) -> 기획 확정 후 주석 해제하면 바로 사용 가능 -> 프론트엔드 파일: `src/components/clients/ClientRegistration.tsx` - ---- - -## 2. 현재 거래처 등록 폼 구조 - -``` -1. 기본 정보 ✅ 활성 - - 사업자등록번호 (필수) - - 거래처 코드 (자동생성) - - 거래처명 (필수) - - 대표자명 (필수) - - 거래처 유형 (매입/매출/매입매출) - - 업태, 종목 - -2. 연락처 정보 ✅ 활성 - - 주소 - - 전화번호, 모바일, 팩스 - - 이메일 - -3. 담당자 정보 ✅ 활성 - - 담당자명, 담당자 전화 - - 시스템 관리자 - -4. 발주처 설정 ❌ 숨김 (기획 미확정) -5. 약정 세금 ❌ 숨김 (기획 미확정) -6. 악성채권 정보 ❌ 숨김 (기획 미확정) - -7. 기타 정보 ✅ 활성 - - 메모 - - 상태 (활성/비활성) -``` - ---- - -## 3. 다음 작업 후보 - -### 3.1 거래처 관리 관련 -- [ ] 거래처 그룹 기능 구현 (client-groups API 이미 있음) -- [ ] 엑셀 내보내기/가져오기 -- [ ] 발주처/약정세금/악성채권 섹션 활성화 (기획 확정 시) - -### 3.2 다른 기능 -- [ ] 견적 관리 페이지 구현 -- [ ] 단가 관리 페이지 완성 -- [ ] 기타 요청 사항 - ---- - -## 4. 참고 파일 - -### 프론트엔드 (sam-react-prod) -``` -src/ -├── hooks/useClientList.ts # API 훅 + 타입 정의 -├── components/clients/ -│ ├── ClientRegistration.tsx # 등록/수정 폼 -│ └── ClientDetail.tsx # 상세 보기 -└── app/[locale]/(protected)/sales/client-management-sales-admin/ - ├── page.tsx # 목록 - ├── new/page.tsx # 등록 - ├── [id]/page.tsx # 상세 - └── [id]/edit/page.tsx # 수정 -``` - -### 백엔드 (sam-api) -``` -app/ -├── Http/Controllers/Api/V1/ClientController.php -├── Http/Requests/Client/ -│ ├── ClientStoreRequest.php -│ └── ClientUpdateRequest.php -├── Models/Orders/Client.php -├── Services/ClientService.php -└── Swagger/v1/ClientApi.php -``` - ---- - -## 5. 다음 세션에서 말할 내용 - -``` -버디 안녕~! 지난번에 거래처 관리 API 연동 완료했어. -- 백엔드 2차 필드 추가 확인 완료 -- 프론트엔드 API 연동 완료 (목록/등록/수정/상세) -- is_active Boolean 변경 대응 완료 -- 발주처/약정세금/악성채권 섹션은 기획 미확정으로 숨김 처리 - -오늘은 [다음 작업 내용] 진행하자~! -``` \ No newline at end of file diff --git a/claudedocs/archive/sessions/[NEXT-2025-12-09] item-crud-session-context.md b/claudedocs/archive/sessions/[NEXT-2025-12-09] item-crud-session-context.md deleted file mode 100644 index 0f94b242..00000000 --- a/claudedocs/archive/sessions/[NEXT-2025-12-09] item-crud-session-context.md +++ /dev/null @@ -1,120 +0,0 @@ -# 품목관리 세션 체크포인트 - -> 작성일: 2025-12-09 -> 수정일: 2025-12-09 -> 상태: ✅ Phase 1 완료! - ---- - -## 🎉 2025-12-09 완료 사항 - -### 백엔드 작업 완료 - -| 항목 | 상태 | -|------|------| -| field_key 저장 방식 변경 (`98_unit` → `unit`) | ✅ 완료 | -| 시스템 예약어 검증 (`SystemFields.php`) | ✅ 완료 | -| 중복 검증 로직 | ✅ 완료 | -| 에러 메시지 한국어화 | ✅ 완료 | - -### 프론트엔드 정리 완료 - -| 항목 | 삭제된 코드 | 상태 | -|------|------------|------| -| Edit 모드 매핑 로직 | ~140줄 | ✅ 완료 | -| `fieldAliases` 객체 | 25줄 | ✅ 완료 | -| `extractFieldName()` 함수 | 7줄 | ✅ 완료 | -| `fieldKeyMap` 생성 로직 | 25줄 | ✅ 완료 | -| `fieldKeyToBackendKey` 변환 | 60줄 | ✅ 완료 | -| **총 삭제** | **~200줄** | ✅ | - -### 빌드 검증 - -```bash -npm run build # ✅ 성공 -``` - ---- - -## 📋 새로운 데이터 흐름 - -### field_key 통일 완료 - -``` -등록: { "unit": "EA" } → 그대로 저장 -조회: DB → { "unit": "EA" } → 그대로 표시 -수정: { "unit": "EA" } → 그대로 저장 - -※ 기존 레거시 데이터 (98_unit 형식)도 그대로 동작 -``` - -### 코드 변경 요약 - -**Before (복잡한 매핑)**: -```typescript -// Edit 모드: 155줄 매핑 로직 -const fieldAliases = { 'unit': '단위', ... }; -const extractFieldName = (key) => { ... }; -const fieldKeyMap = { ... }; -// 여러 단계 변환... -``` - -**After (직접 사용)**: -```typescript -// Edit 모드: 15줄 -useEffect(() => { - if (mode !== 'edit' || !structure || !initialData || isEditDataMapped) return; - resetForm(initialData); // 직접 사용! - setIsEditDataMapped(true); -}, [mode, structure, initialData, isEditDataMapped, resetForm]); -``` - ---- - -## ⏳ 남은 작업 - -### 파일 업로드 500 에러 (검수 중) - -``` -위치: /app/Http/Controllers/Api/V1/ItemsFileController.php (Line 7) -문제: use App\Http\Responses\ApiResponse (잘못된 경로) -수정: use App\Helpers\ApiResponse (올바른 경로) -``` - -### Phase 2: 컴포넌트 분리 (선택적) - -계획 문서: `[PLAN-2025-12-08] dynamic-form-separation-plan.md` - -- 공통 컴포넌트 추출 (FileUpload, BOM, AutoItemCode) -- 품목별 컴포넌트 생성 (FG, PT, SM, RM, CS) -- DynamicFormCore 리팩토링 - ---- - -## 📋 테스트 체크리스트 - -### 등록 테스트 -- [ ] FG(제품) 등록 -- [ ] PT-조립부품 등록 -- [ ] PT-절곡부품 등록 -- [ ] SM/RM/CS 등록 - -### 수정 테스트 -- [ ] 수정 페이지 진입 → 데이터 로드 확인 -- [ ] 드롭다운 값 표시 확인 -- [ ] 수정 후 저장 → 값 유지 확인 - -### 파일 업로드 테스트 -- [ ] 절곡부품 전개도 업로드 -- [ ] 조립부품 전개도 업로드 -- [ ] 제품 시방서/인정서 업로드 - ---- - -## 📚 관련 문서 - -| 문서 | 위치 | -|------|------| -| DynamicForm 분리 계획 | `[PLAN-2025-12-08] dynamic-form-separation-plan.md` | -| Radix UI 버그 해결 | `claudedocs/guides/[FIX-2025-12-05] radix-ui-select-controlled-mode-bug.md` | -| 백엔드 field_key 검증 스펙 | `sam-api/docs/specs/item-master-field-key-validation.md` | diff --git a/claudedocs/archive/sessions/[NEXT-2025-12-10] item-crud-session-context.md b/claudedocs/archive/sessions/[NEXT-2025-12-10] item-crud-session-context.md deleted file mode 100644 index 838823bf..00000000 --- a/claudedocs/archive/sessions/[NEXT-2025-12-10] item-crud-session-context.md +++ /dev/null @@ -1,119 +0,0 @@ -# 품목관리 세션 체크포인트 - -> 작성일: 2025-12-10 -> 이전 세션: 2025-12-09 -> 상태: ✅ 프론트엔드 수정 완료 - ---- - -## 🎯 오늘의 작업 목표 - -### 백엔드 변경 사항 (완료됨) - -**field_key 통일 방식:** -- **기존**: 프론트엔드에서 `unit` → `98_unit` 변환 후 저장 -- **변경**: 백엔드에서 field_key를 그대로 저장/반환 - - 기존 레거시 데이터(`98_unit` 형식)도 그대로 동작 - - 신규 등록 시 `unit`으로 등록하면 `unit`으로 저장 - - 중복 field_key는 백엔드에서 자동 처리 (suffix 추가 또는 사용자 변경) - -**핵심 포인트**: 프론트엔드에서 변환 없이, 백엔드가 주는 값 그대로 사용! - ---- - -## ✅ 완료된 작업 - -### 1. 수정 페이지 `mapApiResponseToFormData` 개선 - -**파일**: `src/app/[locale]/(protected)/items/[id]/edit/page.tsx` - -**변경 내용**: -- 하드코딩된 필드 매핑 제거 (약 90줄 → 50줄) -- 백엔드 응답의 모든 필드를 그대로 formData에 복사 -- 시스템 필드만 제외 (`id`, `tenant_id`, `created_at`, `updated_at`, `deleted_at` 등) - -```typescript -// 변경 후: 백엔드 응답을 그대로 사용 -function mapApiResponseToFormData(data: ItemApiResponse): DynamicFormData { - const formData: DynamicFormData = {}; - const excludeKeys = ['id', 'tenant_id', 'category_id', 'category', - 'created_at', 'updated_at', 'deleted_at', 'component_lines', 'bom']; - - Object.entries(data).forEach(([key, value]) => { - if (!excludeKeys.includes(key) && value !== null && value !== undefined) { - formData[key] = value; - } - }); - - // attributes, options 처리... - return formData; -} -``` - -### 2. item_type 파라미터 수정 - -**변경 파일**: -- `src/app/[locale]/(protected)/items/[id]/page.tsx` (상세 페이지) -- `src/app/[locale]/(protected)/items/[id]/edit/page.tsx` (수정 페이지) - -**변경 내용**: -- 기존: `item_type=MATERIAL` -- 변경: `item_type=SM` / `item_type=RM` / `item_type=CS` (실제 코드 전달) - -```typescript -// 변경 후 -queryParams.append('item_type', itemType); // SM, RM, CS 그대로 전달 -``` - -### 3. 삭제 API item_type 파라미터 추가 - -**파일**: `src/components/items/ItemListClient.tsx` - -**변경 내용**: -- 단건 삭제: `?item_type=${itemToDelete.itemType}` 추가 -- 일괄 삭제: `?item_type=${item?.itemType}` 추가 - -### 4. 빌드 검증 - -```bash -npm run build # ✅ 성공 -``` - ---- - -## 📋 테스트 체크리스트 - -### 등록 테스트 -- [ ] FG(제품) 등록 → 데이터 표시 확인 -- [ ] PT-조립부품 등록 → 데이터 표시 확인 -- [ ] PT-절곡부품 등록 → 데이터 표시 확인 -- [ ] SM/RM/CS 등록 → 데이터 표시 확인 - -### 수정 테스트 -- [ ] 수정 페이지 진입 → 모든 필드 데이터 로드 확인 -- [ ] 드롭다운 값 정상 표시 확인 -- [ ] 수정 후 저장 → 값 유지 확인 - -### 삭제 테스트 -- [ ] 단건 삭제 (SM/RM/CS) -- [ ] 일괄 삭제 (SM/RM/CS) - ---- - -## 🔄 코드 변경 요약 - -| 파일 | 변경 내용 | -|------|----------| -| `items/[id]/page.tsx` | item_type 파라미터: MATERIAL → 실제 코드 | -| `items/[id]/edit/page.tsx` | mapApiResponseToFormData 간소화, item_type 파라미터 수정 | -| `ItemListClient.tsx` | 삭제 API에 item_type 파라미터 추가 (단건/일괄) | - ---- - -## 📚 관련 문서 - -| 문서 | 위치 | -|------|------| -| 이전 세션 컨텍스트 | `[NEXT-2025-12-09] item-crud-session-context.md` | -| DynamicForm 분리 계획 | `[PLAN-2025-12-08] dynamic-form-separation-plan.md` | -| Radix UI 버그 해결 | `claudedocs/guides/[FIX-2025-12-05] radix-ui-select-controlled-mode-bug.md` | \ No newline at end of file diff --git a/claudedocs/archive/sessions/[NEXT-2025-12-12] item-crud-session-context.md b/claudedocs/archive/sessions/[NEXT-2025-12-12] item-crud-session-context.md deleted file mode 100644 index 5808ea2a..00000000 --- a/claudedocs/archive/sessions/[NEXT-2025-12-12] item-crud-session-context.md +++ /dev/null @@ -1,205 +0,0 @@ -# 품목관리 세션 체크포인트 - -> 작성일: 2025-12-12 -> 이전 세션: 2025-12-10 -> 상태: ✅ 프론트엔드 작업 완료 (백엔드 API 대기) - ---- - -## 🎯 오늘의 작업 목표 - -### 전개도 상세 입력 (폭 합계 연동) - ✅ 완료 -### BOM 테이블 UI 수정 - ✅ 완료 -### BOM 데이터 전송/로드 - ✅ 완료 -### 파일 업로드 오류 수정 - ✅ 완료 - ---- - -## ✅ 완료된 작업 - -### 1. BOM API 연동 완료 - -**변경 사항**: -- `child_item_type` 필드 추가 (PRODUCT/MATERIAL 구분) -- BOM 저장 형식: `{ child_item_id, child_item_type, quantity }` 최소 필드만 저장 -- 품목 유형 매핑: FG/PT → PRODUCT, SM/RM/CS → MATERIAL - -**수정 파일**: -- `types.ts`: BOMLine에 `childItemType` 필드 추가 -- `DynamicBOMSection.tsx`: `getChildItemType()` 헬퍼 함수 추가 -- `index.tsx`: BOM 전송 형식 간소화 - -### 2. 수정 화면 BOM 로드 버그 수정 - -**문제**: `mapApiResponseToFormData`가 `bom`을 제외하여 `initialData`에 BOM이 없었음 - -**해결**: `initialBomLines` prop으로 BOM 데이터 별도 전달 - -**수정 파일**: -- `types.ts`: `DynamicItemFormProps`에 `initialBomLines?: BOMLine[]` 추가 -- `edit/page.tsx`: API 응답에서 BOM 추출 → `initialBomLines` state → prop 전달 -- `index.tsx`: `initialBomLines` prop 수신 → useEffect로 `setBomLines()` 호출 - -### 3. BOM key 값 중복 에러 수정 - -**문제**: BOM 항목에 id가 없을 때 빈 문자열로 key 중복 발생 - -**해결**: `page.tsx`의 `mapApiResponseToItemMaster`에서 fallback key 생성 -```typescript -id: String(bomItem.id || bomItem.child_item_id || `bom-${index}`) -``` - -### 4. 이미지 업로드 500 에러 수정 - -**문제**: `bending_diagram`에 Base64 이미지 데이터가 JSON 본문에 포함되어 백엔드 500 에러 - -**해결**: API 호출 전 base64 이미지 데이터 제거 (파일은 별도 API로 업로드) - -**수정 파일**: -- `edit/page.tsx`: base64 이미지 필드 제거 로직 추가 -- `create/page.tsx`: 동일하게 적용 - -```typescript -// API 호출 전 이미지 데이터 제거 -if (submitData.bending_diagram?.startsWith('data:')) delete submitData.bending_diagram; -if (submitData.specification_file?.startsWith('data:')) delete submitData.specification_file; -if (submitData.certification_file?.startsWith('data:')) delete submitData.certification_file; -``` - -### 5. bending_details 배열 전송 오류 수정 - -**문제**: `JSON.stringify()`로 문자열 전송 → 백엔드에서 "배열이어야 합니다" 오류 - -**해결**: PHP가 이해하는 배열 형태로 FormData 전송 - -**수정 파일**: `src/lib/api/items.ts` - -```typescript -// 기존 (문자열) -formData.append('bending_details', JSON.stringify(options.bendingDetails)); - -// 수정 (배열 형태) -options.bendingDetails.forEach((detail, index) => { - Object.entries(detail).forEach(([key, value]) => { - formData.append(`bending_details[${index}][${key}]`, String(value)); - }); -}); -``` - -### 6. 제품 정보 섹션 빈 카드 숨김 - -**문제**: FG 품목 상세에서 "제품 정보" 섹션이 내용 없이 빈 카드로 표시 - -**해결**: 내용이 있을 때만 섹션 표시 - -**수정 파일**: `ItemDetailClient.tsx` - -```typescript -// 기존 -{item.itemType === 'FG' && ( - -// 수정 -{item.itemType === 'FG' && (item.productCategory || item.lotAbbreviation || item.note) && ( -``` - ---- - -## ⏳ 백엔드 대기 사항 - -### 1. 파일 다운로드 인증 문제 🔴 - -**현재 문제**: -- 파일 다운로드 URL(`/storage/{id}`)에 직접 접근 시 `"Unauthorized. Invalid or missing API key"` 에러 -- 브라우저에서 `다운로드` 클릭 시 API 키가 없어서 401 에러 -- 시방서(PDF), 인정서(PDF), 전개도(이미지) 모두 동일한 문제 - -**수정 요청 옵션**: -1. **옵션 A (권장)**: Signed URL 방식 - 임시 토큰이 포함된 URL 생성 (만료 시간 설정) -2. **옵션 B**: 파일 다운로드 엔드포인트를 public으로 변경 (인증 불필요) -3. **옵션 C**: 프론트엔드 프록시 경유 (Next.js API route에서 API 키 추가) - -### 2. 품목 조회 시 파일 URL 미반환 문제 🔴 - -**현재 문제**: -- `bending_diagram`, `specification_file`, `certification_file` 필드에 **file_id(숫자)**만 반환됨 -- 프론트엔드에서 이미지/PDF를 표시하려면 **실제 다운로드 URL**이 필요 -- 현재 file_id만 있어서 파일을 불러올 수 없음 - -**수정 요청**: -품목 조회 응답에서 file_id와 함께 실제 URL도 반환: - -```json -{ - "id": 813, - "bending_diagram": 123, - "bending_diagram_url": "/api/v1/files/download/xxx", - "specification_file": 456, - "specification_file_url": "/api/v1/files/download/yyy", - "certification_file": 789, - "certification_file_url": "/api/v1/files/download/zzz" -} -``` - ---- - -## 🔄 코드 변경 요약 - -| 파일 | 변경 내용 | -|------|----------| -| `types.ts` | BOMLine에 `childItemType` 추가, DynamicItemFormProps에 `initialBomLines` 추가 | -| `DynamicBOMSection.tsx` | `getChildItemType()` 헬퍼 함수, 품목 선택 시 childItemType 설정 | -| `index.tsx` | BOM 전송 형식 간소화, initialBomLines prop 처리 | -| `edit/page.tsx` | initialBomLines 전달, base64 이미지 제거 로직 | -| `create/page.tsx` | base64 이미지 제거 로직 | -| `items/[id]/page.tsx` | BOM key fallback 처리 | -| `ItemDetailClient.tsx` | 제품 정보 섹션 조건부 표시 | -| `lib/api/items.ts` | bending_details 배열 형태로 전송 | - ---- - -## 📋 다음 세션 TODO - -### 백엔드 API 완료 후 -- [ ] 파일 다운로드 URL 처리 (백엔드 응답 형식에 맞춰 적용) -- [ ] 전개도 이미지 표시 테스트 -- [ ] 시방서/인정서 PDF 다운로드 테스트 - -### DynamicItemForm 분할 작업 🎯 -- [ ] `index.tsx` 파일 분할 (현재 2000줄+ → 500줄 이하로) -- [ ] 섹션별 컴포넌트 분리: - - `DynamicFormHeader.tsx` - 헤더/제목 - - `DynamicFormActions.tsx` - 저장/취소 버튼 - - `DynamicBendingSection.tsx` - 전개도 섹션 (기존) - - `DynamicFileSection.tsx` - 파일 업로드 섹션 - - `useDynamicForm.ts` - 메인 로직 훅 -- [ ] 상태 관리 정리 (props drilling 최소화) - -### 파일 업로드 필드 동적화 🆕 -> 참고: `[DESIGN-2025-12-12] item-master-form-builder-roadmap.md` - -**현재 문제**: -- 파일 업로드가 `FileUpload.tsx`로 하드코딩되어 있음 -- 품목기준관리에서 파일 필드를 동적으로 추가할 수 없음 - -**목표**: -- 새 필드 타입 `file`, `files`, `image` 추가 -- 품목기준관리에서 파일 업로드 필드 동적 생성 가능 - -**구현 작업**: -- [ ] `field_type`에 `file` | `files` | `image` 타입 추가 (API 스키마) -- [ ] `FileField.tsx` 컴포넌트 생성 (DynamicItemForm/fields/) -- [ ] `ImageField.tsx` 컴포넌트 생성 (미리보기 포함) -- [ ] `DynamicFieldRenderer.tsx`에 file/image 케이스 추가 -- [ ] `properties` 확장: `{ accept, maxSize, maxFiles }` -- [ ] 품목기준관리 UI에 파일 필드 타입 옵션 추가 -- [ ] 기존 하드코딩된 FileUpload 컴포넌트 동적 필드로 마이그레이션 - ---- - -## 📚 관련 문서 - -| 문서 | 위치 | -|------|------| -| 이전 세션 컨텍스트 | `[NEXT-2025-12-10] item-crud-session-context.md` | -| DynamicForm 분리 계획 | `[PLAN-2025-12-08] dynamic-form-separation-plan.md` | -| Radix UI 버그 해결 | `claudedocs/guides/[FIX-2025-12-05] radix-ui-select-controlled-mode-bug.md` | \ No newline at end of file diff --git a/claudedocs/archive/sessions/[NEXT-2025-12-13] item-file-upload-session-context.md b/claudedocs/archive/sessions/[NEXT-2025-12-13] item-file-upload-session-context.md deleted file mode 100644 index c3dc636d..00000000 --- a/claudedocs/archive/sessions/[NEXT-2025-12-13] item-file-upload-session-context.md +++ /dev/null @@ -1,96 +0,0 @@ -# 품목관리 파일 업로드 세션 컨텍스트 - -## 세션 정보 -- **날짜**: 2025-12-13 -- **커밋**: c026130 - feat: 품목관리 파일 업로드 기능 개선 - -## 완료된 작업 - -### 1. 파일 업로드 API 파라미터 추가 -- `src/lib/api/items.ts`의 `uploadItemFile` 함수에 `fieldKey`, `fileId` 파라미터 추가 -- FormData에 `field_key`, `file_id` 필드 append - -### 2. 타입 정의 추가 -- `src/types/item.ts`에 `ItemFile`, `ItemFiles` 인터페이스 추가 -```typescript -export interface ItemFile { - id: number; - file_name: string; - file_path: string; -} - -export interface ItemFiles { - bending_diagram?: ItemFile[]; - specification?: ItemFile[]; - certification?: ItemFile[]; -} -``` - -### 3. DynamicItemForm 파일 데이터 파싱 -- 새 API 구조 (`files` 객체) 지원 -- 기존 API 구조 폴백 유지 (하위 호환) -- 파일 ID 상태 추가: `existingSpecificationFileId`, `existingCertificationFileId`, `existingBendingDiagramFileId` - -### 4. 시방서/인정서 파일 UI 개선 -- 기존: 파일명 표시 + 별도 파일 선택 input -- 변경: 파일 있으면 `[파일명] [⬇️] [✏️] [🗑️]` 버튼 UI -- 파일 없으면 기존 파일 선택 UI 표시 - -## 진행 중 (백엔드 대기) - -### 파일 업로드 500 에러 -- **증상**: POST `/api/proxy/items/{id}/files` → 500 에러 -- **원인**: 백엔드에서 `field_key`, `file_id` 파라미터 처리 미구현 -- **Next.js 프록시 로그**: -``` -📎 File field: file = 230601_test.pdf (70976 bytes) -📎 Form field: type = certification -📎 Form field: field_key = certification_file -📎 Form field: file_id = 0 -🔵 Response status: 500 -``` -- **상태**: 프론트엔드 준비 완료, 백엔드 수정 대기 중 - -## 다음 세션 TODO - -### 1. DynamicItemForm index.tsx 분리 작업 -- 현재 2000줄+ → 500줄 이하 목표 -- 컴포넌트 분리: - - FormHeader - - ValidationAlert - - DynamicSectionRenderer - - 파일 업로드 섹션 - - BOM 섹션 -- hooks/utils 정리 - -### 2. 파일 업로드 테스트 (백엔드 완료 후) -- 신규 품목 등록 → 파일 업로드 → 수정 페이지 확인 -- 다운로드/수정/삭제 버튼 동작 검증 -- 파일 덮어쓰기 (file_id: 0) 동작 확인 - -## API 구조 참고 - -### 새 API 응답 구조 (조회) -```json -{ - "files": { - "bending_diagram": [{ "id": 1, "file_name": "벤딩도.pdf", "file_path": "/uploads/..." }], - "specification": [{ "id": 2, "file_name": "규격서.pdf", "file_path": "/uploads/..." }], - "certification": [{ "id": 3, "file_name": "인정서.pdf", "file_path": "/uploads/..." }] - } -} -``` - -### 파일 업로드 요청 (FormData) -``` -file: [File] -type: specification | certification | bending_diagram -field_key: specification_file | certification_file | bending_diagram -file_id: 0 (덮어쓰기) | 1, 2, 3... (추가) -``` - -## 주요 파일 위치 -- 타입: `src/types/item.ts` -- API: `src/lib/api/items.ts` -- 폼: `src/components/items/DynamicItemForm/index.tsx` -- 프록시: `src/app/api/proxy/[...path]/route.ts` \ No newline at end of file diff --git a/claudedocs/archive/sessions/[NEXT-2025-12-20] zustand-refactoring-session-context.md b/claudedocs/archive/sessions/[NEXT-2025-12-20] zustand-refactoring-session-context.md deleted file mode 100644 index a2a38694..00000000 --- a/claudedocs/archive/sessions/[NEXT-2025-12-20] zustand-refactoring-session-context.md +++ /dev/null @@ -1,344 +0,0 @@ -# 품목기준관리 Zustand 리팩토링 - 세션 컨텍스트 - -> 다음 세션에서 이 문서를 먼저 읽고 작업 이어가기 - -## 🎯 프로젝트 목표 - -**핵심 목표:** -1. 품목기준관리 100% 동일 기능 구현 -2. **더 유연한 데이터 관리** (Zustand 정규화 구조) -3. **개선된 UX** (Context 3방향 동기화 → Zustand 1곳 수정) - -**접근 방식:** -- 기존 컴포넌트 재사용 ❌ -- 테스트 페이지에서 완전히 새로 구현 ✅ -- 분리된 상태 유지 → 복구 시나리오 보장 - ---- - -## 세션 요약 (2025-12-22 - 11차 세션) - -### ✅ 오늘 완료된 작업 - -1. **기존 품목기준관리와 상세 기능 비교** - - 구현 완료율: 약 72% - - 핵심 CRUD 기능 모두 구현 확인 - -2. **누락된 핵심 기능 식별** - - 🔴 절대경로(absolute_path) 수정 - PathEditDialog - - 🔴 페이지 복제 - handleDuplicatePage - - 🔴 필드 조건부 표시 - ConditionalDisplayUI - - 🟡 칼럼 관리 - ColumnManageDialog - - 🟡 섹션/필드 사용 현황 표시 - -3. **브랜치 분리 완료** - - `feature/item-master-zustand` 브랜치 생성 - - 29개 파일, 8,248줄 커밋 - - master와 분리 관리 가능 - ---- - -## 세션 요약 (2025-12-21 - 10차 세션) - -### ✅ 오늘 완료된 작업 - -1. **기존 품목기준관리와 기능 비교 분석** - - 기존 페이지의 모든 핵심 기능 구현 확인 - - 커스텀 탭 관리는 기존 페이지에서도 비활성화(주석 처리)됨 - - 탭 관리 기능은 로컬 상태만 사용 (백엔드 미연동, 새로고침 시 초기화) - -2. **Phase D-2 (커스텀 탭 관리) 분석 결과** - - 기존 페이지의 "탭 관리" 버튼: 주석 처리됨 (미사용) - - 속성 하위 탭 관리: 로컬 상태로만 동작 (영속성 없음) - - **결론**: 선택적 기능으로 분류, 핵심 기능 구현 완료 - ---- - -## 세션 요약 (2025-12-21 - 9차 세션) - -### ✅ 완료된 작업 - -1. **속성 CRUD API 연동 완료** - - `types.ts`: PropertyActions 인터페이스 추가 - - `useItemMasterStore.ts`: addUnit, updateUnit, deleteUnit, addMaterial, updateMaterial, deleteMaterial, addTreatment, updateTreatment, deleteTreatment 구현 - - `item-master-api.ts`: UnitOptionRequest/Response 타입 수정 (unit_code, unit_name 사용) - -2. **Import 기능 구현 완료** - - `ImportSectionDialog.tsx`: 독립 섹션 목록에서 선택하여 페이지에 연결 - - `ImportFieldDialog.tsx`: 독립 필드 목록에서 선택하여 섹션에 연결 - - `dialogs/index.ts`: Import 다이얼로그 export 추가 - - `HierarchyTab.tsx`: 불러오기 버튼에 Import 다이얼로그 연결 - -3. **섹션 복제 API 연동 완료** - - `SectionsTab.tsx`: handleCloneSection 함수 구현 (API 연동 + toast 알림) - -4. **타입 수정** - - `transformers.ts`: transformUnitOptionResponse 수정 (unit_name, unit_code 사용) - - `useFormStructure.ts`: 단위 옵션 매핑 수정 (unit_name, unit_code 사용) - ---- - -### ✅ 완료된 Phase - -| Phase | 내용 | 상태 | -|-------|------|------| -| Phase 1 | Zustand 스토어 기본 구조 | ✅ | -| Phase 2 | API 연동 (initFromApi) | ✅ | -| Phase 3 | API CRUD 연동 (update 함수들) | ✅ | -| Phase A-1 | 계층구조 기본 표시 | ✅ | -| Phase A-2 | 드래그앤드롭 순서 변경 | ✅ | -| Phase A-3 | 인라인 편집 (페이지/섹션/경로) | ✅ | -| Phase B-1 | 페이지 CRUD 다이얼로그 | ✅ | -| Phase B-2 | 섹션 CRUD 다이얼로그 | ✅ | -| Phase B-3 | 필드 CRUD 다이얼로그 | ✅ | -| Phase B-4 | BOM 관리 UI | ✅ | -| Phase C-1 | 섹션 탭 구현 (SectionsTab.tsx) | ✅ | -| Phase C-2 | 항목 탭 구현 (FieldsTab.tsx) | ✅ | -| Phase D-1 | 속성 탭 기본 구조 (PropertiesTab.tsx) | ✅ | -| Phase E | Import 기능 (섹션/필드 불러오기) | ✅ | - -### ✅ 현재 상태: 핵심 기능 구현 완료 - -**Phase D-2 (커스텀 탭 관리)**: 선택적 기능으로 분류됨 -- 기존 페이지에서도 "탭 관리" 버튼은 주석 처리 (미사용) -- 속성 하위 탭 관리도 로컬 상태로만 동작 (백엔드 미연동) -- 필요 시 추후 구현 가능 - ---- - -## 📋 기능 비교 결과 - -### ✅ 구현 완료된 핵심 기능 - -| 기능 | 테스트 페이지 | 기존 페이지 | -|------|-------------|------------| -| 계층구조 관리 | ✅ | ✅ | -| 페이지 CRUD | ✅ | ✅ | -| 섹션 CRUD | ✅ | ✅ | -| 필드 CRUD | ✅ | ✅ | -| BOM 관리 | ✅ | ✅ | -| 드래그앤드롭 순서 변경 | ✅ | ✅ | -| 인라인 편집 | ✅ | ✅ | -| Import (섹션/필드) | ✅ | ✅ | -| 섹션 복제 | ✅ | ✅ | -| 단위/재질/표면처리 CRUD | ✅ | ✅ | -| 검색/필터 | ✅ | ✅ | - -### ⚠️ 선택적 기능 (기존 페이지에서도 제한적 사용) - -| 기능 | 상태 | 비고 | -|------|------|------| -| 커스텀 메인 탭 관리 | 미구현 | 기존 페이지에서 주석 처리됨 | -| 속성 하위 탭 관리 | 미구현 | 로컬 상태만 (영속성 없음) | -| 칼럼 관리 | 미구현 | 로컬 상태만 (영속성 없음) | - ---- - -## 📋 전체 기능 체크리스트 - -### Phase A: 기본 UI 구조 (계층구조 탭 완성) ✅ - -#### A-1. 계층구조 기본 표시 ✅ 완료 -- [x] 페이지 목록 표시 (좌측 패널) -- [x] 페이지 선택 시 섹션 목록 표시 (우측 패널) -- [x] 섹션 내부 필드 목록 표시 -- [x] 필드 타입별 뱃지 표시 -- [x] BOM 타입 섹션 구분 표시 - -#### A-2. 드래그앤드롭 순서 변경 ✅ 완료 -- [x] 섹션 드래그앤드롭 순서 변경 -- [x] 필드 드래그앤드롭 순서 변경 -- [x] 스토어 reorderSections 함수 구현 -- [x] 스토어 reorderFields 함수 구현 -- [x] DraggableSection 컴포넌트 생성 -- [x] DraggableField 컴포넌트 생성 - -#### A-3. 인라인 편집 ✅ 완료 -- [x] InlineEdit 재사용 컴포넌트 생성 -- [x] 페이지 이름 더블클릭 인라인 수정 -- [x] 섹션 제목 더블클릭 인라인 수정 -- [x] 절대경로 인라인 수정 - ---- - -### Phase B: CRUD 다이얼로그 ✅ - -#### B-1. 페이지 관리 ✅ 완료 -- [x] PageDialog 컴포넌트 (페이지 추가/수정) -- [x] DeleteConfirmDialog (재사용 가능한 삭제 확인) -- [x] 페이지 추가 버튼 연결 -- [x] 페이지 삭제 버튼 연결 - -#### B-2. 섹션 관리 ✅ 완료 -- [x] SectionDialog 컴포넌트 (섹션 추가/수정) -- [x] 섹션 삭제 다이얼로그 -- [x] 섹션 연결해제 다이얼로그 -- [x] 섹션 추가 버튼 연결 -- [x] ImportSectionDialog (섹션 불러오기) ✅ - -#### B-3. 필드 관리 ✅ 완료 -- [x] FieldDialog 컴포넌트 (필드 추가/수정) -- [x] 드롭다운 옵션 동적 관리 -- [x] 필드 삭제 다이얼로그 -- [x] 필드 연결해제 다이얼로그 -- [x] 필드 추가 버튼 연결 -- [x] ImportFieldDialog (필드 불러오기) ✅ - -#### B-4. BOM 관리 ✅ 완료 -- [x] BOMDialog 컴포넌트 (BOM 추가/수정) -- [x] BOM 항목 삭제 다이얼로그 -- [x] BOM 추가 버튼 연결 -- [x] BOM 수정 버튼 연결 - ---- - -### Phase C: 섹션 탭 + 항목 탭 ✅ - -#### C-1. 섹션 탭 ✅ 완료 -- [x] 모든 섹션 목록 표시 (연결된 + 독립) -- [x] 섹션 상세 정보 표시 -- [x] 섹션 내부 필드 표시 (확장/축소) -- [x] 일반 섹션 / BOM 섹션 탭 분리 -- [x] 페이지 연결 상태 표시 -- [x] 섹션 추가/수정/삭제 다이얼로그 연동 -- [x] 섹션 복제 기능 (API 연동 완료) ✅ - -#### C-2. 항목 탭 (마스터 필드) ✅ 완료 -- [x] 모든 필드 목록 표시 -- [x] 필드 상세 정보 표시 -- [x] 검색 기능 (필드명, 필드키, 타입) -- [x] 필터 기능 (전체/독립/연결된 필드) -- [x] 필드 추가/수정/삭제 다이얼로그 연동 -- [x] 독립 필드 → 섹션 연결 기능 - ---- - -### Phase D: 속성 탭 (진행 중) - -#### D-1. 속성 관리 ✅ 완료 -- [x] PropertiesTab.tsx 기본 구조 -- [x] 단위 관리 (CRUD) - API 연동 완료 -- [x] 재질 관리 (CRUD) - API 연동 완료 -- [x] 표면처리 관리 (CRUD) - API 연동 완료 -- [x] PropertyDialog (속성 옵션 추가) - -#### D-2. 탭 관리 (예정) -- [ ] 커스텀 탭 추가/수정/삭제 -- [ ] 속성 하위 탭 추가/수정/삭제 -- [ ] 탭 순서 변경 - ---- - -### Phase E: Import 기능 ✅ - -- [x] ImportSectionDialog (섹션 불러오기) -- [x] ImportFieldDialog (필드 불러오기) -- [x] HierarchyTab 불러오기 버튼 연결 - ---- - -## 📁 파일 구조 - -``` -src/stores/item-master/ -├── types.ts # 정규화된 엔티티 타입 + PropertyActions -├── useItemMasterStore.ts # Zustand 스토어 -├── normalizers.ts # API 응답 정규화 - -src/app/[locale]/(protected)/items-management-test/ -├── page.tsx # 테스트 페이지 메인 -├── components/ # 테스트 페이지 전용 컴포넌트 -│ ├── HierarchyTab.tsx # 계층구조 탭 ✅ -│ ├── DraggableSection.tsx # 드래그 섹션 ✅ -│ ├── DraggableField.tsx # 드래그 필드 ✅ -│ ├── InlineEdit.tsx # 인라인 편집 컴포넌트 ✅ -│ ├── SectionsTab.tsx # 섹션 탭 ✅ (복제 기능 추가) -│ ├── FieldsTab.tsx # 항목 탭 ✅ -│ ├── PropertiesTab.tsx # 속성 탭 ✅ -│ └── dialogs/ # 다이얼로그 컴포넌트 ✅ -│ ├── index.ts # 인덱스 ✅ -│ ├── DeleteConfirmDialog.tsx # 삭제 확인 ✅ -│ ├── PageDialog.tsx # 페이지 다이얼로그 ✅ -│ ├── SectionDialog.tsx # 섹션 다이얼로그 ✅ -│ ├── FieldDialog.tsx # 필드 다이얼로그 ✅ -│ ├── BOMDialog.tsx # BOM 다이얼로그 ✅ -│ ├── PropertyDialog.tsx # 속성 다이얼로그 ✅ -│ ├── ImportSectionDialog.tsx # 섹션 불러오기 ✅ -│ └── ImportFieldDialog.tsx # 필드 불러오기 ✅ -``` - ---- - -## 핵심 파일 위치 - -| 파일 | 용도 | -|-----|------| -| `claudedocs/architecture/[DESIGN-2025-12-20] item-master-zustand-refactoring.md` | 📋 설계 문서 | -| `src/stores/item-master/useItemMasterStore.ts` | 🏪 Zustand 스토어 | -| `src/stores/item-master/types.ts` | 📝 타입 정의 | -| `src/stores/item-master/normalizers.ts` | 🔄 API 응답 정규화 | -| `src/app/[locale]/(protected)/items-management-test/page.tsx` | 🧪 테스트 페이지 | -| `src/components/items/ItemMasterDataManagement.tsx` | 📚 기존 페이지 (참조용) | - ---- - -## 테스트 페이지 접속 - -``` -http://localhost:3000/ko/items-management-test -``` - ---- - -## 브랜치 정보 - -| 항목 | 값 | -|------|-----| -| 작업 브랜치 | `feature/item-master-zustand` | -| 기본 브랜치 | `master` (테스트 페이지 없음) | - -### 브랜치 작업 명령어 - -```bash -# 테스트 페이지 작업 시 -git checkout feature/item-master-zustand - -# master 최신 내용 반영 -git merge master - -# 테스트 완료 후 master에 합치기 -git checkout master -git merge feature/item-master-zustand -``` - ---- - -## 다음 세션 시작 명령 - -``` -누락된 기능 구현해줘 - 절대경로 수정부터 -``` - -또는 - -``` -테스트 페이지 실사용 테스트하고 버그 수정해줘 -``` - ---- - -## 남은 작업 - -### 🔴 누락된 핵심 기능 (100% 구현 위해 필요) -1. **절대경로(absolute_path) 수정** - PathEditDialog -2. **페이지 복제** - handleDuplicatePage -3. **필드 조건부 표시** - ConditionalDisplayUI - -### 🟡 추가 기능 -4. **칼럼 관리** - ColumnManageDialog -5. **섹션/필드 사용 현황 표시** - -### 🟢 마이그레이션 -6. **실사용 테스트**: 테스트 페이지에서 실제 데이터로 CRUD 테스트 -7. **버그 수정**: 발견되는 버그 즉시 수정 -8. **마이그레이션**: 테스트 완료 후 기존 페이지 대체 \ No newline at end of file diff --git a/claudedocs/archive/sessions/[NEXT-2025-12-22] production-session-context.md b/claudedocs/archive/sessions/[NEXT-2025-12-22] production-session-context.md deleted file mode 100644 index 1ebfd298..00000000 --- a/claudedocs/archive/sessions/[NEXT-2025-12-22] production-session-context.md +++ /dev/null @@ -1,97 +0,0 @@ -# [NEXT-2025-12-22] 생산 현황판 세션 컨텍스트 - -## 세션 요약 (2025-12-22) - -### 완료된 작업 ✅ -- [x] Phase 1: 생산 현황판 메인 페이지 구현 -- [x] Phase 2: 작업자 화면 구현 (별도 페이지) -- [x] Phase 3: 전량완료 기능 (확인/완료 팝업, 뱃지) -- [x] Phase 4: 공정상세 섹션 구현 (카드 내 토글) -- [x] Phase 5: 자재투입 모달 구현 -- [x] Phase 6: 작업일지 모달 구현 (⚠️ 개선 필요) -- [x] Phase 7: 이슈보고 모달 구현 -- [x] Phase 8: 네비게이션 연결 (TODO 주석 처리) - -### 다음 세션 TODO ⚠️ - -#### 1. 작업일지 모달 개선 (우선) -**현재**: 단순 테이블 형태로 구현됨 -**요청**: 기안함 상세 화면 스타일 (완성된 문서 형태)로 개선 - -**참고 컴포넌트**: -``` -src/components/approval/DocumentDetail/ -├── ProposalDocument.tsx ← 기품의서 양식 -├── ExpenseReportDocument.tsx ← 지출보고서 양식 -└── ExpenseEstimateDocument.tsx ← 지출품의서 양식 -``` - -**수정 대상**: -``` -src/components/production/WorkerScreen/WorkLogModal.tsx -``` - -**작업 내용**: -- DocumentDetail 컴포넌트 스타일 참고 -- 완성된 문서 형태로 작업일지 양식 재구현 -- 인쇄 친화적 레이아웃 적용 - -#### 2. 작업지시 관리 페이지 (대기) -- 생산 현황판에서 네비게이션 연결 대기 -- 스크린샷/설명 별도 제공 예정 - ---- - -### 생성된 파일 목록 - -``` -src/app/[locale]/(protected)/production/ -├── dashboard/page.tsx ✅ -└── worker-screen/page.tsx ✅ - -src/components/production/ -├── ProductionDashboard/ -│ ├── index.tsx ✅ -│ ├── types.ts ✅ -│ └── mockData.ts ✅ -│ -└── WorkerScreen/ - ├── index.tsx ✅ - ├── types.ts ✅ - ├── WorkCard.tsx ✅ - ├── ProcessDetailSection.tsx ✅ - ├── MaterialInputModal.tsx ✅ - ├── WorkLogModal.tsx ⚠️ 개선 필요 - ├── IssueReportModal.tsx ✅ - ├── CompletionConfirmDialog.tsx ✅ - └── CompletionToast.tsx ✅ - -src/components/ui/ -└── collapsible.tsx ✅ (신규 추가, @radix-ui/react-collapsible 설치됨) -``` - ---- - -### 테스트 URL -- 생산 현황판: http://localhost:3000/ko/production/dashboard -- 작업자 화면: http://localhost:3000/ko/production/worker-screen - ---- - -### 참고 사항 -1. **작업자 화면 = 별도 페이지** (생산 현황판 하위 아님) - - 사이드바 메뉴로 접근 - - "돌아가기" 버튼 불필요 - -2. **모든 alert() → AlertDialog 변환 완료** - - 전량완료 확인/성공 - - 이슈보고 벨리데이션/성공 - -3. **공정상세 = 카드 내 토글 확장** - - Collapsible 컴포넌트 사용 - - 5단계 공정 표시 - ---- - -**작성일**: 2025-12-22 -**상태**: 🔄 작업일지 모달 개선 대기 \ No newline at end of file diff --git a/claudedocs/archive/sessions/[NEXT-2025-12-24] item-master-refactoring-session.md b/claudedocs/archive/sessions/[NEXT-2025-12-24] item-master-refactoring-session.md deleted file mode 100644 index 32d16fe9..00000000 --- a/claudedocs/archive/sessions/[NEXT-2025-12-24] item-master-refactoring-session.md +++ /dev/null @@ -1,134 +0,0 @@ -# 품목기준관리 리팩토링 세션 컨텍스트 - -> **브랜치**: `feature/item-master-zustand` -> **날짜**: 2025-12-24 -> **상태**: Phase 2 완료, 커밋 대기 - ---- - -## 세션 요약 (12차 세션) - -### 완료된 작업 -- [x] 브랜치 상태 확인 (`feature/item-master-zustand`) -- [x] 기존 작업 혼동 정리 (품목관리 CRUD vs 품목기준관리 설정) -- [x] 작업 대상 파일 확인 (`ItemMasterDataManagement.tsx` - 1,799줄) -- [x] 기존 훅 분리 상태 파악 (7개 훅 이미 존재) -- [x] `ItemMasterDataManagement.tsx` 상세 분석 완료 -- [x] 훅 분리 계획서 작성 (`[PLAN-2025-12-24] hook-extraction-plan.md`) -- [x] **Phase 1: 신규 훅 4개 생성** - - `useInitialDataLoading.ts` - 초기 데이터 로딩 (~130줄) - - `useImportManagement.ts` - 섹션/필드 Import (~100줄) - - `useReorderManagement.ts` - 드래그앤드롭 순서 변경 (~80줄) - - `useDeleteManagement.ts` - 삭제/언링크 핸들러 (~100줄) -- [x] **Phase 2: UI 컴포넌트 2개 생성** - - `AttributeTabContent.tsx` - 속성 탭 콘텐츠 (~340줄) - - `ItemMasterDialogs.tsx` - 다이얼로그 통합 (~540줄) -- [x] 빌드 테스트 통과 - -### 현재 상태 -- **메인 컴포넌트**: 1,799줄 → ~1,478줄 (약 320줄 감소) -- **신규 훅**: 4개 생성 및 통합 -- **신규 UI 컴포넌트**: 2개 생성 (향후 추가 통합 가능) -- **빌드**: 통과 - -### 다음 TODO (커밋 후) -1. Git 커밋 (Phase 1, 2 변경사항) -2. Phase 3: 추가 코드 정리 (선택적) - - 속성 탭 내용을 `AttributeTabContent`로 완전 대체 (추가 ~500줄 감소 가능) - - 다이얼로그들을 `ItemMasterDialogs`로 완전 대체 -3. Zustand 도입 (3방향 동기화 문제 해결) - ---- - -## 핵심 정보 - -### 페이지 구분 (중요!) - -| 페이지 | URL | 컴포넌트 | 상태 | -|--------|-----|----------|------| -| 품목관리 CRUD | `/items/` | `DynamicItemForm` | ✅ 훅 분리 완료 (master 적용됨) | -| **품목기준관리 설정** | `/master-data/item-master-data-management` | `ItemMasterDataManagement` | ⏳ **훅 분리 진행 중** | - -### 현재 파일 구조 - -``` -src/components/items/ItemMasterDataManagement/ -├── ItemMasterDataManagement.tsx ← ~1,478줄 (리팩토링 후) -├── hooks/ (11개 - 7개 기존 + 4개 신규) -│ ├── usePageManagement.ts -│ ├── useSectionManagement.ts -│ ├── useFieldManagement.ts -│ ├── useMasterFieldManagement.ts -│ ├── useTemplateManagement.ts -│ ├── useAttributeManagement.ts -│ ├── useTabManagement.ts -│ ├── useInitialDataLoading.ts ← NEW -│ ├── useImportManagement.ts ← NEW -│ ├── useReorderManagement.ts ← NEW -│ └── useDeleteManagement.ts ← NEW -├── components/ (5개 - 3개 기존 + 2개 신규) -│ ├── DraggableSection.tsx -│ ├── DraggableField.tsx -│ ├── ConditionalDisplayUI.tsx -│ ├── AttributeTabContent.tsx ← NEW -│ └── ItemMasterDialogs.tsx ← NEW -├── services/ (6개) -├── dialogs/ (13개) -├── tabs/ (4개) -└── utils/ (1개) -``` - -### 브랜치 상태 - -``` -master (원본 보존) - │ - └── feature/item-master-zustand (현재) - ├── Zustand 테스트 페이지 (/items-management-test/) - 놔둠 - ├── Zustand 스토어 (stores/item-master/) - 나중에 사용 - └── 기존 품목기준관리 페이지 - 훅 분리 진행 중 -``` - -### 작업 진행률 - -``` -시작: ItemMasterDataManagement.tsx 1,799줄 - ↓ Phase 1: 훅 분리 (4개 신규 훅) -현재: ~1,478줄 (-321줄, -18%) - ↓ Phase 2: UI 컴포넌트 분리 (2개 신규 컴포넌트 생성) - ↓ Phase 3: 추가 통합 (선택적) -목표: ~500줄 (메인 컴포넌트) - ↓ Zustand 적용 -최종: 3방향 동기화 문제 해결 -``` - ---- - -## 생성된 파일 목록 - -### 신규 훅 (Phase 1) -1. `hooks/useInitialDataLoading.ts` - 초기 데이터 로딩, 에러 처리 -2. `hooks/useImportManagement.ts` - 섹션/필드 Import 다이얼로그 상태 및 핸들러 -3. `hooks/useReorderManagement.ts` - 드래그앤드롭 순서 변경 -4. `hooks/useDeleteManagement.ts` - 삭제, 언링크, 초기화 핸들러 - -### 신규 UI 컴포넌트 (Phase 2) -1. `components/AttributeTabContent.tsx` - 속성 탭 전체 UI -2. `components/ItemMasterDialogs.tsx` - 모든 다이얼로그 통합 렌더링 - ---- - -## 참고 문서 - -- `[PLAN-2025-12-24] hook-extraction-plan.md` - 훅 분리 계획서 (상세) -- `[DESIGN-2025-12-20] item-master-zustand-refactoring.md` - Zustand 설계서 -- `[IMPL-2025-12-24] item-master-test-and-zustand.md` - 테스트 체크리스트 - ---- - -## 다음 세션 시작 명령 - -``` -품목기준관리 설정 페이지(ItemMasterDataManagement.tsx) 추가 리팩토링 또는 Zustand 도입 진행해줘. -[NEXT-2025-12-24] item-master-refactoring-session.md 문서 확인하고 시작해. -``` diff --git a/claudedocs/archive/sessions/[NEXT-2025-12-30] fetch-wrapper-session-context.md b/claudedocs/archive/sessions/[NEXT-2025-12-30] fetch-wrapper-session-context.md deleted file mode 100644 index 14fc37a8..00000000 --- a/claudedocs/archive/sessions/[NEXT-2025-12-30] fetch-wrapper-session-context.md +++ /dev/null @@ -1,78 +0,0 @@ -ㅏ# 세션 요약 (2025-12-30) - -## 완료된 작업 - -### 1. fetch-wrapper 목적 확인 -- **목적**: 401 에러(세션 만료) 발생 시 로그인 리다이렉트를 **중앙화** -- **장점**: 중복 코드 제거 + 새 작업자도 규칙 준수 가능 - -### 2. Accounting 도메인 완료 (12/12) ✅ -- [x] `SalesManagement/actions.ts` -- [x] `VendorManagement/actions.ts` -- [x] `PurchaseManagement/actions.ts` -- [x] `DepositManagement/actions.ts` -- [x] `WithdrawalManagement/actions.ts` -- [x] `VendorLedger/actions.ts` -- [x] `ReceivablesStatus/actions.ts` -- [x] `ExpectedExpenseManagement/actions.ts` -- [x] `CardTransactionInquiry/actions.ts` -- [x] `DailyReport/actions.ts` -- [x] `BadDebtCollection/actions.ts` -- [x] `BankTransactionInquiry/actions.ts` - -### 3. HR 도메인 진행중 (1/6) -- [x] `EmployeeManagement/actions.ts` (이미 마이그레이션되어 있었음) -- [~] `VacationManagement/actions.ts` (import만 변경됨, 함수 마이그레이션 필요) - -## 다음 세션 TODO - -### HR 도메인 나머지 (5개) -- [ ] `VacationManagement/actions.ts` - 함수 마이그레이션 완료 필요 -- [ ] `SalaryManagement/actions.ts` -- [ ] `CardManagement/actions.ts` -- [ ] `DepartmentManagement/actions.ts` -- [ ] `AttendanceManagement/actions.ts` - -### 기타 도메인 (Approval, Production, Settings, 기타) -- Approval: 4개 -- Production: 4개 -- Settings: 11개 -- 기타: 12개 -- 상세 목록은 체크리스트 문서 참고 - -### 빌드 검증 -- [ ] `npm run build` 실행하여 마이그레이션 검증 - -## 참고 사항 - -### 마이그레이션 패턴 (참고용) -```typescript -// Before -import { cookies } from 'next/headers'; -async function getApiHeaders() { ... } -const response = await fetch(url, { headers }); - -// After -import { serverFetch } from '@/lib/api/fetch-wrapper'; -const { response, error } = await serverFetch(url, { method: 'GET' }); -if (error) return { success: false, error: error.message }; -``` - -### 주요 변경 포인트 -1. `getApiHeaders()` 함수 제거 -2. `import { cookies } from 'next/headers'` 제거 -3. `fetch()` → `serverFetch()` 변경 -4. `{ response, error }` 구조분해 사용 -5. 파일 다운로드(Excel/PDF)는 `cookies` import 유지 (custom Accept 헤더 필요) - -### 특이사항 -- `EmployeeManagement/actions.ts`는 이미 `serverFetch` 사용 중이었음 -- `uploadProfileImage` 함수는 FormData 업로드라 `cookies` import 유지 - -## 체크리스트 문서 -`claudedocs/api/[IMPL-2025-12-30] fetch-wrapper-migration.md` - -## 진행률 -- 전체: 49개 파일 -- 완료: 13개 (27%) -- 남음: 36개 diff --git a/claudedocs/archive/sessions/[NEXT-2025-12-30] partner-management-session-context.md b/claudedocs/archive/sessions/[NEXT-2025-12-30] partner-management-session-context.md deleted file mode 100644 index 896d69cf..00000000 --- a/claudedocs/archive/sessions/[NEXT-2025-12-30] partner-management-session-context.md +++ /dev/null @@ -1,101 +0,0 @@ -# 주일 거래처 관리 세션 컨텍스트 - -Last Updated: 2025-12-30 - -## 세션 요약 (2025-12-30) - -### 완료된 작업 -- [x] 거래처 리스트 필터 위치 수정 (테이블 위로 이동) -- [x] 거래처 폼 컴포넌트 생성 (PartnerForm.tsx) -- [x] 등록 페이지 생성 (/new/page.tsx) -- [x] 상세 페이지 생성 (/[id]/page.tsx) -- [x] 수정 페이지 생성 (/[id]/edit/page.tsx) -- [x] types.ts 확장 (전체 필드 추가) -- [x] actions.ts CRUD 함수 추가 - -### 다음 세션 TODO -- [ ] **회사 정보 + 신용/거래 정보 섹션 합치기** (스크린샷 기준으로 하나의 섹션) -- [ ] 실제 API 연동 - -### 참고 사항 -- 스크린샷에서 "회사 정보"와 "신용/거래 정보"가 하나의 Card 섹션으로 되어 있음 -- 현재 코드는 별도 섹션으로 분리됨 → 합쳐야 함 - ---- - -## 완료된 작업 (전체) - -### 1. 프로젝트 구조 설정 -- [x] `claudedocs/juil/` 문서 폴더 생성 -- [x] `[REF] juil-project-structure.md` 프로젝트 구조 가이드 작성 -- [x] `_index.md` 문서 맵에 juil 섹션 추가 - -### 2. 거래처 관리 리스트 페이지 -- [x] 페이지: `src/app/[locale]/(protected)/juil/project/bidding/partners/page.tsx` -- [x] 컴포넌트: `src/components/business/juil/partners/PartnerListClient.tsx` -- [x] 타입: `src/components/business/juil/partners/types.ts` -- [x] 액션: `src/components/business/juil/partners/actions.ts` (목업 데이터) -- [x] 인덱스: `src/components/business/juil/partners/index.ts` -- [x] 레이아웃 수정: 필터를 테이블 위로 이동, 등록 버튼 상단 배치 - -### 3. 거래처 등록/상세/수정 페이지 -- [x] 폼 컴포넌트: `src/components/business/juil/partners/PartnerForm.tsx` -- [x] 등록 페이지: `src/app/[locale]/(protected)/juil/project/bidding/partners/new/page.tsx` -- [x] 상세 페이지: `src/app/[locale]/(protected)/juil/project/bidding/partners/[id]/page.tsx` -- [x] 수정 페이지: `src/app/[locale]/(protected)/juil/project/bidding/partners/[id]/edit/page.tsx` - -### 4. 구현된 기능 - -#### 리스트 페이지 -- 통계 카드 (전체 거래처 / 미등록) -- 검색 (거래처명, 번호, 대표자, 담당자) -- 탭 필터 (전체 / 신규) -- 테이블 위 필터: `총 N건 | 전체 ▾ | 최신순 ▾` -- 테이블 컬럼: 체크박스, 번호, 거래처번호, 구분, 거래처명, 대표자, 담당자, 전화번호, 매출 결제일, 악성채권, 작업 -- 행 선택 시 수정/삭제 버튼 표시 -- 일괄 삭제 다이얼로그 -- 페이지네이션 -- 모바일 카드 뷰 - -#### 폼 페이지 (등록/상세/수정 공통) -- **기본 정보**: 사업자등록번호, 거래처코드, 거래처명, 대표자명, 거래처유형, 업태, 업종 -- **연락처 정보**: 주소 (우편번호 찾기 DAUM), 전화번호, 모바일, 팩스, 이메일 -- **담당자 정보**: 담당자명, 담당자 전화, 시스템 관리자 -- **회사 정보**: 회사 로고 (BLOB 업로드), 매출 결제일, 신용등급, 거래등급, 세금계산서 이메일 -- **추가 정보**: 미수금, 연체 (토글), 악성채권 (토글) -- **메모**: 추가/삭제 기능 -- **필요 서류**: 파일 업로드 (드래그 앤 드롭) - -#### 모드별 버튼 분기 -- **등록**: 취소 | 저장 -- **수정**: 삭제 | 수정 -- **상세**: 목록가기 | 수정 - -## 테스트 URL - -| 페이지 | URL | 상태 | -|--------|-----|------| -| 거래처 관리 (리스트) | `/ko/juil/project/bidding/partners` | ✅ 완료 | -| 거래처 등록 | `/ko/juil/project/bidding/partners/new` | ✅ 완료 | -| 거래처 상세 | `/ko/juil/project/bidding/partners/1` | ✅ 완료 | -| 거래처 수정 | `/ko/juil/project/bidding/partners/1/edit` | ✅ 완료 | - -## 디렉토리 구조 - -``` -src/ -├── app/[locale]/(protected)/juil/ -│ └── project/bidding/partners/ -│ ├── page.tsx ✅ -│ ├── new/page.tsx ✅ -│ └── [id]/ -│ ├── page.tsx ✅ -│ └── edit/page.tsx ✅ -│ -└── components/business/juil/partners/ - ├── index.ts ✅ - ├── types.ts ✅ - ├── actions.ts ✅ (목업) - ├── PartnerListClient.tsx ✅ - └── PartnerForm.tsx ✅ (섹션 수정 필요) -``` \ No newline at end of file diff --git a/claudedocs/auth/[CASE-2025-11-25] httponly-cookie-security-validation.md b/claudedocs/auth/[CASE-2025-11-25] httponly-cookie-security-validation.md deleted file mode 100644 index 1770b2ee..00000000 --- a/claudedocs/auth/[CASE-2025-11-25] httponly-cookie-security-validation.md +++ /dev/null @@ -1,370 +0,0 @@ -# [CASE STUDY] HttpOnly 쿠키 보안 검증 사례 - -**날짜**: 2025-11-25 -**카테고리**: 보안 검증, 인증 아키텍처, HttpOnly 쿠키 -**결과**: ✅ 보안 설계가 완벽하게 작동함을 검증 - ---- - -## 📋 요약 - -HttpOnly 쿠키를 사용한 인증 시스템에서 **"토큰값이 null로 전달된다"** 는 문제가 발생했으나, 실제로는 **보안이 철저하게 작동하고 있었음**을 확인한 사례. - -**핵심 교훈**: -> **JavaScript로 HttpOnly 쿠키를 절대 읽을 수 없다 = 보안이 제대로 작동하고 있다는 증거!** - ---- - -## 🔴 문제 상황 - -### 증상 -``` -❌ GET https://api.codebridge-x.com/api/v1/item-master/init 401 (Unauthorized) -❌ 백엔드 로그: Authorization 헤더 값이 null -❌ 로그인은 성공했는데 이후 API 호출 시 인증 실패 -``` - -### 초기 의심 지점 -1. API URL 경로 문제? → ❌ 경로는 정상 -2. 헤더 전송 문제? → ❌ 헤더는 전송되고 있음 -3. 쿠키 저장 문제? → ❌ 쿠키는 저장되어 있음 -4. **토큰 추출 문제?** → ✅ **여기가 진짜 원인!** - ---- - -## 🔍 발견 과정 - -### 1단계: 혼란 -```typescript -// auth-headers.ts에서 토큰 추출 시도 -const token = document.cookie - .split('; ') - .find(row => row.startsWith('access_token=')) - ?.split('=')[1]; - -console.log(token); // undefined ← 왜??? -``` - -**의문점**: -- 분명 로그인 성공했는데? -- Application 탭에서 쿠키 보이는데? -- Swagger에서는 같은 토큰으로 잘 되는데? - -### 2단계: 결정적 질문 -> **"어 근데 로그아웃 할 때는 토큰 잘 던지는데 어떤차이야???"** - -### 3단계: 깨달음 -로그아웃 API 코드를 확인해보니... - -```typescript -// /api/auth/logout/route.ts (Next.js API Route - 서버사이드!) -export async function POST(request: NextRequest) { - // ✅ 서버에서는 HttpOnly 쿠키를 읽을 수 있다! - const accessToken = request.cookies.get('access_token')?.value; - - // 토큰이 정상적으로 추출됨! - console.log(accessToken); // "eyJ0eXAiOiJKV1QiLCJh..." -} -``` - -**발견**: 로그아웃은 **Next.js API Route (서버사이드)** 에서 처리하고 있었다! - ---- - -## 💡 근본 원인 - -### HttpOnly 쿠키의 작동 원리 - -``` -┌─────────────────────────────────────────────────────────┐ -│ HttpOnly 쿠키 = JavaScript 접근 차단 (XSS 방지) │ -└─────────────────────────────────────────────────────────┘ - -❌ 클라이언트 JavaScript (브라우저) - ↓ - document.cookie → "" (빈 문자열, 읽기 불가) - ↓ - HttpOnly 쿠키는 보이지 않음! - - -✅ 서버사이드 (Node.js, Next.js API Route) - ↓ - request.cookies.get('access_token') → "토큰값" (읽기 가능!) - ↓ - HttpOnly 쿠키 정상 접근! -``` - -### 우리가 겪은 상황 - -```typescript -// ❌ WRONG: 클라이언트에서 직접 백엔드 호출 -fetch('https://api.codebridge-x.com/api/v1/item-master/init', { - headers: { - 'Authorization': `Bearer ${document.cookie에서_추출}` // null! - // ↑ HttpOnly 쿠키는 JavaScript로 읽을 수 없음! - } -}) -``` - -**결론**: 우리가 막아둔 보안(HttpOnly)이 **완벽하게 작동하고 있었다!** 🎉 - ---- - -## ✅ 해결 방법: Next.js API Proxy Pattern - -### 아키텍처 - -``` -[브라우저] - ↓ fetch('/api/proxy/item-master/init') - ↓ Cookie: access_token=xxx (자동 전송, HttpOnly) - ↓ Headers: { X-API-KEY, Accept } - ↓ ⚠️ Authorization 헤더 없음 (JS로 못 읽으니까!) - -[Next.js 프록시] ← 서버사이드! - ↓ request.cookies.get('access_token') ✅ 읽기 성공! - ↓ fetch('https://backend.com/api/v1/item-master/init') - ↓ Headers: { - ↓ Authorization: 'Bearer {토큰}', ← 프록시가 추가! - ↓ X-API-KEY: '...' - ↓ } - -[PHP 백엔드] - ↓ Authorization 헤더 확인 ✅ - ↓ 인증 성공! 데이터 반환 - -[브라우저] - ↓ 데이터 수신 완료! -``` - -### 구현 - -#### 1. Catch-all 프록시 라우트 생성 -```typescript -// /src/app/api/proxy/[...path]/route.ts -async function proxyRequest( - request: NextRequest, - params: { path: string[] }, - method: string -) { - // 1. 서버에서 HttpOnly 쿠키 읽기 (가능!) - const token = request.cookies.get('access_token')?.value; - - // 2. 백엔드로 프록시 - const backendResponse = await fetch( - `${process.env.NEXT_PUBLIC_API_URL}/api/v1/${params.path.join('/')}`, - { - method, - headers: { - 'Authorization': token ? `Bearer ${token}` : '', - 'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '', - }, - } - ); - - return backendResponse; -} - -export async function GET(request, { params }) { - return proxyRequest(request, params, 'GET'); -} - -export async function POST(request, { params }) { - return proxyRequest(request, params, 'POST'); -} - -// PUT, DELETE도 동일... -``` - -#### 2. API 클라이언트 수정 -```typescript -// /src/lib/api/item-master.ts - -// ❌ BEFORE: 직접 백엔드 호출 -const BASE_URL = 'https://api.codebridge-x.com/api/v1'; - -// ✅ AFTER: 프록시 사용 -const BASE_URL = '/api/proxy'; - -// 이제 모든 API 호출이 프록시를 통함 -export async function getItemMasterInit() { - const response = await fetch(`${BASE_URL}/item-master/init`, { - headers: getAuthHeaders(), - }); - return response; -} -``` - -#### 3. 헤더 유틸리티 간소화 -```typescript -// /src/lib/api/auth-headers.ts - -// ✅ AFTER: Authorization 헤더 제거 (프록시가 처리) -export const getAuthHeaders = (): HeadersInit => { - return { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '', - // Authorization 헤더 없음! 프록시가 추가함 - }; -}; -``` - ---- - -## 🎓 교훈 - -### 1. HttpOnly 쿠키는 정말로 JavaScript 접근을 막는다 -```javascript -// 이것은 실패하도록 설계되었다! -document.cookie // HttpOnly 쿠키는 보이지 않음 - -// 이것이 보안의 핵심! -// XSS 공격으로 스크립트가 실행되어도 토큰을 훔칠 수 없다! -``` - -### 2. "작동 안 함" ≠ "버그" -- 처음엔 "토큰이 null이라서 문제"라고 생각 -- 실제로는 "보안이 제대로 작동하는 것" -- **예상대로 작동하지 않는 것이 설계 의도일 수 있다!** - -### 3. 기존 코드에서 배우기 -- 로그아웃이 작동하는 이유를 분석 -- "왜 이것만 되지?"라는 질문이 해결의 열쇠 -- **작동하는 코드 = 참조 구현** - -### 4. 서버사이드 프록시 패턴의 가치 -``` -보안 (HttpOnly) + 기능 (API 호출) = 프록시 패턴 - ↓ ↓ ↓ -XSS 방지 인증된 API 호출 Best of Both -``` - ---- - -## 🔐 보안 검증 결과 - -### ✅ 검증된 사항 - -1. **JavaScript로 HttpOnly 쿠키를 절대 읽을 수 없음** - - `document.cookie`에서 완전히 숨겨짐 - - 브라우저 콘솔에서도 접근 불가 - - **XSS 공격으로부터 안전!** - -2. **서버사이드에서만 접근 가능** - - Next.js API Route에서 `request.cookies.get()` 성공 - - 토큰이 서버 메모리에만 존재 - - 클라이언트 JavaScript에 노출되지 않음 - -3. **자동 쿠키 전송** - - 브라우저가 same-origin 요청 시 자동 전송 - - HTTPS로 암호화되어 전송 - - Secure, HttpOnly, SameSite 속성으로 보호 - -### 🛡️ 보안 강도 - -| 공격 유형 | 방어 가능 여부 | 이유 | -|----------|----------------|------| -| XSS (Cross-Site Scripting) | ✅ 방어 | JavaScript가 쿠키를 읽을 수 없음 | -| Session Hijacking | ✅ 방어 | HttpOnly + Secure 조합 | -| CSRF | ⚠️ 추가 방어 필요 | SameSite 속성으로 일부 방어 | -| Man-in-the-Middle | ✅ 방어 | HTTPS + Secure 속성 | - ---- - -## 📝 RULES.md 반영 - -이번 사례를 바탕으로 `RULES.md`에 추가된 규칙: - -```markdown -## API Communication with HttpOnly Cookies -**Priority**: 🔴 **Triggers**: Backend API calls requiring authentication - -### Mandatory Proxy Pattern -- ALL authenticated API calls MUST use Next.js API route proxies -- NEVER try to read HttpOnly cookies with JavaScript -- Reference implementation: /api/auth/logout/route.ts -``` - ---- - -## 🎯 적용 범위 - -### 현재 적용됨 -- ✅ 로그인 API (`/api/auth/login`) -- ✅ 로그아웃 API (`/api/auth/logout`) -- ✅ 품목기준관리 API (`/api/proxy/item-master/*`) - -### 향후 적용 필요 -- 품목관리 API (개발 예정) -- 기타 인증 필요 API들 - -### 프록시 사용법 -```typescript -// ❌ WRONG -fetch('https://backend.com/api/v1/some-api') - -// ✅ RIGHT -fetch('/api/proxy/some-api') -``` - ---- - -## 📊 성능 영향 - -### 레이턴시 -- **프록시 추가 레이턴시**: ~5-15ms (Next.js 서버 처리) -- **보안 향상**: 무한대 -- **결론**: 트레이드오프 가치 있음 - -### 서버 부하 -- Next.js 서버가 모든 API 요청을 중계 -- 필요 시 캐싱 전략 추가 가능 -- 현재 규모에서는 문제 없음 - ---- - -## 🔗 관련 파일 - -### 구현 파일 -- `/src/app/api/proxy/[...path]/route.ts` - Catch-all 프록시 -- `/src/lib/api/item-master.ts` - API 클라이언트 -- `/src/lib/api/auth-headers.ts` - 헤더 유틸리티 - -### 참조 파일 -- `/src/app/api/auth/logout/route.ts` - 참조 구현 -- `/Users/byeongcheolryu/.claude/RULES.md` - 규칙 문서 - ---- - -## 💬 팀 피드백 - -> "흐흑 ㅠㅠ 우리가 막아두고 계속 스크립트로 요청했구나" -> -> "보안 검증이 철저하게 됐군 스크립트로 절대 못 뽑아온다는걸 말야 ㅋㅋ" - -**→ 보안이 제대로 작동하고 있었다는 것을 확인한 순간!** - ---- - -## 🎉 결론 - -이번 사례는 **"버그인 줄 알았는데 실은 기능(feature)이었다"** 는 완벽한 예시입니다. - -### Key Takeaways -1. ✅ HttpOnly 쿠키 보안이 완벽하게 작동함을 검증 -2. ✅ 서버사이드 프록시 패턴으로 보안과 기능 모두 확보 -3. ✅ 기존 코드(로그아웃)에서 해결책을 찾음 -4. ✅ 향후 모든 인증 API에 적용할 패턴 확립 - -### 최종 평가 -**🏆 보안 설계: A+** -**🔧 구현 방법: A+** -**📚 문서화: A+** - ---- - -**작성일**: 2025-11-25 -**작성자**: Claude Code -**검증자**: 개발팀 -**상태**: ✅ 완료 및 프로덕션 적용 \ No newline at end of file diff --git a/claudedocs/auth/[IMPL-2025-11-07] auth-guard-usage.md b/claudedocs/auth/[IMPL-2025-11-07] auth-guard-usage.md deleted file mode 100644 index da4db282..00000000 --- a/claudedocs/auth/[IMPL-2025-11-07] auth-guard-usage.md +++ /dev/null @@ -1,335 +0,0 @@ -# Auth Guard Hook 사용 가이드 - -## 개요 - -`useAuthGuard()` Hook은 보호된 페이지에 인증 검증과 브라우저 캐시 방지 기능을 제공합니다. - -## 기능 - -1. **실시간 인증 확인**: 페이지 로드 시 서버에 인증 상태 확인 -2. **뒤로가기 보호**: 로그아웃 후 브라우저 뒤로가기 시 캐시된 페이지 접근 차단 -3. **자동 리다이렉트**: 인증 실패 시 자동으로 로그인 페이지로 이동 - -## 사용 방법 - -### 기본 사용 - -보호가 필요한 모든 페이지에 Hook을 추가하세요: - -```tsx -"use client"; - -import { useAuthGuard } from '@/hooks/useAuthGuard'; - -export default function ProtectedPage() { - // 🔒 인증 보호 및 브라우저 캐시 방지 - useAuthGuard(); - - return ( -
- {/* 보호된 컨텐츠 */} -
- ); -} -``` - -### 적용 예시 - -#### Dashboard 페이지 -```tsx -// src/app/[locale]/dashboard/page.tsx -"use client"; - -import { useAuthGuard } from '@/hooks/useAuthGuard'; - -export default function Dashboard() { - useAuthGuard(); // 한 줄만 추가하면 끝! - - return
Dashboard Content
; -} -``` - -#### Profile 페이지 -```tsx -// src/app/[locale]/profile/page.tsx -"use client"; - -import { useAuthGuard } from '@/hooks/useAuthGuard'; - -export default function Profile() { - useAuthGuard(); - - return
Profile Content
; -} -``` - -#### Settings 페이지 -```tsx -// src/app/[locale]/settings/page.tsx -"use client"; - -import { useAuthGuard } from '@/hooks/useAuthGuard'; - -export default function Settings() { - useAuthGuard(); - - return
Settings Content
; -} -``` - -## 적용이 필요한 페이지 - -다음 페이지들에 `useAuthGuard()` Hook을 적용해야 합니다: - -### 필수 적용 페이지 -- ✅ `/dashboard` - 이미 적용됨 -- ⏳ `/profile` - 적용 필요 -- ⏳ `/settings` - 적용 필요 -- ⏳ `/admin/*` - 모든 관리자 페이지 -- ⏳ `/tenant/*` - 모든 테넌트 관리 페이지 -- ⏳ `/users/*` - 사용자 관리 페이지 -- ⏳ `/reports/*` - 리포트 페이지 -- ⏳ `/analytics/*` - 분석 페이지 -- ⏳ `/inventory/*` - 재고 관리 페이지 -- ⏳ `/finance/*` - 재무 관리 페이지 -- ⏳ `/hr/*` - 인사 관리 페이지 -- ⏳ `/crm/*` - CRM 페이지 - -### 적용 불필요 페이지 -- ❌ `/login` - 게스트 전용 -- ❌ `/signup` - 게스트 전용 -- ❌ `/forgot-password` - 게스트 전용 - -## 동작 방식 - -### 1. 페이지 로드 시 -``` -페이지 컴포넌트 마운트 - ↓ -useAuthGuard() 실행 - ↓ -/api/auth/check 호출 (HttpOnly 쿠키 검증) - ↓ -인증 성공 → 페이지 표시 -인증 실패 → /login으로 리다이렉트 -``` - -### 2. 뒤로가기 시 (브라우저 캐시) -``` -브라우저 뒤로가기 - ↓ -pageshow 이벤트 감지 - ↓ -event.persisted === true? (캐시된 페이지인가?) - ↓ -Yes → window.location.reload() (새로고침) - ↓ -useAuthGuard() 재실행 - ↓ -인증 확인 → 쿠키 없음 → /login 리다이렉트 -``` - -## 내부 구현 - -`src/hooks/useAuthGuard.ts`: - -```typescript -export function useAuthGuard() { - const router = useRouter(); - - useEffect(() => { - // 1. 인증 확인 - const checkAuth = async () => { - const response = await fetch('/api/auth/check'); - if (!response.ok) { - router.replace('/login'); - } - }; - - checkAuth(); - - // 2. 브라우저 캐시 방지 - const handlePageShow = (event: PageTransitionEvent) => { - if (event.persisted) { - window.location.reload(); - } - }; - - window.addEventListener('pageshow', handlePageShow); - - return () => { - window.removeEventListener('pageshow', handlePageShow); - }; - }, [router]); -} -``` - -## API 엔드포인트 - -### GET /api/auth/check - -**목적**: HttpOnly 쿠키를 통한 인증 상태 확인 - -**요청:** -```http -GET /api/auth/check HTTP/1.1 -Cookie: user_token=... -``` - -**응답 (인증 성공):** -```json -{ - "authenticated": true -} -``` -Status: `200 OK` - -**응답 (인증 실패):** -```json -{ - "error": "Not authenticated", - "authenticated": false -} -``` -Status: `401 Unauthorized` - -## 테스트 시나리오 - -### 시나리오 1: 정상 접근 -1. 로그인 상태로 `/dashboard` 접근 -2. ✅ 페이지 정상 표시 -3. 콘솔 로그 없음 (정상 동작) - -### 시나리오 2: 비로그인 접근 -1. 로그아웃 상태로 `/dashboard` URL 직접 입력 -2. ✅ 즉시 `/login`으로 리다이렉트 -3. 콘솔: "⚠️ 인증 실패: 로그인 페이지로 이동" - -### 시나리오 3: 로그아웃 후 뒤로가기 -1. `/dashboard` 접속 (로그인 상태) -2. Logout 버튼 클릭 → `/login` 이동 -3. 브라우저 뒤로가기 버튼 클릭 -4. ✅ 캐시된 페이지 감지 → 새로고침 → `/login` 리다이렉트 -5. 콘솔: "🔄 캐시된 페이지 감지: 새로고침" - -### 시나리오 4: 다른 탭에서 로그아웃 -1. 탭 A: `/dashboard` 접속 (로그인 상태) -2. 탭 B: 같은 브라우저에서 로그아웃 -3. 탭 A: 페이지 새로고침 또는 다른 페이지 이동 -4. ✅ 인증 확인 실패 → `/login` 리다이렉트 - -## Middleware와의 관계 - -| 보안 레이어 | 역할 | 타이밍 | -|-----------|------|--------| -| **Middleware** | 서버 사이드 경로 보호 | 모든 요청 전 | -| **useAuthGuard** | 클라이언트 사이드 보호 | 페이지 마운트 시 | - -### 왜 둘 다 필요한가? - -**Middleware만 있으면?** -- ❌ 브라우저 뒤로가기 캐시 문제 해결 안됨 -- ❌ 실시간 인증 상태 변경 감지 안됨 - -**useAuthGuard만 있으면?** -- ❌ URL 직접 접근 시 보호 지연 (컴포넌트 마운트 후) -- ❌ 서버 사이드 렌더링 보호 안됨 - -**둘 다 있으면:** -- ✅ 서버 + 클라이언트 이중 보호 -- ✅ 브라우저 캐시 문제 해결 -- ✅ 실시간 인증 상태 동기화 - -## 성능 고려사항 - -### API 호출 최소화 -- `useAuthGuard`는 페이지 마운트 시 1회만 호출 -- 페이지 이동 시마다 다시 실행됨 (의도된 동작) - -### 사용자 경험 -- 인증 확인은 비동기로 처리되어 UI 블로킹 없음 -- 인증 실패 시 `router.replace()` 사용 (뒤로가기 히스토리 오염 방지) - -## 문제 해결 - -### 문제: Hook이 작동하지 않음 -**원인:** 페이지가 Server Component로 되어 있음 -**해결:** 파일 상단에 `"use client";` 추가 - -### 문제: 무한 리다이렉트 -**원인:** `/login` 페이지에도 Hook 적용됨 -**해결:** 게스트 전용 페이지에는 Hook 사용 금지 - -### 문제: 뒤로가기 시 여전히 페이지 보임 -**원인:** `pageshow` 이벤트 리스너 미등록 -**해결:** Hook이 올바르게 import되었는지 확인 - -## 향후 개선 사항 - -### 1. 토큰 검증 추가 -현재는 토큰 존재 여부만 확인하지만, 향후 PHP 백엔드에 토큰 유효성 검증 추가 가능: - -```typescript -// /api/auth/check 개선 -const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/verify`, { - headers: { 'Authorization': `Bearer ${token}` } -}); -``` - -### 2. 자동 새로고침 주기 -장시간 페이지 유지 시 주기적 인증 확인: - -```typescript -useEffect(() => { - const interval = setInterval(checkAuth, 5 * 60 * 1000); // 5분마다 - return () => clearInterval(interval); -}, []); -``` - -### 3. 세션 만료 경고 -토큰 만료 임박 시 사용자에게 알림: - -```typescript -if (expiresIn < 5 * 60 * 1000) { - showToast('세션이 곧 만료됩니다. 다시 로그인해주세요.'); -} -``` - -## 요약 - -✅ **적용 완료:** -- Dashboard 페이지 - -⏳ **적용 필요:** -- 다른 모든 보호된 페이지들 - -📝 **사용법:** -```tsx -import { useAuthGuard } from '@/hooks/useAuthGuard'; - -export default function Page() { - useAuthGuard(); // 이 한 줄만 추가! - return
Content
; -} -``` - -🔒 **보안 효과:** -- 브라우저 캐시 악용 방지 -- 실시간 인증 상태 동기화 -- 로그아웃 후 완전한 페이지 접근 차단 - ---- - -## 관련 파일 - -### 프론트엔드 -- `src/hooks/useAuthGuard.ts` - Auth Guard Hook 구현 -- `src/app/api/auth/check/route.ts` - 인증 체크 API -- `src/app/[locale]/(protected)/layout.tsx` - Protected Layout -- `src/middleware.ts` - 인증 미들웨어 -- `src/lib/api/auth/auth-config.ts` - 인증 설정 (라우트) - -### 보호된 페이지 -- `src/app/[locale]/(protected)/dashboard/page.tsx` -- `src/app/[locale]/(protected)/profile/page.tsx` -- `src/app/[locale]/(protected)/settings/page.tsx` diff --git a/claudedocs/auth/[IMPL-2025-11-07] authentication-implementation-guide.md b/claudedocs/auth/[IMPL-2025-11-07] authentication-implementation-guide.md deleted file mode 100644 index ea13a645..00000000 --- a/claudedocs/auth/[IMPL-2025-11-07] authentication-implementation-guide.md +++ /dev/null @@ -1,328 +0,0 @@ -# 인증 시스템 구현 가이드 - -## 📋 개요 - -Laravel PHP 백엔드와 Next.js 15 프론트엔드 간의 3가지 인증 방식을 지원하는 통합 인증 시스템 - ---- - -## 🔐 지원 인증 방식 - -### 1️⃣ Sanctum Session (웹 사용자) -- **대상**: 웹 브라우저 사용자 -- **방식**: HTTP-only 쿠키 기반 세션 -- **보안**: XSS 방어 + CSRF 토큰 -- **Stateful**: Yes - -### 2️⃣ Bearer Token (모바일/SPA) -- **대상**: 모바일 앱, 외부 SPA -- **방식**: Authorization: Bearer {token} -- **보안**: 토큰 만료 시간 관리 -- **Stateful**: No - -### 3️⃣ API Key (시스템 간 통신) -- **대상**: 서버 간 통신, 백그라운드 작업 -- **방식**: X-API-KEY: {key} -- **보안**: 서버 사이드 전용 (환경 변수) -- **Stateful**: No - ---- - -## 📁 파일 구조 - -``` -src/ -├─ lib/api/ -│ ├─ client.ts # 통합 HTTP Client (3가지 인증 방식) -│ │ -│ └─ auth/ -│ ├─ types.ts # 인증 타입 정의 -│ ├─ auth-config.ts # 인증 설정 (라우트, URL) -│ │ -│ ├─ sanctum-client.ts # Sanctum 전용 클라이언트 -│ ├─ bearer-client.ts # Bearer 토큰 클라이언트 -│ ├─ api-key-client.ts # API Key 클라이언트 -│ │ -│ ├─ token-storage.ts # Bearer 토큰 저장 관리 -│ ├─ api-key-validator.ts # API Key 검증 유틸 -│ └─ server-auth.ts # 서버 컴포넌트 인증 유틸 -│ -├─ contexts/ -│ └─ AuthContext.tsx # 클라이언트 인증 상태 관리 -│ -├─ middleware.ts # 통합 미들웨어 (Bot + Auth + i18n) -│ -└─ app/[locale]/ - ├─ (auth)/ - │ └─ login/page.tsx # 로그인 페이지 - │ - └─ (protected)/ - └─ dashboard/page.tsx # 보호된 페이지 -``` - ---- - -## 🔧 환경 변수 설정 - -### .env.local (실제 키 값) -```env -# API Configuration -NEXT_PUBLIC_API_URL=https://api.5130.co.kr -NEXT_PUBLIC_FRONTEND_URL=http://localhost:3000 - -# Authentication Mode -NEXT_PUBLIC_AUTH_MODE=sanctum - -# API Key (서버 사이드 전용 - 절대 공개 금지!) -API_KEY=42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a -``` - -### .env.example (템플릿) -```env -NEXT_PUBLIC_API_URL=https://api.5130.co.kr -NEXT_PUBLIC_FRONTEND_URL=http://localhost:3000 -NEXT_PUBLIC_AUTH_MODE=sanctum -API_KEY=your-secret-api-key-here -``` - ---- - -## 🎯 구현 단계 - -### Phase 1: 핵심 인프라 (필수) -1. `lib/api/auth/types.ts` - 타입 정의 -2. `lib/api/auth/auth-config.ts` - 인증 설정 -3. `lib/api/client.ts` - 통합 HTTP 클라이언트 -4. `lib/api/auth/sanctum-client.ts` - Sanctum 클라이언트 - -### Phase 2: Middleware 통합 -1. `middleware.ts` 확장 - 인증 체크 로직 추가 -2. 라우트 보호 구현 (protected/guest-only) - -### Phase 3: 로그인 페이지 -1. `app/[locale]/(auth)/login/page.tsx` -2. 기존 validation schema 활용 - -### Phase 4: 보호된 페이지 -1. `app/[locale]/(protected)/dashboard/page.tsx` -2. Server Component로 구현 - ---- - -## 🔒 보안 고려사항 - -### 환경 변수 보안 -```yaml -✅ NEXT_PUBLIC_*: 브라우저 노출 가능 -❌ API_KEY: 절대 NEXT_PUBLIC_ 붙이지 말 것! -✅ .env.local은 .gitignore에 포함됨 -``` - -### 인증 방식별 보안 -```yaml -Sanctum: - ✅ HTTP-only 쿠키 (XSS 방어) - ✅ CSRF 토큰 자동 처리 - ✅ Same-Site: Lax - -Bearer Token: - ⚠️ localStorage 사용 (XSS 취약) - ✅ 토큰 만료 시간 체크 - ✅ Refresh token 권장 - -API Key: - ⚠️ 서버 사이드 전용 - ✅ 환경 변수 관리 - ✅ 주기적 갱신 대비 -``` - ---- - -## 📊 Middleware 인증 플로우 - -``` -Request - ↓ -1. Bot Detection (기존) - ├─ Bot → 403 Forbidden - └─ Human → Continue - ↓ -2. Static Files Check - ├─ Static → Skip Auth - └─ Dynamic → Continue - ↓ -3. Public Routes Check - ├─ Public → Skip Auth - └─ Protected → Continue - ↓ -4. Authentication Check - ├─ Sanctum Session Cookie - ├─ Bearer Token (Authorization header) - └─ API Key (X-API-KEY header) - ↓ -5. Protected Routes Guard - ├─ Authenticated → Allow - └─ Not Authenticated → Redirect /login - ↓ -6. Guest Only Routes - ├─ Authenticated → Redirect /dashboard - └─ Not Authenticated → Allow - ↓ -7. i18n Routing - ↓ -Response -``` - ---- - -## 🚀 API 엔드포인트 - -### 로그인 -``` -POST /api/v1/login -Content-Type: application/json - -Request: -{ - "user_id": "hamss", - "user_pwd": "StrongPass!1234" -} - -Response (성공): -{ - "user": { - "id": 1, - "name": "홍길동", - "email": "hamss@example.com" - }, - "message": "로그인 성공" -} - -Cookie: laravel_session=xxx; HttpOnly; SameSite=Lax -``` - -### 로그아웃 -``` -POST /api/v1/logout - -Response: -{ - "message": "로그아웃 성공" -} -``` - -### 현재 사용자 정보 -``` -GET /api/user -Cookie: laravel_session=xxx - -Response: -{ - "id": 1, - "name": "홍길동", - "email": "hamss@example.com" -} -``` - ---- - -## 📝 사용 예시 - -### 1. Sanctum 로그인 (웹 사용자) -```typescript -import { sanctumClient } from '@/lib/api/auth/sanctum-client'; - -const user = await sanctumClient.login({ - user_id: 'hamss', - user_pwd: 'StrongPass!1234' -}); -``` - -### 2. API Key 요청 (서버 사이드) -```typescript -import { createApiKeyClient } from '@/lib/api/auth/api-key-client'; - -const client = createApiKeyClient(); -const data = await client.fetchData('/api/external-data'); -``` - -### 3. Bearer Token 로그인 (모바일) -```typescript -import { bearerClient } from '@/lib/api/auth/bearer-client'; - -const user = await bearerClient.login({ - email: 'user@example.com', - password: 'password' -}); -``` - ---- - -## ⚠️ 주의사항 - -### API Key 갱신 -- PHP 팀에서 주기적으로 새 키 발급 -- `.env.local`의 `API_KEY` 값만 변경 -- 코드 수정 불필요, 서버 재시작만 필요 - -### Git 보안 -- `.env.local`은 절대 커밋 금지 -- `.env.example`만 템플릿으로 커밋 -- `.gitignore`에 `.env.local` 포함 확인 - -### 개발 환경 -- 개발 서버 시작 시 API Key 자동 검증 -- 콘솔에 검증 상태 출력 -- 에러 발생 시 명확한 가이드 제공 - ---- - -## 🔍 트러블슈팅 - -### API Key 에러 -``` -❌ API_KEY is not configured! -📝 Please check: - 1. .env.local file exists - 2. API_KEY is set correctly - 3. Restart development server (npm run dev) - -💡 Contact backend team if you need a new API key. -``` - -### CORS 에러 -- Laravel `config/cors.php` 확인 -- `supports_credentials: true` 설정 -- `allowed_origins`에 Next.js URL 포함 - -### 세션 쿠키 안받아짐 -- Laravel `SANCTUM_STATEFUL_DOMAINS` 확인 -- `localhost:3000` 포함 확인 -- `SESSION_DOMAIN` 설정 확인 - ---- - -## 📚 참고 문서 - -- [Laravel Sanctum 공식 문서](https://laravel.com/docs/sanctum) -- [Next.js Middleware 문서](https://nextjs.org/docs/app/building-your-application/routing/middleware) -- [claudedocs/authentication-design.md](./authentication-design.md) -- [claudedocs/api-requirements.md](./api-requirements.md) - ---- - -## 관련 파일 - -### 프론트엔드 -- `src/lib/api/client.ts` - 통합 HTTP Client -- `src/lib/api/auth/types.ts` - 인증 타입 정의 -- `src/lib/api/auth/auth-config.ts` - 인증 설정 -- `src/lib/api/auth/sanctum-client.ts` - Sanctum 전용 클라이언트 -- `src/lib/api/auth/bearer-client.ts` - Bearer 토큰 클라이언트 -- `src/lib/api/auth/api-key-client.ts` - API Key 클라이언트 -- `src/contexts/AuthContext.tsx` - 클라이언트 인증 상태 관리 -- `src/middleware.ts` - 통합 미들웨어 - -### 설정 파일 -- `.env.local` - 환경 변수 -- `.env.example` - 환경 변수 템플릿 \ No newline at end of file diff --git a/claudedocs/auth/[IMPL-2025-11-07] jwt-cookie-authentication-final.md b/claudedocs/auth/[IMPL-2025-11-07] jwt-cookie-authentication-final.md deleted file mode 100644 index 1e3b0c7f..00000000 --- a/claudedocs/auth/[IMPL-2025-11-07] jwt-cookie-authentication-final.md +++ /dev/null @@ -1,508 +0,0 @@ -# JWT + Cookie + Middleware 인증 설계 (최종) - -**확정된 API 정보:** -- 인증 방식: Bearer Token (JWT) -- 로그인: `POST /api/v1/login` -- 응답: `{ token: "xxx" }` -- Token 저장: **쿠키** (Middleware 접근 가능) - -## ✅ 핵심 발견 - -**JWT도 쿠키에 저장하면 Middleware에서 처리 가능합니다!** - -```typescript -// middleware.ts에서 JWT 토큰 쿠키 접근 -const authToken = request.cookies.get('auth_token'); // ✅ 가능! - -if (!authToken) { - redirect('/login'); -} -``` - -따라서 **기존 Middleware 설계를 거의 그대로 사용**할 수 있습니다. - ---- - -## 📋 아키텍처 (기존과 동일) - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Next.js Frontend │ -├─────────────────────────────────────────────────────────────┤ -│ Middleware (Server) │ -│ ├─ Bot Detection (기존) │ -│ ├─ Authentication Check (신규) │ -│ │ ├─ JWT Token 쿠키 확인 │ -│ │ └─ 없으면 /login 리다이렉트 │ -│ └─ i18n Routing (기존) │ -├─────────────────────────────────────────────────────────────┤ -│ JWT Client (lib/auth/jwt-client.ts) │ -│ ├─ Token을 쿠키에 저장 │ -│ ├─ API 호출 시 Authorization 헤더 추가 │ -│ └─ 401 응답 시 자동 로그아웃 │ -├─────────────────────────────────────────────────────────────┤ -│ Auth Context (contexts/AuthContext.tsx) │ -│ ├─ 사용자 정보 관리 │ -│ └─ login/logout 함수 │ -└─────────────────────────────────────────────────────────────┘ - ↓ HTTP + Cookie + Authorization -┌─────────────────────────────────────────────────────────────┐ -│ Laravel Backend │ -├─────────────────────────────────────────────────────────────┤ -│ JWT Middleware │ -│ └─ Bearer Token 검증 │ -├─────────────────────────────────────────────────────────────┤ -│ API Endpoints │ -│ ├─ POST /api/v1/login → { token: "xxx" } │ -│ ├─ POST /api/v1/register │ -│ ├─ GET /api/v1/user │ -│ └─ POST /api/v1/logout │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## 🔐 인증 플로우 - -### 1. 로그인 - -``` -1. POST /api/v1/login - → { token: "eyJhbGci..." } - -2. Token을 쿠키에 저장 - document.cookie = 'auth_token=xxx; Secure; SameSite=Strict' - -3. /dashboard 리다이렉트 - -4. Middleware가 쿠키 확인 ✓ - -5. 페이지 렌더링 -``` - -### 2. API 호출 - -``` -1. 쿠키에서 Token 읽기 -2. Authorization 헤더에 추가 - Authorization: Bearer xxx -3. Laravel이 JWT 검증 -4. 데이터 반환 -``` - -### 3. 보호된 페이지 접근 - -``` -사용자 → /dashboard - ↓ -Middleware 실행 - ↓ -auth_token 쿠키 확인 - ↓ -있음 → 페이지 표시 -없음 → /login 리다이렉트 -``` - ---- - -## 🛠️ 핵심 구현 - -### 1. Token 저장 (lib/auth/token-storage.ts) - -```typescript -export const tokenStorage = { - /** - * JWT를 쿠키에 저장 - * - Middleware에서 접근 가능 - * - Secure + SameSite로 보안 강화 - */ - set(token: string): void { - const maxAge = 86400; // 24시간 - document.cookie = `auth_token=${token}; path=/; max-age=${maxAge}; SameSite=Strict; Secure`; - }, - - /** - * 쿠키에서 Token 읽기 - * - 클라이언트에서만 사용 - */ - get(): string | null { - if (typeof window === 'undefined') return null; - - const match = document.cookie.match(/auth_token=([^;]+)/); - return match ? match[1] : null; - }, - - /** - * Token 삭제 - */ - remove(): void { - document.cookie = 'auth_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'; - } -}; -``` - -### 2. JWT Client (lib/auth/jwt-client.ts) - -```typescript -import { tokenStorage } from './token-storage'; - -class JwtClient { - private baseURL = 'https://api.5130.co.kr'; - - /** - * 로그인 - */ - async login(email: string, password: string): Promise { - const response = await fetch(`${this.baseURL}/api/v1/login`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email, password }), - }); - - if (!response.ok) { - throw new Error('Login failed'); - } - - const { token } = await response.json(); - - // ✅ Token을 쿠키에 저장 - tokenStorage.set(token); - - // 사용자 정보 조회 - return await this.getCurrentUser(); - } - - /** - * 현재 사용자 정보 - */ - async getCurrentUser(): Promise { - const token = tokenStorage.get(); - - if (!token) { - throw new Error('No token'); - } - - const response = await fetch(`${this.baseURL}/api/v1/user`, { - headers: { - 'Authorization': `Bearer ${token}`, // ✅ Authorization 헤더 - }, - }); - - if (response.status === 401) { - tokenStorage.remove(); - throw new Error('Unauthorized'); - } - - return await response.json(); - } - - /** - * 로그아웃 - */ - async logout(): Promise { - const token = tokenStorage.get(); - - if (token) { - await fetch(`${this.baseURL}/api/v1/logout`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - }, - }); - } - - // ✅ 쿠키 삭제 - tokenStorage.remove(); - } -} - -export const jwtClient = new JwtClient(); -``` - -### 3. Middleware (middleware.ts) - 기존과 거의 동일! - -```typescript -import { NextResponse } from 'next/server'; -import type { NextRequest } from 'next/server'; -import createIntlMiddleware from 'next-intl/middleware'; -import { locales, defaultLocale } from '@/i18n/config'; - -const intlMiddleware = createIntlMiddleware({ - locales, - defaultLocale, - localePrefix: 'as-needed', -}); - -// 보호된 라우트 -const PROTECTED_ROUTES = [ - '/dashboard', - '/profile', - '/settings', - '/admin', - '/tenant', - '/users', - '/reports', -]; - -// 공개 라우트 -const PUBLIC_ROUTES = [ - '/', - '/login', - '/register', - '/about', - '/contact', -]; - -function isProtectedRoute(pathname: string): boolean { - return PROTECTED_ROUTES.some(route => pathname.startsWith(route)); -} - -function isPublicRoute(pathname: string): boolean { - return PUBLIC_ROUTES.some(route => pathname === route || pathname.startsWith(route)); -} - -function stripLocale(pathname: string): string { - for (const locale of locales) { - if (pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`) { - return pathname.slice(`/${locale}`.length) || '/'; - } - } - return pathname; -} - -export function middleware(request: NextRequest) { - const { pathname } = request.nextUrl; - - // 1. Bot Detection (기존 로직) - // ... bot check code ... - - // 2. 정적 파일 제외 - if ( - pathname.includes('/_next/') || - pathname.includes('/api/') || - pathname.match(/\.(ico|png|jpg|jpeg|svg|gif|webp)$/) - ) { - return intlMiddleware(request); - } - - // 3. 로케일 제거 - const pathnameWithoutLocale = stripLocale(pathname); - - // 4. ✅ JWT Token 쿠키 확인 - const authToken = request.cookies.get('auth_token'); - const isAuthenticated = !!authToken; - - // 5. 보호된 라우트 체크 - if (isProtectedRoute(pathnameWithoutLocale) && !isAuthenticated) { - const url = new URL('/login', request.url); - url.searchParams.set('redirect', pathname); - return NextResponse.redirect(url); - } - - // 6. 게스트 전용 라우트 (이미 로그인한 경우) - if ( - (pathnameWithoutLocale === '/login' || pathnameWithoutLocale === '/register') && - isAuthenticated - ) { - return NextResponse.redirect(new URL('/dashboard', request.url)); - } - - // 7. i18n 미들웨어 - return intlMiddleware(request); -} - -export const config = { - matcher: [ - '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico)$).*)', - ], -}; -``` - -**변경 사항:** -```diff -- const sessionCookie = request.cookies.get('laravel_session'); -+ const authToken = request.cookies.get('auth_token'); -``` - -거의 동일합니다! - -### 4. Auth Context (contexts/AuthContext.tsx) - -```typescript -'use client'; - -import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; -import { jwtClient } from '@/lib/auth/jwt-client'; -import { useRouter } from 'next/navigation'; - -interface User { - id: number; - name: string; - email: string; -} - -interface AuthContextType { - user: User | null; - loading: boolean; - login: (email: string, password: string) => Promise; - logout: () => Promise; -} - -const AuthContext = createContext(undefined); - -export function AuthProvider({ children }: { children: ReactNode }) { - const [user, setUser] = useState(null); - const [loading, setLoading] = useState(true); - const router = useRouter(); - - // 초기 로드 시 사용자 정보 가져오기 - useEffect(() => { - jwtClient.getCurrentUser() - .then(setUser) - .catch(() => setUser(null)) - .finally(() => setLoading(false)); - }, []); - - const login = async (email: string, password: string) => { - const user = await jwtClient.login(email, password); - setUser(user); - router.push('/dashboard'); - }; - - const logout = async () => { - await jwtClient.logout(); - setUser(null); - router.push('/login'); - }; - - return ( - - {children} - - ); -} - -export function useAuth() { - const context = useContext(AuthContext); - if (!context) { - throw new Error('useAuth must be used within AuthProvider'); - } - return context; -} -``` - ---- - -## 📊 세션 쿠키 vs JWT 쿠키 비교 - -| 항목 | 세션 쿠키 (Sanctum) | JWT 쿠키 (현재) | -|------|---------------------|------------------| -| **쿠키 이름** | `laravel_session` | `auth_token` | -| **Middleware 접근** | ✅ 가능 | ✅ 가능 | -| **인증 체크** | 쿠키 존재 확인 | 쿠키 존재 확인 | -| **API 호출** | 쿠키 자동 포함 | Authorization 헤더 | -| **CSRF 토큰** | ✅ 필요 | ❌ 불필요 | -| **서버 상태** | Stateful (세션 저장) | Stateless | -| **보안** | HTTP-only 가능 | Secure + SameSite | -| **구현 복잡도** | 동일 | 동일 | - -**결론:** Middleware 관점에서는 거의 동일합니다! - ---- - -## 🎯 구현 순서 - -### Phase 1: 기본 인프라 (30분) -- [x] auth-config.ts -- [ ] token-storage.ts -- [ ] jwt-client.ts -- [ ] types/auth.ts - -### Phase 2: Middleware 통합 (20분) -- [ ] middleware.ts 업데이트 - - JWT 토큰 쿠키 체크 - - Protected routes 가드 - -### Phase 3: Auth Context (20분) -- [ ] AuthContext.tsx -- [ ] layout.tsx에 AuthProvider 추가 - -### Phase 4: 로그인 페이지 (40분) -- [ ] /login/page.tsx -- [ ] LoginForm 컴포넌트 -- [ ] Form validation (react-hook-form + zod) - -### Phase 5: 테스트 (30분) -- [ ] 로그인 → 대시보드 -- [ ] 비로그인 → 대시보드 → /login 튕김 -- [ ] 로그아웃 → 다시 튕김 - -**총 소요시간: 약 2시간 20분** - ---- - -## ✅ 최종 정리 - -### 핵심 포인트 - -1. **JWT를 쿠키에 저장** → Middleware 접근 가능 -2. **기존 Middleware 설계 유지** → 가드 컴포넌트 불필요 -3. **차이점은 미미함:** - - 쿠키 이름: `laravel_session` → `auth_token` - - CSRF 토큰 불필요 - - API 호출 시 Authorization 헤더 추가 - -### 장점 - -- ✅ Middleware에서 서버사이드 인증 체크 -- ✅ 클라이언트 가드 컴포넌트 불필요 -- ✅ 중복 코드 제거 -- ✅ 기존 설계(authentication-design.md) 거의 그대로 사용 - -### 변경 사항 - -**최소한의 변경만 필요:** -```typescript -// 1. Token 저장: 쿠키 사용 -tokenStorage.set(token); - -// 2. Middleware: 쿠키 이름만 변경 -const authToken = request.cookies.get('auth_token'); - -// 3. API 호출: Authorization 헤더 추가 -headers: { 'Authorization': `Bearer ${token}` } - -// 4. CSRF 토큰: 제거 -// getCsrfToken() 불필요 -``` - ---- - -## 🚀 다음 단계 - -1. ✅ 설계 확정 완료 -2. ⏳ 디자인 컴포넌트 대기 -3. ⏳ 백엔드 API 엔드포인트 확인 - - POST /api/v1/register - - GET /api/v1/user - - POST /api/v1/logout -4. 🚀 구현 시작 (2-3시간) - -**준비되면 바로 시작합니다!** 🎯 - ---- - -## 관련 파일 - -### 프론트엔드 -- `src/middleware.ts` - 인증 미들웨어 -- `src/lib/api/auth/auth-config.ts` - 인증 설정 (라우트, URL) -- `src/lib/api/auth/token-storage.ts` - Token 저장 관리 -- `src/lib/api/auth/jwt-client.ts` - JWT 클라이언트 -- `src/contexts/AuthContext.tsx` - 클라이언트 인증 상태 관리 -- `src/app/[locale]/(auth)/login/page.tsx` - 로그인 페이지 -- `src/app/[locale]/(protected)/dashboard/page.tsx` - 보호된 페이지 - -### 설정 파일 -- `.env.local` - 환경 변수 (API URL, API Key) -- `next.config.ts` - Next.js 설정 \ No newline at end of file diff --git a/claudedocs/auth/[IMPL-2025-11-07] middleware-issue-resolution.md b/claudedocs/auth/[IMPL-2025-11-07] middleware-issue-resolution.md deleted file mode 100644 index de3adf51..00000000 --- a/claudedocs/auth/[IMPL-2025-11-07] middleware-issue-resolution.md +++ /dev/null @@ -1,178 +0,0 @@ -# Middleware 인증 문제 해결 보고서 - -## 📅 작성일: 2025-11-07 - -## 🔍 문제 증상 - -로그인하지 않은 상태에서 `/dashboard`에 접근 시, 인증 체크가 작동하지 않고 대시보드에 바로 접근되는 문제가 발생했습니다. - -### 증상 상세 -- ✅ 로그인/로그아웃 기능 정상 작동 -- ✅ 쿠키(`user_token`) 저장/삭제 정상 -- ❌ Middleware에서 보호된 라우트 접근 차단 실패 -- ❌ Middleware console.log가 터미널에 전혀 출력되지 않음 - ---- - -## 🐛 발견된 문제들 - -### 1. Next.js 15 + next-intl 호환성 문제 -**위치**: `next.config.ts` - -**원인**: -- Next.js 15에서 next-intl v4를 사용할 때 `turbopack` 설정이 필수 -- 이 설정이 없으면 middleware가 제대로 컴파일되지 않음 - -**해결**: -```typescript -// next.config.ts -const nextConfig: NextConfig = { - turbopack: {}, // ✅ 추가 -}; -``` - ---- - -### 2. 복잡한 Matcher 정규식 -**위치**: `src/middleware.ts` - `config.matcher` - -**원인**: -- 너무 복잡한 regex 패턴으로 라우트 매칭 실패 -- 중복된 matcher 패턴 (정규식 + 명시적 경로) - -**기존 코드**: -```typescript -matcher: [ - '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico)$).*)', - '/dashboard/:path*', - '/login', - '/register', -] -``` - -**해결**: -```typescript -matcher: [ - '/((?!api|_next/static|_next/image|favicon.ico|.*\\..*|robots\\.txt).*)', -] -``` - ---- - -### 3. isPublicRoute 함수 로직 버그 ⭐ (핵심 문제) -**위치**: `src/middleware.ts` - `isPublicRoute()` 함수 - -**원인**: -```typescript -// 문제 코드 -function isPublicRoute(pathname: string): boolean { - return AUTH_CONFIG.publicRoutes.some(route => - pathname === route || pathname.startsWith(route) - ); -} -``` - -**버그 시나리오**: -1. `AUTH_CONFIG.publicRoutes`에 `'/'` 포함 -2. `/dashboard`.startsWith('/') → `true` 반환 -3. 모든 경로가 public route로 잘못 판단됨 -4. 인증 체크가 스킵되어 보호된 라우트 접근 가능 - -**해결**: -```typescript -function isPublicRoute(pathname: string): boolean { - return AUTH_CONFIG.publicRoutes.some(route => { - // '/' 는 정확히 일치해야만 public - if (route === '/') { - return pathname === '/'; - } - // 다른 라우트는 시작 일치 허용 - return pathname === route || pathname.startsWith(route + '/'); - }); -} -``` - -**수정 후 동작**: -- `/` → public ✅ -- `/dashboard` → protected ✅ -- `/about` → public ✅ -- `/about/team` → public ✅ - ---- - -## ✅ 해결 결과 - -### 적용된 수정 사항 -1. ✅ `next.config.ts`에 `turbopack: {}` 추가 -2. ✅ Middleware matcher 단순화 -3. ✅ `isPublicRoute()` 함수 로직 수정 -4. ✅ 디버깅 로그 제거 (클린 코드) - -### 검증 결과 -```bash -# 로그아웃 상태에서 /dashboard 접근 시: -[Auth Required] Redirecting to /login from /dashboard -→ 자동으로 /login 페이지로 리다이렉트 ✅ - -# 로그인 상태에서 /dashboard 접근 시: -[Authenticated] Mode: bearer, Path: /dashboard -→ 정상 접근 ✅ -``` - ---- - -## 📝 교훈 - -### 1. Middleware 디버깅 -- **브라우저 콘솔이 아닌 서버 터미널**에서 로그 확인 -- `console.log`는 서버 사이드에서 실행되므로 터미널 출력 - -### 2. 문자열 매칭 주의 -- `startsWith('/')` 같은 패턴은 모든 경로와 매칭됨 -- Root path(`/`)는 항상 정확한 일치(`===`) 사용 - -### 3. Next.js 버전별 설정 -- Next.js 15 + next-intl 사용 시 `turbopack` 설정 필수 -- 공식 문서 및 마이그레이션 가이드 확인 필요 - ---- - -## 🔗 관련 파일 - -### 수정된 파일 -- `next.config.ts` - turbopack 설정 추가 -- `src/middleware.ts` - isPublicRoute 로직 수정, matcher 단순화 - -### 관련 설정 파일 -- `src/lib/api/auth/auth-config.ts` - 라우트 설정 -- `src/lib/api/auth/sanctum-client.ts` - 인증 로직 -- `src/lib/api/auth/token-storage.ts` - 토큰 관리 - ---- - -## 🎯 현재 인증 플로우 - -### 로그인 -1. 사용자가 `/login`에서 인증 정보 입력 -2. PHP API(`/api/v1/login`)로 요청 (API Key 포함) -3. Bearer Token 발급 (`user_token`) -4. localStorage 저장 + Cookie 동기화 -5. `/dashboard`로 리다이렉트 - -### 보호된 라우트 접근 -1. Middleware에서 요청 가로채기 -2. Cookie에서 `user_token` 확인 -3. 토큰 있음 → 통과 -4. 토큰 없음 → `/login`으로 리다이렉트 - -### 로그아웃 -1. PHP API(`/api/v1/logout`) 호출 -2. localStorage 및 Cookie 정리 -3. `/login`으로 리다이렉트 - ---- - -## 📚 참고 자료 -- Next.js 15 Middleware 공식 문서 -- next-intl v4 마이그레이션 가이드 -- `claudedocs/research_nextjs15_middleware_authentication_2025-11-07.md` \ No newline at end of file diff --git a/claudedocs/auth/[IMPL-2025-11-07] route-protection-architecture.md b/claudedocs/auth/[IMPL-2025-11-07] route-protection-architecture.md deleted file mode 100644 index 4f6d48dd..00000000 --- a/claudedocs/auth/[IMPL-2025-11-07] route-protection-architecture.md +++ /dev/null @@ -1,513 +0,0 @@ -# Route Protection Architecture - 최종 구조 - -## 개요 - -**2단계 보호 시스템:** -1. **Middleware (서버)**: 모든 페이지 요청 시 인증 확인 -2. **Layout Hook (클라이언트)**: 보호된 페이지의 브라우저 캐시 방지 - ---- - -## 폴더 구조 - -``` -src/app/[locale]/ -├── (auth)/ # 게스트 전용 페이지 -│ └── login/ -│ └── page.tsx # 로그인 페이지 (컴포넌트 재사용) -│ -├── (protected)/ # ✅ 보호된 페이지 그룹 -│ ├── layout.tsx # 🔒 useAuthGuard() 여기서만! -│ └── dashboard/ -│ └── page.tsx # useAuthGuard() 불필요 -│ -├── login/ # 직접 접근용 로그인 페이지 -│ └── page.tsx -│ -├── signup/ # 직접 접근용 회원가입 페이지 -│ └── page.tsx -│ -├── page.tsx # 홈페이지 (공개) -└── layout.tsx # 루트 레이아웃 -``` - -**Route Group 설명:** -- `(auth)`: 괄호로 감싸져 있어 URL에 포함되지 않음 - - `/login` → `src/app/[locale]/login/page.tsx` - - `/(auth)/login` → 동일한 `/login` URL -- `(protected)`: Layout 기반 보호 그룹 - - `/dashboard` → `src/app/[locale]/(protected)/dashboard/page.tsx` - - Layout의 `useAuthGuard()`가 자동 적용 - ---- - -## 보호 레이어 상세 - -### Layer 1: Middleware (서버 사이드) - -**파일:** `src/middleware.ts` - -**역할:** -- 모든 HTTP 요청 차단 (페이지, API, 리소스) -- HttpOnly 쿠키 검증 -- 인증 실패 시 `/login` 리다이렉트 - -**적용 범위:** -- URL 직접 입력 -- 링크 클릭 -- 새로고침 (F5) -- 프로그래매틱 네비게이션 - -**코드:** -```typescript -// src/middleware.ts -function checkAuthentication(request: NextRequest) { - const tokenCookie = request.cookies.get('user_token'); - if (tokenCookie?.value) { - return { isAuthenticated: true, authMode: 'bearer' }; - } - return { isAuthenticated: false, authMode: null }; -} - -// 보호된 경로 체크 -if (!isAuthenticated && !isPublicRoute && !isGuestOnlyRoute) { - return NextResponse.redirect(new URL('/login', request.url)); -} -``` - ---- - -### Layer 2: Protected Layout (클라이언트 사이드) - -**파일:** `src/app/[locale]/(protected)/layout.tsx` - -**역할:** -- 페이지 마운트 시 인증 재확인 -- 브라우저 BFCache (뒤로가기 캐시) 감지 및 새로고침 -- 다른 탭에서 로그아웃 시 동기화 - -**적용 범위:** -- `(protected)` 폴더 하위 모든 페이지 -- 브라우저 뒤로가기 -- 페이지 캐시 복원 - -**코드:** -```typescript -// src/app/[locale]/(protected)/layout.tsx -"use client"; - -import { useAuthGuard } from '@/hooks/useAuthGuard'; - -export default function ProtectedLayout({ children }) { - useAuthGuard(); // 모든 하위 페이지에 자동 적용 - return <>{children}; -} -``` - ---- - -## 시나리오별 동작 - -### ✅ 시나리오 1: URL 직접 입력 (비로그인) - -``` -http://localhost:3000/dashboard 입력 - ↓ -🛡️ Middleware 실행 - → 쿠키 없음 - → /login 리다이렉트 - ↓ -로그인 페이지 표시 -(Layout Hook은 실행되지 않음) -``` - -**결과:** Middleware만으로 차단 완료 ✅ - ---- - -### ✅ 시나리오 2: 정상 로그인 후 접근 - -``` -로그인 성공 → /dashboard 이동 - ↓ -🛡️ Middleware 실행 - → 쿠키 있음 - → 통과 - ↓ -(protected)/layout.tsx 마운트 - → useAuthGuard() 실행 - → /api/auth/check 호출 - → 인증 성공 - ↓ -dashboard/page.tsx 렌더링 -``` - -**결과:** 이중 검증 통과 ✅ - ---- - -### ✅ 시나리오 3: 로그아웃 후 뒤로가기 (핵심!) - -``` -/dashboard 접속 (로그인 상태) - ↓ -Logout 버튼 클릭 - → /api/auth/logout 호출 - → HttpOnly 쿠키 삭제 - → /login 이동 - ↓ -브라우저 뒤로가기 버튼 클릭 - ↓ -⚠️ 브라우저 캐시에서 /dashboard 복원 - → 서버 요청 없음 - → Middleware 실행 안됨 ❌ - ↓ -🛡️ (protected)/layout.tsx 복원 - → useAuthGuard() 실행 - → pageshow 이벤트 감지 - → event.persisted === true (캐시됨) - → window.location.reload() 실행 - ↓ -새로고침 → 서버 요청 발생 - ↓ -🛡️ Middleware 실행 - → 쿠키 없음 - → /login 리다이렉트 - ↓ -로그인 페이지 표시 -``` - -**결과:** Layout Hook이 캐시 우회 → Middleware 재실행 ✅ - ---- - -### ✅ 시나리오 4: 다른 탭에서 로그아웃 - -``` -탭 A: /dashboard 접속 (로그인 상태) -탭 B: 로그아웃 - ↓ -탭 A: 페이지 새로고침 또는 네비게이션 - ↓ -🛡️ Middleware 실행 - → 쿠키 없음 (탭 B에서 삭제됨) - → /login 리다이렉트 -``` - -**결과:** 쿠키 공유로 즉시 차단 ✅ - ---- - -## 새 페이지 추가 방법 - -### 보호된 페이지 추가 - -**단계:** -1. `(protected)` 폴더 안에 페이지 생성 -2. **끝!** (자동으로 보호됨) - -**예시:** -```bash -# Profile 페이지 생성 -mkdir -p src/app/[locale]/(protected)/profile -``` - -```tsx -// src/app/[locale]/(protected)/profile/page.tsx -"use client"; - -export default function Profile() { - // useAuthGuard() 불필요! Layout에서 자동 처리 - return
Profile Content
; -} -``` - -**URL:** `/profile` (Route Group 괄호는 URL에 포함 안됨) - ---- - -### 공개 페이지 추가 - -**단계:** -1. `(protected)` 폴더 **밖**에 페이지 생성 -2. `auth-config.ts`의 `publicRoutes`에 추가 (필요시) - -**예시:** -```bash -# About 페이지 생성 (공개) -mkdir -p src/app/[locale]/about -``` - -```tsx -// src/app/[locale]/about/page.tsx -export default function About() { - return
About Us (Public)
; -} -``` - -```typescript -// src/lib/api/auth/auth-config.ts -export const AUTH_CONFIG = { - publicRoutes: [ - '/about', // 추가 - ], - // ... -}; -``` - ---- - -## 구현 상세 - -### useAuthGuard Hook - -**파일:** `src/hooks/useAuthGuard.ts` - -```typescript -export function useAuthGuard() { - const router = useRouter(); - - useEffect(() => { - // 1. 페이지 로드 시 인증 확인 - const checkAuth = async () => { - const response = await fetch('/api/auth/check'); - if (!response.ok) { - router.replace('/login'); - } - }; - - checkAuth(); - - // 2. 브라우저 캐시 감지 및 새로고침 - const handlePageShow = (event: PageTransitionEvent) => { - if (event.persisted) { - console.log('🔄 캐시된 페이지 감지: 새로고침'); - window.location.reload(); - } - }; - - window.addEventListener('pageshow', handlePageShow); - - return () => { - window.removeEventListener('pageshow', handlePageShow); - }; - }, [router]); -} -``` - -**핵심 로직:** -1. `checkAuth()`: `/api/auth/check` 호출로 실시간 인증 확인 -2. `pageshow` 이벤트: `event.persisted`로 캐시 감지 -3. `window.location.reload()`: 강제 새로고침으로 Middleware 재실행 - ---- - -### Auth Check API - -**파일:** `src/app/api/auth/check/route.ts` - -```typescript -export async function GET(request: NextRequest) { - const token = request.cookies.get('user_token')?.value; - - if (!token) { - return NextResponse.json( - { error: 'Not authenticated', authenticated: false }, - { status: 401 } - ); - } - - return NextResponse.json( - { authenticated: true }, - { status: 200 } - ); -} -``` - -**역할:** -- HttpOnly 쿠키 읽기 -- 인증 상태 반환 (200 or 401) - ---- - -## 보안 장점 - -### ✅ 이전 (각 페이지에 Hook) -``` -각 페이지마다 useAuthGuard() 수동 추가 -→ 누락 위험 ⚠️ -→ 보일러플레이트 코드 증가 -``` - -### ✅ 현재 (Layout 기반) -``` -(protected)/layout.tsx에서 한 번만 -→ 새 페이지 자동 보호 -→ 누락 불가능 -→ 코드 중복 제거 -``` - ---- - -## 설정 파일 - -### auth-config.ts - -**파일:** `src/lib/api/auth/auth-config.ts` - -```typescript -export const AUTH_CONFIG = { - // 🔓 공개 라우트 (인증 불필요) - publicRoutes: [], - - // 🔒 보호된 라우트 (참고용, 실제로는 기본 정책으로 보호) - protectedRoutes: [ - '/dashboard', - '/profile', - '/settings', - '/admin', - // ... 모든 보호된 경로 - ], - - // 👤 게스트 전용 라우트 (로그인 후 접근 불가) - guestOnlyRoutes: [ - '/login', - '/signup', - '/forgot-password', - ], - - // 리다이렉트 설정 - redirects: { - afterLogin: '/dashboard', - afterLogout: '/login', - unauthorized: '/login', - }, -}; -``` - ---- - -## 테스트 체크리스트 - -### 필수 테스트 - -- [ ] **URL 직접 입력 (비로그인)** - - `/dashboard` 입력 → `/login` 리다이렉트 - -- [ ] **로그인 후 접근** - - 로그인 → `/dashboard` 정상 표시 - -- [ ] **로그아웃 후 뒤로가기** - - 로그아웃 → 뒤로가기 → 캐시 감지 → 새로고침 → `/login` 리다이렉트 - -- [ ] **다른 탭에서 로그아웃** - - 탭 A: `/dashboard` 유지 - - 탭 B: 로그아웃 - - 탭 A: 새로고침 → `/login` 리다이렉트 - -- [ ] **새 보호된 페이지 추가** - - `(protected)/profile` 생성 → 자동 보호 확인 - ---- - -## 트러블슈팅 - -### 문제: 로그아웃 후 뒤로가기 시 페이지 보임 - -**원인:** Layout이 Client Component가 아님 - -**해결:** -```tsx -// (protected)/layout.tsx 파일 상단에 추가 -"use client"; -``` - ---- - -### 문제: 404 에러 (페이지를 찾을 수 없음) - -**원인:** 폴더 이름 오타 또는 Route Group 괄호 누락 - -**확인:** -```bash -# 올바른 경로 -src/app/[locale]/(protected)/dashboard/page.tsx - -# 잘못된 경로 -src/app/[locale]/protected/dashboard/page.tsx # 괄호 없음 -``` - ---- - -### 문제: 무한 리다이렉트 - -**원인:** `/login` 페이지에도 보호 적용됨 - -**확인:** -- `/login`이 `(protected)` 폴더 **밖**에 있는지 확인 -- `guestOnlyRoutes`에 `/login` 포함 확인 - ---- - -## 성능 고려사항 - -### API 호출 최소화 -- `useAuthGuard`는 페이지 마운트 시 **1회만** 호출 -- 브라우저 캐시 복원 시에만 추가 호출 (새로고침) - -### 사용자 경험 -- 인증 확인은 비동기로 처리 (UI 블로킹 없음) -- `router.replace()` 사용으로 뒤로가기 히스토리 오염 방지 - ---- - -## 향후 페이지 추가 계획 - -### 즉시 적용 가능 (보호됨) -`(protected)` 폴더에 추가하면 자동 보호: - -``` -(protected)/ -├── profile/ # 사용자 프로필 -├── settings/ # 설정 -├── admin/ # 관리자 -│ ├── users/ -│ ├── tenants/ -│ └── reports/ -├── inventory/ # 재고 관리 -├── finance/ # 재무 -├── hr/ # 인사 -└── crm/ # CRM -``` - ---- - -## 요약 - -### ✅ 최종 아키텍처 - -``` -보호 정책: -1. Middleware (서버): 모든 요청 차단 -2. Layout (클라이언트): 캐시 우회 및 실시간 동기화 - -폴더 구조: -- (protected)/layout.tsx: 한 곳에서만 관리 -- (protected)/**/page.tsx: 자동으로 보호됨 - -장점: -✅ 코드 중복 제거 -✅ 누락 불가능 -✅ 브라우저 캐시 문제 해결 -✅ 확장성 (새 페이지 자동 보호) -✅ 유지보수성 향상 -``` - ---- - -## 참고 문서 - -- **HttpOnly Cookie 구현**: `claudedocs/httponly-cookie-implementation.md` -- **Auth Guard 사용법**: `claudedocs/auth-guard-usage.md` -- **Middleware 설정**: `src/middleware.ts` -- **Auth 설정**: `src/lib/api/auth/auth-config.ts` \ No newline at end of file diff --git a/claudedocs/auth/[IMPL-2025-11-10] token-management-guide.md b/claudedocs/auth/[IMPL-2025-11-10] token-management-guide.md deleted file mode 100644 index 6d6d860a..00000000 --- a/claudedocs/auth/[IMPL-2025-11-10] token-management-guide.md +++ /dev/null @@ -1,467 +0,0 @@ -# Token Management System Guide - -완전한 Access Token & Refresh Token 시스템 구현 가이드 - -## 📋 목차 - -1. [시스템 개요](#시스템-개요) -2. [토큰 라이프사이클](#토큰-라이프사이클) -3. [API 엔드포인트](#api-엔드포인트) -4. [자동 토큰 갱신](#자동-토큰-갱신) -5. [사용 예시](#사용-예시) -6. [보안 고려사항](#보안-고려사항) - ---- - -## 시스템 개요 - -### 토큰 구조 - -```json -{ - "access_token": "214|EU7drdTBYN1fru0MylLXwjJbi2svXcikn5ofvmTI354d09c7", - "refresh_token": "215|6hAPWcO05jtfSDV9Yz4kLQi3qZDFuycMqrNITOV3c27bd0cb", - "token_type": "Bearer", - "expires_in": 7200, - "expires_at": "2025-11-10 15:49:38" -} -``` - -### 저장 방식 - -**HttpOnly 쿠키** (XSS 공격 방지): -- `access_token`: 2시간 만료 (7200초) -- `refresh_token`: 7일 만료 (604800초) - -**보안 속성**: -- `HttpOnly`: JavaScript 접근 불가 -- `Secure`: HTTPS만 전송 -- `SameSite=Strict`: CSRF 공격 방지 - ---- - -## 토큰 라이프사이클 - -### 1. 로그인 (Token 발급) - -``` -사용자 로그인 - ↓ -POST /api/auth/login - ↓ -PHP Backend /api/v1/login - ↓ -access_token + refresh_token 발급 - ↓ -HttpOnly 쿠키에 저장 - ↓ -대시보드로 이동 -``` - -### 2. 인증된 요청 - -``` -보호된 페이지 접근 - ↓ -Middleware 인증 체크 - ↓ -access_token 존재? - ├─ Yes → 접근 허용 - └─ No → refresh_token 확인 - ├─ 있음 → 자동 갱신 시도 - └─ 없음 → 로그인 페이지로 -``` - -### 3. 토큰 갱신 - -``` -access_token 만료 (2시간 후) - ↓ -보호된 API 호출 시도 - ↓ -401 Unauthorized 응답 - ↓ -POST /api/auth/refresh - ↓ -refresh_token으로 새 토큰 발급 - ↓ -새 access_token + refresh_token 쿠키 업데이트 - ↓ -원래 API 호출 재시도 - ↓ -성공 -``` - -### 4. 로그아웃 - -``` -사용자 로그아웃 - ↓ -POST /api/auth/logout - ↓ -PHP Backend /api/v1/logout (토큰 무효화) - ↓ -HttpOnly 쿠키 삭제 - ↓ -로그인 페이지로 이동 -``` - ---- - -## API 엔드포인트 - -### 1. Login API - -**Endpoint**: `POST /api/auth/login` - -**Request**: -```typescript -{ - user_id: string; - user_pwd: string; -} -``` - -**Response**: -```typescript -{ - message: string; - user: UserObject; - tenant: TenantObject | null; - menus: MenuItem[]; - token_type: "Bearer"; - expires_in: number; - expires_at: string; -} -``` - -**쿠키 설정**: -- `access_token` (HttpOnly, 2시간) -- `refresh_token` (HttpOnly, 7일) - ---- - -### 2. Refresh Token API - -**Endpoint**: `POST /api/auth/refresh` - -**쿠키 필요**: `refresh_token` - -**Response** (성공): -```typescript -{ - message: "Token refreshed successfully"; - token_type: "Bearer"; - expires_in: number; - expires_at: string; -} -``` - -**Response** (실패): -```typescript -{ - error: "Token refresh failed"; - needsReauth: true; -} -``` - -**쿠키 업데이트**: -- 새 `access_token` (2시간) -- 새 `refresh_token` (7일) - ---- - -### 3. Auth Check API - -**Endpoint**: `GET /api/auth/check` - -**기능**: -1. `access_token` 존재 → 200 OK with `authenticated: true` -2. `access_token` 없음 + `refresh_token` 있음 → 자동 갱신 시도 - - 갱신 성공 → 200 OK with `authenticated: true, refreshed: true` - - 갱신 실패 → 401 Unauthorized -3. 둘 다 없음 → 401 Unauthorized - -**Response**: -```typescript -// ✅ 인증 성공 (200) -{ - authenticated: true; - refreshed?: boolean; // 자동 갱신 여부 -} - -// ❌ 인증 실패 (401) -{ - error: string; // 'Not authenticated' 또는 'Token refresh failed' -} -``` - -**참고**: -- 🔵 **Next.js 내부 API** (PHP 백엔드 X) -- 성능 최적화: 로컬 쿠키만 확인하여 빠른 응답 - -> ⚠️ **2025-11-27 변경사항**: -> - `LoginPage.tsx`에서 auth/check 호출 제거됨 -> - **제거 이유**: -> 1. 미들웨어(`middleware.ts`)에서 이미 동일한 처리를 함 (guestOnlyRoutes 리다이렉트) -> 2. 401 응답이 Network 탭에 에러로 표시되어 백엔드 개발자 혼란 유발 -> 3. 불필요한 API 호출로 인한 성능 저하 -> - **대체 방안**: 미들웨어가 서버 사이드에서 쿠키 체크 후 리다이렉트 처리 -> - 참고: `src/components/auth/LoginPage.tsx` 주석 참조 - ---- - -### 4. Logout API - -**Endpoint**: `POST /api/auth/logout` - -**기능**: -1. PHP 백엔드에 로그아웃 요청 (토큰 무효화) -2. `access_token`, `refresh_token` 쿠키 삭제 - ---- - -## 자동 토큰 갱신 - -### 1. Middleware에서 자동 갱신 - -`src/middleware.ts`: -```typescript -// access_token 또는 refresh_token이 있으면 인증됨 -const accessToken = request.cookies.get('access_token'); -const refreshToken = request.cookies.get('refresh_token'); - -if ((accessToken && accessToken.value) || (refreshToken && refreshToken.value)) { - return { isAuthenticated: true, authMode: 'bearer' }; -} -``` - -### 2. Auth Check에서 자동 갱신 - -`src/app/api/auth/check/route.ts`: -```typescript -// access_token 없고 refresh_token만 있으면 자동 갱신 -if (refreshToken && !accessToken) { - const refreshResponse = await fetch('/api/v1/refresh', {...}); - // 새 토큰을 HttpOnly 쿠키로 설정 -} -``` - -### 3. Proxy에서 자동 갱신 (✅ 2025-11-27 구현) - -`src/app/api/proxy/[...path]/route.ts`: -```typescript -// 401 응답 시 자동 토큰 갱신 후 재시도 -if (backendResponse.status === 401 && refreshToken) { - const refreshResult = await refreshAccessToken(refreshToken); - - if (refreshResult.success && refreshResult.accessToken) { - // 새 토큰으로 원래 요청 재시도 - token = refreshResult.accessToken; - backendResponse = await executeBackendRequest(url, method, token, body, contentType); - - // 새 토큰을 쿠키에 저장 - createTokenCookies(newTokens).forEach(cookie => { - clientResponse.headers.append('Set-Cookie', cookie); - }); - } else { - // 리프레시 실패 → 쿠키 삭제 후 401 반환 - return NextResponse.json({ error: 'Authentication failed', needsReauth: true }, { status: 401 }); - } -} -``` - -**동작 방식**: -1. 백엔드 API 호출 (access_token 사용) -2. 401 Unauthorized 응답 받음 -3. refresh_token으로 `/api/v1/refresh` 호출 -4. 성공 시: 새 토큰으로 원래 요청 재시도 + 쿠키 업데이트 -5. 실패 시: 쿠키 삭제 + `needsReauth: true` 응답 - -> **장점**: 프론트엔드 코드 수정 없이 모든 `/api/proxy/*` 요청에 자동 토큰 갱신 적용 - -### 4. API Client에서 자동 갱신 (Legacy) - -`src/lib/api/client.ts`: -```typescript -// withTokenRefresh 헬퍼 함수 사용 -const data = await withTokenRefresh(() => - apiClient.get('/protected/resource') -); -``` - -**동작 방식**: -1. API 호출 시도 -2. 401 응답 받음 -3. `/api/auth/refresh` 호출 -4. 성공 시 원래 API 재시도 -5. 실패 시 로그인 페이지로 리다이렉트 - -> **참고**: 대부분의 API 호출은 프록시를 통해 자동 갱신되므로 직접 사용할 필요 없음 - ---- - -## 사용 예시 - -### 예시 1: 보호된 페이지에서 API 호출 - -```typescript -// src/app/[locale]/(protected)/dashboard/page.tsx -import { withTokenRefresh } from '@/lib/api/client'; - -export default function Dashboard() { - const fetchData = async () => { - try { - // 자동 토큰 갱신 포함 - const data = await withTokenRefresh(() => - fetch('/api/protected/data', { - credentials: 'include' // 쿠키 포함 - }) - ); - - console.log('Data fetched:', data); - } catch (error) { - console.error('Fetch failed:', error); - } - }; - - return
...
; -} -``` - -### 예시 2: 수동 토큰 갱신 - -```typescript -// src/lib/auth/token-refresh.ts -import { refreshTokenClient } from '@/lib/auth/token-refresh'; - -async function handleProtectedAction() { - try { - // API 호출 - const response = await fetch('/api/protected/action'); - - if (!response.ok) { - // 401 에러 시 토큰 갱신 시도 - const refreshed = await refreshTokenClient(); - - if (refreshed) { - // 재시도 - return await fetch('/api/protected/action'); - } - } - - return response; - } catch (error) { - console.error('Action failed:', error); - } -} -``` - -### 예시 3: Protected Layout - -```typescript -// src/app/[locale]/(protected)/layout.tsx -"use client"; - -import { useAuthGuard } from '@/hooks/useAuthGuard'; - -export default function ProtectedLayout({ children }) { - // 자동으로 /api/auth/check 호출 - // access_token 없으면 refresh_token으로 자동 갱신 - useAuthGuard(); - - return <>{children}; -} -``` - ---- - -## 보안 고려사항 - -### ✅ 구현된 보안 기능 - -1. **HttpOnly 쿠키** - - JavaScript에서 토큰 접근 불가 - - XSS 공격으로부터 보호 - -2. **Secure 플래그** - - HTTPS에서만 쿠키 전송 - - 중간자 공격 방지 - -3. **SameSite=Strict** - - CSRF 공격 방지 - - 크로스 사이트 요청 차단 - -4. **토큰 만료 시간** - - Access Token: 2시간 (짧은 수명) - - Refresh Token: 7일 (긴 수명) - -5. **에러 메시지 일반화** - - 백엔드 상세 에러 노출 방지 - - 정보 유출 차단 - -### ⚠️ 추가 권장 사항 - -1. **Token Rotation** - - Refresh 시 새로운 refresh_token 발급 (현재 구현됨 ✅) - -2. **Rate Limiting** - - 로그인 시도 제한 - - Refresh 요청 제한 - -3. **IP 검증** - - 토큰 발급 시 IP 기록 - - 다른 IP에서 사용 시 경고 - -4. **Device Fingerprinting** - - 토큰 발급 디바이스 기록 - - 이상 접근 탐지 - -5. **Logout Blacklist** - - 로그아웃 된 토큰 블랙리스트 관리 - - 재사용 방지 - ---- - -## 트러블슈팅 - -### 문제 1: 로그인 후 바로 로그아웃됨 - -**원인**: 쿠키가 설정되지 않음 - -**해결**: -1. 브라우저 개발자 도구 → Application → Cookies 확인 -2. `access_token`, `refresh_token` 존재 확인 -3. 없으면 `/api/auth/login` 응답 헤더 확인 - -### 문제 2: Token refresh 무한 루프 - -**원인**: Refresh token도 만료됨 - -**해결**: -1. `/api/auth/refresh` 응답 확인 -2. 401 응답 시 로그인 페이지로 리다이렉트 -3. `needsReauth: true` 플래그 확인 - -### 문제 3: CORS 에러 - -**원인**: 크로스 도메인 요청 시 쿠키 전송 실패 - -**해결**: -```typescript -fetch('/api/protected', { - credentials: 'include' // 쿠키 포함 -}) -``` - ---- - -## 참고 파일 - -- `src/app/api/auth/login/route.ts` - 로그인 API -- `src/app/api/auth/refresh/route.ts` - 토큰 갱신 API -- `src/app/api/auth/check/route.ts` - 인증 체크 API -- `src/app/api/auth/logout/route.ts` - 로그아웃 API -- `src/middleware.ts` - 인증 미들웨어 -- `src/lib/auth/token-refresh.ts` - 토큰 갱신 유틸리티 -- `src/lib/api/client.ts` - API 클라이언트 (자동 갱신) \ No newline at end of file diff --git a/claudedocs/auth/[IMPL-2025-11-13] safari-cookie-compatibility.md b/claudedocs/auth/[IMPL-2025-11-13] safari-cookie-compatibility.md deleted file mode 100644 index 7c683493..00000000 --- a/claudedocs/auth/[IMPL-2025-11-13] safari-cookie-compatibility.md +++ /dev/null @@ -1,504 +0,0 @@ -# Safari 쿠키 호환성 및 크로스 브라우저 가이드 - -## 📋 목차 -1. [문제 상황](#문제-상황) -2. [원인 분석](#원인-분석) -3. [해결 방법](#해결-방법) -4. [수정된 파일](#수정된-파일) -5. [크로스 브라우저 개발 가이드라인](#크로스-브라우저-개발-가이드라인) -6. [테스트 체크리스트](#테스트-체크리스트) - ---- - -## 문제 상황 - -### Safari에서 발생한 인증 문제 -- **로그인**: 성공했으나 대시보드로 이동 불가 ({"error":"Not authenticated"}) -- **로그아웃**: 로그아웃 버튼 클릭 시 정상 동작하지 않음 -- **크롬/파이어폭스**: 정상 작동 - -### 증상 -```bash -# Safari 브라우저 -✅ 로그인 API 호출 성공 (200 OK) -❌ 대시보드 접근 실패 (401 Unauthorized) -❌ 쿠키가 저장되지 않음 - -# Chrome/Firefox 브라우저 -✅ 모든 기능 정상 작동 -``` - ---- - -## 원인 분석 - -### Safari의 엄격한 쿠키 정책 - -Safari는 다른 브라우저보다 **쿠키 보안 정책이 엄격**합니다: - -#### 1. Secure 속성 제한 -```typescript -// ❌ Safari에서 작동하지 않음 (HTTP 환경) -const cookie = 'access_token=xxx; HttpOnly; Secure; SameSite=Strict'; - -// Safari 로직: -// - HTTP (localhost:3000) + Secure 속성 = 쿠키 저장 거부 -// - HTTPS만 Secure 쿠키 허용 -``` - -Chrome/Firefox는 `localhost`에서 `Secure` 속성을 허용하지만, **Safari는 허용하지 않습니다**. - -#### 2. SameSite=Strict의 제약 -```typescript -// SameSite=Strict: 모든 크로스 사이트 요청에서 쿠키 차단 -// - 너무 엄격하여 일부 정상적인 요청도 차단될 수 있음 - -// SameSite=Lax: CSRF 보호 + 유연성 -// - GET 요청과 top-level navigation에서는 쿠키 전송 허용 -// - 대부분의 웹 애플리케이션에 적합 -``` - -#### 3. 쿠키 삭제 시 속성 불일치 -Safari는 쿠키를 삭제할 때 **설정할 때와 정확히 동일한 속성**을 요구합니다: - -```typescript -// ❌ Safari에서 쿠키 삭제 실패 -// 설정: HttpOnly + SameSite=Lax (Secure 없음) -// 삭제: HttpOnly + Secure + SameSite=Strict - -// ✅ Safari에서 쿠키 삭제 성공 -// 설정: HttpOnly + SameSite=Lax (Secure 없음) -// 삭제: HttpOnly + SameSite=Lax (Secure 없음) -``` - ---- - -## 해결 방법 - -### 핵심 원칙: 환경별 조건부 쿠키 설정 - -```typescript -// 1. 환경 감지 -const isProduction = process.env.NODE_ENV === 'production'; - -// 2. 조건부 Secure 속성 -const cookie = [ - 'access_token=xxx', - 'HttpOnly', // ✅ 항상 유지 (XSS 보호) - ...(isProduction ? ['Secure'] : []), // ✅ HTTPS에서만 적용 - 'SameSite=Lax', // ✅ CSRF 보호 + 호환성 - 'Path=/', - 'Max-Age=7200', -].join('; '); -``` - -### 환경별 쿠키 속성 - -| 환경 | Secure | SameSite | HttpOnly | 설명 | -|------|--------|----------|----------|------| -| **Development** (HTTP) | ❌ 없음 | Lax | ✅ 있음 | Safari 호환성 | -| **Production** (HTTPS) | ✅ 있음 | Lax | ✅ 있음 | 완전한 보안 | - ---- - -## 수정된 파일 - -### 1. `src/app/api/auth/login/route.ts` - -**수정 위치**: 150-170 라인 - -```typescript -// ❌ 기존 코드 (Safari 비호환) -const accessTokenCookie = [ - `access_token=${data.access_token}`, - 'HttpOnly', - 'Secure', // 개발 환경에서 문제 발생 - 'SameSite=Strict', // 너무 엄격 - 'Path=/', - `Max-Age=${data.expires_in || 7200}`, -].join('; '); -``` - -```typescript -// ✅ 수정 코드 (Safari 호환) -const isProduction = process.env.NODE_ENV === 'production'; - -const accessTokenCookie = [ - `access_token=${data.access_token}`, - 'HttpOnly', // ✅ JavaScript cannot access (XSS 보호) - ...(isProduction ? ['Secure'] : []), // ✅ HTTPS only in production - 'SameSite=Lax', // ✅ CSRF protection (Lax for compatibility) - 'Path=/', - `Max-Age=${data.expires_in || 7200}`, -].join('; '); - -const refreshTokenCookie = [ - `refresh_token=${data.refresh_token}`, - 'HttpOnly', - ...(isProduction ? ['Secure'] : []), - 'SameSite=Lax', - 'Path=/', - 'Max-Age=604800', // 7 days -].join('; '); -``` - -**변경 사항**: -- ✅ `Secure` 속성을 환경에 따라 조건부 적용 -- ✅ `SameSite`를 `Strict`에서 `Lax`로 변경 -- ✅ `refresh_token`도 동일하게 적용 - ---- - -### 2. `src/app/api/auth/check/route.ts` - -**수정 위치**: 75-95 라인 (토큰 갱신 시) - -```typescript -// ✅ 수정 코드 -if (refreshResponse.ok) { - const data = await refreshResponse.json(); - - // Safari compatibility: Secure only in production - const isProduction = process.env.NODE_ENV === 'production'; - - const accessTokenCookie = [ - `access_token=${data.access_token}`, - 'HttpOnly', - ...(isProduction ? ['Secure'] : []), - 'SameSite=Lax', - 'Path=/', - `Max-Age=${data.expires_in || 7200}`, - ].join('; '); - - const refreshTokenCookie = [ - `refresh_token=${data.refresh_token}`, - 'HttpOnly', - ...(isProduction ? ['Secure'] : []), - 'SameSite=Lax', - 'Path=/', - 'Max-Age=604800', - ].join('; '); - - // ... 쿠키 설정 -} -``` - -**변경 사항**: -- ✅ 토큰 갱신 시에도 동일한 쿠키 설정 적용 -- ✅ login/route.ts와 일관성 유지 - ---- - -### 3. `src/app/api/auth/logout/route.ts` - -**수정 위치**: 52-71 라인 (쿠키 삭제) - -```typescript -// ❌ 기존 코드 (Safari에서 쿠키 삭제 실패) -const clearAccessToken = [ - 'access_token=', - 'HttpOnly', - 'Secure', // 설정 시와 속성 불일치 - 'SameSite=Strict', // 설정 시와 속성 불일치 - 'Path=/', - 'Max-Age=0', -].join('; '); -``` - -```typescript -// ✅ 수정 코드 (Safari에서 쿠키 삭제 성공) -// Safari compatibility: Must use same attributes as when setting cookies -const isProduction = process.env.NODE_ENV === 'production'; - -const clearAccessToken = [ - 'access_token=', - 'HttpOnly', - ...(isProduction ? ['Secure'] : []), // ✅ login과 동일 - 'SameSite=Lax', // ✅ login과 동일 - 'Path=/', - 'Max-Age=0', // Delete immediately -].join('; '); - -const clearRefreshToken = [ - 'refresh_token=', - 'HttpOnly', - ...(isProduction ? ['Secure'] : []), - 'SameSite=Lax', - 'Path=/', - 'Max-Age=0', -].join('; '); -``` - -**변경 사항**: -- ✅ 쿠키 삭제 시 설정 시와 **정확히 동일한 속성** 사용 -- ✅ Safari의 엄격한 쿠키 삭제 정책 대응 - ---- - -## 크로스 브라우저 개발 가이드라인 - -### 필수 테스트 브라우저 - -모든 브라우저 관련 기능 개발 시 **다음 브라우저에서 반드시 테스트**: - -| 브라우저 | 우선순위 | 주요 특징 | 테스트 환경 | -|---------|---------|----------|------------| -| **Chrome** | 🔴 High | 가장 관대한 정책 | macOS/Windows | -| **Safari** | 🔴 High | 가장 엄격한 정책 | macOS/iOS | -| **Firefox** | 🟡 Medium | 중간 수준 정책 | macOS/Windows | -| **Edge** | 🟢 Low | Chrome 기반 | Windows | - -**개발 우선순위**: Safari 기준으로 개발하면 다른 브라우저에서도 작동합니다. - ---- - -### 쿠키 관련 개발 원칙 - -#### 1. 환경별 조건부 설정 -```typescript -// ✅ 항상 환경 체크 -const isProduction = process.env.NODE_ENV === 'production'; -const isSecure = isProduction; // HTTPS 여부 - -// ✅ Secure 속성은 항상 조건부로 -...(isSecure ? ['Secure'] : []) -``` - -#### 2. HttpOnly는 항상 유지 -```typescript -// ✅ XSS 공격 방지를 위해 HttpOnly는 항상 포함 -'HttpOnly', // 절대 제거하지 말 것 -``` - -#### 3. SameSite는 Lax 권장 -```typescript -// ✅ CSRF 보호 + 유연성 -'SameSite=Lax', // 대부분의 웹 앱에 적합 - -// ⚠️ Strict는 너무 엄격 -'SameSite=Strict', // 특별한 이유가 있을 때만 사용 -``` - -#### 4. 쿠키 삭제 시 속성 일치 -```typescript -// ✅ 설정할 때와 삭제할 때 속성이 정확히 일치해야 함 -const setCookie = 'token=xxx; HttpOnly; SameSite=Lax'; -const deleteCookie = 'token=; HttpOnly; SameSite=Lax; Max-Age=0'; -``` - ---- - -### 로컬스토리지 vs 쿠키 선택 가이드 - -| 저장소 | 용도 | 보안 | Safari 호환성 | -|--------|------|------|---------------| -| **HttpOnly Cookie** | 인증 토큰 | ✅ 높음 (XSS 방지) | ✅ 조건부 설정 필요 | -| **LocalStorage** | 사용자 정보, 설정 | ⚠️ 낮음 (XSS 취약) | ✅ 호환성 좋음 | - -**원칙**: 민감한 데이터(토큰)는 HttpOnly 쿠키, 일반 데이터는 LocalStorage - ---- - -### Safari 개발 시 주의사항 - -#### 1. 쿠키 관련 -- ✅ HTTP 환경에서 `Secure` 속성 제거 -- ✅ 쿠키 설정과 삭제 시 속성 일치 -- ✅ `SameSite=Lax` 사용 권장 - -#### 2. 네트워크 요청 -```typescript -// ✅ Safari는 credentials 설정에 민감 -fetch('/api/auth/check', { - method: 'GET', - credentials: 'include', // Safari에서 쿠키 전송 필수 -}); -``` - -#### 3. 로컬스토리지 -```typescript -// ✅ Safari Private Mode에서 localStorage 제한 -try { - localStorage.setItem('key', 'value'); -} catch (error) { - // Safari Private Mode 대응 - console.warn('LocalStorage unavailable:', error); -} -``` - -#### 4. 날짜/시간 -```typescript -// ❌ Safari에서 파싱 실패 가능 -new Date('2024-01-01 12:00:00'); - -// ✅ ISO 8601 형식 사용 -new Date('2024-01-01T12:00:00Z'); -``` - ---- - -### 크로스 브라우저 테스트 도구 - -#### 개발 환경 테스트 -```bash -# Chrome -open -a "Google Chrome" http://localhost:3000 - -# Safari -open -a Safari http://localhost:3000 - -# Firefox -open -a Firefox http://localhost:3000 -``` - -#### 개발자 도구 활용 -```javascript -// Safari: Develop → Show Web Inspector → Storage -// Chrome: DevTools → Application → Cookies -// Firefox: DevTools → Storage → Cookies - -// 쿠키 확인 사항: -// - Name: access_token, refresh_token -// - HttpOnly: ✅ 체크 -// - Secure: 환경에 따라 조건부 -// - SameSite: Lax -``` - ---- - -## 테스트 체크리스트 - -### 로그인 기능 테스트 - -#### Chrome -- [ ] 로그인 성공 -- [ ] 대시보드 접근 가능 -- [ ] 쿠키 저장 확인 (DevTools → Application → Cookies) -- [ ] HttpOnly 속성 확인 -- [ ] 로그아웃 성공 -- [ ] 쿠키 삭제 확인 - -#### Safari -- [ ] 로그인 성공 -- [ ] 대시보드 접근 가능 -- [ ] 쿠키 저장 확인 (Web Inspector → Storage → Cookies) -- [ ] HttpOnly 속성 확인 -- [ ] Secure 속성 **없음** 확인 (개발 환경) -- [ ] 로그아웃 성공 -- [ ] 쿠키 삭제 확인 - -#### Firefox (선택) -- [ ] 로그인 성공 -- [ ] 대시보드 접근 가능 -- [ ] 쿠키 저장 확인 -- [ ] 로그아웃 성공 - ---- - -### 인증 상태 확인 테스트 - -#### 시나리오 1: 페이지 새로고침 -- [ ] Chrome: 로그인 상태 유지 -- [ ] Safari: 로그인 상태 유지 -- [ ] Firefox: 로그인 상태 유지 - -#### 시나리오 2: 브라우저 재시작 -- [ ] Chrome: 로그인 상태 유지 (Remember me) -- [ ] Safari: 로그인 상태 유지 -- [ ] Firefox: 로그인 상태 유지 - -#### 시나리오 3: 토큰 만료 -- [ ] Chrome: 자동 토큰 갱신 -- [ ] Safari: 자동 토큰 갱신 -- [ ] Firefox: 자동 토큰 갱신 - ---- - -### 프로덕션 배포 전 체크리스트 - -#### 환경 설정 -- [ ] `NODE_ENV=production` 설정 확인 -- [ ] HTTPS 인증서 설정 완료 -- [ ] 환경 변수 `.env.production` 확인 - -#### 쿠키 설정 확인 -- [ ] Production 환경에서 `Secure` 속성 포함 확인 -- [ ] `HttpOnly` 속성 유지 확인 -- [ ] `SameSite=Lax` 설정 확인 -- [ ] `Max-Age` 적절히 설정 (access: 2h, refresh: 7d) - -#### 브라우저 테스트 (HTTPS) -- [ ] Chrome: 로그인/로그아웃 정상 -- [ ] Safari: 로그인/로그아웃 정상 -- [ ] Firefox: 로그인/로그아웃 정상 -- [ ] Safari iOS: 모바일 테스트 - ---- - -## 문제 해결 가이드 - -### 쿠키가 저장되지 않는 경우 - -#### 1. Safari 개발 환경 -```typescript -// 체크 포인트: -// ✅ Secure 속성이 조건부로 설정되어 있는가? -...(isProduction ? ['Secure'] : []) - -// ✅ SameSite가 Lax인가? -'SameSite=Lax' - -// ✅ HttpOnly는 포함되어 있는가? -'HttpOnly' -``` - -#### 2. Safari Private Mode -Safari Private Mode에서는 일부 쿠키가 제한될 수 있습니다. -→ 일반 모드에서 테스트하세요. - -#### 3. 쿠키 도메인 설정 -```typescript -// ✅ localhost에서는 Domain 속성 생략 -// ❌ 'Domain=localhost' (불필요) -``` - ---- - -### 쿠키가 삭제되지 않는 경우 - -#### Safari 로그아웃 문제 -```typescript -// ❌ 설정 시와 삭제 시 속성 불일치 -// 설정: HttpOnly + SameSite=Lax -// 삭제: HttpOnly + Secure + SameSite=Strict - -// ✅ 설정 시와 삭제 시 속성 일치 -const isProduction = process.env.NODE_ENV === 'production'; -const cookie = [ - 'token=', - 'HttpOnly', - ...(isProduction ? ['Secure'] : []), // 일치 - 'SameSite=Lax', // 일치 - 'Max-Age=0', -].join('; '); -``` - ---- - -## 관련 문서 - -- [MDN - HTTP Cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) -- [MDN - SameSite Cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite) -- [Safari Cookie Policy](https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/) - ---- - -## 업데이트 히스토리 - -| 날짜 | 내용 | 작성자 | -|------|------|--------| -| 2024-XX-XX | Safari 쿠키 호환성 문서 작성 | Claude | - ---- - -**📌 기억하세요**: 브라우저 관련 기능 개발 시 **Safari를 기준으로 개발**하면 다른 브라우저에서도 작동합니다! \ No newline at end of file diff --git a/claudedocs/auth/[IMPL-2025-12-04] signup-page-blocking.md b/claudedocs/auth/[IMPL-2025-12-04] signup-page-blocking.md deleted file mode 100644 index 1ecc52ef..00000000 --- a/claudedocs/auth/[IMPL-2025-12-04] signup-page-blocking.md +++ /dev/null @@ -1,74 +0,0 @@ -# MVP 회원가입 페이지 차단 - -> **날짜**: 2025-12-04 -> **상태**: 완료 -> **목적**: MVP 버전에서 회원가입 접근 차단 (운영 페이지로 이동 예정) - ---- - -## 변경 사항 - -### 1. auth-config.ts -**파일**: `src/lib/api/auth/auth-config.ts` - -```typescript -// Before -guestOnlyRoutes: ['/login', '/signup', '/forgot-password'] - -// After -guestOnlyRoutes: ['/login', '/forgot-password'] -``` - -- `/signup`을 guestOnlyRoutes에서 제거 -- 주석에 변경 이유 기록 - -### 2. LoginPage.tsx -**파일**: `src/components/auth/LoginPage.tsx` - -| 제거된 요소 | 위치 | -|------------|------| -| 헤더 회원가입 버튼 | Line 188 (이전) | -| "계정 만들기" 버튼 | Line 304-310 (이전) | -| 하단 회원가입 링크 | Line 314-325 (이전) | - -- 총 3개의 회원가입 관련 UI 요소 제거 -- 주석으로 제거 이유 기록 - -### 3. middleware.ts -**파일**: `src/middleware.ts` - -```typescript -// 4.5️⃣ MVP: /signup 접근 차단 → /login 리다이렉트 (2025-12-04) -if (pathnameWithoutLocale === '/signup' || pathnameWithoutLocale.startsWith('/signup/')) { - console.log(`[Signup Blocked] Redirecting to /login from ${pathname}`); - return NextResponse.redirect(new URL('/login', request.url)); -} -``` - -- URL 직접 접근 시 `/login`으로 리다이렉트 -- 로그 출력으로 접근 시도 추적 가능 - ---- - -## 유지된 파일 (삭제 안함) - -| 파일 | 이유 | -|------|------| -| `src/app/[locale]/signup/page.tsx` | 운영 페이지에서 재사용 예정 | -| `src/app/api/auth/signup/route.ts` | API 로직 재사용 예정 | - ---- - -## 테스트 체크리스트 - -- [ ] 로그인 페이지에서 회원가입 링크 없음 -- [ ] `/signup` URL 직접 접근 시 `/login`으로 리다이렉트 -- [ ] `/ko/signup` (로케일 포함) 접근 시 `/login`으로 리다이렉트 -- [ ] 기존 로그인 기능 정상 동작 - ---- - -## 향후 작업 - -- 회원가입 기능을 운영 페이지(관리자용)로 이동 -- 운영 페이지에서 사용자 등록 기능 구현 diff --git a/claudedocs/auth/[IMPL-2025-12-30] token-refresh-caching.md b/claudedocs/auth/[IMPL-2025-12-30] token-refresh-caching.md deleted file mode 100644 index f4bbaa69..00000000 --- a/claudedocs/auth/[IMPL-2025-12-30] token-refresh-caching.md +++ /dev/null @@ -1,512 +0,0 @@ -# Token Refresh Caching 구현 문서 - -> 작성일: 2025-12-30 -> 상태: 완료 - -## 1. 문제 상황 - -### 1.1 증상 -페이지 로드 시 여러 API 호출이 동시에 발생할 때, 일부 요청이 401 에러와 함께 실패하고 로그인 페이지로 리다이렉트되는 현상. - -### 1.2 원인 분석 -`useEffect`에서 여러 API를 동시에 호출할 때 **refresh_token 충돌** 발생: - -``` -시간 → -──────────────────────────────────────────────────────────────────── -[요청 A] access_token 만료 → 401 → refresh_token 사용 → ✅ 새 토큰 발급 (기존 refresh_token 폐기) -[요청 B] access_token 만료 → 401 → refresh_token 사용 → ❌ 실패 (이미 폐기된 토큰) -[요청 C] access_token 만료 → 401 → refresh_token 사용 → ❌ 실패 (이미 폐기된 토큰) -──────────────────────────────────────────────────────────────────── -``` - -**핵심 문제**: refresh_token은 일회용(One-Time Use)이므로, 첫 번째 요청이 사용하면 즉시 폐기됨. - -### 1.3 영향 범위 -- **Proxy 경로** (`/api/proxy/*`): 클라이언트 → Next.js → PHP 백엔드 -- **Server Actions** (`serverFetch`): Server Component에서 직접 API 호출 - ---- - -## 2. 해결 방법: Request Coalescing (요청 병합) 패턴 - -### 2.1 패턴 설명 -동시에 발생하는 동일한 요청을 하나로 병합하여 처리하는 표준 패턴. - -``` -시간 → -──────────────────────────────────────────────────────────────────── -[요청 A] 401 → refresh 시작 (Promise 생성) → ✅ 새 토큰 → 캐시 저장 -[요청 B] 401 → 캐시된 Promise 대기 ────────→ ✅ 같은 새 토큰 사용 -[요청 C] 401 → 캐시된 Promise 대기 ────────→ ✅ 같은 새 토큰 사용 -──────────────────────────────────────────────────────────────────── -``` - -### 2.2 구현 특징 -- **5초 캐싱**: refresh 결과를 5초간 캐시 -- **Promise 공유**: 진행 중인 refresh Promise를 여러 요청이 공유 -- **모듈 레벨 캐시**: Proxy와 serverFetch가 동일한 캐시 공유 - ---- - -## 3. 구현 코드 - -### 3.1 파일 구조 -``` -src/lib/api/ -├── refresh-token.ts # 🆕 공통 토큰 갱신 모듈 (캐싱 로직 포함) -├── fetch-wrapper.ts # serverFetch (import from refresh-token) -└── errors.ts # 에러 타입 정의 - -src/app/api/proxy/ -└── [...path]/route.ts # Proxy (import from refresh-token) - -src/app/api/auth/ -├── check/route.ts # 🔧 인증 확인 API (2026-01-08 통합) -└── refresh/route.ts # 🔧 토큰 갱신 API (2026-01-08 통합) -``` - -### 3.2 공통 모듈: `refresh-token.ts` - -```typescript -/** - * 🔄 Refresh Token 공통 모듈 - * - * 문제: useEffect에서 여러 API 동시 호출 시 refresh_token 충돌 - * 해결: 5초간 refresh 결과 캐싱 + Promise 공유 - */ - -export type RefreshResult = { - success: boolean; - accessToken?: string; - refreshToken?: string; - expiresIn?: number; -}; - -// 캐시 상태 (모듈 레벨에서 공유) -let refreshCache: { - promise: Promise | null; - timestamp: number; - result: RefreshResult | null; -} = { - promise: null, - timestamp: 0, - result: null, -}; - -const REFRESH_CACHE_TTL = 5000; // 5초 - -/** - * 실제 토큰 갱신 수행 (내부 함수) - */ -async function doRefreshToken(refreshToken: string): Promise { - try { - const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/refresh`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'X-API-KEY': process.env.API_KEY || '', - }, - body: JSON.stringify({ refresh_token: refreshToken }), - }); - - if (!response.ok) { - return { success: false }; - } - - const data = await response.json(); - return { - success: true, - accessToken: data.access_token, - refreshToken: data.refresh_token, - expiresIn: data.expires_in, - }; - } catch (error) { - console.error('🔴 [RefreshToken] Token refresh error:', error); - return { success: false }; - } -} - -/** - * 토큰 갱신 함수 (5초 캐싱 적용) - * - * 동시 요청 시: - * 1. 캐시된 결과가 있으면 즉시 반환 - * 2. 진행 중인 refresh가 있으면 그 Promise를 기다림 - * 3. 둘 다 없으면 새 refresh 시작 - */ -export async function refreshAccessToken( - refreshToken: string, - caller: string = 'unknown' -): Promise { - const now = Date.now(); - - // 1. 캐시된 결과가 유효하면 즉시 반환 - if (refreshCache.result?.success && now - refreshCache.timestamp < REFRESH_CACHE_TTL) { - console.log(`🔵 [${caller}] Using cached refresh result`); - return refreshCache.result; - } - - // 2. 진행 중인 refresh가 있으면 그 결과를 기다림 - if (refreshCache.promise && now - refreshCache.timestamp < REFRESH_CACHE_TTL) { - console.log(`🔵 [${caller}] Waiting for ongoing refresh...`); - return refreshCache.promise; - } - - // 3. 새 refresh 시작 - console.log(`🔄 [${caller}] Starting new refresh request...`); - refreshCache.timestamp = now; - refreshCache.result = null; - - refreshCache.promise = doRefreshToken(refreshToken).then(result => { - refreshCache.result = result; - return result; - }); - - return refreshCache.promise; -} -``` - -### 3.3 사용 예시 - -**Proxy에서 사용:** -```typescript -// src/app/api/proxy/[...path]/route.ts -import { refreshAccessToken } from '@/lib/api/refresh-token'; - -// 401 응답 시 -const refreshResult = await refreshAccessToken(refreshToken, 'PROXY'); -``` - -**serverFetch에서 사용:** -```typescript -// src/lib/api/fetch-wrapper.ts -import { refreshAccessToken } from './refresh-token'; - -// 401 응답 시 -const refreshResult = await refreshAccessToken(refreshToken, 'serverFetch'); -``` - ---- - -## 4. 시행착오 기록 - -### 4.1 초기 문제: 중복 구현 -처음에는 Proxy와 serverFetch에서 각각 캐싱 로직을 별도로 구현했음. - -**문제점:** -- 코드 중복 (~80줄씩) -- 두 캐시가 분리되어 있어 비효율적 -- 유지보수 어려움 - -**해결:** 공통 모듈 `refresh-token.ts`로 통합 - -### 4.2 빌드 오류: .next 폴더 손상 -``` -Error: Cannot find module './4586.js' -``` - -**원인:** 이전 빌드 아티팩트와 새 코드 간 충돌 - -**해결:** -```bash -rm -rf .next -npm run build -``` - -### 4.3 런타임 오류: app-paths-manifest.json 누락 -``` -500 Error: .next/server/app-paths-manifest.json not found -``` - -**원인:** 빌드 중 .next 폴더 손상 - -**해결:** -```bash -rm -rf .next -npm run dev -``` - -### 4.4 Safari 호환성 문제 (이전 세션에서 해결) -Safari에서 `SameSite=Strict` + `Secure` 조합이 localhost에서 쿠키 저장 실패. - -**해결:** -- `SameSite=Strict` → `SameSite=Lax` -- `Secure`는 프로덕션에서만 적용 - ---- - -## 5. 동작 흐름도 - -### 5.1 정상 흐름 (토큰 유효) -``` -클라이언트 → Proxy/serverFetch → API 요청 → 200 OK → 응답 반환 -``` - -### 5.2 토큰 갱신 흐름 (단일 요청) -``` -클라이언트 → Proxy/serverFetch → API 요청 → 401 - ↓ - refreshAccessToken() - ↓ - 새 토큰 발급 + 쿠키 저장 - ↓ - 원래 요청 재시도 → 200 OK -``` - -### 5.3 토큰 갱신 흐름 (동시 요청 - 캐싱 적용) -``` -[요청 A] → 401 → refreshAccessToken() → 새 refresh 시작 ──┐ -[요청 B] → 401 → refreshAccessToken() → Promise 대기 ────┼→ 같은 새 토큰 공유 -[요청 C] → 401 → refreshAccessToken() → Promise 대기 ────┘ - ↓ - 각자 원래 요청 재시도 -``` - ---- - -## 6. 설정 값 - -| 항목 | 값 | 설명 | -|------|-----|------| -| REFRESH_CACHE_TTL | 5초 | refresh 결과 캐시 유지 시간 | -| access_token Max-Age | 7200초 (2시간) | API에서 전달받은 값 사용 | -| refresh_token Max-Age | 604800초 (7일) | 장기 보관 | - ---- - -## 7. 로그 메시지 - -### 7.1 캐시 히트 (이미 갱신된 토큰 재사용) -``` -🔵 [PROXY] Using cached refresh result (age: 1234ms) -🔵 [serverFetch] Using cached refresh result (age: 1234ms) -``` - -### 7.2 대기 중 (다른 요청이 갱신 중) -``` -🔵 [PROXY] Waiting for ongoing refresh... -🔵 [serverFetch] Waiting for ongoing refresh... -``` - -### 7.3 새 갱신 시작 -``` -🔄 [PROXY] Starting new refresh request... -🔄 [serverFetch] Starting new refresh request... -✅ [RefreshToken] Token refreshed successfully -``` - -### 7.4 갱신 실패 -``` -🔴 [RefreshToken] Token refresh failed: { status: 401, ... } -``` - ---- - -## 8. 관련 파일 - -| 파일 | 역할 | 통합일 | -|------|------|--------| -| `src/lib/api/refresh-token.ts` | 공통 토큰 갱신 모듈 (캐싱 로직) | 2025-12-30 | -| `src/lib/api/fetch-wrapper.ts` | Server Actions용 fetch wrapper | 2025-12-30 | -| `src/lib/utils/redirect-error.ts` | Next.js redirect 에러 감지 유틸리티 | 2026-01-08 | -| `src/app/api/proxy/[...path]/route.ts` | 클라이언트 API 프록시 | 2025-12-30 | -| `src/app/api/auth/login/route.ts` | 로그인 및 초기 토큰 설정 | - | -| `src/app/api/auth/check/route.ts` | 인증 상태 확인 API | 2026-01-08 | -| `src/app/api/auth/refresh/route.ts` | 토큰 갱신 프록시 API | 2026-01-08 | - ---- - -## 9. 이 패턴이 "편법"이 아닌 이유 - -### 9.1 업계 표준 패턴 -- **Request Coalescing / Request Deduplication**: 공식 명칭 -- React Query, SWR, Apollo Client 등에서 동일 패턴 사용 -- CDN (Cloudflare, Fastly)에서도 동일 원리 적용 - -### 9.2 설계 원칙 준수 -- **DRY**: 중복 요청 제거 -- **효율성**: 서버 부하 감소 -- **일관성**: 모든 요청이 같은 새 토큰 사용 - -### 9.3 향후 위험성 없음 -- 5초 TTL은 충분히 짧아 토큰 갱신 지연 문제 없음 -- 실패 시 다음 요청에서 새로 갱신 시도 -- 캐시는 메모리 기반이라 서버 재시작 시 자동 초기화 - ---- - -## 10. 업데이트 이력 - -### 10.0 [2026-01-15] 미들웨어 사전 갱신 기능 추가 - -**관련 문서:** `[IMPL-2026-01-15] middleware-pre-refresh.md` - -Request Coalescing 패턴만으로는 auth/check + serverFetch 동시 호출 시 Race Condition이 완전히 해결되지 않아, **미들웨어에서 페이지 렌더링 전 토큰을 미리 갱신**하는 기능 추가. - -두 기능은 상호 보완적: -- **미들웨어 사전 갱신**: 페이지 로드 전 토큰 준비 (1차 방어) -- **Request Coalescing**: API 호출 시 401 발생 시 중복 갱신 방지 (2차 방어) - -### 10.1 [2026-01-08] 누락된 API 라우트 통합 - -**문제 발견:** -`/api/auth/check`와 `/api/auth/refresh` 라우트가 공유 캐시를 사용하지 않고 자체 fetch 로직을 사용하고 있었음. - -**증상:** -``` -🔍 Refresh API response status: 401 -❌ Refresh API failed: 401 {"error":"리프레시 토큰이 유효하지 않거나 만료되었습니다","error_code":"TOKEN_EXPIRED"} -⚠️ Returning 401 due to refresh failure -GET /api/auth/check 401 -``` - -**원인:** -1. `serverFetch`에서 refresh 성공 → Token Rotation으로 이전 refresh_token 폐기 -2. `/api/auth/check`가 동시에 호출됨 -3. 자체 fetch 로직으로 이미 폐기된 토큰 사용 시도 → 실패 → 로그인 페이지 이동 - -**해결:** -두 파일 모두 `refreshAccessToken()` 공유 함수를 사용하도록 수정: - -```typescript -// src/app/api/auth/check/route.ts -import { refreshAccessToken } from '@/lib/api/refresh-token'; - -const refreshResult = await refreshAccessToken(refreshToken, 'auth/check'); -``` - -```typescript -// src/app/api/auth/refresh/route.ts -import { refreshAccessToken } from '@/lib/api/refresh-token'; - -const refreshResult = await refreshAccessToken(refreshToken, 'api/auth/refresh'); -``` - -**결과:** -모든 refresh 경로가 동일한 5초 캐시를 공유하여 Token Rotation 충돌 방지. - -### 10.2 [2026-01-08] 53개 Server Actions 파일 수정 - -**문제:** -`redirect('/login')` 호출 시 발생하는 `NEXT_REDIRECT` 에러가 catch 블록에서 잡혀 `{ success: false }` 반환 → 무한 루프 - -**해결:** -모든 actions.ts 파일에 `isRedirectError` 처리 추가: - -```typescript -import { isRedirectError } from 'next/dist/client/components/redirect'; - -} catch (error) { - if (isRedirectError(error)) throw error; - // ... 기존 에러 처리 -} -``` - -### 10.3 [2026-01-08] refresh 실패 결과 캐시 버그 수정 - -**문제:** -refresh 실패 결과도 5초간 캐시되어, 후속 요청들이 모두 실패 결과를 받음. - -**해결:** -`refresh-token.ts`에서 성공한 결과만 캐시하도록 수정: - -```typescript -// 1. 캐시된 성공 결과가 유효하면 즉시 반환 -if (refreshCache.result && refreshCache.result.success && now - refreshCache.timestamp < REFRESH_CACHE_TTL) { - return refreshCache.result; -} - -// 2-1. 이전 refresh가 실패했으면 캐시 초기화 -if (refreshCache.result && !refreshCache.result.success) { - refreshCache.promise = null; - refreshCache.result = null; -} -``` - -### 10.4 [2026-01-08] isRedirectError 자체 유틸리티 함수로 변경 - -**문제:** -Next.js 내부 경로(`next/dist/client/components/redirect`)가 버전 15에서 `redirect-error`로 변경됨. -내부 경로 의존 시 Next.js 업데이트마다 수정 필요. - -**해결:** -자체 유틸리티 함수 생성하여 Next.js 내부 경로 의존성 제거: - -```typescript -// src/lib/utils/redirect-error.ts -export function isNextRedirectError(error: unknown): boolean { - return ( - typeof error === 'object' && - error !== null && - 'digest' in error && - typeof (error as { digest: string }).digest === 'string' && - (error as { digest: string }).digest.startsWith('NEXT_REDIRECT') - ); -} -``` - -**장점:** -- Next.js 버전 업데이트에 영향 안 받음 -- 내부 경로 의존성 제거 -- 한 곳에서 관리 가능 - ---- - -## 11. 신규 Server Actions 개발 가이드 - -### 11.1 필수 패턴 - -새로운 `actions.ts` 파일 생성 시 반드시 아래 패턴을 따라야 합니다: - -```typescript -'use server'; - -import { isNextRedirectError } from '@/lib/utils/redirect-error'; -import { serverFetch } from '@/lib/api/fetch-wrapper'; - -export async function someAction(params: SomeParams): Promise { - try { - const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/some-endpoint`; - - const { response, error } = await serverFetch(url, { - method: 'GET', // 또는 POST, PUT, DELETE - }); - - if (error || !response) { - return { success: false, error: error?.message || '요청 실패' }; - } - - const data = await response.json(); - return { success: true, data }; - - } catch (error) { - // ⚠️ 필수: redirect 에러는 다시 throw해야 함 - if (isNextRedirectError(error)) throw error; - - console.error('[SomeAction] error:', error); - return { success: false, error: '서버 오류가 발생했습니다.' }; - } -} -``` - -### 11.2 왜 isNextRedirectError 처리가 필수인가? - -``` -serverFetch에서 401 응답 시: -1. refresh_token으로 토큰 갱신 시도 -2. 갱신 실패 시 redirect('/login') 호출 -3. redirect()는 NEXT_REDIRECT 에러를 throw -4. 이 에러가 catch에서 잡히면 → { success: false } 반환 → 무한 루프 -5. 이 에러를 다시 throw하면 → Next.js가 정상 리다이렉트 처리 -``` - -### 11.3 체크리스트 - -새 actions.ts 파일 생성 시: - -- [ ] `import { isNextRedirectError } from '@/lib/utils/redirect-error';` 추가 -- [ ] `import { serverFetch } from '@/lib/api/fetch-wrapper';` 사용 -- [ ] 모든 catch 블록에 `if (isNextRedirectError(error)) throw error;` 추가 -- [ ] 파일 내 모든 export 함수에 동일 패턴 적용 \ No newline at end of file diff --git a/claudedocs/auth/[IMPL-2026-01-15] middleware-pre-refresh.md b/claudedocs/auth/[IMPL-2026-01-15] middleware-pre-refresh.md deleted file mode 100644 index 8d56ae6a..00000000 --- a/claudedocs/auth/[IMPL-2026-01-15] middleware-pre-refresh.md +++ /dev/null @@ -1,424 +0,0 @@ -# 미들웨어 토큰 사전 갱신 (Pre-Refresh) 구현 문서 - -> 작성일: 2026-01-15 -> 상태: 완료 - -## 1. 문제 상황 - -### 1.1 기존 Request Coalescing 패턴의 한계 - -`refresh-token.ts`의 5초 캐싱 패턴으로 동시 API 호출 시 중복 갱신은 방지했지만, **auth/check + serverFetch 동시 호출** 문제가 완전히 해결되지 않았음. - -### 1.2 Race Condition 시나리오 - -``` -페이지 로드 시 (access_token 만료, refresh_token만 있는 상태) - -시간 → -──────────────────────────────────────────────────────────────────── -[페이지 렌더링 시작] - ↓ -[useEffect] → auth/check 호출 ─────┐ -[Server Component] → serverFetch ──┼─→ 둘 다 refresh_token 필요 - ↓ - 첫 번째가 갱신하면 두 번째는? - (캐시 공유해도 타이밍 문제 발생 가능) -──────────────────────────────────────────────────────────────────── -``` - -### 1.3 증상 -- 페이지 로드 시 간헐적으로 401 에러 -- 토큰 만료 직후 첫 페이지 접속 시 로그인 페이지로 튕김 -- 콘솔에 `Token refresh failed` 로그 - ---- - -## 2. 해결 방법: 미들웨어 사전 갱신 (Pre-Refresh) - -### 2.1 핵심 아이디어 - -**페이지 렌더링 전에 미들웨어에서 토큰을 미리 갱신**하여, 페이지 로드 시 모든 API 호출이 이미 갱신된 access_token을 사용하도록 함. - -``` -시간 → -──────────────────────────────────────────────────────────────────── -[브라우저 요청] → [미들웨어 7.5단계] - ↓ - access_token 없고 refresh_token만 있음? - ↓ YES - 백엔드 /api/v1/refresh 호출 (1회) - ↓ - Set-Cookie: access_token, refresh_token - ↓ -[페이지 렌더링] → auth/check, serverFetch 모두 새 access_token 사용 - ↓ - ✅ Race Condition 없음 -──────────────────────────────────────────────────────────────────── -``` - -### 2.2 기존 패턴과의 관계 - -| 기능 | 목적 | 실행 시점 | 파일 | -|------|------|----------|------| -| **Request Coalescing** | 동시 API 호출 시 refresh 중복 방지 | API 호출 시 401 응답 후 | `refresh-token.ts` | -| **미들웨어 사전 갱신** | 페이지 로드 전 토큰 준비 | 미들웨어 실행 시 | `middleware.ts` | - -두 기능은 **상호 보완적**: -- 미들웨어가 사전 갱신하면 대부분의 경우 API 호출 시 401이 발생하지 않음 -- 만약 미들웨어 이후 토큰이 만료되면 Request Coalescing이 백업으로 동작 - ---- - -## 3. 구현 코드 - -### 3.1 파일 위치 -``` -src/middleware.ts -``` - -### 3.2 추가된 코드 구조 - -```typescript -// 1. 캐시 객체 (모듈 레벨) -let middlewareRefreshCache: { - promise: Promise | null; - timestamp: number; - result: RefreshResult | null; -} = { promise: null, timestamp: 0, result: null }; - -const MIDDLEWARE_REFRESH_CACHE_TTL = 5000; // 5초 - -// 2. checkAuthentication() 확장 -function checkAuthentication(request: NextRequest): { - isAuthenticated: boolean; - authMode: 'sanctum' | 'bearer' | 'api-key' | null; - needsRefresh: boolean; // 🆕 access_token 없고 refresh_token만 있음 - refreshToken: string | null; // 🆕 갱신에 사용할 토큰 -} - -// 3. refreshTokenInMiddleware() 함수 -async function refreshTokenInMiddleware(refreshToken: string): Promise - -// 4. middleware() 함수 내 7.5단계 -export async function middleware(request: NextRequest) { - // ... 기존 1~7단계 ... - - // 7.5단계: 토큰 사전 갱신 - if (needsRefresh && refreshToken) { - const refreshResult = await refreshTokenInMiddleware(refreshToken); - // Set-Cookie로 새 토큰 설정 - } - - // ... 기존 8~10단계 ... -} -``` - -### 3.3 checkAuthentication() 반환값 변경 - -**변경 전:** -```typescript -return { - isAuthenticated: boolean; - authMode: 'sanctum' | 'bearer' | 'api-key' | null; -} -``` - -**변경 후:** -```typescript -return { - isAuthenticated: boolean; - authMode: 'sanctum' | 'bearer' | 'api-key' | null; - needsRefresh: boolean; // access_token 없고 refresh_token만 있으면 true - refreshToken: string | null; // 갱신에 사용할 refresh_token 값 -} -``` - -### 3.4 7.5단계 사전 갱신 로직 - -```typescript -// 7️⃣.5️⃣ 🔄 토큰 사전 갱신 (Race Condition 방지) -if (needsRefresh && refreshToken) { - console.log(`🔄 [Middleware] Pre-refreshing token before page render: ${pathname}`); - - const refreshResult = await refreshTokenInMiddleware(refreshToken); - - if (refreshResult.success && refreshResult.accessToken) { - const isProduction = process.env.NODE_ENV === 'production'; - const intlResponse = intlMiddleware(request); - - // Set-Cookie 헤더로 새 토큰 전송 - const accessTokenCookie = [ - `access_token=${refreshResult.accessToken}`, - 'HttpOnly', - ...(isProduction ? ['Secure'] : []), - 'SameSite=Lax', - 'Path=/', - `Max-Age=${refreshResult.expiresIn || 7200}`, - ].join('; '); - - const refreshTokenCookie = [ - `refresh_token=${refreshResult.refreshToken}`, - 'HttpOnly', - ...(isProduction ? ['Secure'] : []), - 'SameSite=Lax', - 'Path=/', - 'Max-Age=604800', // 7 days (하드코딩) - ].join('; '); - - intlResponse.headers.append('Set-Cookie', accessTokenCookie); - intlResponse.headers.append('Set-Cookie', refreshTokenCookie); - // ... 기타 쿠키 ... - - return intlResponse; - } else { - // 갱신 실패 시 로그인 페이지로 - return NextResponse.redirect(new URL('/login', request.url)); - } -} -``` - ---- - -## 4. 동작 흐름도 - -### 4.1 정상 흐름 (access_token 유효) - -``` -브라우저 → 미들웨어 → checkAuthentication() - ↓ - needsRefresh = false (access_token 있음) - ↓ - 7.5단계 스킵 → 페이지 렌더링 -``` - -### 4.2 사전 갱신 흐름 (access_token 만료, refresh_token 유효) - -``` -브라우저 → 미들웨어 → checkAuthentication() - ↓ - needsRefresh = true (access_token 없음, refresh_token 있음) - ↓ - 7.5단계: refreshTokenInMiddleware() 호출 - ↓ - 백엔드 /api/v1/refresh → 새 토큰 발급 - ↓ - Set-Cookie: access_token, refresh_token - ↓ - 페이지 렌더링 (새 토큰으로) -``` - -### 4.3 갱신 실패 흐름 (refresh_token도 만료) - -``` -브라우저 → 미들웨어 → checkAuthentication() - ↓ - needsRefresh = true - ↓ - 7.5단계: refreshTokenInMiddleware() 호출 - ↓ - 백엔드 → 401 (refresh_token 만료) - ↓ - redirect('/login') -``` - ---- - -## 5. 설정 값 - -| 항목 | 값 | 설명 | -|------|-----|------| -| MIDDLEWARE_REFRESH_CACHE_TTL | 5초 | 미들웨어 캐시 유지 시간 | -| access_token Max-Age | 7200초 (2시간) | 백엔드 expires_in 값 또는 기본값 | -| refresh_token Max-Age | 604800초 (7일) | 하드코딩 (백엔드에서 미제공) | - ---- - -## 6. 로그 메시지 - -### 6.1 사전 갱신 시작 -``` -🔄 [Middleware] Pre-refreshing token before page render: /dashboard -``` - -### 6.2 캐시 히트 -``` -🔵 [Middleware] Using cached refresh result (age: 1234ms) -``` - -### 6.3 진행 중인 갱신 대기 -``` -🔵 [Middleware] Waiting for ongoing refresh... -``` - -### 6.4 갱신 성공 -``` -✅ [Middleware] Pre-refresh successful -✅ [Middleware] Pre-refresh complete, new tokens set in cookies -``` - -### 6.5 갱신 실패 -``` -🔴 [Middleware] Pre-refresh failed: 401 -🔴 [Middleware] Pre-refresh failed, redirecting to login -``` - ---- - -## 7. Edge Runtime 고려사항 - -### 7.1 모듈 레벨 캐시의 한계 - -Edge Runtime에서는 모듈 레벨 변수가 **요청 간 공유되지 않을 수 있음**. -따라서 `middlewareRefreshCache`는 **같은 요청 내 중복 갱신 방지**에만 효과적. - -### 7.2 5초 캐시의 역할 - -- 같은 요청 처리 중 여러 번 호출되는 경우 방지 -- Edge 인스턴스 간 캐시 공유는 불가능 -- 충분히 짧아서 토큰 갱신 지연 문제 없음 - ---- - -## 8. 관련 파일 - -| 파일 | 역할 | -|------|------| -| `src/middleware.ts` | 미들웨어 사전 갱신 로직 | -| `src/lib/api/refresh-token.ts` | Request Coalescing 패턴 (백업) | -| `src/app/api/auth/check/route.ts` | 인증 확인 API | -| `src/app/api/auth/refresh/route.ts` | 토큰 갱신 프록시 | - ---- - -## 9. 관련 문서 - -- `[IMPL-2025-12-30] token-refresh-caching.md` - Request Coalescing 패턴 문서 -- `[IMPL-2025-11-07] middleware-issue-resolution.md` - 미들웨어 기본 구조 - ---- - -## 10. 업데이트 이력 - -### 10.1 [2026-01-15] 초기 구현 - -**배경:** -- auth/check와 serverFetch 동시 호출 시 Race Condition 발생 -- 기존 Request Coalescing만으로는 완전히 해결되지 않음 - -**구현 내용:** -1. `middlewareRefreshCache` 캐시 객체 추가 -2. `refreshTokenInMiddleware()` 함수 구현 -3. `checkAuthentication()`에 `needsRefresh`, `refreshToken` 반환 추가 -4. 7.5단계 사전 갱신 로직 추가 - -**결과:** -- 페이지 렌더링 전 토큰 갱신 완료 -- 이후 API 호출들은 새 access_token 사용 -- Race Condition 완전 해결 - -### 10.2 [2026-01-15] 파편화된 API route 통합 - -**배경:** -- `/api/menus` 등 별도 route에서 refresh 로직 없이 바로 401 반환 -- 1~2시간 방치 후 로그인 페이지로 튕기는 문제 발생 - -**수행 내용:** -1. 클라이언트 호출 경로 변경: - - `/api/menus` → `/api/proxy/menus` (menuRefresh.ts) - - `/api/files/${id}/download` → `/api/proxy/files/${id}/download` (DocumentCreate, DraftBox) -2. 파편화된 API route 삭제: - - `src/app/api/menus/` - 삭제 - - `src/app/api/files/` - 삭제 - - `src/app/api/tenants/` - 삭제 (미사용) - - `src/lib/api/php-proxy.ts` - 삭제 (중복 유틸) - -**결과:** -- 모든 API 호출이 `/api/proxy`를 통해 refresh 로직 적용 -- 토큰 만료 시 자동 갱신 후 재시도 - -### 10.3 [2026-01-15] 인증 흐름 전면 재설계 - -**배경:** -- pre-refresh 실패 시 무한 리다이렉트 루프 발생 -- 5️⃣ 게스트 전용 라우트에서 `needsRefresh` 상태를 고려하지 않음 -- `refresh_token`만 있는 상태를 "로그인됨"으로 섣부르게 판정 - -**문제의 무한 루프 시나리오:** -``` -/login 접근 (refresh_token만 있음) - ↓ -5️⃣ isAuthenticated=true (refresh_token 있으니까) → /dashboard로 리다이렉트 - ↓ -7.5️⃣ pre-refresh 시도 → 401 실패 → /login으로 리다이렉트 - ↓ -무한 반복! -``` - -**핵심 원인:** -- `refresh_token`만 있는 상태 = "로그인됨"이 아니라 "로그인 가능성 있음" -- 실제로 refresh 성공해야 "진짜 로그인" -- 5️⃣에서 이걸 확인 안 하고 바로 /dashboard로 보냄 - -**수정 내용 (5️⃣ 게스트 전용 라우트):** -```typescript -if (isGuestOnlyRoute(pathnameWithoutLocale)) { - // needsRefresh인 경우: 먼저 refresh 시도해서 "진짜 로그인"인지 확인 - if (needsRefresh && refreshToken) { - const refreshResult = await refreshTokenInMiddleware(refreshToken); - - if (refreshResult.success) { - // ✅ 진짜 로그인됨 → /dashboard로 (쿠키 설정) - return redirectToDashboard(with new cookies); - } else { - // ❌ 로그인 안 됨 → 쿠키 삭제 후 로그인 페이지 표시 (리다이렉트 없이!) - return showLoginPage(with cleared cookies); - } - } - - // access_token 있음 = 확실히 로그인됨 → /dashboard로 - if (isAuthenticated) { - return redirectToDashboard(); - } - - // 쿠키 없음 = 비로그인 → 로그인 페이지 표시 - return showLoginPage(); -} -``` - -**수정 후 흐름:** -``` -/login 접근 (refresh_token만 있음) - ↓ -5️⃣ needsRefresh=true → refresh 먼저 시도 - ↓ -├─ 성공 → "진짜 로그인" → /dashboard (왕복 1회) -└─ 실패 → "로그인 안 됨" → 쿠키 삭제 → 로그인 페이지 (왕복 0회!) -``` - -**결과:** -- 무한 리다이렉트 루프 완전 해결 -- 불필요한 /dashboard → /login 왕복 제거 -- refresh 실패 시 바로 로그인 페이지 표시 - ---- - -## 11. TODO (Phase 2) - -### 쿠키 설정 공통 모듈화 - -현재 쿠키 설정 코드가 6곳에 중복: -- `/api/proxy/[...path]/route.ts` -- `/api/auth/login/route.ts` -- `/api/auth/check/route.ts` -- `/api/auth/refresh/route.ts` -- `middleware.ts` -- `fetch-wrapper.ts` - -**계획:** -```typescript -// src/lib/api/cookie-utils.ts (신규) -export function createTokenCookies(tokens: TokenSet): string[] -export function clearTokenCookies(): string[] -``` - -**효과:** 유지보수성 향상 (쿠키 설정 변경 시 1곳만 수정) diff --git a/claudedocs/auth/[PLAN] httponly-cookie-implementation.md b/claudedocs/auth/[PLAN] httponly-cookie-implementation.md deleted file mode 100644 index a1a5a9a3..00000000 --- a/claudedocs/auth/[PLAN] httponly-cookie-implementation.md +++ /dev/null @@ -1,391 +0,0 @@ -# HttpOnly Cookie Implementation - Security Upgrade - -## 보안 개선 개요 - -### 이전 방식 (보안 위험: 🔴 7.6/10) -```typescript -// ❌ XSS 취약점: JavaScript로 토큰 접근 가능 -localStorage.setItem('user_token', token); -document.cookie = `user_token=${token}; SameSite=Lax`; // Non-HttpOnly -``` - -**취약점:** -- localStorage는 모든 JavaScript에서 접근 가능 -- XSS 공격 시 토큰 탈취 가능 -- 쿠키가 HttpOnly가 아니어서 `document.cookie`로 읽기 가능 - -### 새로운 방식 (보안 위험: 🟢 2.8/10) -```typescript -// ✅ XSS 방어: JavaScript로 토큰 접근 불가능 -Set-Cookie: user_token=...; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=604800 -``` - -**보안 개선:** -- HttpOnly 쿠키: JavaScript에서 완전히 차단 -- Secure: HTTPS 연결에서만 전송 -- SameSite=Strict: CSRF 공격 방어 -- 토큰이 클라이언트 JavaScript에 노출되지 않음 - ---- - -## 구현 세부사항 - -### 1. 로그인 프록시 (`src/app/api/auth/login/route.ts`) - -```typescript -export async function POST(request: NextRequest) { - const { user_id, user_pwd } = await request.json(); - - // PHP 백엔드 API 호출 - const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/login`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '', - }, - body: JSON.stringify({ user_id, user_pwd }), - }); - - const data = await response.json(); - - // HttpOnly 쿠키 설정 (JavaScript 접근 불가) - const cookieOptions = [ - `user_token=${data.user_token}`, - 'HttpOnly', // ✅ JavaScript 접근 차단 - 'Secure', // ✅ HTTPS 전용 - 'SameSite=Strict', // ✅ CSRF 방어 - 'Path=/', - 'Max-Age=604800', // 7일 - ].join('; '); - - // 응답: 토큰은 제외하고 사용자 정보만 반환 - return NextResponse.json( - { - message: data.message, - user: data.user, - tenant: data.tenant, - menus: data.menus, - }, - { - status: 200, - headers: { 'Set-Cookie': cookieOptions }, - } - ); -} -``` - -### 2. 로그아웃 프록시 (`src/app/api/auth/logout/route.ts`) - -```typescript -export async function POST(request: NextRequest) { - // HttpOnly 쿠키에서 토큰 읽기 - const token = request.cookies.get('user_token')?.value; - - if (token) { - // PHP 백엔드 로그아웃 API 호출 - await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/logout`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '', - }, - }); - } - - // HttpOnly 쿠키 삭제 - const cookieOptions = [ - 'user_token=', - 'HttpOnly', - 'Secure', - 'SameSite=Strict', - 'Path=/', - 'Max-Age=0', // 즉시 삭제 - ].join('; '); - - return NextResponse.json( - { message: 'Logged out successfully' }, - { status: 200, headers: { 'Set-Cookie': cookieOptions } } - ); -} -``` - -### 3. 클라이언트 로그인 (`src/components/auth/LoginPage.tsx`) - -```typescript -const handleLogin = async () => { - try { - // ✅ Next.js API Route로 프록시 - const response = await fetch('/api/auth/login', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - user_id: userId, - user_pwd: password, - }), - }); - - const data = await response.json(); - - console.log('✅ 로그인 성공:', data.message); - console.log('📦 사용자 정보:', data.user); - console.log('🔐 토큰은 안전한 HttpOnly 쿠키에 저장됨 (JavaScript 접근 불가)'); - - // 대시보드로 이동 - router.push("/dashboard"); - } catch (err: any) { - console.error('❌ 로그인 실패:', err); - setError(err.message || t('invalidCredentials')); - } -}; -``` - -### 4. 클라이언트 로그아웃 (`src/app/[locale]/dashboard/page.tsx`) - -```typescript -const handleLogout = async () => { - try { - // ✅ Next.js API Route로 프록시 - const response = await fetch('/api/auth/logout', { - method: 'POST', - }); - - if (response.ok) { - console.log('✅ 로그아웃 완료: HttpOnly 쿠키 삭제됨'); - } - - router.push('/login'); - } catch (error) { - console.error('로그아웃 처리 중 오류:', error); - router.push('/login'); - } -}; -``` - -### 5. 미들웨어 인증 확인 (`src/middleware.ts`) - -```typescript -function checkAuthentication(request: NextRequest): { - isAuthenticated: boolean; - authMode: 'sanctum' | 'bearer' | 'api-key' | null; -} { - // 1. Bearer Token 확인 (HttpOnly 쿠키에서) - const tokenCookie = request.cookies.get('user_token'); - if (tokenCookie && tokenCookie.value) { - return { isAuthenticated: true, authMode: 'bearer' }; - } - - // 2. Bearer Token 확인 (Authorization 헤더) - const authHeader = request.headers.get('authorization'); - if (authHeader?.startsWith('Bearer ')) { - return { isAuthenticated: true, authMode: 'bearer' }; - } - - return { isAuthenticated: false, authMode: null }; -} -``` - ---- - -## 테스트 가이드 - -### 1. 로그인 테스트 - -**단계:** -1. 브라우저에서 `http://localhost:3000/login` 접속 -2. 로그인 정보 입력: - - User ID: `zomking` - - Password: 테스트 비밀번호 -3. 로그인 버튼 클릭 - -**예상 결과:** -- ✅ 대시보드로 리다이렉트 -- ✅ 브라우저 개발자 도구 → Application → Cookies에서 `user_token` 확인 -- ✅ `user_token` 쿠키의 HttpOnly 플래그 확인 (체크되어 있어야 함) -- ✅ 콘솔에 "로그인 성공" 메시지 출력 - -**HttpOnly 쿠키 확인 방법:** -```javascript -// 브라우저 콘솔에서 실행 -console.log(document.cookie); -// 결과: user_token이 보이지 않아야 함 (HttpOnly로 차단됨) -``` - -### 2. 인증 상태 확인 테스트 - -**단계:** -1. 로그인 상태에서 주소창에 `http://localhost:3000/dashboard` 직접 입력 -2. 페이지 새로고침 (F5) - -**예상 결과:** -- ✅ 대시보드 페이지 정상 표시 -- ✅ 로그인 페이지로 리다이렉트되지 않음 -- ✅ 서버 터미널에 "[Auth Check] Token found in cookie" 로그 출력 - -### 3. 비로그인 상태 차단 테스트 - -**단계:** -1. 로그아웃 버튼 클릭 또는 쿠키 수동 삭제 -2. 주소창에 `http://localhost:3000/dashboard` 직접 입력 - -**예상 결과:** -- ✅ 로그인 페이지로 자동 리다이렉트 -- ✅ URL에 `?redirect=/dashboard` 파라미터 포함 -- ✅ 서버 터미널에 "[Auth Required] Redirecting to /login" 로그 출력 - -### 4. 로그아웃 테스트 - -**단계:** -1. 로그인 상태에서 대시보드의 "Logout" 버튼 클릭 - -**예상 결과:** -- ✅ 로그인 페이지로 리다이렉트 -- ✅ 브라우저 개발자 도구 → Cookies에서 `user_token` 쿠키 삭제됨 -- ✅ 콘솔에 "로그아웃 완료: HttpOnly 쿠키 삭제됨" 메시지 출력 -- ✅ 다시 `/dashboard` 접근 시 로그인 페이지로 리다이렉트 - -### 5. XSS 방어 확인 (보안 테스트) - -**단계:** -1. 로그인 상태에서 브라우저 콘솔 열기 -2. 다음 코드 실행: -```javascript -// localStorage 토큰 읽기 시도 -console.log('localStorage token:', localStorage.getItem('user_token')); -// 결과: null (토큰이 localStorage에 없음) - -// 쿠키 토큰 읽기 시도 -console.log('cookie token:', document.cookie); -// 결과: user_token이 보이지 않음 (HttpOnly로 차단됨) -``` - -**예상 결과:** -- ✅ `localStorage.getItem('user_token')` → `null` -- ✅ `document.cookie` → `user_token`이 포함되지 않음 -- ✅ JavaScript로 토큰 접근 완전히 차단 확인 - -### 6. 서버 터미널 로그 확인 - -**로그인 시:** -``` -✅ Login successful - Token stored in HttpOnly cookie -``` - -**미들웨어 실행 시:** -``` -[Auth Check] Token found in cookie -[Auth Check] User authenticated with bearer mode -``` - -**로그아웃 시:** -``` -✅ Backend logout API called successfully -✅ Logout complete - HttpOnly cookie cleared -``` - ---- - -## 보안 비교표 - -| 항목 | 이전 방식 (localStorage) | 새로운 방식 (HttpOnly Cookie) | -|------|------------------------|------------------------------| -| **XSS 공격** | 🔴 취약 (7.6/10) | 🟢 방어 (2.8/10) | -| **JavaScript 접근** | ❌ 가능 (`localStorage.getItem()`) | ✅ 차단 (HttpOnly) | -| **document.cookie 접근** | ❌ 가능 | ✅ 차단 (HttpOnly) | -| **CSRF 방어** | ⚠️ 부분적 (SameSite=Lax) | ✅ 강화 (SameSite=Strict) | -| **HTTPS 강제** | ❌ 없음 | ✅ Secure 플래그 | -| **토큰 노출** | ❌ 클라이언트에 노출 | ✅ 클라이언트에서 숨김 | - ---- - -## 삭제된 파일 - -다음 파일들은 더 이상 필요하지 않아 삭제되었습니다: - -1. `src/lib/api/auth/sanctum-client.ts` - 직접 PHP API 호출 및 localStorage 사용 -2. `src/lib/api/auth/token-storage.ts` - localStorage 기반 토큰 저장 관리 - -**이유:** -- HttpOnly 쿠키 방식으로 전환하면서 localStorage 사용 불필요 -- Next.js Route Handlers가 PHP API 프록시 역할 수행 -- 토큰은 서버 측에서만 처리 (클라이언트 코드에서 토큰 관리 불필요) - ---- - -## 환경 변수 - -`.env.local` 파일에 필요한 환경 변수: - -```env -NEXT_PUBLIC_API_URL=https://api.5130.co.kr -NEXT_PUBLIC_API_KEY=42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a -NEXT_PUBLIC_FRONTEND_URL=http://localhost:3000 -NEXT_PUBLIC_AUTH_MODE=sanctum -``` - ---- - -## 다음 보안 개선 단계 (향후 계획) - -### Option 2: Backend Session (더 높은 보안) -- PHP Laravel에서 세션 기반 인증으로 전환 -- 프론트엔드는 세션 ID만 관리 -- 보안 위험: 🟢 1.5/10 - -### Option 3: BFF Pattern (엔터프라이즈급) -- Backend For Frontend 패턴 구현 -- Next.js API Routes가 모든 인증 로직 담당 -- PHP API는 내부 API로만 사용 -- 보안 위험: 🟢 1.2/10 - ---- - -## 트러블슈팅 - -### 문제: 쿠키가 설정되지 않음 -**원인:** Secure 플래그 때문에 HTTP 환경에서 차단 -**해결:** 개발 환경에서는 `Secure` 플래그 제거 가능 (프로덕션에서는 필수) - -### 문제: 미들웨어에서 토큰을 읽지 못함 -**원인:** 쿠키 이름 불일치 또는 Path 설정 문제 -**해결:** `request.cookies.get('user_token')` 확인 및 `Path=/` 설정 확인 - -### 문제: 로그인 후에도 인증 실패 -**원인:** 쿠키가 다른 도메인에 설정됨 -**해결:** SameSite 설정 확인 및 도메인 일치 여부 확인 - ---- - -## 결론 - -✅ **보안 개선 완료:** -- XSS 공격 위험: 7.6/10 → 2.8/10 -- JavaScript 토큰 접근 완전 차단 -- CSRF 방어 강화 -- HTTPS 강제 적용 - -✅ **구현 완료 항목:** -1. Next.js Route Handlers (로그인/로그아웃 프록시) -2. HttpOnly 쿠키 저장 방식 -3. 클라이언트 코드 업데이트 -4. 미들웨어 인증 확인 (기존 코드 호환) -5. 레거시 코드 제거 (sanctum-client.ts, token-storage.ts) - -🔄 **테스트 필요:** -- 로그인/로그아웃 플로우 -- HttpOnly 쿠키 동작 확인 -- 비로그인 상태 차단 확인 -- XSS 방어 검증 - ---- - -## 관련 파일 - -### 프론트엔드 -- `src/app/api/auth/login/route.ts` - 로그인 프록시 API -- `src/app/api/auth/logout/route.ts` - 로그아웃 프록시 API -- `src/components/auth/LoginPage.tsx` - 로그인 페이지 컴포넌트 -- `src/middleware.ts` - 인증 미들웨어 -- `src/app/[locale]/dashboard/page.tsx` - 대시보드 (로그아웃 버튼) - -### 설정 파일 -- `.env.local` - 환경 변수 (API URL, API Key) \ No newline at end of file diff --git a/claudedocs/auth/[REF] nextjs15-middleware-authentication-research.md b/claudedocs/auth/[REF] nextjs15-middleware-authentication-research.md deleted file mode 100644 index cbf42904..00000000 --- a/claudedocs/auth/[REF] nextjs15-middleware-authentication-research.md +++ /dev/null @@ -1,478 +0,0 @@ -# Next.js 15 Middleware Authentication Issues - Research Report - -**Date**: November 7, 2025 -**Project**: sam-react-prod -**Research Focus**: Next.js 15 middleware not executing, console logs not appearing, next-intl integration - ---- - -## Executive Summary - -**ROOT CAUSE IDENTIFIED**: The project has duplicate middleware files: -- `/Users/.../sam-react-prod/middleware.ts` (root level) -- `/Users/.../sam-react-prod/src/middleware.ts` (inside src directory) - -**Next.js only supports ONE middleware.ts file per project.** Having duplicate files causes Next.js to ignore or behave unpredictably with middleware execution, which explains why console logs are not appearing and protected routes are not being blocked. - -**Confidence Level**: HIGH (95%) -Based on official Next.js documentation and multiple community reports confirming this issue. - ---- - -## Problem Analysis - -### Current Situation -1. Middleware exists in both project root AND src directory (duplicate files) -2. Console logs from middleware not appearing in terminal -3. Protected routes not being blocked despite middleware configuration -4. Cookies work correctly (set/delete properly), indicating the issue is NOT with authentication logic itself -5. Middleware matcher configuration appears correct - -### Why Middleware Isn't Executing - -**Primary Issue: Duplicate Middleware Files** -- Next.js only recognizes ONE middleware file per project -- When both `middleware.ts` (root) and `src/middleware.ts` exist, Next.js behavior is undefined -- Typically, Next.js will ignore both or only recognize one unpredictably -- This causes complete middleware execution failure - -**Source**: Official Next.js documentation and GitHub discussions (#50026, #73040090) - ---- - -## Key Research Findings - -### 1. Middleware File Location Rules (CRITICAL) - -**Next.js Convention:** -- **With `src/` directory**: Place middleware at `src/middleware.ts` (same level as `src/app`) -- **Without `src/` directory**: Place middleware at `middleware.ts` (same level as `app` or `pages`) -- **Only ONE middleware file allowed per project** - -**Current Project Structure:** -``` -sam-react-prod/ -├── middleware.ts ← DUPLICATE (should be removed) -├── src/ -│ ├── middleware.ts ← CORRECT location for src-based projects -│ ├── app/ -│ └── ... -``` - -**Action Required**: Delete the root-level `middleware.ts` and keep only `src/middleware.ts` - -**Confidence**: 100% - This is the primary issue - ---- - -### 2. Console.log Debugging in Middleware - -**Where Console Logs Appear:** -- Middleware runs **server-side**, not client-side -- Console logs appear in the **terminal** where you run `npm run dev`, NOT in browser console -- If middleware isn't executing at all, no logs will appear anywhere - -**Debugging Techniques:** -1. Check terminal output (where `npm run dev` is running) -2. Add console.log at the very beginning of middleware function -3. Verify middleware returns NextResponse (next() or redirect) -4. Use structured logging: `console.log('[Middleware]', { pathname, cookies, headers })` - -**Example Debug Pattern:** -```typescript -export function middleware(request: NextRequest) { - console.log('=== MIDDLEWARE START ===', { - pathname: request.nextUrl.pathname, - method: request.method, - timestamp: new Date().toISOString() - }); - - // ... rest of middleware logic - - console.log('=== MIDDLEWARE END ==='); - return response; -} -``` - -**Sources**: Stack Overflow (#70343453), GitHub discussions (#66104) - ---- - -### 3. Next-Intl Middleware Integration Patterns - -**Recommended Pattern for Next.js 15 + next-intl + Authentication:** - -```typescript -import createMiddleware from 'next-intl/middleware'; -import { NextRequest, NextResponse } from 'next/server'; - -// Create i18n middleware -const intlMiddleware = createMiddleware({ - locales: ['en', 'ko'], - defaultLocale: 'en' -}); - -export function middleware(request: NextRequest) { - const { pathname } = request.nextUrl; - - // 1. Remove locale prefix for route checking - const pathnameWithoutLocale = getPathnameWithoutLocale(pathname); - - // 2. Check if route is public (skip auth) - if (isPublicRoute(pathnameWithoutLocale)) { - return intlMiddleware(request); - } - - // 3. Check authentication - const isAuthenticated = checkAuth(request); - - // 4. Protect routes - redirect if not authenticated - if (isProtectedRoute(pathnameWithoutLocale) && !isAuthenticated) { - const loginUrl = new URL('/login', request.url); - loginUrl.searchParams.set('redirect', pathname); - return NextResponse.redirect(loginUrl); - } - - // 5. Apply i18n middleware for all other requests - return intlMiddleware(request); -} -``` - -**Execution Order:** -1. Locale detection (next-intl) should run FIRST to normalize URLs -2. Authentication checks run AFTER locale normalization -3. Both use the same middleware function (no separate middleware files) - -**Key Insight**: Your current implementation follows this pattern correctly, but it's not executing due to the duplicate file issue. - -**Sources**: next-intl official documentation, Medium articles by Issam Ahwach and Yoko Hailemariam - ---- - -### 4. Middleware Matcher Configuration - -**Current Configuration (Correct):** -```typescript -export const config = { - matcher: [ - '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico)$).*)', - '/dashboard/:path*', - '/login', - '/register', - ], -}; -``` - -**Analysis**: This configuration is correct and should work. It: -- Excludes static files and Next.js internals -- Explicitly includes dashboard, login, and register routes -- Uses negative lookahead regex for general matching - -**Best Practice Matcher Patterns:** -```typescript -// Exclude static files (most common) -'/((?!api|_next/static|_next/image|favicon.ico).*)' - -// Protect specific routes only -['/dashboard/:path*', '/admin/:path*'] - -// Protect everything except public routes -'/((?!_next|static|public|api|auth).*)' -``` - -**Sources**: Next.js official docs, Medium articles on middleware matchers - ---- - -### 5. Authentication Check Implementation - -**Current Implementation Analysis:** - -Your `checkAuthentication()` function checks for: -1. Bearer token in cookies (`user_token`) -2. Bearer token in Authorization header -3. Laravel Sanctum session cookie (`laravel_session`) -4. API key in headers (`x-api-key`) - -**This is CORRECT** - the logic is sound. - -**Why It Appears Not to Work:** -- The middleware isn't executing at all due to duplicate files -- Once the duplicate file issue is fixed, this authentication logic should work correctly - -**Verification Method After Fix:** -```typescript -// Add at the top of checkAuthentication function -export function checkAuthentication(request: NextRequest) { - console.log('[Auth Check]', { - hasCookie: !!request.cookies.get('user_token'), - hasAuthHeader: !!request.headers.get('authorization'), - hasSession: !!request.cookies.get('laravel_session'), - hasApiKey: !!request.headers.get('x-api-key') - }); - - // ... existing logic -} -``` - ---- - -## Common Next.js 15 Middleware Issues (Beyond Your Case) - -### Issue 1: Middleware Not Returning Response -**Problem**: Middleware must return NextResponse -**Solution**: Always return `NextResponse.next()`, `NextResponse.redirect()`, or `NextResponse.rewrite()` - -### Issue 2: Matcher Not Matching Routes -**Problem**: Regex patterns too restrictive -**Solution**: Test with simple matcher first: `matcher: ['/dashboard/:path*']` - -### Issue 3: Console Logs Not Visible -**Problem**: Looking in browser console instead of terminal -**Solution**: Check the terminal where dev server is running - -### Issue 4: Middleware Caching Issues -**Problem**: Old middleware code cached during development -**Solution**: Restart dev server, clear `.next` folder - -**Sources**: Multiple Stack Overflow threads and GitHub issues - ---- - -## Solution Implementation Steps - -### Step 1: Remove Duplicate Middleware File (CRITICAL) - -```bash -# Delete the root-level middleware.ts -rm /Users/byeongcheolryu/codebridgex/sam_project/sam-next/sma-next-project/sam-react-prod/middleware.ts - -# Keep only src/middleware.ts -``` - -### Step 2: Restart Development Server - -```bash -# Stop current dev server (Ctrl+C) -# Clear Next.js cache -rm -rf .next - -# Restart dev server -npm run dev -``` - -### Step 3: Test Middleware Execution - -**Test in Terminal (where npm run dev runs):** -- Navigate to `/dashboard` in browser -- Check terminal for console logs: `[Middleware] Original: /dashboard` -- Should see authentication checks and redirects - -**Expected Terminal Output:** -``` -[Middleware] Original: /dashboard, Without Locale: /dashboard -[Auth Required] Redirecting to /login from /dashboard -``` - -### Step 4: Verify Protected Routes - -**Test Cases:** -1. Access `/dashboard` without authentication → Should redirect to `/login?redirect=/dashboard` -2. Access `/login` when authenticated → Should redirect to `/dashboard` -3. Access `/` (public route) → Should load without redirect -4. Access `/ko/dashboard` (with locale) → Should handle locale and redirect appropriately - -### Step 5: Monitor Console Output - -Add enhanced logging to track middleware execution: - -```typescript -export function middleware(request: NextRequest) { - const timestamp = new Date().toISOString(); - console.log(`\n${'='.repeat(50)}`); - console.log(`[${timestamp}] MIDDLEWARE EXECUTION START`); - console.log(`Path: ${request.nextUrl.pathname}`); - console.log(`Method: ${request.method}`); - - // ... existing logic with detailed logs at each step - - console.log(`[${timestamp}] MIDDLEWARE EXECUTION END`); - console.log(`${'='.repeat(50)}\n`); - return response; -} -``` - ---- - -## Additional Recommendations - -### 1. Environment Variables Validation - -Add startup validation to ensure required env vars are present: - -```typescript -// In auth-config.ts -const requiredEnvVars = [ - 'NEXT_PUBLIC_API_URL', - 'NEXT_PUBLIC_FRONTEND_URL' -]; - -requiredEnvVars.forEach(varName => { - if (!process.env[varName]) { - console.error(`Missing required environment variable: ${varName}`); - } -}); -``` - -### 2. Middleware Performance Monitoring - -Add timing logs to identify bottlenecks: - -```typescript -export function middleware(request: NextRequest) { - const startTime = Date.now(); - - // ... middleware logic - - const duration = Date.now() - startTime; - console.log(`[Middleware] Execution time: ${duration}ms`); - return response; -} -``` - -### 3. Cookie Security Configuration - -Ensure cookies are configured securely: - -```typescript -// When setting cookies (in auth logic, not middleware) -{ - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'lax', - path: '/', - maxAge: 60 * 60 * 24 * 7 // 7 days -} -``` - -### 4. Next.js 15 Specific Considerations - -**Next.js 15 Changes:** -- Improved middleware performance with edge runtime optimization -- Better TypeScript support for middleware -- Enhanced matcher configuration with glob patterns -- Middleware now respects `output: 'standalone'` configuration - -**Compatibility Check:** -```bash -# Verify Next.js version -npm list next -# Should show: next@15.5.6 (matches your package.json) -``` - ---- - -## Testing Checklist - -After implementing the fix (removing duplicate middleware file): - -- [ ] Middleware console logs appear in terminal -- [ ] Protected routes redirect to login when unauthenticated -- [ ] Login redirects to dashboard when authenticated -- [ ] Locale URLs work correctly (e.g., `/ko/dashboard`) -- [ ] Static files bypass middleware (no logs for images/CSS) -- [ ] API routes behave as expected -- [ ] Bot detection works for protected paths -- [ ] Cookie authentication functions correctly -- [ ] Redirect parameter works (`/login?redirect=/dashboard`) - ---- - -## References and Sources - -### Official Documentation -- Next.js Middleware: https://nextjs.org/docs/app/building-your-application/routing/middleware -- next-intl Middleware: https://next-intl.dev/docs/routing/middleware -- Next.js 15 Release Notes: https://nextjs.org/blog/next-15 - -### Community Resources -- Stack Overflow: Multiple threads on middleware execution issues -- GitHub Discussions: vercel/next.js #50026, #66104, #73040090 -- Medium Articles: - - "Simplifying Next.js Authentication and Internationalization" by Issam Ahwach - - "Conquering Auth v5 and next-intl Middleware" by Yoko Hailemariam - -### Key GitHub Issues -- Middleware file location conflicts: #50026 -- Middleware not triggering: #73040090, #66104 -- Console.log in middleware: #70343453 -- next-intl integration: amannn/next-intl #1613, #341 - ---- - -## Confidence Assessment - -**Overall Confidence**: 95% - -**High Confidence (95%+)**: -- Duplicate middleware file is the root cause -- File location requirements per Next.js conventions -- Console.log behavior (terminal vs browser) - -**Medium Confidence (70-85%)**: -- Specific next-intl integration patterns (implementation-dependent) -- Cookie configuration best practices (environment-dependent) - -**Areas Requiring Verification**: -- AUTH_CONFIG.protectedRoutes array contents -- Actual cookie names used by Laravel backend -- Production deployment configuration - ---- - -## Next Steps - -1. **Immediate Action**: Remove duplicate `middleware.ts` from project root -2. **Verify Fix**: Restart dev server and test middleware execution -3. **Monitor**: Check terminal logs during testing -4. **Validate**: Run through complete authentication flow -5. **Document**: Update project documentation with correct middleware setup - ---- - -## Appendix: Middleware Execution Flow Diagram - -``` -Request Received - ↓ -[Next.js Checks for middleware.ts] - ↓ -[Duplicate Files Detected] ← CURRENT ISSUE - ↓ -[Undefined Behavior / No Execution] - ↓ -[No Console Logs, No Auth Checks] - - -After Fix: -Request Received - ↓ -[Next.js Loads src/middleware.ts] - ↓ -[Middleware Function Executes] - ↓ -1. Log pathname -2. Check bot detection -3. Check public routes -4. Check authentication -5. Apply next-intl middleware -6. Return response - ↓ -[Route Protected / Locale Applied / Request Continues] -``` - ---- - -**Report Generated**: November 7, 2025 -**Research Method**: Web search (5 queries) + documentation analysis + code review -**Total Sources**: 40+ Stack Overflow threads, GitHub issues, and official docs analyzed diff --git a/claudedocs/auth/[REF] session-migration-backend.md b/claudedocs/auth/[REF] session-migration-backend.md deleted file mode 100644 index 253deb16..00000000 --- a/claudedocs/auth/[REF] session-migration-backend.md +++ /dev/null @@ -1,615 +0,0 @@ -# 세션 기반 인증 전환 가이드 - 백엔드 (PHP/Laravel) - -## 📋 개요 - -**목적**: JWT 토큰 기반 → 세션 기반 인증으로 전환하여 보안 강화 - -**주요 보안 개선 사항**: -- ✅ 로그아웃 시 즉시 세션 무효화 (토큰 만료 대기 불필요) -- ✅ 세션 하이재킹 실시간 감지 (IP/User-Agent 추적) -- ✅ 관리자의 강제 로그아웃 기능 -- ✅ 1계정 1세션 강제 (동시 로그인 제한) -- ✅ 의심스러운 활동 자동 차단 - ---- - -## 🔧 1단계: 환경 설정 - -### 1.1 세션 드라이버 설정 - -```bash -# .env -SESSION_DRIVER=redis -SESSION_LIFETIME=120 # 2시간 (분 단위) -SESSION_SECURE_COOKIE=true -SESSION_DOMAIN=.yourdomain.com # 서브도메인 공유 시 -REDIS_HOST=127.0.0.1 -REDIS_PASSWORD=null -REDIS_PORT=6379 -``` - -### 1.2 세션 설정 파일 - -```php -// config/session.php -return [ - 'driver' => env('SESSION_DRIVER', 'redis'), - 'lifetime' => env('SESSION_LIFETIME', 120), - 'expire_on_close' => false, - 'encrypt' => true, // 🔒 세션 데이터 암호화 - 'http_only' => true, // 🔒 XSS 방지 - 'same_site' => 'strict', // 🔒 CSRF 방지 - 'secure' => env('SESSION_SECURE_COOKIE', true), // 🔒 HTTPS only - - // 세션 가비지 컬렉션 - 'lottery' => [2, 100], - - // 세션 쿠키 이름 - 'cookie' => env( - 'SESSION_COOKIE', - Str::slug(env('APP_NAME', 'laravel'), '_').'_session' - ), -]; -``` - ---- - -## 🔐 2단계: 인증 가드 변경 - -### 2.1 Auth 설정 - -```php -// config/auth.php -'guards' => [ - 'web' => [ - 'driver' => 'session', - 'provider' => 'users', - ], - - 'api' => [ - 'driver' => 'session', // Sanctum → Session 변경 - 'provider' => 'users', - ], -], -``` - ---- - -## 🚪 3단계: 로그인 컨트롤러 수정 - -### 3.1 기존 코드 (토큰 기반) - -```php -// ❌ 제거할 코드 -public function login(Request $request) -{ - // JWT 토큰 발급 - $token = auth()->attempt($credentials); - - return response()->json([ - 'access_token' => $token, - 'refresh_token' => $refreshToken, - 'token_type' => 'bearer', - 'expires_in' => 7200, - ]); -} -``` - -### 3.2 새로운 코드 (세션 기반) - -```php -// ✅ 새로운 로그인 로직 -namespace App\Http\Controllers\Auth; - -use Illuminate\Http\Request; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\DB; - -class LoginController extends Controller -{ - public function login(Request $request) - { - // 입력 검증 - $credentials = $request->validate([ - 'user_id' => 'required|string', - 'user_pwd' => 'required|string', - ]); - - // 🔒 세션 기반 인증 - if (Auth::attempt([ - 'user_id' => $credentials['user_id'], - 'password' => $credentials['user_pwd'] - ], $request->filled('remember'))) { - - // 🔒 세션 재생성 (세션 고정 공격 방지) - $request->session()->regenerate(); - - // 🔒 보안 정보 저장 (하이재킹 감지용) - session([ - 'ip_address' => $request->ip(), - 'user_agent' => $request->userAgent(), - 'login_at' => now()->toDateTimeString(), - ]); - - // 🔒 동시 로그인 제한 (옵션) - $this->limitConcurrentSessions(Auth::user()); - - // 사용자 정보 반환 (토큰 없음!) - return response()->json([ - 'message' => 'Login successful', - 'user' => [ - 'id' => Auth::user()->id, - 'user_id' => Auth::user()->user_id, - 'name' => Auth::user()->name, - 'email' => Auth::user()->email, - 'phone' => Auth::user()->phone, - ], - 'tenant' => Auth::user()->tenant, - 'menus' => Auth::user()->menus, - 'roles' => Auth::user()->roles, - ]); - } - - // 인증 실패 - return response()->json([ - 'error' => 'Invalid credentials' - ], 401); - } - - /** - * 🔒 동시 로그인 제한 (1계정 1세션) - */ - protected function limitConcurrentSessions($user) - { - // 현재 세션 ID 제외하고 모든 세션 삭제 - DB::table('sessions') - ->where('user_id', $user->id) - ->where('id', '!=', session()->getId()) - ->delete(); - } -} -``` - ---- - -## 🚪 4단계: 로그아웃 컨트롤러 수정 - -```php -// app/Http/Controllers/Auth/LogoutController.php -namespace App\Http\Controllers\Auth; - -use Illuminate\Http\Request; -use Illuminate\Support\Facades\Auth; - -class LogoutController extends Controller -{ - public function logout(Request $request) - { - // 🔒 세션 무효화 - Auth::logout(); - - // 🔒 세션 데이터 삭제 - $request->session()->invalidate(); - - // 🔒 CSRF 토큰 재생성 - $request->session()->regenerateToken(); - - return response()->json([ - 'message' => 'Logged out successfully' - ]); - } -} -``` - ---- - -## 🛡️ 5단계: 세션 하이재킹 감지 미들웨어 - -### 5.1 미들웨어 생성 - -```bash -php artisan make:middleware DetectSessionHijacking -``` - -### 5.2 미들웨어 코드 - -```php -// app/Http/Middleware/DetectSessionHijacking.php -namespace App\Http\Middleware; - -use Closure; -use Illuminate\Http\Request; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Log; - -class DetectSessionHijacking -{ - /** - * 세션 하이재킹 감지 및 차단 - */ - public function handle(Request $request, Closure $next) - { - if (Auth::check()) { - $user = Auth::user(); - - // 🔒 IP 주소 변경 감지 - if (session('ip_address') && session('ip_address') !== $request->ip()) { - Log::warning('Session hijacking detected: IP changed', [ - 'user_id' => $user->id, - 'old_ip' => session('ip_address'), - 'new_ip' => $request->ip(), - ]); - - // 세션 파괴 및 로그아웃 - Auth::logout(); - $request->session()->invalidate(); - $request->session()->regenerateToken(); - - return response()->json([ - 'error' => 'Session security violation detected', - 'code' => 'SESSION_HIJACKED', - 'message' => 'Your session has been terminated for security reasons.' - ], 401); - } - - // 🔒 User-Agent 변경 감지 - if (session('user_agent') && session('user_agent') !== $request->userAgent()) { - Log::warning('Session hijacking detected: User-Agent changed', [ - 'user_id' => $user->id, - 'old_ua' => session('user_agent'), - 'new_ua' => $request->userAgent(), - ]); - - Auth::logout(); - $request->session()->invalidate(); - $request->session()->regenerateToken(); - - return response()->json([ - 'error' => 'Session security violation detected', - 'code' => 'SESSION_HIJACKED' - ], 401); - } - } - - return $next($request); - } -} -``` - -### 5.3 미들웨어 등록 - -```php -// app/Http/Kernel.php -protected $middlewareGroups = [ - 'api' => [ - \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, - 'throttle:api', - \Illuminate\Routing\Middleware\SubstituteBindings::class, - \App\Http\Middleware\DetectSessionHijacking::class, // ✅ 추가 - ], -]; -``` - ---- - -## 🌐 6단계: CORS 설정 (중요!) - -### 6.1 CORS 설정 파일 - -```php -// config/cors.php -return [ - 'paths' => ['api/*', 'sanctum/csrf-cookie'], - - 'allowed_methods' => ['*'], - - 'allowed_origins' => [ - 'http://localhost:3000', // 개발 환경 - 'https://yourdomain.com', // 프로덕션 - 'https://app.yourdomain.com', // 프로덕션 앱 - ], - - 'allowed_origins_patterns' => [], - - 'allowed_headers' => ['*'], - - 'exposed_headers' => [], - - 'max_age' => 0, - - 'supports_credentials' => true, // ✅ 세션 쿠키 전송 허용 (필수!) -]; -``` - ---- - -## 🗑️ 7단계: 토큰 관련 코드 제거 - -### 7.1 삭제할 엔드포인트 - -```php -// routes/api.php - -// ❌ 삭제: 토큰 갱신 엔드포인트 (세션은 자동 갱신) -// Route::post('/refresh', [TokenController::class, 'refresh']); -``` - -### 7.2 삭제할 컨트롤러 - -```bash -# ❌ 삭제 또는 주석 처리 -# app/Http/Controllers/Auth/TokenRefreshController.php -``` - ---- - -## ✅ 8단계: 세션 확인 엔드포인트 추가 - -```php -// routes/api.php -Route::get('/auth/check', [AuthController::class, 'check']); -``` - -```php -// app/Http/Controllers/Auth/AuthController.php -public function check(Request $request) -{ - if (Auth::check()) { - return response()->json([ - 'authenticated' => true, - 'user' => [ - 'id' => Auth::user()->id, - 'name' => Auth::user()->name, - 'email' => Auth::user()->email, - ] - ]); - } - - return response()->json([ - 'authenticated' => false - ]); -} -``` - ---- - -## 🧪 9단계: 테스트 - -### 9.1 로그인 테스트 - -```bash -curl -X POST http://localhost:8000/api/v1/login \ - -H "Content-Type: application/json" \ - -H "X-API-KEY: your-api-key" \ - -d '{"user_id": "test", "user_pwd": "password"}' \ - -c cookies.txt # 쿠키 저장 - -# 응답: -# { -# "message": "Login successful", -# "user": {...}, -# "tenant": {...} -# } -# -# Set-Cookie: laravel_session=abc123... -``` - -### 9.2 세션 확인 테스트 - -```bash -curl -X GET http://localhost:8000/api/v1/auth/check \ - -H "X-API-KEY: your-api-key" \ - -b cookies.txt # 저장된 쿠키 사용 - -# 응답: -# { -# "authenticated": true, -# "user": {...} -# } -``` - -### 9.3 로그아웃 테스트 - -```bash -curl -X POST http://localhost:8000/api/v1/logout \ - -H "X-API-KEY: your-api-key" \ - -b cookies.txt - -# 응답: -# { -# "message": "Logged out successfully" -# } -``` - -### 9.4 세션 하이재킹 감지 테스트 - -```bash -# 1. 로그인 (IP: A) -curl -X POST http://localhost:8000/api/v1/login \ - -H "X-API-KEY: your-api-key" \ - -d '{"user_id": "test", "user_pwd": "password"}' \ - -c cookies.txt - -# 2. 다른 IP에서 같은 세션 ID 사용 시도 (IP: B) -# → 자동 차단되어야 함 -``` - ---- - -## 🔒 10단계: 추가 보안 강화 (옵션) - -### 10.1 Rate Limiting (무차별 대입 공격 방지) - -```php -// routes/api.php -Route::middleware(['throttle:5,1'])->group(function () { - Route::post('/login', [LoginController::class, 'login']); -}); - -// 5번 시도 후 1분 대기 -``` - -### 10.2 세션 활동 로그 - -```php -// app/Models/SessionLog.php 생성 -Schema::create('session_logs', function (Blueprint $table) { - $table->id(); - $table->unsignedBigInteger('user_id'); - $table->string('ip_address'); - $table->text('user_agent'); - $table->timestamp('login_at'); - $table->timestamp('logout_at')->nullable(); - $table->timestamps(); -}); -``` - -```php -// 로그인 시 기록 -SessionLog::create([ - 'user_id' => Auth::id(), - 'ip_address' => $request->ip(), - 'user_agent' => $request->userAgent(), - 'login_at' => now(), -]); -``` - -### 10.3 관리자 강제 로그아웃 기능 - -```php -// app/Http/Controllers/Admin/SessionController.php -public function forceLogout(Request $request, $userId) -{ - // 특정 사용자의 모든 세션 삭제 - DB::table('sessions') - ->where('user_id', $userId) - ->delete(); - - return response()->json([ - 'message' => 'User sessions terminated' - ]); -} -``` - ---- - -## 📊 마이그레이션 체크리스트 - -### 필수 작업 - -- [ ] `.env` 파일 세션 드라이버 설정 -- [ ] `config/session.php` 보안 설정 적용 -- [ ] `config/auth.php` 가드를 세션으로 변경 -- [ ] 로그인 컨트롤러 수정 (토큰 제거, 세션 사용) -- [ ] 로그아웃 컨트롤러 수정 (세션 무효화) -- [ ] `config/cors.php`에서 `supports_credentials: true` 설정 -- [ ] 세션 하이재킹 감지 미들웨어 추가 -- [ ] `/api/v1/refresh` 엔드포인트 삭제 -- [ ] `/api/v1/auth/check` 엔드포인트 추가 - -### 권장 작업 - -- [ ] Rate Limiting 적용 -- [ ] 세션 활동 로그 테이블 생성 -- [ ] 관리자 강제 로그아웃 기능 구현 -- [ ] 동시 로그인 제한 적용 - -### 테스트 - -- [ ] 로그인 → 세션 생성 확인 -- [ ] 로그아웃 → 세션 파괴 확인 -- [ ] 세션 하이재킹 감지 테스트 -- [ ] CORS 크로스 도메인 테스트 -- [ ] 동시 로그인 제한 테스트 - ---- - -## 🚨 주의사항 - -### 1. 세션 저장소 (Redis) 필수 - -```bash -# Redis 설치 확인 -redis-cli ping -# 응답: PONG - -# Redis 접속 테스트 -redis-cli -> KEYS *session* -``` - -### 2. CORS 설정 필수 - -- `supports_credentials: true` 반드시 설정 -- 프론트엔드 도메인을 `allowed_origins`에 추가 -- `*` (와일드카드) 사용 불가 (credentials와 충돌) - -### 3. HTTPS 필수 (프로덕션) - -```bash -# .env -SESSION_SECURE_COOKIE=true # HTTPS만 쿠키 전송 -``` - -### 4. 세션 쿠키 이름 확인 - -```php -// config/session.php -'cookie' => 'laravel_session', // 프론트엔드에서 이 이름 사용 -``` - ---- - -## 📞 프론트엔드 팀 공유 사항 - -### API 변경 사항 - -**로그인 응답 변경**: -```json -// ❌ 이전 (토큰 반환) -{ - "access_token": "eyJhbG...", - "refresh_token": "eyJhbG...", - "token_type": "bearer", - "expires_in": 7200 -} - -// ✅ 이후 (토큰 없음, 세션 쿠키만) -{ - "message": "Login successful", - "user": {...}, - "tenant": {...} -} - -// Set-Cookie: laravel_session=abc123... -``` - -**필수 요구사항**: -- 모든 API 호출에 `credentials: 'include'` 추가 -- 세션 쿠키를 자동으로 포함하여 전송 -- `/api/auth/refresh` 엔드포인트 사용 중단 - ---- - -## 🎯 완료 후 확인사항 - -1. ✅ 로그인 시 세션 쿠키 생성 -2. ✅ 로그아웃 시 즉시 접근 차단 -3. ✅ IP 변경 시 자동 차단 -4. ✅ User-Agent 변경 시 자동 차단 -5. ✅ 관리자 강제 로그아웃 작동 -6. ✅ Redis에 세션 데이터 저장 확인 - ---- - -## 📚 참고 자료 - -- [Laravel Session 공식 문서](https://laravel.com/docs/session) -- [Laravel Authentication 공식 문서](https://laravel.com/docs/authentication) -- [Redis Session Driver](https://laravel.com/docs/redis) - ---- - -**작성일**: 2025-11-12 -**작성자**: Claude Code -**버전**: 1.0 \ No newline at end of file diff --git a/claudedocs/auth/[REF] session-migration-frontend.md b/claudedocs/auth/[REF] session-migration-frontend.md deleted file mode 100644 index fa0ed028..00000000 --- a/claudedocs/auth/[REF] session-migration-frontend.md +++ /dev/null @@ -1,580 +0,0 @@ -# 세션 기반 인증 전환 가이드 - 프론트엔드 (Next.js) - -## 📋 개요 - -**목적**: 백엔드 세션 기반 인증에 맞춰 프론트엔드 수정 - -**주요 변경 사항**: -- ❌ JWT 토큰 저장 로직 제거 -- ✅ 백엔드 세션 쿠키 전달 방식으로 변경 -- ❌ 토큰 갱신 엔드포인트 제거 -- ✅ 모든 API 호출에 `credentials: 'include'` 추가 - ---- - -## 🔍 현재 구조 분석 - -### 현재 파일 구조 - -``` -src/ -├── app/ -│ └── api/ -│ └── auth/ -│ ├── login/route.ts # 백엔드 토큰 → 쿠키 저장 -│ ├── logout/route.ts # 쿠키 삭제 -│ ├── refresh/route.ts # ❌ 삭제 예정 -│ └── check/route.ts # 쿠키 확인 -├── lib/ -│ └── auth/ -│ └── token-refresh.ts # ❌ 삭제 예정 -└── middleware.ts # 인증 체크 -``` - ---- - -## 📝 백엔드 준비 대기 상황 - -### 백엔드에서 준비 중인 사항 - -1. **세션 드라이버 Redis 설정** -2. **인증 가드 세션으로 변경** -3. **로그인 API 응답 변경**: - ```json - // 변경 전 - { - "access_token": "eyJhbG...", - "refresh_token": "eyJhbG...", - "token_type": "bearer" - } - - // 변경 후 - { - "message": "Login successful", - "user": {...}, - "tenant": {...} - } - // + Set-Cookie: laravel_session=abc123 - ``` -4. **CORS 설정**: `supports_credentials: true` -5. **세션 하이재킹 감지 미들웨어** -6. **`/api/v1/auth/check` 엔드포인트 추가** - ---- - -## 🛠️ 프론트엔드 변경 작업 - -### 1️⃣ 로그인 API 수정 - -**파일**: `src/app/api/auth/login/route.ts` - -**변경 사항**: -- ✅ `credentials: 'include'` 추가 -- ✅ 백엔드 세션 쿠키를 클라이언트로 전달 -- ❌ 토큰 저장 로직 제거 - -```typescript -// src/app/api/auth/login/route.ts -import { NextResponse } from 'next/server'; -import type { NextRequest } from 'next/server'; - -/** - * 🔵 세션 기반 로그인 프록시 - * - * 변경 사항: - * - 토큰 저장 로직 제거 - * - 백엔드 세션 쿠키를 클라이언트로 전달 - * - credentials: 'include' 추가 - */ - -interface BackendLoginResponse { - message: string; - user: { - id: number; - user_id: string; - name: string; - email: string; - phone: string; - }; - tenant: { - id: number; - company_name: string; - business_num: string; - tenant_st_code: string; - other_tenants: unknown[]; - }; - menus: Array<{ - id: number; - parent_id: number | null; - name: string; - url: string; - icon: string; - sort_order: number; - is_external: number; - external_url: string | null; - }>; - roles: Array<{ - id: number; - name: string; - description: string; - }>; -} - -export async function POST(request: NextRequest) { - try { - const body = await request.json(); - const { user_id, user_pwd } = body; - - if (!user_id || !user_pwd) { - return NextResponse.json( - { error: 'User ID and password are required' }, - { status: 400 } - ); - } - - // ✅ 백엔드 세션 기반 로그인 호출 - const backendResponse = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/login`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '', - }, - body: JSON.stringify({ user_id, user_pwd }), - credentials: 'include', // ✅ 세션 쿠키 수신 - }); - - if (!backendResponse.ok) { - let errorMessage = 'Authentication failed'; - - if (backendResponse.status === 422) { - errorMessage = 'Invalid credentials provided'; - } else if (backendResponse.status === 429) { - errorMessage = 'Too many login attempts. Please try again later'; - } else if (backendResponse.status >= 500) { - errorMessage = 'Service temporarily unavailable'; - } - - return NextResponse.json( - { error: errorMessage }, - { status: backendResponse.status === 422 ? 401 : backendResponse.status } - ); - } - - const data: BackendLoginResponse = await backendResponse.json(); - - // ✅ 백엔드 세션 쿠키를 클라이언트로 전달 - const sessionCookie = backendResponse.headers.get('set-cookie'); - - const response = NextResponse.json({ - message: data.message, - user: data.user, - tenant: data.tenant, - menus: data.menus, - roles: data.roles, - }, { status: 200 }); - - // ✅ 백엔드 세션 쿠키 전달 - if (sessionCookie) { - response.headers.set('Set-Cookie', sessionCookie); - } - - console.log('✅ Login successful - Session cookie set'); - return response; - - } catch (error) { - console.error('Login proxy error:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ); - } -} -``` - ---- - -### 2️⃣ 로그아웃 API 수정 - -**파일**: `src/app/api/auth/logout/route.ts` - -**변경 사항**: -- ✅ `credentials: 'include'` 추가 -- ✅ 세션 쿠키를 백엔드로 전달 -- ❌ 수동 쿠키 삭제 로직 제거 (백엔드가 처리) - -```typescript -// src/app/api/auth/logout/route.ts -import { NextResponse } from 'next/server'; -import type { NextRequest } from 'next/server'; - -/** - * 🔵 세션 기반 로그아웃 프록시 - * - * 변경 사항: - * - 백엔드에 세션 쿠키 전달하여 세션 파괴 - * - 수동 쿠키 삭제 로직 제거 - */ -export async function POST(request: NextRequest) { - try { - // ✅ 백엔드 로그아웃 호출 (세션 파괴) - const sessionCookie = request.headers.get('cookie'); - - await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/logout`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '', - 'Cookie': sessionCookie || '', - }, - credentials: 'include', // ✅ 세션 쿠키 포함 - }); - - console.log('✅ Logout complete - Session destroyed on backend'); - - return NextResponse.json( - { message: 'Logged out successfully' }, - { status: 200 } - ); - - } catch (error) { - console.error('Logout proxy error:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ); - } -} -``` - ---- - -### 3️⃣ 인증 체크 API 수정 - -**파일**: `src/app/api/auth/check/route.ts` - -**변경 사항**: -- ✅ `credentials: 'include'` 추가 -- ✅ 백엔드 `/api/v1/auth/check` 호출 -- ❌ 토큰 갱신 로직 제거 - -```typescript -// src/app/api/auth/check/route.ts -import { NextResponse } from 'next/server'; -import type { NextRequest } from 'next/server'; - -/** - * 🔵 세션 기반 인증 상태 확인 - * - * 변경 사항: - * - 백엔드 세션 검증 API 호출 - * - 토큰 갱신 로직 제거 (세션은 자동 연장) - */ -export async function GET(request: NextRequest) { - try { - const sessionCookie = request.headers.get('cookie'); - - if (!sessionCookie) { - return NextResponse.json( - { authenticated: false }, - { status: 200 } - ); - } - - // ✅ 백엔드 세션 검증 - const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/auth/check`, { - method: 'GET', - headers: { - 'Accept': 'application/json', - 'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '', - 'Cookie': sessionCookie, - }, - credentials: 'include', // ✅ 세션 쿠키 포함 - }); - - if (response.ok) { - const data = await response.json(); - return NextResponse.json( - { - authenticated: data.authenticated, - user: data.user || null - }, - { status: 200 } - ); - } - - return NextResponse.json( - { authenticated: false }, - { status: 200 } - ); - - } catch (error) { - console.error('Auth check error:', error); - return NextResponse.json( - { authenticated: false }, - { status: 200 } - ); - } -} -``` - ---- - -### 4️⃣ 미들웨어 수정 - -**파일**: `src/middleware.ts` - -**변경 사항**: -- ✅ 세션 쿠키 확인 (`laravel_session`) -- ❌ 토큰 쿠키 확인 제거 (`access_token`, `refresh_token`) - -```typescript -// src/middleware.ts (checkAuthentication 함수만) - -/** - * 인증 체크 함수 - * 세션 쿠키 기반으로 변경 - */ -function checkAuthentication(request: NextRequest): { - isAuthenticated: boolean; - authMode: 'session' | 'api-key' | null; -} { - // ✅ Laravel 세션 쿠키 확인 - const sessionCookie = request.cookies.get('laravel_session'); - if (sessionCookie && sessionCookie.value) { - return { isAuthenticated: true, authMode: 'session' }; - } - - // API Key (API 호출용) - const apiKey = request.headers.get('x-api-key'); - if (apiKey) { - return { isAuthenticated: true, authMode: 'api-key' }; - } - - return { isAuthenticated: false, authMode: null }; -} -``` - ---- - -### 5️⃣ 파일 삭제 - -**삭제할 파일**: -```bash -# ❌ 토큰 갱신 API (세션은 자동 연장) -rm src/app/api/auth/refresh/route.ts - -# ❌ 토큰 갱신 유틸리티 -rm src/lib/auth/token-refresh.ts -``` - ---- - -## 📋 변경 작업 체크리스트 - -### 필수 변경 - -- [ ] `src/app/api/auth/login/route.ts` - - [ ] `credentials: 'include'` 추가 - - [ ] 백엔드 세션 쿠키 전달 로직 추가 - - [ ] 토큰 저장 로직 제거 (151-174 라인) - -- [ ] `src/app/api/auth/logout/route.ts` - - [ ] `credentials: 'include'` 추가 - - [ ] 세션 쿠키를 백엔드로 전달 - - [ ] 수동 쿠키 삭제 로직 제거 (52-68 라인) - -- [ ] `src/app/api/auth/check/route.ts` - - [ ] `credentials: 'include'` 추가 - - [ ] 백엔드 `/api/v1/auth/check` 호출 - - [ ] 토큰 갱신 로직 제거 (51-102 라인) - -- [ ] `src/middleware.ts` - - [ ] `laravel_session` 쿠키 확인으로 변경 - - [ ] `access_token`, `refresh_token` 확인 제거 (132-136 라인) - -- [ ] 파일 삭제 - - [ ] `src/app/api/auth/refresh/route.ts` - - [ ] `src/lib/auth/token-refresh.ts` - -### 클라이언트 컴포넌트 확인 - -- [ ] 모든 `fetch()` 호출에 `credentials: 'include'` 추가 -- [ ] 토큰 관련 상태 관리 제거 (있다면) -- [ ] 로그인 후 리다이렉트 로직 확인 - ---- - -## 🧪 테스트 계획 - -### 백엔드 준비 완료 후 테스트 - -#### 1. 로그인 테스트 - -```typescript -// 브라우저 개발자 도구 → Network 탭 -fetch('/api/auth/login', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - user_id: 'test', - user_pwd: 'password' - }), - credentials: 'include' // ✅ 확인 -}); - -// 응답 확인: -// 1. Set-Cookie: laravel_session=abc123... -// 2. Response Body: { message: "Login successful", user: {...} } -``` - -#### 2. 세션 쿠키 확인 - -```javascript -// 브라우저 개발자 도구 → Application → Cookies -// laravel_session 쿠키 존재 확인 -document.cookie; // "laravel_session=abc123..." -``` - -#### 3. 인증 체크 테스트 - -```typescript -fetch('/api/auth/check', { - credentials: 'include' -}); - -// 응답: { authenticated: true, user: {...} } -``` - -#### 4. 로그아웃 테스트 - -```typescript -fetch('/api/auth/logout', { - method: 'POST', - credentials: 'include' -}); - -// 확인: -// 1. laravel_session 쿠키 삭제됨 -// 2. /api/auth/check 호출 시 authenticated: false -``` - -#### 5. 세션 하이재킹 감지 테스트 - -```bash -# 1. 로그인 (정상 IP) -# 2. 쿠키 복사 -# 3. VPN 또는 다른 네트워크에서 접근 시도 -# 4. 자동 차단 확인 (401 Unauthorized) -``` - ---- - -## 🚨 주의사항 - -### 1. CORS 에러 발생 시 - -**증상**: -``` -Access to fetch at 'http://api.example.com/api/v1/login' from origin 'http://localhost:3000' -has been blocked by CORS policy: The value of the 'Access-Control-Allow-Credentials' header -in the response is '' which must be 'true' when the request's credentials mode is 'include'. -``` - -**해결**: 백엔드 팀에 확인 요청 -- `config/cors.php`에서 `supports_credentials: true` 설정 -- `allowed_origins`에 프론트엔드 도메인 추가 -- 와일드카드 `*` 사용 불가 - -### 2. 쿠키가 전송되지 않는 경우 - -**원인**: -- `credentials: 'include'` 누락 -- HTTPS 환경에서 `Secure` 쿠키 설정 - -**확인**: -```typescript -// 모든 API 호출에 추가 -fetch(url, { - credentials: 'include' // ✅ 필수! -}); -``` - -### 3. 개발 환경 (localhost) - -**개발 환경에서는 HTTPS 없이도 작동**: -- 백엔드 `.env`: `SESSION_SECURE_COOKIE=false` -- 프로덕션에서는 반드시 `true` - -### 4. 세션 만료 시간 - -- 백엔드 설정: `SESSION_LIFETIME=120` (2시간) -- 사용자가 2시간 동안 활동 없으면 자동 로그아웃 -- 활동 중에는 자동 연장 - ---- - -## 🔄 마이그레이션 단계 - -### 단계 1: 백엔드 준비 (백엔드 팀) -- [ ] Redis 세션 드라이버 설정 -- [ ] 인증 가드 변경 -- [ ] CORS 설정 -- [ ] API 응답 변경 -- [ ] 테스트 완료 - -### 단계 2: 프론트엔드 변경 (현재 팀) -- [ ] 로그인 API 수정 -- [ ] 로그아웃 API 수정 -- [ ] 인증 체크 API 수정 -- [ ] 미들웨어 수정 -- [ ] 토큰 관련 파일 삭제 - -### 단계 3: 통합 테스트 -- [ ] 로그인/로그아웃 플로우 -- [ ] 세션 유지 확인 -- [ ] 세션 하이재킹 감지 -- [ ] 동시 로그인 제한 - -### 단계 4: 배포 -- [ ] 스테이징 환경 배포 -- [ ] 프로덕션 배포 -- [ ] 모니터링 - ---- - -## 📞 백엔드 팀 협업 포인트 - -### 확인 필요 사항 - -1. **세션 쿠키 이름**: `laravel_session` (확인 필요) -2. **CORS 도메인 화이트리스트**: 프론트엔드 도메인 추가 요청 -3. **세션 만료 시간**: 2시간 적절한지 확인 -4. **API 엔드포인트**: - - ✅ `/api/v1/login` (세션 생성) - - ✅ `/api/v1/logout` (세션 파괴) - - ✅ `/api/v1/auth/check` (세션 검증) - - ❌ `/api/v1/refresh` (삭제) - -### 배포 전 확인 - -- [ ] 백엔드 배포 완료 확인 -- [ ] API 응답 형식 변경 확인 -- [ ] CORS 설정 적용 확인 -- [ ] 세션 쿠키 전송 확인 - ---- - -## 📚 참고 자료 - -- [Next.js API Routes](https://nextjs.org/docs/api-routes/introduction) -- [MDN: Fetch API with credentials](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#sending_a_request_with_credentials_included) -- [MDN: HTTP Cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) - ---- - -**작성일**: 2025-11-12 -**작성자**: Claude Code -**버전**: 1.0 -**상태**: ⏳ 백엔드 준비 대기 중 \ No newline at end of file diff --git a/claudedocs/auth/[REF] session-migration-summary.md b/claudedocs/auth/[REF] session-migration-summary.md deleted file mode 100644 index ab9d8c5d..00000000 --- a/claudedocs/auth/[REF] session-migration-summary.md +++ /dev/null @@ -1,366 +0,0 @@ -# 세션 기반 인증 전환 - 프로젝트 요약 - -## 📌 프로젝트 개요 - -**목표**: JWT 토큰 기반 → 세션 기반 인증으로 전환하여 보안 강화 - -**작업 기간**: 2-3일 (백엔드 1-2일, 프론트엔드 1일) - -**상태**: ⏳ 백엔드 준비 중 → 프론트엔드 대기 - ---- - -## 🎯 전환 이유 (보안 강화) - -| 보안 항목 | JWT 토큰 (현재) | 세션 (전환 후) | -|----------|----------------|---------------| -| 로그아웃 효과 | 쿠키만 삭제, 토큰 유효 | 세션 파괴, 즉시 차단 ✅ | -| 토큰 탈취 시 | 만료까지 악용 가능 (2시간) | 즉시 무효화 가능 ✅ | -| 세션 하이재킹 감지 | 어려움 | 실시간 감지 (IP/UA) ✅ | -| 강제 로그아웃 | 불가능 | 관리자가 즉시 가능 ✅ | -| 동시 로그인 제한 | 어려움 | 1계정 1세션 강제 ✅ | - -**결론**: ERP 시스템의 민감한 업무 데이터 보호에 세션이 더 적합 - ---- - -## 📊 아키텍처 변경 - -### 현재 (JWT 토큰) - -``` -[클라이언트] --user_id/pwd--> [Next.js] --user_id/pwd--> [PHP 백엔드] - | | - | <--access_token------ | - | refresh_token | - | | -[쿠키: access_token] <---저장--- | | -[쿠키: refresh_token] | | -``` - -### 전환 후 (세션) - -``` -[클라이언트] --user_id/pwd--> [Next.js] --user_id/pwd--> [PHP 백엔드] - | | - | <--세션 생성 -------> [Redis] - | Session ID: abc123 | - | | -[쿠키: laravel_session=abc123]<-전달- | -``` - ---- - -## 🔄 작업 단계 - -### 단계 1: 백엔드 작업 (PHP/Laravel) ⏳ 진행 중 - -**담당**: 백엔드 팀 -**예상 기간**: 1-2일 - -#### 필수 작업 -- [ ] Redis 세션 드라이버 설정 (`.env`, `config/session.php`) -- [ ] 인증 가드 변경 (Sanctum → Session) -- [ ] 로그인 컨트롤러 수정 (토큰 제거, 세션 생성) -- [ ] 로그아웃 컨트롤러 수정 (세션 파괴) -- [ ] CORS 설정 (`supports_credentials: true`) -- [ ] 세션 하이재킹 감지 미들웨어 추가 -- [ ] `/api/v1/auth/check` 엔드포인트 추가 -- [ ] `/api/v1/refresh` 엔드포인트 삭제 - -#### 권장 작업 -- [ ] Rate Limiting 적용 -- [ ] 세션 활동 로그 -- [ ] 관리자 강제 로그아웃 기능 - -**📄 상세 가이드**: `SESSION_MIGRATION_BACKEND.md` - ---- - -### 단계 2: 프론트엔드 작업 (Next.js) ⏸️ 대기 중 - -**담당**: 프론트엔드 팀 -**예상 기간**: 1일 - -#### 필수 작업 -- [ ] `src/app/api/auth/login/route.ts` 수정 - - `credentials: 'include'` 추가 - - 백엔드 세션 쿠키 전달 - - 토큰 저장 로직 제거 - -- [ ] `src/app/api/auth/logout/route.ts` 수정 - - `credentials: 'include'` 추가 - - 세션 쿠키를 백엔드로 전달 - -- [ ] `src/app/api/auth/check/route.ts` 수정 - - 백엔드 세션 검증 API 호출 - - 토큰 갱신 로직 제거 - -- [ ] `src/middleware.ts` 수정 - - `laravel_session` 쿠키 확인 - - 토큰 쿠키 확인 제거 - -- [ ] 파일 삭제 - - `src/app/api/auth/refresh/route.ts` - - `src/lib/auth/token-refresh.ts` - -**📄 상세 가이드**: `SESSION_MIGRATION_FRONTEND.md` - ---- - -### 단계 3: 통합 테스트 - -**담당**: 양 팀 협업 -**예상 기간**: 0.5일 - -- [ ] 로그인 플로우 테스트 -- [ ] 로그아웃 즉시 차단 확인 -- [ ] 세션 유지 확인 (페이지 새로고침) -- [ ] 세션 하이재킹 감지 테스트 -- [ ] CORS 크로스 도메인 테스트 -- [ ] 동시 로그인 제한 테스트 - ---- - -## 📋 API 변경 사항 요약 - -### 로그인 API - -**엔드포인트**: `POST /api/v1/login` - -**요청**: 변경 없음 -```json -{ - "user_id": "test", - "user_pwd": "password" -} -``` - -**응답**: 토큰 제거 -```json -// ❌ 이전 -{ - "access_token": "eyJhbG...", - "refresh_token": "eyJhbG...", - "token_type": "bearer", - "expires_in": 7200, - "user": {...} -} - -// ✅ 이후 -{ - "message": "Login successful", - "user": {...}, - "tenant": {...}, - "menus": [...], - "roles": [...] -} -// + Set-Cookie: laravel_session=abc123... -``` - ---- - -### 로그아웃 API - -**엔드포인트**: `POST /api/v1/logout` - -**변경 사항**: -- 세션 쿠키를 받아 Redis에서 세션 삭제 -- 즉시 접근 차단 - ---- - -### 인증 체크 API (신규) - -**엔드포인트**: `GET /api/v1/auth/check` - -**응답**: -```json -{ - "authenticated": true, - "user": { - "id": 1, - "name": "홍길동", - "email": "hong@example.com" - } -} -``` - ---- - -### 토큰 갱신 API (삭제) - -**엔드포인트**: ~~`POST /api/v1/refresh`~~ ❌ 삭제 - -**이유**: 세션은 활동 시 자동 연장됨 - ---- - -## 🔐 보안 기능 - -### 1. 세션 하이재킹 자동 감지 - -```php -// 백엔드 미들웨어가 자동 감지 -if (session('ip_address') !== request()->ip()) { - // 세션 즉시 파괴 및 차단 - Auth::logout(); - session()->invalidate(); - return 401 Unauthorized; -} -``` - -### 2. 동시 로그인 제한 - -```php -// 로그인 시 다른 모든 세션 종료 -DB::table('sessions') - ->where('user_id', $userId) - ->where('id', '!=', session()->getId()) - ->delete(); -``` - -### 3. 관리자 강제 로그아웃 - -```php -// 관리자가 특정 사용자 세션 강제 종료 -DB::table('sessions') - ->where('user_id', $suspiciousUserId) - ->delete(); -``` - ---- - -## 🚨 주의사항 - -### 백엔드 - -1. **CORS 설정 필수** - ```php - 'supports_credentials' => true, - 'allowed_origins' => [ - 'http://localhost:3000', // 개발 - 'https://yourdomain.com', // 프로덕션 - ], - ``` - -2. **Redis 필수** - - 세션 저장소로 Redis 사용 - - Redis 장애 대비 클러스터 구성 권장 - -3. **HTTPS 필수 (프로덕션)** - ```bash - SESSION_SECURE_COOKIE=true - ``` - -### 프론트엔드 - -1. **credentials: 'include' 필수** - ```typescript - fetch(url, { - credentials: 'include' // 모든 API 호출에 추가 - }); - ``` - -2. **세션 쿠키 이름 확인** - - 백엔드: `laravel_session` - - 미들웨어에서 이 이름으로 확인 - ---- - -## 📞 팀 간 커뮤니케이션 - -### 백엔드 → 프론트엔드 알림 필요 - -- [ ] 백엔드 배포 완료 -- [ ] API 응답 형식 변경 완료 -- [ ] CORS 설정 적용 완료 -- [ ] 테스트 환경 준비 완료 - -### 프론트엔드 → 백엔드 요청 사항 - -- [ ] 프론트엔드 도메인을 CORS `allowed_origins`에 추가 - - 개발: `http://localhost:3000` - - 프로덕션: `https://app.yourdomain.com` - -- [ ] 세션 쿠키 이름 확인: `laravel_session` - ---- - -## 🧪 테스트 시나리오 - -### 시나리오 1: 정상 로그인/로그아웃 - -```bash -1. 로그인 → 세션 쿠키 생성 확인 -2. 인증 API 호출 → 정상 작동 확인 -3. 로그아웃 → 세션 쿠키 삭제 확인 -4. 인증 API 호출 → 401 Unauthorized 확인 -``` - -### 시나리오 2: 세션 하이재킹 감지 - -```bash -1. 로그인 (IP: A) -2. 세션 쿠키 복사 -3. 다른 IP(B)에서 같은 쿠키 사용 시도 -4. 자동 차단 확인 (401 Unauthorized) -``` - -### 시나리오 3: 동시 로그인 제한 - -```bash -1. 기기 A에서 로그인 -2. 기기 B에서 같은 계정 로그인 -3. 기기 A 세션 자동 종료 확인 -``` - ---- - -## 📅 일정 - -| 단계 | 담당 | 예상 기간 | 상태 | -|------|------|-----------|------| -| 백엔드 작업 | 백엔드 팀 | 1-2일 | ⏳ 진행 중 | -| 프론트엔드 작업 | 프론트엔드 팀 | 1일 | ⏸️ 대기 | -| 통합 테스트 | 양 팀 | 0.5일 | ⏸️ 대기 | -| 스테이징 배포 | DevOps | 0.5일 | ⏸️ 대기 | -| 프로덕션 배포 | DevOps | 협의 | ⏸️ 대기 | - ---- - -## 📚 문서 목록 - -1. **SESSION_MIGRATION_BACKEND.md** - 백엔드 상세 가이드 -2. **SESSION_MIGRATION_FRONTEND.md** - 프론트엔드 상세 가이드 -3. **SESSION_MIGRATION_SUMMARY.md** - 본 문서 (프로젝트 요약) - ---- - -## 🎯 완료 기준 - -### 백엔드 완료 조건 -- [ ] 세션 기반 인증 구현 완료 -- [ ] 세션 하이재킹 감지 작동 -- [ ] CORS 설정 완료 -- [ ] API 응답 형식 변경 완료 -- [ ] 단위 테스트 통과 - -### 프론트엔드 완료 조건 -- [ ] 토큰 관련 코드 제거 완료 -- [ ] 세션 쿠키 기반 인증 적용 -- [ ] 모든 API 호출에 `credentials: 'include'` 추가 -- [ ] 로그인/로그아웃 플로우 정상 작동 - -### 통합 테스트 완료 조건 -- [ ] 로그인/로그아웃 시나리오 통과 -- [ ] 세션 하이재킹 감지 작동 확인 -- [ ] 동시 로그인 제한 작동 확인 -- [ ] CORS 에러 없음 - ---- - -**작성일**: 2025-11-12 -**작성자**: Claude Code -**버전**: 1.0 -**상태**: ⏳ 백엔드 작업 진행 중 \ No newline at end of file diff --git a/claudedocs/auth/[REF] token-security-nextjs15-research.md b/claudedocs/auth/[REF] token-security-nextjs15-research.md deleted file mode 100644 index ee6d4cb8..00000000 --- a/claudedocs/auth/[REF] token-security-nextjs15-research.md +++ /dev/null @@ -1,1614 +0,0 @@ -# Token Storage Security Research: Next.js 15 + Laravel Backend -**Research Date:** 2025-11-07 -**Confidence Level:** High (85%) - ---- - -## Executive Summary - -Current implementation stores Bearer tokens in localStorage and syncs them to non-HttpOnly cookies, creating significant security vulnerabilities. This research identifies 5 frontend-implementable solutions ranging from quick fixes to architectural improvements, with a clear recommendation based on security, complexity, and Laravel Sanctum compatibility. - -**Key Finding:** Laravel Sanctum's recommended approach for SPAs is cookie-based session authentication, not token-based authentication. This architectural mismatch is the root cause of security issues. - ---- - -## 1. Security Risk Assessment: Current Implementation - -### Current Architecture -```javascript -// ❌ Current vulnerable implementation -localStorage.setItem('token', token); // XSS vulnerable -document.cookie = `user_token=${token}; path=/; max-age=604800; SameSite=Lax`; // JS accessible -``` - -### Critical Vulnerabilities - -#### 🔴 HIGH RISK: XSS Token Exposure -- **localStorage Vulnerability:** Any JavaScript executing on the page can access localStorage -- **Attack Vector:** Reflective XSS, Stored XSS, DOM-based XSS, third-party script compromise -- **Impact:** Complete session hijacking, account takeover, data exfiltration -- **NIST Recommendation:** NIST 800-63B explicitly recommends NOT using HTML5 Local Storage for session secrets - -#### 🔴 HIGH RISK: Non-HttpOnly Cookie Exposure -- **JavaScript Access:** `document.cookie` allows reading the token from any script -- **Attack Vector:** XSS attacks can steal the cookie value directly -- **Impact:** Token theft, session replay attacks -- **OWASP Position:** HttpOnly cookies are fundamental XSS protection - -#### 🟡 MEDIUM RISK: CSRF Protection Gaps -- **Current SameSite=Lax:** Provides partial CSRF protection -- **Vulnerability Window:** Chrome has a 2-minute window where POST requests bypass Lax restrictions (SSO compatibility) -- **GET Request Risk:** SameSite=Lax doesn't protect GET requests that perform state changes -- **Cross-Origin Same-Site:** SameSite is powerless against same-site but cross-origin attacks - -#### 🟡 MEDIUM RISK: Long-Lived Tokens -- **max-age=604800 (7 days):** Extended exposure window if token is compromised -- **No Rotation:** Compromised tokens remain valid for entire duration -- **Impact:** Prolonged unauthorized access after breach - -### Risk Severity Matrix - -| Vulnerability | Likelihood | Impact | Severity | CVSS Score | -|---------------|------------|---------|----------|------------| -| XSS → localStorage theft | High | Critical | 🔴 Critical | 8.6 | -| XSS → Non-HttpOnly cookie theft | High | Critical | 🔴 Critical | 8.6 | -| CSRF (2-min window) | Medium | High | 🟡 High | 6.5 | -| Token replay (long-lived) | Medium | High | 🟡 High | 6.8 | -| **Overall Risk Score** | - | - | 🔴 **Critical** | **7.6** | - -### Real-World Attack Scenario - -```javascript -// Attacker injects malicious script via XSS vulnerability - -``` - -**Attack Success Rate:** 100% if XSS vulnerability exists -**User Detection:** Nearly impossible without security monitoring -**Recovery Complexity:** High (requires password reset, token revocation) - ---- - -## 2. Laravel Sanctum Architectural Context - -### Sanctum's Dual Authentication Model - -Laravel Sanctum supports **two distinct authentication patterns**: - -#### Pattern A: SPA Authentication (Cookie-Based) ✅ Recommended -- **Token Type:** Session cookies (Laravel's built-in session system) -- **Security:** HttpOnly, Secure, SameSite cookies -- **CSRF Protection:** Built-in via `/sanctum/csrf-cookie` endpoint -- **Use Case:** First-party SPAs on same top-level domain -- **XSS Protection:** Yes (HttpOnly prevents JavaScript access) - -#### Pattern B: API Token Authentication (Bearer Tokens) ⚠️ Not for SPAs -- **Token Type:** Long-lived personal access tokens -- **Security:** Must be stored by client (localStorage/cookie decision) -- **CSRF Protection:** Not needed (no cookies) -- **Use Case:** Mobile apps, third-party integrations, CLI tools -- **XSS Protection:** No (tokens must be accessible to JavaScript) - -### Current Implementation Analysis - -Your current implementation attempts to use **Pattern B (API tokens)** with an **SPA architecture**, which is the root cause of security issues: - -``` -❌ Current: API Token Pattern for SPA - Laravel → Generates Bearer token → Next.js stores in localStorage - Problem: XSS vulnerable, not Sanctum's recommended approach - -✅ Sanctum Recommended: Cookie-Based Session for SPA - Laravel → Issues session cookie → Next.js uses automatic cookie transmission - Benefit: HttpOnly protection, built-in CSRF, XSS resistant -``` - -### Key Quote from Laravel Sanctum Documentation - -> "For SPA authentication, Sanctum does not use tokens of any kind. Instead, Sanctum uses Laravel's built-in cookie based session authentication services." - -> "When your Laravel backend and single-page application (SPA) are on the same top-level domain, cookie-based session authentication is the optimal choice." - ---- - -## 3. Five Frontend-Implementable Solutions - -### Solution 1: Quick Fix - HttpOnly Cookies with Route Handler Proxy -**Complexity:** Low | **Security Improvement:** High | **Implementation Time:** 2-4 hours - -#### Architecture -``` -Next.js Client → Next.js Route Handler → Laravel API - ↓ (HttpOnly cookie) - Client (cookie auto-sent) -``` - -#### Implementation - -**Step 1: Create Login Route Handler** -```typescript -// app/api/auth/login/route.ts -import { NextRequest, NextResponse } from 'next/server'; -import { cookies } from 'next/headers'; - -export async function POST(request: NextRequest) { - const { email, password } = await request.json(); - - // Call Laravel login endpoint - const response = await fetch(`${process.env.LARAVEL_API_URL}/api/login`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email, password }) - }); - - const data = await response.json(); - - if (response.ok && data.token) { - // Store token in HttpOnly cookie (server-side only) - const cookieStore = await cookies(); - cookieStore.set('auth_token', data.token, { - httpOnly: true, // ✅ Prevents JavaScript access - secure: process.env.NODE_ENV === 'production', // ✅ HTTPS only in production - sameSite: 'lax', // ✅ CSRF protection - maxAge: 60 * 60 * 24 * 7, // 7 days - path: '/' - }); - - // Return user data (NOT token) - return NextResponse.json({ - user: data.user, - success: true - }); - } - - return NextResponse.json( - { error: 'Invalid credentials' }, - { status: 401 } - ); -} -``` - -**Step 2: Create API Proxy Route Handler** -```typescript -// app/api/proxy/[...path]/route.ts -import { NextRequest, NextResponse } from 'next/server'; -import { cookies } from 'next/headers'; - -export async function GET( - request: NextRequest, - { params }: { params: { path: string[] } } -) { - return proxyRequest(request, params.path, 'GET'); -} - -export async function POST(request: NextRequest, { params }: { params: { path: string[] } }) { - return proxyRequest(request, params.path, 'POST'); -} - -// Add PUT, DELETE, PATCH as needed - -async function proxyRequest( - request: NextRequest, - path: string[], - method: string -) { - const cookieStore = await cookies(); - const token = cookieStore.get('auth_token')?.value; - - if (!token) { - return NextResponse.json( - { error: 'Unauthorized' }, - { status: 401 } - ); - } - - const apiPath = path.join('/'); - const url = `${process.env.LARAVEL_API_URL}/api/${apiPath}`; - - // Forward request to Laravel with Bearer token - const response = await fetch(url, { - method, - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - ...Object.fromEntries(request.headers) - }, - body: method !== 'GET' ? await request.text() : undefined - }); - - const data = await response.json(); - return NextResponse.json(data, { status: response.status }); -} -``` - -**Step 3: Update Client-Side API Calls** -```typescript -// lib/api.ts - Before (❌ Vulnerable) -const response = await fetch(`${LARAVEL_API_URL}/api/users`, { - headers: { - 'Authorization': `Bearer ${localStorage.getItem('token')}` // ❌ XSS vulnerable - } -}); - -// After (✅ Secure) -const response = await fetch('/api/proxy/users'); // ✅ Cookie auto-sent -``` - -**Step 4: Middleware Protection** -```typescript -// middleware.ts -import { NextRequest, NextResponse } from 'next/server'; - -export function middleware(request: NextRequest) { - const token = request.cookies.get('auth_token'); - - // Protect routes - if (!token && request.nextUrl.pathname.startsWith('/dashboard')) { - return NextResponse.redirect(new URL('/login', request.url)); - } - - return NextResponse.next(); -} - -export const config = { - matcher: ['/dashboard/:path*', '/profile/:path*'] -}; -``` - -#### Pros -- ✅ Eliminates localStorage XSS vulnerability -- ✅ HttpOnly cookies prevent JavaScript token access -- ✅ Simple migration path (incremental adoption) -- ✅ Works with existing Laravel Bearer token system -- ✅ SameSite=Lax provides CSRF protection -- ✅ Minimal Laravel backend changes - -#### Cons -- ⚠️ Extra network hop (Next.js → Laravel) -- ⚠️ Slight latency increase (typically 10-50ms) -- ⚠️ Not using Sanctum's recommended cookie-based sessions -- ⚠️ Still requires token management on Next.js server -- ⚠️ Duplicate API routes for proxying - -#### When to Use -- Quick security improvement needed -- Can't modify Laravel backend immediately -- Existing Bearer token system must be preserved -- Team familiar with Route Handlers - ---- - -### Solution 2: Sanctum Cookie-Based Sessions (Recommended) -**Complexity:** Medium | **Security Improvement:** Excellent | **Implementation Time:** 1-2 days - -#### Architecture -``` -Next.js Client → Laravel Sanctum (Session Cookies) - ↓ (HttpOnly session cookie + CSRF token) - Client (automatic cookie transmission) -``` - -This is **Laravel Sanctum's officially recommended pattern for SPAs**. - -#### Implementation - -**Step 1: Configure Laravel Sanctum for SPA** -```php -// config/sanctum.php -'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( - '%s%s', - 'localhost,localhost:3000,127.0.0.1,127.0.0.1:3000,::1', - env('APP_URL') ? ','.parse_url(env('APP_URL'), PHP_URL_HOST) : '' -))), - -'middleware' => [ - 'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class, - 'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class, -], -``` - -```env -# .env -SESSION_DRIVER=cookie -SESSION_LIFETIME=120 -SESSION_DOMAIN=localhost # or .yourdomain.com for subdomains -SANCTUM_STATEFUL_DOMAINS=localhost:3000,yourdomain.com -``` - -**Step 2: Laravel CORS Configuration** -```php -// config/cors.php -return [ - 'paths' => ['api/*', 'sanctum/csrf-cookie'], - 'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:3000')], - 'allowed_methods' => ['*'], - 'allowed_headers' => ['*'], - 'exposed_headers' => [], - 'max_age' => 0, - 'supports_credentials' => true, // ✅ Critical for cookies -]; -``` - -**Step 3: Create Next.js Login Flow** -```typescript -// app/actions/auth.ts (Server Action) -'use server'; - -import { cookies } from 'next/headers'; -import { redirect } from 'next/navigation'; - -const LARAVEL_API = process.env.LARAVEL_API_URL!; -const FRONTEND_URL = process.env.NEXT_PUBLIC_FRONTEND_URL!; - -export async function login(formData: FormData) { - const email = formData.get('email') as string; - const password = formData.get('password') as string; - - try { - // Step 1: Get CSRF cookie from Laravel - await fetch(`${LARAVEL_API}/sanctum/csrf-cookie`, { - method: 'GET', - credentials: 'include', // ✅ Include cookies - }); - - // Step 2: Attempt login - const response = await fetch(`${LARAVEL_API}/login`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Referer': FRONTEND_URL, - }, - credentials: 'include', // ✅ Include cookies - body: JSON.stringify({ email, password }), - }); - - if (!response.ok) { - return { error: 'Invalid credentials' }; - } - - const data = await response.json(); - - // Step 3: Session cookie is automatically set by Laravel - // No manual token storage needed! - - } catch (error) { - return { error: 'Login failed' }; - } - - redirect('/dashboard'); -} - -export async function logout() { - await fetch(`${LARAVEL_API}/logout`, { - method: 'POST', - credentials: 'include', - }); - - redirect('/login'); -} -``` - -**Step 4: Client Component with Server Action** -```typescript -// app/login/page.tsx -'use client'; - -import { login } from '@/app/actions/auth'; -import { useFormStatus } from 'react-dom'; - -function SubmitButton() { - const { pending } = useFormStatus(); - return ( - - ); -} - -export default function LoginPage() { - return ( -
- - - - - ); -} -``` - -**Step 5: API Route Handler for Client Components** -```typescript -// app/api/users/route.ts -import { NextRequest, NextResponse } from 'next/server'; - -export async function GET(request: NextRequest) { - const response = await fetch(`${process.env.LARAVEL_API_URL}/api/users`, { - method: 'GET', - headers: { - 'Accept': 'application/json', - 'Cookie': request.headers.get('cookie') || '', // ✅ Forward session cookie - }, - credentials: 'include', - }); - - const data = await response.json(); - return NextResponse.json(data, { status: response.status }); -} -``` - -**Step 6: Middleware for Protected Routes** -```typescript -// middleware.ts -import { NextRequest, NextResponse } from 'next/server'; - -export async function middleware(request: NextRequest) { - const sessionCookie = request.cookies.get('laravel_session'); - - if (!sessionCookie) { - return NextResponse.redirect(new URL('/login', request.url)); - } - - // Verify session with Laravel - const response = await fetch(`${process.env.LARAVEL_API_URL}/api/user`, { - headers: { - 'Cookie': request.headers.get('cookie') || '', - }, - credentials: 'include', - }); - - if (!response.ok) { - return NextResponse.redirect(new URL('/login', request.url)); - } - - return NextResponse.next(); -} - -export const config = { - matcher: ['/dashboard/:path*', '/profile/:path*'] -}; -``` - -**Step 7: Next.js Configuration** -```javascript -// next.config.js -module.exports = { - async rewrites() { - return [ - { - source: '/api/laravel/:path*', - destination: `${process.env.LARAVEL_API_URL}/api/:path*`, - }, - ]; - }, -}; -``` - -#### Pros -- ✅ **Sanctum's officially recommended pattern** -- ✅ HttpOnly, Secure, SameSite cookies (best-in-class security) -- ✅ Built-in CSRF protection via `/sanctum/csrf-cookie` -- ✅ No token management needed (Laravel handles everything) -- ✅ Automatic cookie transmission (no manual headers) -- ✅ Session-based (no long-lived tokens) -- ✅ XSS resistant (cookies inaccessible to JavaScript) -- ✅ Supports subdomain authentication (`.yourdomain.com`) - -#### Cons -- ⚠️ Requires Laravel backend configuration changes -- ⚠️ Must be on same top-level domain (or subdomain) -- ⚠️ CORS configuration complexity -- ⚠️ Session state on backend (not stateless) -- ⚠️ Credential forwarding required for proxied requests - -#### When to Use -- ✅ **First-party SPA on same/subdomain** (your case) -- ✅ Can modify Laravel backend -- ✅ Want Sanctum's recommended security pattern -- ✅ Long-term production solution needed -- ✅ Team willing to learn cookie-based sessions - ---- - -### Solution 3: Token Encryption in Storage (Defense in Depth) -**Complexity:** Low-Medium | **Security Improvement:** Medium | **Implementation Time:** 4-6 hours - -#### Architecture -``` -Laravel → Encrypted Token → localStorage (encrypted) → Decrypt on use → API -``` - -This is a **defense-in-depth approach** that adds a layer of protection without architectural changes. - -#### Implementation - -**Step 1: Create Encryption Utility** -```typescript -// lib/crypto.ts -import { AES, enc } from 'crypto-js'; - -// Generate encryption key from environment -const ENCRYPTION_KEY = process.env.NEXT_PUBLIC_ENCRYPTION_KEY || generateKey(); - -function generateKey(): string { - // In production, use a proper secret management system - if (typeof window === 'undefined') { - throw new Error('NEXT_PUBLIC_ENCRYPTION_KEY must be set'); - } - return window.crypto.randomUUID(); -} - -export function encryptToken(token: string): string { - return AES.encrypt(token, ENCRYPTION_KEY).toString(); -} - -export function decryptToken(encryptedToken: string): string { - const bytes = AES.decrypt(encryptedToken, ENCRYPTION_KEY); - return bytes.toString(enc.Utf8); -} - -// Clear tokens on encryption key rotation -export function clearAuthData() { - localStorage.removeItem('enc_token'); - document.cookie = 'auth_status=; max-age=0; path=/'; -} -``` - -**Step 2: Update Login Flow** -```typescript -// lib/auth.ts -import { encryptToken, decryptToken } from './crypto'; - -export async function login(email: string, password: string) { - const response = await fetch(`${LARAVEL_API_URL}/api/login`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email, password }) - }); - - const data = await response.json(); - - if (response.ok && data.token) { - // Encrypt token before storage - const encryptedToken = encryptToken(data.token); - localStorage.setItem('enc_token', encryptedToken); - - // Set HttpOnly-capable status cookie (no token) - document.cookie = `auth_status=authenticated; path=/; max-age=604800; SameSite=Strict`; - - return { success: true, user: data.user }; - } - - return { success: false, error: 'Invalid credentials' }; -} - -export function getAuthToken(): string | null { - const encrypted = localStorage.getItem('enc_token'); - if (!encrypted) return null; - - try { - return decryptToken(encrypted); - } catch { - // Token corruption or key change - clearAuthData(); - return null; - } -} -``` - -**Step 3: Create Secure API Client** -```typescript -// lib/api-client.ts -import { getAuthToken } from './auth'; - -export async function apiRequest(endpoint: string, options: RequestInit = {}) { - const token = getAuthToken(); - - if (!token) { - throw new Error('No authentication token'); - } - - const response = await fetch(`${LARAVEL_API_URL}/api/${endpoint}`, { - ...options, - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - ...options.headers, - }, - }); - - if (response.status === 401) { - // Token expired or invalid - clearAuthData(); - window.location.href = '/login'; - } - - return response; -} -``` - -**Step 4: Add Content Security Policy** -```typescript -// middleware.ts -import { NextResponse } from 'next/server'; -import type { NextRequest } from 'next/server'; - -export function middleware(request: NextRequest) { - const response = NextResponse.next(); - - // Add strict CSP to mitigate XSS - response.headers.set( - 'Content-Security-Policy', - [ - "default-src 'self'", - "script-src 'self' 'unsafe-inline' 'unsafe-eval'", // Adjust based on needs - "style-src 'self' 'unsafe-inline'", - "img-src 'self' data: https:", - "font-src 'self' data:", - "connect-src 'self' " + process.env.LARAVEL_API_URL, - "frame-ancestors 'none'", - "base-uri 'self'", - "form-action 'self'", - ].join('; ') - ); - - // Additional security headers - response.headers.set('X-Frame-Options', 'DENY'); - response.headers.set('X-Content-Type-Options', 'nosniff'); - response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin'); - - return response; -} -``` - -**Step 5: Token Rotation Strategy** -```typescript -// lib/token-rotation.ts -import { apiRequest } from './api-client'; -import { encryptToken } from './crypto'; - -export async function refreshToken(): Promise { - try { - const response = await apiRequest('auth/refresh', { - method: 'POST' - }); - - const data = await response.json(); - - if (data.token) { - const encryptedToken = encryptToken(data.token); - localStorage.setItem('enc_token', encryptedToken); - return true; - } - } catch { - return false; - } - - return false; -} - -// Call periodically (e.g., every 30 minutes) -export function startTokenRotation() { - setInterval(async () => { - await refreshToken(); - }, 30 * 60 * 1000); -} -``` - -#### Pros -- ✅ Adds encryption layer without architectural changes -- ✅ Minimal code changes (incremental adoption) -- ✅ Defense-in-depth approach -- ✅ Works with existing Bearer token system -- ✅ No Laravel backend changes required -- ✅ Can combine with other solutions - -#### Cons -- ⚠️ **Still vulnerable to XSS** (encryption key accessible to JavaScript) -- ⚠️ False sense of security (encryption ≠ protection from XSS) -- ⚠️ Additional complexity (encryption/decryption overhead) -- ⚠️ Key management challenges (rotation, storage) -- ⚠️ Performance impact (crypto operations) -- ⚠️ Not a substitute for HttpOnly cookies - -#### When to Use -- ⚠️ **Only as defense-in-depth** alongside other solutions -- ⚠️ Cannot implement HttpOnly cookies immediately -- ⚠️ Need incremental security improvements -- ⚠️ Compliance requirement for data-at-rest encryption - -#### Security Warning -**This is NOT a primary security solution.** If an attacker can execute JavaScript (XSS), they can: -1. Access the encryption key (hardcoded or in environment) -2. Decrypt the token -3. Steal the plaintext token - -Use this **only as an additional layer**, not as the main security mechanism. - ---- - -### Solution 4: BFF (Backend for Frontend) Pattern -**Complexity:** High | **Security Improvement:** Excellent | **Implementation Time:** 3-5 days - -#### Architecture -``` -Next.js Client → Next.js BFF Server → Laravel API - ↓ (HttpOnly session cookie) - Client (no tokens) -``` - -The BFF acts as a secure proxy and token manager, keeping all tokens server-side. - -#### Implementation - -**Step 1: Create BFF Session Management** -```typescript -// lib/bff/session.ts -import { SignJWT, jwtVerify } from 'jose'; -import { cookies } from 'next/headers'; - -const SECRET = new TextEncoder().encode(process.env.SESSION_SECRET!); - -export interface SessionData { - userId: string; - laravelToken: string; // Stored server-side only - expiresAt: number; -} - -export async function createSession(data: SessionData): Promise { - const token = await new SignJWT({ userId: data.userId }) - .setProtectedHeader({ alg: 'HS256' }) - .setExpirationTime('7d') - .setIssuedAt() - .sign(SECRET); - - const cookieStore = await cookies(); - cookieStore.set('session', token, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'strict', - maxAge: 60 * 60 * 24 * 7, - path: '/', - }); - - // Store Laravel token in Redis/database (not in JWT) - await storeTokenInRedis(data.userId, data.laravelToken, data.expiresAt); - - return token; -} - -export async function getSession(): Promise { - const cookieStore = await cookies(); - const token = cookieStore.get('session')?.value; - - if (!token) return null; - - try { - const { payload } = await jwtVerify(token, SECRET); - const userId = payload.userId as string; - - // Retrieve Laravel token from Redis - const laravelToken = await getTokenFromRedis(userId); - - if (!laravelToken) return null; - - return { - userId, - laravelToken, - expiresAt: payload.exp! * 1000, - }; - } catch { - return null; - } -} - -// Redis token storage (example with ioredis) -import Redis from 'ioredis'; -const redis = new Redis(process.env.REDIS_URL!); - -async function storeTokenInRedis(userId: string, token: string, expiresAt: number) { - const ttl = Math.floor((expiresAt - Date.now()) / 1000); - await redis.setex(`token:${userId}`, ttl, token); -} - -async function getTokenFromRedis(userId: string): Promise { - return await redis.get(`token:${userId}`); -} -``` - -**Step 2: Create BFF Login Endpoint** -```typescript -// app/api/bff/auth/login/route.ts -import { NextRequest, NextResponse } from 'next/server'; -import { createSession } from '@/lib/bff/session'; - -export async function POST(request: NextRequest) { - const { email, password } = await request.json(); - - // Authenticate with Laravel - const response = await fetch(`${process.env.LARAVEL_API_URL}/api/login`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email, password }) - }); - - const data = await response.json(); - - if (response.ok && data.token) { - // Create BFF session (Laravel token stored server-side) - await createSession({ - userId: data.user.id, - laravelToken: data.token, - expiresAt: Date.now() + (7 * 24 * 60 * 60 * 1000), - }); - - // Return user data only (no tokens) - return NextResponse.json({ - user: data.user, - success: true - }); - } - - return NextResponse.json( - { error: 'Invalid credentials' }, - { status: 401 } - ); -} -``` - -**Step 3: Create BFF API Proxy** -```typescript -// app/api/bff/proxy/[...path]/route.ts -import { NextRequest, NextResponse } from 'next/server'; -import { getSession } from '@/lib/bff/session'; - -export async function GET( - request: NextRequest, - { params }: { params: { path: string[] } } -) { - return proxyRequest(request, params.path, 'GET'); -} - -export async function POST(request: NextRequest, { params }: { params: { path: string[] } }) { - return proxyRequest(request, params.path, 'POST'); -} - -async function proxyRequest( - request: NextRequest, - path: string[], - method: string -) { - // Get session (retrieves Laravel token from Redis) - const session = await getSession(); - - if (!session) { - return NextResponse.json( - { error: 'Unauthorized' }, - { status: 401 } - ); - } - - const apiPath = path.join('/'); - const url = `${process.env.LARAVEL_API_URL}/api/${apiPath}`; - - // Forward request with Laravel token (token never reaches client) - const response = await fetch(url, { - method, - headers: { - 'Authorization': `Bearer ${session.laravelToken}`, - 'Content-Type': 'application/json', - }, - body: method !== 'GET' ? await request.text() : undefined - }); - - const data = await response.json(); - return NextResponse.json(data, { status: response.status }); -} -``` - -**Step 4: Client-Side API Calls** -```typescript -// lib/api.ts -export async function apiCall(endpoint: string, options: RequestInit = {}) { - // All calls go through BFF (no token management on client) - const response = await fetch(`/api/bff/proxy/${endpoint}`, options); - - if (response.status === 401) { - // Session expired - window.location.href = '/login'; - } - - return response; -} -``` - -**Step 5: Middleware Protection** -```typescript -// middleware.ts -import { NextRequest, NextResponse } from 'next/server'; -import { getSession } from '@/lib/bff/session'; - -export async function middleware(request: NextRequest) { - const session = await getSession(); - - if (!session && request.nextUrl.pathname.startsWith('/dashboard')) { - return NextResponse.redirect(new URL('/login', request.url)); - } - - return NextResponse.next(); -} - -export const config = { - matcher: ['/dashboard/:path*', '/profile/:path*'] -}; -``` - -**Step 6: Add Token Refresh Logic** -```typescript -// lib/bff/refresh.ts -import { getSession, createSession } from './session'; - -export async function refreshLaravelToken(): Promise { - const session = await getSession(); - - if (!session) return false; - - // Call Laravel token refresh endpoint - const response = await fetch(`${process.env.LARAVEL_API_URL}/api/auth/refresh`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${session.laravelToken}`, - }, - }); - - if (response.ok) { - const data = await response.json(); - - // Update stored token - await createSession({ - userId: session.userId, - laravelToken: data.token, - expiresAt: Date.now() + (7 * 24 * 60 * 60 * 1000), - }); - - return true; - } - - return false; -} -``` - -#### Pros -- ✅ **Maximum security** - tokens never reach client -- ✅ HttpOnly session cookies (XSS resistant) -- ✅ Centralized token management (BFF controls all tokens) -- ✅ Token rotation without client awareness -- ✅ Single authentication boundary (BFF) -- ✅ Easy to add additional security layers (rate limiting, fraud detection) -- ✅ Clean separation of concerns - -#### Cons -- ⚠️ High complexity (new architecture layer) -- ⚠️ Requires infrastructure (Redis/database for token storage) -- ⚠️ Additional latency (Next.js → BFF → Laravel) -- ⚠️ Increased operational overhead (BFF maintenance) -- ⚠️ Session state management complexity -- ⚠️ Not suitable for serverless (requires stateful backend) - -#### When to Use -- ✅ Enterprise applications with high security requirements -- ✅ Team has resources for complex architecture -- ✅ Need centralized token management -- ✅ Multiple clients (web + mobile) sharing backend -- ✅ Microservices architecture - ---- - -### Solution 5: Hybrid Approach (Sanctum Sessions + Short-Lived Access Tokens) -**Complexity:** Medium-High | **Security Improvement:** Excellent | **Implementation Time:** 2-3 days - -#### Architecture -``` -Next.js → Laravel Sanctum Session Cookie → Short-lived access token → API - (HttpOnly, long-lived) (in-memory, 15min TTL) -``` - -Combines session security with token flexibility. - -#### Implementation - -**Step 1: Laravel Token Issuance Endpoint** -```php -// Laravel: routes/api.php -Route::middleware('auth:sanctum')->group(function () { - Route::post('/token/issue', function (Request $request) { - $user = $request->user(); - - // Issue short-lived personal access token - $token = $user->createToken('access', ['*'], now()->addMinutes(15)); - - return response()->json([ - 'token' => $token->plainTextToken, - 'expires_at' => now()->addMinutes(15)->timestamp, - ]); - }); -}); -``` - -**Step 2: Next.js Token Management Hook** -```typescript -// hooks/useAccessToken.ts -import { useState, useEffect, useCallback } from 'react'; - -interface TokenData { - token: string; - expiresAt: number; -} - -let tokenCache: TokenData | null = null; // In-memory only - -export function useAccessToken() { - const [token, setToken] = useState(null); - - const refreshToken = useCallback(async () => { - // Check cache first - if (tokenCache && tokenCache.expiresAt > Date.now() + 60000) { - setToken(tokenCache.token); - return tokenCache.token; - } - - try { - // Request new token using Sanctum session - const response = await fetch('/api/token/issue', { - method: 'POST', - credentials: 'include', // Send session cookie - }); - - if (response.ok) { - const data = await response.json(); - - // Store in memory only (never localStorage) - tokenCache = { - token: data.token, - expiresAt: data.expires_at * 1000, - }; - - setToken(data.token); - return data.token; - } - } catch (error) { - console.error('Token refresh failed', error); - } - - return null; - }, []); - - useEffect(() => { - refreshToken(); - - // Auto-refresh every 10 minutes (before 15min expiry) - const interval = setInterval(refreshToken, 10 * 60 * 1000); - - return () => clearInterval(interval); - }, [refreshToken]); - - return { token, refreshToken }; -} -``` - -**Step 3: Secure API Client** -```typescript -// lib/api-client.ts -import { useAccessToken } from '@/hooks/useAccessToken'; - -export function useApiClient() { - const { token, refreshToken } = useAccessToken(); - - const apiCall = async (endpoint: string, options: RequestInit = {}) => { - if (!token) { - await refreshToken(); - } - - const response = await fetch(`${process.env.LARAVEL_API_URL}/api/${endpoint}`, { - ...options, - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - ...options.headers, - }, - }); - - // Handle token expiration - if (response.status === 401) { - const newToken = await refreshToken(); - - if (newToken) { - // Retry with new token - return fetch(`${process.env.LARAVEL_API_URL}/api/${endpoint}`, { - ...options, - headers: { - 'Authorization': `Bearer ${newToken}`, - 'Content-Type': 'application/json', - ...options.headers, - }, - }); - } - } - - return response; - }; - - return { apiCall }; -} -``` - -**Step 4: Login Flow (Sanctum Session)** -```typescript -// app/actions/auth.ts -'use server'; - -export async function login(formData: FormData) { - const email = formData.get('email') as string; - const password = formData.get('password') as string; - - // Get CSRF cookie - await fetch(`${process.env.LARAVEL_API_URL}/sanctum/csrf-cookie`, { - credentials: 'include', - }); - - // Login (creates Sanctum session) - const response = await fetch(`${process.env.LARAVEL_API_URL}/login`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'include', - body: JSON.stringify({ email, password }), - }); - - if (!response.ok) { - return { error: 'Invalid credentials' }; - } - - // Session cookie is set (HttpOnly) - // No tokens stored on client yet - - return { success: true }; -} -``` - -**Step 5: Next.js API Proxy for Token Issuance** -```typescript -// app/api/token/issue/route.ts -import { NextRequest, NextResponse } from 'next/server'; - -export async function POST(request: NextRequest) { - // Forward session cookie to Laravel - const response = await fetch(`${process.env.LARAVEL_API_URL}/api/token/issue`, { - method: 'POST', - headers: { - 'Cookie': request.headers.get('cookie') || '', - }, - credentials: 'include', - }); - - if (response.ok) { - const data = await response.json(); - return NextResponse.json(data); - } - - return NextResponse.json( - { error: 'Token issuance failed' }, - { status: response.status } - ); -} -``` - -#### Pros -- ✅ Long-lived session security (HttpOnly cookie) -- ✅ Short-lived token reduces exposure window (15min) -- ✅ In-memory tokens (never localStorage) -- ✅ Automatic token rotation -- ✅ Combines Sanctum sessions with API tokens -- ✅ Flexible for different API patterns - -#### Cons -- ⚠️ Complex token lifecycle management -- ⚠️ Requires both session and token authentication -- ⚠️ In-memory tokens lost on tab close/refresh -- ⚠️ Additional API calls for token issuance -- ⚠️ Backend must support both auth methods - -#### When to Use -- ✅ Need both session and token benefits -- ✅ High-security requirements -- ✅ Complex API authentication needs -- ✅ Team experienced with hybrid auth patterns - ---- - -## 4. Comparison Matrix - -| Solution | Security | Complexity | Laravel Changes | Implementation Time | Production Ready | Recommended | -|----------|----------|------------|-----------------|---------------------|------------------|-------------| -| **1. HttpOnly Proxy** | 🟢 High | 🟢 Low | None | 2-4 hours | ✅ Yes | 🟡 Quick Fix | -| **2. Sanctum Sessions** | 🟢 Excellent | 🟡 Medium | Moderate | 1-2 days | ✅ Yes | ✅ **Recommended** | -| **3. Token Encryption** | 🟡 Medium | 🟢 Low-Medium | None | 4-6 hours | ⚠️ Defense-in-Depth Only | ❌ Not Primary | -| **4. BFF Pattern** | 🟢 Excellent | 🔴 High | None | 3-5 days | ✅ Yes (w/ infra) | 🟡 Enterprise Only | -| **5. Hybrid Approach** | 🟢 Excellent | 🟡 Medium-High | Moderate | 2-3 days | ✅ Yes | 🟡 Advanced | - -### Security Risk Reduction - -| Solution | XSS Protection | CSRF Protection | Token Exposure | Overall Risk | -|----------|----------------|-----------------|----------------|--------------| -| **Current** | ❌ None | 🟡 Partial (SameSite) | 🔴 High | 🔴 **Critical (7.6)** | -| **1. HttpOnly Proxy** | ✅ Full | ✅ Full | 🟢 Low | 🟢 **Low (2.8)** | -| **2. Sanctum Sessions** | ✅ Full | ✅ Full (CSRF token) | 🟢 Minimal | 🟢 **Minimal (1.5)** | -| **3. Token Encryption** | ⚠️ Partial | 🟡 Partial | 🟡 Medium | 🟡 **Medium (5.2)** | -| **4. BFF Pattern** | ✅ Full | ✅ Full | 🟢 None (server-only) | 🟢 **Minimal (1.2)** | -| **5. Hybrid** | ✅ Full | ✅ Full | 🟢 Low (short-lived) | 🟢 **Low (2.0)** | - ---- - -## 5. Final Recommendation - -### Primary Recommendation: Solution 2 - Sanctum Cookie-Based Sessions - -**Rationale:** -1. **Laravel Sanctum's Official Pattern** - This is explicitly designed for your use case -2. **Best Security** - HttpOnly cookies + built-in CSRF protection + no token exposure -3. **Simplicity** - Leverages Laravel's built-in session system (no custom token management) -4. **Production-Ready** - Battle-tested pattern used by thousands of Laravel SPAs -5. **Maintainability** - Less code to maintain, framework handles security - -### Implementation Roadmap - -#### Phase 1: Preparation (Day 1) -1. Configure Laravel Sanctum for stateful authentication -2. Update CORS settings to support credentials -3. Test CSRF cookie endpoint -4. Configure session driver (database/redis recommended for production) - -#### Phase 2: Authentication Flow (Day 1-2) -1. Create Next.js Server Actions for login/logout -2. Implement CSRF cookie fetching -3. Update login UI to use Server Actions -4. Test authentication flow end-to-end - -#### Phase 3: API Integration (Day 2) -1. Create Next.js Route Handlers for API proxying -2. Update client-side API calls to use Route Handlers -3. Implement cookie forwarding in Route Handlers -4. Test protected API endpoints - -#### Phase 4: Middleware & Protection (Day 2) -1. Implement Next.js middleware for route protection -2. Add session verification with Laravel -3. Handle authentication redirects -4. Test protected routes - -#### Phase 5: Migration & Cleanup (Day 3) -1. Gradually migrate existing localStorage code -2. Remove localStorage token storage -3. Remove non-HttpOnly cookie code -4. Comprehensive testing (unit, integration, E2E) - -### Fallback Recommendation: Solution 1 - HttpOnly Proxy - -**If you cannot modify Laravel backend immediately:** -- Implement Solution 1 as an interim measure -- Migrate to Solution 2 when backend changes are possible -- Solution 1 provides 80% of the security benefit with minimal backend changes - -### Not Recommended: Solution 3 - Token Encryption - -**Why not:** -- Provides false sense of security -- Still fundamentally vulnerable to XSS -- Adds complexity without significant security benefit -- Should only be used as defense-in-depth alongside other solutions - ---- - -## 6. Additional Security Best Practices - -### 1. Content Security Policy (CSP) -```typescript -// next.config.js -module.exports = { - async headers() { - return [ - { - source: '/:path*', - headers: [ - { - key: 'Content-Security-Policy', - value: [ - "default-src 'self'", - "script-src 'self' 'strict-dynamic'", - "style-src 'self' 'unsafe-inline'", - "img-src 'self' data: https:", - "font-src 'self' data:", - "connect-src 'self' " + process.env.LARAVEL_API_URL, - "frame-ancestors 'none'", - "base-uri 'self'", - "form-action 'self'" - ].join('; ') - } - ] - } - ]; - } -}; -``` - -### 2. Security Headers -```typescript -// middleware.ts -export function middleware(request: NextRequest) { - const response = NextResponse.next(); - - response.headers.set('X-Frame-Options', 'DENY'); - response.headers.set('X-Content-Type-Options', 'nosniff'); - response.headers.set('X-XSS-Protection', '1; mode=block'); - response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin'); - response.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()'); - - return response; -} -``` - -### 3. Token Rotation -```php -// Laravel: Automatic token rotation -Route::middleware('auth:sanctum')->get('/user', function (Request $request) { - // Rotate session ID periodically - $request->session()->regenerate(); - - return $request->user(); -}); -``` - -### 4. Rate Limiting -```php -// Laravel: config/sanctum.php -'middleware' => [ - 'throttle:api', // Add rate limiting - 'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class, -]; -``` - -### 5. Monitoring & Alerting -```typescript -// Monitor authentication anomalies -export async function logAuthEvent(event: string, metadata: any) { - await fetch('/api/security/log', { - method: 'POST', - body: JSON.stringify({ - event, - metadata, - timestamp: Date.now(), - userAgent: navigator.userAgent, - }) - }); -} - -// Call on suspicious activities -logAuthEvent('multiple_login_failures', { email }); -logAuthEvent('session_hijacking_detected', { oldIp, newIp }); -``` - ---- - -## 7. Migration Checklist - -### Pre-Migration -- [ ] Audit current authentication flows -- [ ] Identify all API endpoints using Bearer tokens -- [ ] Document current user sessions and states -- [ ] Backup authentication configuration -- [ ] Set up staging environment for testing - -### During Migration -- [ ] Implement new authentication pattern -- [ ] Update all API calls to use new method -- [ ] Test authentication flows (login, logout, session timeout) -- [ ] Test protected routes and middleware -- [ ] Verify CSRF protection is working -- [ ] Load test authentication endpoints -- [ ] Security audit of new implementation - -### Post-Migration -- [ ] Remove localStorage token storage code -- [ ] Remove non-HttpOnly cookie code -- [ ] Update documentation for developers -- [ ] Monitor error rates and authentication metrics -- [ ] Force logout all existing sessions (optional) -- [ ] Communicate changes to users if needed - -### Rollback Plan -- [ ] Keep old authentication code commented (not deleted) for 1 sprint -- [ ] Maintain backward compatibility during transition period -- [ ] Document rollback procedure -- [ ] Monitor user complaints and authentication errors - ---- - -## 8. Testing Strategy - -### Security Testing -```typescript -// Test 1: Verify tokens not in localStorage -test('tokens should not be in localStorage', () => { - const token = localStorage.getItem('token'); - const authToken = localStorage.getItem('auth_token'); - - expect(token).toBeNull(); - expect(authToken).toBeNull(); -}); - -// Test 2: Verify HttpOnly cookies cannot be accessed -test('auth cookies should be HttpOnly', () => { - const cookies = document.cookie; - - expect(cookies).not.toContain('auth_token'); - expect(cookies).not.toContain('laravel_session'); -}); - -// Test 3: Verify CSRF protection -test('API calls without CSRF token should fail', async () => { - const response = await fetch('/api/protected', { - method: 'POST', - // No CSRF token - }); - - expect(response.status).toBe(419); // CSRF token mismatch -}); - -// Test 4: XSS injection attempt -test('XSS should not access auth cookies', () => { - const script = document.createElement('script'); - script.innerHTML = ` - try { - const token = document.cookie.match(/auth_token=([^;]+)/); - window.stolenToken = token; - } catch (e) { - window.xssFailed = true; - } - `; - document.body.appendChild(script); - - expect(window.stolenToken).toBeUndefined(); - expect(window.xssFailed).toBe(true); -}); -``` - -### Integration Testing -```typescript -// Test authentication flow -test('complete authentication flow', async () => { - // 1. Get CSRF cookie - await fetch('/sanctum/csrf-cookie'); - - // 2. Login - const loginResponse = await fetch('/login', { - method: 'POST', - credentials: 'include', - body: JSON.stringify({ email: 'test@example.com', password: 'password' }) - }); - - expect(loginResponse.ok).toBe(true); - - // 3. Access protected resource - const userResponse = await fetch('/api/user', { - credentials: 'include' - }); - - expect(userResponse.ok).toBe(true); - - // 4. Logout - const logoutResponse = await fetch('/logout', { - method: 'POST', - credentials: 'include' - }); - - expect(logoutResponse.ok).toBe(true); - - // 5. Verify session cleared - const unauthorizedResponse = await fetch('/api/user', { - credentials: 'include' - }); - - expect(unauthorizedResponse.status).toBe(401); -}); -``` - -### Performance Testing -```bash -# Load test authentication endpoints -ab -n 1000 -c 10 -p login.json -T application/json http://localhost:3000/api/auth/login - -# Monitor response times -# Target: < 200ms for authentication flows -# Target: < 100ms for API calls with session -``` - ---- - -## 9. Compliance & Standards - -### OWASP ASVS 4.0 Compliance - -| Requirement | Current | Solution 2 | Solution 4 | -|-------------|---------|-----------|-----------| -| V3.2.1: Session tokens HttpOnly | ❌ No | ✅ Yes | ✅ Yes | -| V3.2.2: Cookie Secure flag | ❌ No | ✅ Yes | ✅ Yes | -| V3.2.3: Cookie SameSite | 🟡 Lax | ✅ Lax/Strict | ✅ Strict | -| V3.3.1: CSRF protection | 🟡 Partial | ✅ Full | ✅ Full | -| V3.5.2: Session timeout | 🟡 7 days | ✅ Configurable | ✅ Configurable | -| V8.3.4: XSS protection | ❌ No | ✅ Yes | ✅ Yes | - -### PCI DSS Compliance -- **Requirement 6.5.9 (XSS):** Solution 2 & 4 provide XSS protection -- **Requirement 8.2.3 (MFA):** Can be added to any solution -- **Requirement 8.2.4 (Password Security):** Laravel provides bcrypt hashing - -### GDPR Compliance -- **Article 32 (Security):** Solution 2 & 4 meet security requirements -- **Data Minimization:** Session-based auth minimizes token exposure -- **Right to Erasure:** Easy to delete session data - ---- - -## 10. References & Further Reading - -### Official Documentation -- [Laravel Sanctum - SPA Authentication](https://laravel.com/docs/11.x/sanctum#spa-authentication) -- [Next.js Authentication Guide](https://nextjs.org/docs/app/guides/authentication) -- [Next.js 15 cookies() function](https://nextjs.org/docs/app/api-reference/functions/cookies) -- [OWASP SameSite Cookie Attribute](https://owasp.org/www-community/SameSite) -- [NIST 800-63B Session Management](https://pages.nist.gov/800-63-3/sp800-63b.html) - -### Security Resources -- [OWASP Content Security Policy](https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html) -- [Auth0: Backend for Frontend Pattern](https://auth0.com/blog/the-backend-for-frontend-pattern-bff/) -- [PortSwigger: Bypassing SameSite Restrictions](https://portswigger.net/web-security/csrf/bypassing-samesite-restrictions) -- [MDN: HttpOnly Cookie Attribute](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies) - -### Community Discussions -- [Is it safe to store JWT in localStorage?](https://stackoverflow.com/questions/44133536/is-it-safe-to-store-a-jwt-in-localstorage-with-reactjs) -- [Token storage security debate](https://dev.to/cotter/localstorage-vs-cookies-all-you-need-to-know-about-storing-jwt-tokens-securely-in-the-front-end-15id) - ---- - -## Conclusion - -Your current implementation (localStorage + non-HttpOnly cookies) has a **Critical** risk score of **7.6/10** due to XSS vulnerabilities. - -**Recommended Action:** Migrate to **Solution 2 (Sanctum Cookie-Based Sessions)** within the next sprint. This is Laravel Sanctum's officially recommended pattern for SPAs and provides the best security-to-complexity ratio. - -**Quick Win:** If immediate migration isn't possible, implement **Solution 1 (HttpOnly Proxy)** as a temporary measure to eliminate localStorage vulnerabilities within 2-4 hours. - -**Do Not:** Rely solely on **Solution 3 (Token Encryption)** as it provides a false sense of security and is still vulnerable to XSS attacks. - -The research shows a clear industry consensus: **HttpOnly cookies with CSRF protection are the gold standard for SPA authentication security**, and Laravel Sanctum provides this pattern out of the box. - ---- - -**Research Confidence:** 85% -**Sources Consulted:** 25+ -**Last Updated:** 2025-11-07 diff --git a/claudedocs/backend/2026-03-02_구현내역.md b/claudedocs/backend/2026-03-02_구현내역.md deleted file mode 100644 index d83165ec..00000000 --- a/claudedocs/backend/2026-03-02_구현내역.md +++ /dev/null @@ -1,38 +0,0 @@ -# 2026-03-02 (월) 백엔드 구현 내역 - -## 1. `🆕 신규` [roadmap] 중장기 계획 테이블 마이그레이션 추가 - -**커밋**: `3ca161e` | **유형**: feat - -### 배경 -관리자 패널에서 프로젝트 로드맵을 관리할 수 있도록 데이터베이스 테이블이 필요했음. - -### 구현 내용 -- `admin_roadmap_plans` 테이블 생성 — 계획 마스터 (제목, 카테고리, 상태, Phase, 진행률) -- `admin_roadmap_milestones` 테이블 생성 — 마일스톤 관리 (plan_id FK, 상태, 예정일) - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `database/migrations/2026_03_02_000000_create_admin_roadmap_tables.php` | 신규 생성 | - ---- - -## 2. `🆕 신규` [rd] AI 견적 엔진 테이블 생성 + 모듈 카탈로그 시더 - -**커밋**: `abe0460` | **유형**: feat - -### 배경 -AI 기반 자동 견적 시스템을 위한 데이터 저장 구조 및 초기 모듈 카탈로그 데이터가 필요했음. - -### 구현 내용 -- `ai_quotation_modules` 테이블 — SAM 모듈 카탈로그 (18개 모듈 정의) -- `ai_quotations` 테이블 — AI 견적 요청/결과 저장 -- `ai_quotation_items` 테이블 — AI 추천 모듈 목록 -- `AiQuotationModuleSeeder` — customer-pricing 기반 초기 데이터 시딩 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `database/migrations/2026_03_02_100000_create_ai_quotation_tables.php` | 신규 생성 | -| `database/seeders/AiQuotationModuleSeeder.php` | 신규 생성 | diff --git a/claudedocs/backend/2026-03-03_구현내역.md b/claudedocs/backend/2026-03-03_구현내역.md deleted file mode 100644 index 61cf1d30..00000000 --- a/claudedocs/backend/2026-03-03_구현내역.md +++ /dev/null @@ -1,197 +0,0 @@ -# 2026-03-03 (화) 백엔드 구현 내역 - -## 1. `⚙️ 설정` [ai] Gemini 모델 버전 업그레이드 - -**커밋**: `f79d008` | **유형**: chore - -### 배경 -Google Gemini 모델의 새 버전(2.5-flash)이 출시되어 기존 2.0-flash에서 업그레이드 필요. - -### 구현 내용 -- `config/services.php` — fallback 기본 모델명 `gemini-2.5-flash`로 변경 -- `AiReportService.php` — fallback 기본값 동일 변경 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `config/services.php` | 수정 | -| `app/Services/AiReportService.php` | 수정 | - ---- - -## 2. `🔧 수정` [deploy] 배포 시 .env 권한 640 보장 추가 - -**커밋**: `7e309e4` | **유형**: fix - -### 배경 -2026-03-03 장애 발생 — vi 편집으로 `.env` 파일 권한이 600으로 변경되어 PHP-FPM이 읽기 실패 → 500 에러. 재발 방지를 위해 배포 파이프라인에 권한 보장 로직 추가. - -### 구현 내용 -- Stage/Production Jenkinsfile 배포 스크립트에 `chmod 640 .env` 추가 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `Jenkinsfile` | 수정 | - ---- - -## 3. `🔧 수정` [hr] 사업소득자 임금대장 컬럼 추가 - -**커밋**: `b3c7d08` | **유형**: feat (기존 테이블 확장) - -### 배경 -사업소득자(프리랜서)를 시스템 회원이 아닌 직접 입력 대상자로 지원하기 위해 추가 컬럼 필요. - -### 구현 내용 -- `user_id` nullable 변경 (직접 입력 대상자 지원) -- `display_name`, `business_reg_number` 컬럼 추가 -- 기존 데이터는 earner 프로필에서 자동 채움 마이그레이션 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `database/migrations/..._add_display_name_to_business_income_payments.php` | 신규 생성 | - ---- - -## 4. `🔧 수정` [ai-quotation] 제조 견적서 마이그레이션 추가 - -**커밋**: `da1142a` | **유형**: feat (기존 테이블 확장) - -### 배경 -AI 견적 시스템에서 제조업 견적서를 지원하기 위해 기존 테이블 확장 및 가격표 테이블 신규 생성 필요. - -### 구현 내용 -- `ai_quotations` 테이블에 `quote_mode`, `quote_number`, `product_category` 컬럼 추가 -- `ai_quotation_items` 테이블에 `specification`, `unit`, `quantity`, `unit_price`, `total_price`, `item_category`, `floor_code` 컬럼 추가 -- `ai_quote_price_tables` 테이블 신규 생성 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `database/migrations/..._add_manufacture_fields_to_ai_quotations.php` | 신규 생성 | - ---- - -## 5. `🔧 수정` [today-issue] 날짜 기반 이전 이슈 조회 기능 추가 - -**커밋**: `83a7745` | **유형**: feat (기존 기능 확장) - -### 배경 -오늘의 이슈를 특정 날짜 기준으로 과거 데이터도 조회할 수 있어야 함. 이전에는 현재 날짜 기준만 지원했음. - -### 구현 내용 -- `TodayIssueController`에 `date` 파라미터(YYYY-MM-DD) 추가 -- `TodayIssueService.summary()`에 날짜 기반 필터링 로직 구현 -- 이전 이슈 조회 시 만료(active) 필터 무시하여 과거 데이터 조회 가능 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Http/Controllers/Api/V1/TodayIssueController.php` | 수정 | -| `app/Services/TodayIssueService.php` | 수정 | - ---- - -## 6. `🔧 수정` [approval] 결재 수신함 날짜 범위 필터 추가 - -**커밋**: `b7465be` | **유형**: feat (기존 기능 확장) - -### 배경 -결재 수신함에서 특정 기간의 결재 건만 조회할 수 있도록 날짜 필터 필요. - -### 구현 내용 -- `InboxIndexRequest`에 `start_date`/`end_date` 검증 룰 추가 -- `ApprovalService.inbox()`에 `created_at` 날짜 범위 필터 구현 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Http/Requests/Approval/InboxIndexRequest.php` | 수정 | -| `app/Services/ApprovalService.php` | 수정 | - ---- - -## 7. `🔧 수정` [daily-report] 자금현황 카드용 필드 추가 - -**커밋**: `ad27090` | **유형**: feat (기존 API 확장) - -### 배경 -일일보고서 대시보드에 자금현황 카드를 표시하기 위해 미수금/미지급금/당월 예상 지출 데이터 필요. - -### 구현 내용 -- 미수금 잔액(`receivable_balance`) 계산 로직 구현 -- 미지급금 잔액(`payable_balance`) 계산 로직 구현 -- 당월 예상 지출(`monthly_expense_total`) 계산 로직 구현 -- summary API 응답에 자금현황 3개 필드 포함 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Services/DailyReportService.php` | 수정 | - ---- - -## 8. `🔧 수정` [stock,client,status-board] 날짜 필터 및 조건 보완 - -**커밋**: `4244334` | **유형**: feat (기존 기능 확장) - -### 배경 -재고/거래처/현황판 화면에서 날짜 범위 필터가 미지원이었고, 부실채권 현황에 비활성 데이터가 포함되는 이슈. - -### 구현 내용 -- `StockController/StockService` — 입출고 이력 기반 날짜 범위 필터 추가 -- `ClientService` — 등록일 기간 필터(`start_date`/`end_date`) 추가 -- `StatusBoardService` — 부실채권 현황에 `is_active` 조건 추가 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Http/Controllers/Api/V1/StockController.php` | 수정 | -| `app/Services/StockService.php` | 수정 | -| `app/Services/ClientService.php` | 수정 | -| `app/Services/StatusBoardService.php` | 수정 | - ---- - -## 9. `🔧 수정` [hr] Leave 모델 확장 + 결재양식 마이그레이션 추가 - -**커밋**: `23c6cf6` | **유형**: feat (기존 모델 확장) - -### 배경 -기존 연차/반차만 지원하던 휴가 시스템에 출장, 재택근무, 외근, 조퇴, 지각, 결근 등 근태 유형 확장 필요. 결재 양식(근태신청, 사유서)도 추가. - -### 구현 내용 -- Leave 타입 6개 추가: `business_trip`, `remote`, `field_work`, `early_leave`, `late_reason`, `absent_reason` -- 그룹 상수: `VACATION_TYPES`, `ATTENDANCE_REQUEST_TYPES`, `REASON_REPORT_TYPES` -- `FORM_CODE_MAP` — 유형 → 결재양식코드 매핑 -- `ATTENDANCE_STATUS_MAP` — 유형 → 근태상태 매핑 -- 결재양식 2개 추가: `attendance_request`(근태신청), `reason_report`(사유서) - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Models/Tenants/Leave.php` | 수정 | -| `database/migrations/..._insert_attendance_approval_forms.php` | 신규 생성 | - ---- - -## 10. `🔧 수정` [production] 자재투입 모달 개선 - -**커밋**: `fc53789` | **유형**: fix (기존 기능 버그 수정 + 개선) - -### 배경 -자재투입 시 lot 미관리 품목(L-Bar, 보강평철)이 목록에 표시되는 이슈, BOM 그룹키 부재로 동일 자재 구분 불가, 셔터박스 순서가 작업일지와 불일치. - -### 구현 내용 -- `getMaterialsForItem` — `lot_managed===false` 품목을 자재투입 목록에서 제외 -- `getMaterialsForItem` — `bom_group_key` 필드 추가 (category+partType 기반 고유키) -- `BendingInfoBuilder` — `shutterPartTypes`에서 `top_cover`/`fin_cover` 제거 (중복 방지) -- `BendingInfoBuilder` — 셔터박스 루프 순서 파트→길이로 변경 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Services/Production/BendingInfoBuilder.php` | 수정 | -| `app/Services/WorkOrderService.php` | 수정 | diff --git a/claudedocs/backend/2026-03-04_구현내역.md b/claudedocs/backend/2026-03-04_구현내역.md deleted file mode 100644 index f1c7e198..00000000 --- a/claudedocs/backend/2026-03-04_구현내역.md +++ /dev/null @@ -1,336 +0,0 @@ -# 2026-03-04 (수) 백엔드 구현 내역 - -## 1. `🔧 수정` [inspection] 캘린더 스케줄 조회 API 추가 - -**커밋**: `e9fd75f` | **유형**: feat (기존 검사 모듈에 캘린더 API 추가) - -### 배경 -검사 일정을 캘린더 형태로 표시하기 위한 API 필요. - -### 구현 내용 -- `GET /api/v1/inspections/calendar` 엔드포인트 추가 -- `year`, `month`, `inspector`, `status` 파라미터 지원 -- React 프론트엔드 `CalendarItemApi` 형식에 맞춰 응답 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Http/Controllers/Api/V1/InspectionController.php` | 수정 | -| `app/Services/InspectionService.php` | 수정 | -| `routes/api/v1/production.php` | 수정 | - ---- - -## 2. `🆕 신규` [barobill] 바로빌 연동 API 엔드포인트 추가 - -**커밋**: `4f3467c` | **유형**: feat - -### 배경 -바로빌(전자세금계산서/은행/카드 연동 서비스) API 연동을 위한 백엔드 엔드포인트 필요. - -### 구현 내용 -- `GET /api/v1/barobill/status` — 연동 현황 조회 -- `POST /api/v1/barobill/login` — 로그인 정보 등록 -- `POST /api/v1/barobill/signup` — 회원가입 정보 등록 -- `GET /api/v1/barobill/bank-service-url` — 은행 서비스 URL -- `GET /api/v1/barobill/account-link-url` — 계좌 연동 URL -- `GET /api/v1/barobill/card-link-url` — 카드 연동 URL -- `GET /api/v1/barobill/certificate-url` — 공인인증서 URL - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Http/Controllers/Api/V1/BarobillController.php` | 신규 생성 | -| `routes/api/v1/finance.php` | 수정 | - ---- - -## 3. `🔧 수정` [expense,loan] 대시보드 상세 필터 및 가지급금 카테고리 분류 - -**커밋**: `1deeafc` | **유형**: feat (기존 대시보드 확장) - -### 배경 -경비/가지급금 대시보드에서 날짜 범위 필터와 검색 기능이 없었고, 가지급금에 카테고리(카드/경조사/상품권/접대비) 분류 필요. - -### 구현 내용 -- `ExpectedExpenseController/Service` — dashboardDetail에 `start_date`/`end_date`/`search` 파라미터 추가 -- `Loan` 모델 — category 상수 및 라벨 정의 (카드/경조사/상품권/접대비) -- `LoanService` — dashboard에 `category_breakdown` 집계 추가 -- 마이그레이션 — loans 테이블 `category` 컬럼 추가 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Http/Controllers/Api/V1/ExpectedExpenseController.php` | 수정 | -| `app/Models/Tenants/Loan.php` | 수정 | -| `app/Services/ExpectedExpenseService.php` | 수정 | -| `app/Services/LoanService.php` | 수정 | -| `database/migrations/2026_03_04_100000_add_category_to_loans_table.php` | 신규 생성 | - ---- - -## 4. `🔧 수정` [models] User 모델 import 누락/오류 수정 - -**커밋**: `da04b84` | **유형**: fix (버그 수정) - -### 배경 -Tenants 네임스페이스에서 `User::class`가 `App\Models\Tenants\User`로 잘못 해석되는 문제. Loan, TodayIssue 모델에서 User import 경로 오류. - -### 구현 내용 -- `Loan.php` — `App\Models\Members\User` import 추가 -- `TodayIssue.php` — `App\Models\Users\User` → `App\Models\Members\User` 수정 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Models/Tenants/Loan.php` | 수정 | -| `app/Models/Tenants/TodayIssue.php` | 수정 | - ---- - -## 5. `🔧 수정` [cards] 리다이렉트 추가 - -**커밋**: `76192fc` | **유형**: fix (하위호환) - -### 배경 -프론트엔드에서 기존 `cards/stats` 경로로 호출하는 코드가 있어 새 경로로 리다이렉트 필요. - -### 구현 내용 -- `cards/stats` → `card-transactions/dashboard` 리다이렉트 라우트 추가 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `routes/api/v1/finance.php` | 수정 | - ---- - -## 6. `🔧 수정` [address] 주소 필드 255자 → 500자 확장 - -**커밋**: `7cf70db` | **유형**: fix (제한 완화) - -### 배경 -실제 주소 데이터가 255자를 초과하는 경우 발생. DB와 FormRequest 검증 모두 확장 필요. - -### 구현 내용 -- DB 마이그레이션 — `clients`, `tenants`, `site_briefings`, `sites` 테이블 address 컬럼 `varchar(500)` -- FormRequest 8개 파일 — `max:255` → `max:500` 변경 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Http/Requests/Client/ClientStoreRequest.php` | 수정 | -| `app/Http/Requests/Client/ClientUpdateRequest.php` | 수정 | -| `app/Http/Requests/SiteBriefing/StoreSiteBriefingRequest.php` | 수정 | -| `app/Http/Requests/SiteBriefing/UpdateSiteBriefingRequest.php` | 수정 | -| `app/Http/Requests/Tenant/TenantStoreRequest.php` | 수정 | -| `app/Http/Requests/Tenant/TenantUpdateRequest.php` | 수정 | -| `app/Http/Requests/V1/Site/StoreSiteRequest.php` | 수정 | -| `app/Http/Requests/V1/Site/UpdateSiteRequest.php` | 수정 | -| `database/migrations/..._extend_address_columns_to_500.php` | 신규 생성 | - ---- - -## 7. `🔧 수정` [dashboard] D1.7 기획서 기반 리스크 감지형 서비스 리팩토링 - -**커밋**: `e637e3d` | **유형**: feat (기존 대시보드 대규모 리팩토링) - -### 배경 -D1.7 기획서 요구사항에 따라 접대비/복리후생비/매출채권 대시보드를 단순 집계에서 리스크 감지형으로 전환. - -### 구현 내용 -- `EntertainmentService` — 리스크 감지형 전환 (주말/심야, 기피업종, 고액결제, 증빙미비) -- `WelfareService` — 리스크 감지형 전환 (비과세 한도 초과, 사적 사용 의심, 특정인 편중, 항목별 한도 초과) -- `ReceivablesService` — summary를 `cards` + `check_points` 구조로 개선 (누적/당월 미수금, Top3 거래처) -- `LoanService` — getCategoryBreakdown 전체 대상으로 집계 조건 변경 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Services/EntertainmentService.php` | 수정 (대규모) | -| `app/Services/WelfareService.php` | 수정 (대규모) | -| `app/Services/ReceivablesService.php` | 수정 (대규모) | -| `app/Services/LoanService.php` | 수정 | - ---- - -## 8. `🔧 수정` [entertainment,welfare] 바로빌 조인 컬럼명 및 심야 시간 파싱 수정 - -**커밋**: `f665d3a` | **유형**: fix (버그 수정) - -### 배경 -바로빌 카드거래 테이블 조인 시 컬럼명 불일치 및 심야 판별 함수 오류. - -### 구현 내용 -- `approval_no` → `approval_num` 컬럼명 수정 -- `use_time` 심야 판별: `HOUR()` → `SUBSTRING` 문자열 파싱으로 변경 -- `whereNotNull('bct.use_time')` 조건 추가 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Services/EntertainmentService.php` | 수정 | -| `app/Services/WelfareService.php` | 수정 | - ---- - -## 9. `🆕 신규` [approval] 지출결의서 양식 등록 및 고도화 - -**커밋**: `b86af29`, `282bf26` | **유형**: feat - -### 배경 -전자결재에 지출결의서 양식을 등록하고, HTML body_template 필드로 정형화된 양식 제공. - -### 구현 내용 -- `approval_forms` 테이블에 `body_template` TEXT 컬럼 추가 (마이그레이션) -- 지출결의서(expense) 양식 데이터 등록 -- 참조 문서 기반으로 정형 양식 HTML 리디자인 — 지출형식/세금계산서 체크박스, 기본정보, 8열 내역 테이블, 합계, 첨부 섹션 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `database/migrations/..._add_body_template_to_approval_forms.php` | 신규 생성 | -| `database/migrations/..._insert_expense_approval_form.php` | 신규 생성 | -| `database/migrations/..._update_expense_approval_form_body_template.php` | 신규 생성 | - ---- - -## 10. `🆕 신규` [entertainment] 접대비 상세 조회 API + `🔧 수정` 가지급금 날짜 필터 - -**커밋**: `66da297`, `a173a5a`, `94b96e2`, `2f3ec13` | **유형**: feat + fix - -### 배경 -접대비 상세 대시보드(손금한도, 월별추이, 거래내역)가 필요하고, 가지급금 대시보드에도 날짜 필터 지원 필요. - -### 구현 내용 -- `EntertainmentController/Service` — `getDetail()` 상세 조회 API 신규 (손금한도, 월별추이, 사용자분포, 거래내역, 분기현황) -- 수입금액별 추가한도 계산 (세법 기준), 거래건별 리스크 감지 -- `LoanController/Service` — dashboard에 `start_date`/`end_date` 파라미터 지원 (기존 수정) -- `getCategoryBreakdown` SQL alias 충돌 수정 -- 분기 사용액 조회에 날짜 필터 적용 -- 라우트: `GET /entertainment/detail` 엔드포인트 추가 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Http/Controllers/Api/V1/EntertainmentController.php` | 수정 | -| `app/Http/Controllers/Api/V1/LoanController.php` | 수정 | -| `app/Services/EntertainmentService.php` | 수정 (대규모) | -| `app/Services/LoanService.php` | 수정 | -| `routes/api/v1/finance.php` | 수정 | - ---- - -## 11. `🆕 신규` [calendar,vat] 캘린더 CRUD 및 부가세 상세 조회 API - -**커밋**: `74a60e0` | **유형**: feat - -### 배경 -일정 관리를 위한 캘린더 CRUD API와 부가세 상세 조회 대시보드 API 필요. - -### 구현 내용 -- `CalendarController/Service` — 일정 등록/수정/삭제 API 신규 -- `VatController/Service` — `getDetail()` 상세 조회 신규 (요약, 참조테이블, 미발행 목록, 신고기간 옵션) -- 라우트: `POST/PUT/DELETE /calendar/schedules`, `GET /vat/detail` - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Http/Controllers/Api/V1/CalendarController.php` | 신규 생성 | -| `app/Http/Controllers/Api/V1/VatController.php` | 신규 생성 | -| `app/Services/CalendarService.php` | 신규 생성 | -| `app/Services/VatService.php` | 신규 생성 | -| `routes/api/v1/finance.php` | 수정 | - ---- - -## 12. `🆕 신규` [shipment] 배차정보 다중 행 시스템 - -**커밋**: `851862` | **유형**: feat - -### 배경 -기존 출하 건에 단일 배차정보만 저장 가능했으나, 다중 차량 배차를 지원해야 함. - -### 구현 내용 -- `shipment_vehicle_dispatches` 테이블 신규 생성 (seq, logistics_company, arrival_datetime, tonnage, vehicle_no, driver_contact, remarks) -- `ShipmentVehicleDispatch` 모델 신규 -- `Shipment` 모델에 `vehicleDispatches()` HasMany 관계 추가 -- `ShipmentService` — `syncDispatches()` 추가, store/update/delete/show/index에서 연동 -- FormRequest — Store/Update에 `vehicle_dispatches` 배열 검증 규칙 추가 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Models/Tenants/ShipmentVehicleDispatch.php` | 신규 생성 | -| `app/Models/Tenants/Shipment.php` | 수정 | -| `app/Services/ShipmentService.php` | 수정 | -| `app/Http/Requests/Shipment/ShipmentStoreRequest.php` | 수정 | -| `app/Http/Requests/Shipment/ShipmentUpdateRequest.php` | 수정 | -| `database/migrations/..._create_shipment_vehicle_dispatches_table.php` | 신규 생성 | - ---- - -## 13. `🔧 수정` [production] 자재투입 bom_group_key 개별 저장 - -**커밋**: `5ee97c2` | **유형**: fix (기존 기능 보완) - -### 배경 -동일 자재가 다른 BOM 그룹에 속할 때 구분이 안 되는 문제. bom_group_key로 개별 식별 필요. - -### 구현 내용 -- `work_order_material_inputs` 테이블에 `bom_group_key` 컬럼 추가 -- 기투입 조회를 `stock_lot_id` + `bom_group_key` 복합키로 변경 -- `replace` 모드 지원 (기존 삭제 → 재등록) - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Http/Requests/WorkOrder/MaterialInputForItemRequest.php` | 수정 | -| `app/Models/Production/WorkOrderMaterialInput.php` | 수정 | -| `app/Services/WorkOrderService.php` | 수정 | -| `database/migrations/..._bom_group_key_to_work_order_material_inputs.php` | 신규 생성 | - ---- - -## 14. `🔧 수정` [production] 절곡 검사 데이터 전체 item 복제 + bending EAV 변환 - -**커밋**: `897511c` | **유형**: fix (기존 검사 로직 개선) - -### 배경 -절곡 검사 시 동일 작업지시의 모든 item에 검사 데이터가 복제 저장되어야 하며, products 배열을 bending EAV 레코드로 변환 필요. - -### 구현 내용 -- `storeItemInspection` — bending/bending_wip 시 동일 작업지시 모든 item에 복제 저장 -- `transformBendingProductsToRecords` — products 배열 → bending EAV 레코드 변환 -- `getMaterialInputLots` — 품목코드별 그룹핑으로 변경 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Services/WorkOrderService.php` | 수정 (대규모) | - ---- - -## 15. `🆕 신규` [outbound] 배차차량 관리 API - -**커밋**: `1a8bb46` | **유형**: feat - -### 배경 -출고 관련 배차차량을 독립적으로 관리(조회/수정/통계)하는 API 필요. - -### 구현 내용 -- `VehicleDispatchService` — index(검색/필터/페이지네이션), stats(선불/착불/합계), show, update -- `VehicleDispatchController` + `VehicleDispatchUpdateRequest` -- options JSON 컬럼 추가 (dispatch_no, status, freight_cost_type, supply_amount, vat, total_amount, writer) -- inventory.php에 `vehicle-dispatches` 라우트 4개 등록 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Http/Controllers/Api/V1/VehicleDispatchController.php` | 신규 생성 | -| `app/Http/Requests/VehicleDispatch/VehicleDispatchUpdateRequest.php` | 신규 생성 | -| `app/Services/VehicleDispatchService.php` | 신규 생성 | -| `app/Models/Tenants/ShipmentVehicleDispatch.php` | 수정 | -| `app/Services/ShipmentService.php` | 수정 | -| `database/migrations/..._options_to_shipment_vehicle_dispatches_table.php` | 신규 생성 | -| `routes/api/v1/inventory.php` | 수정 | diff --git a/claudedocs/backend/2026-03-05_구현내역.md b/claudedocs/backend/2026-03-05_구현내역.md deleted file mode 100644 index 56be9469..00000000 --- a/claudedocs/backend/2026-03-05_구현내역.md +++ /dev/null @@ -1,386 +0,0 @@ -# 2026-03-05 (목) 백엔드 구현 내역 - -## 1. `🔧 수정` [storage] RecordStorageUsage 명령어 수정 - -**커밋**: `e0bb19a` | **유형**: fix (버그 수정) - -### 배경 -`Tenant::where('status', 'active')` 하드코딩 사용 중이나 tenants 테이블에 `status` 컬럼이 없고 `tenant_st_code`를 사용함. 모델 스코프 사용으로 수정. - -### 구현 내용 -- `Tenant::where('status', 'active')` → `Tenant::active()` 스코프 사용 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Console/Commands/RecordStorageUsage.php` | 수정 | - ---- - -## 2. `🆕 신규` [dashboard-ceo] CEO 대시보드 섹션별 API 및 일일보고서 엑셀 - -**커밋**: `e8da2ea`, `f1a3e0f` | **유형**: feat + fix - -### 배경 -CEO 전용 대시보드에 매출/매입/생산/미출고/시공/근태 등 6개 섹션 데이터를 제공하는 API 및 엑셀 다운로드 기능 필요. - -### 구현 내용 -- `DashboardCeoController/Service` — 6개 섹션 API 신규 (매출/매입/생산/미출고/시공/근태) -- `DailyReportController/Service` — 엑셀 다운로드 API (`GET /daily-report/export`) -- 라우트: dashboard 하위 6개 + `daily-report/export` 엔드포인트 -- 공정명 컬럼 수정 (`p.name` → `p.process_name`) -- 근태 부서 조인 수정 (`users.department_id` → `tenant_user_profiles` 경유) - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Http/Controllers/Api/V1/DashboardCeoController.php` | 신규 생성 | -| `app/Services/DashboardCeoService.php` | 신규 생성 | -| `app/Http/Controllers/Api/V1/DailyReportController.php` | 수정 | -| `app/Services/DailyReportService.php` | 수정 | -| `routes/api/v1/common.php` | 수정 | -| `routes/api/v1/finance.php` | 수정 | - ---- - -## 3. `🔧 수정` [daily-report] 엑셀 내보내기 어음/외상매출채권 현황 및 리팩토링 - -**커밋**: `1b2363d`, `fefd129` | **유형**: feat + refactor (기존 엑셀 기능 확장/개선) - -### 배경 -일일보고서 엑셀에 어음/외상매출채권 현황 섹션이 빠져있었고, 엑셀과 화면 데이터가 불일치하는 문제. - -### 구현 내용 -- `DailyReportExport` — 어음 현황 테이블 + 합계 + 스타일링 추가 -- `DailyReportService` — exportData를 `dailyAccounts()` 재사용 구조로 리팩토링 -- 헤더 라벨 전월이월/당월입금/당월출금/잔액으로 수정 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Exports/DailyReportExport.php` | 수정 | -| `app/Services/DailyReportService.php` | 수정 (리팩토링) | - ---- - -## 4. `🔧 수정` [production] 절곡 검사 FormRequest 검증 누락 수정 - -**커밋**: `ef7d9fa` | **유형**: fix (버그 수정) - -### 배경 -`StoreItemInspectionRequest`에 `inspection_data.products` 검증 규칙이 누락되어 `validated()`에서 products 데이터가 제거되는 버그. - -### 구현 내용 -- `products.*.id`, `bendingStatus`, `lengthMeasured`, `widthMeasured`, `gapPoints` 검증 규칙 추가 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Http/Requests/WorkOrder/StoreItemInspectionRequest.php` | 수정 | - ---- - -## 5. `🆕 신규` [approval] Document ↔ Approval 브릿지 연동 (Phase 4.2) - -**커밋**: `cd847e0` | **유형**: feat - -### 배경 -문서(Document) 시스템과 결재(Approval) 시스템을 연동하여, 문서 상신 시 결재가 자동 생성되고 결재 처리 시 문서 상태가 동기화되어야 함. - -### 구현 내용 -- `Approval` 모델에 `linkable` morphTo 관계 추가 -- `DocumentService` — 상신 시 Approval 자동 생성 + approval_steps 변환 -- `ApprovalService` — 승인/반려/회수 시 Document 상태 동기화 -- `approvals` 테이블에 `linkable_type`, `linkable_id` 컬럼 마이그레이션 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Models/Tenants/Approval.php` | 수정 | -| `app/Services/ApprovalService.php` | 수정 | -| `app/Services/DocumentService.php` | 수정 | -| `database/migrations/..._add_linkable_to_approvals_table.php` | 신규 생성 | - ---- - -## 6. `🔧 수정` [process] 공정단계 options 컬럼 추가 - -**커밋**: `1f7f45e` | **유형**: feat (기존 테이블 확장) - -### 배경 -공정단계별 검사 설정/범위 등 확장 속성을 저장할 JSON 컬럼 필요. - -### 구현 내용 -- `ProcessStep` 모델에 `options` JSON 컬럼 추가 (fillable, cast) -- Store/UpdateProcessStepRequest에 `inspection_setting`, `inspection_scope` 검증 규칙 -- `process_steps` 테이블 마이그레이션 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Http/Requests/V1/ProcessStep/StoreProcessStepRequest.php` | 수정 | -| `app/Http/Requests/V1/ProcessStep/UpdateProcessStepRequest.php` | 수정 | -| `app/Models/ProcessStep.php` | 수정 | -| `database/migrations/..._add_options_to_process_steps_table.php` | 신규 생성 | - ---- - -## 7. `🔄 리팩토링` [production] 셔터박스 prefix isStandard 파라미터 제거 - -**커밋**: `d4f21f0` | **유형**: refactor - -### 배경 -CF/CL/CP/CB 품목이 모든 길이에 등록되어 boxSize와 무관하게 적용됨. isStandard 분기가 불필요. - -### 구현 내용 -- `resolveShutterBoxPrefix()`에서 `isStandard` 파라미터 및 분기 로직 제거 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Services/Production/BendingInfoBuilder.php` | 수정 | -| `app/Services/Production/PrefixResolver.php` | 수정 | - ---- - -## 8. `🔧 수정` [production] 자재투입 replace 모드 지원 - -**커밋**: `7432fb1` | **유형**: feat (기존 기능 확장) - -### 배경 -자재투입 시 기존 투입 데이터를 교체하는 방식 선택 가능하도록 지원. - -### 구현 내용 -- `registerMaterialInputForItem`에 `replace` 파라미터 추가 -- Controller에서 request body의 `replace` 값 전달 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Http/Controllers/Api/V1/WorkOrderController.php` | 수정 | - ---- - -## 9. `🔄 리팩토링` [core] 모델 스코프 적용 규칙 추가 - -**커밋**: `9b8cdfa` | **유형**: refactor - -### 배경 -`where` 하드코딩 대신 모델에 정의된 스코프를 우선 사용하도록 코드 규칙 명시. - -### 구현 내용 -- `RecordStorageUsage` — where 하드코딩 → `Tenant::active()` 스코프 -- `CLAUDE.md` — 쿼리 수정 시 모델 스코프 우선 규칙 명시 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `CLAUDE.md` | 수정 | -| `app/Console/Commands/RecordStorageUsage.php` | 수정 | - ---- - -## 10. `⚙️ 설정` [infra] Slack 알림 채널 분리 - -**커밋**: `3d4dd9f` | **유형**: chore - -### 배경 -배포 알림 채널을 product_infra에서 deploy_api로 분리하여 알림 관리 개선. - -### 구현 내용 -- Jenkinsfile Slack 알림 채널 `product_infra` → `deploy_api` 변경 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `Jenkinsfile` | 수정 | - ---- - -## 11. `🔧 수정` [approval] 결재 테이블 확장 (3건) - -**커밋**: `ac72487`, `558a393`, `ce1f910` | **유형**: feat (기존 테이블 확장) - -### 배경 -결재 시스템에 기안자 읽음 확인, 재상신 횟수, 반려 이력 추적 기능 필요. - -### 구현 내용 -- `drafter_read_at` 컬럼 — 기안자 완료 결과 확인 타임스탬프 (미읽음 뱃지 지원) -- `resubmit_count` 컬럼 — 재상신 횟수 추적 -- `rejection_history` JSON 컬럼 — 반려 이력 저장 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `database/migrations/..._add_drafter_read_at_to_approvals_table.php` | 신규 생성 | -| `database/migrations/..._add_resubmit_count_to_approvals_table.php` | 신규 생성 | -| `database/migrations/..._add_rejection_history_to_approvals_table.php` | 신규 생성 | - ---- - -## 12. `🆕 신규` [rd] CM송 저장 테이블 마이그레이션 - -**커밋**: `66d1004` | **유형**: feat - -### 배경 -AI 생성 CM송(광고 음악) 데이터 저장을 위한 테이블 필요. - -### 구현 내용 -- `cm_songs` 테이블 생성 — tenant_id, user_id, company_name, industry, lyrics, audio_path, options - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `database/migrations/2026_03_05_170000_create_cm_songs_table.php` | 신규 생성 | - ---- - -## 13. `🆕 신규` [approval] 결재양식 마이그레이션 (3건) - -**커밋**: `f41605c`, `0f25a5d`, `846ced3` | **유형**: feat - -### 배경 -전자결재에 재직증명서, 경력증명서, 위촉증명서 양식 추가 필요. - -### 구현 내용 -- `employment_cert` — 재직증명서 양식 등록 -- `career_cert` — 경력증명서 양식 등록 -- `appointment_cert` — 위촉증명서 양식 등록 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `database/migrations/2026_03_05_184507_add_employment_cert_form.php` | 신규 생성 | -| `database/migrations/2026_03_05_230000_add_career_cert_form.php` | 신규 생성 | -| `database/migrations/2026_03_05_234000_add_appointment_cert_form.php` | 신규 생성 | - ---- - -## 14. `🔧 수정` [bill,loan] 어음 V8 확장 필드 및 가지급금 상품권 카테고리 - -**커밋**: `8c9f2fc` | **유형**: feat (기존 모델 대규모 확장) - -### 배경 -어음 관리에 V8 규격(증권종류, 할인, 배서, 추심, 개서, 부도 등) 54개 필드 지원 필요. 가지급금에 상품권 카테고리 및 상태(보유/사용/폐기) 관리 필요. - -### 구현 내용 -- `Bill` 모델 — V8 확장 필드 54개 추가, 수취/발행 어음·수표별 세분화된 상태 체계 -- `BillService` — `assignV8Fields`/`syncInstallments` 헬퍼, instrument_type/medium 필터 -- `BillInstallment` — type/counterparty 필드 추가 -- `Loan` 모델 — holding/used/disposed 상태 + metadata(JSON) 필드 -- `LoanService` — 상품권 카테고리 지원 (summary 상태별 집계, store 기본상태 holding) -- FormRequest — V8 확장 필드 검증 규칙 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Models/Tenants/Bill.php` | 수정 (대규모) | -| `app/Models/Tenants/BillInstallment.php` | 수정 | -| `app/Models/Tenants/Loan.php` | 수정 | -| `app/Services/BillService.php` | 수정 | -| `app/Services/LoanService.php` | 수정 | -| `app/Http/Requests/V1/Bill/StoreBillRequest.php` | 수정 | -| `app/Http/Requests/V1/Bill/UpdateBillRequest.php` | 수정 | -| `app/Http/Requests/Loan/LoanStoreRequest.php` | 수정 | -| `app/Http/Requests/Loan/LoanUpdateRequest.php` | 수정 | -| `app/Http/Requests/Loan/LoanIndexRequest.php` | 수정 | -| `app/Http/Controllers/Api/V1/LoanController.php` | 수정 | -| `database/migrations/..._add_v8_fields_to_bills_table.php` | 신규 생성 | -| `database/migrations/..._add_metadata_to_loans_table.php` | 신규 생성 | - ---- - -## 15. `🆕 신규` [loan] 상품권 접대비 자동 연동 + `🔧 수정` 후속 수정 (5건) - -**커밋**: `31d2f08`, `03f86f3`, `652ac3d`, `7fe856f`, `c57e768` | **유형**: feat + fix - -### 배경 -상품권이 사용+접대비해당일 경우 expense_accounts에 자동으로 접대비 레코드를 생성/삭제해야 함. 관련 집계 및 수정/삭제 정책도 정비. - -### 구현 내용 -- `ExpenseAccount` — `loan_id` 필드 + `SUB_TYPE_GIFT_CERTIFICATE` 상수 추가 -- `LoanService` — 상품권 used+접대비해당 시 expense_accounts 자동 upsert/삭제 (🆕) -- store()에서도 접대비 자동 연동 호출 (🔧) -- `getCategoryBreakdown` — used/disposed 상품권은 가지급금 집계에서 제외 (🔧) -- dashboard summary/목록에서도 used/disposed 상품권 제외 (🔧) -- `isEditable()`/`isDeletable()` — 상품권이면 상태 무관하게 허용 (🔧) -- 접대비 연동 시 `receipt_no`에 시리얼번호 매핑 (🔧) -- `expense_accounts`에 `loan_id` 컬럼 마이그레이션 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Models/Tenants/ExpenseAccount.php` | 수정 | -| `app/Models/Tenants/Loan.php` | 수정 | -| `app/Services/LoanService.php` | 수정 (다회) | -| `database/migrations/..._add_loan_id_to_expense_accounts_table.php` | 신규 생성 | - ---- - -## 16. `🆕 신규` [생산지시] 전용 API 엔드포인트 신규 생성 + `🔧 수정` 후속 수정 (4건) - -**커밋**: `2df8ecf`, `59d13ee`, `38c2402`, `0aa0a85` | **유형**: feat + fix - -### 배경 -수주 기반 생산지시 전용 API가 없어 프론트엔드에서 여러 API를 조합해야 했음. 전용 엔드포인트로 통합. - -### 구현 내용 -- `ProductionOrderService` — 목록(index), 통계(stats), 상세(show) 구현 (🆕) -- Order 기반 생산지시 대상 필터링 (IN_PROGRESS~SHIPPED) -- `workOrderProgress` 가공 필드, `production_ordered_at` 첫 WO 기반 -- BOM 공정 분류 추출 (order_nodes.options.bom_result) -- `ProductionOrderController` + `ProductionOrderIndexRequest` + Swagger 문서 (🆕) -- 날짜 포맷 Y-m-d 변환, `withCount('nodes')` 개소수 추가 (🔧) -- 자재투입 시 WO 자동 상태 전환 (`autoStartWorkOrderOnMaterialInput`) (🆕) -- `process_id=null`인 구매품/서비스 WO 제외 (🔧) -- `extractBomProcessGroups` BOM 파싱 수정 (🔧) -- 재고생산 보조 공정을 일반 워크플로우에서 분리 (`is_auxiliary` 플래그) (🆕) - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Http/Controllers/Api/V1/ProductionOrderController.php` | 신규 생성 | -| `app/Http/Requests/ProductionOrder/ProductionOrderIndexRequest.php` | 신규 생성 | -| `app/Services/ProductionOrderService.php` | 신규 생성 + 수정 | -| `app/Swagger/v1/ProductionOrderApi.php` | 신규 생성 | -| `app/Services/WorkOrderService.php` | 수정 | -| `app/Services/OrderService.php` | 수정 | -| `routes/api/v1/production.php` | 수정 | - ---- - -## 17. `🆕 신규` [품질관리] 백엔드 API 구현 + `🔧 수정` 후속 수정 (3건) - -**커밋**: `a6e29bc`, `3600c7b`, `0f26ea5` | **유형**: feat + fix - -### 배경 -품질관리서(제품검사 요청서) 및 실적신고 관리를 위한 백엔드 API 전체 구현. - -### 구현 내용 -- 품질관리서(quality_documents) CRUD API 14개 엔드포인트 (🆕) -- 실적신고(performance_reports) 관리 API 6개 엔드포인트 (🆕) -- DB 마이그레이션 4개 테이블 (🆕) -- 모델 4개 + 서비스 2개 + 컨트롤러 2개 + FormRequest 4개 (🆕) -- 납품일 Y-m-d 포맷 변환, 개소 수 order_nodes 루트 노드 기준 변경 (🔧) -- 수주선택 API에 `client_name` 필드 추가 (🔧) - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Http/Controllers/Api/V1/QualityDocumentController.php` | 신규 생성 | -| `app/Http/Controllers/Api/V1/PerformanceReportController.php` | 신규 생성 | -| `app/Http/Requests/Quality/QualityDocumentStoreRequest.php` | 신규 생성 | -| `app/Http/Requests/Quality/QualityDocumentUpdateRequest.php` | 신규 생성 | -| `app/Http/Requests/Quality/PerformanceReportConfirmRequest.php` | 신규 생성 | -| `app/Http/Requests/Quality/PerformanceReportMemoRequest.php` | 신규 생성 | -| `app/Models/Qualitys/QualityDocument.php` | 신규 생성 | -| `app/Models/Qualitys/QualityDocumentOrder.php` | 신규 생성 | -| `app/Models/Qualitys/QualityDocumentLocation.php` | 신규 생성 | -| `app/Models/Qualitys/PerformanceReport.php` | 신규 생성 | -| `app/Services/QualityDocumentService.php` | 신규 생성 + 수정 | -| `app/Services/PerformanceReportService.php` | 신규 생성 | -| `database/migrations/..._create_quality_documents_table.php` | 신규 생성 | -| `database/migrations/..._create_quality_document_orders_table.php` | 신규 생성 | -| `database/migrations/..._create_quality_document_locations_table.php` | 신규 생성 | -| `database/migrations/..._create_performance_reports_table.php` | 신규 생성 | -| `routes/api/v1/quality.php` | 신규 생성 | diff --git a/claudedocs/backend/2026-03-06_구현내역.md b/claudedocs/backend/2026-03-06_구현내역.md deleted file mode 100644 index a8cd9eb8..00000000 --- a/claudedocs/backend/2026-03-06_구현내역.md +++ /dev/null @@ -1,287 +0,0 @@ -# 2026-03-06 (금) 백엔드 구현 내역 - -## 1. `🔧 수정` [생산지시] 보조 공정 WO 카운트 제외 - -**커밋**: `a845f52` | **유형**: fix (기존 기능 보완) - -### 배경 -목록 조회 시 `work_orders_count`에 보조 공정(재고생산) WO가 포함되어 공정 진행률이 부정확. - -### 구현 내용 -- `withCount`에서 `is_auxiliary` WO 제외 조건 추가 -- `whereNotNull(process_id)` + `options->is_auxiliary` 조건 적용 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Services/ProductionOrderService.php` | 수정 | - ---- - -## 2. `🔧 수정` [loan] 상품권 summary에 접대비 집계 추가 - -**커밋**: `a7973bb` | **유형**: feat (기존 API 확장) - -### 배경 -상품권 대시보드에서 접대비로 전환된 건수/금액을 별도로 표시해야 함. - -### 구현 내용 -- `expense_accounts` 테이블에서 접대비(상품권) 건수/금액 조회 -- `entertainment_count`, `entertainment_amount` 응답 필드 추가 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Services/LoanService.php` | 수정 | - ---- - -## 3. `🔧 수정` [receivables] 상위 거래처 집계 soft delete 제외 - -**커밋**: `be9c1ba` | **유형**: fix (버그 수정) - -### 배경 -매출채권 상위 거래처 집계 쿼리에서 soft delete된 레코드가 포함되어 금액이 부풀려지는 이슈. - -### 구현 내용 -- orders, deposits, bills 서브쿼리에 `whereNull('deleted_at')` 추가 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Services/ReceivablesService.php` | 수정 | - ---- - -## 4. `🆕 신규` [finance] 계정과목 및 일반전표 API 추가 - -**커밋**: `12d172e` | **유형**: feat - -### 배경 -회계 시스템의 핵심인 계정과목 관리 및 일반전표(입금/출금/수동전표 통합 목록) API 신규 구현. - -### 구현 내용 -- `AccountCode` 모델/서비스/컨트롤러 — 계정과목 CRUD -- `JournalEntry`, `JournalEntryLine` 모델 — 전표/전표 분개 모델 -- `GeneralJournalEntryService` — 입금/출금/수동전표 UNION 통합 목록, 수동전표 CRUD -- `GeneralJournalEntryController` + FormRequest 검증 클래스 -- finance 라우트 등록, i18n 메시지 키 추가 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Http/Controllers/Api/V1/AccountSubjectController.php` | 신규 생성 | -| `app/Http/Controllers/Api/V1/GeneralJournalEntryController.php` | 신규 생성 | -| `app/Http/Requests/V1/AccountSubject/StoreAccountSubjectRequest.php` | 신규 생성 | -| `app/Http/Requests/V1/GeneralJournalEntry/StoreManualJournalRequest.php` | 신규 생성 | -| `app/Http/Requests/V1/GeneralJournalEntry/UpdateJournalRequest.php` | 신규 생성 | -| `app/Models/Tenants/AccountCode.php` | 신규 생성 | -| `app/Models/Tenants/JournalEntry.php` | 신규 생성 | -| `app/Models/Tenants/JournalEntryLine.php` | 신규 생성 | -| `app/Services/AccountCodeService.php` | 신규 생성 | -| `app/Services/GeneralJournalEntryService.php` | 신규 생성 | -| `lang/ko/error.php` | 수정 | -| `lang/ko/message.php` | 수정 | -| `routes/api/v1/finance.php` | 수정 | - ---- - -## 5. `🔧 수정` [finance] 일반전표 source 필드 및 페이지네이션 수정 - -**커밋**: `816c25a` | **유형**: fix (신규 기능 후속 수정) - -### 배경 -입금/출금 조회 시 source가 CASE WHEN으로 불필요하게 분기되었고, 페이지네이션 응답 구조가 프론트엔드 기대와 불일치. - -### 구현 내용 -- deposits/withdrawals 조회 시 source를 항상 `'linked'`로 고정 -- 페이지네이션 meta 래핑 제거 → 플랫 구조로 변경 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Services/GeneralJournalEntryService.php` | 수정 | - ---- - -## 6. `🆕 신규` [menu] 즐겨찾기 테이블 마이그레이션 - -**커밋**: `a67c5d9` | **유형**: feat - -### 배경 -사용자별 메뉴 즐겨찾기 기능을 위한 데이터 테이블 필요. - -### 구현 내용 -- `menu_favorites` 테이블 — tenant_id, user_id, menu_id, sort_order -- unique 제약: (tenant_id, user_id, menu_id) -- FK cascade delete: users, menus - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `database/migrations/2026_03_06_143037_create_menu_favorites_table.php` | 신규 생성 | - ---- - -## 7. `🔧 수정` [departments] options JSON 컬럼 추가 - -**커밋**: `56e7164` | **유형**: feat (기존 테이블 확장) - -### 배경 -조직도 숨기기 등 부서별 확장 속성을 저장할 JSON 컬럼 필요. - -### 구현 내용 -- `departments` 테이블에 `options` JSON 컬럼 추가 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `database/migrations/..._add_options_to_departments_table.php` | 신규 생성 | - ---- - -## 8. `🆕 신규` [approval] 결재양식 마이그레이션 (6건) - -**커밋**: `58fedb0`, `eb28b57`, `c5a0115`, `9d4143a`, `449fce1`, `96def0d` | **유형**: feat - -### 배경 -전자결재 양식 확대 — 사용인감계, 사직서, 위임장, 이사회의사록, 견적서, 공문서 양식 추가. - -### 구현 내용 -- `seal_usage` — 사용인감계 양식 -- `resignation` — 사직서 양식 -- `delegation` — 위임장 양식 -- `board_minutes` — 이사회의사록 양식 -- `quotation` — 견적서 양식 -- `official_letter` — 공문서 양식 -- 전체 테넌트에 자동 등록 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `database/migrations/2026_03_06_100000_add_resignation_form.php` | 신규 생성 | -| `database/migrations/2026_03_06_210000_add_seal_usage_form.php` | 신규 생성 | -| `database/migrations/2026_03_06_230000_add_delegation_form.php` | 신규 생성 | -| `database/migrations/2026_03_06_233000_add_board_minutes_form.php` | 신규 생성 | -| `database/migrations/2026_03_06_235000_add_quotation_form.php` | 신규 생성 | -| `database/migrations/2026_03_07_000000_add_official_letter_form.php` | 신규 생성 | - ---- - -## 9. `🆕 신규` [database] 경조사비 관리 테이블 + 메뉴 추가 - -**커밋**: `0ea5fa5`, `22160e5` | **유형**: feat - -### 배경 -거래처 경조사비 관리대장 기능 신규 도입. 데이터 테이블 및 사이드바 메뉴 추가 필요. - -### 구현 내용 -- `condolence_expenses` 테이블 — 경조사일자, 지출일자, 거래처명, 내역, 구분(축의/부조), 부조금, 선물, 총금액 -- 각 테넌트의 부가세관리 메뉴 하위에 경조사비관리 메뉴 자동 추가 (중복 방지) - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `database/migrations/..._create_condolence_expenses_table.php` | 신규 생성 | -| `database/migrations/..._add_condolence_expenses_menu.php` | 신규 생성 | - ---- - -## 10. `🆕 신규` [문서스냅샷] rendered_html 저장 지원 + Lazy Snapshot API - -**커밋**: `293330c`, `5ebf940`, `c5d5b5d` | **유형**: feat + fix - -### 배경 -문서의 렌더링된 HTML을 스냅샷으로 저장하여 PDF 변환/인쇄 등에 활용. 편집 권한 없이도 스냅샷 갱신 가능한 Lazy Snapshot API 필요. - -### 구현 내용 -- `Document` 모델 $fillable에 `rendered_html` 추가 (🔧) -- `DocumentService` create/update에서 rendered_html 저장 (🔧) -- Store/Update/UpsertRequest에 `rendered_html` 검증 추가 (🔧) -- `WorkOrderService` 검사문서/작업일지 생성 시 rendered_html 전달 (🔧) -- `PATCH /documents/{id}/snapshot` — canEdit 체크 없이 rendered_html만 업데이트 (🆕) -- `resolveInspectionDocument()`에 `snapshot_document_id` 반환 (🆕) - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Models/Documents/Document.php` | 수정 | -| `app/Services/DocumentService.php` | 수정 | -| `app/Services/WorkOrderService.php` | 수정 | -| `app/Http/Requests/Document/StoreRequest.php` | 수정 | -| `app/Http/Requests/Document/UpdateRequest.php` | 수정 | -| `app/Http/Requests/Document/UpsertRequest.php` | 수정 | -| `app/Http/Controllers/Api/V1/Documents/DocumentController.php` | 수정 | -| `routes/api/v1/documents.php` | 수정 | - ---- - -## 11. `🔧 수정` [품질관리] order_ids 영속성 + location 데이터 저장 - -**커밋**: `f2eede6` | **유형**: feat (기존 API 확장) - -### 배경 -품질관리서에 수주 연결 및 개소별 검사 데이터(시공규격, 변경사유, 검사결과)를 저장해야 함. - -### 구현 내용 -- StoreRequest/UpdateRequest에 `order_ids`, `locations` 검증 추가 -- `QualityDocumentLocation`에 `inspection_data`(JSON) fillable/cast 추가 -- store()에 `syncOrders` 연동, update()에 `syncOrders` + `updateLocations` 연동 -- `inspection_data` 컬럼 추가 마이그레이션 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Http/Requests/Quality/QualityDocumentStoreRequest.php` | 수정 | -| `app/Http/Requests/Quality/QualityDocumentUpdateRequest.php` | 수정 | -| `app/Models/Qualitys/QualityDocumentLocation.php` | 수정 | -| `app/Services/QualityDocumentService.php` | 수정 (대규모) | -| `database/migrations/..._inspection_data_to_quality_document_locations.php` | 신규 생성 | - ---- - -## 12. `🆕 신규` 제품검사 요청서 Document(EAV) 자동생성 및 동기화 - -**커밋**: `2231c9a` | **유형**: feat - -### 배경 -품질관리서 생성/수정/수주연결 시 제품검사 요청서 Document를 EAV 방식으로 자동 생성하고 동기화해야 함. - -### 구현 내용 -- `document_template_sections`에 `description` 컬럼 추가 -- `QualityDocumentService`에 `syncRequestDocument()` 메서드 추가 -- 기본필드, 섹션 데이터, 사전고지 테이블 EAV 자동매핑 -- `rendered_html` 초기화 (데이터 변경 시 재캡처 트리거) -- `transformToFrontend`에 `request_document_id` 포함 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Models/Documents/DocumentTemplateSection.php` | 수정 | -| `app/Services/QualityDocumentService.php` | 수정 (대규모) | -| `database/migrations/..._add_description_to_document_template_sections.php` | 신규 생성 | - ---- - -## 13. `⚙️ 설정` [API] logging, docs, seeder 등 부수 정리 - -**커밋**: `ff85530` | **유형**: chore - -### 배경 -여러 파일의 경로, 설정, 문서 등 소소한 정리 작업. - -### 구현 내용 -- `LOGICAL_RELATIONSHIPS.md` 보완 (최신 모델 관계 반영) -- `Legacy5130Calculator` 수정 -- `logging.php` 설정 추가 -- `KyungdongItemSeeder` 수정 -- docs 문서 경로 수정 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `LOGICAL_RELATIONSHIPS.md` | 수정 | -| `app/Helpers/Legacy5130Calculator.php` | 수정 | -| `config/logging.php` | 수정 | -| `database/seeders/Kyungdong/KyungdongItemSeeder.php` | 수정 | -| `docs/INDEX.md` | 수정 | diff --git a/claudedocs/backend/2026-03-07_구현내역.md b/claudedocs/backend/2026-03-07_구현내역.md deleted file mode 100644 index 3381b8ee..00000000 --- a/claudedocs/backend/2026-03-07_구현내역.md +++ /dev/null @@ -1,40 +0,0 @@ -# 2026-03-07 (토) 백엔드 구현 내역 - -## 1. `🆕 신규` [approval] 연차사용촉진 통지서 1차/2차 양식 마이그레이션 - -**커밋**: `ad93743` | **유형**: feat - -### 배경 -근로기준법에 따른 연차사용촉진 통지서(1차/2차) 양식을 전자결재 시스템에 등록 필요. - -### 구현 내용 -- `leave_promotion_1st` — 연차사용촉진 통지서 (1차) 양식, hr 카테고리 -- `leave_promotion_2nd` — 연차사용촉진 통지서 (2차) 양식, hr 카테고리 -- 전체 테넌트에 자동 등록 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `database/migrations/2026_03_07_100000_add_leave_promotion_forms.php` | 신규 생성 | - ---- - -## 2. `🔧 수정` [품질검사] 수주 선택 필터링 + 개소 상세 + 검사 상태 개선 - -**커밋**: `3ac64d5` | **유형**: feat (기존 API 확장) - -### 배경 -품질관리서 작성 시 수주 선택 API에 거래처/품목 필터가 없고, 개소별 상세 데이터 부족. 검사 상태 판별 로직도 개선 필요. - -### 구현 내용 -- `availableOrders` — `client_id`/`item_id` 필터 파라미터 지원 -- 응답에 `client_id`, `client_name`, `item_id`, `item_name`, `locations`(개소 상세) 추가 -- `show` — 개소별 데이터에 거래처/모델 정보 포함 -- `DocumentService` — `fqcStatus`를 rootNodes 기반으로 변경 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Services/QualityDocumentService.php` | 수정 | -| `app/Services/DocumentService.php` | 수정 | -| `LOGICAL_RELATIONSHIPS.md` | 수정 | diff --git a/claudedocs/backend/2026-03-08_구현내역.md b/claudedocs/backend/2026-03-08_구현내역.md deleted file mode 100644 index 6600870a..00000000 --- a/claudedocs/backend/2026-03-08_구현내역.md +++ /dev/null @@ -1,47 +0,0 @@ -# 2026-03-08 (일) 백엔드 구현 내역 - -## 1. `🔧 수정` [finance] 계정과목 확장 및 전표 연동 시스템 구현 - -**커밋**: `0044779` | **유형**: feat (3/6 신규 기능 대규모 확장) - -### 배경 -3/6에 추가한 계정과목/일반전표 기본 API를 확장하여 기본 계정과목 시딩, 전표 자동 연동(카드거래/세금계산서), 계정과목 업데이트 기능 구현. - -### 구현 내용 - -#### 계정과목 확장 (🔧 기존 확장) -- `AccountCode` 모델 확장 — 관계, 스코프, 헬퍼 추가 -- `AccountCodeService` 확장 — 업데이트, 트리 조회, 기본 계정과목 시딩 로직 -- `UpdateAccountSubjectRequest` 신규 — 업데이트 검증 규칙 -- `StoreAccountSubjectRequest` — 추가 검증 규칙 보강 - -#### 전표 자동 연동 (🆕 신규) -- `JournalSyncService` 신규 — 카드거래/세금계산서 → 전표 자동 생성 서비스 -- `SyncsExpenseAccounts` 트레이트 — 경비계정 동기화 공통 로직 -- `CardTransactionController` 확장 — 전표 연동 엔드포인트 추가 -- `TaxInvoiceController` 확장 — 전표 연동 엔드포인트 추가 - -#### 데이터베이스 (🆕 신규) -- `expense_accounts` 테이블에 전표 연결 컬럼 마이그레이션 (journal_entry_id 등) -- `account_codes` 테이블 확장 마이그레이션 (추가 속성 컬럼) -- 전체 테넌트 기본 계정과목 시딩 마이그레이션 - -### 변경 파일 -| 파일 | 작업 | -|------|------| -| `app/Http/Controllers/Api/V1/AccountSubjectController.php` | 수정 | -| `app/Http/Controllers/Api/V1/CardTransactionController.php` | 수정 (대규모) | -| `app/Http/Controllers/Api/V1/TaxInvoiceController.php` | 수정 (대규모) | -| `app/Http/Requests/V1/AccountSubject/StoreAccountSubjectRequest.php` | 수정 | -| `app/Http/Requests/V1/AccountSubject/UpdateAccountSubjectRequest.php` | 신규 생성 | -| `app/Models/Tenants/AccountCode.php` | 수정 | -| `app/Models/Tenants/ExpenseAccount.php` | 수정 | -| `app/Models/Tenants/JournalEntry.php` | 수정 | -| `app/Services/AccountCodeService.php` | 수정 (대규모) | -| `app/Services/GeneralJournalEntryService.php` | 수정 | -| `app/Services/JournalSyncService.php` | 신규 생성 | -| `app/Traits/SyncsExpenseAccounts.php` | 신규 생성 | -| `database/migrations/..._add_journal_link_to_expense_accounts_table.php` | 신규 생성 | -| `database/migrations/..._enhance_account_codes_table.php` | 신규 생성 | -| `database/migrations/..._seed_default_account_codes_for_all_tenants.php` | 신규 생성 | -| `routes/api/v1/finance.php` | 수정 | diff --git a/claudedocs/backend/_index.md b/claudedocs/backend/_index.md deleted file mode 100644 index 0d32c0cd..00000000 --- a/claudedocs/backend/_index.md +++ /dev/null @@ -1,72 +0,0 @@ -# SAM API 백엔드 구현 내역서 - -## 2026년 3월 1주차 (3/2 ~ 3/8) - -총 **83개 커밋**, 7일간 구현 내역 - -### 태그 범례 -| 태그 | 의미 | -|------|------| -| `🆕 신규` | 새로운 기능/API/테이블 생성 | -| `🔧 수정` | 기존 기능 버그 수정, 확장, 보완 | -| `🔄 리팩토링` | 기능 변경 없이 코드 구조 개선 | -| `⚙️ 설정` | 환경 설정, 인프라, 문서 정리 | - -### 날짜별 문서 - -| 날짜 | 파일 | 주요 작업 | 🆕 | 🔧 | 🔄 | ⚙️ | -|------|------|-----------|-----|-----|-----|-----| -| 3/2 (월) | [2026-03-02_구현내역.md](./2026-03-02_구현내역.md) | 로드맵 테이블, AI 견적 엔진 | 2 | - | - | - | -| 3/3 (화) | [2026-03-03_구현내역.md](./2026-03-03_구현내역.md) | Gemini 업그레이드, 배포 수정, HR 확장, 자재투입 개선 | - | 7 | - | 1 | -| 3/4 (수) | [2026-03-04_구현내역.md](./2026-03-04_구현내역.md) | 바로빌 연동, 리스크 대시보드, 지출결의서, 배차 시스템 | 6 | 9 | - | - | -| 3/5 (목) | [2026-03-05_구현내역.md](./2026-03-05_구현내역.md) | CEO 대시보드, 어음 V8, 상품권 접대비, 생산지시, 품질관리 | 7 | 7 | 2 | 1 | -| 3/6 (금) | [2026-03-06_구현내역.md](./2026-03-06_구현내역.md) | 계정과목/일반전표, 문서 스냅샷, 결재양식 6종, 경조사비 | 7 | 5 | - | 1 | -| 3/7 (토) | [2026-03-07_구현내역.md](./2026-03-07_구현내역.md) | 연차촉진 통지서, 품질검사 필터링 | 1 | 1 | - | - | -| 3/8 (일) | [2026-03-08_구현내역.md](./2026-03-08_구현내역.md) | 계정과목 확장, 전표 연동 시스템 | - | 1 | - | - | -| **합계** | | | **23** | **30** | **2** | **3** | - -### 도메인별 주요 기능 - -#### 재무/회계 -- 🆕 계정과목 및 일반전표 API 신규 구축 -- 🆕 전표 자동 연동 (카드거래/세금계산서) -- 🆕 접대비 상세 조회 API + 리스크 감지 -- 🆕 부가세 상세 조회 API -- 🆕 경조사비 관리 테이블 -- 🆕 바로빌 연동 API -- 🔧 접대비/복리후생비 리스크 감지형 대시보드 전환 -- 🔧 매출채권 상세 대시보드 개선 -- 🔧 가지급금 카테고리 분류 (카드/경조사/상품권/접대비) -- 🔧 상품권 접대비 자동 연동 -- 🔧 어음 V8 확장 필드 (54개) - -#### 생산/품질 -- 🆕 생산지시 전용 API (목록/통계/상세) -- 🆕 품질관리서 CRUD API (14개 엔드포인트) -- 🆕 실적신고 관리 API (6개 엔드포인트) -- 🆕 제품검사 요청서 EAV 자동생성 -- 🆕 보조 공정(재고생산) 분리 -- 🔧 절곡 검사 데이터 복제/EAV 변환 -- 🔧 자재투입 bom_group_key/replace 모드 - -#### 전자결재 -- 🆕 Document ↔ Approval 브릿지 연동 -- 🆕 결재양식 11종 추가 (지출결의서, 근태신청, 사유서, 재직증명서 등) -- 🔧 drafter_read_at, resubmit_count, rejection_history 컬럼 - -#### 대시보드/리포트 -- 🆕 CEO 대시보드 6개 섹션 API -- 🆕 일일보고서 엑셀 내보내기 -- 🔧 자금현황 카드 필드 - -#### 출고/배차 -- 🆕 배차정보 다중 행 시스템 -- 🆕 배차차량 관리 API - -#### 인프라/기타 -- ⚙️ Gemini 2.5-flash 업그레이드 -- 🔧 .env 권한 640 보장 (배포) -- ⚙️ Slack 알림 채널 분리 -- 🆕 문서 rendered_html 스냅샷 API -- 🆕 메뉴 즐겨찾기 테이블 -- 🔧 주소 필드 500자 확장 diff --git a/claudedocs/board/[IMPL-2025-12-30] dynamic-board-creation.md b/claudedocs/board/[IMPL-2025-12-30] dynamic-board-creation.md deleted file mode 100644 index 97230f95..00000000 --- a/claudedocs/board/[IMPL-2025-12-30] dynamic-board-creation.md +++ /dev/null @@ -1,120 +0,0 @@ -# 게시판 동적 생성 구현 - -> 작성일: 2025-12-30 -> 상태: 완료 - -## 개요 - -게시판 관리에서 게시판을 등록하면 고객센터 메뉴에 자동으로 추가되고, -해당 게시판 페이지가 동적으로 렌더링되도록 구현합니다. - ---- - -## 작업 목록 - -### Phase 1: 게시판 관리 폼 수정 - -- [x] 1.1 대상 옵션에 "권한" 추가 - - 현재: 전사, 부서 - - 변경: 전사, 부서, **권한** - - 파일: `src/components/board/BoardManagement/types.ts` -- [x] 1.2 권한 선택 시 다중 선택 체크박스 표시 - - 파일: `src/components/board/BoardManagement/BoardForm.tsx` - - MOCK_PERMISSIONS: 관리자, 매니저, 직원, 게스트 -- [x] 1.3 API 요청 데이터에 권한 정보 포함 - - 파일: `src/components/board/BoardManagement/actions.ts` - - transformFrontendToApi: permissions → extra_settings.permissions - -### Phase 2: 메뉴 즉시 갱신 - -- [x] 2.1 게시판 등록 성공 후 `forceRefreshMenus()` 호출 - - 파일: `src/app/[locale]/(protected)/board/board-management/new/page.tsx` -- [x] 2.2 게시판 수정 성공 후 `forceRefreshMenus()` 호출 - - 파일: `src/app/[locale]/(protected)/board/board-management/[id]/edit/page.tsx` - -### Phase 3: 동적 게시판 라우트 생성 - -- [x] 3.1 `/customer-center/[boardCode]/page.tsx` - 리스트 -- [x] 3.2 `/customer-center/[boardCode]/[postId]/page.tsx` - 상세 -- [x] 3.3 `/customer-center/[boardCode]/create/page.tsx` - 등록 -- [x] 3.4 `/customer-center/[boardCode]/[postId]/edit/page.tsx` - 수정 - -### Phase 4: 테스트 및 검증 - -- [ ] 4.1 게시판 등록 → 메뉴 자동 추가 확인 -- [ ] 4.2 동적 게시판 리스트/상세/등록/수정 동작 확인 -- [ ] 4.3 권한별 접근 제어 확인 - ---- - -## 기술 명세 - -### 대상 타입 - -| 대상 | 옆 셀렉트박스 | API 필드 | -|------|---------------|----------| -| 전사 | 없음 | `target: 'all'` | -| 부서 | 부서 단일 선택 | `target: 'department', target_id: number` | -| 권한 | 권한 다중 선택 (체크박스) | `target: 'permission', permissions: string[]` | - -### 게시판 타입 - -- **기본 타입**: 1:1문의 형태 (댓글 사용 가능) -- **참고 페이지**: `/customer-center/qna` - -### 메뉴 갱신 플로우 - -``` -게시판 등록 API 호출 (POST /api/v1/boards) - ↓ -백엔드: 게시판 생성 + 메뉴 테이블에 추가 - ↓ -프론트: 등록 성공 응답 받음 - ↓ -프론트: forceRefreshMenus() 호출 - ↓ -사이드바 메뉴 즉시 업데이트 -``` - -### 동적 게시판 URL 구조 - -``` -/boards/[boardCode] → 목록 -/boards/[boardCode]/create → 등록 -/boards/[boardCode]/[postId] → 상세 -/boards/[boardCode]/[postId]/edit → 수정 -``` - -> **URL 변경 이력 (2025-12-30)** -> - 변경 전: `/customer-center/[boardCode]` -> - 변경 후: `/boards/[boardCode]` -> - 사유: 백엔드 메뉴 API path 규칙에 맞춤 (`/boards/free`, `/boards/board_xxx`) - ---- - -## 관련 파일 - -### 수정된 파일 -- `src/components/board/BoardManagement/types.ts` - BoardTarget에 'permission' 추가 -- `src/components/board/BoardManagement/BoardForm.tsx` - 권한 다중 선택 UI 추가 -- `src/components/board/BoardManagement/actions.ts` - permissions 변환 로직 -- `src/components/customer-center/shared/types.ts` - SystemBoardCode 확장 -- `src/app/[locale]/(protected)/board/board-management/new/page.tsx` - forceRefreshMenus 호출 -- `src/app/[locale]/(protected)/board/board-management/[id]/edit/page.tsx` - forceRefreshMenus 호출 - -### 새로 생성된 파일 -- `src/app/[locale]/(protected)/boards/[boardCode]/page.tsx` - 동적 게시판 목록 -- `src/app/[locale]/(protected)/boards/[boardCode]/[postId]/page.tsx` - 동적 게시판 상세 -- `src/app/[locale]/(protected)/boards/[boardCode]/create/page.tsx` - 동적 게시판 등록 -- `src/app/[locale]/(protected)/boards/[boardCode]/[postId]/edit/page.tsx` - 동적 게시판 수정 - ---- - -## 진행 로그 - -| 날짜 | 작업 내용 | -|------|----------| -| 2025-12-30 | 요구사항 정리 및 체크리스트 생성 | -| 2025-12-30 | Phase 1~3 구현 완료 | -| 2025-12-30 | URL 경로 변경: `/customer-center/[boardCode]` → `/boards/[boardCode]` | -| 2025-12-30 | API URL 불일치 해결: `system-boards` → `boards` (DynamicBoard/actions.ts 생성) | \ No newline at end of file diff --git a/claudedocs/board/[PLAN-2025-12-19] board-management-implementation.md b/claudedocs/board/[PLAN-2025-12-19] board-management-implementation.md deleted file mode 100644 index 7e690b5b..00000000 --- a/claudedocs/board/[PLAN-2025-12-19] board-management-implementation.md +++ /dev/null @@ -1,313 +0,0 @@ -ㅓ# 게시판 관리 기능 구현 계획서 - -> 작성일: 2025-12-19 -> 상태: 🔴 **계획 검토 대기** - ---- - -## 1. 개요 - -### 1.1 기능 요약 -게시판 리스트/등록/상세/댓글 기능 구현 - -### 1.2 참고 페이지 -- **탭 네비게이션**: `src/components/items/ItemListClient.tsx` (품목관리) -- **테이블 필터 위치**: `src/components/accounting/SalesManagement/index.tsx` (매출관리) -- **공통 레이아웃**: `IntegratedListTemplateV2` 템플릿 - -### 1.3 디자인 스펙 (스크린샷 기준) - -#### 리스트 페이지 -| 항목 | 내용 | -|------|------| -| 페이지 타이틀 | 게시판 | -| 페이지 설명 | 게시판의 게시글을 등록하고 관리합니다. | -| 날짜 범위 | 2025-09-01 ~ 2025-09-03 | -| 탭 | 전체보드, 전자결재, 인쇄, 미역, 우울 + **게시글 등록** 버튼 | -| 게시판 필터 | 공지사항 (게시판명, 게시판명2, 나의 게시글) | -| 검색 | 검색 입력창 | -| 정렬 | 최신순 ▼ | -| 테이블 컬럼 | No., 제목, 작성자, 등록일, 조회수 | -| 행 클릭 | 게시글 상세 화면으로 이동 | - -#### 등록/수정 페이지 -| 항목 | 내용 | -|------|------| -| 페이지 타이틀 | 게시글 상세 | -| 페이지 설명 | 게시글을 등록하고 관리합니다. | -| **게시글 정보** (필수 섹션) | | -| - 게시판 | Select: "게시판을 선택해주세요" | -| - 상단 노출 | Radio: 사용안함 / **사용함** | -| - 제목 | Input: "제목을 입력해주세요" | -| - 내용 | **WYSIWYG 에디터** (B, I, U, S, 정렬, 목록, 링크, 이미지 등) | -| - 첨부파일 | 파일 찾기 버튼 | -| - 작성자 | 읽기 전용 (예: 홍길동) | -| - 댓글 | Radio: **사용안함** / 사용함 | -| - 등록일시 | 읽기 전용 (예: 2025-09-09 12:20) | -| 상단 노출 제한 | 최대 5개까지 설정 가능, 초과 시 Alert 표시 | - -#### 상세 페이지 -| 항목 | 내용 | -|------|------| -| 페이지 타이틀 | 게시글 상세 | -| 페이지 설명 | 게시글을 조회합니다. | -| 버튼 (본인 글만) | **삭제**, **수정** | -| 게시판명 라벨 | 게시판명 | -| 제목 | 제목 | -| 메타 정보 | 작성자 \| 날짜 \| 조회수 (예: 홍길동 \| 2025-09-03 12:23 \| 조회수 123) | -| 내용 | HTML 콘텐츠 (이미지 포함) | -| 첨부파일 | 다운로드 링크 (예: abc.pdf) | -| 댓글 등록 | Textarea + 등록 버튼 (댓글 사용함 설정 시만 표시) | - -#### 댓글 섹션 -| 항목 | 내용 | -|------|------| -| 댓글 수 | 댓글 N | -| 댓글 정보 | 프로필 이미지, 부서명 이름 직책, 등록일시, 댓글 내용 | -| 수정 버튼 (본인만) | 클릭 시 인풋박스에 기존 댓글 내용 입력 상태로 변경 | -| 삭제 버튼 (본인만) | 클릭 시 **"정말 삭제하시겠습니까?"** 확인 Alert 표시 | - ---- - -## 2. WYSIWYG 에디터 추천 - -### 2.1 옵션 비교 - -| 라이브러리 | 장점 | 단점 | 추천도 | -|------------|------|------|--------| -| **TipTap** | 최신, Headless (커스텀 자유), React 네이티브, shadcn/ui 호환 | 학습 곡선 | ⭐⭐⭐⭐⭐ | -| **CKEditor 5** | 기능 풍부, 엔터프라이즈급, 이미지 업로드 내장 | 무거움, 스타일 충돌, 라이선스 | ⭐⭐⭐⭐ | -| **Quill** | 간단, 가벼움 | 구식 스타일, 유지보수 부족 | ⭐⭐⭐ | -| **Editor.js** | 블록 기반, 노션 스타일 | JSON 출력 (HTML 아님), 변환 필요 | ⭐⭐⭐ | -| **React-Quill** | Quill + React 래퍼 | Next.js SSR 이슈 | ⭐⭐ | - -### 2.2 최종 추천: **TipTap** - -**이유**: -1. **Headless 아키텍처**: shadcn/ui와 Tailwind CSS 완벽 호환 -2. **모던 React**: useState/useEffect 패턴, TypeScript 완벽 지원 -3. **확장성**: 필요한 기능만 설치 (경량화) -4. **커뮤니티**: 활발한 개발, 문서화 우수 -5. **이미지 업로드**: 커스텀 핸들러로 S3/백엔드 연동 용이 - -### 2.3 필요 패키지 (TipTap) - -```bash -npm install @tiptap/react @tiptap/pm @tiptap/starter-kit \ - @tiptap/extension-image @tiptap/extension-link \ - @tiptap/extension-underline @tiptap/extension-text-align \ - @tiptap/extension-placeholder -``` - ---- - -## 3. 파일 구조 - -``` -src/ -├── app/[locale]/(protected)/board/ -│ ├── page.tsx # 게시판 리스트 페이지 -│ ├── create/ -│ │ └── page.tsx # 게시글 등록 페이지 -│ └── [id]/ -│ ├── page.tsx # 게시글 상세 페이지 -│ └── edit/ -│ └── page.tsx # 게시글 수정 페이지 -│ -├── components/board/ -│ ├── BoardList/ -│ │ ├── index.tsx # 리스트 메인 컴포넌트 -│ │ └── types.ts # 타입 정의 -│ ├── BoardForm/ -│ │ ├── index.tsx # 등록/수정 폼 -│ │ └── types.ts -│ ├── BoardDetail/ -│ │ ├── index.tsx # 상세 보기 -│ │ └── types.ts -│ ├── CommentSection/ -│ │ ├── index.tsx # 댓글 섹션 -│ │ ├── CommentItem.tsx # 개별 댓글 컴포넌트 -│ │ └── types.ts -│ └── RichTextEditor/ -│ ├── index.tsx # TipTap 에디터 래퍼 -│ ├── MenuBar.tsx # 에디터 툴바 -│ └── extensions.ts # TipTap 확장 설정 -│ -└── hooks/ - └── useBoardList.ts # 게시판 목록 API 훅 -``` - ---- - -## 4. 구현 체크리스트 - -### Phase 1: 기반 작업 (에디터 + 타입) - -- [ ] **1.1** TipTap 패키지 설치 -- [ ] **1.2** `RichTextEditor` 컴포넌트 구현 - - [ ] 1.2.1 기본 에디터 (Bold, Italic, Underline, Strike) - - [ ] 1.2.2 텍스트 정렬 (좌/중/우) - - [ ] 1.2.3 목록 (Bullet, Ordered) - - [ ] 1.2.4 링크 삽입 - - [ ] 1.2.5 이미지 업로드 (파일 선택 → 백엔드 업로드 → URL 삽입) -- [ ] **1.3** `types.ts` 정의 - - [ ] 1.3.1 Board (게시판 타입) - - [ ] 1.3.2 Post (게시글 타입) - - [ ] 1.3.3 Comment (댓글 타입) - -### Phase 2: 리스트 페이지 - -- [ ] **2.1** `BoardList/index.tsx` 구현 - - [ ] 2.1.1 IntegratedListTemplateV2 적용 - - [ ] 2.1.2 DateRangeSelector (날짜 범위) - - [ ] 2.1.3 탭 네비게이션 (전체보드, 전자결재, 인쇄, 미역, 우울) - - [ ] 2.1.4 게시판 필터 (공지사항 드롭다운) - - [ ] 2.1.5 검색 입력창 - - [ ] 2.1.6 정렬 드롭다운 (최신순) - - [ ] 2.1.7 총 N건 표시 -- [ ] **2.2** 테이블 구현 - - [ ] 2.2.1 컬럼: No., 제목, 작성자, 등록일, 조회수 - - [ ] 2.2.2 체크박스 선택 - - [ ] 2.2.3 행 클릭 → 상세 이동 -- [ ] **2.3** 모바일 카드 뷰 구현 -- [ ] **2.4** 페이지네이션 구현 -- [ ] **2.5** `page.tsx` 라우트 생성 - -### Phase 3: 등록/수정 페이지 - -- [ ] **3.1** `BoardForm/index.tsx` 구현 - - [ ] 3.1.1 게시판 Select (게시판 목록) - - [ ] 3.1.2 상단 노출 Radio (사용안함/사용함) - - [ ] 3.1.2.1 최대 5개 제한 Alert - - [ ] 3.1.3 제목 Input - - [ ] 3.1.4 내용 RichTextEditor - - [ ] 3.1.5 첨부파일 업로드 (다중 파일) - - [ ] 3.1.6 작성자 표시 (읽기 전용) - - [ ] 3.1.7 댓글 Radio (사용안함/사용함) - - [ ] 3.1.8 등록일시 표시 (읽기 전용) - - [ ] 3.1.9 등록 버튼 -- [ ] **3.2** 수정 모드 구현 (기존 데이터 로드) -- [ ] **3.3** 유효성 검사 - - [ ] 3.3.1 필수 필드: 게시판, 제목, 내용 -- [ ] **3.4** `create/page.tsx` 라우트 생성 -- [ ] **3.5** `[id]/edit/page.tsx` 라우트 생성 - -### Phase 4: 상세 페이지 - -- [ ] **4.1** `BoardDetail/index.tsx` 구현 - - [ ] 4.1.1 게시판명 라벨 - - [ ] 4.1.2 제목 - - [ ] 4.1.3 메타 정보 (작성자 | 날짜 | 조회수) - - [ ] 4.1.4 내용 (HTML 렌더링) - - [ ] 4.1.5 첨부파일 다운로드 링크 -- [ ] **4.2** 삭제/수정 버튼 (본인 글만 표시) - - [ ] 4.2.1 삭제 버튼 → **AlertDialog** ("정말 삭제하시겠습니까?") - - [ ] 4.2.2 수정 버튼 → 수정 페이지 이동 -- [ ] **4.3** `[id]/page.tsx` 라우트 생성 - -### Phase 5: 댓글 기능 - -- [ ] **5.1** `CommentSection/index.tsx` 구현 - - [ ] 5.1.1 댓글 등록 Textarea + 버튼 - - [ ] 5.1.2 댓글 수 표시 ("댓글 N") -- [ ] **5.2** `CommentItem.tsx` 구현 - - [ ] 5.2.1 프로필 이미지 - - [ ] 5.2.2 부서명 이름 직책 - - [ ] 5.2.3 등록일시 - - [ ] 5.2.4 댓글 내용 - - [ ] 5.2.5 수정 버튼 (본인만) → 인라인 수정 모드 - - [ ] 5.2.6 삭제 버튼 (본인만) → **AlertDialog** -- [ ] **5.3** 댓글 CRUD API 연동 - -### Phase 6: API 연동 + 마무리 - -- [ ] **6.1** Mock 데이터 → 실제 API 연동 -- [ ] **6.2** 에러 핸들링 (toast 알림) -- [ ] **6.3** 로딩 상태 UI -- [ ] **6.4** 반응형 테스트 (모바일/태블릿/데스크톱) -- [ ] **6.5** 접근 권한 테스트 (본인 글/타인 글) - -### Phase 7: 문서화 - -- [ ] **7.1** 테스트 URL 문서 업데이트 (`[REF] all-pages-test-urls.md`) - - [ ] 7.1.1 게시판 섹션 신규 추가 (기존 구역과 별도) - - [ ] 7.1.2 메인 리스트 URL만 등록 -- [ ] **7.2** 이 문서 완료 처리 - ---- - -## 5. 주의사항 (버디가 자주 틀리는 것들) - -### 5.1 디스크립션 확인 필수 -- [ ] 리스트: "게시판의 게시글을 등록하고 관리합니다." -- [ ] 등록: "게시글을 등록하고 관리합니다." -- [ ] 상세: "게시글을 조회합니다." - -### 5.2 테이블 컬럼 타이틀 정확히 -- [ ] No. (번호 아님) -- [ ] 제목 -- [ ] 작성자 -- [ ] 등록일 -- [ ] 조회수 - -### 5.3 카드/라벨 텍스트 정확히 -- [ ] "게시판" (게시판명 아님, Select label) -- [ ] "상단 노출" (상단고정 아님) -- [ ] "댓글" (댓글허용 아님) -- [ ] "댓글 N" (댓글 수 표시) - -### 5.4 팝업 메시지 정확히 -- [ ] 삭제 확인: **"정말 삭제하시겠습니까?"** -- [ ] 상단 노출 초과: **"상단 노출은 5개까지 설정 가능합니다."** - -### 5.5 본인 글/댓글 체크 -- [ ] 게시글 삭제/수정 버튼 → 본인 글만 -- [ ] 댓글 수정/삭제 버튼 → 본인 댓글만 - ---- - -## 6. 기술 스택 - -| 항목 | 기술 | -|------|------| -| 프레임워크 | Next.js 14 App Router | -| UI 컴포넌트 | shadcn/ui | -| 스타일링 | Tailwind CSS | -| 에디터 | **TipTap** | -| 폼 | React Hook Form (권장) | -| 상태 관리 | React useState/useCallback | -| API | Next.js API Routes (Proxy) | -| 팝업 | AlertDialog, Dialog (Radix UI) | - ---- - -## 7. 작업 완료 후 필수 조치 - -### 7.1 테스트 URL 문서 업데이트 - -`claudedocs/[REF] all-pages-test-urls.md` 파일에 다음 내용 추가: - -```markdown -## 게시판 (Board) - 🆕 NEW SECTION - -| 페이지 | URL | 비고 | -|--------|-----|------| -| 게시판 목록 | `/ko/board` | 🆕 NEW | -``` - -> ⚠️ **참고**: 상세/수정/등록 페이지는 메인 리스트에서 접근 가능하므로 별도 등록하지 않음 - -### 7.2 _index.md 업데이트 - -`claudedocs/_index.md`에 board/ 폴더 섹션 추가 - ---- - -## 8. 승인 대기 - -- [ ] 사용자 확인 완료 -- [ ] 작업 시작 - ---- - -*작성: Claude Code* \ No newline at end of file diff --git a/claudedocs/changes/20250108_order_frontend_api_integration.md b/claudedocs/changes/20250108_order_frontend_api_integration.md deleted file mode 100644 index fabe8c51..00000000 --- a/claudedocs/changes/20250108_order_frontend_api_integration.md +++ /dev/null @@ -1,92 +0,0 @@ -# 수주 관리 Frontend API 연동 - -**날짜:** 2025-01-08 -**Phase:** Phase 2 - Frontend 연동 -**관련 Plan:** docs/plans/order-management-plan.md - -## 변경 개요 - -수주 관리 React 페이지들을 백엔드 API와 연동 완료. Mock 데이터를 제거하고 실제 API 호출로 대체. - -## 수정된 파일 - -### 1. `src/components/orders/actions.ts` (신규 생성) -- Server Actions 패턴으로 API 클라이언트 구현 -- 주요 함수: - - `getOrders()`: 수주 목록 조회 - - `getOrderById(id)`: 수주 상세 조회 - - `createOrder(data)`: 수주 등록 - - `updateOrder(id, data)`: 수주 수정 - - `deleteOrder(id)`: 수주 삭제 - - `deleteOrders(ids)`: 수주 일괄 삭제 - - `updateOrderStatus(id, status)`: 수주 상태 변경 - - `getOrderStats()`: 통계 조회 -- 데이터 변환: API snake_case → Frontend camelCase -- 상태 매핑: API 상태(DRAFT, CONFIRMED 등) → Frontend 상태(order_registered, order_confirmed 등) - -### 2. `src/components/orders/index.ts` (수정) -- actions.ts export 추가 -- 타입 충돌 해결 (OrderItem → OrderItemApi) - -### 3. `src/app/[locale]/(protected)/sales/order-management-sales/page.tsx` (수정) -- SAMPLE_ORDERS (~115줄) 제거 -- API 연동 state 추가: `orders`, `apiStats`, `isLoading`, `isDeleting` -- `loadData()` 함수로 API 호출 (getOrders, getOrderStats) -- 삭제 핸들러에 API 호출 추가 (deleteOrder, deleteOrders) -- 로딩 UI 추가 - -### 4. `src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx` (수정) -- SAMPLE_ITEMS, SAMPLE_ORDERS (~250줄) 제거 -- useEffect에서 getOrderById API 호출 -- handleConfirmCancel에서 updateOrderStatus API 호출 -- isCancelling 로딩 상태 적용 - -### 5. `src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx` (수정) -- SAMPLE_ORDER (~50줄) 제거 -- useEffect에서 getOrderById API 호출 -- handleSave에서 updateOrder API 호출 - -### 6. `src/app/[locale]/(protected)/sales/order-management-sales/new/page.tsx` (수정) -- handleSave에서 createOrder API 호출 - -## 기술 패턴 - -### Server Actions 패턴 -```typescript -"use server"; -import { serverFetch } from "@/lib/api/serverFetch"; - -export async function getOrders() { - const response = await serverFetch("/orders"); - // 데이터 변환 로직 -} -``` - -### 데이터 변환 -- API: `order_no`, `client_name`, `site_name` -- Frontend: `orderNo`, `clientName`, `siteName` - -### 상태 매핑 -| API | Frontend | -|-----|----------| -| DRAFT | order_registered | -| CONFIRMED | order_confirmed | -| IN_PROGRESS | production_ordered | -| COMPLETED | shipped | -| CANCELLED | cancelled | - -## 테스트 체크리스트 - -- [ ] 수주 목록 로드 -- [ ] 수주 상세 조회 -- [ ] 수주 등록 (견적 선택 후) -- [ ] 수주 수정 -- [ ] 수주 개별 삭제 -- [ ] 수주 일괄 삭제 -- [ ] 수주 취소 -- [ ] 통계 카드 표시 - -## 연관 작업 - -- Phase 1: Order API 백엔드 구현 (커밋: de19ac9) -- Phase 1.1: OrderController/Service 구현 (진행 중) diff --git a/claudedocs/changes/20250108_order_phase3_advanced_features.md b/claudedocs/changes/20250108_order_phase3_advanced_features.md deleted file mode 100644 index 208d7ad4..00000000 --- a/claudedocs/changes/20250108_order_phase3_advanced_features.md +++ /dev/null @@ -1,113 +0,0 @@ -# 수주 관리 Phase 3 - 고급 기능 - -**날짜:** 2025-01-08 -**Phase:** Phase 3 - 고급 기능 -**관련 Plan:** docs/plans/order-management-plan.md - -## 변경 개요 - -수주 관리 시스템에 견적→수주 변환 및 생산지시 생성 기능 추가. - -## API 추가 사항 - -### 1. 견적에서 수주 생성 -- **Endpoint**: `POST /api/v1/orders/from-quote/{quoteId}` -- **기능**: 기존 견적서를 기반으로 수주를 자동 생성 -- **검증**: 이미 수주가 생성된 견적은 중복 생성 방지 - -### 2. 생산지시 생성 -- **Endpoint**: `POST /api/v1/orders/{id}/production-order` -- **기능**: 확정된 수주에서 작업지시(WorkOrder) 생성 -- **검증**: CONFIRMED 상태의 수주만 생산지시 가능 - -## 수정된 파일 - -### API (Laravel) - -#### 1. `app/Services/OrderService.php` -- `createFromQuote(int $quoteId, array $data)`: 견적→수주 변환 로직 -- `createProductionOrder(int $orderId, array $data)`: 생산지시 생성 로직 -- `generateWorkOrderNo(int $tenantId)`: 작업지시번호 자동 생성 - -#### 2. `app/Http/Controllers/Api/V1/OrderController.php` -- `createFromQuote()`: 견적→수주 액션 -- `createProductionOrder()`: 생산지시 생성 액션 - -#### 3. `app/Http/Requests/Order/CreateFromQuoteRequest.php` (신규) -- 견적→수주 변환 요청 검증 -- 선택 필드: delivery_date, memo - -#### 4. `app/Http/Requests/Order/CreateProductionOrderRequest.php` (신규) -- 생산지시 생성 요청 검증 -- 선택 필드: process_type, assignee_id, team_id, scheduled_date, memo - -#### 5. `routes/api.php` -- `POST /orders/from-quote/{quoteId}`: 견적→수주 라우트 -- `POST /orders/{id}/production-order`: 생산지시 라우트 - -#### 6. `lang/ko/message.php` -- `order.created_from_quote`: 견적에서 수주가 생성되었습니다. -- `order.production_order_created`: 생산지시가 생성되었습니다. - -#### 7. `lang/ko/error.php` -- `order.already_created_from_quote`: 이미 해당 견적에서 수주가 생성되었습니다. -- `order.must_be_confirmed_for_production`: 확정 상태의 수주만 생산지시를 생성할 수 있습니다. -- `order.production_order_already_exists`: 이미 생산지시가 존재합니다. -- `quote.not_found`: 견적을 찾을 수 없습니다. - -### Frontend (React) - -#### 1. `src/components/orders/actions.ts` -- 타입 추가: `CreateFromQuoteData`, `CreateProductionOrderData`, `WorkOrder`, `ProductionOrderResult` -- API 인터페이스 추가: `ApiWorkOrder`, `ApiProductionOrderResponse` -- `createOrderFromQuote(quoteId, data)`: 견적→수주 API 호출 -- `createProductionOrder(orderId, data)`: 생산지시 생성 API 호출 -- `transformWorkOrderApiToFrontend()`: WorkOrder 데이터 변환 - -## 비즈니스 로직 - -### 견적→수주 변환 흐름 -``` -Quote (견적) - ↓ createFromQuote() -Order (수주) - DRAFT 상태 - - quote_id 연결 - - client, site_name 복사 - - items 변환 (quantity=calculated_quantity) - - 금액 재계산 -``` - -### 생산지시 생성 흐름 -``` -Order (수주) - CONFIRMED 상태 - ↓ createProductionOrder() -WorkOrder (작업지시) - PENDING 상태 - - sales_order_id 연결 - - project_name = site_name - - process_type 설정 - ↓ -Order 상태 → IN_PROGRESS -``` - -### 상태 전환 규칙 (기존) -``` -DRAFT → CONFIRMED → IN_PROGRESS → COMPLETED - ↓ ↓ ↓ -CANCELLED (어느 단계에서든 취소 가능) -``` - -## 테스트 체크리스트 - -- [ ] 견적→수주 생성 (정상 케이스) -- [ ] 견적→수주 생성 (중복 방지) -- [ ] 견적→수주 생성 (존재하지 않는 견적) -- [ ] 생산지시 생성 (정상 케이스) -- [ ] 생산지시 생성 (CONFIRMED 아닌 수주) -- [ ] 생산지시 생성 (중복 방지) -- [ ] 수주 상태 자동 변경 (CONFIRMED → IN_PROGRESS) - -## 연관 작업 - -- Phase 1: Order API 백엔드 구현 (커밋: de19ac9) -- Phase 2: Frontend API 연동 (커밋: 572ffe8) -- Phase 3: 고급 기능 (현재) diff --git a/claudedocs/components/_registry.md b/claudedocs/components/_registry.md deleted file mode 100644 index 7859de84..00000000 --- a/claudedocs/components/_registry.md +++ /dev/null @@ -1,691 +0,0 @@ -# Component Registry - -> Auto-generated: 2026-02-12T01:56:50.520Z -> Total: **501** components - -## UI (53) - -### ui (53) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| Accordion | accordion.tsx | none | | Y | 66 | -| AccountNumberInput | account-number-input.tsx | none | AccountNumberInputProps | Y | 95 | -| Alert | alert.tsx | none | VariantProps | | 59 | -| AlertDialog | alert-dialog.tsx | none | | Y | 158 | -| Badge | badge.tsx | none | VariantProps | | 47 | -| BusinessNumberInput | business-number-input.tsx | none | BusinessNumberInputProps | Y | 114 | -| Button | button.tsx | none | VariantProps | | 62 | -| Calendar | calendar.tsx | none | | Y | 138 | -| Card | card.tsx | none | | | 93 | -| CardNumberInput | card-number-input.tsx | none | CardNumberInputProps | Y | 95 | -| ChartWrapper | chart-wrapper.tsx | named | ChartWrapperProps | | 66 | -| Checkbox | checkbox.tsx | none | | Y | 33 | -| Collapsible | collapsible.tsx | none | | Y | 33 | -| Command | command.tsx | none | | Y | 177 | -| ConfirmDialog | confirm-dialog.tsx | both | ConfirmDialogProps | Y | 226 | -| CurrencyInput | currency-input.tsx | none | CurrencyInputProps | Y | 220 | -| DatePicker | date-picker.tsx | none | DatePickerProps | Y | 279 | -| Dialog | dialog.tsx | none | | Y | 137 | -| Drawer | drawer.tsx | none | | Y | 133 | -| DropdownMenu | dropdown-menu.tsx | none | | Y | 258 | -| EmptyState | empty-state.tsx | both | ButtonProps | Y | 227 | -| ErrorCard | error-card.tsx | named | ErrorCardProps | Y | 196 | -| ErrorMessage | error-message.tsx | named | ErrorMessageProps | | 38 | -| FileDropzone | file-dropzone.tsx | both | FileDropzoneProps | Y | 227 | -| FileInput | file-input.tsx | both | FileInputProps | Y | 226 | -| FileList | file-list.tsx | both | FileListProps | Y | 276 | -| ImageUpload | image-upload.tsx | both | ImageUploadProps | Y | 309 | -| Input | input.tsx | none | | | 22 | -| Label | label.tsx | none | | Y | 25 | -| LoadingSpinner | loading-spinner.tsx | named | LoadingSpinnerProps | | 114 | -| MultiSelectCombobox | multi-select-combobox.tsx | named | MultiSelectComboboxProps | Y | 128 | -| NumberInput | number-input.tsx | none | NumberInputProps | Y | 280 | -| PersonalNumberInput | personal-number-input.tsx | none | PersonalNumberInputProps | Y | 101 | -| PhoneInput | phone-input.tsx | none | PhoneInputProps | Y | 95 | -| Popover | popover.tsx | none | | Y | 53 | -| Progress | progress.tsx | none | | Y | 32 | -| QuantityInput | quantity-input.tsx | none | QuantityInputProps | Y | 271 | -| RadioGroup | radio-group.tsx | none | | Y | 46 | -| ScrollArea | scroll-area.tsx | none | | Y | 53 | -| SearchableSelect | searchable-select.tsx | named | SearchableSelectProps | Y | 219 | -| Select | select.tsx | none | | Y | 192 | -| Separator | separator.tsx | none | SeparatorProps | Y | 32 | -| Sheet | sheet.tsx | none | | Y | 146 | -| Skeleton | skeleton.tsx | none | SkeletonProps | Y | 679 | -| Slider | slider.tsx | none | | | 26 | -| StatusBadge | status-badge.tsx | both | StatusBadgeProps | Y | 123 | -| Switch | switch.tsx | none | | Y | 32 | -| Table | table.tsx | none | | | 117 | -| Tabs | tabs.tsx | none | | Y | 66 | -| Textarea | textarea.tsx | none | | | 25 | -| TimePicker | time-picker.tsx | none | TimePickerProps | Y | 191 | -| Tooltip | tooltip.tsx | none | | Y | 48 | -| VisuallyHidden | visually-hidden.tsx | none | | Y | 14 | - -## ATOMS (3) - -### atoms (3) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| BadgeSm | BadgeSm.tsx | named | BadgeSmProps | Y | 117 | -| ScrollableButtonGroup | ScrollableButtonGroup.tsx | named | ScrollableButtonGroupProps | | 53 | -| TabChip | TabChip.tsx | named | TabChipProps | Y | 72 | - -## MOLECULES (8) - -### molecules (8) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| DateRangeSelector | DateRangeSelector.tsx | named | DateRangeSelectorProps | Y | 217 | -| FormField | FormField.tsx | named | FormFieldProps | | 296 | -| IconWithBadge | IconWithBadge.tsx | named | IconWithBadgeProps | Y | 51 | -| MobileFilter | MobileFilter.tsx | named | MobileFilterProps | Y | 335 | -| StandardDialog | StandardDialog.tsx | named | StandardDialogProps | Y | 219 | -| StatusBadge | StatusBadge.tsx | named | StatusBadgeProps | Y | 111 | -| TableActions | TableActions.tsx | named | TableActionsProps | Y | 89 | -| YearQuarterFilter | YearQuarterFilter.tsx | named | YearQuarterFilterProps | Y | 98 | - -## ORGANISMS (12) - -### organisms (12) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| DataTable | DataTable.tsx | named | DataTableProps | Y | 363 | -| EmptyState | EmptyState.tsx | named | EmptyStateProps | Y | 38 | -| FormActions | FormActions.tsx | named | FormActionsProps | | 74 | -| FormFieldGrid | FormFieldGrid.tsx | named | FormFieldGridProps | | 35 | -| FormSection | FormSection.tsx | named | FormSectionProps | | 62 | -| MobileCard | MobileCard.tsx | named | InfoFieldProps | Y | 347 | -| PageHeader | PageHeader.tsx | named | PageHeaderProps | Y | 42 | -| PageLayout | PageLayout.tsx | named | PageLayoutProps | Y | 32 | -| ScreenVersionHistory | ScreenVersionHistory.tsx | named | ScreenVersionHistoryProps | Y | 75 | -| SearchableSelectionModal | SearchableSelectionModal.tsx | named | | Y | 253 | -| SearchFilter | SearchFilter.tsx | named | SearchFilterProps | Y | 58 | -| StatCards | StatCards.tsx | named | StatCardsProps | Y | 67 | - -## COMMON (16) - -### common (16) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| AccessDenied | AccessDenied.tsx | named | AccessDeniedProps | Y | 68 | -| CalendarHeader | CalendarHeader.tsx | named | | Y | 148 | -| DayCell | DayCell.tsx | named | DayCellProps | Y | 93 | -| DayTimeView | DayTimeView.tsx | named | | Y | 167 | -| EditableTable | EditableTable.tsx | both | EditableTableProps | Y | 333 | -| EmptyPage | EmptyPage.tsx | named | EmptyPageProps | Y | 150 | -| MonthView | MonthView.tsx | named | WeekRowProps | Y | 264 | -| MorePopover | MorePopover.tsx | named | MorePopoverProps | Y | 45 | -| NoticePopupModal | NoticePopupModal.tsx | named | NoticePopupModalProps | Y | 171 | -| ParentMenuRedirect | ParentMenuRedirect.tsx | named | ParentMenuRedirectProps | Y | 82 | -| PermissionGuard | PermissionGuard.tsx | named | PermissionGuardProps | Y | 44 | -| ScheduleBar | ScheduleBar.tsx | named | ScheduleBarProps | Y | 96 | -| ScheduleCalendar | ScheduleCalendar.tsx | named | | Y | 194 | -| ServerErrorPage | ServerErrorPage.tsx | named | ServerErrorPageProps | Y | 140 | -| WeekTimeView | WeekTimeView.tsx | named | | Y | 217 | -| WeekView | WeekView.tsx | named | | Y | 211 | - -## LAYOUT (3) - -### layout (3) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| CommandMenuSearch | CommandMenuSearch.tsx | default | | Y | 199 | -| HeaderFavoritesBar | HeaderFavoritesBar.tsx | default | HeaderFavoritesBarProps | Y | 156 | -| Sidebar | Sidebar.tsx | default | SidebarProps | | 390 | - -## DEV (2) - -### dev (2) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| DevFillProvider | DevFillContext.tsx | named | DevFillProviderProps | Y | 179 | -| DevToolbar | DevToolbar.tsx | both | | Y | 499 | - -## DOMAIN (404) - -### LanguageSelect.tsx (1) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| LanguageSelect | LanguageSelect.tsx | named | LanguageSelectProps | Y | 90 | - -### ThemeSelect.tsx (1) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| ThemeSelect | ThemeSelect.tsx | named | ThemeSelectProps | Y | 82 | - -### accounting (19) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| BadDebtDetail | BadDebtDetail.tsx | named | BadDebtDetailProps | Y | 963 | -| BadDebtDetailClientV2 | BadDebtDetailClientV2.tsx | named | BadDebtDetailClientV2Props | Y | 136 | -| BillDetail | BillDetail.tsx | named | BillDetailProps | Y | 540 | -| BillManagementClient | BillManagementClient.tsx | named | BillManagementClientProps | Y | 522 | -| CardTransactionDetailClient | CardTransactionDetailClient.tsx | default | CardTransactionDetailClientProps | Y | 139 | -| CreditAnalysisDocument | CreditAnalysisDocument.tsx | named | CreditAnalysisDocumentProps | Y | 210 | -| CreditSignal | CreditSignal.tsx | named | CreditSignalProps | Y | 58 | -| DepositDetail | DepositDetail.tsx | named | DepositDetailProps | Y | 327 | -| DepositDetailClientV2 | DepositDetailClientV2.tsx | default | DepositDetailClientV2Props | Y | 144 | -| PurchaseDetail | PurchaseDetail.tsx | named | PurchaseDetailProps | Y | 697 | -| PurchaseDetailModal | PurchaseDetailModal.tsx | named | PurchaseDetailModalProps | Y | 402 | -| RiskRadarChart | RiskRadarChart.tsx | named | RiskRadarChartProps | Y | 95 | -| SalesDetail | SalesDetail.tsx | named | SalesDetailProps | Y | 579 | -| VendorDetail | VendorDetail.tsx | named | VendorDetailProps | Y | 684 | -| VendorDetailClient | VendorDetailClient.tsx | named | VendorDetailClientProps | Y | 586 | -| VendorLedgerDetail | VendorLedgerDetail.tsx | named | VendorLedgerDetailProps | Y | 386 | -| VendorManagementClient | VendorManagementClient.tsx | named | VendorManagementClientProps | Y | 574 | -| WithdrawalDetail | WithdrawalDetail.tsx | named | WithdrawalDetailProps | Y | 327 | -| WithdrawalDetailClientV2 | WithdrawalDetailClientV2.tsx | default | WithdrawalDetailClientV2Props | Y | 144 | - -### approval (11) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| ApprovalLineBox | ApprovalLineBox.tsx | named | ApprovalLineBoxProps | Y | 85 | -| ApprovalLineSection | ApprovalLineSection.tsx | named | ApprovalLineSectionProps | Y | 108 | -| BasicInfoSection | BasicInfoSection.tsx | named | BasicInfoSectionProps | Y | 81 | -| DocumentDetailModalV2 | DocumentDetailModalV2.tsx | named | | Y | 94 | -| ExpenseEstimateDocument | ExpenseEstimateDocument.tsx | named | ExpenseEstimateDocumentProps | Y | 130 | -| ExpenseEstimateForm | ExpenseEstimateForm.tsx | named | ExpenseEstimateFormProps | Y | 167 | -| ExpenseReportDocument | ExpenseReportDocument.tsx | named | ExpenseReportDocumentProps | Y | 138 | -| ExpenseReportForm | ExpenseReportForm.tsx | named | ExpenseReportFormProps | Y | 243 | -| ProposalDocument | ProposalDocument.tsx | named | ProposalDocumentProps | Y | 117 | -| ProposalForm | ProposalForm.tsx | named | ProposalFormProps | Y | 234 | -| ReferenceSection | ReferenceSection.tsx | named | ReferenceSectionProps | Y | 109 | - -### attendance (2) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| AttendanceComplete | AttendanceComplete.tsx | default | AttendanceCompleteProps | Y | 83 | -| GoogleMap | GoogleMap.tsx | default | GoogleMapProps | Y | 309 | - -### auth (2) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| LoginPage | LoginPage.tsx | named | | Y | 301 | -| SignupPage | SignupPage.tsx | named | | Y | 763 | - -### board (8) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| BoardDetail | BoardDetail.tsx | named | BoardDetailProps | Y | 120 | -| BoardDetailClientV2 | BoardDetailClientV2.tsx | named | BoardDetailClientV2Props | Y | 308 | -| BoardForm | BoardForm.tsx | named | BoardFormProps | Y | 271 | -| BoardListUnified | BoardListUnified.tsx | both | | Y | 372 | -| CommentItem | CommentItem.tsx | both | CommentItemProps | Y | 161 | -| DynamicBoardCreateForm | DynamicBoardCreateForm.tsx | named | DynamicBoardCreateFormProps | Y | 166 | -| DynamicBoardEditForm | DynamicBoardEditForm.tsx | named | DynamicBoardEditFormProps | Y | 253 | -| MenuBar | MenuBar.tsx | named | MenuBarProps | Y | 289 | - -### business (97) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| BiddingDetailForm | BiddingDetailForm.tsx | default | BiddingDetailFormProps | Y | 533 | -| BiddingListClient | BiddingListClient.tsx | default | BiddingListClientProps | Y | 385 | -| CalendarSection | CalendarSection.tsx | named | CalendarSectionProps | Y | 421 | -| CardManagementSection | CardManagementSection.tsx | named | CardManagementSectionProps | Y | 71 | -| CategoryDialog | CategoryDialog.tsx | named | | Y | 89 | -| CEODashboard | CEODashboard.tsx | named | | Y | 407 | -| ConstructionDashboard | ConstructionDashboard.tsx | named | | Y | 19 | -| ConstructionDetailCard | ConstructionDetailCard.tsx | named | ConstructionDetailCardProps | Y | 83 | -| ConstructionDetailClient | ConstructionDetailClient.tsx | default | ConstructionDetailClientProps | Y | 732 | -| ConstructionMainDashboard | ConstructionMainDashboard.tsx | named | | Y | 196 | -| ConstructionManagementListClient | ConstructionManagementListClient.tsx | default | ConstructionManagementListClientProps | Y | 540 | -| ContractDetailForm | ContractDetailForm.tsx | default | ContractDetailFormProps | Y | 541 | -| ContractDocumentModal | ContractDocumentModal.tsx | named | ContractDocumentModalProps | Y | 64 | -| ContractInfoCard | ContractInfoCard.tsx | named | ContractInfoCardProps | Y | 169 | -| ContractInfoCard | ContractInfoCard.tsx | named | ContractInfoCardProps | Y | 58 | -| ContractListClient | ContractListClient.tsx | default | ContractListClientProps | Y | 399 | -| DailyReportSection | DailyReportSection.tsx | named | DailyReportSectionProps | Y | 37 | -| Dashboard | Dashboard.tsx | named | | Y | 33 | -| DashboardSettingsDialog | DashboardSettingsDialog.tsx | named | DashboardSettingsDialogProps | Y | 744 | -| DashboardSwitcher | DashboardSwitcher.tsx | named | | Y | 89 | -| DebtCollectionSection | DebtCollectionSection.tsx | named | DebtCollectionSectionProps | Y | 62 | -| DetailAccordion | DetailAccordion.tsx | default | DetailAccordionProps | Y | 199 | -| DetailCard | DetailCard.tsx | default | DetailCardProps | Y | 69 | -| DetailModal | DetailModal.tsx | named | DetailModalProps | Y | 763 | -| DirectConstructionContent | DirectConstructionContent.tsx | named | DirectConstructionContentProps | Y | 157 | -| DirectConstructionModal | DirectConstructionModal.tsx | named | DirectConstructionModalProps | Y | 60 | -| ElectronicApprovalModal | ElectronicApprovalModal.tsx | named | ElectronicApprovalModalProps | Y | 299 | -| ElectronicApprovalModal | ElectronicApprovalModal.tsx | none | | | 2 | -| EnhancedDailyReportSection | EnhancedSections.tsx | named | EnhancedDailyReportSectionProps | Y | 534 | -| EntertainmentSection | EntertainmentSection.tsx | named | EntertainmentSectionProps | Y | 53 | -| EstimateDetailForm | EstimateDetailForm.tsx | default | EstimateDetailFormProps | Y | 761 | -| EstimateDetailTableSection | EstimateDetailTableSection.tsx | named | EstimateDetailTableSectionProps | Y | 657 | -| EstimateDocumentContent | EstimateDocumentContent.tsx | named | EstimateDocumentContentProps | Y | 286 | -| EstimateDocumentModal | EstimateDocumentModal.tsx | named | EstimateDocumentModalProps | Y | 88 | -| EstimateInfoSection | EstimateInfoSection.tsx | named | EstimateInfoSectionProps | Y | 262 | -| EstimateListClient | EstimateListClient.tsx | default | EstimateListClientProps | Y | 376 | -| EstimateSummarySection | EstimateSummarySection.tsx | named | EstimateSummarySectionProps | Y | 182 | -| ExpenseDetailSection | ExpenseDetailSection.tsx | named | ExpenseDetailSectionProps | Y | 197 | -| HandoverReportDetailForm | HandoverReportDetailForm.tsx | default | HandoverReportDetailFormProps | Y | 694 | -| HandoverReportDocumentModal | HandoverReportDocumentModal.tsx | named | HandoverReportDocumentModalProps | Y | 236 | -| HandoverReportListClient | HandoverReportListClient.tsx | default | HandoverReportListClientProps | Y | 387 | -| IndirectConstructionContent | IndirectConstructionContent.tsx | named | IndirectConstructionContentProps | Y | 143 | -| IndirectConstructionModal | IndirectConstructionModal.tsx | named | IndirectConstructionModalProps | Y | 60 | -| IssueDetailForm | IssueDetailForm.tsx | default | IssueDetailFormProps | Y | 625 | -| IssueManagementListClient | IssueManagementListClient.tsx | default | IssueManagementListClientProps | Y | 514 | -| ItemDetailClient | ItemDetailClient.tsx | default | ItemDetailClientProps | Y | 487 | -| ItemManagementClient | ItemManagementClient.tsx | default | ItemManagementClientProps | Y | 618 | -| KanbanColumn | KanbanColumn.tsx | default | KanbanColumnProps | Y | 53 | -| LaborDetailClient | LaborDetailClient.tsx | default | LaborDetailClientProps | Y | 121 | -| LaborManagementClient | LaborManagementClient.tsx | default | LaborManagementClientProps | Y | 372 | -| MainDashboard | MainDashboard.tsx | named | | | 2652 | -| MonthlyExpenseSection | MonthlyExpenseSection.tsx | named | MonthlyExpenseSectionProps | Y | 38 | -| OrderDetailForm | OrderDetailForm.tsx | default | OrderDetailFormProps | Y | 276 | -| OrderDetailItemTable | OrderDetailItemTable.tsx | named | OrderDetailItemTableProps | Y | 445 | -| OrderDialogs | OrderDialogs.tsx | named | OrderDialogsProps | Y | 66 | -| OrderDocumentModal | OrderDocumentModal.tsx | named | OrderDocumentModalProps | Y | 311 | -| OrderInfoCard | OrderInfoCard.tsx | named | OrderInfoCardProps | Y | 143 | -| OrderManagementListClient | OrderManagementListClient.tsx | default | OrderManagementListClientProps | Y | 608 | -| OrderManagementUnified | OrderManagementUnified.tsx | both | OrderManagementUnifiedProps | Y | 641 | -| OrderMemoCard | OrderMemoCard.tsx | named | OrderMemoCardProps | Y | 29 | -| OrderScheduleCard | OrderScheduleCard.tsx | named | OrderScheduleCardProps | Y | 42 | -| PartnerForm | PartnerForm.tsx | default | PartnerFormProps | Y | 642 | -| PartnerListClient | PartnerListClient.tsx | default | PartnerListClientProps | Y | 335 | -| PhotoDocumentContent | PhotoDocumentContent.tsx | named | PhotoDocumentContentProps | Y | 130 | -| PhotoDocumentModal | PhotoDocumentModal.tsx | named | PhotoDocumentModalProps | Y | 60 | -| PhotoTable | PhotoTable.tsx | named | PhotoTableProps | Y | 153 | -| PriceAdjustmentSection | PriceAdjustmentSection.tsx | named | PriceAdjustmentSectionProps | Y | 150 | -| PricingDetailClient | PricingDetailClient.tsx | default | PricingDetailClientProps | Y | 135 | -| PricingListClient | PricingListClient.tsx | default | PricingListClientProps | Y | 477 | -| ProgressBillingDetailForm | ProgressBillingDetailForm.tsx | default | ProgressBillingDetailFormProps | Y | 193 | -| ProgressBillingInfoCard | ProgressBillingInfoCard.tsx | named | ProgressBillingInfoCardProps | Y | 78 | -| ProgressBillingItemTable | ProgressBillingItemTable.tsx | named | ProgressBillingItemTableProps | Y | 193 | -| ProgressBillingManagementListClient | ProgressBillingManagementListClient.tsx | default | ProgressBillingManagementListClientProps | Y | 343 | -| ProjectCard | ProjectCard.tsx | default | ProjectCardProps | Y | 89 | -| ProjectDetailClient | ProjectDetailClient.tsx | default | ProjectDetailClientProps | Y | 197 | -| ProjectEndDialog | ProjectEndDialog.tsx | default | ProjectEndDialogProps | Y | 192 | -| ProjectGanttChart | ProjectGanttChart.tsx | default | ProjectGanttChartProps | Y | 367 | -| ProjectKanbanBoard | ProjectKanbanBoard.tsx | default | ProjectKanbanBoardProps | Y | 244 | -| ProjectListClient | ProjectListClient.tsx | default | ProjectListClientProps | Y | 629 | -| ReceivableSection | ReceivableSection.tsx | named | ReceivableSectionProps | Y | 69 | -| ScheduleDetailModal | ScheduleDetailModal.tsx | named | ScheduleDetailModalProps | Y | 290 | -| SECTION_THEME_STYLES | components.tsx | named | | Y | 434 | -| SiteBriefingForm | SiteBriefingForm.tsx | default | SiteBriefingFormProps | Y | 957 | -| SiteBriefingListClient | SiteBriefingListClient.tsx | default | SiteBriefingListClientProps | Y | 362 | -| SiteDetailClientV2 | SiteDetailClientV2.tsx | both | SiteDetailClientV2Props | Y | 141 | -| SiteDetailForm | SiteDetailForm.tsx | default | SiteDetailFormProps | Y | 386 | -| SiteManagementListClient | SiteManagementListClient.tsx | default | SiteManagementListClientProps | Y | 338 | -| StageCard | StageCard.tsx | default | StageCardProps | Y | 89 | -| StatusBoardSection | StatusBoardSection.tsx | named | StatusBoardSectionProps | Y | 72 | -| StructureReviewDetailClientV2 | StructureReviewDetailClientV2.tsx | both | StructureReviewDetailClientV2Props | Y | 149 | -| StructureReviewDetailForm | StructureReviewDetailForm.tsx | default | StructureReviewDetailFormProps | Y | 390 | -| StructureReviewListClient | StructureReviewListClient.tsx | default | StructureReviewListClientProps | Y | 375 | -| TodayIssueSection | TodayIssueSection.tsx | named | TodayIssueSectionProps | Y | 453 | -| UtilityManagementListClient | UtilityManagementListClient.tsx | default | UtilityManagementListClientProps | Y | 395 | -| VatSection | VatSection.tsx | named | VatSectionProps | Y | 38 | -| WelfareSection | WelfareSection.tsx | named | WelfareSectionProps | Y | 53 | -| WorkerStatusListClient | WorkerStatusListClient.tsx | default | WorkerStatusListClientProps | Y | 416 | - -### checklist-management (7) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| ChecklistDetail | ChecklistDetail.tsx | named | ChecklistDetailProps | Y | 316 | -| ChecklistDetailClient | ChecklistDetailClient.tsx | named | ChecklistDetailClientProps | Y | 123 | -| ChecklistForm | ChecklistForm.tsx | named | ChecklistFormProps | Y | 173 | -| ChecklistListClient | ChecklistListClient.tsx | default | | Y | 520 | -| ItemDetail | ItemDetail.tsx | named | ItemDetailProps | Y | 224 | -| ItemDetailClient | ItemDetailClient.tsx | named | ItemDetailClientProps | Y | 111 | -| ItemForm | ItemForm.tsx | named | ItemFormProps | Y | 351 | - -### clients (3) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| ClientDetail | ClientDetail.tsx | named | ClientDetailProps | Y | 254 | -| ClientDetailClientV2 | ClientDetailClientV2.tsx | named | ClientDetailClientV2Props | Y | 253 | -| ClientRegistration | ClientRegistration.tsx | named | ClientRegistrationProps | Y | 468 | - -### customer-center (9) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| EventDetail | EventDetail.tsx | both | EventDetailProps | Y | 102 | -| EventList | EventList.tsx | both | | Y | 261 | -| FAQList | FAQList.tsx | both | | Y | 172 | -| InquiryDetail | InquiryDetail.tsx | both | InquiryDetailProps | Y | 359 | -| InquiryDetailClientV2 | InquiryDetailClientV2.tsx | both | InquiryDetailClientV2Props | Y | 224 | -| InquiryForm | InquiryForm.tsx | both | InquiryFormProps | Y | 237 | -| InquiryList | InquiryList.tsx | both | | Y | 292 | -| NoticeDetail | NoticeDetail.tsx | both | NoticeDetailProps | Y | 102 | -| NoticeList | NoticeList.tsx | both | | Y | 227 | - -### document-system (11) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| ApprovalLine | ApprovalLine.tsx | named | ApprovalLineProps | Y | 170 | -| ConstructionApprovalTable | ConstructionApprovalTable.tsx | named | ConstructionApprovalTableProps | Y | 116 | -| DocumentContent | DocumentContent.tsx | named | DocumentContentProps | Y | 59 | -| DocumentHeader | DocumentHeader.tsx | named | DocumentHeaderProps | Y | 248 | -| DocumentToolbar | DocumentToolbar.tsx | named | DocumentToolbarProps | Y | 327 | -| DocumentViewer | DocumentViewer.tsx | named | | Y | 378 | -| InfoTable | InfoTable.tsx | named | InfoTableProps | Y | 95 | -| LotApprovalTable | LotApprovalTable.tsx | named | LotApprovalTableProps | Y | 122 | -| QualityApprovalTable | QualityApprovalTable.tsx | named | QualityApprovalTableProps | Y | 123 | -| SectionHeader | SectionHeader.tsx | named | SectionHeaderProps | Y | 46 | -| SignatureSection | SignatureSection.tsx | named | SignatureSectionProps | Y | 107 | - -### hr (24) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| AttendanceInfoDialog | AttendanceInfoDialog.tsx | named | | Y | 301 | -| CardDetail | CardDetail.tsx | named | CardDetailProps | Y | 132 | -| CardForm | CardForm.tsx | named | CardFormProps | Y | 246 | -| CardManagementUnified | CardManagementUnified.tsx | named | CardManagementUnifiedProps | Y | 267 | -| CSVUploadDialog | CSVUploadDialog.tsx | named | CSVUploadDialogProps | Y | 252 | -| CSVUploadPage | CSVUploadPage.tsx | named | CSVUploadPageProps | Y | 355 | -| DepartmentDialog | DepartmentDialog.tsx | named | | Y | 92 | -| DepartmentStats | DepartmentStats.tsx | named | | Y | 18 | -| DepartmentToolbar | DepartmentToolbar.tsx | named | | Y | 60 | -| DepartmentTree | DepartmentTree.tsx | named | | Y | 70 | -| DepartmentTreeItem | DepartmentTreeItem.tsx | named | | Y | 118 | -| EmployeeDetail | EmployeeDetail.tsx | named | EmployeeDetailProps | Y | 222 | -| EmployeeDialog | EmployeeDialog.tsx | named | | Y | 582 | -| EmployeeForm | EmployeeForm.tsx | named | EmployeeFormProps | Y | 1052 | -| EmployeeToolbar | EmployeeToolbar.tsx | named | EmployeeToolbarProps | Y | 82 | -| FieldSettingsDialog | FieldSettingsDialog.tsx | named | FieldSettingsDialogProps | Y | 259 | -| ReasonInfoDialog | ReasonInfoDialog.tsx | named | | Y | 140 | -| SalaryDetailDialog | SalaryDetailDialog.tsx | named | SalaryDetailDialogProps | Y | 420 | -| UserInviteDialog | UserInviteDialog.tsx | named | UserInviteDialogProps | Y | 116 | -| VacationAdjustDialog | VacationAdjustDialog.tsx | named | VacationAdjustDialogProps | Y | 225 | -| VacationGrantDialog | VacationGrantDialog.tsx | named | VacationGrantDialogProps | Y | 202 | -| VacationRegisterDialog | VacationRegisterDialog.tsx | named | VacationRegisterDialogProps | Y | 201 | -| VacationRequestDialog | VacationRequestDialog.tsx | named | VacationRequestDialogProps | Y | 208 | -| VacationTypeSettingsDialog | VacationTypeSettingsDialog.tsx | named | VacationTypeSettingsDialogProps | Y | 192 | - -### items (65) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| AssemblyPartForm | AssemblyPartForm.tsx | default | AssemblyPartFormProps | | 337 | -| AttributeTabContent | AttributeTabContent.tsx | named | AttributeTabContentProps | Y | 453 | -| BendingDiagramSection | BendingDiagramSection.tsx | default | BendingDiagramSectionProps | | 477 | -| BendingPartForm | BendingPartForm.tsx | default | BendingPartFormProps | | 304 | -| BOMManagementSection | BOMManagementSection.tsx | named | BOMManagementSectionProps | Y | 293 | -| BOMSection | BOMSection.tsx | default | BOMSectionProps | | 366 | -| CheckboxField | CheckboxField.tsx | named | | Y | 47 | -| ColumnDialog | ColumnDialog.tsx | named | ColumnDialogProps | Y | 124 | -| ColumnManageDialog | ColumnManageDialog.tsx | named | ColumnManageDialogProps | Y | 210 | -| ComputedField | ComputedField.tsx | named | | Y | 136 | -| ConditionalDisplayUI | ConditionalDisplayUI.tsx | named | ConditionalDisplayUIProps | | 349 | -| CurrencyField | CurrencyField.tsx | named | | Y | 127 | -| DateField | DateField.tsx | named | | Y | 45 | -| DraggableField | DraggableField.tsx | named | DraggableFieldProps | | 130 | -| DraggableSection | DraggableSection.tsx | named | DraggableSectionProps | | 140 | -| DrawingCanvas | DrawingCanvas.tsx | named | DrawingCanvasProps | Y | 404 | -| DropdownField | DropdownField.tsx | named | | Y | 141 | -| DuplicateCodeDialog | DuplicateCodeDialog.tsx | named | DuplicateCodeDialogProps | Y | 49 | -| DynamicBOMSection | DynamicBOMSection.tsx | default | DynamicBOMSectionProps | Y | 515 | -| DynamicFieldRenderer | DynamicFieldRenderer.tsx | named | | Y | 86 | -| DynamicTableSection | DynamicTableSection.tsx | default | DynamicTableSectionProps | Y | 200 | -| ErrorAlertDialog | ErrorAlertDialog.tsx | named | ErrorAlertDialogProps | Y | 51 | -| ErrorAlertProvider | ErrorAlertContext.tsx | named | ErrorAlertProviderProps | Y | 94 | -| FieldDialog | FieldDialog.tsx | named | FieldDialogProps | Y | 478 | -| FieldDrawer | FieldDrawer.tsx | named | FieldDrawerProps | Y | 682 | -| FileField | FileField.tsx | named | | Y | 200 | -| FileUpload | FileUpload.tsx | default | FileUploadProps | Y | 233 | -| FileUploadFields | FileUploadFields.tsx | named | FileUploadFieldsProps | Y | 240 | -| FormHeader | FormHeader.tsx | named | FormHeaderProps | Y | 31 | -| FormHeader | FormHeader.tsx | default | FormHeaderProps | | 62 | -| ImportFieldDialog | ImportFieldDialog.tsx | named | ImportFieldDialogProps | Y | 279 | -| ImportSectionDialog | ImportSectionDialog.tsx | named | ImportSectionDialogProps | Y | 221 | -| ItemDetailClient | ItemDetailClient.tsx | default | ItemDetailClientProps | Y | 638 | -| ItemDetailEdit | ItemDetailEdit.tsx | named | ItemDetailEditProps | Y | 390 | -| ItemDetailView | ItemDetailView.tsx | named | ItemDetailViewProps | Y | 275 | -| ItemFormContext | ItemFormContext.tsx | both | ItemFormProviderProps | Y | 77 | -| ItemListClient | ItemListClient.tsx | default | | Y | 607 | -| ItemMasterDataManagement | ItemMasterDataManagement.tsx | named | | Y | 1006 | -| ItemMasterDialogs | ItemMasterDialogs.tsx | named | ItemMasterDialogsProps | Y | 968 | -| ItemTypeSelect | ItemTypeSelect.tsx | default | ItemTypeSelectProps | Y | 76 | -| LoadTemplateDialog | LoadTemplateDialog.tsx | named | LoadTemplateDialogProps | Y | 103 | -| MasterFieldDialog | MasterFieldDialog.tsx | named | MasterFieldDialogProps | Y | 306 | -| MaterialForm | MaterialForm.tsx | default | MaterialFormProps | | 354 | -| MultiSelectField | MultiSelectField.tsx | named | | Y | 192 | -| NumberField | NumberField.tsx | named | | Y | 58 | -| OptionDialog | OptionDialog.tsx | named | OptionDialogProps | Y | 262 | -| PageDialog | PageDialog.tsx | named | PageDialogProps | Y | 107 | -| PartForm | PartForm.tsx | default | PartFormProps | | 273 | -| PathEditDialog | PathEditDialog.tsx | named | PathEditDialogProps | Y | 86 | -| ProductForm | ProductForm.tsx | both | ProductFormProps | | 307 | -| PurchasedPartForm | PurchasedPartForm.tsx | default | PurchasedPartFormProps | | 336 | -| RadioField | RadioField.tsx | named | | Y | 92 | -| ReferenceField | ReferenceField.tsx | named | | Y | 168 | -| SectionDialog | SectionDialog.tsx | named | SectionDialogProps | Y | 335 | -| SectionsTab | SectionsTab.tsx | named | SectionsTabProps | Y | 363 | -| SectionTemplateDialog | SectionTemplateDialog.tsx | named | SectionTemplateDialogProps | Y | 180 | -| TableCellRenderer | TableCellRenderer.tsx | named | TableCellRendererProps | Y | 85 | -| TabManagementDialogs | TabManagementDialogs.tsx | named | TabManagementDialogsProps | Y | 409 | -| TemplateFieldDialog | TemplateFieldDialog.tsx | named | TemplateFieldDialogProps | Y | 392 | -| TextareaField | TextareaField.tsx | named | | Y | 51 | -| TextField | TextField.tsx | named | | Y | 48 | -| ToggleField | ToggleField.tsx | named | | Y | 62 | -| UnitValueField | UnitValueField.tsx | named | | Y | 129 | -| ValidationAlert | ValidationAlert.tsx | named | ValidationAlertProps | Y | 42 | -| ValidationAlert | ValidationAlert.tsx | default | ValidationAlertProps | | 50 | - -### material (13) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| ImportInspectionInputModal | ImportInspectionInputModal.tsx | named | ImportInspectionInputModalProps | Y | 798 | -| InspectionCreate | InspectionCreate.tsx | named | Props | Y | 364 | -| InventoryAdjustmentDialog | InventoryAdjustmentDialog.tsx | named | Props | Y | 236 | -| ReceivingDetail | ReceivingDetail.tsx | named | Props | Y | 921 | -| ReceivingList | ReceivingList.tsx | named | | Y | 467 | -| ReceivingProcessDialog | ReceivingProcessDialog.tsx | named | Props | Y | 238 | -| ReceivingReceiptContent | ReceivingReceiptContent.tsx | named | ReceivingReceiptContentProps | Y | 132 | -| ReceivingReceiptDialog | ReceivingReceiptDialog.tsx | named | Props | Y | 46 | -| StockAuditModal | StockAuditModal.tsx | named | StockAuditModalProps | Y | 237 | -| StockStatusDetail | StockStatusDetail.tsx | named | StockStatusDetailProps | Y | 313 | -| StockStatusList | StockStatusList.tsx | named | | Y | 473 | -| SuccessDialog | SuccessDialog.tsx | named | Props | Y | 49 | -| SupplierSearchModal | SupplierSearchModal.tsx | named | SupplierSearchModalProps | Y | 161 | - -### orders (10) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| ContractDocument | ContractDocument.tsx | named | ContractDocumentProps | Y | 246 | -| ItemAddDialog | ItemAddDialog.tsx | named | ItemAddDialogProps | Y | 317 | -| OrderDocumentModal | OrderDocumentModal.tsx | named | OrderDocumentModalProps | Y | 207 | -| OrderRegistration | OrderRegistration.tsx | named | OrderRegistrationProps | Y | 1087 | -| OrderSalesDetailEdit | OrderSalesDetailEdit.tsx | named | OrderSalesDetailEditProps | Y | 735 | -| OrderSalesDetailView | OrderSalesDetailView.tsx | named | OrderSalesDetailViewProps | Y | 824 | -| PurchaseOrderDocument | PurchaseOrderDocument.tsx | named | PurchaseOrderDocumentProps | Y | 223 | -| QuotationSelectDialog | QuotationSelectDialog.tsx | named | QuotationSelectDialogProps | Y | 114 | -| SalesOrderDocument | SalesOrderDocument.tsx | named | SalesOrderDocumentProps | Y | 638 | -| TransactionDocument | TransactionDocument.tsx | named | TransactionDocumentProps | Y | 226 | - -### outbound (11) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| DeliveryConfirmation | DeliveryConfirmation.tsx | named | DeliveryConfirmationProps | Y | 18 | -| ShipmentCreate | ShipmentCreate.tsx | named | | Y | 772 | -| ShipmentDetail | ShipmentDetail.tsx | named | ShipmentDetailProps | Y | 671 | -| ShipmentEdit | ShipmentEdit.tsx | named | ShipmentEditProps | Y | 791 | -| ShipmentList | ShipmentList.tsx | named | | Y | 399 | -| ShipmentOrderDocument | ShipmentOrderDocument.tsx | named | ShipmentOrderDocumentProps | Y | 647 | -| ShippingSlip | ShippingSlip.tsx | named | ShippingSlipProps | Y | 18 | -| TransactionStatement | TransactionStatement.tsx | named | TransactionStatementProps | Y | 154 | -| VehicleDispatchDetail | VehicleDispatchDetail.tsx | named | VehicleDispatchDetailProps | Y | 181 | -| VehicleDispatchEdit | VehicleDispatchEdit.tsx | named | VehicleDispatchEditProps | Y | 399 | -| VehicleDispatchList | VehicleDispatchList.tsx | named | | Y | 331 | - -### pricing (5) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| PricingFinalizeDialog | PricingFinalizeDialog.tsx | both | PricingFinalizeDialogProps | Y | 95 | -| PricingFormClient | PricingFormClient.tsx | both | PricingFormClientProps | Y | 780 | -| PricingHistoryDialog | PricingHistoryDialog.tsx | both | PricingHistoryDialogProps | Y | 170 | -| PricingListClient | PricingListClient.tsx | both | PricingListClientProps | Y | 387 | -| PricingRevisionDialog | PricingRevisionDialog.tsx | both | PricingRevisionDialogProps | Y | 95 | - -### pricing-distribution (3) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| PriceDistributionDetail | PriceDistributionDetail.tsx | both | Props | Y | 539 | -| PriceDistributionDocumentModal | PriceDistributionDocumentModal.tsx | both | Props | Y | 158 | -| PriceDistributionList | PriceDistributionList.tsx | both | | Y | 328 | - -### pricing-table-management (3) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| PricingTableDetailClient | PricingTableDetailClient.tsx | named | PricingTableDetailClientProps | Y | 93 | -| PricingTableForm | PricingTableForm.tsx | named | PricingTableFormProps | Y | 486 | -| PricingTableListClient | PricingTableListClient.tsx | default | | Y | 381 | - -### process-management (12) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| InspectionPreviewModal | InspectionPreviewModal.tsx | named | InspectionPreviewModalProps | Y | 265 | -| InspectionSettingModal | InspectionSettingModal.tsx | named | InspectionSettingModalProps | Y | 294 | -| ProcessDetail | ProcessDetail.tsx | named | ProcessDetailProps | Y | 451 | -| ProcessDetailClientV2 | ProcessDetailClientV2.tsx | named | ProcessDetailClientV2Props | Y | 137 | -| ProcessForm | ProcessForm.tsx | named | ProcessFormProps | Y | 829 | -| ProcessListClient | ProcessListClient.tsx | default | ProcessListClientProps | Y | 546 | -| ProcessWorkLogContent | ProcessWorkLogContent.tsx | named | ProcessWorkLogContentProps | Y | 136 | -| ProcessWorkLogPreviewModal | ProcessWorkLogPreviewModal.tsx | named | ProcessWorkLogPreviewModalProps | Y | 45 | -| RuleModal | RuleModal.tsx | named | RuleModalProps | Y | 352 | -| StepDetail | StepDetail.tsx | named | StepDetailProps | Y | 212 | -| StepDetailClient | StepDetailClient.tsx | named | StepDetailClientProps | Y | 115 | -| StepForm | StepForm.tsx | named | StepFormProps | Y | 397 | - -### production (31) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| AssigneeSelectModal | AssigneeSelectModal.tsx | named | AssigneeSelectModalProps | Y | 317 | -| BendingInspectionContent | BendingInspectionContent.tsx | named | BendingInspectionContentProps | Y | 490 | -| BendingWipInspectionContent | BendingWipInspectionContent.tsx | named | BendingWipInspectionContentProps | Y | 304 | -| BendingWorkLogContent | BendingWorkLogContent.tsx | named | BendingWorkLogContentProps | Y | 194 | -| CompletionConfirmDialog | CompletionConfirmDialog.tsx | named | CompletionConfirmDialogProps | Y | 64 | -| CompletionToast | CompletionToast.tsx | named | CompletionToastProps | Y | 28 | -| InspectionCheckbox | inspection-shared.tsx | named | | Y | 282 | -| InspectionInputModal | InspectionInputModal.tsx | named | InspectionInputModalProps | Y | 978 | -| InspectionReportModal | InspectionReportModal.tsx | named | InspectionReportModalProps | Y | 409 | -| IssueReportModal | IssueReportModal.tsx | named | IssueReportModalProps | Y | 178 | -| MaterialInputModal | MaterialInputModal.tsx | named | MaterialInputModalProps | Y | 333 | -| ProcessDetailSection | ProcessDetailSection.tsx | named | ProcessDetailSectionProps | Y | 392 | -| SalesOrderSelectModal | SalesOrderSelectModal.tsx | named | SalesOrderSelectModalProps | Y | 102 | -| ScreenInspectionContent | ScreenInspectionContent.tsx | named | ScreenInspectionContentProps | Y | 310 | -| ScreenWorkLogContent | ScreenWorkLogContent.tsx | named | ScreenWorkLogContentProps | Y | 201 | -| SlatInspectionContent | SlatInspectionContent.tsx | named | SlatInspectionContentProps | Y | 297 | -| SlatJointBarInspectionContent | SlatJointBarInspectionContent.tsx | named | SlatJointBarInspectionContentProps | Y | 311 | -| SlatWorkLogContent | SlatWorkLogContent.tsx | named | SlatWorkLogContentProps | Y | 198 | -| TemplateInspectionContent | TemplateInspectionContent.tsx | named | TemplateInspectionContentProps | Y | 719 | -| WipProductionModal | WipProductionModal.tsx | named | WipProductionModalProps | Y | 272 | -| WorkCard | WorkCard.tsx | named | WorkCardProps | Y | 188 | -| WorkCompletionResultDialog | WorkCompletionResultDialog.tsx | named | WorkCompletionResultDialogProps | Y | 85 | -| WorkItemCard | WorkItemCard.tsx | named | WorkItemCardProps | Y | 382 | -| WorkLogContent | WorkLogContent.tsx | named | WorkLogContentProps | Y | 195 | -| WorkLogModal | WorkLogModal.tsx | named | WorkLogModalProps | Y | 152 | -| WorkOrderCreate | WorkOrderCreate.tsx | named | | Y | 545 | -| WorkOrderDetail | WorkOrderDetail.tsx | named | WorkOrderDetailProps | Y | 656 | -| WorkOrderEdit | WorkOrderEdit.tsx | named | WorkOrderEditProps | Y | 656 | -| WorkOrderList | WorkOrderList.tsx | named | | Y | 460 | -| WorkOrderListPanel | WorkOrderListPanel.tsx | named | WorkOrderListPanelProps | Y | 132 | -| WorkResultList | WorkResultList.tsx | named | | Y | 374 | - -### quality (11) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| InspectionCreate | InspectionCreate.tsx | named | | Y | 695 | -| InspectionDetail | InspectionDetail.tsx | named | InspectionDetailProps | Y | 1126 | -| InspectionList | InspectionList.tsx | named | | Y | 388 | -| InspectionReportDocument | InspectionReportDocument.tsx | named | InspectionReportDocumentProps | Y | 416 | -| InspectionReportModal | InspectionReportModal.tsx | named | InspectionReportModalProps | Y | 170 | -| InspectionRequestDocument | InspectionRequestDocument.tsx | named | InspectionRequestDocumentProps | Y | 258 | -| InspectionRequestModal | InspectionRequestModal.tsx | named | InspectionRequestModalProps | Y | 40 | -| MemoModal | MemoModal.tsx | named | MemoModalProps | Y | 92 | -| OrderSelectModal | OrderSelectModal.tsx | named | OrderSelectModalProps | Y | 111 | -| PerformanceReportList | PerformanceReportList.tsx | named | | Y | 604 | -| ProductInspectionInputModal | ProductInspectionInputModal.tsx | named | ProductInspectionInputModalProps | Y | 486 | - -### quotes (15) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| DiscountModal | DiscountModal.tsx | named | DiscountModalProps | Y | 232 | -| FormulaViewModal | FormulaViewModal.tsx | named | FormulaViewModalProps | Y | 316 | -| ItemSearchModal | ItemSearchModal.tsx | named | ItemSearchModalProps | Y | 114 | -| LocationDetailPanel | LocationDetailPanel.tsx | named | LocationDetailPanelProps | Y | 827 | -| LocationEditModal | LocationEditModal.tsx | named | LocationEditModalProps | Y | 283 | -| LocationListPanel | LocationListPanel.tsx | named | LocationListPanelProps | Y | 575 | -| PurchaseOrderDocument | PurchaseOrderDocument.tsx | named | PurchaseOrderDocumentProps | | 265 | -| QuoteDocument | QuoteDocument.tsx | named | QuoteDocumentProps | | 409 | -| QuoteFooterBar | QuoteFooterBar.tsx | named | QuoteFooterBarProps | Y | 236 | -| QuoteManagementClient | QuoteManagementClient.tsx | named | QuoteManagementClientProps | Y | 713 | -| QuotePreviewContent | QuotePreviewContent.tsx | named | QuotePreviewContentProps | Y | 434 | -| QuotePreviewModal | QuotePreviewModal.tsx | named | QuotePreviewModalProps | Y | 132 | -| QuoteRegistration | QuoteRegistration.tsx | named | QuoteRegistrationProps | Y | 1023 | -| QuoteSummaryPanel | QuoteSummaryPanel.tsx | named | QuoteSummaryPanelProps | Y | 277 | -| QuoteTransactionModal | QuoteTransactionModal.tsx | named | QuoteTransactionModalProps | Y | 324 | - -### settings (16) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| AccountDetail | AccountDetail.tsx | named | AccountDetailProps | Y | 356 | -| AccountDetail | AccountDetail.tsx | named | AccountDetailProps | Y | 369 | -| AddCompanyDialog | AddCompanyDialog.tsx | named | AddCompanyDialogProps | Y | 149 | -| ItemSettingsDialog | ItemSettingsDialog.tsx | named | ItemSettingsDialogProps | Y | 336 | -| PaymentHistoryClient | PaymentHistoryClient.tsx | named | PaymentHistoryClientProps | Y | 255 | -| PermissionDetail | PermissionDetail.tsx | named | PermissionDetailProps | Y | 456 | -| PermissionDetailClient | PermissionDetailClient.tsx | named | PermissionDetailClientProps | Y | 700 | -| PermissionDialog | PermissionDialog.tsx | named | | Y | 109 | -| PopupDetail | PopupDetail.tsx | both | PopupDetailProps | Y | 125 | -| PopupDetailClientV2 | PopupDetailClientV2.tsx | named | PopupDetailClientV2Props | Y | 199 | -| PopupForm | PopupForm.tsx | both | PopupFormProps | Y | 319 | -| PopupList | PopupList.tsx | both | PopupListProps | Y | 198 | -| RankDialog | RankDialog.tsx | named | | Y | 89 | -| SubscriptionClient | SubscriptionClient.tsx | named | SubscriptionClientProps | Y | 242 | -| SubscriptionManagement | SubscriptionManagement.tsx | named | SubscriptionManagementProps | Y | 250 | -| TitleDialog | TitleDialog.tsx | named | | Y | 90 | - -### templates (11) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| DetailActions | DetailActions.tsx | both | DetailActionsProps | Y | 172 | -| DetailField | DetailField.tsx | both | DetailFieldProps | Y | 91 | -| DetailFieldSkeleton | DetailFieldSkeleton.tsx | both | DetailFieldSkeletonProps | Y | 48 | -| DetailGrid | DetailGrid.tsx | both | DetailGridProps | Y | 63 | -| DetailGridSkeleton | DetailGridSkeleton.tsx | both | DetailGridSkeletonProps | Y | 61 | -| DetailSection | DetailSection.tsx | both | DetailSectionProps | Y | 97 | -| DetailSectionSkeleton | DetailSectionSkeleton.tsx | both | DetailSectionSkeletonProps | Y | 53 | -| DetailSectionSkeleton | skeletons.tsx | both | DetailFieldSkeletonProps | Y | 183 | -| FieldInput | FieldInput.tsx | both | FieldInputProps | Y | 408 | -| FieldRenderer | FieldRenderer.tsx | named | FieldRendererProps | Y | 390 | -| IntegratedListTemplateV2 | IntegratedListTemplateV2.tsx | named | IntegratedListTemplateV2Props | Y | 1087 | - -### vehicle-management (3) - -| Component | File | Export | Props | Client | Lines | -|-----------|------|--------|-------|--------|-------| -| Config | config.tsx | none | | Y | 431 | -| Config | config.tsx | none | | Y | 479 | -| Config | config.tsx | none | | Y | 266 | diff --git a/claudedocs/construction/[IMPL-2026-01-05] category-management-checklist.md b/claudedocs/construction/[IMPL-2026-01-05] category-management-checklist.md deleted file mode 100644 index 59090938..00000000 --- a/claudedocs/construction/[IMPL-2026-01-05] category-management-checklist.md +++ /dev/null @@ -1,98 +0,0 @@ -# [IMPL-2026-01-05] 카테고리관리 페이지 구현 체크리스트 - -## 개요 -- **위치**: 발주관리 > 기준정보 > 카테고리관리 -- **URL**: `/ko/juil/order/base-info/categories` -- **참조 페이지**: `/ko/settings/ranks` (직급관리) -- **기능**: 동일, 텍스트/라벨만 다름 - -## 스크린샷 분석 - -### UI 구성 -| 구성요소 | 내용 | -|---------|------| -| 타이틀 | 카테고리관리 | -| 설명 | 카테고리를 등록하고 관리합니다. | -| 입력필드 라벨 | 카테고리 | -| 입력필드 placeholder | 카테고리를 입력해주세요 | -| 테이블 컬럼 | 카테고리, 작업 | -| 기본 데이터 | 슬라이드 OPEN 사이즈, 모터, 공정자재, 철물 | - -### Description 영역 (참고용, UI 미구현) -1. 추가 버튼 클릭 시 목록 최하단에 추가 -2. 드래그&드롭으로 순서 변경 -3. 수정 버튼 → 수정 팝업 -4. 삭제 버튼 → 조건별 Alert: - - 품목 사용 중: "(카테고리명)을 사용하고 있는 품목이 있습니다. 모두 변경 후 삭제가 가능합니다." - - 미사용: "정말 삭제하시겠습니까?" → "삭제가 되었습니다." - - 기본 카테고리: "기본 카테고리는 삭제가 불가합니다." - -## 구현 체크리스트 - -### Phase 1: 파일 구조 생성 -- [x] `src/app/[locale]/(protected)/juil/order/base-info/categories/page.tsx` 생성 -- [x] `src/components/business/juil/category-management/` 디렉토리 생성 - -### Phase 2: 컴포넌트 구현 (RankManagement 복제 + 수정) -- [x] `index.tsx` - CategoryManagement 메인 컴포넌트 - - 타이틀: "카테고리관리" - - 설명: "카테고리를 등록하고 관리합니다. 드래그하여 순서를 변경할 수 있습니다." - - 아이콘: `FolderTree` - - 입력 placeholder: "카테고리를 입력해주세요" -- [x] `types.ts` - Category 타입 정의 -- [x] `actions.ts` - Server Actions (목데이터) -- [x] `CategoryDialog.tsx` - 수정 다이얼로그 - -### Phase 3: 텍스트 변경 사항 -| 원본 (ranks) | 변경 (categories) | 상태 | -|-------------|-------------------|------| -| 직급 | 카테고리 | ✅ | -| 직급관리 | 카테고리관리 | ✅ | -| 사원의 직급을 관리합니다 | 카테고리를 등록하고 관리합니다 | ✅ | -| 직급명을 입력하세요 | 카테고리를 입력해주세요 | ✅ | -| 직급이 추가되었습니다 | 카테고리가 추가되었습니다 | ✅ | -| 직급이 수정되었습니다 | 카테고리가 수정되었습니다 | ✅ | -| 직급이 삭제되었습니다 | 카테고리가 삭제되었습니다 | ✅ | -| 등록된 직급이 없습니다 | 등록된 카테고리가 없습니다 | ✅ | - -### Phase 4: 삭제 로직 (삭제 조건 처리) -- [x] 기본 카테고리 삭제 불가 로직 추가 (`isDefault` 플래그) -- [x] 조건별 Alert 메시지 분기 (actions.ts의 `errorType` 반환) -- [ ] 품목 사용 여부 체크 로직 추가 (추후 API 연동 시) - -### Phase 5: 목데이터 설정 -- [x] 기본 카테고리 4개 설정 완료 -```typescript -const mockCategories = [ - { id: '1', name: '슬라이드 OPEN 사이즈', order: 1, isDefault: true }, - { id: '2', name: '모터', order: 2, isDefault: true }, - { id: '3', name: '공정자재', order: 3, isDefault: true }, - { id: '4', name: '철물', order: 4, isDefault: true }, -]; -``` - -### Phase 6: 테스트 URL 문서 업데이트 -- [x] `claudedocs/[REF] juil-pages-test-urls.md` 업데이트 - - 발주관리 > 기준정보 섹션 추가 - - 카테고리관리 URL 추가 - -## 파일 구조 - -``` -src/ -├── app/[locale]/(protected)/juil/order/ -│ └── base-info/ -│ └── categories/ -│ └── page.tsx -└── components/business/juil/ - └── category-management/ - ├── index.tsx - ├── types.ts - ├── actions.ts - └── CategoryDialog.tsx -``` - -## 진행 상태 -- 생성일: 2026-01-05 -- 상태: ✅ 완료 (목데이터 기반) -- 남은 작업: API 연동 시 품목 사용 여부 체크 로직 추가 \ No newline at end of file diff --git a/claudedocs/construction/[IMPL-2026-01-05] item-management-checklist.md b/claudedocs/construction/[IMPL-2026-01-05] item-management-checklist.md deleted file mode 100644 index 37156b4b..00000000 --- a/claudedocs/construction/[IMPL-2026-01-05] item-management-checklist.md +++ /dev/null @@ -1,209 +0,0 @@ -# [IMPL-2026-01-05] 품목관리 페이지 구현 체크리스트 - -## 개요 -- **위치**: 발주관리 > 기준정보 > 품목관리 -- **URL**: `/ko/juil/order/base-info/items` -- **참조 템플릿**: IntegratedListTemplateV2 (리스트 페이지 표준) -- **기능**: 품목 CRUD, 필터링, 검색, 정렬 - -## 스크린샷 분석 - -### 헤더 영역 -| 구성요소 | 내용 | -|---------|------| -| 타이틀 | 품목관리 | -| 설명 | 품목을 등록하여 관리합니다. | -| 날짜 필터 | 날짜 범위 선택 (DateRangePicker) | -| 빠른 날짜 버튼 | 전체년도, 전전월, 전월, 당월, 어제, 오늘 | -| 액션 버튼 | 품목 등록 (빨간색 primary) | - -### 통계 카드 -| 카드 | 내용 | -|------|------| -| 전체 품목 | 전체 품목 수 표시 | -| 사용 품목 | 사용 중인 품목 수 표시 | - -### 검색 및 필터 영역 -| 구성요소 | 내용 | -|---------|------| -| 검색 입력 | 품목명 검색 | -| 선택 카운트 | N건 / N건 선택 | -| 삭제 버튼 | 선택된 항목 일괄 삭제 | - -### 테이블 컬럼 -| 컬럼 | 타입 | 필터 옵션 | -|------|------|----------| -| 체크박스 | checkbox | - | -| 품목번호 | text | - | -| 물품유형 | select filter | 전체, 제품, 부품, 소모품, 공과 | -| 카테고리 | select filter + search | 전체, 기본, (카테고리 목록) | -| 품목명 | text | - | -| 규격 | select filter | 전체, 인정, 비인정 | -| 단위 | text | - | -| 구분 | select filter | 전체, 경품발주, 원자재발주, 외주발주 | -| 상태 | badge | 승인, 작업 | -| 작업 | actions | 수정(연필 아이콘) | - -### Description 영역 (참고용, UI 미구현) -1. 품목 등록 버튼 - 클릭 시 품목 상세 등록 화면으로 이동 -2. 물품유형 셀렉트 박스 - 전체/제품/부품/소모품/공과 (디폴트: 전체) -3. 카테고리 셀렉트 박스, 검색 - 전체/기본/카테고리 목록 (디폴트: 전체) -4. 규격 셀렉트 박스 - 전체/인정/비인정 (디폴트: 전체) -5. 구분 셀렉트 박스 - 전체/경품발주/원자재발주/외주발주 (디폴트: 전체) -6. 상태 셀렉트 박스 - 전체/사용/중지 (디폴트: 전체) -7. 정렬 셀렉트 박스 - 최신순/등록순 (디폴트: 최신순) - -## 구현 체크리스트 - -### Phase 1: 파일 구조 생성 -- [x] `src/app/[locale]/(protected)/juil/order/base-info/items/page.tsx` 생성 -- [x] `src/components/business/juil/item-management/` 디렉토리 생성 - -### Phase 2: 타입 및 상수 정의 -- [x] `types.ts` - Item 타입 정의 - ```typescript - interface Item { - id: string; - itemNumber: string; // 품목번호 - itemType: ItemType; // 물품유형 - categoryId: string; // 카테고리 ID - categoryName: string; // 카테고리명 - itemName: string; // 품목명 - specification: string; // 규격 (인쇄/비인쇄) - unit: string; // 단위 - orderType: OrderType; // 구분 - status: ItemStatus; // 상태 - createdAt: string; - updatedAt: string; - } - ``` -- [x] `constants.ts` - 필터 옵션 상수 정의 - ```typescript - // 물품유형 - const ITEM_TYPES = ['전체', '제품', '부품', '소모품', '공과']; - - // 규격 - const SPECIFICATIONS = ['전체', '인정', '비인정']; - - // 구분 - const ORDER_TYPES = ['전체', '경품발주', '원자재발주', '외주발주']; - - // 상태 - const ITEM_STATUSES = ['전체', '사용', '중지']; - - // 정렬 - const SORT_OPTIONS = ['최신순', '등록순']; - ``` - -### Phase 3: 메인 컴포넌트 구현 -- [x] `index.tsx` - ItemManagement 메인 컴포넌트 (export) -- [x] `ItemManagementClient.tsx` - 클라이언트 컴포넌트 - - IntegratedListTemplateV2 사용 - - 헤더: 타이틀, 설명, 날짜필터, 품목등록 버튼 - - 통계 카드: StatCards 컴포넌트 활용 - - 테이블: 컬럼 헤더 필터 포함 - - 검색 및 삭제 기능 - -### Phase 4: 테이블 컬럼 설정 -- [x] 테이블 컬럼 정의 (ItemManagementClient.tsx 내 포함) - - 체크박스 컬럼 - - 품목번호 컬럼 - - 물품유형 컬럼 (헤더 필터 Select) - - 카테고리 컬럼 (헤더 필터 Select + 검색) - - 품목명 컬럼 - - 규격 컬럼 (헤더 필터 Select) - - 단위 컬럼 - - 구분 컬럼 (헤더 필터 Select) - - 상태 컬럼 (Badge 표시) - - 작업 컬럼 (수정 버튼) - -### Phase 5: Server Actions (목데이터) -- [x] `actions.ts` - Server Actions 구현 - - `getItemList()` - 품목 목록 조회 - - `getItemStats()` - 통계 조회 - - `deleteItem()` - 품목 삭제 - - `deleteItems()` - 품목 일괄 삭제 - - `getCategoryOptions()` - 카테고리 목록 조회 - -### Phase 6: 목데이터 설정 -```typescript -const mockItems: Item[] = [ - { id: '1', itemNumber: '123123', itemType: '제품', categoryName: '카테고리명', itemName: '품목명', specification: '인쇄', unit: 'SET', orderType: '외주발주', status: '승인' }, - { id: '2', itemNumber: '123123', itemType: '부품', categoryName: '카테고리명', itemName: '품목명', specification: '비인쇄', unit: 'SET', orderType: '외주발주', status: '승인' }, - { id: '3', itemNumber: '123123', itemType: '소모품', categoryName: '카테고리명', itemName: '품목명', specification: '인쇄', unit: 'SET', orderType: '외주발주', status: '승인' }, - { id: '4', itemNumber: '123123', itemType: '공과', categoryName: '카테고리명', itemName: '품목명', specification: '비인쇄', unit: 'EA', orderType: '공과', status: '작업' }, - { id: '5', itemNumber: '123123', itemType: '부품', categoryName: '카테고리명', itemName: '품목명', specification: '인쇄', unit: 'EA', orderType: '원자재발주', status: '작업' }, - { id: '6', itemNumber: '123123', itemType: '소모품', categoryName: '카테고리명', itemName: '품목명', specification: '비인쇄', unit: '승인', orderType: '외주발주', status: '작업' }, - { id: '7', itemNumber: '123123', itemType: '소모품', categoryName: '카테고리명', itemName: '품목명', specification: '인쇄', unit: '승인', orderType: '공과', status: '작업' }, -]; - -const mockStats = { - totalItems: 7, - activeItems: 5, -}; -``` - -### Phase 7: 헤더 필터 컴포넌트 -- [x] tableHeaderActions 영역에 Select 필터 구현 - - 물품유형 필터 - - 규격 필터 - - 구분 필터 - - 정렬 필터 - -### Phase 8: 등록/상세/수정 페이지 구현 -- [x] 품목 등록 버튼 클릭 → `/ko/juil/order/base-info/items/new` 이동 -- [x] 수정 버튼 클릭 → `/ko/juil/order/base-info/items/[id]?mode=edit` 이동 -- [x] 등록/수정/상세 페이지 구현 (ItemDetailClient.tsx) -- [x] Server Actions (getItem, createItem, updateItem) 구현 -- [x] 발주 항목 동적 추가/삭제 기능 - -### Phase 9: 테스트 URL 문서 업데이트 -- [x] `claudedocs/[REF] juil-pages-test-urls.md` 업데이트 - - 품목관리 URL 추가 - -## 파일 구조 - -``` -src/ -├── app/[locale]/(protected)/juil/order/ -│ └── base-info/ -│ └── items/ -│ ├── page.tsx -│ ├── new/ -│ │ └── page.tsx -│ └── [id]/ -│ └── page.tsx -└── components/business/juil/ - └── item-management/ - ├── index.tsx - ├── ItemManagementClient.tsx - ├── ItemDetailClient.tsx - ├── types.ts - ├── constants.ts - └── actions.ts -``` - -## 참조 컴포넌트 -- `IntegratedListTemplateV2` - 리스트 템플릿 -- `StatCards` - 통계 카드 -- `DateRangePicker` - 날짜 범위 선택 -- `Select` - 필터 셀렉트박스 -- `Badge` - 상태 표시 -- `Button` - 버튼 -- `Checkbox` - 체크박스 - -## UI 구현 참고 -- 컬럼 헤더 내 필터 Select: 기존 프로젝트 내 유사 구현 검색 필요 -- 날짜 빠른 선택 버튼 그룹: 기존 컴포넌트 활용 또는 신규 구현 - -## 진행 상태 -- 생성일: 2026-01-05 -- 상태: ✅ 전체 완료 (리스트 + 상세/등록/수정) - -## 히스토리 -| 날짜 | 작업 내용 | 상태 | -|------|----------|------| -| 2026-01-05 | 체크리스트 작성 | ✅ | -| 2026-01-05 | 리스트 페이지 구현 (Phase 1-7, 9) | ✅ | -| 2026-01-05 | 규격 필터 수정 (인쇄/비인쇄 → 인정/비인정) | ✅ | -| 2026-01-05 | 상세/등록/수정 페이지 구현 (Phase 8) | ✅ | diff --git a/claudedocs/construction/[IMPL-2026-01-05] pricing-management-checklist.md b/claudedocs/construction/[IMPL-2026-01-05] pricing-management-checklist.md deleted file mode 100644 index 93a91da0..00000000 --- a/claudedocs/construction/[IMPL-2026-01-05] pricing-management-checklist.md +++ /dev/null @@ -1,119 +0,0 @@ -# [IMPL-2026-01-05] 단가관리 리스트 페이지 구현 체크리스트 - -## 개요 -- **위치**: 발주관리 > 기준정보 > 단가관리 -- **URL**: `/ko/juil/order/base-info/pricing` -- **참조 페이지**: `/ko/juil/order/order-management` (OrderManagementListClient) -- **패턴**: IntegratedListTemplateV2 + StatCards - -## 스크린샷 분석 - -### UI 구성 - -#### 1. 헤더 영역 -| 구성요소 | 내용 | -|---------|------| -| 타이틀 | 단가관리 | -| 설명 | 단가를 등록하고 관리합니다. | - -#### 2. 달력 + 액션 버튼 영역 -| 구성요소 | 내용 | -|---------|------| -| 날짜 선택 | DateRangeSelector (2025-09-01 ~ 2025-09-03) | -| 액션 버튼들 | 담당단가, 진행단가, 확정, 발행, 이력, 오류, **단가 등록** | - -#### 3. StatCards (통계 카드) -| 카드 | 값 | 설명 | -|------|-----|------| -| 미완료 | 9 | 미완료 단가 | -| 확정 | 5 | 확정된 단가 | -| 발행 | 4 | 발행된 단가 | - -#### 4. 필터 영역 (테이블 헤더) -| 필터 | 옵션 | 기본값 | -|------|------|--------| -| 품목유형 | 전체, 박스, 부속, 소모품, 공과 | 전체 | -| 카테고리 | 전기, (카테고리 목록) | - | -| 규격 | 전체, 진행, 미진행 | 전체 | -| 구분 | 전체, 금동량, 임의적용가, 미구분 | 전체 | -| 상세 | 전체, 사용, 유지, 미등록 | 전체 | -| 정렬 | 최신순, 등록순 | 최신순 | - -#### 5. 테이블 컬럼 -| 컬럼 | 설명 | -|------|------| -| 체크박스 | 행 선택 | -| 단가번호 | 단가 고유번호 | -| 품목유형 | 박스/부속/소모품/공과 | -| 카테고리 | 품목 카테고리 | -| 품목 | 품목명 | -| 금액량 | 수량 정보 | -| 정량 | 정량 정보 | -| 단가 | 단가 금액 | -| 구매처 | 구매처 정보 | -| 예상단가 | 예상 단가 | -| 이전단가 | 이전 단가 | -| 판매단가 | 판매 단가 | -| 실적 | 실적 정보 | - -## 구현 체크리스트 - -### Phase 1: 파일 구조 생성 -- [x] `src/app/[locale]/(protected)/juil/order/base-info/pricing/page.tsx` 생성 -- [x] `src/components/business/juil/pricing-management/` 디렉토리 생성 - -### Phase 2: 타입 및 상수 정의 -- [x] `types.ts` - Pricing 타입, 필터 옵션, 상태 스타일 - - Pricing 인터페이스 - - PricingStats 인터페이스 - - 품목유형 옵션 (ITEM_TYPE_OPTIONS) - - 규격 옵션 (SPEC_OPTIONS) - - 구분 옵션 (DIVISION_OPTIONS) - - 상세 옵션 (DETAIL_OPTIONS) - - 정렬 옵션 (SORT_OPTIONS) - - 상태 스타일 (PRICING_STATUS_STYLES) - -### Phase 3: Server Actions (목데이터) -- [x] `actions.ts` - - getPricingList() - 목록 조회 - - getPricingStats() - 통계 조회 - - deletePricing() - 단일 삭제 - - deletePricings() - 일괄 삭제 - -### Phase 4: 리스트 컴포넌트 -- [x] `PricingListClient.tsx` - - IntegratedListTemplateV2 사용 - - DateRangeSelector (날짜 범위 선택) - - StatCards (미완료/확정/발행) - - 필터 셀렉트 박스들 (품목유형, 규격, 구분, 상세, 정렬) - - 액션 버튼들 (담당단가, 진행단가, 확정, 발행, 이력, 오류, 단가 등록) - - 테이블 렌더링 - - 모바일 카드 렌더링 - - 삭제 다이얼로그 - -### Phase 5: 목데이터 설정 -- [x] 7개 목데이터 설정 완료 - -### Phase 6: 테스트 URL 문서 업데이트 -- [x] `claudedocs/[REF] juil-pages-test-urls.md` 업데이트 - -## 파일 구조 - -``` -src/ -├── app/[locale]/(protected)/juil/order/ -│ └── base-info/ -│ └── pricing/ -│ └── page.tsx -└── components/business/juil/ - └── pricing-management/ - ├── index.ts - ├── types.ts - ├── actions.ts - └── PricingListClient.tsx -``` - -## 진행 상태 -- 생성일: 2026-01-05 -- 상태: ✅ 완료 (목데이터 기반) -- 남은 작업: API 연동 시 실제 데이터 연결 \ No newline at end of file diff --git a/claudedocs/construction/[IMPL-2026-01-09] partner-management-api-integration.md b/claudedocs/construction/[IMPL-2026-01-09] partner-management-api-integration.md deleted file mode 100644 index 4e75315b..00000000 --- a/claudedocs/construction/[IMPL-2026-01-09] partner-management-api-integration.md +++ /dev/null @@ -1,117 +0,0 @@ -# Phase 2.2 거래처관리 API 연동 - -**날짜**: 2026-01-09 -**작업**: 거래처관리 Mock → API 연동 - -## 개요 - -시공사 페이지 API 연동 계획 Phase 2.2 - 거래처관리(partners) API 연동 완료. - -## 변경 사항 - -### Backend (API) - -#### 1. 서비스 (ClientService.php) -- `stats()` - 거래처 통계 조회 (신규) - - total: 전체 거래처 수 - - sales: 판매 거래처 (client_type='SALES') - - purchase: 구매 거래처 (client_type='PURCHASE') - - both: 판매/구매 거래처 (client_type='BOTH') - - badDebt: 악성채권 보유 거래처 수 - - normal: 정상 거래처 수 -- `bulkDestroy()` - 일괄 삭제 (신규) - - 주문 존재 시 해당 거래처는 건너뜀 - -#### 2. 컨트롤러 (ClientController.php) -- `stats()` - GET /api/v1/clients/stats -- `bulkDestroy()` - DELETE /api/v1/clients/bulk - -#### 3. 라우트 (api.php) -```php -Route::get('/stats', [ClientController::class, 'stats']); -Route::delete('/bulk', [ClientController::class, 'bulkDestroy']); -``` - -### Frontend (React) - -#### 1. actions.ts -- Mock 데이터 제거 (mockPartners 배열) -- API 연동 구현 - - `getPartnerList()` - GET /api/v1/clients - - `getPartner()` - GET /api/v1/clients/{id} - - `createPartner()` - POST /api/v1/clients - - `updatePartner()` - PUT /api/v1/clients/{id} - - `getPartnerStats()` - GET /api/v1/clients/stats - - `deletePartner()` - DELETE /api/v1/clients/{id} - - `deletePartners()` - DELETE /api/v1/clients/bulk - -#### 2. 변환 함수 -- `transformClientType()` - client_type → partnerType 변환 -- `transformPartnerType()` - partnerType → client_type 변환 -- `transformPartner()` - API 응답 → Partner 타입 변환 -- `transformPartnerToApi()` - PartnerFormData → API 요청 데이터 변환 - -## API 매핑 - -| Frontend | Backend | 비고 | -|----------|---------|------| -| id | id | string ↔ int | -| partnerCode | client_code | 자동 생성 | -| businessNumber | business_no | | -| partnerName | name | | -| representative | contact_person | | -| partnerType | client_type | sales/SALES, purchase/PURCHASE, both/BOTH | -| businessType | business_type | | -| businessCategory | business_item | | -| address1 | address | | -| phone | phone | | -| mobile | mobile | | -| fax | fax | | -| email | email | | -| manager | manager_name | | -| managerPhone | manager_tel | | -| systemManager | system_manager | | -| outstandingAmount | outstanding_amount | 계산 필드 (매출-입금) | -| overdueToggle | is_overdue | | -| isBadDebt | has_bad_debt | 계산 필드 | -| isActive | is_active | | -| createdAt | created_at | | -| updatedAt | updated_at | | - -### Frontend 전용 필드 (기본값 사용) -- zipCode, address2: '' -- logoUrl, logoBlob: null -- salesPaymentDay, paymentDay: 0 -- creditRating, transactionGrade: '' -- memos, documents: [] -- category: '' -- overdueDays: is_overdue ? 30 : 0 - -## 설계 결정 - -### 기존 Client API 재사용 -- `/api/v1/clients` 기존 엔드포인트 확장 사용 -- 별도의 `/api/v1/construction/partners` 생성하지 않음 -- accounting/vendors 와 construction/partners 모두 Client API 사용 - -### 악성채권 통계 -- BadDebt 테이블과 연계하여 악성채권 보유 거래처 수 계산 -- 상태가 '추심중' 또는 '법적조치'인 활성 악성채권만 카운트 - -### 필터링 전략 -- 검색(`q`): API에서 처리 (name, client_code, contact_person) -- 악성채권 필터: 프론트엔드에서 처리 (API 전체 반환 후 필터) -- 정렬: 프론트엔드에서 처리 (API 기본 정렬 사용) - -## 진행률 - -시공사 API 연동: 4/9 (44%) -- [x] Phase 1.1 견적관리 -- [x] Phase 1.2 인수인계보고서관리 -- [x] Phase 2.1 현장관리 -- [x] Phase 2.2 거래처관리 ← 현재 완료 -- [ ] Phase 2.3 자재관리 -- [ ] Phase 3.1 발주관리 -- [ ] Phase 3.2 재고관리 -- [ ] Phase 4.1 정산관리 -- [ ] Phase 4.2 급여관리 \ No newline at end of file diff --git a/claudedocs/construction/[IMPL-2026-01-09] site-management-api-integration.md b/claudedocs/construction/[IMPL-2026-01-09] site-management-api-integration.md deleted file mode 100644 index 307362d8..00000000 --- a/claudedocs/construction/[IMPL-2026-01-09] site-management-api-integration.md +++ /dev/null @@ -1,90 +0,0 @@ -# Phase 2.1 현장관리 API 연동 - -**날짜**: 2026-01-09 -**작업**: 현장관리 Mock → API 연동 - -## 개요 - -시공사 페이지 API 연동 계획 Phase 2.1 - 현장관리(site-management) API 연동 완료. - -## 변경 사항 - -### Backend (API) - -#### 1. 마이그레이션 -- `2026_01_09_162534_add_construction_fields_to_sites_table.php` - - `site_code` (VARCHAR 50) - 현장코드 - - `client_id` (FK → clients) - 거래처 연결 - - `status` (ENUM) - unregistered/suspended/active/pending - - 인덱스: tenant_id + site_code, tenant_id + status - -#### 2. 모델 (Site.php) -- 상태 상수 추가: STATUS_UNREGISTERED, STATUS_SUSPENDED, STATUS_ACTIVE, STATUS_PENDING -- fillable 확장: site_code, client_id, status -- Client 관계 추가 - -#### 3. 서비스 (SiteService.php) -- `index()` - 필터 확장 (status, client_id, start_date, end_date) -- `stats()` - 상태별 통계 조회 (신규) -- `bulkDestroy()` - 일괄 삭제 (신규) - -#### 4. 컨트롤러 (SiteController.php) -- `stats()` - GET /api/v1/sites/stats -- `bulkDestroy()` - DELETE /api/v1/sites/bulk - -#### 5. 라우트 (api.php) -```php -Route::get('/stats', [SiteController::class, 'stats']); -Route::delete('/bulk', [SiteController::class, 'bulkDestroy']); -``` - -### Frontend (React) - -#### 1. types.ts -- SiteStats에 suspended, pending 필드 추가 - -#### 2. actions.ts -- Mock 데이터 제거 -- API 연동 구현 - - `getSiteList()` - GET /api/v1/sites - - `getSiteStats()` - GET /api/v1/sites/stats - - `deleteSite()` - DELETE /api/v1/sites/{id} - - `deleteSites()` - DELETE /api/v1/sites/bulk - -## API 매핑 - -| Frontend | Backend | 비고 | -|----------|---------|------| -| id | id | string ↔ int | -| siteCode | site_code | | -| partnerId | client_id | | -| partnerName | client.name | 관계 eager load | -| siteName | name | | -| address | address | | -| status | status | 동일 | -| createdAt | created_at | | -| updatedAt | updated_at | | - -## 설계 결정 - -### is_active vs status -- `is_active` (boolean): 사용 여부 (활성화/비활성화) -- `status` (enum): 상태값 (미등록/중지/사용/보류) -- 두 필드는 다른 용도로 둘 다 유지 - -### 기존 API 활용 -- `/api/v1/sites` 기존 엔드포인트 확장 사용 -- `/api/v1/construction/sites` 별도 생성하지 않음 - -## 진행률 - -시공사 API 연동: 3/9 (33%) -- [x] Phase 1.1 견적관리 -- [x] Phase 1.2 인수인계보고서관리 -- [x] Phase 2.1 현장관리 ← 현재 완료 -- [ ] Phase 2.2 거래처관리 -- [ ] Phase 2.3 자재관리 -- [ ] Phase 3.1 발주관리 -- [ ] Phase 3.2 재고관리 -- [ ] Phase 4.1 정산관리 -- [ ] Phase 4.2 급여관리 \ No newline at end of file diff --git a/claudedocs/construction/[IMPL-2026-01-12] project-detail-checklist.md b/claudedocs/construction/[IMPL-2026-01-12] project-detail-checklist.md deleted file mode 100644 index 842d86dc..00000000 --- a/claudedocs/construction/[IMPL-2026-01-12] project-detail-checklist.md +++ /dev/null @@ -1,52 +0,0 @@ -# 프로젝트 실행관리 상세 페이지 구현 체크리스트 - -## 구현 일자: 2026-01-12 - -## 페이지 구조 -- 페이지 경로: `/construction/project/management/[id]` -- 칸반 보드 형태의 상세 페이지 -- 프로젝트 → 단계 → 상세 연동 - ---- - -## 작업 목록 - -### 1. 타입 및 데이터 준비 -- [x] types.ts - 상세 페이지용 타입 추가 (Stage, StageDetail, ProjectDetail 등) -- [x] actions.ts - 상세 페이지 목업 데이터 추가 - -### 2. 칸반 보드 컴포넌트 -- [x] ProjectKanbanBoard.tsx - 칸반 보드 컨테이너 -- [x] KanbanColumn.tsx - 칸반 컬럼 공통 컴포넌트 -- [x] ProjectCard.tsx - 프로젝트 카드 (진행률, 계약금, 기간) -- [x] StageCard.tsx - 단계 카드 (입찰/계약/시공) -- [x] DetailCard.tsx - 상세 카드 (현장설명회 등 단순 목록) - -### 3. 프로젝트 종료 팝업 -- [x] ProjectEndDialog.tsx - 프로젝트 종료 다이얼로그 - -### 4. 메인 페이지 조립 -- [x] ProjectDetailClient.tsx - 메인 클라이언트 컴포넌트 -- [x] page.tsx - 상세 페이지 진입점 - -### 5. 검증 -- [ ] 칸반 보드 동작 확인 (프로젝트→단계→상세 연동) -- [ ] 프로젝트 종료 팝업 동작 확인 -- [ ] 리스트 페이지에서 상세 페이지 이동 확인 - ---- - -## 참고 사항 -- 1차 구현: 상세 하위 목록 없는 경우 (현장설명회) 먼저 구현 -- 이후 추가로 보면서 맞춰가기 -- 기존 리스트 페이지 패턴 참고 - ---- - -## 진행 상황 -- 시작: 2026-01-12 -- 현재 상태: 1차 구현 완료, 브라우저 검증 대기 - -## 테스트 URL -- 리스트 페이지: http://localhost:3000/ko/construction/project/management -- 상세 페이지: http://localhost:3000/ko/construction/project/management/1 diff --git a/claudedocs/construction/[PLAN-2026-01-02] estimate-detail-form-refactoring.md b/claudedocs/construction/[PLAN-2026-01-02] estimate-detail-form-refactoring.md deleted file mode 100644 index d00beb25..00000000 --- a/claudedocs/construction/[PLAN-2026-01-02] estimate-detail-form-refactoring.md +++ /dev/null @@ -1,231 +0,0 @@ -# EstimateDetailForm.tsx 파일 분할 계획서 - -## 현황 분석 - -- **파일 위치**: `src/components/business/juil/estimates/EstimateDetailForm.tsx` -- **현재 라인 수**: 2,088줄 -- **문제점**: 단일 파일에 모든 섹션, 핸들러, 상태 관리가 집중되어 유지보수 어려움 - -## 파일 구조 분석 - -### 현재 구조 (라인 범위) - -| 구분 | 라인 | 설명 | -|------|------|------| -| Imports | 1-56 | React, UI 컴포넌트, 타입 | -| 상수/유틸 | 58-75 | MOCK_MATERIALS, MOCK_EXPENSES, formatAmount | -| Props | 77-81 | EstimateDetailFormProps | -| State | 88-127 | formData, 로딩, 다이얼로그, 모달 상태 | -| 핸들러 - 네비게이션 | 130-140 | handleBack, handleEdit, handleCancel | -| 핸들러 - 저장/삭제 | 143-182 | handleSave, handleConfirmSave, handleDelete, handleConfirmDelete | -| 핸들러 - 견적 요약 | 185-227 | handleAddSummaryItem, handleRemoveSummaryItem, handleSummaryItemChange | -| 핸들러 - 공과 상세 | 230-259 | handleAddExpenseItem, handleRemoveExpenseItem, handleExpenseItemChange | -| 핸들러 - 단가 조정 | 262-283 | handlePriceAdjustmentChange | -| 핸들러 - 견적 상세 | 286-343 | handleAddDetailItem, handleRemoveDetailItem, handleDetailItemChange | -| 핸들러 - 파일 업로드 | 346-435 | handleDocumentUpload, handleDocumentRemove, 드래그앤드롭 | -| useMemo | 438-482 | pageTitle, pageDescription, headerActions | -| JSX - 견적 정보 | 496-526 | 견적 정보 Card | -| JSX - 현장설명회 | 528-551 | 현장설명회 정보 Card | -| JSX - 입찰 정보 | 553-736 | 입찰 정보 Card + 파일 업로드 | -| JSX - 견적 요약 | 738-890 | 견적 요약 정보 Table | -| JSX - 공과 상세 | 892-1071 | 공과 상세 Table | -| JSX - 단가 조정 | 1073-1224 | 품목 단가 조정 Table | -| JSX - 견적 상세 | 1226-2017 | 견적 상세 Table (가장 큰 섹션) | -| 모달/다이얼로그 | 2020-2085 | 전자결재, 견적서, 삭제/저장 다이얼로그 | - ---- - -## 분할 계획 - -### 1단계: 섹션 컴포넌트 분리 - -``` -src/components/business/juil/estimates/ -├── EstimateDetailForm.tsx # 메인 컴포넌트 (축소) -├── sections/ -│ ├── index.ts # 섹션 export -│ ├── EstimateInfoSection.tsx # 견적 정보 + 현장설명회 + 입찰 정보 -│ ├── EstimateSummarySection.tsx # 견적 요약 정보 -│ ├── ExpenseDetailSection.tsx # 공과 상세 -│ ├── PriceAdjustmentSection.tsx # 품목 단가 조정 -│ └── EstimateDetailTableSection.tsx # 견적 상세 테이블 -├── hooks/ -│ ├── index.ts # hooks export -│ └── useEstimateCalculations.ts # 계산 로직 (면적, 무게, 단가 등) -└── utils/ - ├── index.ts # utils export - ├── constants.ts # MOCK_MATERIALS, MOCK_EXPENSES - └── formatters.ts # formatAmount -``` - -### 2단계: 각 파일 상세 - -#### 2.1 constants.ts (~20줄) -```typescript -// MOCK_MATERIALS, MOCK_EXPENSES 이동 -export const MOCK_MATERIALS = [...]; -export const MOCK_EXPENSES = [...]; -``` - -#### 2.2 formatters.ts (~10줄) -```typescript -// formatAmount 함수 이동 -export function formatAmount(amount: number): string { ... } -``` - -#### 2.3 useEstimateCalculations.ts (~100줄) -```typescript -// 견적 상세 테이블의 계산 로직 분리 -// - 면적, 무게, 철제스크린, 코킹, 레일, 하장 등 계산 -// - 합계 계산 로직 -export function useEstimateCalculations( - item: EstimateDetailItem, - priceAdjustmentData: PriceAdjustmentData, - useAdjustedPrice: boolean -) { ... } - -export function calculateTotals( - items: EstimateDetailItem[], - priceAdjustmentData: PriceAdjustmentData, - useAdjustedPrice: boolean -) { ... } -``` - -#### 2.4 EstimateInfoSection.tsx (~250줄) -```typescript -// 견적 정보 + 현장설명회 + 입찰 정보 Card 3개 -// 파일 업로드 영역 포함 -interface EstimateInfoSectionProps { - formData: EstimateDetailFormData; - setFormData: React.Dispatch>; - isViewMode: boolean; - documentInputRef: React.RefObject; -} -``` - -#### 2.5 EstimateSummarySection.tsx (~200줄) -```typescript -// 견적 요약 정보 테이블 -interface EstimateSummarySectionProps { - summaryItems: EstimateSummaryItem[]; - summaryMemo: string; - isViewMode: boolean; - onAddItem: () => void; - onRemoveItem: (id: string) => void; - onItemChange: (id: string, field: keyof EstimateSummaryItem, value: string | number) => void; - onMemoChange: (memo: string) => void; -} -``` - -#### 2.6 ExpenseDetailSection.tsx (~200줄) -```typescript -// 공과 상세 테이블 -interface ExpenseDetailSectionProps { - expenseItems: ExpenseItem[]; - isViewMode: boolean; - onAddItems: (count: number) => void; - onRemoveSelected: () => void; - onItemChange: (id: string, field: keyof ExpenseItem, value: string | number) => void; - onSelectItem: (id: string, selected: boolean) => void; - onSelectAll: (selected: boolean) => void; -} -``` - -#### 2.7 PriceAdjustmentSection.tsx (~200줄) -```typescript -// 품목 단가 조정 테이블 -interface PriceAdjustmentSectionProps { - priceAdjustmentData: PriceAdjustmentData; - isViewMode: boolean; - onPriceChange: (key: string, value: number) => void; - onSave: () => void; - onApplyAll: () => void; - onReset: () => void; -} -``` - -#### 2.8 EstimateDetailTableSection.tsx (~600줄) -```typescript -// 견적 상세 테이블 (가장 큰 섹션) -interface EstimateDetailTableSectionProps { - detailItems: EstimateDetailItem[]; - priceAdjustmentData: PriceAdjustmentData; - useAdjustedPrice: boolean; - isViewMode: boolean; - onAddItems: (count: number) => void; - onRemoveItem: (id: string) => void; - onRemoveSelected: () => void; - onItemChange: (id: string, field: keyof EstimateDetailItem, value: string | number) => void; - onSelectItem: (id: string, selected: boolean) => void; - onSelectAll: (selected: boolean) => void; - onApplyAdjustedPrice: () => void; - onReset: () => void; -} -``` - ---- - -## 분할 후 예상 라인 수 - -| 파일 | 예상 라인 수 | -|------|-------------| -| EstimateDetailForm.tsx (메인) | ~300줄 | -| EstimateInfoSection.tsx | ~250줄 | -| EstimateSummarySection.tsx | ~200줄 | -| ExpenseDetailSection.tsx | ~200줄 | -| PriceAdjustmentSection.tsx | ~200줄 | -| EstimateDetailTableSection.tsx | ~600줄 | -| useEstimateCalculations.ts | ~100줄 | -| constants.ts | ~20줄 | -| formatters.ts | ~10줄 | -| **총합** | ~1,880줄 (약 10% 감소) | - ---- - -## 실행 순서 - -### Phase 1: 유틸리티 분리 (5분) -- [ ] `utils/constants.ts` 생성 -- [ ] `utils/formatters.ts` 생성 -- [ ] `utils/index.ts` 생성 - -### Phase 2: 계산 로직 분리 (10분) -- [ ] `hooks/useEstimateCalculations.ts` 생성 -- [ ] `hooks/index.ts` 생성 - -### Phase 3: 섹션 컴포넌트 분리 (30분) -- [ ] `sections/EstimateInfoSection.tsx` 생성 -- [ ] `sections/EstimateSummarySection.tsx` 생성 -- [ ] `sections/ExpenseDetailSection.tsx` 생성 -- [ ] `sections/PriceAdjustmentSection.tsx` 생성 -- [ ] `sections/EstimateDetailTableSection.tsx` 생성 -- [ ] `sections/index.ts` 생성 - -### Phase 4: 메인 컴포넌트 리팩토링 (10분) -- [ ] EstimateDetailForm.tsx에서 분리된 컴포넌트 import -- [ ] 핸들러 정리 및 props 전달 -- [ ] 불필요한 코드 제거 - -### Phase 5: 검증 (5분) -- [ ] TypeScript 빌드 확인 -- [ ] 기능 동작 확인 - ---- - -## 주의사항 - -1. **상태 관리**: formData, setFormData는 메인 컴포넌트에서 관리, 섹션에 props로 전달 -2. **타입 일관성**: 기존 types.ts의 타입 그대로 사용 -3. **핸들러 위치**: 핸들러는 메인 컴포넌트에 유지, 섹션에 콜백으로 전달 -4. **조정단가 상태**: appliedPrices, useAdjustedPrice는 메인 컴포넌트에서 관리 - ---- - -## 5가지 수정사항 (분할 후 진행) - -| # | 항목 | 수정 위치 (분할 후) | -|---|------|-------------------| -| 2 | 품목 단가 초기화 → 품목 단가만 | PriceAdjustmentSection.tsx | -| 3 | 견적 상세 인풋 필드 추가 | EstimateDetailTableSection.tsx | -| 4 | 견적 상세 초기화 버튼 수정 | EstimateDetailTableSection.tsx | -| 5 | 각 섹션별 초기화 분리 | 각 Section 컴포넌트 | \ No newline at end of file diff --git a/claudedocs/construction/[PLAN-2026-01-05] order-detail-form-separation.md b/claudedocs/construction/[PLAN-2026-01-05] order-detail-form-separation.md deleted file mode 100644 index 8c636d20..00000000 --- a/claudedocs/construction/[PLAN-2026-01-05] order-detail-form-separation.md +++ /dev/null @@ -1,292 +0,0 @@ -# OrderDetailForm.tsx 분리 계획서 - -**생성일**: 2026-01-05 -**현재 파일 크기**: 1,273줄 -**목표**: 유지보수성 향상을 위한 컴포넌트 분리 - ---- - -## 현재 파일 구조 분석 - -| 영역 | 라인 | 비율 | 내용 | -|------|------|------|------| -| Import & Types | 1-69 | 5% | 의존성 및 타입 import | -| Props Interface | 70-74 | 0.5% | 컴포넌트 props | -| State & Hooks | 76-113 | 3% | 상태 관리 (12개 useState) | -| Handlers | 114-433 | 25% | 핸들러 함수들 (20+개) | -| JSX Render | 435-1271 | 66% | UI 렌더링 | - -### 주요 핸들러 분류 (114-433줄) -- **Navigation**: handleBack, handleEdit, handleCancel (114-125) -- **Form Field**: handleFieldChange (127-133) -- **CRUD Operations**: handleSave, handleDelete, handleDuplicate (135-199) -- **Category Operations**: handleAddCategory, handleDeleteCategory, handleCategoryChange (206-247) -- **Item Operations**: handleAddItems, handleDeleteSelectedItems, handleDeleteAllItems, handleItemChange (249-327) -- **Selection**: handleToggleSelection, handleToggleSelectAll (330-357) -- **Calendar**: handleCalendarDateClick, handleCalendarMonthChange (359-385) - -### 주요 JSX 영역 (435-1271줄) -- **발주 정보 Card**: 447-559 (112줄) -- **계약 정보 Card**: 561-694 (133줄) -- **발주 스케줄 Calendar**: 696-715 (19줄) -- **발주 상세 테이블**: 717-1172 (455줄) ⚠️ **가장 큰 부분** -- **카테고리 추가 버튼**: 1174-1182 (8줄) -- **비고 Card**: 1184-1198 (14줄) -- **Dialogs**: 1201-1261 (60줄) -- **Document Modal**: 1263-1270 (7줄) - ---- - -## 분리 계획 - -### Phase 1: 커스텀 훅 분리 - -**파일**: `hooks/useOrderDetailForm.ts` -**예상 크기**: ~250줄 - -```typescript -// 추출할 내용 -- formData 상태 관리 -- selectedItems, addCounts, categoryFilters 상태 -- calendarDate, selectedCalendarDate 상태 -- 모든 핸들러 함수들 -- calendarEvents useMemo -``` - -**장점**: -- 비즈니스 로직과 UI 분리 -- 테스트 용이성 향상 -- 재사용 가능 - ---- - -### Phase 2: 카드 컴포넌트 분리 - -#### 2-1. `cards/OrderInfoCard.tsx` -**예상 크기**: ~120줄 - -```typescript -interface OrderInfoCardProps { - formData: OrderDetailFormData; - isViewMode: boolean; - onFieldChange: (field: keyof OrderDetailFormData, value: any) => void; -} -``` - -**포함 내용**: 발주번호, 발주일, 구분, 상태, 발주담당자, 화물도착지 - ---- - -#### 2-2. `cards/ContractInfoCard.tsx` -**예상 크기**: ~150줄 - -```typescript -interface ContractInfoCardProps { - formData: OrderDetailFormData; - isViewMode: boolean; - isEditMode: boolean; - onFieldChange: (field: keyof OrderDetailFormData, value: any) => void; -} -``` - -**포함 내용**: 거래처명, 현장명, 계약번호, 공사PM, 공사담당자 - ---- - -#### 2-3. `cards/OrderScheduleCard.tsx` -**예상 크기**: ~50줄 - -```typescript -interface OrderScheduleCardProps { - events: ScheduleEvent[]; - currentDate: Date; - selectedDate: Date | null; - onDateClick: (date: Date) => void; - onMonthChange: (date: Date) => void; -} -``` - -**포함 내용**: ScheduleCalendar 래핑 - ---- - -#### 2-4. `cards/OrderMemoCard.tsx` -**예상 크기**: ~40줄 - -```typescript -interface OrderMemoCardProps { - memo: string; - isViewMode: boolean; - onMemoChange: (value: string) => void; -} -``` - -**포함 내용**: 비고 Textarea - ---- - -### Phase 3: 테이블 컴포넌트 분리 (가장 중요) - -#### 3-1. `tables/OrderDetailItemTable.tsx` -**예상 크기**: ~350줄 - -```typescript -interface OrderDetailItemTableProps { - category: OrderDetailCategory; - isEditMode: boolean; - isViewMode: boolean; - selectedItems: Set; - addCount: number; - onAddCountChange: (count: number) => void; - onAddItems: (count: number) => void; - onDeleteSelectedItems: () => void; - onDeleteAllItems: () => void; - onCategoryChange: (field: keyof OrderDetailCategory, value: string) => void; - onItemChange: (itemId: string, field: keyof OrderDetailItem, value: any) => void; - onToggleSelection: (itemId: string) => void; - onToggleSelectAll: () => void; -} -``` - -**포함 내용**: -- 카드 헤더 (왼쪽: 발주 상세/N건 선택/삭제, 오른쪽: 숫자/추가/카테고리/🗑️) -- 테이블 전체 (TableHeader + TableBody) -- 합계 행 - ---- - -#### 3-2. `tables/OrderDetailItemRow.tsx` (선택적) -**예상 크기**: ~150줄 - -```typescript -interface OrderDetailItemRowProps { - item: OrderDetailItem; - index: number; - isEditMode: boolean; - isSelected: boolean; - onItemChange: (field: keyof OrderDetailItem, value: any) => void; - onToggleSelection: () => void; -} -``` - -**포함 내용**: 단일 테이블 행 렌더링 - ---- - -### Phase 4: 다이얼로그 분리 - -#### 4-1. `dialogs/OrderDialogs.tsx` -**예상 크기**: ~80줄 - -```typescript -interface OrderDialogsProps { - // 저장 다이얼로그 - showSaveDialog: boolean; - onSaveDialogChange: (open: boolean) => void; - onConfirmSave: () => void; - // 삭제 다이얼로그 - showDeleteDialog: boolean; - onDeleteDialogChange: (open: boolean) => void; - onConfirmDelete: () => void; - // 카테고리 삭제 다이얼로그 - showCategoryDeleteDialog: string | null; - onCategoryDeleteDialogChange: (categoryId: string | null) => void; - onConfirmDeleteCategory: () => void; - // 공통 - isLoading: boolean; -} -``` - ---- - -## 분리 후 예상 구조 - -``` -src/components/business/juil/order-management/ -├── OrderDetailForm.tsx (~200줄, 메인 컴포넌트) -├── hooks/ -│ └── useOrderDetailForm.ts (~250줄, 비즈니스 로직) -├── cards/ -│ ├── OrderInfoCard.tsx (~120줄) -│ ├── ContractInfoCard.tsx (~150줄) -│ ├── OrderScheduleCard.tsx (~50줄) -│ └── OrderMemoCard.tsx (~40줄) -├── tables/ -│ ├── OrderDetailItemTable.tsx (~350줄) -│ └── OrderDetailItemRow.tsx (~150줄, 선택적) -├── dialogs/ -│ └── OrderDialogs.tsx (~80줄) -├── modals/ -│ └── OrderDocumentModal.tsx (기존) -├── actions.ts (기존) -└── types.ts (기존) -``` - ---- - -## 분리 전후 비교 - -| 지표 | Before | After | -|------|--------|-------| -| 메인 파일 크기 | 1,273줄 | ~200줄 | -| 가장 큰 파일 | 1,273줄 | ~350줄 | -| 파일 개수 | 1 | 8-9 | -| 테스트 용이성 | 낮음 | 높음 | -| 재사용성 | 낮음 | 중간 | - ---- - -## 실행 체크리스트 - -### Phase 1: 커스텀 훅 분리 -- [ ] `hooks/useOrderDetailForm.ts` 생성 -- [ ] 상태 변수들 이동 -- [ ] 핸들러 함수들 이동 -- [ ] useMemo 이동 -- [ ] OrderDetailForm.tsx에서 훅 사용 - -### Phase 2: 카드 컴포넌트 분리 -- [ ] `cards/OrderInfoCard.tsx` 생성 -- [ ] `cards/ContractInfoCard.tsx` 생성 -- [ ] `cards/OrderScheduleCard.tsx` 생성 -- [ ] `cards/OrderMemoCard.tsx` 생성 -- [ ] OrderDetailForm.tsx에서 import 및 사용 - -### Phase 3: 테이블 컴포넌트 분리 -- [ ] `tables/OrderDetailItemTable.tsx` 생성 -- [ ] `tables/OrderDetailItemRow.tsx` 생성 (선택적) -- [ ] OrderDetailForm.tsx에서 import 및 사용 - -### Phase 4: 다이얼로그 분리 -- [ ] `dialogs/OrderDialogs.tsx` 생성 -- [ ] OrderDetailForm.tsx에서 import 및 사용 - -### Phase 5: 최종 검증 -- [ ] TypeScript 타입 오류 없음 -- [ ] ESLint 경고 없음 -- [ ] 빌드 성공 -- [ ] 기능 테스트 (view/edit 모드) -- [ ] 불필요한 import 제거 - ---- - -## 우선순위 권장 - -1. **Phase 1 (Hook)** + **Phase 3 (Table)** 먼저 진행 - - 가장 큰 효과 (전체 코드의 ~60% 분리) - - 테이블이 455줄로 가장 큼 - -2. Phase 2 (Cards) 진행 - - 추가 ~360줄 분리 - -3. Phase 4 (Dialogs) 진행 - - 마무리 정리 - ---- - -## 주의사항 - -- **타입 export**: 새 컴포넌트에서 사용할 타입들 types.ts에서 export 확인 -- **props drilling**: 너무 깊어지면 Context 고려 -- **테스트**: 분리 후 view/edit 모드 모두 테스트 필수 -- **점진적 진행**: 한 번에 모든 분리보다 단계별 진행 권장 diff --git a/claudedocs/construction/[PLAN-2026-01-05] order-management-implementation.md b/claudedocs/construction/[PLAN-2026-01-05] order-management-implementation.md deleted file mode 100644 index a2f3ae03..00000000 --- a/claudedocs/construction/[PLAN-2026-01-05] order-management-implementation.md +++ /dev/null @@ -1,323 +0,0 @@ -# 발주관리 페이지 구현 계획서 - -> **작성일**: 2026-01-05 -> **작업 경로**: `/juil/order/order-management` -> **상태**: ✅ 구현 완료 - ---- - -## 📋 스크린샷 분석 결과 - -### 화면 구성 - -#### 1. 상단 - 발주 스케줄 (달력 영역) -| 요소 | 설명 | -|------|------| -| **뷰 전환** | 주(Week) / 월(Month) 탭 전환 | -| **년월 네비게이션** | 2025년 12월 ◀ ▶ 버튼 | -| **필터** | 작업반장별 필터 (이번년+8주 화살표 버튼) | -| **일정 바(Bar)** | "담당자 - 현장명 / 발주번호" 형태로 여러 날에 걸쳐 표시 | -| **일정 색상** | 회색(완료), 파란색(진행중) 구분 | -| **일자 뱃지** | 빨간 원 안에 숫자 (06, 07, 08 등) - 상태/건수 표시 | -| **더보기** | +15 형태로 해당 일자에 추가 일정 있음 표시 | -| **달력 클릭** | 특정 일자 클릭 시 아래 리스트에 해당 일자 데이터만 필터링 | - -#### 2. 하단 - 발주 목록 (리스트 영역) -| 요소 | 설명 | -|------|------| -| **날짜 범위** | 2025-09-01 ~ 2025-09-03 형태 | -| **빠른 필터 탭** | 당해년도 / 전년도 / 전월 / 당월 / 어제 / 오늘 | -| **검색** | 검색창 + 건수 표시 (7건, 12건 선택) | -| **상태 필터** | 빨간 원 숫자 버튼들 (전체/상태별) | -| **삭제 버튼** | 선택된 항목 삭제 | - -#### 3. 테이블 컬럼 -| 컬럼 | 설명 | -|------|------| -| 체크박스 | 선택 | -| 계약일련번호 | - | -| 거래처 | 회사명 | -| 현장명 | 작업 현장 | -| 병동 | - | -| 공 | - | -| 시APM | 담당 PM | -| 발주번호 | 발주 식별 번호 | -| 발주번 담자 | 발주 담당자 | -| 발주처 | - | -| 작업반 시공품 | 작업 내용 | -| 기간 | 작업 기간 | -| 구분 | 상태 구분 | -| 실적 납품일 | 실제 납품 완료일 | -| 납품일 | 예정 납품일 | - -#### 4. 작업 버튼 (선택 시) -- 수정 버튼 -- 삭제 버튼 - ---- - -## 🏗️ 구현 범위 - -### Phase 1: 공통 달력 컴포넌트 (ScheduleCalendar) -**재사용 가능한 스케줄 달력 컴포넌트** - -``` -src/components/common/ -└── ScheduleCalendar/ - ├── index.tsx # 메인 컴포넌트 - ├── ScheduleCalendar.tsx # 달력 본체 - ├── CalendarHeader.tsx # 헤더 (년월/뷰전환/필터) - ├── MonthView.tsx # 월간 뷰 - ├── WeekView.tsx # 주간 뷰 - ├── ScheduleBar.tsx # 일정 바 컴포넌트 - ├── DayCell.tsx # 일자 셀 컴포넌트 - ├── MorePopover.tsx # +N 더보기 팝오버 - ├── types.ts # 타입 정의 - └── utils.ts # 유틸리티 함수 -``` - -**기능 요구사항**: -- [ ] 월간/주간 뷰 전환 -- [ ] 년월 네비게이션 (이전/다음) -- [ ] 일정 바(Bar) 렌더링 (여러 날에 걸침) -- [ ] 일정 색상 구분 (상태별) -- [ ] 일자별 뱃지 숫자 표시 -- [ ] +N 더보기 기능 (3개 초과 시) -- [ ] 일자 클릭 이벤트 콜백 -- [ ] 필터 영역 slot (외부에서 주입) -- [ ] 반응형 디자인 - -### Phase 2: 발주관리 리스트 페이지 -**페이지 및 컴포넌트 구조** - -``` -src/app/[locale]/(protected)/juil/order/ -└── order-management/ - └── page.tsx # 페이지 엔트리 - -src/components/business/juil/order-management/ -├── OrderManagementListClient.tsx # 메인 클라이언트 컴포넌트 -├── OrderCalendarSection.tsx # 달력 섹션 (ScheduleCalendar 사용) -├── OrderListSection.tsx # 리스트 섹션 -├── OrderStatusFilter.tsx # 상태 필터 (빨간 원 숫자) -├── OrderDateFilter.tsx # 날짜 빠른 필터 (당해년도/전월 등) -├── types.ts # 타입 정의 -├── actions.ts # Server Actions -└── index.ts # 배럴 export -``` - -**기능 요구사항**: -- [ ] 달력과 리스트 통합 레이아웃 -- [ ] 달력 일자 클릭 → 리스트 필터 연동 -- [ ] 날짜 범위 선택 -- [ ] 빠른 날짜 필터 (당해년도/전년도/전월/당월/어제/오늘) -- [ ] 상태별 필터 (빨간 원 숫자 버튼) -- [ ] 검색 기능 -- [ ] 테이블 (체크박스/정렬/페이지네이션) -- [ ] 선택 시 작업 버튼 표시 -- [ ] 삭제 기능 - ---- - -## 📦 기술 의존성 - -### 새로 설치 필요 -```bash -# FullCalendar 라이브러리 (또는 커스텀 구현) -npm install @fullcalendar/core @fullcalendar/react @fullcalendar/daygrid @fullcalendar/timegrid @fullcalendar/interaction -``` - -**대안**: FullCalendar 없이 커스텀 달력 컴포넌트로 구현 -- 장점: 번들 사이즈 감소, 완전한 커스터마이징 -- 단점: 구현 복잡도 증가 - -### 기존 사용 -- `IntegratedListTemplateV2` - 리스트 템플릿 -- `DateRangeSelector` - 날짜 범위 선택 -- `date-fns` - 날짜 유틸리티 - ---- - -## 🔧 세부 구현 체크리스트 - -### Phase 1: 공통 달력 컴포넌트 (ScheduleCalendar) - -#### 1.1 기본 구조 및 타입 정의 -- [ ] `types.ts` 생성 (ScheduleEvent, CalendarView, CalendarProps 등) -- [ ] `utils.ts` 생성 (날짜 계산, 일정 위치 계산 등) -- [ ] 컴포넌트 폴더 구조 생성 - -#### 1.2 CalendarHeader 컴포넌트 -- [ ] 년월 표시 및 네비게이션 (◀ ▶) -- [ ] 주/월 뷰 전환 탭 -- [ ] 필터 slot (children으로 외부 주입) - -#### 1.3 MonthView 컴포넌트 -- [ ] 월간 그리드 레이아웃 (7x6) -- [ ] 요일 헤더 (일~토) -- [ ] 날짜 셀 렌더링 -- [ ] 이전/다음 달 날짜 표시 (opacity 처리) -- [ ] 오늘 날짜 하이라이트 - -#### 1.4 WeekView 컴포넌트 -- [ ] 주간 그리드 레이아웃 (7 컬럼) -- [ ] 요일 헤더 (날짜 + 요일) -- [ ] 날짜 셀 렌더링 - -#### 1.5 DayCell 컴포넌트 -- [ ] 날짜 숫자 표시 -- [ ] 뱃지 숫자 표시 (빨간 원) -- [ ] 클릭 이벤트 처리 -- [ ] 선택 상태 스타일 - -#### 1.6 ScheduleBar 컴포넌트 -- [ ] 일정 바 렌더링 (시작~종료 날짜) -- [ ] 여러 날에 걸치는 바 계산 (주 단위 분할) -- [ ] 색상 구분 (상태별) -- [ ] 호버/클릭 이벤트 -- [ ] 텍스트 truncate 처리 - -#### 1.7 MorePopover 컴포넌트 -- [ ] +N 버튼 렌더링 -- [ ] 팝오버로 숨겨진 일정 목록 표시 -- [ ] 일정 항목 클릭 이벤트 - -#### 1.8 메인 ScheduleCalendar 컴포넌트 -- [ ] 상태 관리 (현재 월, 뷰 모드, 선택된 날짜) -- [ ] 일정 데이터 받아서 렌더링 -- [ ] 이벤트 콜백 (onDateClick, onEventClick, onMonthChange) -- [ ] 반응형 처리 - -### Phase 2: 발주관리 리스트 페이지 - -#### 2.1 타입 및 설정 -- [ ] `types.ts` - Order 타입, 필터 옵션, 상태 정의 -- [ ] `actions.ts` - Server Actions (목업 데이터) - -#### 2.2 page.tsx -- [ ] 페이지 라우트 생성 -- [ ] 메타데이터 설정 -- [ ] 클라이언트 컴포넌트 import - -#### 2.3 OrderDateFilter 컴포넌트 -- [ ] 빠른 날짜 필터 버튼 (당해년도/전년도/전월/당월/어제/오늘) -- [ ] 클릭 시 날짜 범위 계산 -- [ ] 활성화 상태 스타일 - -#### 2.4 OrderStatusFilter 컴포넌트 -- [ ] 상태별 필터 버튼 (빨간 원 숫자) -- [ ] 전체/상태별 카운트 표시 -- [ ] 선택 상태 스타일 - -#### 2.5 OrderCalendarSection 컴포넌트 -- [ ] ScheduleCalendar 사용 -- [ ] 필터 영역 (작업반장 셀렉트) -- [ ] 일자 클릭 이벤트 → 리스트 필터 연동 -- [ ] 스케줄 데이터 매핑 - -#### 2.6 OrderListSection 컴포넌트 -- [ ] IntegratedListTemplateV2 기반 -- [ ] 테이블 컬럼 정의 -- [ ] 행 렌더링 (체크박스, 데이터, 작업 버튼) -- [ ] 선택 시 작업 버튼 표시 -- [ ] 모바일 카드 렌더링 - -#### 2.7 OrderManagementListClient 컴포넌트 -- [ ] 전체 상태 관리 (달력 + 리스트 연동) -- [ ] 달력 일자 선택 → 리스트 필터 -- [ ] 날짜 범위 필터 -- [ ] 상태 필터 -- [ ] 검색 필터 -- [ ] 정렬 -- [ ] 페이지네이션 -- [ ] 삭제 기능 - -### Phase 3: 통합 테스트 및 마무리 -- [ ] 달력-리스트 연동 테스트 -- [ ] 반응형 테스트 -- [ ] 목업 데이터 검증 -- [ ] 테스트 URL 등록 - ---- - -## 🎨 디자인 명세 - -### 달력 색상 -| 상태 | 바 색상 | 뱃지 색상 | -|------|---------|-----------| -| 완료 | 회색 (`bg-gray-400`) | - | -| 진행중 | 파란색 (`bg-blue-500`) | 빨간색 (`bg-red-500`) | -| 대기 | 노란색 (`bg-yellow-500`) | 빨간색 (`bg-red-500`) | - -### 레이아웃 -``` -+--------------------------------------------------+ -| 📅 발주관리 [발주 등록] | -+--------------------------------------------------+ -| [발주 스케줄] | -| +----------------------------------------------+ | -| | 2025년 12월 [주] [월] [작업반장 ▼] | | -| | ◀ ▶ | | -| |----------------------------------------------| -| | 일 | 월 | 화 | 수 | 목 | 금 | 토 | | -| |----------------------------------------------| -| | | | 1 | 2 | 3 | 4 | 5 | | -| | 📊 | | ━━━━━━━━━━━━━━━━━━━ 일정바 ━━━━━━ | | -| |----------------------------------------------| -| | 6 | 7 | 8 | 9 | 10 | 11 | 12 | | -| | ⓪ | ⓪ | | | | | | | -| +----------------------------------------------+ | -+--------------------------------------------------+ -| [발주 목록] | -| +----------------------------------------------+ | -| | 2025-09-01 ~ 2025-09-03 | | -| | [당해년도][전년도][전월][당월][어제][오늘] | | -| |----------------------------------------------| -| | 🔍 검색... 7건 | ⓿ ❶ ❷ ❸ | [삭제] | | -| |----------------------------------------------| -| | ☐ | 번호 | 거래처 | 현장명 | ... | 작업 | | -| | ☐ | 1 | A사 | 현장1 | ... | [버튼들] | | -| +----------------------------------------------+ | -+--------------------------------------------------+ -``` - ---- - -## 📝 참고사항 - -### 달력 라이브러리 선택 -**추천: 커스텀 구현** -- FullCalendar는 기능이 과도하고 번들 사이즈가 큼 -- 스크린샷의 요구사항은 커스텀으로 충분히 구현 가능 -- `date-fns` 활용하여 날짜 계산 - -### 기존 패턴 준수 -- `IntegratedListTemplateV2` 사용 -- `DateRangeSelector` 재사용 -- `StructureReviewListClient` 패턴 참조 - -### 향후 확장 -- 다른 페이지에서 ScheduleCalendar 재사용 -- 일정 등록/수정 모달 추가 예정 -- 드래그 앤 드롭 일정 이동 (선택적) - ---- - -## ✅ 작업 순서 - -1. **Phase 1.1-1.2**: 타입 정의 및 CalendarHeader -2. **Phase 1.3-1.4**: MonthView / WeekView -3. **Phase 1.5-1.6**: DayCell / ScheduleBar -4. **Phase 1.7-1.8**: MorePopover / 메인 컴포넌트 -5. **Phase 2.1-2.2**: 발주관리 타입 및 페이지 -6. **Phase 2.3-2.4**: 날짜/상태 필터 -7. **Phase 2.5-2.6**: 달력/리스트 섹션 -8. **Phase 2.7**: 메인 클라이언트 컴포넌트 -9. **Phase 3**: 통합 테스트 - ---- - -## 🔗 관련 문서 -- `[REF] juil-project-structure.md` - 주일 프로젝트 구조 -- `StructureReviewListClient.tsx` - 리스트 패턴 참조 -- `IntegratedListTemplateV2.tsx` - 템플릿 참조 \ No newline at end of file diff --git a/claudedocs/construction/[REF] construction-project-flow.md b/claudedocs/construction/[REF] construction-project-flow.md deleted file mode 100644 index 42f8a608..00000000 --- a/claudedocs/construction/[REF] construction-project-flow.md +++ /dev/null @@ -1,82 +0,0 @@ -# Juil Project Process Flow Analysis -Based on provided flowcharts. - -## 1. Project Progress Flow (Main Lifecycle) - -### Modules & Roles -| Role | Key Activities | Output/State | -|---|---|---| -| **Field Briefing User** | Attend briefing, Upload data | Project Initiated | -| **Estimate/Bid Manager** | Create Estimate (Approve/Return)
Bid Participation
Win/Loss Check | Estimate Created
Bid Submitted
Project Won/Lost | -| **Contract Manager** | Create Contract (Approve/Return)
Contract Execution
Handover Decision | Contract Finalized | -| **Order/Construction Manager** | Handover Creation (Approve/Return)
Field Measurement
Structural Review (if needed)
Order Creation (Approve/Return)
Construction Start | Handover Doc
Measurement Data
Structural Report
Order Placed | -| **Progress Billing Manager** | Create Progress Billing (Approve/Return)
Change Contract Check
Client Approval
Settlement | Bill Created
Settlement Complete | - ---- - -## 2. Construction & Billing Detail Flow - -### Detailed Steps by Role - -#### Order Manager -1. **Handover**: Create handover document -> Approval Loop. -2. **Field Work**: Field Measurement. -3. **Engineering**: Structural Review (Condition: if needed). -4. **Ordering**: Create Order -> Approval Loop. - -#### Construction Manager -1. **Execution**: Start Construction. -2. **Resources**: Request Vehicles/Equipment. -3. **Management**: Construction Management -> Issue Check. -4. **Issue Handling**: Manage Issues if they arise. - -#### Work Foreman (Field) -1. **Assignment**: Receive Construction Assignment. -2. **Personnel**: Check New Personnel -> Sign up if needed. -3. **Attendance**: GPS Attendance Check. -4. **Daily Work**: - - Perform Construction Work. - - Photo Documentation. - - Work Report. - - Personnel Status Report. - -#### Progress Billing Manager -1. **Billing**: Create Progress Billing -> Approval Loop. -2. **Change Mgmt**: Check if Change Contract is needed. - - If needed: Trigger Contract Manager flow. -3. **Client**: Get Construction Company (Client) Approval. -4. **Finish**: Settlement. - -#### Contract Manager (Change Process) -1. **Drafting**: Create Change Contract (triggered by Billing). -2. **Approval**: Internal Approval Loop. -3. **Execution**: Change Contract Process. -4. **Client**: Get Construction Company (Client) Approval. -5. **Finish**: Change Contract Complete. - ---- - -## 3. Proposed Menu Structure (Juil) - -Based on the flow, the recommended menu structure is: - -- **Dashboard**: Overall Status -- **Project Management** (프로젝트 관리) - - Field Briefing (현장설명회) - - Estimates & Bids (견적/입찰) - - Contracts (계약관리) -- **Construction Management** (공사관리) - - Handovers (인수인계) - - Field Measurements (현장실측) - - Structural Reviews (구조검토) - - Orders (발주관리) - - Construction Execution (시공관리) - Includes Vehicles, Issues -- **Field Work** (현장작업) - Mobile Optimized? - - My Assignments (시공할당) - - Personnel Mgmt (인력관리) - - Attendance (GPS출근) - - Daily Reports (업무보고/사진) -- **Billing & Settlement** (기성/정산) - - Progress Billing (기성청구) - - Change Contracts (변경계약) - - Settlements (정산관리) diff --git a/claudedocs/construction/[REF] juil-project-structure.md b/claudedocs/construction/[REF] juil-project-structure.md deleted file mode 100644 index 771bf246..00000000 --- a/claudedocs/construction/[REF] juil-project-structure.md +++ /dev/null @@ -1,89 +0,0 @@ -# 주일 공사 MES 프로젝트 구조 - -Last Updated: 2025-12-30 - -## 프로젝트 개요 - -| 항목 | 내용 | -|------|------| -| 업체명 | 주일 | -| 업종 | 공사 (건설/시공) | -| 프로젝트 유형 | MES (Manufacturing Execution System) | -| 기존 프로젝트 | 경동 (셔터 업체) | - -## 디렉토리 구조 - -``` -src/app/[locale]/(protected)/ -├── juil/ # 주일 전용 페이지들 -│ ├── page.tsx # 메인 페이지 (예정) -│ ├── [기능명]/ # 각 기능별 페이지 -│ └── ... -│ -├── dev/ -│ └── juil-test-urls/ # 테스트 URL 관리 페이지 -│ ├── page.tsx # 서버 컴포넌트 (MD 파싱) -│ └── JuilTestUrlsClient.tsx # 클라이언트 컴포넌트 -│ -└── (기존 경동 페이지들) -``` - -## 컴포넌트 구조 (예정) - -``` -src/components/business/juil/ # 주일 전용 비즈니스 컴포넌트 -├── common/ # 공통 컴포넌트 -├── [기능명]/ # 기능별 컴포넌트 -└── ... -``` - -## 테스트 URL 페이지 - -| 항목 | 내용 | -|------|------| -| URL | http://localhost:3000/dev/juil-test-urls | -| MD 파일 | `claudedocs/[REF] juil-pages-test-urls.md` | -| 용도 | 개발 중인 주일 페이지 URL 관리 및 빠른 접근 | - -### MD 파일 형식 - -```markdown -## 카테고리명 - -| 페이지 | URL | 상태 | -|--------|-----|------| -| **페이지명** | `/ko/juil/...` | 상태표시 | -``` - -## 경동 vs 주일 비교 - -| 항목 | 경동 | 주일 | -|------|------|------| -| 업종 | 셔터 | 공사 | -| 경로 | `/ko/...` (기존 경로) | `/ko/juil/...` | -| 컴포넌트 | `src/components/...` | `src/components/business/juil/...` | -| 문서 | `claudedocs/...` | `claudedocs/juil/...` | - -## 개발 가이드 - -### 새 페이지 추가 시 - -1. `src/app/[locale]/(protected)/juil/[기능명]/` 폴더 생성 -2. `page.tsx` 생성 -3. 필요 시 `src/components/business/juil/[기능명]/` 컴포넌트 생성 -4. `claudedocs/[REF] juil-pages-test-urls.md`에 URL 추가 - -### 테스트 URL 등록 - -`claudedocs/[REF] juil-pages-test-urls.md` 파일에 마크다운 테이블 형식으로 추가: - -```markdown -| **새페이지** | `/ko/juil/new-page` | NEW | -``` - -## 관련 파일 목록 - -- `claudedocs/[REF] juil-pages-test-urls.md` - 테스트 URL 목록 -- `claudedocs/juil/` - 주일 프로젝트 문서 폴더 -- `src/app/[locale]/(protected)/juil/` - 페이지 파일 -- `src/components/business/juil/` - 컴포넌트 파일 \ No newline at end of file diff --git a/claudedocs/customer-center/[IMPL-2025-12-19] inquiry-management.md b/claudedocs/customer-center/[IMPL-2025-12-19] inquiry-management.md deleted file mode 100644 index be57588e..00000000 --- a/claudedocs/customer-center/[IMPL-2025-12-19] inquiry-management.md +++ /dev/null @@ -1,89 +0,0 @@ -# [IMPL-2025-12-19] 1:1 문의 관리 구현 - -## 개요 -- **페이지**: 1:1 문의 (고객센터) -- **URL**: `/ko/customer-center/inquiries` -- **참조**: 공지사항, 이벤트, 게시판 구조 - -## 체크리스트 - -### Phase 1: 기본 구조 -- [ ] types.ts 생성 (Inquiry 타입, 필터 옵션 등) -- [ ] Mock 데이터 생성 - -### Phase 2: 목록 페이지 -- [ ] InquiryList.tsx 생성 - - [ ] IntegratedListTemplateV2 사용 - - [ ] 날짜 범위 선택 (DateRangeSelector) - - [ ] 문의 등록 버튼 - - [ ] 검색창 - - [ ] 테이블 필터 3개 (상담분류, 상태, 정렬) - - [ ] 테이블 컬럼: No., 상담분류, 제목, 상태, 등록일 -- [ ] page.tsx (목록) - -### Phase 3: 상세 페이지 -- [ ] InquiryDetail.tsx 생성 - - [ ] 문의 영역 (제목, 작성자, 날짜, 내용, 첨부파일) - - [ ] 답변 영역 (작성자, 날짜, 내용, 첨부파일) - - [ ] 댓글 등록 입력창 - - [ ] 댓글 목록 (프로필, 이름, 내용, 날짜, 수정/삭제) - - [ ] 삭제/수정 버튼 -- [ ] [id]/page.tsx (상세) - -### Phase 4: 등록/수정 페이지 -- [ ] InquiryForm.tsx 생성 - - [ ] 상담분류 선택 - - [ ] 제목 입력 - - [ ] 내용 에디터 (게시판 에디터 사용) - - [ ] 파일 첨부 -- [ ] create/page.tsx (등록) -- [ ] [id]/edit/page.tsx (수정) - -### Phase 5: 마무리 -- [ ] index.tsx export -- [ ] 테스트 URL 문서 업데이트 - -## 스펙 상세 - -### 목록 페이지 -| 필드 | 타입 | 설명 | -|------|------|------| -| 상담분류 필터 | Select | 전체, 문의하기, 신고하기, 건의사항, 서비스오류 | -| 상태 필터 | Select | 전체, 답변대기, 답변완료 | -| 정렬 | Select | 최신순, 오래된순 | - -### 테이블 컬럼 -| 컬럼 | 설명 | -|------|------| -| No. | 번호 | -| 상담분류 | 문의하기, 신고하기, 건의사항, 서비스오류 | -| 제목 | 문의 제목 | -| 상태 | 답변대기, 답변완료 | -| 등록일 | YYYY-MM-DD | - -### 상세 페이지 구조 -1. **문의 영역** - - 제목 - - 작성자 | 등록일시 - - 내용 (에디터 콘텐츠) - - 첨부파일 - -2. **답변 영역** - - 작성자 | 답변일시 - - 내용 - - 첨부파일 - -3. **댓글 영역** - - 댓글 등록 입력창 + 등록 버튼 - - 댓글 목록 - - 프로필 이미지 - - 이름 - - 댓글 내용 - - 등록일시 - - 수정/삭제 버튼 - -## 테스트 URL -- 목록: `/ko/customer-center/inquiries` -- 상세: `/ko/customer-center/inquiries/[id]` -- 등록: `/ko/customer-center/inquiries/create` -- 수정: `/ko/customer-center/inquiries/[id]/edit` \ No newline at end of file diff --git a/claudedocs/dashboard/[FIX-2026-03-09] ceo-dashboard-fix-plan.md b/claudedocs/dashboard/[FIX-2026-03-09] ceo-dashboard-fix-plan.md deleted file mode 100644 index 22964407..00000000 --- a/claudedocs/dashboard/[FIX-2026-03-09] ceo-dashboard-fix-plan.md +++ /dev/null @@ -1,213 +0,0 @@ -# CEO 대시보드 수정계획서 (최종) - -**작성일**: 2026-03-09 -**기반 문서**: `[QA-2026-03-09] ceo-dashboard-ui-verification.md` -**검증 수준**: 화면(Chrome DevTools) + 프론트엔드 코드 + 백엔드 코드 + 실제 데이터 전부 확인 완료 - ---- - -## 최종 이슈 요약 - -| 분류 | 건수 | 내용 | -|------|------|------| -| 🟡 백엔드 개선 | 1건 | 현황판/채권추심 sub_label 필드 추가 | -| 🟢 프론트엔드 개선 | 2건 | 더미값 제거, 매입 라벨 명확화 | -| ✅ 수정 불필요 (오진 정정) | 8건 | 아래 상세 표 참조 | - ---- - -## 1. 수정 필요 항목 - -### B3. 현황판/채권추심 거래처 sub_label 필드 추가 🟡 - -**현상**: 프론트엔드에서 하드코딩된 더미 거래처명 사용 중 - -| 위치 | 더미값 | TODO 주석 | -|------|--------|----------| -| `transformers/status-issue.ts:20-29` | "주식회사 부산화학 외" 등 | ✅ 있음 (line 18) | -| `transformers/receivable.ts:96-103` | "(주)부산화학 외" 등 | ✅ 있음 (line 96) | - -**백엔드 수정 내용**: - -1. **`StatusBoardService.php`** — 각 항목 응답에 `sub_label` 필드 추가 - - `getBadDebtStatus()` (line 68-83): 최다 금액 거래처 실제 이름 조회 - - `getNewClientStatus()`: 최근 등록 업체명 조회 - - 기타 항목도 해당 시 sub_label 제공 - -2. **`BadDebtService.php`** — `summary()` 응답에 per-card 거래처 정보 추가 - - `top_client_name`: 누적 악성채권 최다 금액 거래처명 - - 카드별 `sub_label`: 해당 카테고리 최다 금액 거래처명 + 건수 - -**프론트엔드 후속 작업**: B3 완료 후 → F1 (더미값 제거) - ---- - -### F1. 더미 거래처명 제거 (B3 완료 후) 🟢 - -**대상 파일**: -- `src/lib/api/dashboard/transformers/status-issue.ts` - - Line 20-29: `STATUS_BOARD_FALLBACK_SUB_LABELS` 상수 제거 - - Line 35-53: `buildStatusSubLabel()` → API `sub_label` 직접 사용 - -- `src/lib/api/dashboard/transformers/receivable.ts` - - Line 98-103: `DEBT_COLLECTION_FALLBACK_SUB_LABELS` 상수 제거 - - Line 109-121: `buildDebtSubLabel()` → API 제공 값 사용 - ---- - -### F3. 매입 섹션 "당월" 컨텍스트 명확화 🟢 - -**현상**: -- 섹션 subtitle: "당월 매입 실적" + Badge: "당월" -- 카드 라벨: "누적 매입" (line 65 — 이것 자체는 정확) -- **실제 데이터**: `cumulative_purchase` = 연간 누적 (2026-01-01 ~ 오늘, Feb 포함) -- 일별 매입 내역: 2026-02-27 거래 표시 → "당월 매입 내역" 제목 하에 전월 데이터 포함 - -**코드 확인**: -- `PurchaseStatusSection.tsx:50` — `subtitle="당월 매입 실적"` -- `PurchaseStatusSection.tsx:53` — `당월` -- `PurchaseStatusSection.tsx:65` — `누적 매입` -- `DashboardCeoService.php:175-180` — `whereYear('purchase_date', $year)` = 연간 누적 - -**수정 방향**: -- subtitle: "당월 매입 실적" → "매입 실적" 또는 "연간 매입 현황" -- Badge: "당월" → 제거 또는 "YTD"로 변경 -- 하단 카드 title: "당월 매입 내역" → "최근 매입 내역" - ---- - -## 2. 수정 불필요 항목 (최종 정리) - -### 1차 QA → 2차 검증 → 최종 검토로 순차 정정된 이슈들 - -| # | 이전 보고 | 최종 검증 결과 | 검증 근거 | -|---|----------|-------------|----------| -| ~~C1~~ | 5개 섹션 본문 미렌더링 | **LazySection 정상** — 스크롤 시 로드 | `LazySection.tsx` IntersectionObserver + DOM 확인 | -| ~~C2~~ | 매출 금액 10배 차이 | **NavBar=누적, 본문=당월 구분 표시** | `SalesStatusSection.tsx:63` "누적 매출" 라벨 확인 | -| ~~C3~~ | 발행어음 데이터 불일치 | **다른 테이블 (설계 의도)** | `expected_expenses` 테이블(예측) ≠ `bills` 테이블(실제) | -| ~~C4~~ | 채권추심 건수 불일치 | **다른 산출 기준 (설계 의도)** | StatusBoard=레코드 7건(status=collecting) vs BadDebt=거래처 5곳(distinct client_id) | -| ~~I2~~ | 카드 월별 합계 0원 버그 | **정상 — 해당 월 데이터 없음** | 카드 거래 20건 모두 2025-01~2026-01-28, 2/3월 거래 0건 | -| ~~I3~~ | 미수금 산출 기준 차이 | **다른 산출 방식 (설계 의도)** | 화면 확인 | -| ~~M1~~ | 가지급금 카드 금액 차이 | **다른 기준 (설계 의도)** | 가지급금 전환 기준 vs 카드 사용 총액 | -| ~~발주~~ | 현황판 "발주" 미표시 | **의도적 숨김** | `STATUS_BOARD_HIDDEN_ITEMS.has('purchases')` (2026-03-03 비활성화) | - -### 상세 정정 사항 - -#### ~~B1: 카드 월별 합계 0원~~ — SQL 버그 아님 ✅ - -**이전 판단**: `DB::raw('DATE(COALESCE(used_at, withdrawal_date))')` SQL 버그 - -**최종 판단**: **데이터가 없어서 0이 정상** - -``` -카드 거래 20건 날짜 분포: -- 가장 오래된 거래: 2025-01-12 (used_at=NULL, withdrawal_date만) -- 가장 최근 거래: 2026-01-28 (used_at='2026-01-28') -- 2026-02 거래: 0건 -- 2026-03 거래: 0건 -→ current_month_total=0, previous_month_total=0 모두 정확 -``` - -**QA 오진 원인**: 카드사용내역 리스트 페이지에서 "15건 표시"를 보고 당월/전월에도 데이터가 있을 것으로 추정했으나, 리스트는 전체 기간 데이터를 표시. - -**참고**: `summary()` 메서드의 SQL 패턴(`DB::raw COALESCE`)이 `index()` 메서드(`whereDate + orWhere`)와 다르지만, 현재 데이터에서는 정상 작동 확인. 향후 코드 일관성을 위해 패턴 통일은 권장하나, 긴급하지 않음. - -#### ~~B2: 현황판 vs 채권추심 건수~~ — 설계 의도 ✅ - -**이전 판단**: 건수 통일 필요 - -**최종 판단**: **의도적으로 다른 관점 제공** - -| API | 쿼리 | 의미 | -|-----|------|------| -| `StatusBoardService.php:72` | `where('status', 'collecting')->count()` | "지금 추심중인 건" = 7건 | -| `BadDebtService.php:107-111` | `distinct('client_id')->count('client_id')` | "악성채권이 있는 거래처 수" = 5곳 | - -현황판은 "현재 추심 진행 중인 건수"를, 채권추심 본문은 "악성채권 보유 거래처 수"를 보여주는 것으로, 각각 다른 관점의 지표임. 사용자가 혼동할 수 있으나, 정보 제공 목적이 다름. - -#### ~~B5: 매입 "당월" 기간~~ — 백엔드 정상, 프론트 라벨 이슈 ✅ - -`DashboardCeoService.php:175-180`의 `cumulative_purchase`는 `whereYear(2026)` = 연간 누적으로 정확히 산출. "당월" 라벨은 프론트엔드 이슈 → F3으로 처리. - ---- - -## 3. 수정 우선순위 - -| 순위 | 이슈 | 영역 | 난이도 | 비고 | -|------|------|------|--------|------| -| 1 | B3: sub_label 필드 추가 | 백엔드 | 중 | 거래처 조회 쿼리 추가 | -| 2 | F1: 더미 거래처명 제거 | 프론트 | 하 | B3 완료 후 상수/함수 제거 | -| 3 | F3: 매입 섹션 라벨 명확화 | 프론트 | 하 | 텍스트만 변경 | - ---- - -## 4. 수정 후 재검수 계획 - -| 단계 | 항목 | 검증 방법 | -|------|------|----------| -| 1 | B3+F1 수정 후: 거래처명 | 현황판/채권추심에 실제 거래처명 표시 확인 | -| 2 | F3 수정 후: 매입 라벨 | "당월" 대신 적절한 기간 표현 확인 | -| 3 | 전체: 상세 모달 | 각 섹션 모달 열기/닫기/날짜필터 동작 확인 | - ---- - -## 부록: 관련 파일 위치 - -### 백엔드 (sam-api) -| 파일 | 이슈 | 상태 | -|------|------|------| -| `app/Services/StatusBoardService.php:68-83` | B3 (sub_label 추가) | 수정 필요 | -| `app/Services/BadDebtService.php:107-111` | B3 (per-card 거래처) | 수정 필요 | -| `app/Services/CardTransactionService.php:109-153` | ~~B1~~ | ✅ 정상 (데이터 없음이 원인) | -| `app/Services/ExpectedExpenseService.php:241-301` | ~~B4~~ | ✅ 정상 (다른 테이블, 설계 의도) | -| `app/Services/DashboardCeoService.php:166-234` | ~~B5~~ | ✅ 정상 (프론트 라벨만 수정) | - -### 프론트엔드 (sam-react-prod) -| 파일 | 이슈 | 상태 | -|------|------|------| -| `src/lib/api/dashboard/transformers/status-issue.ts:18-53` | F1 (더미 sub_label) | 수정 필요 (B3 후) | -| `src/lib/api/dashboard/transformers/receivable.ts:96-121` | F1 (더미 sub_label) | 수정 필요 (B3 후) | -| `src/components/business/CEODashboard/sections/PurchaseStatusSection.tsx:50-53,161` | F3 (라벨) | 수정 필요 | -| `src/components/business/CEODashboard/LazySection.tsx` | ~~C1~~ | ✅ 정상 | -| `src/components/business/CEODashboard/sections/SalesStatusSection.tsx:63` | ~~F2~~ | ✅ 정상 ("누적 매출" 명확) | - ---- - -## 5. 하단 섹션 추가 검증 결과 (3차) - -### 생산/출고/시공/근태 4개 섹션 소스 페이지 대조 + 코드 검증 - -| 섹션 | 대시보드 | 소스 페이지 | API 엔드포인트 | 결론 | -|------|---------|-----------|-------------|------| -| 생산 현황 | 0공정 | 작업지시 39건 (모두 2월/작업대기) | `dashboard/production/summary` | ✅ 정확 (오늘 예정 없음) | -| 출고 현황 | 0건/0원 | 당일출고 0건, 전체 8건 (12~1월) | (생산과 동일 API) | ✅ 정확 (당월 건 없음) | -| 시공 현황 | 0건 | 시공진행 7/완료 4 (**Mock 데이터**) | `dashboard/construction/summary` | ✅ 비교불가 (소스=Mock) | -| 근태 현황 | 0명 전부 | 미출근 55명/출근 0명 | `dashboard/attendance/summary` | ✅ 설계 차이 (기록 vs 명부) | - -### 참고 사항 (향후 개선 검토) - -1. **시공 현황 — NULL end_date 처리** (`DashboardCeoService.php:555-567`) - - 현재 쿼리: `contract_end_date >= $monthEnd` 조건에서 NULL 제외됨 - - 실제 계약 데이터 투입 시, 진행 중(`end_date IS NULL`) 계약이 대시보드에 미표시될 수 있음 - - 권장: `orWhere(fn($q) => $q->where('start_date', '<=', $monthEnd)->whereNull('end_date'))` 조건 추가 검토 - -2. **시공관리 페이지 — API 미연동** (`construction/management/actions.ts`) - - `TODO: 실제 API 연동 시 구현` 주석 → 현재 전체가 Mock 데이터 - - 대시보드 시공 섹션은 실제 `contracts` 테이블 조회 → 소스 페이지와의 정합성 검증은 API 연동 완료 후 재실시 - -3. **근태 대시보드 — "미출근" 미표시** - - 대시보드는 `attendances` 테이블 레코드 기반 → 출근 기록 없으면 모두 0 - - 근태관리 페이지는 사원 명부 기반 → "미출근 55명" 표시 - - CEO 관점에서 "미출근" 정보가 필요한지는 비즈니스 결정 사항 - ---- - -## 검증 이력 - -| 단계 | 내용 | 결과 | -|------|------|------| -| 1차 QA | 화면 검수 (18개 섹션) | Critical 4건 + Important 3건 보고 | -| 2차 검증 | LazySection + API 응답 확인 | Critical 4건 → 전부 정정 (버그 아님) | -| 3차 검토 | 백엔드 코드 + 실제 DB 데이터 확인 | Important 3건 중 1건(카드 0원) 추가 정정 | -| 4차 추가 검증 | 하단 4개 섹션 소스 대조 + 코드 검증 | 4건 모두 정상 (참고 3건 기록) | -| **최종 결론** | **실제 수정 필요: 3건** (백엔드 1 + 프론트 2) | 나머지 모두 수정 불필요 확인 | diff --git a/claudedocs/dashboard/[IMPL-2025-11-10] dashboard-integration-complete.md b/claudedocs/dashboard/[IMPL-2025-11-10] dashboard-integration-complete.md deleted file mode 100644 index 9b551bf8..00000000 --- a/claudedocs/dashboard/[IMPL-2025-11-10] dashboard-integration-complete.md +++ /dev/null @@ -1,212 +0,0 @@ -# 대시보드 통합 완료 보고서 - -## 작업 완료 시간 -2025-11-10 17:55 - -## 완료된 작업 - -### 1. 페이지 교체 -✅ 기존 `dashboard/page.tsx` 백업 완료 (`page.tsx.backup`) -✅ 새로운 역할 기반 대시보드 페이지로 교체 -✅ Dashboard Layout 생성 및 연결 - -### 2. 파일 구조 -``` -src/app/[locale]/(protected)/dashboard/ -├── layout.tsx # DashboardLayout을 적용하는 레이아웃 -├── page.tsx # 새로운 역할 기반 대시보드 (마이그레이션 완료) -└── page.tsx.backup # 기존 페이지 백업 -``` - -### 3. 로그인/로그아웃 통합 - -#### 로그인 시 (`LoginPage.tsx`) -```typescript -// 사용자 정보를 localStorage에 저장 -const userData = { - role: data.user?.role || 'CEO', - name: data.user?.user_name || userId, - position: data.user?.position || '사용자', - userId: userId, -}; -localStorage.setItem('user', JSON.stringify(userData)); -``` - -#### 로그아웃 시 (`DashboardLayout.tsx`) -```typescript -const handleLogout = async () => { - // 1. API 호출로 HttpOnly 쿠키 삭제 - await fetch('/api/auth/logout', { method: 'POST' }); - - // 2. localStorage 정리 - localStorage.removeItem('user'); - - // 3. 로그인 페이지로 리다이렉트 - router.push('/login'); -}; -``` - -### 4. UI 컴포넌트 추가 - -추가로 복사된 UI 컴포넌트: -- ✅ `checkbox.tsx` -- ✅ `card.tsx` -- ✅ `badge.tsx` -- ✅ `progress.tsx` -- ✅ `utils.ts` (공통 유틸리티) -- ✅ `dialog.tsx` -- ✅ `dropdown-menu.tsx` -- ✅ `popover.tsx` -- ✅ `switch.tsx` -- ✅ `textarea.tsx` -- ✅ `table.tsx` -- ✅ `tabs.tsx` -- ✅ `separator.tsx` - -### 5. 의존성 설치 - -추가 설치된 패키지: -```json -{ - "@radix-ui/react-progress": "^latest", - "@radix-ui/react-checkbox": "^latest" -} -``` - -## 동작 방식 - -### 로그인 플로우 -1. 사용자가 로그인 폼 제출 -2. `/api/auth/login` API 호출 -3. 성공 시 사용자 정보를 localStorage에 저장 -4. `/dashboard`로 리다이렉트 - -### 대시보드 표시 -1. `DashboardLayout`이 localStorage에서 사용자 정보 읽기 -2. 사용자 역할에 따라 메뉴 생성 -3. `Dashboard` 컴포넌트가 역할에 맞는 대시보드 표시 -4. CEO → CEODashboard -5. ProductionManager → ProductionManagerDashboard -6. Worker → WorkerDashboard -7. SystemAdmin → SystemAdminDashboard -8. Sales → SalesLeadDashboard - -### 역할 전환 -1. 헤더의 드롭다운에서 역할 선택 -2. localStorage 업데이트 -3. `roleChanged` 이벤트 발생 -4. Dashboard 컴포넌트가 자동으로 리렌더링 -5. 새로운 역할에 맞는 대시보드 표시 - -### 로그아웃 플로우 -1. 유저 프로필 드롭다운에서 "로그아웃" 클릭 -2. `/api/auth/logout` API 호출 (HttpOnly 쿠키 삭제) -3. localStorage에서 사용자 정보 제거 -4. `/login`으로 리다이렉트 - -## 테스트 방법 - -### 1. 개발 서버 실행 -```bash -npm run dev -``` - -### 2. 로그인 테스트 -1. `http://localhost:3000/login` 접속 -2. 로그인 (기본 테스트 계정 사용) -3. 대시보드로 자동 이동 확인 - -### 3. 역할별 대시보드 테스트 -대시보드 헤더의 역할 선택 드롭다운에서: -- CEO (대표이사) -- ProductionManager (생산관리자) -- Worker (생산작업자) -- SystemAdmin (시스템관리자) -- Sales (영업사원) - -각 역할로 전환하여 다른 대시보드가 표시되는지 확인 - -### 4. 로그아웃 테스트 -1. 우측 상단 유저 프로필 클릭 -2. "로그아웃" 선택 -3. 로그인 페이지로 이동 확인 - -## 빌드 상태 - -✅ **컴파일 성공**: 모든 모듈이 정상적으로 컴파일됨 -⚠️ **ESLint 경고**: 일부 미사용 변수 경고 존재 (기능에는 영향 없음) - -빌드 결과: -``` -✓ Compiled successfully in 5.0s -``` - -## 알려진 이슈 - -### ESLint 경고 -- 미사용 import 및 변수 -- 일부 컴포넌트의 `any` 타입 사용 -- `alert`, `setTimeout` 등 브라우저 전역 객체 참조 - -**해결 방법**: 이후 코드 정리 작업에서 처리 예정 (기능 동작에는 문제 없음) - -## 다음 단계 - -### 즉시 가능 -1. ✅ 로그인 후 대시보드 확인 -2. ✅ 역할 전환 기능 테스트 -3. ✅ 로그아웃 기능 테스트 - -### 추가 작업 필요 -1. ESLint 경고 정리 -2. TypeScript 타입 개선 -3. 하위 라우트 생성 (판매관리, 생산관리 등) -4. API 통합 작업 -5. 실제 사용자 데이터 연동 - -## 파일 변경 사항 요약 - -### 생성된 파일 -- `src/app/[locale]/(protected)/dashboard/layout.tsx` -- `src/app/[locale]/(protected)/dashboard/page.tsx.backup` - -### 수정된 파일 -- `src/app/[locale]/(protected)/dashboard/page.tsx` (완전 교체) -- `src/components/auth/LoginPage.tsx` (localStorage 저장 로직 추가) -- `src/layouts/DashboardLayout.tsx` (로그아웃 기능 추가) - -### 추가된 컴포넌트 및 의존성 -- 40+ 비즈니스 컴포넌트 -- 13+ UI 컴포넌트 -- Zustand stores (메뉴, 테마 관리) -- Custom hooks (useUserRole, useCurrentTime) - -## 결론 - -✅ **마이그레이션 완료**: 모든 대시보드 컴포넌트가 성공적으로 Next.js 프로젝트로 통합됨 -✅ **빌드 성공**: 프로젝트가 정상적으로 컴파일됨 -✅ **로그인 통합**: 로그인/로그아웃 플로우가 새로운 대시보드와 연동됨 -✅ **역할 기반 시스템**: 5가지 역할별 대시보드가 동작함 - -이제 `npm run dev`로 개발 서버를 실행하고 로그인하면 새로운 역할 기반 대시보드를 확인할 수 있습니다! - ---- - -## 관련 파일 - -### 프론트엔드 -- `src/app/[locale]/(protected)/dashboard/layout.tsx` - 대시보드 레이아웃 -- `src/app/[locale]/(protected)/dashboard/page.tsx` - 역할 기반 대시보드 페이지 -- `src/layouts/DashboardLayout.tsx` - 대시보드 레이아웃 컴포넌트 -- `src/components/business/Dashboard.tsx` - 대시보드 라우터 -- `src/components/business/CEODashboard.tsx` - CEO 대시보드 -- `src/components/business/ProductionManagerDashboard.tsx` - 생산관리자 대시보드 -- `src/components/business/WorkerDashboard.tsx` - 작업자 대시보드 -- `src/components/business/SystemAdminDashboard.tsx` - 시스템관리자 대시보드 -- `src/components/business/SalesLeadDashboard.tsx` - 영업 대시보드 -- `src/components/auth/LoginPage.tsx` - 로그인 페이지 (localStorage 저장) -- `src/hooks/useUserRole.ts` - 역할 관리 훅 - -### 참조 문서 -- `claudedocs/dashboard/[REF] dashboard-migration-summary.md` - 대시보드 마이그레이션 요약 -- `claudedocs/auth/[IMPL-2025-11-07] authentication-implementation-guide.md` - 인증 구현 가이드 diff --git a/claudedocs/dashboard/[IMPL-2025-11-11] dashboard-cleanup-summary.md b/claudedocs/dashboard/[IMPL-2025-11-11] dashboard-cleanup-summary.md deleted file mode 100644 index 0f6ddc14..00000000 --- a/claudedocs/dashboard/[IMPL-2025-11-11] dashboard-cleanup-summary.md +++ /dev/null @@ -1,197 +0,0 @@ -# 대시보드 레이아웃 정리 완료 보고서 - -## 작업 일시 -2025-11-11 - -## 작업 개요 -DashboardLayout.tsx에서 테스트용 역할 선택 셀렉트 메뉴를 제거하고, 간단한 로그아웃 버튼으로 교체하여 UI를 정리했습니다. - -## 변경 사항 - -### 1. 제거된 기능 - -#### 역할 선택 셀렉트 메뉴 -```tsx -// ❌ 제거됨 - -``` - -#### 관련 코드 제거 -- `handleRoleChange()` 함수 (역할 전환 로직) -- `roleDashboards` 배열 (역할 정의) -- `setCurrentRole`, `setUserName`, `setUserPosition` state setter 함수 - -### 2. 추가된 기능 - -#### 간단한 로그아웃 버튼 -```tsx -// ✅ 추가됨 - -``` - -### 3. 유지된 기능 - -#### 유저 프로필 표시 -```tsx -
-
-
- -
-
-

{userName}

-

{userPosition}

-
-
-
-``` - -#### 로그아웃 기능 -```tsx -const handleLogout = async () => { - try { - // 1. HttpOnly 쿠키 삭제 API 호출 - const response = await fetch('/api/auth/logout', { - method: 'POST', - }); - - if (response.ok) { - console.log('✅ 로그아웃 완료: HttpOnly 쿠키 삭제됨'); - } - - // 2. localStorage 정리 - localStorage.removeItem('user'); - - // 3. 로그인 페이지로 리다이렉트 - router.push('/login'); - } catch (error) { - console.error('로그아웃 처리 중 오류:', error); - localStorage.removeItem('user'); - router.push('/login'); - } -}; -``` - -## 헤더 레이아웃 비교 - -### 변경 전 -``` -[메뉴] [검색바] ... [테마토글] [유저프로필(드롭다운)] [역할선택 셀렉트] -``` - -### 변경 후 -``` -[메뉴] [검색바] ... [테마토글] [유저프로필] [로그아웃 버튼] -``` - -## 영향 분석 - -### ✅ 긍정적 영향 -1. **UI 단순화**: 불필요한 역할 전환 기능 제거로 헤더가 깔끔해짐 -2. **사용자 혼란 방지**: 테스트용 기능이 프로덕션에 노출되지 않음 -3. **명확한 로그아웃**: 드롭다운 대신 버튼으로 로그아웃 기능 명확화 -4. **코드 정리**: 미사용 함수 및 변수 제거로 코드 가독성 향상 - -### 🔄 기능 변경 없음 -- 역할 기반 대시보드 표시 기능은 유지됨 (로그인 시 역할에 따라 자동 결정) -- 로그아웃 기능 동작 방식 유지 -- 메뉴 생성 로직 유지 - -## 파일 변경 내역 - -### 수정된 파일 -- `src/layouts/DashboardLayout.tsx` - - 역할 선택 셀렉트 메뉴 제거 (Line 407-420) - - `handleRoleChange` 함수 제거 (Line 232-277) - - `roleDashboards` 배열 제거 (Line 100-107) - - state setter 함수 제거 (setCurrentRole, setUserName, setUserPosition) - - 유저 프로필 드롭다운을 일반 div로 변경 - - 로그아웃 버튼 추가 - -### 백업된 파일 -- `src/app/[locale]/(protected)/dashboard/page.tsx.backup` (참고용) - -## 빌드 상태 - -✅ **컴파일 성공**: `✓ Compiled successfully in 3.2s` -⚠️ **ESLint 경고**: 비즈니스 컴포넌트의 미사용 변수 (기능에 영향 없음) - -## 테스트 방법 - -### 1. 로그인 플로우 -```bash -1. npm run dev -2. http://localhost:3000/login 접속 -3. 로그인 (API에서 반환된 역할에 따라 자동 대시보드 표시) -``` - -### 2. 로그아웃 테스트 -```bash -1. 대시보드 우측 상단 "로그아웃" 버튼 클릭 -2. 로그인 페이지로 리다이렉트 확인 -3. localStorage에서 user 정보 삭제 확인 (개발자 도구) -``` - -### 3. 역할 기반 대시보드 -- CEO로 로그인 → CEODashboard 표시 -- ProductionManager로 로그인 → ProductionManagerDashboard 표시 -- Worker로 로그인 → WorkerDashboard 표시 -- SystemAdmin로 로그인 → SystemAdminDashboard 표시 -- Sales로 로그인 → SalesLeadDashboard 표시 - -## 다음 단계 - -### 권장 작업 -1. ESLint 경고 정리 (비즈니스 컴포넌트의 미사용 변수) -2. 역할 관리 기능을 별도 설정 페이지로 이동 (관리자용) -3. 프로필 설정 페이지 추가 (사용자 정보 수정) -4. 로그아웃 버튼에 확인 다이얼로그 추가 (선택사항) - -### 추후 개선 사항 -1. 역할 전환 기능이 필요한 경우: - - 시스템 관리자 전용 설정 페이지에 추가 - - 개발/테스트 환경에서만 활성화 - - 권한 검증 로직 추가 - -2. 사용자 경험 개선: - - 로그아웃 시 확인 모달 추가 - - 프로필 드롭다운 메뉴 추가 (프로필 보기, 설정, 로그아웃) - - 알림 기능 추가 - -## 결론 - -✅ **정리 완료**: 테스트용 역할 선택 기능 제거 -✅ **기능 유지**: 역할 기반 대시보드 시스템 정상 동작 -✅ **빌드 성공**: 컴파일 및 동작 정상 -✅ **UI 개선**: 깔끔하고 명확한 헤더 레이아웃 - -대시보드 레이아웃이 프로덕션에 적합한 상태로 정리되었습니다! - ---- - -## 관련 파일 - -### 프론트엔드 -- `src/layouts/DashboardLayout.tsx` - 대시보드 레이아웃 (역할 선택 제거, 로그아웃 버튼 추가) -- `src/app/[locale]/(protected)/dashboard/page.tsx` - 대시보드 페이지 -- `src/app/[locale]/(protected)/dashboard/page.tsx.backup` - 기존 페이지 백업 - -### 참조 문서 -- `claudedocs/dashboard/[IMPL-2025-11-10] dashboard-integration-complete.md` - 대시보드 통합 완료 보고서 diff --git a/claudedocs/dashboard/[IMPL-2025-11-11] sidebar-active-menu-sync.md b/claudedocs/dashboard/[IMPL-2025-11-11] sidebar-active-menu-sync.md deleted file mode 100644 index 4bda06ff..00000000 --- a/claudedocs/dashboard/[IMPL-2025-11-11] sidebar-active-menu-sync.md +++ /dev/null @@ -1,596 +0,0 @@ -# 사이드바 메뉴 활성화 자동 동기화 구현 - -## 📋 개요 - -URL 직접 입력, 브라우저 뒤로가기/앞으로가기 시에도 사이드바 메뉴가 자동으로 활성화되도록 개선 - ---- - -## 🎯 해결한 문제 - -### 기존 문제점 - -**문제 상황:** -- 메뉴 클릭 시에만 `activeMenu` 상태가 업데이트됨 -- URL을 직접 입력하거나 브라우저 뒤로가기를 하면 메뉴 활성화 상태가 동기화되지 않음 -- 현재 페이지와 사이드바 메뉴 상태가 불일치 - -**예시:** -```typescript -// 문제 시나리오 -1. /dashboard/settings 메뉴 클릭 → settings 메뉴 활성화 ✅ -2. /dashboard 페이지로 뒤로가기 → settings 메뉴 여전히 활성화 ❌ -3. URL 직접 입력: /inventory → 메뉴 활성화 안됨 ❌ -``` - -### 원인 분석 - -```typescript -// ❌ 기존 코드: 클릭 이벤트에만 의존 -const handleMenuClick = (menuId: string, path: string) => { - setActiveMenu(menuId); // 클릭할 때만 업데이트 - router.push(path); -}; - -// ❌ 경로 변경 감지 로직 없음 -// usePathname 훅을 사용하지 않아 URL 변경을 감지하지 못함 -``` - ---- - -## ✅ 구현 솔루션 - -### 1. usePathname 훅 추가 - -```typescript -import { useRouter, usePathname } from 'next/navigation'; - -export default function DashboardLayout({ children }: DashboardLayoutProps) { - const pathname = usePathname(); // 현재 경로 추적 - // ... -} -``` - -**역할:** -- Next.js App Router의 현재 경로를 실시간으로 추적 -- 경로가 변경될 때마다 자동으로 리렌더링 트리거 - ---- - -### 2. 경로 기반 메뉴 활성화 로직 - -```typescript -// 현재 경로에 맞는 메뉴 자동 활성화 (URL 직접 입력, 뒤로가기 대응) -useEffect(() => { - if (!pathname || menuItems.length === 0) return; - - // 경로 정규화 (로케일 제거) - const normalizedPath = pathname.replace(/^\/(ko|en|ja)/, ''); - - // 메뉴 탐색 함수: 메인 메뉴와 서브메뉴 모두 탐색 - const findActiveMenu = (items: MenuItem[]): { menuId: string; parentId?: string } | null => { - for (const item of items) { - // 현재 메뉴의 경로와 일치하는지 확인 - if (item.path && normalizedPath.startsWith(item.path)) { - return { menuId: item.id }; - } - - // 서브메뉴가 있으면 재귀적으로 탐색 - if (item.children && item.children.length > 0) { - for (const child of item.children) { - if (child.path && normalizedPath.startsWith(child.path)) { - return { menuId: child.id, parentId: item.id }; - } - } - } - } - return null; - }; - - const result = findActiveMenu(menuItems); - - if (result) { - // 활성 메뉴 설정 - setActiveMenu(result.menuId); - - // 부모 메뉴가 있으면 자동으로 확장 - if (result.parentId && !expandedMenus.includes(result.parentId)) { - setExpandedMenus(prev => [...prev, result.parentId!]); - } - - console.log('🎯 경로 기반 메뉴 활성화:', { - path: normalizedPath, - menuId: result.menuId, - parentId: result.parentId - }); - } -}, [pathname, menuItems, setActiveMenu, expandedMenus]); -``` - ---- - -## 🔍 핵심 기능 상세 - -### 1. 경로 정규화 - -```typescript -const normalizedPath = pathname.replace(/^\/(ko|en|ja)/, ''); -``` - -**목적:** -- 다국어 로케일 프리픽스 제거 (`/ko/dashboard` → `/dashboard`) -- 메뉴 경로와 비교할 수 있는 일관된 형식 생성 - -**지원 로케일:** -- `ko` (한국어) -- `en` (영어) -- `ja` (일본어) - ---- - -### 2. 재귀적 메뉴 탐색 - -```typescript -const findActiveMenu = (items: MenuItem[]): { menuId: string; parentId?: string } | null => { - for (const item of items) { - // 1단계: 메인 메뉴 확인 - if (item.path && normalizedPath.startsWith(item.path)) { - return { menuId: item.id }; - } - - // 2단계: 서브메뉴 확인 (재귀) - if (item.children && item.children.length > 0) { - for (const child of item.children) { - if (child.path && normalizedPath.startsWith(child.path)) { - return { menuId: child.id, parentId: item.id }; // 부모 ID도 반환 - } - } - } - } - return null; -}; -``` - -**동작 방식:** - -| 현재 경로 | 메뉴 구조 | 탐색 결과 | -|-----------|-----------|-----------| -| `/dashboard` | `dashboard: { path: '/dashboard' }` | `{ menuId: 'dashboard' }` | -| `/master-data/product` | `master-data → product: { path: '/master-data/product' }` | `{ menuId: 'product', parentId: 'master-data' }` | -| `/inventory/stock` | `inventory: { path: '/inventory' }` | `{ menuId: 'inventory' }` | - -**특징:** -- `startsWith()` 사용으로 하위 경로도 매칭 - - `/inventory` → `/inventory/stock`도 매칭 ✅ -- 서브메뉴인 경우 부모 ID도 함께 반환 -- Depth-first 탐색으로 가장 구체적인 매칭 우선 - ---- - -### 3. 자동 서브메뉴 확장 - -```typescript -if (result.parentId && !expandedMenus.includes(result.parentId)) { - setExpandedMenus(prev => [...prev, result.parentId!]); -} -``` - -**동작:** -- 서브메뉴가 활성화되면 부모 메뉴를 자동으로 확장 -- 사용자가 서브메뉴 위치를 바로 확인 가능 - -**예시:** -```typescript -// URL: /master-data/product -// 결과: -// 1. 'master-data' 메뉴 자동 확장 ✅ -// 2. 'product' 서브메뉴 활성화 ✅ -``` - ---- - -## 📁 수정된 파일 - -### `/src/layouts/DashboardLayout.tsx` - -**변경 사항:** - -1. **Import 추가** -```typescript -import { useRouter, usePathname } from 'next/navigation'; -import type { MenuItem } from '@/store/menuStore'; -``` - -2. **pathname 훅 사용** -```typescript -const pathname = usePathname(); // 현재 경로 추적 -``` - -3. **경로 기반 메뉴 활성화 useEffect 추가** -```typescript -useEffect(() => { - // 경로 정규화 → 메뉴 탐색 → 활성화 + 확장 -}, [pathname, menuItems, setActiveMenu, expandedMenus]); -``` - ---- - -## 🎬 동작 시나리오 - -### 시나리오 1: URL 직접 입력 - -``` -1. 사용자: 주소창에 '/inventory' 입력 -2. usePathname: '/ko/inventory' 감지 -3. 정규화: '/inventory' -4. findActiveMenu: 'inventory' 메뉴 찾음 -5. setActiveMenu('inventory') 실행 -6. 결과: 사이드바에서 'inventory' 메뉴 활성화 ✅ -``` - ---- - -### 시나리오 2: 브라우저 뒤로가기 - -``` -1. 현재 페이지: /master-data/product (product 메뉴 활성화) -2. 사용자: 뒤로가기 클릭 -3. 경로 변경: /dashboard -4. usePathname: '/ko/dashboard' 감지 -5. findActiveMenu: 'dashboard' 메뉴 찾음 -6. setActiveMenu('dashboard') 실행 -7. 결과: 사이드바에서 'dashboard' 메뉴 활성화 ✅ -``` - ---- - -### 시나리오 3: 서브메뉴 직접 접근 - -``` -1. 사용자: URL 직접 입력 '/master-data/customer' -2. usePathname: '/ko/master-data/customer' 감지 -3. 정규화: '/master-data/customer' -4. findActiveMenu: 'customer' 메뉴 찾음 (parentId: 'master-data') -5. setActiveMenu('customer') 실행 -6. expandedMenus에 'master-data' 추가 -7. 결과: - - 'master-data' 메뉴 자동 확장 ✅ - - 'customer' 서브메뉴 활성화 ✅ -``` - ---- - -## 🔄 동작 흐름도 - -``` -┌─────────────────────────────────────────────────────┐ -│ URL 변경 이벤트 │ -│ - 직접 입력, 뒤로가기, 앞으로가기, router.push() │ -└─────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────┐ -│ usePathname 훅이 새로운 경로 감지 │ -│ 예: '/ko/master-data/product' │ -└─────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────┐ -│ useEffect 트리거 │ -│ 의존성: [pathname, menuItems, ...] │ -└─────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────┐ -│ 경로 정규화 │ -│ '/ko/master-data/product' → '/master-data/product' │ -└─────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────┐ -│ findActiveMenu() 함수 실행 │ -│ - 메인 메뉴 탐색 │ -│ - 서브메뉴 재귀 탐색 │ -└─────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────┐ -│ 매칭된 메뉴 찾음 │ -│ { menuId: 'product', parentId: 'master-data' } │ -└─────────────────────────────────────────────────────┘ - ↓ - ┌────────────────┴────────────────┐ - ↓ ↓ -┌──────────────────┐ ┌──────────────────────┐ -│ setActiveMenu │ │ 부모 메뉴 자동 확장 │ -│ ('product') │ │ master-data 확장 │ -└──────────────────┘ └──────────────────────┘ - ↓ ↓ -┌─────────────────────────────────────────────────────┐ -│ 사이드바 UI 업데이트 │ -│ ✅ 'product' 메뉴 활성화 (파란색) │ -│ ✅ 'master-data' 메뉴 확장 (서브메뉴 표시) │ -└─────────────────────────────────────────────────────┘ -``` - ---- - -## 🧪 테스트 케이스 - -### 테스트 1: 메인 메뉴 직접 접근 -```typescript -// Given: 사용자가 URL 직접 입력 -URL: /dashboard - -// When: 페이지 로드 -pathname: '/ko/dashboard' -normalizedPath: '/dashboard' - -// Then: dashboard 메뉴 활성화 -activeMenu: 'dashboard' ✅ -expandedMenus: [] (부모 없음) -``` - ---- - -### 테스트 2: 서브메뉴 직접 접근 -```typescript -// Given: 사용자가 서브메뉴 URL 직접 입력 -URL: /master-data/product - -// When: 페이지 로드 -pathname: '/ko/master-data/product' -normalizedPath: '/master-data/product' - -// Then: 서브메뉴 활성화 + 부모 확장 -activeMenu: 'product' ✅ -expandedMenus: ['master-data'] ✅ -``` - ---- - -### 테스트 3: 뒤로가기 -```typescript -// Given: -// 현재 페이지: /inventory (inventory 메뉴 활성화) -// 이전 페이지: /dashboard - -// When: 브라우저 뒤로가기 클릭 -pathname 변경: '/ko/inventory' → '/ko/dashboard' - -// Then: 메뉴 자동 전환 -activeMenu: 'inventory' → 'dashboard' ✅ -``` - ---- - -### 테스트 4: 앞으로가기 -```typescript -// Given: -// 현재 페이지: /dashboard (dashboard 메뉴 활성화) -// 다음 페이지: /inventory (history에 존재) - -// When: 브라우저 앞으로가기 클릭 -pathname 변경: '/ko/dashboard' → '/ko/inventory' - -// Then: 메뉴 자동 전환 -activeMenu: 'dashboard' → 'inventory' ✅ -``` - ---- - -### 테스트 5: 프로그래매틱 네비게이션 -```typescript -// Given: 코드에서 router.push() 호출 -router.push('/settings') - -// When: 경로 변경 -pathname: '/ko/settings' - -// Then: 메뉴 자동 활성화 -activeMenu: 'settings' ✅ -``` - ---- - -## 💡 기술적 고려사항 - -### 1. 성능 최적화 - -**의존성 배열 최소화:** -```typescript -useEffect(() => { - // ... -}, [pathname, menuItems, setActiveMenu, expandedMenus]); -``` - -- `pathname` 변경 시에만 실행 -- `menuItems` 변경은 초기 로드 시 한 번만 발생 -- 불필요한 리렌더링 방지 - -**조기 리턴:** -```typescript -if (!pathname || menuItems.length === 0) return; -``` - -- 조건 불만족 시 즉시 종료 -- 불필요한 계산 방지 - ---- - -### 2. 로케일 처리 - -```typescript -const normalizedPath = pathname.replace(/^\/(ko|en|ja)/, ''); -``` - -**지원 로케일:** -- 한국어 (`ko`) -- 영어 (`en`) -- 일본어 (`ja`) - -**확장성:** -```typescript -// 새로운 로케일 추가 시 -const normalizedPath = pathname.replace(/^\/(ko|en|ja|zh|fr)/, ''); -``` - ---- - -### 3. 경로 매칭 로직 - -**startsWith() 사용 이유:** -```typescript -if (item.path && normalizedPath.startsWith(item.path)) { - return { menuId: item.id }; -} -``` - -**장점:** -- 하위 경로 자동 매칭 - - `/inventory` → `/inventory/stock` 매칭 ✅ -- 동적 라우트 지원 - - `/product/:id` → `/product/123` 매칭 ✅ - -**주의사항:** -- 구체적인 경로를 먼저 탐색해야 함 -- 예: `/settings/profile`을 먼저 확인, 그 다음 `/settings` - ---- - -### 4. 타입 안전성 - -```typescript -interface MenuItem { - id: string; - label: string; - icon: LucideIcon; - path: string; - children?: MenuItem[]; -} - -const findActiveMenu = (items: MenuItem[]): { menuId: string; parentId?: string } | null => { - // ... -}; -``` - -**타입 체크:** -- `menuId`: string (필수) -- `parentId`: string | undefined (선택) -- 반환값: null 가능 (매칭 실패 시) - ---- - -## 🎨 사용자 경험 개선 - -### Before (이전) -``` -❌ URL 직접 입력: /inventory - → 메뉴 활성화 안됨 (사용자 혼란) - -❌ 뒤로가기: /dashboard로 이동 - → 이전 메뉴 여전히 활성화 (불일치) - -❌ 서브메뉴 URL 접근: /master-data/product - → 부모 메뉴 닫혀있음 (위치 파악 어려움) -``` - -### After (개선 후) -``` -✅ URL 직접 입력: /inventory - → inventory 메뉴 자동 활성화 - -✅ 뒤로가기: /dashboard로 이동 - → dashboard 메뉴 자동 활성화 - -✅ 서브메뉴 URL 접근: /master-data/product - → 부모 메뉴 자동 확장 + 서브메뉴 활성화 -``` - ---- - -## 🐛 엣지 케이스 처리 - -### 1. 메뉴에 없는 경로 -```typescript -// URL: /unknown-page -// 결과: findActiveMenu() → null -// 처리: activeMenu 변경 없음 (이전 상태 유지) -``` - ---- - -### 2. 메뉴가 로드되지 않음 -```typescript -if (!pathname || menuItems.length === 0) return; -``` - -**처리:** -- 조기 리턴으로 에러 방지 -- menuItems 로드 후 자동 실행 - ---- - -### 3. 중복 경로 -```typescript -// 메뉴 구조: -// - dashboard: { path: '/dashboard' } -// - reports: { path: '/dashboard/reports' } - -// URL: /dashboard/reports -// 결과: 'reports' 메뉴 활성화 (더 구체적인 경로 우선) -``` - ---- - -### 4. 로케일 없는 경로 -```typescript -// URL: /dashboard (로케일 없음) -const normalizedPath = pathname.replace(/^\/(ko|en|ja)/, ''); -// 결과: '/dashboard' (변경 없음) -// 처리: 정상 작동 ✅ -``` - ---- - -## 📊 개선 효과 - -### 메트릭 - -| 지표 | Before | After | 개선율 | -|------|--------|-------|--------| -| URL 직접 입력 시 메뉴 동기화 | 0% | 100% | +100% | -| 뒤로가기 시 메뉴 동기화 | 0% | 100% | +100% | -| 서브메뉴 자동 확장 | 수동 | 자동 | +100% | -| 사용자 혼란도 | 높음 | 낮음 | -80% | - ---- - -## 🔗 관련 문서 - -- [Route Protection Architecture](./[IMPL-2025-11-07]%20route-protection-architecture.md) -- [Menu System Implementation](./[IMPL-2025-11-08]%20dynamic-menu-generation.md) -- [DashboardLayout Migration](./[IMPL-2025-11-11]%20dashboardlayout-centralization.md) -- [Empty Page Configuration](./[IMPL-2025-11-11]%20empty-page-configuration.md) - ---- - -## 📚 참고 자료 - -- [Next.js usePathname](https://nextjs.org/docs/app/api-reference/functions/use-pathname) -- [Next.js useRouter](https://nextjs.org/docs/app/api-reference/functions/use-router) -- [React useEffect](https://react.dev/reference/react/useEffect) - ---- - -**작성일:** 2025-11-11 -**작성자:** Claude Code -**마지막 수정:** 2025-11-11 - ---- - -## 관련 파일 - -### 프론트엔드 -- `src/layouts/DashboardLayout.tsx` - usePathname 훅으로 경로 기반 메뉴 활성화 -- `src/components/layout/Sidebar.tsx` - 사이드바 컴포넌트 -- `src/store/menuStore.ts` - 메뉴 상태 관리 (Zustand) - -### 참조 문서 -- `claudedocs/dashboard/[IMPL-2025-11-13] sidebar-scroll-improvements.md` - 사이드바 스크롤 개선 -- `claudedocs/architecture/[REF] architecture-integration-risks.md` - 미들웨어 아키텍처 \ No newline at end of file diff --git a/claudedocs/dashboard/[IMPL-2025-11-13] sidebar-scroll-improvements.md b/claudedocs/dashboard/[IMPL-2025-11-13] sidebar-scroll-improvements.md deleted file mode 100644 index 3bf9868c..00000000 --- a/claudedocs/dashboard/[IMPL-2025-11-13] sidebar-scroll-improvements.md +++ /dev/null @@ -1,416 +0,0 @@ -# 사이드바 스크롤 및 UX 개선 - -## 개요 - -레프트 메뉴(사이드바)의 스크롤 기능과 사용자 경험을 개선한 작업입니다. 메뉴가 많아져도 편리하게 탐색할 수 있도록 자동 스크롤, sticky 고정, macOS 스타일 스크롤바 등을 구현했습니다. - -**작업 일자**: 2025-11-13 -**관련 파일**: -- `src/components/layout/Sidebar.tsx` -- `src/layouts/DashboardLayout.tsx` -- `src/app/globals.css` - ---- - -## 구현된 기능 - -### 1. 메뉴 영역 독립 스크롤 - -**문제**: 메뉴가 많아도 사이드바가 화면 크기에 맞춰 늘어나서 스크롤이 생기지 않음 - -**해결**: -- 사이드바 컨테이너에 고정 높이 설정: `h-[calc(100vh-24px)]` -- 메뉴 영역에 `flex-1 overflow-y-auto` 적용 -- 화면 전체 스크롤과 독립적으로 메뉴만 스크롤 가능 - -**파일**: `src/layouts/DashboardLayout.tsx:166` -```tsx -