From 65a8510c0b4cafcbc660be0a259a678579a5c06e Mon Sep 17 00:00:00 2001 From: byeongcheolryu Date: Thu, 27 Nov 2025 22:19:50 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=ED=92=88=EB=AA=A9=EA=B8=B0=EC=A4=80?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20=EB=8F=99?= =?UTF-8?q?=EA=B8=B0=ED=99=94=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BOM 항목 추가/수정/삭제 시 섹션탭 즉시 반영 - 섹션 복제 시 UI 즉시 업데이트 (null vs undefined 이슈 해결) - 항목 수정 기능 추가 (useTemplateManagement) - 실시간 동기화 문서 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .gitignore | 7 + .../[GUIDE] collaboration-with-claude.md | 96 ++ ...PL-2025-11-27] item-master-api-refactor.md | 307 ++++ ...25-11-26] item-master-api-pending-tasks.md | 116 -- claudedocs/_index.md | 135 ++ .../[IMPL-2025-11-07] api-key-management.md | 16 +- ...[IMPL-2025-11-11] api-route-type-safety.md | 13 + claudedocs/{ => api}/[REF] api-analysis.md | 17 +- .../{ => api}/[REF] api-requirements.md | 18 +- ...IMPL-2025-11-13] browser-support-policy.md | 20 +- .../[IMPL-2025-11-18] ssr-hydration-fix.md | 15 +- ...025-11-19] multi-tenancy-implementation.md | 21 +- .../[REF] architecture-integration-risks.md | 24 +- ...ST-2025-11-19] multi-tenancy-test-guide.md | 0 ...5-11-07] seo-bot-blocking-configuration.md | 0 .../[IMPL-2025-11-11] chart-warning-fix.md | 0 ...L-2025-11-11] error-pages-configuration.md | 0 ...25-11-12] modal-select-layout-shift-fix.md | 0 .../[IMPL-2025-11-17] item-list-css-sync.md | 0 .../[INDEX] DOCUMENTATION-MAP.md | 0 claudedocs/{ => archive}/[LEGACY] 00_INDEX.md | 0 .../[LEGACY] authentication-design.md | 0 .../[PLAN-2025-11-18] refactoring-plan.md | 0 .../[PLAN-2025-11-21] component-separation.md | 0 .../[REF-2025-11-18] cleanup-summary.md | 0 .../[REF-2025-11-18] unused-files-report.md | 0 ...EF-2025-11-21] type-error-fix-checklist.md | 0 .../[REF] code-quality-report.md | 0 .../[REF] communication_improvement_guide.md | 0 .../[REF] component-usage-analysis.md | 0 .../[REF] production-deployment-checklist.md | 0 .../{ => archive}/[REF] project-context.md | 0 ...-11-18] localStorage-ssr-fix-checkpoint.md | 0 ...25] httponly-cookie-security-validation.md | 0 .../[IMPL-2025-11-07] auth-guard-usage.md | 16 + ...07] authentication-implementation-guide.md | 20 +- ...-11-07] jwt-cookie-authentication-final.md | 19 +- ...2025-11-07] middleware-issue-resolution.md | 0 ...25-11-07] route-protection-architecture.md | 0 ...IMPL-2025-11-10] token-management-guide.md | 47 +- ...2025-11-13] safari-cookie-compatibility.md | 0 .../[PLAN] httponly-cookie-implementation.md | 16 +- ...js15-middleware-authentication-research.md | 0 .../[REF] session-migration-backend.md | 0 .../[REF] session-migration-frontend.md | 0 .../[REF] session-migration-summary.md | 0 .../[REF] token-security-nextjs15-research.md | 0 ...5-11-10] dashboard-integration-complete.md | 21 + ...L-2025-11-11] dashboard-cleanup-summary.md | 12 + ...PL-2025-11-11] sidebar-active-menu-sync.md | 15 +- ...2025-11-13] sidebar-scroll-improvements.md | 15 +- .../[REF] dashboard-migration-summary.md | 21 + .../[GUIDE] CSS-MIGRATION-WORKFLOW.md | 16 +- .../[GUIDE] LARGE-FILE-WORKFLOW.md | 14 + .../[GUIDE] ZOD-VALIDATION-TROUBLESHOOTING.md | 11 +- .../[IMPL-2025-11-06] i18n-usage-guide.md | 22 +- ...[IMPL-2025-11-07] form-validation-guide.md | 23 +- .../[REF] nextjs-error-handling-guide.md | 25 +- ...[ANALYSIS-2025-11-21] item-master-notes.md | 0 ...[ANALYSIS-2025-11-26] item-master-notes.md | 1371 +++++++++++++++++ .../[ANALYSIS] item-master-data-management.md | 0 ...I-2025-11-20] item-master-specification.md | 0 ...11-23] item-master-backend-requirements.md | 20 +- ...11-24] item-management-dynamic-api-spec.md | 21 +- ...item-master-data-management-api-request.md | 16 +- ...2025-11-25] section-template-fields-api.md | 16 + ...11-24] item-management-dynamic-frontend.md | 17 +- .../[GUIDE] ITEM-MANAGEMENT-MIGRATION.md | 0 ...] item-master-api-integration-checklist.md | 0 .../[IMPL-2025-11-27] realtime-sync-fixes.md | 188 +++ ...25-11-26] item-master-api-pending-tasks.md | 227 +++ ...-11-26] item-master-pending-integration.md | 106 ++ ...5-11-27] item-form-component-separation.md | 276 ++++ ...25-11-26] item-master-hooks-refactoring.md | 21 +- .../[REF] api-requirements-items.md | 21 +- package-lock.json | 64 + package.json | 6 +- src/app/api/proxy/[...path]/route.ts | 185 ++- src/components/auth/LoginPage.tsx | 82 +- src/components/common/ServerErrorPage.tsx | 140 ++ src/components/items/ItemForm/BOMSection.tsx | 365 +++++ .../items/ItemForm/BendingDiagramSection.tsx | 367 +++++ src/components/items/ItemForm/FormHeader.tsx | 62 + .../items/ItemForm/ValidationAlert.tsx | 50 + src/components/items/ItemForm/constants.ts | 93 ++ .../ItemForm/context/ItemFormContext.tsx | 77 + .../items/ItemForm/context/index.ts | 12 + .../items/ItemForm/forms/MaterialForm.tsx | 354 +++++ .../items/ItemForm/forms/PartForm.tsx | 273 ++++ .../items/ItemForm/forms/ProductForm.tsx | 337 ++++ src/components/items/ItemForm/forms/index.ts | 7 + .../ItemForm/forms/parts/AssemblyPartForm.tsx | 336 ++++ .../ItemForm/forms/parts/BendingPartForm.tsx | 302 ++++ .../forms/parts/PurchasedPartForm.tsx | 318 ++++ .../items/ItemForm/forms/parts/index.ts | 12 + src/components/items/ItemForm/hooks/index.ts | 12 + .../items/ItemForm/hooks/useBOMManagement.ts | 221 +++ .../items/ItemForm/hooks/useBendingDetails.ts | 182 +++ .../items/ItemForm/hooks/useItemFormState.ts | 364 +++++ .../{ItemForm.tsx => ItemForm/index.tsx} | 1154 +------------- src/components/items/ItemForm/types.ts | 21 + .../items/ItemMasterDataManagement.tsx | 364 ++++- .../components/ConditionalDisplayUI.tsx | 2 +- .../components/DraggableField.tsx | 2 +- .../components/DraggableSection.tsx | 4 +- .../dialogs/FieldDialog.tsx | 14 +- .../dialogs/FieldDrawer.tsx | 16 +- .../dialogs/ImportFieldDialog.tsx | 279 ++++ .../dialogs/ImportSectionDialog.tsx | 221 +++ .../dialogs/MasterFieldDialog.tsx | 4 +- .../dialogs/SectionDialog.tsx | 259 ++-- .../dialogs/TemplateFieldDialog.tsx | 18 +- .../hooks/useAttributeManagement.ts | 34 +- .../hooks/useFieldManagement.ts | 89 +- .../hooks/useMasterFieldManagement.ts | 16 +- .../hooks/useSectionManagement.ts | 175 ++- .../hooks/useTabManagement.ts | 45 +- .../hooks/useTemplateManagement.ts | 229 +-- .../tabs/HierarchyTab/index.tsx | 158 +- .../tabs/MasterFieldTab/index.tsx | 16 +- .../tabs/SectionsTab.tsx | 99 +- src/contexts/ItemMasterContext.tsx | 1305 +++++++++++----- src/lib/api/error-handler.ts | 6 + src/lib/api/item-master.ts | 746 ++++++++- src/lib/api/transformers.ts | 144 +- src/lib/utils/validation.ts | 2 +- src/messages/en.json | 1 + src/messages/ko.json | 1 + src/types/item-master-api.ts | 253 ++- tsconfig.tsbuildinfo | 2 +- 130 files changed, 11031 insertions(+), 2287 deletions(-) create mode 100644 claudedocs/[GUIDE] collaboration-with-claude.md create mode 100644 claudedocs/[IMPL-2025-11-27] item-master-api-refactor.md delete mode 100644 claudedocs/[NEXT-2025-11-26] item-master-api-pending-tasks.md create mode 100644 claudedocs/_index.md rename claudedocs/{ => api}/[IMPL-2025-11-07] api-key-management.md (94%) rename claudedocs/{ => api}/[IMPL-2025-11-11] api-route-type-safety.md (95%) rename claudedocs/{ => api}/[REF] api-analysis.md (94%) rename claudedocs/{ => api}/[REF] api-requirements.md (93%) rename claudedocs/{ => architecture}/[IMPL-2025-11-13] browser-support-policy.md (94%) rename claudedocs/{ => architecture}/[IMPL-2025-11-18] ssr-hydration-fix.md (83%) rename claudedocs/{ => architecture}/[REF-2025-11-19] multi-tenancy-implementation.md (96%) rename claudedocs/{ => architecture}/[REF] architecture-integration-risks.md (96%) rename claudedocs/{ => architecture}/[TEST-2025-11-19] multi-tenancy-test-guide.md (100%) rename claudedocs/{ => archive}/[IMPL-2025-11-07] seo-bot-blocking-configuration.md (100%) rename claudedocs/{ => archive}/[IMPL-2025-11-11] chart-warning-fix.md (100%) rename claudedocs/{ => archive}/[IMPL-2025-11-11] error-pages-configuration.md (100%) rename claudedocs/{ => archive}/[IMPL-2025-11-12] modal-select-layout-shift-fix.md (100%) rename claudedocs/{ => archive}/[IMPL-2025-11-17] item-list-css-sync.md (100%) rename claudedocs/{ => archive}/[INDEX] DOCUMENTATION-MAP.md (100%) rename claudedocs/{ => archive}/[LEGACY] 00_INDEX.md (100%) rename claudedocs/{ => archive}/[LEGACY] authentication-design.md (100%) rename claudedocs/{ => archive}/[PLAN-2025-11-18] refactoring-plan.md (100%) rename claudedocs/{ => archive}/[PLAN-2025-11-21] component-separation.md (100%) rename claudedocs/{ => archive}/[REF-2025-11-18] cleanup-summary.md (100%) rename claudedocs/{ => archive}/[REF-2025-11-18] unused-files-report.md (100%) rename claudedocs/{ => archive}/[REF-2025-11-21] type-error-fix-checklist.md (100%) rename claudedocs/{ => archive}/[REF] code-quality-report.md (100%) rename claudedocs/{ => archive}/[REF] communication_improvement_guide.md (100%) rename claudedocs/{ => archive}/[REF] component-usage-analysis.md (100%) rename claudedocs/{ => archive}/[REF] production-deployment-checklist.md (100%) rename claudedocs/{ => archive}/[REF] project-context.md (100%) rename claudedocs/{ => archive}/[SESSION-2025-11-18] localStorage-ssr-fix-checkpoint.md (100%) rename claudedocs/{ => auth}/[CASE-2025-11-25] httponly-cookie-security-validation.md (100%) rename claudedocs/{ => auth}/[IMPL-2025-11-07] auth-guard-usage.md (93%) rename claudedocs/{ => auth}/[IMPL-2025-11-07] authentication-implementation-guide.md (91%) rename claudedocs/{ => auth}/[IMPL-2025-11-07] jwt-cookie-authentication-final.md (95%) rename claudedocs/{ => auth}/[IMPL-2025-11-07] middleware-issue-resolution.md (100%) rename claudedocs/{ => auth}/[IMPL-2025-11-07] route-protection-architecture.md (100%) rename claudedocs/{ => auth}/[IMPL-2025-11-10] token-management-guide.md (81%) rename claudedocs/{ => auth}/[IMPL-2025-11-13] safari-cookie-compatibility.md (100%) rename claudedocs/{ => auth}/[PLAN] httponly-cookie-implementation.md (96%) rename claudedocs/{ => auth}/[REF] nextjs15-middleware-authentication-research.md (100%) rename claudedocs/{ => auth}/[REF] session-migration-backend.md (100%) rename claudedocs/{ => auth}/[REF] session-migration-frontend.md (100%) rename claudedocs/{ => auth}/[REF] session-migration-summary.md (100%) rename claudedocs/{ => auth}/[REF] token-security-nextjs15-research.md (100%) rename claudedocs/{ => dashboard}/[IMPL-2025-11-10] dashboard-integration-complete.md (83%) rename claudedocs/{ => dashboard}/[IMPL-2025-11-11] dashboard-cleanup-summary.md (92%) rename claudedocs/{ => dashboard}/[IMPL-2025-11-11] sidebar-active-menu-sync.md (97%) rename claudedocs/{ => dashboard}/[IMPL-2025-11-13] sidebar-scroll-improvements.md (95%) rename claudedocs/{ => dashboard}/[REF] dashboard-migration-summary.md (81%) rename claudedocs/{ => guides}/[GUIDE] CSS-MIGRATION-WORKFLOW.md (96%) rename claudedocs/{ => guides}/[GUIDE] LARGE-FILE-WORKFLOW.md (97%) rename claudedocs/{ => guides}/[GUIDE] ZOD-VALIDATION-TROUBLESHOOTING.md (98%) rename claudedocs/{ => guides}/[IMPL-2025-11-06] i18n-usage-guide.md (95%) rename claudedocs/{ => guides}/[IMPL-2025-11-07] form-validation-guide.md (96%) rename claudedocs/{ => guides}/[REF] nextjs-error-handling-guide.md (95%) rename claudedocs/{ => item-master}/[ANALYSIS-2025-11-21] item-master-notes.md (100%) create mode 100644 claudedocs/item-master/[ANALYSIS-2025-11-26] item-master-notes.md rename claudedocs/{ => item-master}/[ANALYSIS] item-master-data-management.md (100%) rename claudedocs/{ => item-master}/[API-2025-11-20] item-master-specification.md (100%) rename claudedocs/{ => item-master}/[API-2025-11-23] item-master-backend-requirements.md (93%) rename claudedocs/{ => item-master}/[API-2025-11-24] item-management-dynamic-api-spec.md (97%) rename claudedocs/{ => item-master}/[API-2025-11-25] item-master-data-management-api-request.md (97%) rename claudedocs/{ => item-master}/[API-REQUEST-2025-11-25] section-template-fields-api.md (95%) rename claudedocs/{ => item-master}/[DESIGN-2025-11-24] item-management-dynamic-frontend.md (97%) rename claudedocs/{ => item-master}/[GUIDE] ITEM-MANAGEMENT-MIGRATION.md (100%) rename claudedocs/{ => item-master}/[IMPL-2025-11-20] item-master-api-integration-checklist.md (100%) create mode 100644 claudedocs/item-master/[IMPL-2025-11-27] realtime-sync-fixes.md create mode 100644 claudedocs/item-master/[NEXT-2025-11-26] item-master-api-pending-tasks.md create mode 100644 claudedocs/item-master/[NEXT-2025-11-26] item-master-pending-integration.md create mode 100644 claudedocs/item-master/[PLAN-2025-11-27] item-form-component-separation.md rename claudedocs/{ => item-master}/[REF-2025-11-26] item-master-hooks-refactoring.md (91%) rename claudedocs/{ => item-master}/[REF] api-requirements-items.md (97%) create mode 100644 src/components/common/ServerErrorPage.tsx create mode 100644 src/components/items/ItemForm/BOMSection.tsx create mode 100644 src/components/items/ItemForm/BendingDiagramSection.tsx create mode 100644 src/components/items/ItemForm/FormHeader.tsx create mode 100644 src/components/items/ItemForm/ValidationAlert.tsx create mode 100644 src/components/items/ItemForm/constants.ts create mode 100644 src/components/items/ItemForm/context/ItemFormContext.tsx create mode 100644 src/components/items/ItemForm/context/index.ts create mode 100644 src/components/items/ItemForm/forms/MaterialForm.tsx create mode 100644 src/components/items/ItemForm/forms/PartForm.tsx create mode 100644 src/components/items/ItemForm/forms/ProductForm.tsx create mode 100644 src/components/items/ItemForm/forms/index.ts create mode 100644 src/components/items/ItemForm/forms/parts/AssemblyPartForm.tsx create mode 100644 src/components/items/ItemForm/forms/parts/BendingPartForm.tsx create mode 100644 src/components/items/ItemForm/forms/parts/PurchasedPartForm.tsx create mode 100644 src/components/items/ItemForm/forms/parts/index.ts create mode 100644 src/components/items/ItemForm/hooks/index.ts create mode 100644 src/components/items/ItemForm/hooks/useBOMManagement.ts create mode 100644 src/components/items/ItemForm/hooks/useBendingDetails.ts create mode 100644 src/components/items/ItemForm/hooks/useItemFormState.ts rename src/components/items/{ItemForm.tsx => ItemForm/index.tsx} (60%) create mode 100644 src/components/items/ItemForm/types.ts create mode 100644 src/components/items/ItemMasterDataManagement/dialogs/ImportFieldDialog.tsx create mode 100644 src/components/items/ItemMasterDataManagement/dialogs/ImportSectionDialog.tsx diff --git a/.gitignore b/.gitignore index b362f7ba..d061a18d 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,10 @@ build/ # ---> Unused components and contexts (archived) src/components/_unused/ src/contexts/_unused/ + +# ---> Playwright (E2E 테스트 - 로컬 전용) +e2e/ +playwright.config.ts +playwright-report/ +test-results/ +.playwright/ diff --git a/claudedocs/[GUIDE] collaboration-with-claude.md b/claudedocs/[GUIDE] collaboration-with-claude.md new file mode 100644 index 00000000..fe634187 --- /dev/null +++ b/claudedocs/[GUIDE] collaboration-with-claude.md @@ -0,0 +1,96 @@ +# Claude와 효율적인 협업 가이드 + +## 데이터 페칭 요청 시 체크리스트 + +``` +□ API 엔드포인트 + 메서드 +□ 응답 JSON 구조 (성공/실패) +□ 인증 방식 (HttpOnly 쿠키면 프록시 필요) +□ UI 시나리오 (로딩/성공/실패) +``` + +이 4가지만 있으면 바로 구현 가능! + +--- + +## CSS vs 데이터 페칭 협업 차이 + +| CSS | 데이터 페칭 | +|-----|------------| +| 시각적 비교로 충분 | 보이지 않는 로직이라 설명 필요 | +| "이거 안 맞아" → 바로 이해 | "안 돼" → 어디서? 왜? 필요 | +| 스크린샷이 곧 스펙 | API 응답이 곧 스펙 | + +--- + +## 상황별 질문 템플릿 + +### 1. 데이터 페칭 요청 + +```markdown +## 기능: [기능명] + +### API +- 엔드포인트: +- 메서드: GET/POST/PUT/DELETE +- 인증: HttpOnly 쿠키 / Bearer + +### 응답 구조 +```json +{ ... } +``` + +### UI 동작 +- 로딩 중: +- 성공 시: +- 실패 시: +``` + +### 2. 에러 발생 시 + +```markdown +## 에러: [에러 메시지] + +### Network 탭 정보 +- Request URL: +- Request Headers: +- Response: + +### 콘솔 에러 (있다면) +``` + +### 3. 기능 요청 시 + +```markdown +## 기능: [기능명] + +### 기대 동작 +1. [트리거] 클릭/입력/등 +2. [API 호출] (있다면) +3. [성공 시] → +4. [실패 시] → +``` + +### 4. CSS 수정 요청 시 + +```markdown +## 수정 대상: [컴포넌트/파일명] + +### 현재 +[현재 상태 설명 또는 스크린샷] + +### 원하는 +[기대 상태 설명 또는 React 원본 참조] +``` + +--- + +## 기억할 점 + +- **CSS**: 지금처럼 하면 완벽 ✅ +- **데이터 페칭**: API 응답 구조 + 시나리오만 추가하면 OK +- **에러**: 전체 에러 스택/Network 탭 정보 공유하면 빠른 해결 + +--- + +*2025-11-27 작성* diff --git a/claudedocs/[IMPL-2025-11-27] item-master-api-refactor.md b/claudedocs/[IMPL-2025-11-27] item-master-api-refactor.md new file mode 100644 index 00000000..8e6b01e9 --- /dev/null +++ b/claudedocs/[IMPL-2025-11-27] item-master-api-refactor.md @@ -0,0 +1,307 @@ +# 품목기준관리 API 구조 변경 대응 작업 + +## 작업 일자: 2025-11-27 + +--- + +## 1. 백엔드 API 변경 요약 + +### 1.1 핵심 구조 변경: 독립 엔티티 + 링크 테이블 + +**Before (CASCADE FK)**: +``` +item_pages + ↓ page_id FK (CASCADE) - 삭제 시 연쇄 삭제 +item_sections + ↓ section_id FK (CASCADE) - 삭제 시 연쇄 삭제 +item_fields / item_bom_items +``` + +**After (독립 + 링크)**: +``` +item_pages (독립) +item_sections (독립) → entity_relationships (링크 테이블) +item_fields (독립) +item_bom_items (독립) +``` + +### 1.2 `item_master_fields` 테이블 통합 → DROP + +- `item_master_fields` 테이블 삭제됨 +- `item_fields` 테이블에 통합: + - `section_id` → nullable (독립 필드는 NULL) + - `category`, `description`, `is_common` 컬럼 추가 + +**결과**: 마스터 항목 = `item_fields` WHERE `section_id IS NULL` + +### 1.3 `entity_relationships` 테이블 구조 + +```sql +CREATE TABLE entity_relationships ( + id BIGINT PRIMARY KEY, + tenant_id BIGINT, + group_id INT DEFAULT 1, -- 1: 품목관리 + parent_type ENUM('page', 'section'), + parent_id BIGINT, + child_type ENUM('section', 'field', 'bom'), + child_id BIGINT, + order_no INT DEFAULT 0, + metadata JSON, + created_at, updated_at +); +``` + +### 1.4 새 API 엔드포인트 (14개) + +**페이지-섹션 연결**: +- `POST /pages/{pageId}/link-section` - { child_id, order_no? } +- `DELETE /pages/{pageId}/unlink-section/{sectionId}` + +**페이지-필드 직접 연결**: +- `POST /pages/{pageId}/link-field` - { child_id, order_no? } +- `DELETE /pages/{pageId}/unlink-field/{fieldId}` + +**페이지 관계 조회**: +- `GET /pages/{pageId}/relationships` +- `GET /pages/{pageId}/structure` ⭐ 전체 구조 조회 + +**섹션-필드 연결**: +- `POST /sections/{sectionId}/link-field` - { child_id, order_no? } +- `DELETE /sections/{sectionId}/unlink-field/{fieldId}` + +**섹션-BOM 연결**: +- `POST /sections/{sectionId}/link-bom` - { child_id, order_no? } +- `DELETE /sections/{sectionId}/unlink-bom/{bomId}` + +**섹션 관계 조회**: +- `GET /sections/{sectionId}/relationships` + +**순서 변경**: +- `POST /relationships/reorder` + ```json + { + "parent_type": "page", + "parent_id": 1, + "ordered_items": [ + { "child_type": "section", "child_id": 1 }, + { "child_type": "section", "child_id": 2 } + ] + } + ``` + +### 1.5 독립 엔티티 API + +**섹션**: +- `GET /sections` - 독립 섹션 목록 (is_template 필터 가능) +- `POST /sections` - 독립 섹션 생성 +- `POST /sections/{id}/clone` - 섹션 복제 +- `GET /sections/{id}/usage` - 섹션 사용처 조회 + +**필드**: +- `GET /fields` - 독립 필드 목록 +- `POST /fields` - 독립 필드 생성 +- `POST /fields/{id}/clone` - 필드 복제 +- `GET /fields/{id}/usage` - 필드 사용처 조회 + +**BOM**: +- `GET /bom-items` - 독립 BOM 목록 +- `POST /bom-items` - 독립 BOM 생성 + +--- + +## 2. 프론트엔드 수정 계획 + +### 2.1 타입 정의 수정 (item-master-api.ts) ✅ 완료 + +**제거 또는 수정**: +- [x] `ItemMasterField` → `ItemField`로 통합 (section_id = null) ✅ +- [x] `MasterFieldRequest`, `MasterFieldResponse` deprecated 표시 ✅ + +**추가**: +- [x] `EntityRelationship` 타입 추가 (`EntityRelationshipResponse`) ✅ +- [x] `LinkEntityRequest` 타입 추가 ✅ +- [x] `ReorderRelationshipsRequest` 타입 추가 ✅ +- [x] `PageStructureResponse` 타입 추가 ✅ +- [x] `LinkBomRequest` 타입 추가 ✅ + +**수정**: +- [x] `ItemFieldResponse`에 `category`, `description`, `is_common` 추가 ✅ +- [x] `IndependentFieldRequest`에 `category`, `description`, `is_common` 추가 ✅ +- [x] `InitResponse`에 `fields` 필드 추가, `masterFields` deprecated ✅ + +### 2.2 API 함수 수정 (lib/api/item-master.ts) ✅ 완료 + +**deprecated 표시**: +- [x] `masterFields.list()` - deprecated, `fields.list()` 사용 권장 ✅ +- [x] `masterFields.create()` - deprecated, `fields.createIndependent()` 사용 권장 ✅ +- [x] `masterFields.update()` - deprecated, `fields.update()` 사용 권장 ✅ +- [x] `masterFields.delete()` - deprecated, `fields.delete()` 사용 권장 ✅ + +**추가 (link/unlink API)**: +- [x] `pages.linkSection()` ✅ (기존) +- [x] `pages.unlinkSection()` ✅ (기존) +- [x] `pages.linkField()` ✅ +- [x] `pages.unlinkField()` ✅ +- [x] `sections.linkField()` ✅ (기존) +- [x] `sections.unlinkField()` ✅ (기존) +- [x] `sections.linkBom()` ✅ +- [x] `sections.unlinkBom()` ✅ +- [x] `pages.getRelationships()` ✅ +- [x] `pages.getStructure()` ✅ (기존) +- [x] `sections.getRelationships()` ✅ +- [x] `relationships.reorder()` ✅ + +**추가 (독립 엔티티 API)** - 기존 구현됨: +- [x] `sections.list()` ✅ +- [x] `sections.createIndependent()` ✅ +- [x] `sections.clone()` ✅ +- [x] `sections.getUsage()` ✅ +- [x] `fields.list()` ✅ +- [x] `fields.createIndependent()` ✅ +- [x] `fields.clone()` ✅ +- [x] `fields.getUsage()` ✅ + +### 2.3 Context 수정 (ItemMasterContext.tsx) ✅ 완료 + +**상태 변경**: +- [x] `itemMasterFields` → deprecated 표시 (기존 유지, `independentFields` 병행) ✅ +- [x] 독립 필드 = `independentFields` state 이미 존재 ✅ + +**함수 변경 (API 마이그레이션 + deprecated 표시)**: +- [x] `addItemMasterField` → `fields.createIndependent()` 사용 ✅ +- [x] `updateItemMasterField` → `fields.update()` 사용 ✅ +- [x] `deleteItemMasterField` → `fields.delete()` 사용 ✅ +- [x] `loadItemMasterFields` → deprecated 표시 추가 ✅ + +**신규 함수 (이미 구현됨)**: +- [x] `linkSectionToPage(pageId, sectionId)` ✅ (line 2225) +- [x] `unlinkSectionFromPage(pageId, sectionId)` ✅ (line 2294) +- [x] `linkFieldToSection(sectionId, fieldId)` ✅ (line 2328) +- [x] `unlinkFieldFromSection(sectionId, fieldId)` ✅ (line 2366) + +### 2.4 UI 컴포넌트 수정 + +**계층구조 탭 (HierarchyTab)**: ✅ 완료 +- [x] 섹션 추가 시 → link-section API 사용 ✅ (기존 구현) +- [x] 섹션 제거 시 → unlink-section API 사용 ✅ (기존 구현) +- [x] 필드 제거 시 → unlinkFieldFromSection API 사용 ✅ +- [x] confirm/toast 메시지 "연결 해제"로 변경 ✅ +- [x] ItemMasterDataManagement.tsx에서 handleUnlinkFieldWithTracking 사용 ✅ + +**섹션 탭 (SectionsTab)**: ✅ 완료 (2025-11-27) +- [x] handleDeleteTemplateField → unlinkFieldFromSection API 호출로 변경 ✅ +- [x] SectionsTab.tsx 필드 삭제 아이콘 → Unlink 아이콘 (orange) ✅ +- [x] 버튼 title "삭제" → "연결 해제" 변경 ✅ +- [x] confirm 메시지 "연결을 해제하시겠습니까?" 변경 ✅ +- [x] toast 메시지 "항목 연결이 해제되었습니다" 변경 ✅ +- [x] useTemplateManagement.ts에 linkFieldToSection, unlinkFieldFromSection import 추가 ✅ + +**항목 탭 (MasterFieldTab → FieldTab)**: ✅ 완료 +- [x] 데이터 소스: Context에서 `fields.*` API 사용 (2025-11-27) +- [x] CRUD → 독립 필드 API 사용 (`fields.createIndependent()`, `fields.update()`, `fields.delete()`) +- [x] useMasterFieldManagement.ts에 deprecated 표시 추가 +- [x] MasterFieldTab/index.tsx UI 텍스트 "항목"으로 변경 +- [x] MasterFieldDialog 제목/설명 변경 +- [x] toast 메시지 "마스터 항목" → "항목" 변경 +- [x] 다이얼로그 텍스트 변경 완료 (FieldDialog, FieldDrawer, TemplateFieldDialog, ImportFieldDialog) + +**속성 탭 (AttributesTab)**: ✅ 분석 완료 - itemMasterFields 양방향 연동 (2025-11-27) +- [x] 데이터 소스: `itemMasterFields` (Context) + 로컬 옵션 state ✅ +- [x] 양방향 연동: 항목탭 ⇄ 속성탭 (같은 itemMasterFields 참조) +- [x] 항목 추가/수정/삭제 → 속성탭에도 반영 ✅ +- [x] 속성 옵션 변경 → 해당 필드의 dropdown options 자동 업데이트 (useAttributeManagement.ts:131-161) + +**ImportFieldDialog**: ✅ 탭 통합 완료 (2025-11-27) +- [x] 항목/독립필드 탭 → 단일 필드 목록으로 통합 ✅ +- [x] `fields` prop 추가, `independentFields`/`itemMasterFields` deprecated ✅ +- [x] `onImport` 시그니처 단순화: `(source?: ImportSource)` → `()` ✅ +- [x] `handleImportField` 함수: source 분기 제거 → `linkFieldToSection` 단일 호출 ✅ + +--- + +## 3. 삭제 동작 변경 + +### 3.1 기존 (CASCADE 삭제) + +``` +페이지 삭제 → 연결된 섹션도 삭제 → 연결된 필드도 삭제 +``` + +### 3.2 변경 후 (unlink만) + +``` +페이지 삭제 → entity_relationships에서 링크만 제거 + → 섹션은 섹션 탭에 유지 + → 필드는 항목 탭에 유지 +``` + +**UI에서 "삭제" vs "연결 해제" 구분**: +- 연결 해제: 현재 페이지/섹션에서만 제거, 원본 유지 +- 실제 삭제: 엔티티 자체를 삭제 (모든 곳에서 사라짐) + +--- + +## 4. 데이터 통일 + +### 4.1 필드 속성 공유 + +**현재 문제**: +- 마스터 항목에서 `is_required` 설정 +- 계층구조/섹션에서는 복사본이라 반영 안됨 + +**해결**: +- 이제 같은 `item_fields` 레코드를 링크로 참조 +- 한 곳에서 수정 → 모든 곳에 반영 + +### 4.2 UI 표시 통일 + +| 탭 | 데이터 소스 | 용도 | +|---|---|---| +| 항목 탭 | `itemMasterFields` (Context) | 독립 필드 CRUD | +| 속성 탭 | `itemMasterFields` + 로컬 옵션 state | 단위/재질/표면처리 + 필드 연동 | +| 계층구조 탭 | `entity_relationships` → `item_fields` | 페이지-섹션-필드 구조 | +| 섹션 탭 | `entity_relationships` → `item_fields` | 섹션-필드 연결 관리 | + +**핵심**: 모든 탭이 `item_fields`를 공유 → 어디서 수정해도 전체 반영! + +--- + +## 5. 작업 체크리스트 + +### Phase 1: 타입 및 API 수정 ✅ 완료 +- [x] `item-master-api.ts` 타입 정의 수정 ✅ +- [x] `lib/api/item-master.ts` API 함수 추가 ✅ + +### Phase 2: Context 수정 ✅ 완료 +- [x] `ItemMasterContext.tsx` 상태 및 함수 수정 ✅ + +### Phase 3: UI 컴포넌트 수정 ✅ 완료 +- [x] 계층구조 탭 - 필드 삭제 → unlink 변경 ✅ +- [x] 섹션 탭 - handleDeleteTemplateField → unlinkFieldFromSection API 호출 ✅ (2025-11-27) +- [x] 항목 탭 - 데이터 소스 변경 + UI 텍스트 변경 ✅ (다이얼로그 포함 완료) +- [x] 속성 탭 - 동일 데이터 소스 사용 (통합됨) ✅ +- [x] ImportFieldDialog - 탭 통합 완료 (항목/독립필드 → 필드) ✅ (2025-11-27) + +### Phase 4: 테스트 +- [ ] 페이지 삭제 시 섹션 유지 확인 +- [ ] 섹션에서 필드 제거 시 항목 탭에 유지 확인 +- [ ] 필드 속성 변경 시 모든 탭에 반영 확인 + +--- + +## 6. 관련 파일 + +### 프론트엔드 +- `src/types/item-master-api.ts` +- `src/lib/api/item-master.ts` +- `src/contexts/ItemMasterContext.tsx` +- `src/components/items/ItemMasterDataManagement.tsx` +- `src/components/items/ItemMasterDataManagement/tabs/HierarchyTab/` +- `src/components/items/ItemMasterDataManagement/tabs/SectionsTab.tsx` +- `src/components/items/ItemMasterDataManagement/tabs/MasterFieldTab/` +- `src/components/items/ItemMasterDataManagement/dialogs/ImportFieldDialog.tsx` + +### 백엔드 (참조) +- `app/Models/ItemMaster/EntityRelationship.php` +- `app/Swagger/v1/EntityRelationshipApi.php` +- `routes/api.php` diff --git a/claudedocs/[NEXT-2025-11-26] item-master-api-pending-tasks.md b/claudedocs/[NEXT-2025-11-26] item-master-api-pending-tasks.md deleted file mode 100644 index 078708c2..00000000 --- a/claudedocs/[NEXT-2025-11-26] item-master-api-pending-tasks.md +++ /dev/null @@ -1,116 +0,0 @@ -# 품목기준관리 - API 대기 및 다음 작업 - -**작성일**: 2025-11-26 -**상태**: API 대기 중 - ---- - -## 1. 현재 대기 중인 API 작업 - -### 1.1 계층구조(페이지) 탭 - -| 기능 | 설명 | 상태 | -|------|------|------| -| 생성 데이터 연결 | 최하위 항목(필드)까지 공통으로 연결 | ⏳ 대기 | -| 페이지 삭제 | 실제 삭제 (Soft Delete) | ⏳ 대기 | -| 섹션 연결 끊기 | 삭제가 아닌 연결만 해제 (`page_id = null`) | ⏳ 대기 | -| 항목 연결 끊기 | 삭제가 아닌 연결만 해제 | ⏳ 대기 | -| 섹션 불러오기 | 기존 섹션 목록에서 선택하여 연결 | ⏳ 대기 | -| 섹션 리스트 조회 | 연결 가능한 섹션 목록 표시 | ⏳ 대기 | -| 항목 불러오기 | 기존 항목 목록에서 선택하여 연결 | ⏳ 대기 | -| 항목 리스트 조회 | 연결 가능한 항목 목록 표시 | ⏳ 대기 | - -### 1.2 섹션 탭 - -| 기능 | 설명 | 상태 | -|------|------|------| -| 항목 불러오기 | 마스터 항목에서 선택하여 추가 | ⏳ 대기 | -| 항목 리스트 조회 | 추가 가능한 마스터 항목 목록 | ⏳ 대기 | - -### 1.3 데이터 동기화 - -| 기능 | 설명 | 상태 | -|------|------|------| -| 개별 수정 시 연결된 데이터 동기화 | 마스터 항목 수정 → 연결된 모든 필드에 반영 | ⏳ 대기 | - ---- - -## 2. 삭제 vs 연결 끊기 정리 - -``` -[계층구조 탭에서] -├─ 페이지 삭제 → 실제 삭제 (Soft Delete) -├─ 섹션 제거 → 연결만 끊기 (page_id = null), 섹션 데이터는 유지 -└─ 항목 제거 → 연결만 끊기 (section_id = null), 항목 데이터는 유지 - -[섹션 탭에서] -├─ 섹션 삭제 → 실제 삭제 (Soft Delete) -└─ 항목 삭제 → 실제 삭제 (Soft Delete) - -[마스터 항목 탭에서] -└─ 마스터 항목 삭제 → 실제 삭제 (참조된 필드는 master_field_id = null) -``` - ---- - -## 3. 데이터 연결 구조 - -``` -마스터 항목 (master_fields) - ↓ 참조 (master_field_id) -섹션 템플릿 항목 (template_fields) ←──┐ - ↓ 복사 │ -섹션 내 항목 (fields) ───────────────┘ - ↑ 소속 (section_id) -섹션 (sections) - ↑ 소속 (page_id) - 연결/해제 가능 -페이지 (pages) = 품목유형별 필드 구성 -``` - ---- - -## 4. 작업 순서 - -### Step 1: 품목기준관리 API 연동 (현재 대기) -- 위 1~3번 항목 API 연동 -- 품목기준관리 페이지 최종 완료 - -### Step 2: 품목관리 동적 렌더링 API 검토 -- 품목기준관리 완료 시점에 필요한 API 다시 검토 -- 추가 API 필요 여부 확인 -- `[API-2025-11-24] item-management-dynamic-api-spec.md` 업데이트 - -### Step 3: 품목관리 페이지 동적 렌더링 구현 - -``` -품목 등록 페이지 (/items/create) - │ - ├─ 품목유형 선택 (FG, PT, SM, RM, CS) - │ - ├─ GET /api/v1/item-master/pages?item_type={선택된유형} - │ - └─ 응답받은 섹션/필드 구조로 동적 폼 생성 -``` - -### 4.2 참고 문서 - -- `claudedocs/[API-2025-11-25] item-master-data-management-api-request.md` -- `claudedocs/[API-2025-11-24] item-management-dynamic-api-spec.md` -- `src/types/item-master-api.ts` -- `src/lib/api/item-master.ts` - ---- - -## 5. 핵심 개념 (잊지 말 것!) - -> **"페이지"는 실제 URL 경로가 아니라, 품목유형별 필드 구성 템플릿이다!** - -``` -품목기준관리의 "페이지" - = 품목유형(FG, PT, SM, RM, CS)별로 - = 품목 등록 시 어떤 섹션/필드를 보여줄지 정의하는 템플릿 -``` - ---- - -**다음 세션 시작 시**: 이 문서 확인 후 API 상태 체크하고 작업 진행 \ No newline at end of file diff --git a/claudedocs/_index.md b/claudedocs/_index.md new file mode 100644 index 00000000..81d4da1b --- /dev/null +++ b/claudedocs/_index.md @@ -0,0 +1,135 @@ +# claudedocs 문서 맵 + +> 프로젝트 기술 문서 인덱스 (Last Updated: 2025-11-27) + +## 폴더 구조 + +``` +claudedocs/ +├── _index.md # 이 파일 - 문서 맵 +├── auth/ # 🔐 인증 & 토큰 관리 +├── item-master/ # 📦 품목기준관리 +├── dashboard/ # 📊 대시보드 & 사이드바 +├── api/ # 🔌 API 통합 +├── guides/ # 📚 범용 가이드 +├── architecture/ # 🏗️ 아키텍처 & 시스템 +└── archive/ # 📁 레거시/완료된 문서 +``` + +--- + +## 🔐 auth/ - 인증 & 토큰 관리 + +| 파일 | 설명 | +|------|------| +| `token-management-guide.md` | ⭐ **핵심** - Access/Refresh Token 완전 가이드 | +| `jwt-cookie-authentication-final.md` | JWT + HttpOnly Cookie 구현 | +| `auth-guard-usage.md` | AuthGuard 훅 사용법 | +| `route-protection-architecture.md` | 라우트 보호 아키텍처 | +| `middleware-issue-resolution.md` | 미들웨어 이슈 해결 | +| `safari-cookie-compatibility.md` | Safari 쿠키 호환성 | +| `httponly-cookie-implementation.md` | HttpOnly 쿠키 구현 계획 | +| `httponly-cookie-security-validation.md` | 보안 검증 케이스 | +| `session-migration-*.md` | 세션 마이그레이션 관련 | +| `nextjs15-middleware-*.md` | Next.js 15 미들웨어 연구 | + +--- + +## 📦 item-master/ - 품목기준관리 + +| 파일 | 설명 | +|------|------| +| `[IMPL-2025-11-27] realtime-sync-fixes.md` | ⭐ **최신** - 실시간 동기화 수정 (BOM, 섹션 복제, 항목 수정) | +| `item-master-api-pending-tasks.md` | 진행중인 API 연동 작업 | +| `item-master-pending-integration.md` | 대기중인 통합 작업 | +| `item-master-specification.md` | API 명세 | +| `item-master-backend-requirements.md` | 백엔드 요구사항 | +| `item-management-dynamic-api-spec.md` | 동적 필드 API 스펙 | +| `item-management-dynamic-frontend.md` | 동적 필드 프론트엔드 설계 | +| `item-master-data-management.md` | 데이터 관리 분석 | +| `item-master-hooks-refactoring.md` | Hooks 리팩토링 | +| `ITEM-MANAGEMENT-MIGRATION.md` | 마이그레이션 가이드 | + +--- + +## 📊 dashboard/ - 대시보드 & 사이드바 + +| 파일 | 설명 | +|------|------| +| `dashboard-integration-complete.md` | 대시보드 통합 완료 | +| `dashboard-cleanup-summary.md` | 정리 요약 | +| `dashboard-migration-summary.md` | 마이그레이션 요약 | +| `sidebar-active-menu-sync.md` | 사이드바 메뉴 동기화 | +| `sidebar-scroll-improvements.md` | 스크롤 개선 | + +--- + +## 🔌 api/ - API 통합 + +| 파일 | 설명 | +|------|------| +| `api-requirements.md` | API 요구사항 | +| `api-analysis.md` | API 분석 | +| `api-route-type-safety.md` | 라우트 타입 안전성 | +| `api-key-management.md` | API 키 관리 | + +--- + +## 📚 guides/ - 범용 가이드 + +| 파일 | 설명 | +|------|------| +| `i18n-usage-guide.md` | 다국어 사용 가이드 | +| `form-validation-guide.md` | 폼 유효성 검사 | +| `CSS-MIGRATION-WORKFLOW.md` | CSS 마이그레이션 워크플로우 | +| `LARGE-FILE-WORKFLOW.md` | 대용량 파일 작업 워크플로우 | +| `ZOD-VALIDATION-TROUBLESHOOTING.md` | Zod 유효성 검사 트러블슈팅 | +| `nextjs-error-handling-guide.md` | Next.js 에러 처리 | + +--- + +## 🏗️ architecture/ - 아키텍처 & 시스템 + +| 파일 | 설명 | +|------|------| +| `multi-tenancy-implementation.md` | 멀티테넌시 구현 | +| `multi-tenancy-test-guide.md` | 멀티테넌시 테스트 | +| `architecture-integration-risks.md` | 통합 리스크 | +| `browser-support-policy.md` | 브라우저 지원 정책 | +| `ssr-hydration-fix.md` | SSR 하이드레이션 수정 | + +--- + +## 📁 archive/ - 레거시/완료된 문서 + +완료되거나 더 이상 활성화되지 않은 문서들. 참조용으로 보관. + +--- + +## 문서 작성 규칙 + +### 파일명 컨벤션 +``` +[TYPE-YYYY-MM-DD] description.md +``` + +**TYPE 종류**: +- `IMPL` - 구현 문서 +- `API` - API 명세/요청 +- `GUIDE` - 사용 가이드 +- `REF` - 참조 문서 +- `ANALYSIS` - 분석 노트 +- `PLAN` - 계획 문서 +- `DESIGN` - 설계 문서 +- `TEST` - 테스트 가이드 +- `NEXT` - 다음 작업 목록 + +### 폴더 배치 기준 +1. **기능/도메인 우선**: 문서 주제에 맞는 폴더에 배치 +2. **범용 가이드**: 여러 기능에 적용되면 `guides/`에 배치 +3. **완료된 작업**: 더 이상 활성화되지 않으면 `archive/`로 이동 +4. **신규 도메인**: 3개 이상 문서가 생기면 새 폴더 생성 고려 + +### 문서 업데이트 +- 중요 변경 시 문서 상단에 날짜와 함께 변경사항 기록 +- `_index.md`에 새 문서 추가 시 테이블 업데이트 diff --git a/claudedocs/[IMPL-2025-11-07] api-key-management.md b/claudedocs/api/[IMPL-2025-11-07] api-key-management.md similarity index 94% rename from claudedocs/[IMPL-2025-11-07] api-key-management.md rename to claudedocs/api/[IMPL-2025-11-07] api-key-management.md index a8dacd81..c71e1969 100644 --- a/claudedocs/[IMPL-2025-11-07] api-key-management.md +++ b/claudedocs/api/[IMPL-2025-11-07] api-key-management.md @@ -303,4 +303,18 @@ API Key 설정 완료 후: - **API Key 발급**: PHP 백엔드 팀 - **기술 지원**: 프론트엔드 팀 -- **보안 문제**: DevOps/보안 팀 \ No newline at end of file +- **보안 문제**: 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/[IMPL-2025-11-11] api-route-type-safety.md b/claudedocs/api/[IMPL-2025-11-11] api-route-type-safety.md similarity index 95% rename from claudedocs/[IMPL-2025-11-11] api-route-type-safety.md rename to claudedocs/api/[IMPL-2025-11-11] api-route-type-safety.md index 612d2d50..74e6ce73 100644 --- a/claudedocs/[IMPL-2025-11-11] api-route-type-safety.md +++ b/claudedocs/api/[IMPL-2025-11-11] api-route-type-safety.md @@ -319,3 +319,16 @@ interface Response { **작성일:** 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/[REF] api-analysis.md b/claudedocs/api/[REF] api-analysis.md similarity index 94% rename from claudedocs/[REF] api-analysis.md rename to claudedocs/api/[REF] api-analysis.md index e99d9f7c..0175f239 100644 --- a/claudedocs/[REF] api-analysis.md +++ b/claudedocs/api/[REF] api-analysis.md @@ -324,4 +324,19 @@ curl -X GET https://api.5130.co.kr/api/user \ 하지만 최종 결정은 백엔드 아키텍처와 요구사항에 따라야 합니다! -**백엔드 개발자에게 이 문서 공유 후 협의 추천** 👍 \ No newline at end of file +**백엔드 개발자에게 이 문서 공유 후 협의 추천** 👍 + +--- + +## 관련 파일 + +### 프론트엔드 +- `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/[REF] api-requirements.md b/claudedocs/api/[REF] api-requirements.md similarity index 93% rename from claudedocs/[REF] api-requirements.md rename to claudedocs/api/[REF] api-requirements.md index d13a690d..3d0eb057 100644 --- a/claudedocs/[REF] api-requirements.md +++ b/claudedocs/api/[REF] api-requirements.md @@ -417,4 +417,20 @@ interface RegisterData { --- -**API 준비되면 바로 알려주세요! 🚀** \ No newline at end of file +**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/[IMPL-2025-11-13] browser-support-policy.md b/claudedocs/architecture/[IMPL-2025-11-13] browser-support-policy.md similarity index 94% rename from claudedocs/[IMPL-2025-11-13] browser-support-policy.md rename to claudedocs/architecture/[IMPL-2025-11-13] browser-support-policy.md index a745e393..fd465918 100644 --- a/claudedocs/[IMPL-2025-11-13] browser-support-policy.md +++ b/claudedocs/architecture/[IMPL-2025-11-13] browser-support-policy.md @@ -495,4 +495,22 @@ if (typeof feature === 'undefined') { 2. **크로스 브라우저 테스트 필수**: Chrome, Safari, Edge 3. **사용자 친화적 안내**: IE 사용자에게 명확한 업그레이드 안내 -**문의**: 고객센터 또는 개발팀 \ No newline at end of file +**문의**: 고객센터 또는 개발팀 + +--- + +## 관련 파일 + +### 프론트엔드 +- `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/[IMPL-2025-11-18] ssr-hydration-fix.md b/claudedocs/architecture/[IMPL-2025-11-18] ssr-hydration-fix.md similarity index 83% rename from claudedocs/[IMPL-2025-11-18] ssr-hydration-fix.md rename to claudedocs/architecture/[IMPL-2025-11-18] ssr-hydration-fix.md index 8b46d1e7..428ee35d 100644 --- a/claudedocs/[IMPL-2025-11-18] ssr-hydration-fix.md +++ b/claudedocs/architecture/[IMPL-2025-11-18] ssr-hydration-fix.md @@ -90,4 +90,17 @@ useEffect(() => { ## 참고 문서 - Next.js SSR/Hydration: https://nextjs.org/docs/messages/react-hydration-error -- React useEffect: https://react.dev/reference/react/useEffect \ No newline at end of file +- 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/[REF-2025-11-19] multi-tenancy-implementation.md b/claudedocs/architecture/[REF-2025-11-19] multi-tenancy-implementation.md similarity index 96% rename from claudedocs/[REF-2025-11-19] multi-tenancy-implementation.md rename to claudedocs/architecture/[REF-2025-11-19] multi-tenancy-implementation.md index 426d3d26..6faab5e5 100644 --- a/claudedocs/[REF-2025-11-19] multi-tenancy-implementation.md +++ b/claudedocs/architecture/[REF-2025-11-19] multi-tenancy-implementation.md @@ -1023,4 +1023,23 @@ const response = await fetch(`/api/tenants/350/item-master-config`); **문서 버전**: 1.1 (tenant.id 반영) **마지막 업데이트**: 2025-11-19 -**다음 리뷰**: Phase 1 완료 후 \ No newline at end of file +**다음 리뷰**: 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/[REF] architecture-integration-risks.md b/claudedocs/architecture/[REF] architecture-integration-risks.md similarity index 96% rename from claudedocs/[REF] architecture-integration-risks.md rename to claudedocs/architecture/[REF] architecture-integration-risks.md index dcae673d..8ffe3050 100644 --- a/claudedocs/[REF] architecture-integration-risks.md +++ b/claudedocs/architecture/[REF] architecture-integration-risks.md @@ -842,4 +842,26 @@ const tenantId = createTenantId('acme-corp'); **다음 리뷰**: 설계 가이드 통합 후 또는 주요 아키텍처 변경 시 **작성자**: Claude Code -**승인 필요**: 프로젝트 매니저, 시니어 개발자 \ No newline at end of file +**승인 필요**: 프로젝트 매니저, 시니어 개발자 + +--- + +## 관련 파일 + +### 프론트엔드 +- `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/[TEST-2025-11-19] multi-tenancy-test-guide.md b/claudedocs/architecture/[TEST-2025-11-19] multi-tenancy-test-guide.md similarity index 100% rename from claudedocs/[TEST-2025-11-19] multi-tenancy-test-guide.md rename to claudedocs/architecture/[TEST-2025-11-19] multi-tenancy-test-guide.md diff --git a/claudedocs/[IMPL-2025-11-07] seo-bot-blocking-configuration.md b/claudedocs/archive/[IMPL-2025-11-07] seo-bot-blocking-configuration.md similarity index 100% rename from claudedocs/[IMPL-2025-11-07] seo-bot-blocking-configuration.md rename to claudedocs/archive/[IMPL-2025-11-07] seo-bot-blocking-configuration.md diff --git a/claudedocs/[IMPL-2025-11-11] chart-warning-fix.md b/claudedocs/archive/[IMPL-2025-11-11] chart-warning-fix.md similarity index 100% rename from claudedocs/[IMPL-2025-11-11] chart-warning-fix.md rename to claudedocs/archive/[IMPL-2025-11-11] chart-warning-fix.md diff --git a/claudedocs/[IMPL-2025-11-11] error-pages-configuration.md b/claudedocs/archive/[IMPL-2025-11-11] error-pages-configuration.md similarity index 100% rename from claudedocs/[IMPL-2025-11-11] error-pages-configuration.md rename to claudedocs/archive/[IMPL-2025-11-11] error-pages-configuration.md diff --git a/claudedocs/[IMPL-2025-11-12] modal-select-layout-shift-fix.md b/claudedocs/archive/[IMPL-2025-11-12] modal-select-layout-shift-fix.md similarity index 100% rename from claudedocs/[IMPL-2025-11-12] modal-select-layout-shift-fix.md rename to claudedocs/archive/[IMPL-2025-11-12] modal-select-layout-shift-fix.md diff --git a/claudedocs/[IMPL-2025-11-17] item-list-css-sync.md b/claudedocs/archive/[IMPL-2025-11-17] item-list-css-sync.md similarity index 100% rename from claudedocs/[IMPL-2025-11-17] item-list-css-sync.md rename to claudedocs/archive/[IMPL-2025-11-17] item-list-css-sync.md diff --git a/claudedocs/[INDEX] DOCUMENTATION-MAP.md b/claudedocs/archive/[INDEX] DOCUMENTATION-MAP.md similarity index 100% rename from claudedocs/[INDEX] DOCUMENTATION-MAP.md rename to claudedocs/archive/[INDEX] DOCUMENTATION-MAP.md diff --git a/claudedocs/[LEGACY] 00_INDEX.md b/claudedocs/archive/[LEGACY] 00_INDEX.md similarity index 100% rename from claudedocs/[LEGACY] 00_INDEX.md rename to claudedocs/archive/[LEGACY] 00_INDEX.md diff --git a/claudedocs/[LEGACY] authentication-design.md b/claudedocs/archive/[LEGACY] authentication-design.md similarity index 100% rename from claudedocs/[LEGACY] authentication-design.md rename to claudedocs/archive/[LEGACY] authentication-design.md diff --git a/claudedocs/[PLAN-2025-11-18] refactoring-plan.md b/claudedocs/archive/[PLAN-2025-11-18] refactoring-plan.md similarity index 100% rename from claudedocs/[PLAN-2025-11-18] refactoring-plan.md rename to claudedocs/archive/[PLAN-2025-11-18] refactoring-plan.md diff --git a/claudedocs/[PLAN-2025-11-21] component-separation.md b/claudedocs/archive/[PLAN-2025-11-21] component-separation.md similarity index 100% rename from claudedocs/[PLAN-2025-11-21] component-separation.md rename to claudedocs/archive/[PLAN-2025-11-21] component-separation.md diff --git a/claudedocs/[REF-2025-11-18] cleanup-summary.md b/claudedocs/archive/[REF-2025-11-18] cleanup-summary.md similarity index 100% rename from claudedocs/[REF-2025-11-18] cleanup-summary.md rename to claudedocs/archive/[REF-2025-11-18] cleanup-summary.md diff --git a/claudedocs/[REF-2025-11-18] unused-files-report.md b/claudedocs/archive/[REF-2025-11-18] unused-files-report.md similarity index 100% rename from claudedocs/[REF-2025-11-18] unused-files-report.md rename to claudedocs/archive/[REF-2025-11-18] unused-files-report.md diff --git a/claudedocs/[REF-2025-11-21] type-error-fix-checklist.md b/claudedocs/archive/[REF-2025-11-21] type-error-fix-checklist.md similarity index 100% rename from claudedocs/[REF-2025-11-21] type-error-fix-checklist.md rename to claudedocs/archive/[REF-2025-11-21] type-error-fix-checklist.md diff --git a/claudedocs/[REF] code-quality-report.md b/claudedocs/archive/[REF] code-quality-report.md similarity index 100% rename from claudedocs/[REF] code-quality-report.md rename to claudedocs/archive/[REF] code-quality-report.md diff --git a/claudedocs/[REF] communication_improvement_guide.md b/claudedocs/archive/[REF] communication_improvement_guide.md similarity index 100% rename from claudedocs/[REF] communication_improvement_guide.md rename to claudedocs/archive/[REF] communication_improvement_guide.md diff --git a/claudedocs/[REF] component-usage-analysis.md b/claudedocs/archive/[REF] component-usage-analysis.md similarity index 100% rename from claudedocs/[REF] component-usage-analysis.md rename to claudedocs/archive/[REF] component-usage-analysis.md diff --git a/claudedocs/[REF] production-deployment-checklist.md b/claudedocs/archive/[REF] production-deployment-checklist.md similarity index 100% rename from claudedocs/[REF] production-deployment-checklist.md rename to claudedocs/archive/[REF] production-deployment-checklist.md diff --git a/claudedocs/[REF] project-context.md b/claudedocs/archive/[REF] project-context.md similarity index 100% rename from claudedocs/[REF] project-context.md rename to claudedocs/archive/[REF] project-context.md diff --git a/claudedocs/[SESSION-2025-11-18] localStorage-ssr-fix-checkpoint.md b/claudedocs/archive/[SESSION-2025-11-18] localStorage-ssr-fix-checkpoint.md similarity index 100% rename from claudedocs/[SESSION-2025-11-18] localStorage-ssr-fix-checkpoint.md rename to claudedocs/archive/[SESSION-2025-11-18] localStorage-ssr-fix-checkpoint.md diff --git a/claudedocs/[CASE-2025-11-25] httponly-cookie-security-validation.md b/claudedocs/auth/[CASE-2025-11-25] httponly-cookie-security-validation.md similarity index 100% rename from claudedocs/[CASE-2025-11-25] httponly-cookie-security-validation.md rename to claudedocs/auth/[CASE-2025-11-25] httponly-cookie-security-validation.md diff --git a/claudedocs/[IMPL-2025-11-07] auth-guard-usage.md b/claudedocs/auth/[IMPL-2025-11-07] auth-guard-usage.md similarity index 93% rename from claudedocs/[IMPL-2025-11-07] auth-guard-usage.md rename to claudedocs/auth/[IMPL-2025-11-07] auth-guard-usage.md index b0c44ef1..da4db282 100644 --- a/claudedocs/[IMPL-2025-11-07] auth-guard-usage.md +++ b/claudedocs/auth/[IMPL-2025-11-07] auth-guard-usage.md @@ -317,3 +317,19 @@ export default function Page() { - 브라우저 캐시 악용 방지 - 실시간 인증 상태 동기화 - 로그아웃 후 완전한 페이지 접근 차단 + +--- + +## 관련 파일 + +### 프론트엔드 +- `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/[IMPL-2025-11-07] authentication-implementation-guide.md b/claudedocs/auth/[IMPL-2025-11-07] authentication-implementation-guide.md similarity index 91% rename from claudedocs/[IMPL-2025-11-07] authentication-implementation-guide.md rename to claudedocs/auth/[IMPL-2025-11-07] authentication-implementation-guide.md index 521365b8..ea13a645 100644 --- a/claudedocs/[IMPL-2025-11-07] authentication-implementation-guide.md +++ b/claudedocs/auth/[IMPL-2025-11-07] authentication-implementation-guide.md @@ -307,4 +307,22 @@ const user = await bearerClient.login({ - [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) \ No newline at end of file +- [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/[IMPL-2025-11-07] jwt-cookie-authentication-final.md b/claudedocs/auth/[IMPL-2025-11-07] jwt-cookie-authentication-final.md similarity index 95% rename from claudedocs/[IMPL-2025-11-07] jwt-cookie-authentication-final.md rename to claudedocs/auth/[IMPL-2025-11-07] jwt-cookie-authentication-final.md index 7e2ccb0c..1e3b0c7f 100644 --- a/claudedocs/[IMPL-2025-11-07] jwt-cookie-authentication-final.md +++ b/claudedocs/auth/[IMPL-2025-11-07] jwt-cookie-authentication-final.md @@ -488,4 +488,21 @@ headers: { 'Authorization': `Bearer ${token}` } - POST /api/v1/logout 4. 🚀 구현 시작 (2-3시간) -**준비되면 바로 시작합니다!** 🎯 \ No newline at end of file +**준비되면 바로 시작합니다!** 🎯 + +--- + +## 관련 파일 + +### 프론트엔드 +- `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/[IMPL-2025-11-07] middleware-issue-resolution.md b/claudedocs/auth/[IMPL-2025-11-07] middleware-issue-resolution.md similarity index 100% rename from claudedocs/[IMPL-2025-11-07] middleware-issue-resolution.md rename to claudedocs/auth/[IMPL-2025-11-07] middleware-issue-resolution.md diff --git a/claudedocs/[IMPL-2025-11-07] route-protection-architecture.md b/claudedocs/auth/[IMPL-2025-11-07] route-protection-architecture.md similarity index 100% rename from claudedocs/[IMPL-2025-11-07] route-protection-architecture.md rename to claudedocs/auth/[IMPL-2025-11-07] route-protection-architecture.md diff --git a/claudedocs/[IMPL-2025-11-10] token-management-guide.md b/claudedocs/auth/[IMPL-2025-11-10] token-management-guide.md similarity index 81% rename from claudedocs/[IMPL-2025-11-10] token-management-guide.md rename to claudedocs/auth/[IMPL-2025-11-10] token-management-guide.md index 79968c5f..6d6d860a 100644 --- a/claudedocs/[IMPL-2025-11-10] token-management-guide.md +++ b/claudedocs/auth/[IMPL-2025-11-10] token-management-guide.md @@ -199,7 +199,15 @@ HttpOnly 쿠키 삭제 **참고**: - 🔵 **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` 주석 참조 --- @@ -239,7 +247,40 @@ if (refreshToken && !accessToken) { } ``` -### 3. API Client에서 자동 갱신 +### 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 @@ -256,6 +297,8 @@ const data = await withTokenRefresh(() => 4. 성공 시 원래 API 재시도 5. 실패 시 로그인 페이지로 리다이렉트 +> **참고**: 대부분의 API 호출은 프록시를 통해 자동 갱신되므로 직접 사용할 필요 없음 + --- ## 사용 예시 diff --git a/claudedocs/[IMPL-2025-11-13] safari-cookie-compatibility.md b/claudedocs/auth/[IMPL-2025-11-13] safari-cookie-compatibility.md similarity index 100% rename from claudedocs/[IMPL-2025-11-13] safari-cookie-compatibility.md rename to claudedocs/auth/[IMPL-2025-11-13] safari-cookie-compatibility.md diff --git a/claudedocs/[PLAN] httponly-cookie-implementation.md b/claudedocs/auth/[PLAN] httponly-cookie-implementation.md similarity index 96% rename from claudedocs/[PLAN] httponly-cookie-implementation.md rename to claudedocs/auth/[PLAN] httponly-cookie-implementation.md index 16becad6..a1a5a9a3 100644 --- a/claudedocs/[PLAN] httponly-cookie-implementation.md +++ b/claudedocs/auth/[PLAN] httponly-cookie-implementation.md @@ -374,4 +374,18 @@ NEXT_PUBLIC_AUTH_MODE=sanctum - 로그인/로그아웃 플로우 - HttpOnly 쿠키 동작 확인 - 비로그인 상태 차단 확인 -- XSS 방어 검증 \ No newline at end of file +- 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/[REF] nextjs15-middleware-authentication-research.md b/claudedocs/auth/[REF] nextjs15-middleware-authentication-research.md similarity index 100% rename from claudedocs/[REF] nextjs15-middleware-authentication-research.md rename to claudedocs/auth/[REF] nextjs15-middleware-authentication-research.md diff --git a/claudedocs/[REF] session-migration-backend.md b/claudedocs/auth/[REF] session-migration-backend.md similarity index 100% rename from claudedocs/[REF] session-migration-backend.md rename to claudedocs/auth/[REF] session-migration-backend.md diff --git a/claudedocs/[REF] session-migration-frontend.md b/claudedocs/auth/[REF] session-migration-frontend.md similarity index 100% rename from claudedocs/[REF] session-migration-frontend.md rename to claudedocs/auth/[REF] session-migration-frontend.md diff --git a/claudedocs/[REF] session-migration-summary.md b/claudedocs/auth/[REF] session-migration-summary.md similarity index 100% rename from claudedocs/[REF] session-migration-summary.md rename to claudedocs/auth/[REF] session-migration-summary.md diff --git a/claudedocs/[REF] token-security-nextjs15-research.md b/claudedocs/auth/[REF] token-security-nextjs15-research.md similarity index 100% rename from claudedocs/[REF] token-security-nextjs15-research.md rename to claudedocs/auth/[REF] token-security-nextjs15-research.md diff --git a/claudedocs/[IMPL-2025-11-10] dashboard-integration-complete.md b/claudedocs/dashboard/[IMPL-2025-11-10] dashboard-integration-complete.md similarity index 83% rename from claudedocs/[IMPL-2025-11-10] dashboard-integration-complete.md rename to claudedocs/dashboard/[IMPL-2025-11-10] dashboard-integration-complete.md index c512254f..9b551bf8 100644 --- a/claudedocs/[IMPL-2025-11-10] dashboard-integration-complete.md +++ b/claudedocs/dashboard/[IMPL-2025-11-10] dashboard-integration-complete.md @@ -189,3 +189,24 @@ npm run dev ✅ **역할 기반 시스템**: 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/[IMPL-2025-11-11] dashboard-cleanup-summary.md b/claudedocs/dashboard/[IMPL-2025-11-11] dashboard-cleanup-summary.md similarity index 92% rename from claudedocs/[IMPL-2025-11-11] dashboard-cleanup-summary.md rename to claudedocs/dashboard/[IMPL-2025-11-11] dashboard-cleanup-summary.md index 355e7280..0f6ddc14 100644 --- a/claudedocs/[IMPL-2025-11-11] dashboard-cleanup-summary.md +++ b/claudedocs/dashboard/[IMPL-2025-11-11] dashboard-cleanup-summary.md @@ -183,3 +183,15 @@ const handleLogout = async () => { ✅ **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/[IMPL-2025-11-11] sidebar-active-menu-sync.md b/claudedocs/dashboard/[IMPL-2025-11-11] sidebar-active-menu-sync.md similarity index 97% rename from claudedocs/[IMPL-2025-11-11] sidebar-active-menu-sync.md rename to claudedocs/dashboard/[IMPL-2025-11-11] sidebar-active-menu-sync.md index e202bcb1..4bda06ff 100644 --- a/claudedocs/[IMPL-2025-11-11] sidebar-active-menu-sync.md +++ b/claudedocs/dashboard/[IMPL-2025-11-11] sidebar-active-menu-sync.md @@ -580,4 +580,17 @@ const normalizedPath = pathname.replace(/^\/(ko|en|ja)/, ''); **작성일:** 2025-11-11 **작성자:** Claude Code -**마지막 수정:** 2025-11-11 \ No newline at end of file +**마지막 수정:** 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/[IMPL-2025-11-13] sidebar-scroll-improvements.md b/claudedocs/dashboard/[IMPL-2025-11-13] sidebar-scroll-improvements.md similarity index 95% rename from claudedocs/[IMPL-2025-11-13] sidebar-scroll-improvements.md rename to claudedocs/dashboard/[IMPL-2025-11-13] sidebar-scroll-improvements.md index ca74723f..3bf9868c 100644 --- a/claudedocs/[IMPL-2025-11-13] sidebar-scroll-improvements.md +++ b/claudedocs/dashboard/[IMPL-2025-11-13] sidebar-scroll-improvements.md @@ -400,4 +400,17 @@ useEffect(() => { 3. **미니멀리즘**: 필요할 때만 UI 요소 표시 (스크롤바) 4. **성능**: 불필요한 리렌더링과 스크롤 방지 -이러한 작은 개선들이 모여 전체적인 사용자 만족도를 크게 향상시킬 수 있습니다. \ No newline at end of file +이러한 작은 개선들이 모여 전체적인 사용자 만족도를 크게 향상시킬 수 있습니다. + +--- + +## 관련 파일 + +### 프론트엔드 +- `src/components/layout/Sidebar.tsx` - 사이드바 컴포넌트 (스크롤 및 ref 처리) +- `src/layouts/DashboardLayout.tsx` - 대시보드 레이아웃 (sticky, 경로 매칭) +- `src/app/globals.css` - macOS 스타일 스크롤바 CSS (301-344 라인) + +### 참조 문서 +- `claudedocs/dashboard/[IMPL-2025-11-11] sidebar-active-menu-sync.md` - 메뉴 활성화 동기화 +- `claudedocs/architecture/[IMPL-2025-11-13] browser-support-policy.md` - 브라우저 지원 정책 \ No newline at end of file diff --git a/claudedocs/[REF] dashboard-migration-summary.md b/claudedocs/dashboard/[REF] dashboard-migration-summary.md similarity index 81% rename from claudedocs/[REF] dashboard-migration-summary.md rename to claudedocs/dashboard/[REF] dashboard-migration-summary.md index 398804b9..b4b480a7 100644 --- a/claudedocs/[REF] dashboard-migration-summary.md +++ b/claudedocs/dashboard/[REF] dashboard-migration-summary.md @@ -147,3 +147,24 @@ To test the migration: 3. Test role switching via dropdown 4. Verify each dashboard loads correctly 5. Check responsive design (mobile/desktop) + +--- + +## 관련 파일 + +### 프론트엔드 +- `src/components/business/Dashboard.tsx` - 대시보드 라우터 (lazy loading) +- `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/layouts/DashboardLayout.tsx` - 대시보드 레이아웃 +- `src/components/layout/Sidebar.tsx` - 사이드바 컴포넌트 +- `src/hooks/useUserRole.ts` - 역할 관리 훅 +- `src/hooks/useCurrentTime.ts` - 현재 시간 훅 +- `src/store/menuStore.ts` - 메뉴 상태 관리 (Zustand) +- `src/store/themeStore.ts` - 테마 상태 관리 (Zustand) + +### 참조 문서 +- `claudedocs/dashboard/[IMPL-2025-11-10] dashboard-integration-complete.md` - 대시보드 통합 완료 diff --git a/claudedocs/[GUIDE] CSS-MIGRATION-WORKFLOW.md b/claudedocs/guides/[GUIDE] CSS-MIGRATION-WORKFLOW.md similarity index 96% rename from claudedocs/[GUIDE] CSS-MIGRATION-WORKFLOW.md rename to claudedocs/guides/[GUIDE] CSS-MIGRATION-WORKFLOW.md index 558f6fd3..567d9c22 100644 --- a/claudedocs/[GUIDE] CSS-MIGRATION-WORKFLOW.md +++ b/claudedocs/guides/[GUIDE] CSS-MIGRATION-WORKFLOW.md @@ -409,4 +409,18 @@ ## 버전 히스토리 - **v1.0** (2025-11-17): 초안 작성, 4가지 방법론 정의 -- **v2.0** (2025-11-17): 실험 완료, 베스트 프랙티스 확립, 표준 워크플로우 정립 \ No newline at end of file +- **v2.0** (2025-11-17): 실험 완료, 베스트 프랙티스 확립, 표준 워크플로우 정립 + +--- + +## 관련 파일 + +### 프론트엔드 +- `src/components/items/ItemListClient.tsx` - 품목 관리 리스트 페이지 (마이그레이션 대상) +- `src/components/ui/tabs.tsx` - Tabs UI 컴포넌트 + +### 참조 소스 (React 원본) +- `sma-react-v2.0/src/components/ItemManagement.tsx` - React 원본 파일 + +### 참조 문서 +- `claudedocs/guides/[GUIDE] LARGE-FILE-WORKFLOW.md` - 대용량 파일 작업 워크플로우 \ No newline at end of file diff --git a/claudedocs/[GUIDE] LARGE-FILE-WORKFLOW.md b/claudedocs/guides/[GUIDE] LARGE-FILE-WORKFLOW.md similarity index 97% rename from claudedocs/[GUIDE] LARGE-FILE-WORKFLOW.md rename to claudedocs/guides/[GUIDE] LARGE-FILE-WORKFLOW.md index 9e3bea0f..a2038c20 100644 --- a/claudedocs/[GUIDE] LARGE-FILE-WORKFLOW.md +++ b/claudedocs/guides/[GUIDE] LARGE-FILE-WORKFLOW.md @@ -548,3 +548,17 @@ AI: - v1.1.0 (2025-01-15): Phase 4 추가 - 복잡한 다중 작업 처리 프로토콜 - 이유: 여러 요구사항 동시 처리 시 누락 발생 방지 - 목적: TodoWrite 기반 체계적 작업 분해 및 순차 실행 + +--- + +## 관련 파일 + +### 프론트엔드 +- `src/components/items/ItemListClient.tsx` - 품목 관리 리스트 페이지 +- `src/components/items/ItemForm.tsx` - 품목 등록/수정 폼 + +### 참조 소스 (React 원본) +- `sma-react-v2.0/src/components/ItemManagement.tsx` - React 원본 파일 (2600줄) + +### 참조 문서 +- `claudedocs/guides/[GUIDE] CSS-MIGRATION-WORKFLOW.md` - CSS 마이그레이션 워크플로우 diff --git a/claudedocs/[GUIDE] ZOD-VALIDATION-TROUBLESHOOTING.md b/claudedocs/guides/[GUIDE] ZOD-VALIDATION-TROUBLESHOOTING.md similarity index 98% rename from claudedocs/[GUIDE] ZOD-VALIDATION-TROUBLESHOOTING.md rename to claudedocs/guides/[GUIDE] ZOD-VALIDATION-TROUBLESHOOTING.md index 1cbdef75..7359c64e 100644 --- a/claudedocs/[GUIDE] ZOD-VALIDATION-TROUBLESHOOTING.md +++ b/claudedocs/guides/[GUIDE] ZOD-VALIDATION-TROUBLESHOOTING.md @@ -657,6 +657,11 @@ const materialSchemaBase = itemMasterBaseSchema Claude Code ## 관련 파일 -- `/src/lib/utils/validation.ts` -- `/src/components/items/ItemForm.tsx` -- `/src/types/item.ts` \ No newline at end of file + +### 프론트엔드 +- `src/lib/utils/validation.ts` - Zod 유효성 검증 스키마 정의 +- `src/components/items/ItemForm.tsx` - 품목 등록/수정 폼 컴포넌트 +- `src/types/item.ts` - 품목 타입 정의 + +### 참조 문서 +- `claudedocs/guides/[IMPL-2025-11-07] form-validation-guide.md` - 폼 및 유효성 검증 가이드 \ No newline at end of file diff --git a/claudedocs/[IMPL-2025-11-06] i18n-usage-guide.md b/claudedocs/guides/[IMPL-2025-11-06] i18n-usage-guide.md similarity index 95% rename from claudedocs/[IMPL-2025-11-06] i18n-usage-guide.md rename to claudedocs/guides/[IMPL-2025-11-06] i18n-usage-guide.md index 035893c1..450e4cb2 100644 --- a/claudedocs/[IMPL-2025-11-06] i18n-usage-guide.md +++ b/claudedocs/guides/[IMPL-2025-11-06] i18n-usage-guide.md @@ -735,4 +735,24 @@ export default function ClientComponent() { **문서 작성일**: 2025-11-06 **작성자**: Claude Code -**프로젝트**: Multi-tenant ERP System \ No newline at end of file +**프로젝트**: Multi-tenant ERP System + +--- + +## 관련 파일 + +### 프론트엔드 +- `src/i18n/config.ts` - i18n 설정 (지원 언어, 기본 언어) +- `src/i18n/request.ts` - 서버사이드 메시지 로딩 +- `src/messages/ko.json` - 한국어 메시지 +- `src/messages/en.json` - 영어 메시지 +- `src/messages/ja.json` - 일본어 메시지 +- `src/middleware.ts` - 로케일 감지 + 봇 차단 미들웨어 +- `src/app/[locale]/layout.tsx` - 루트 레이아웃 (NextIntlClientProvider) +- `src/components/LanguageSwitcher.tsx` - 언어 전환 컴포넌트 + +### 설정 파일 +- `next.config.ts` - Next.js 설정 (next-intl 플러그인) + +### 참조 문서 +- `claudedocs/architecture/[REF] architecture-integration-risks.md` - 아키텍처 통합 위험 분석 \ No newline at end of file diff --git a/claudedocs/[IMPL-2025-11-07] form-validation-guide.md b/claudedocs/guides/[IMPL-2025-11-07] form-validation-guide.md similarity index 96% rename from claudedocs/[IMPL-2025-11-07] form-validation-guide.md rename to claudedocs/guides/[IMPL-2025-11-07] form-validation-guide.md index ebeca903..655ea65c 100644 --- a/claudedocs/[IMPL-2025-11-07] form-validation-guide.md +++ b/claudedocs/guides/[IMPL-2025-11-07] form-validation-guide.md @@ -1017,4 +1017,25 @@ describe('loginSchema', () => { **문서 유효기간**: 2025-11-06 ~ **다음 업데이트**: 새로운 폼 패턴 추가 시 -**작성자**: Claude Code \ No newline at end of file +**작성자**: Claude Code + +--- + +## 관련 파일 + +### 프론트엔드 +- `src/lib/validation/auth.schema.ts` - 인증 관련 스키마 +- `src/lib/validation/product.schema.ts` - 제품 관련 스키마 +- `src/lib/utils/validation.ts` - 공통 유효성 검증 유틸리티 +- `src/lib/utils/form-error.ts` - 다국어 에러 메시지 유틸리티 +- `src/components/form/FormInput.tsx` - 재사용 가능 Input 컴포넌트 +- `src/components/form/FormSelect.tsx` - 재사용 가능 Select 컴포넌트 +- `src/components/items/ItemForm.tsx` - 품목 등록/수정 폼 + +### 다국어 메시지 +- `src/messages/ko.json` - 한국어 유효성 검증 메시지 +- `src/messages/en.json` - 영어 유효성 검증 메시지 +- `src/messages/ja.json` - 일본어 유효성 검증 메시지 + +### 참조 문서 +- `claudedocs/guides/[GUIDE] ZOD-VALIDATION-TROUBLESHOOTING.md` - Zod 유효성 검증 문제 해결 \ No newline at end of file diff --git a/claudedocs/[REF] nextjs-error-handling-guide.md b/claudedocs/guides/[REF] nextjs-error-handling-guide.md similarity index 95% rename from claudedocs/[REF] nextjs-error-handling-guide.md rename to claudedocs/guides/[REF] nextjs-error-handling-guide.md index 96a5e7d8..20e837bc 100644 --- a/claudedocs/[REF] nextjs-error-handling-guide.md +++ b/claudedocs/guides/[REF] nextjs-error-handling-guide.md @@ -703,4 +703,27 @@ export default function Error() { ## 마무리 -이 가이드를 바탕으로 Next.js 15 App Router 프로젝트에 체계적인 에러 처리와 로딩 상태 관리를 구현할 수 있습니다. 파일 위치와 우선순위를 정확히 이해하고, 각 파일의 역할과 요구사항을 준수하여 사용자 경험을 개선하세요. \ No newline at end of file +이 가이드를 바탕으로 Next.js 15 App Router 프로젝트에 체계적인 에러 처리와 로딩 상태 관리를 구현할 수 있습니다. 파일 위치와 우선순위를 정확히 이해하고, 각 파일의 역할과 요구사항을 준수하여 사용자 경험을 개선하세요. + +--- + +## 관련 파일 + +### 프론트엔드 +- `src/app/global-error.tsx` - 루트 레벨 에러 처리 +- `src/app/error.tsx` - 전역 에러 바운더리 +- `src/app/not-found.tsx` - 전역 404 페이지 +- `src/app/loading.tsx` - 전역 로딩 UI +- `src/app/[locale]/error.tsx` - 로케일별 에러 처리 +- `src/app/[locale]/not-found.tsx` - 로케일별 404 페이지 +- `src/app/[locale]/loading.tsx` - 로케일별 로딩 UI +- `src/app/[locale]/(protected)/error.tsx` - Protected 그룹 에러 처리 +- `src/app/[locale]/(protected)/loading.tsx` - Protected 그룹 로딩 UI + +### 다국어 메시지 +- `src/messages/ko.json` - 한국어 에러/404 메시지 +- `src/messages/en.json` - 영어 에러/404 메시지 +- `src/messages/ja.json` - 일본어 에러/404 메시지 + +### 참조 문서 +- `claudedocs/guides/[IMPL-2025-11-06] i18n-usage-guide.md` - 다국어 설정 가이드 \ No newline at end of file diff --git a/claudedocs/[ANALYSIS-2025-11-21] item-master-notes.md b/claudedocs/item-master/[ANALYSIS-2025-11-21] item-master-notes.md similarity index 100% rename from claudedocs/[ANALYSIS-2025-11-21] item-master-notes.md rename to claudedocs/item-master/[ANALYSIS-2025-11-21] item-master-notes.md diff --git a/claudedocs/item-master/[ANALYSIS-2025-11-26] item-master-notes.md b/claudedocs/item-master/[ANALYSIS-2025-11-26] item-master-notes.md new file mode 100644 index 00000000..8be887b4 --- /dev/null +++ b/claudedocs/item-master/[ANALYSIS-2025-11-26] item-master-notes.md @@ -0,0 +1,1371 @@ + prev.map(section => + section.id === sectionId + ? { ...section, bom_items: [...(section.bom_items || []), newBOM] } + : section +)); +``` + +--- + +### 2. 섹션 복제 실시간 반영 (`ItemMasterContext.tsx`) + +**문제**: 섹션 복제 후 새로고침해야 목록에 표시됨 + +**원인 1**: `page_id === null` 체크가 `undefined`를 처리하지 못함 +- API 응답: `page_id: null` +- 변환 후: `page_id: undefined` (transformer에서 변환 시 undefined로 됨) +- `null === undefined` → `false` → 잘못된 분기 진입 + +**해결**: strict equality(`===`) → loose equality(`==`) 변경 + +```typescript +// 변경 전 +if (clonedSection.page_id === null) + +// 변경 후 +if (clonedSection.page_id == null) // null과 undefined 둘 다 true +``` + +**원인 2**: 모듈 섹션 복제 시 `itemPages` 업데이트 누락 + +**해결**: `else` 분기에 `setItemPages` 업데이트 추가 + +```typescript +if (clonedSection.page_id == null) { + // 독립 섹션 → independentSections에 추가 + setIndependentSections(prev => [...prev, clonedSection]); +} else { + // 모듈 섹션 → itemPages 내 해당 페이지에 추가 + setItemPages(prev => prev.map(page => { + if (page.id === clonedSection.page_id) { + return { ...page, sections: [...page.sections, clonedSection] }; + } + return page; + })); +} +``` + +--- + +### 3. 항목 수정 기능 (`useTemplateManagement.ts`) + +**문제**: 섹션탭에서 항목 수정 버튼 클릭 시 아무 동작 없음 + +**원인**: `handleAddTemplateField` 함수가 추가 로직만 있고 수정 로직이 없음 + +**해결**: `editingTemplateFieldId` 체크 후 `updateField` API 호출 분기 추가 + +```typescript +const handleAddTemplateField = async () => { + // 수정 모드 + if (editingTemplateFieldId) { + const updateData = { + field_name: templateFieldName, + field_type: templateFieldInputType, + is_required: templateFieldRequired, + // ... 기타 필드 + }; + await updateField(editingTemplateFieldId, updateData); + toast.success('항목이 수정되었습니다'); + resetTemplateFieldForm(); + return; + } + + // 추가 모드 (기존 로직) + // ... +}; +``` + +--- + +## 백엔드 요청 사항 (미해결) + +### 섹션 복제 시 필드 복제 문제 + +**API**: `POST /v1/item-master/sections/{id}/clone` + +**현재 동작**: 섹션 복제 시 연결된 필드들도 새로운 레코드로 복제됨 + +**문제점**: 섹션-필드는 링크 관계이므로 섹션만 복제하고 필드는 기존 필드에 링크만 연결해야 함 + +**백엔드 수정 요청**: +```php +// 현재 코드 (문제) +foreach ($section->fields as $field) { + $newField = $field->replicate(); // ❌ 새 필드 레코드 생성 + $newField->section_id = $clonedSection->id; + $newField->save(); +} + +// 수정 요청 +foreach ($section->fields as $field) { + // ✅ 기존 필드를 새 섹션에 링크만 연결 + $clonedSection->fields()->attach($field->id); +} +``` + +**파일 위치**: `/sam-api/app/Services/ItemMaster/ItemSectionService.php` (Line 99-113) + +--- + +## 디버깅 로그 (제거 가능) + +개발 중 추가된 디버깅 로그들 (프로덕션 전 제거 권장): + +```typescript +// ItemMasterContext.tsx - cloneSection +console.log('[cloneSection] API 응답 원본:', sectionData); +console.log('[cloneSection] 변환 후 섹션:', {...}); +console.log('[cloneSection] 독립 섹션 추가 (independentSections)'); +console.log('[cloneSection] independentSections 업데이트:', newSections.length); + +// SectionsTab.tsx +console.log('[SectionsTab] 📥 sectionTemplates prop changed:', {...}); +console.log('[SectionsTab] 🔄 Rendering section templates:', {...}); + +// ItemMasterDataManagement.tsx +console.log('[sectionsAsTemplates] useMemo 재계산!', {...}); +console.log('[ItemMasterDataManagement] 📋 sectionsAsTemplates changed:', {...}); +``` + +--- + +## 수정된 파일 목록 + +| 파일 | 수정 내용 | +|---|---| +| `src/contexts/ItemMasterContext.tsx` | BOM 동기화, 섹션 복제 수정 | +| `src/components/items/ItemMasterDataManagement/hooks/useTemplateManagement.ts` | 항목 수정 로직 추가 | +| `src/components/items/ItemMasterDataManagement/dialogs/TemplateFieldDialog.tsx` | prop 타입 수정 (void → void \| Promise) | + +--- + +## 향후 작업 시 주의사항 + +1. **양방향 동기화 필수**: 데이터 변경 시 `itemPages`와 `independentSections` 모두 업데이트해야 함 +2. **null vs undefined**: API 응답의 null 값이 transformer 거치면서 undefined로 바뀔 수 있음 → `== null` 사용 권장 +3. **useMemo 의존성**: `sectionsAsTemplates`의 의존성 배열 확인 → 두 상태가 모두 업데이트되어야 재계산됨 \ No newline at end of file diff --git a/claudedocs/item-master/[NEXT-2025-11-26] item-master-api-pending-tasks.md b/claudedocs/item-master/[NEXT-2025-11-26] item-master-api-pending-tasks.md new file mode 100644 index 00000000..7a61a4b9 --- /dev/null +++ b/claudedocs/item-master/[NEXT-2025-11-26] item-master-api-pending-tasks.md @@ -0,0 +1,227 @@ +# 품목기준관리 - 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/item-master/[NEXT-2025-11-26] item-master-pending-integration.md b/claudedocs/item-master/[NEXT-2025-11-26] item-master-pending-integration.md new file mode 100644 index 00000000..e0e3eb89 --- /dev/null +++ b/claudedocs/item-master/[NEXT-2025-11-26] item-master-pending-integration.md @@ -0,0 +1,106 @@ +# 품목기준관리 - 백엔드 통합 대기 작업 + +**작성일**: 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/item-master/[PLAN-2025-11-27] item-form-component-separation.md b/claudedocs/item-master/[PLAN-2025-11-27] item-form-component-separation.md new file mode 100644 index 00000000..67d7874c --- /dev/null +++ b/claudedocs/item-master/[PLAN-2025-11-27] item-form-component-separation.md @@ -0,0 +1,276 @@ +# ItemForm.tsx 컴포넌트 분리 계획 + +## 작업 일자: 2025-11-27 (분석) + +--- + +## 1. 현재 상태 + +| 항목 | 내용 | +|------|------| +| **파일 경로** | `src/components/items/ItemForm.tsx` | +| **파일 크기** | 2,600줄 | +| **useState 수** | 25개+ | +| **주요 섹션** | 5개 Card | +| **복잡도** | 높음 (품목유형별 조건부 렌더링) | + +--- + +## 2. 구조 분석 + +``` +ItemForm.tsx (2,600줄) +├── Constants (62-134) ────────────────── 72줄 +│ ├── PART_TYPE_CATEGORIES +│ └── PART_ITEM_NAMES +│ +├── State & Hooks (142-218) ───────────── 76줄 +│ └── 25+ useState hooks +│ +├── Functions (220-409) ───────────────── 190줄 +│ ├── generateItemCode() +│ ├── handleFormSubmit() +│ └── handleItemTypeChange() +│ +└── JSX (411-2599) ────────────────────── 2,188줄 + ├── Validation Alert (414-459) ────── 45줄 + ├── Header (461-499) ──────────────── 38줄 + ├── 기본 정보 Card (501-1780) ──────── 1,279줄 ⚠️ 가장 큼 + │ ├── FG (제품) ─────── 102줄 + │ ├── PT (부품) ─────── 822줄 ⚠️ + │ │ ├── ASSEMBLY ──── 216줄 + │ │ ├── BENDING ───── 464줄 + │ │ └── PURCHASED ─── 223줄 + │ └── RM/SM/CS ──────── 320줄 + │ + ├── FG 비고 섹션 (1784-1940) ────────── 156줄 + ├── 전개도 Card (1942-2280) ─────────── 338줄 + └── BOM Card (2281-2584) ────────────── 303줄 +``` + +--- + +## 3. 분리 계획 + +### Phase 1: 상수 분리 (즉시 가능) + +``` +src/components/items/ItemForm/ +├── constants.ts # PART_TYPE_CATEGORIES, PART_ITEM_NAMES +├── types.ts # 타입 정의 +└── index.tsx # 메인 컴포넌트 +``` + +**작업 내용**: ✅ 완료 +- [x] `constants.ts` 생성 및 상수 이동 +- [x] `types.ts` 생성 (ItemFormProps 등) +- [x] import 경로 수정 + +### Phase 2: 섹션 컴포넌트 분리 + +``` +src/components/items/ItemForm/ +├── sections/ +│ ├── FormHeader.tsx # 헤더 + 저장/취소 버튼 +│ ├── ValidationAlert.tsx # 폼 에러 Alert +│ ├── BendingDiagramSection.tsx # 전개도 카드 (338줄) +│ └── BOMSection.tsx # 부품 구성 카드 (303줄) +``` + +**작업 내용**: ✅ 완료 (2025-11-27) +- [x] `FormHeader.tsx` 분리 (63줄) - ItemForm/ 폴더에 배치 +- [x] `ValidationAlert.tsx` 분리 (45줄) - ItemForm/ 폴더에 배치 +- [x] `BendingDiagramSection.tsx` 분리 (~300줄) - ItemForm/ 폴더에 배치 +- [x] `BOMSection.tsx` 분리 (~280줄) - ItemForm/ 폴더에 배치 + +### Phase 3: 품목 유형별 폼 분리 + +``` +src/components/items/ItemForm/ +├── forms/ +│ ├── ProductForm.tsx # FG (제품) - 102줄 +│ ├── PartForm.tsx # PT (부품) - 822줄 → 추가 분리 필요 +│ └── MaterialForm.tsx # RM/SM/CS - 320줄 +``` + +**작업 내용**: ✅ 완료 (2025-11-27) +- [x] `ProductForm.tsx` 분리 (FG 전용 필드) +- [x] `MaterialForm.tsx` 분리 (RM/SM/CS 공통) +- [x] `PartForm.tsx` 분리 (PT 전용, 하위 분리 완료) + +### Phase 4: 부품 유형별 추가 분리 + +``` +src/components/items/ItemForm/forms/ +├── parts/ +│ ├── AssemblyPartForm.tsx # 조립 부품 - ~300줄 +│ ├── BendingPartForm.tsx # 절곡 부품 - ~280줄 +│ └── PurchasedPartForm.tsx # 구매 부품 - ~270줄 +``` + +**작업 내용**: ✅ 완료 (2025-11-27) +- [x] `AssemblyPartForm.tsx` 분리 +- [x] `BendingPartForm.tsx` 분리 +- [x] `PurchasedPartForm.tsx` 분리 +- [x] `parts/index.ts` export 파일 생성 + +### Phase 5: 공통 컴포넌트 & 훅 + +``` +src/components/items/ItemForm/ +├── context/ +│ ├── ItemFormContext.tsx # 폼 상태 컨텍스트 +│ └── index.ts # export 파일 +└── hooks/ + ├── useItemFormState.ts # 25+ useState 통합 + ├── useBOMManagement.ts # BOM 라인 관리 + ├── useBendingDetails.ts # 전개도 계산 + └── index.ts # export 파일 +``` + +**작업 내용**: ✅ 완료 (2025-11-27) +- [x] ItemFormContext.tsx 생성 (Context Provider) +- [x] useItemFormState.ts 생성 (상태 통합 훅) +- [x] useBOMManagement.ts 생성 (BOM 관리 훅) +- [x] useBendingDetails.ts 생성 (전개도 계산 훅) +- [x] export 파일 생성 + +--- + +## 4. 분리 우선순위 + +| 우선순위 | 대상 | 효과 | 난이도 | API 의존 | +|---------|------|------|--------|----------| +| 🔴 **1** | constants.ts | 즉시 분리 가능 | 쉬움 | ❌ | +| 🔴 **2** | BOMSection.tsx | 독립적, 재사용 가능 | 쉬움 | ⚠️ 검색만 | +| 🟡 **3** | BendingDiagramSection.tsx | 독립적 | 쉬움 | ❌ | +| 🟡 **4** | ValidationAlert.tsx | 독립적 | 쉬움 | ❌ | +| 🟡 **5** | FormHeader.tsx | 독립적 | 쉬움 | ❌ | +| 🟡 **6** | MaterialForm.tsx | 명확한 경계 | 중간 | ❌ | +| 🟡 **7** | ProductForm.tsx | 명확한 경계 | 중간 | ❌ | +| 🟢 **8** | PartForm + 하위 분리 | 복잡한 상태 의존성 | 어려움 | ❌ | +| 🟢 **9** | useItemFormState.ts | 전체 리팩토링 필요 | 어려움 | ❌ | + +--- + +## 5. 주의사항 + +### 상태 공유 문제 +- 품목 유형별 폼이 `react-hook-form`의 `setValue`, `getValues` 공유 +- 하위 폼들이 부모의 선택 상태에 의존 +- 품목코드 자동생성 로직이 여러 필드 값 조합 + +### 해결 방안 +1. **Context 패턴**: ItemFormContext로 공유 상태 관리 +2. **Props Drilling**: 필요한 props만 하위 컴포넌트에 전달 +3. **Render Props**: 유연한 컴포넌트 조합 + +### 권장 접근법 +```typescript +// ItemFormContext.tsx +interface ItemFormContextType { + form: UseFormReturn; + selectedItemType: ItemType | ''; + selectedPartType: string; + // ... 공유 상태 +} + +// 하위 컴포넌트에서 사용 +const { form, selectedItemType } = useItemFormContext(); +``` + +--- + +## 6. 예상 결과 + +### Before +``` +src/components/items/ +├── ItemForm.tsx (2,600줄) ← 모놀리식 +``` + +### After +``` +src/components/items/ItemForm/ +├── index.tsx (300줄) ← 메인 컴포넌트 +├── constants.ts (72줄) +├── types.ts (50줄) +├── context.tsx (100줄) +├── sections/ +│ ├── FormHeader.tsx (50줄) +│ ├── ValidationAlert.tsx (50줄) +│ ├── BendingDiagramSection.tsx (350줄) +│ └── BOMSection.tsx (320줄) +├── forms/ +│ ├── ProductForm.tsx (120줄) +│ ├── MaterialForm.tsx (350줄) +│ └── PartForm.tsx (200줄) +│ └── parts/ +│ ├── AssemblyPartForm.tsx (230줄) +│ ├── BendingPartForm.tsx (480줄) +│ └── PurchasedPartForm.tsx (240줄) +├── components/ +│ ├── UnitSelect.tsx (60줄) +│ └── StatusSelect.tsx (50줄) +└── hooks/ + ├── useItemFormState.ts (100줄) + ├── useBOMManagement.ts (80줄) + └── useBendingDetails.ts (60줄) +``` + +**총 파일 수**: 1개 → 18개 +**최대 파일 크기**: 2,600줄 → ~480줄 +**평균 파일 크기**: ~150줄 + +--- + +## 7. 관련 파일 + +- `src/components/items/ItemForm.tsx` - 메인 대상 +- `src/components/items/ItemTypeSelect.tsx` - 이미 분리됨 +- `src/components/items/FileUpload.tsx` - 이미 분리됨 +- `src/components/items/DrawingCanvas.tsx` - 이미 분리됨 +- `src/components/items/BOMManagementSection.tsx` - 참고용 (다른 BOM 컴포넌트) +- `src/lib/utils/validation.ts` - Zod 스키마 + +--- + +## 8. 작업 체크리스트 + +### Phase 1: 즉시 가능 (API 독립적) ✅ 완료 (2025-11-27) +- [x] constants.ts 분리 +- [x] types.ts 분리 +- [x] ValidationAlert.tsx 분리 +- [x] FormHeader.tsx 분리 + +### Phase 2: 섹션 분리 ✅ 완료 (2025-11-27) +- [x] BendingDiagramSection.tsx 분리 +- [x] BOMSection.tsx 분리 + +### Phase 3: 폼 분리 ✅ 완료 (2025-11-27) +- [x] MaterialForm.tsx 분리 (RM/SM/CS) +- [x] ProductForm.tsx 분리 (FG) + ProductCertificationSection +- [x] PartForm.tsx 분리 (PT) +- [x] forms/index.ts export 파일 생성 +- [x] index.tsx에서 ProductForm, ProductCertificationSection 적용 + +### Phase 4: 부품 폼 분리 ✅ 완료 (2025-11-27) +- [x] AssemblyPartForm.tsx 분리 (~300줄) +- [x] BendingPartForm.tsx 분리 (~280줄) +- [x] PurchasedPartForm.tsx 분리 (~270줄) +- [x] parts/index.ts export 파일 생성 +- [x] PartForm.tsx에서 하위 컴포넌트 적용 (~273줄로 감소) + +### Phase 5: 훅 & 컨텍스트 ✅ 완료 (2025-11-27) +- [x] context/ItemFormContext.tsx 생성 (~80줄) +- [x] context/index.ts export 파일 생성 +- [x] hooks/useItemFormState.ts 생성 (~280줄) - 25+ useState 통합 +- [x] hooks/useBOMManagement.ts 생성 (~180줄) - BOM 라인 관리 +- [x] hooks/useBendingDetails.ts 생성 (~150줄) - 전개도 계산 +- [x] hooks/index.ts export 파일 생성 + +### Phase 6: 테스트 & 검증 +- [ ] 모든 품목 유형 등록 테스트 +- [ ] 수정 모드 테스트 +- [ ] 폼 검증 테스트 +- [ ] BOM 추가/삭제 테스트 \ No newline at end of file diff --git a/claudedocs/[REF-2025-11-26] item-master-hooks-refactoring.md b/claudedocs/item-master/[REF-2025-11-26] item-master-hooks-refactoring.md similarity index 91% rename from claudedocs/[REF-2025-11-26] item-master-hooks-refactoring.md rename to claudedocs/item-master/[REF-2025-11-26] item-master-hooks-refactoring.md index 921583e2..933e560b 100644 --- a/claudedocs/[REF-2025-11-26] item-master-hooks-refactoring.md +++ b/claudedocs/item-master/[REF-2025-11-26] item-master-hooks-refactoring.md @@ -424,4 +424,23 @@ ItemMasterDataManagement/ ├── utils/ ├── types.ts └── index.tsx (메인 컴포넌트, ~200줄 목표) -``` \ No newline at end of file +``` + +--- + +## 관련 파일 + +### 프론트엔드 +- `src/components/items/ItemMasterDataManagement.tsx` - 메인 컴포넌트 (리팩토링 대상) +- `src/components/items/ItemMasterDataManagement/hooks/index.ts` - 훅 export +- `src/components/items/ItemMasterDataManagement/hooks/usePageManagement.ts` - 페이지 관리 훅 +- `src/components/items/ItemMasterDataManagement/hooks/useSectionManagement.ts` - 섹션 관리 훅 +- `src/components/items/ItemMasterDataManagement/hooks/useFieldManagement.ts` - 필드 관리 훅 +- `src/components/items/ItemMasterDataManagement/hooks/useMasterFieldManagement.ts` - 마스터 필드 훅 +- `src/components/items/ItemMasterDataManagement/hooks/useTemplateManagement.ts` - 템플릿 관리 훅 +- `src/components/items/ItemMasterDataManagement/hooks/useAttributeManagement.ts` - 속성 관리 훅 +- `src/components/items/ItemMasterDataManagement/hooks/useTabManagement.ts` - 탭 관리 훅 +- `src/contexts/ItemMasterContext.tsx` - Context Provider + +### 참조 문서 +- `claudedocs/item-master/[NEXT-2025-11-26] item-master-api-pending-tasks.md` - API 연동 작업 체크리스트 \ No newline at end of file diff --git a/claudedocs/[REF] api-requirements-items.md b/claudedocs/item-master/[REF] api-requirements-items.md similarity index 97% rename from claudedocs/[REF] api-requirements-items.md rename to claudedocs/item-master/[REF] api-requirements-items.md index 87c582f3..0d080ccd 100644 --- a/claudedocs/[REF] api-requirements-items.md +++ b/claudedocs/item-master/[REF] api-requirements-items.md @@ -915,4 +915,23 @@ GET /api/files/{file_id}/download - 파일 저장소 구현 가이드 참조 추가 (`/downloads/file_storage_implementation_guide.md`) - 테넌트별 파일 저장 경로 구조 명시 - 파일명 처리 방식 명시 (난수 저장명 + 원본명 보존) - - 차단 확장자 목록 추가 (보안) \ No newline at end of file + - 차단 확장자 목록 추가 (보안) + +--- + +## 관련 파일 + +### 프론트엔드 +- `src/types/item.ts` - 품목 타입 정의 +- `src/lib/api/items.ts` - 품목 API 클라이언트 +- `src/components/items/ItemListClient.tsx` - 품목 목록 화면 +- `src/components/items/ItemDetailClient.tsx` - 품목 상세 화면 +- `src/components/items/ItemForm.tsx` - 품목 등록/수정 폼 +- `src/lib/utils/validation.ts` - Zod 검증 스키마 + +### 설정 파일 +- `.env.local` - API URL 및 키 설정 + +### 참조 문서 +- `claudedocs/item-master/[API-2025-11-23] item-master-backend-requirements.md` - 백엔드 요구사항 +- `claudedocs/item-master/[DESIGN-2025-11-24] item-management-dynamic-frontend.md` - 동적 화면 설계 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2f3eba12..e7840e58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "zustand": "^5.0.8" }, "devDependencies": { + "@playwright/test": "^1.57.0", "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", @@ -1092,6 +1093,22 @@ "node": ">=12.4.0" } }, + "node_modules/@playwright/test": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -5787,6 +5804,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -7509,6 +7541,38 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", diff --git a/package.json b/package.json index af4afbd3..3bcb3064 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,10 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "test:e2e:headed": "playwright test --headed" }, "dependencies": { "@hookform/resolvers": "^5.2.2", @@ -42,6 +45,7 @@ "zustand": "^5.0.8" }, "devDependencies": { + "@playwright/test": "^1.57.0", "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", diff --git a/src/app/api/proxy/[...path]/route.ts b/src/app/api/proxy/[...path]/route.ts index a38e8a49..5553f8eb 100644 --- a/src/app/api/proxy/[...path]/route.ts +++ b/src/app/api/proxy/[...path]/route.ts @@ -72,8 +72,77 @@ async function refreshAccessToken(refreshToken: string): Promise<{ } } +/** + * 백엔드 API 요청 실행 함수 + */ +async function executeBackendRequest( + url: URL, + method: string, + token: string | undefined, + body: string | undefined, + contentType: string +): Promise { + return fetch(url.toString(), { + method, + headers: { + 'Content-Type': contentType, + 'Accept': 'application/json', + 'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '', + 'Authorization': token ? `Bearer ${token}` : '', + }, + body, + }); +} + +/** + * 쿠키 생성 헬퍼 함수 + */ +function createTokenCookies(tokens: { accessToken?: string; refreshToken?: string; expiresIn?: number }) { + const cookies: string[] = []; + + if (tokens.accessToken) { + cookies.push([ + `access_token=${tokens.accessToken}`, + 'HttpOnly', + 'Secure', + 'SameSite=Strict', + 'Path=/', + `Max-Age=${tokens.expiresIn || 7200}`, + ].join('; ')); + } + + if (tokens.refreshToken) { + cookies.push([ + `refresh_token=${tokens.refreshToken}`, + 'HttpOnly', + 'Secure', + 'SameSite=Strict', + 'Path=/', + 'Max-Age=604800', // 7 days + ].join('; ')); + } + + return cookies; +} + +/** + * 쿠키 삭제 헬퍼 함수 (토큰 만료 시) + */ +function createClearTokenCookies(): string[] { + return [ + 'access_token=; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=0', + 'refresh_token=; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=0', + ]; +} + /** * Catch-all proxy handler for all HTTP methods + * + * 🔄 토큰 갱신 로직: + * 1. 현재 access_token으로 백엔드 요청 + * 2. 401 응답 시 → refresh_token으로 새 토큰 발급 + * 3. 새 토큰으로 원래 요청 재시도 + * 4. 재시도도 실패하면 → 쿠키 삭제 후 401 반환 */ async function proxyRequest( request: NextRequest, @@ -85,21 +154,8 @@ async function proxyRequest( let token = request.cookies.get('access_token')?.value; const refreshToken = request.cookies.get('refresh_token')?.value; - // 1-1. access_token이 없고 refresh_token이 있으면 자동 갱신 - let newTokens: { accessToken?: string; refreshToken?: string; expiresIn?: number } | null = null; - if (!token && refreshToken) { - console.log('🔄 [PROXY] No access_token, attempting refresh...'); - const refreshResult = await refreshAccessToken(refreshToken); - if (refreshResult.success && refreshResult.accessToken) { - token = refreshResult.accessToken; - newTokens = refreshResult; - } - } - // 2. 백엔드 URL 구성 const backendUrl = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/${params.path.join('/')}`; - - // 쿼리 파라미터 추가 const url = new URL(backendUrl); request.nextUrl.searchParams.forEach((value, key) => { url.searchParams.append(key, value); @@ -107,48 +163,63 @@ async function proxyRequest( // 3. 요청 바디 읽기 (POST, PUT, DELETE) let body: string | undefined; - if (['POST', 'PUT', 'DELETE'].includes(method)) { - // Content-Type에 따라 바디 처리 - const contentType = request.headers.get('content-type') || ''; + const contentType = request.headers.get('content-type') || 'application/json'; + if (['POST', 'PUT', 'DELETE'].includes(method)) { if (contentType.includes('application/json')) { body = await request.text(); - - // 🔍 디버깅: 전송 데이터 로그 - console.log('🔵 [PROXY DEBUG] Request Details:'); - console.log(' Method:', method); - console.log(' URL:', url.toString()); - console.log(' Body:', body); - console.log(' Token:', token ? `${token.substring(0, 20)}...` : 'null'); + console.log('🔵 [PROXY] Request:', method, url.toString()); + console.log('🔵 [PROXY] Request Body:', body); // 디버깅용 } else if (contentType.includes('multipart/form-data')) { - // FormData는 그대로 전달 - const formData = await request.formData(); - // FormData를 백엔드로 전달하기 위해 다시 변환 + // multipart는 formData로 처리해야 하지만, 현재는 지원하지 않음 + console.warn('🟡 [PROXY] multipart/form-data is not fully supported'); body = await request.text(); } + } else { + console.log('🔵 [PROXY] Request:', method, url.toString()); } // 4. 백엔드로 프록시 요청 - const backendResponse = await fetch(url.toString(), { - method, - headers: { - 'Content-Type': request.headers.get('content-type') || 'application/json', - 'Accept': 'application/json', - 'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '', - 'Authorization': token ? `Bearer ${token}` : '', - }, - body, - }); + let backendResponse = await executeBackendRequest(url, method, token, body, contentType); + let newTokens: { accessToken?: string; refreshToken?: string; expiresIn?: number } | null = null; - // 5. 응답 데이터 읽기 + // 5. 🔄 401 응답 시 토큰 갱신 후 재시도 + if (backendResponse.status === 401 && refreshToken) { + console.log('🔄 [PROXY] Got 401, attempting token refresh...'); + + const refreshResult = await refreshAccessToken(refreshToken); + + if (refreshResult.success && refreshResult.accessToken) { + console.log('✅ [PROXY] Token refreshed, retrying original request...'); + + // 새 토큰으로 원래 요청 재시도 + token = refreshResult.accessToken; + newTokens = refreshResult; + backendResponse = await executeBackendRequest(url, method, token, body, contentType); + + console.log('🔵 [PROXY] Retry response status:', backendResponse.status); + } else { + // 리프레시 실패 → 쿠키 삭제하고 401 반환 + console.warn('🔴 [PROXY] Token refresh failed, clearing cookies...'); + + const clearResponse = NextResponse.json( + { error: 'Authentication failed', needsReauth: true }, + { status: 401 } + ); + + createClearTokenCookies().forEach(cookie => { + clearResponse.headers.append('Set-Cookie', cookie); + }); + + return clearResponse; + } + } + + // 6. 응답 데이터 읽기 const responseData = await backendResponse.text(); + console.log('🔵 [PROXY] Response status:', backendResponse.status); - // 🔍 디버깅: 백엔드 응답 로그 - console.log('🔵 [PROXY DEBUG] Backend Response:'); - console.log(' Status:', backendResponse.status); - console.log(' Response:', responseData.substring(0, 500)); // 처음 500자만 - - // 6. 클라이언트로 응답 전달 + // 7. 클라이언트로 응답 전달 const clientResponse = new NextResponse(responseData, { status: backendResponse.status, headers: { @@ -156,31 +227,11 @@ async function proxyRequest( }, }); - // 6-1. 토큰이 갱신되었으면 새 쿠키 설정 + // 8. 토큰이 갱신되었으면 새 쿠키 설정 if (newTokens && newTokens.accessToken) { - const accessTokenCookie = [ - `access_token=${newTokens.accessToken}`, - 'HttpOnly', - 'Secure', - 'SameSite=Strict', - 'Path=/', - `Max-Age=${newTokens.expiresIn || 7200}`, - ].join('; '); - - clientResponse.headers.append('Set-Cookie', accessTokenCookie); - - if (newTokens.refreshToken) { - const refreshTokenCookie = [ - `refresh_token=${newTokens.refreshToken}`, - 'HttpOnly', - 'Secure', - 'SameSite=Strict', - 'Path=/', - 'Max-Age=604800', // 7 days - ].join('; '); - clientResponse.headers.append('Set-Cookie', refreshTokenCookie); - } - + createTokenCookies(newTokens).forEach(cookie => { + clientResponse.headers.append('Set-Cookie', cookie); + }); console.log('🍪 [PROXY] New tokens set in cookies'); } diff --git a/src/components/auth/LoginPage.tsx b/src/components/auth/LoginPage.tsx index e5e18a7f..b1762bd2 100644 --- a/src/components/auth/LoginPage.tsx +++ b/src/components/auth/LoginPage.tsx @@ -27,31 +27,45 @@ export function LoginPage() { const [showPassword, setShowPassword] = useState(false); const [rememberMe, setRememberMe] = useState(false); const [error, setError] = useState(""); - const [isChecking, setIsChecking] = useState(true); + // 2025-11-27: isChecking 상태 제거 - 미들웨어에서 인증 체크하므로 불필요 + // const [isChecking, setIsChecking] = useState(true); const [isLoggingIn, setIsLoggingIn] = useState(false); // ✅ 로그인 진행 중 상태 - // 이미 로그인된 상태인지 확인 (페이지 로드 시, 뒤로가기 시) - useEffect(() => { - const checkAuth = async () => { - try { - // 🔵 Next.js 내부 API - 쿠키에서 토큰 확인 (PHP 호출 X, 성능 최적화) - const response = await fetch('/api/auth/check'); - - if (response.ok) { - // 이미 로그인됨 → 대시보드로 리다이렉트 (replace로 히스토리에서 제거) - router.replace('/dashboard'); - return; - } - // 인증 안됨 (401) → 현재 페이지 유지 - } catch { - // API 호출 실패 → 현재 페이지 유지 - } finally { - setIsChecking(false); - } - }; - - checkAuth(); - }, [router]); + /** + * 🚫 2025-11-27: auth/check API 호출 제거 + * + * [이전 동작] + * - 로그인 페이지 진입 시 /api/auth/check 호출 + * - 이미 로그인된 사용자를 대시보드로 리다이렉트 + * + * [제거 이유] + * 1. 미들웨어(middleware.ts)에서 이미 동일한 처리를 함 + * - guestOnlyRoutes(/login, /signup)에서 인증된 사용자 → /dashboard 리다이렉트 + * 2. 401 응답이 Network 탭에 에러로 표시되어 백엔드 개발자 혼란 유발 + * 3. 불필요한 API 호출로 인한 성능 저하 + * + * [대체 방안] + * - 미들웨어가 서버 사이드에서 쿠키 체크 후 리다이렉트 처리 + * - 클라이언트에서 추가 API 호출 불필요 + * + * @see middleware.ts - isGuestOnlyRoute(), checkAuthentication() + */ + // useEffect(() => { + // const checkAuth = async () => { + // try { + // const response = await fetch('/api/auth/check'); + // if (response.ok) { + // router.replace('/dashboard'); + // return; + // } + // } catch { + // // API 호출 실패 → 현재 페이지 유지 + // } finally { + // setIsChecking(false); + // } + // }; + // checkAuth(); + // }, [router]); const handleLogin = async () => { // ✅ 중복 요청 방지 @@ -137,17 +151,17 @@ export function LoginPage() { }; - // 인증 체크 중일 때는 로딩 표시 - if (isChecking) { - return ( -
-
-
-

Loading...

-
-
- ); - } + // 2025-11-27: isChecking 로딩 UI 제거 - 미들웨어에서 처리하므로 불필요 + // if (isChecking) { + // return ( + //
+ //
+ //
+ //

Loading...

+ //
+ //
+ // ); + // } return (
diff --git a/src/components/common/ServerErrorPage.tsx b/src/components/common/ServerErrorPage.tsx new file mode 100644 index 00000000..48e68081 --- /dev/null +++ b/src/components/common/ServerErrorPage.tsx @@ -0,0 +1,140 @@ +'use client'; + +import { usePathname } from 'next/navigation'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { + ServerCrash, + RefreshCw, + Home, + ArrowLeft, + MessageCircleQuestion, +} from 'lucide-react'; +import { useRouter } from 'next/navigation'; + +interface ServerErrorPageProps { + title?: string; + message?: string; + errorCode?: string | number; + onRetry?: () => void; + showBackButton?: boolean; + showHomeButton?: boolean; + showContactInfo?: boolean; + contactEmail?: string; +} + +export function ServerErrorPage({ + title = '서버 오류가 발생했습니다', + message = '일시적인 문제가 발생했습니다. 잠시 후 다시 시도해 주세요.', + errorCode, + onRetry, + showBackButton = true, + showHomeButton = true, + showContactInfo = true, + contactEmail = 'admin@company.com', +}: ServerErrorPageProps) { + const router = useRouter(); + const pathname = usePathname(); + + const handleRetry = () => { + if (onRetry) { + onRetry(); + } else { + window.location.reload(); + } + }; + + return ( +
+ + +
+
+
+ +
+
+ ! +
+
+
+ + {title} + + {errorCode && ( +

+ 오류 코드: {errorCode} +

+ )} +
+ + +
+

+ {message} +

+

+ 문제가 지속되면 관리자에게 문의해 주세요. +

+
+ +
+ + + {showBackButton && ( + + )} + + {showHomeButton && ( + + )} +
+ + {showContactInfo && ( +
+
+ + + 문제가 계속되면{' '} + + 관리자에게 문의 + + 해 주세요. + +
+ {pathname && ( +

+ 발생 위치: {pathname} +

+ )} +
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/items/ItemForm/BOMSection.tsx b/src/components/items/ItemForm/BOMSection.tsx new file mode 100644 index 00000000..fa23c878 --- /dev/null +++ b/src/components/items/ItemForm/BOMSection.tsx @@ -0,0 +1,365 @@ +/** + * BOMSection - 부품 구성 (BOM) 섹션 + */ + +import { Fragment } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Badge } from '@/components/ui/badge'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { + Popover, + PopoverAnchor, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover'; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '@/components/ui/command'; +import { Check, Package, Plus, Search, Trash2 } from 'lucide-react'; +import type { BOMLine } from '@/types/item'; +import type { BOMSearchState } from './types'; + +export interface BOMSectionProps { + bomLines: BOMLine[]; + setBomLines: (lines: BOMLine[]) => void; + bomSearchStates: Record; + setBomSearchStates: (states: Record) => void; + isSubmitting: boolean; +} + +export default function BOMSection({ + bomLines, + setBomLines, + bomSearchStates, + setBomSearchStates, + isSubmitting, +}: BOMSectionProps) { + return ( + + +
+ 부품 구성 (BOM) + +
+
+ + {bomLines.length === 0 ? ( +
+ +

+ 아직 부품 구성이 추가되지 않았습니다 +

+

+ 품목의 구성 부품, 원자재, 부자재를 추가할 수 있습니다 +

+
+ ) : ( +
+ + + + 품목코드 / 품목명 입력 + 품목명 + 규격 + 재질 + 수량 + 단위 + 단가 + 비고 + 삭제 + + + + {bomLines.map((line) => { + // 각 라인별 검색 상태 가져오기 + const searchState = bomSearchStates[line.id] || { searchValue: '', isOpen: false }; + const searchValue = searchState.searchValue; + const searchOpen = searchState.isOpen; + + // TODO: 실제 itemMasters 데이터로 교체 필요 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const availableItems: any[] = []; + + return ( + + + +
+ { + setBomSearchStates({ + ...bomSearchStates, + [line.id]: { ...searchState, isOpen: open }, + }); + }} + > +
+ + { + // 단순 입력만 처리 (서버 자동완성 준비) + setBomSearchStates({ + ...bomSearchStates, + [line.id]: { ...searchState, searchValue: e.target.value }, + }); + }} + className="w-full" + readOnly={!!line.childItemCode} + /> + + {line.childItemCode && ( +
+ +
+ )} +
+ + + + + + { + setBomSearchStates({ + ...bomSearchStates, + [line.id]: { ...searchState, searchValue: value }, + }); + }} + /> + + 검색 결과가 없습니다. + + {availableItems.map((item) => ( + { + // TODO: 품목 선택 시 데이터 채우기 로직 + const isBendingPart = item.partType === 'BENDING'; + + setBomLines( + bomLines.map((l) => + l.id === line.id + ? { + ...l, + childItemCode: item.itemCode || '', + childItemName: item.itemName || '', + specification: item.specification || '', + material: item.material || '', + unit: item.unit || 'EA', + unitPrice: 0, // TODO: pricing에서 가져오기 + isBending: isBendingPart, + bendingDiagram: isBendingPart + ? item.bendingDiagram + : undefined, + } + : l + ) + ); + setBomSearchStates({ + ...bomSearchStates, + [line.id]: { searchValue: '', isOpen: false }, + }); + }} + className="cursor-pointer" + > +
+
+
+ + {item.itemCode} + + {item.itemName} + {item.specification && ( + + ({item.specification}) + + )} +
+
+ + {item.unit} + +
+
+ ))} +
+
+
+
+
+
+
+ {line.childItemName || '-'} + + {line.specification || '-'} + + + { + setBomLines( + bomLines.map((l) => + l.id === line.id ? { ...l, material: e.target.value } : l + ) + ); + }} + placeholder="재질" + className="w-full text-xs" + /> + + + { + setBomLines( + bomLines.map((l) => + l.id === line.id ? { ...l, quantity: Number(e.target.value) } : l + ) + ); + }} + min="0" + step="0.01" + className="w-full" + /> + + + {line.unit} + + + { + setBomLines( + bomLines.map((l) => + l.id === line.id ? { ...l, unitPrice: Number(e.target.value) } : l + ) + ); + }} + min="0" + className="w-full text-right" + /> + + + { + setBomLines( + bomLines.map((l) => + l.id === line.id ? { ...l, note: e.target.value } : l + ) + ); + }} + placeholder="비고" + className="w-full text-xs" + /> + + + + +
+ + {/* 절곡품인 경우 전개도 정보 표시 */} + {line.isBending && line.bendingDiagram && ( + + +
+
+ + 절곡품 전개도 정보 + +
+ + {/* 전개도 이미지 */} +
+ +
+ 절곡 전개도 +
+
+
+
+
+ )} +
+ ); + })} +
+
+
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/src/components/items/ItemForm/BendingDiagramSection.tsx b/src/components/items/ItemForm/BendingDiagramSection.tsx new file mode 100644 index 00000000..1867a8c2 --- /dev/null +++ b/src/components/items/ItemForm/BendingDiagramSection.tsx @@ -0,0 +1,367 @@ +/** + * BendingDiagramSection - 절곡품/조립품 전개도 섹션 + */ + +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { FileImage, Plus, Trash2, X } from 'lucide-react'; +import type { BendingDetail } from '@/types/item'; +import type { UseFormSetValue } from 'react-hook-form'; +import type { CreateItemFormData } from '@/lib/utils/validation'; + +export interface BendingDiagramSectionProps { + selectedPartType: string; + bendingDiagramInputMethod: 'file' | 'drawing'; + setBendingDiagramInputMethod: (method: 'file' | 'drawing') => void; + bendingDiagram: string; + setBendingDiagram: (diagram: string) => void; + setBendingDiagramFile: (file: File | null) => void; + setIsDrawingOpen: (open: boolean) => void; + bendingDetails: BendingDetail[]; + setBendingDetails: (details: BendingDetail[]) => void; + setWidthSum: (sum: string) => void; + setValue: UseFormSetValue; + isSubmitting: boolean; +} + +export default function BendingDiagramSection({ + selectedPartType, + bendingDiagramInputMethod, + setBendingDiagramInputMethod, + bendingDiagram, + setBendingDiagram, + setBendingDiagramFile, + setIsDrawingOpen, + bendingDetails, + setBendingDetails, + setWidthSum, + setValue, + isSubmitting, +}: BendingDiagramSectionProps) { + // 폭 합계 업데이트 헬퍼 + const updateWidthSum = (details: BendingDetail[]) => { + const totalSum = details.reduce((acc, d) => { + const calc = d.input + d.elongation; + return acc + calc; + }, 0); + setWidthSum(totalSum.toString()); + setValue('length', totalSum.toString()); + }; + + return ( + + + + + {selectedPartType === 'ASSEMBLY' ? '조립품 전개도 (바라시)' : '절곡품 전개도 (바라시)'} + + + + {/* 입력방식 선택 */} +
+ +
+
+ setBendingDiagramInputMethod(e.target.value as 'file')} + className="h-4 w-4" + /> + +
+
+ setBendingDiagramInputMethod(e.target.value as 'drawing')} + className="h-4 w-4" + /> + +
+
+

+ * 전개도 이미지를 파일로 업로드하거나 직접 그릴 수 있습니다 +

+
+ + {/* 파일 선택 방식 */} + {bendingDiagramInputMethod === 'file' && ( +
+ +
+ { + const file = e.target.files?.[0]; + if (file && typeof window !== 'undefined') { + setBendingDiagramFile(file); + const reader = new window.FileReader(); + reader.onloadend = () => { + setBendingDiagram(reader.result as string); + }; + reader.readAsDataURL(file); + } + }} + disabled={isSubmitting} + /> +

+ * {selectedPartType === 'ASSEMBLY' + ? '조립품 전개도 이미지를 업로드하세요(JPG, PNG, PDF 등)' + : '절곡품 전개도 이미지를 업로드하세요(JPG, PNG, PDF 등)'} +

+
+ + {/* 전개도 이미지 미리보기 */} + {bendingDiagram && ( +
+
+

미리보기

+ +
+ 전개도 미리보기 +
+ )} +
+ )} + + {/* 드로잉 방식 */} + {bendingDiagramInputMethod === 'drawing' && ( +
+ +

+ * 캔버스에서 직접 전개도를 그릴 수 있습니다 +

+ + {/* 전개도 미리보기 */} + {bendingDiagram && ( +
+
+

미리보기

+ +
+ 전개도 미리보기 +
+ )} +
+ )} + + {/* 전개도 상세 입력 (치수 계산) - BENDING 전용 */} + {selectedPartType === 'BENDING' && ( +
+
+ + +
+ + {bendingDetails.length > 0 ? ( +
+ + + + + + + + + + + + + + {bendingDetails.map((detail, index) => { + const calculated = detail.input + detail.elongation; + + return ( + + + + + + + + + + ); + })} + + + + + + + + +
번호입력값연신율계산값음영A각삭제
{detail.no} + { + const newDetails = [...bendingDetails]; + const value = e.target.value === '' ? 0 : parseFloat(e.target.value); + newDetails[index] = { + ...detail, + input: isNaN(value) ? 0 : value, + }; + setBendingDetails(newDetails); + updateWidthSum(newDetails); + }} + className="h-8 text-center" + /> + + { + const newDetails = [...bendingDetails]; + const value = e.target.value === '' ? -1 : parseFloat(e.target.value); + newDetails[index] = { + ...detail, + elongation: isNaN(value) ? -1 : value, + }; + setBendingDetails(newDetails); + updateWidthSum(newDetails); + }} + className="h-8 text-center" + /> + + {calculated.toFixed(1)} + + { + const newDetails = [...bendingDetails]; + newDetails[index] = { + ...detail, + shaded: e.target.checked, + }; + setBendingDetails(newDetails); + }} + className="h-4 w-4" + /> + + { + const newDetails = [...bendingDetails]; + newDetails[index] = { + ...detail, + aAngle: parseFloat(e.target.value) || undefined, + }; + setBendingDetails(newDetails); + }} + className="h-8 text-center" + placeholder="각도" + /> + + +
+ 폭 합계: + + {bendingDetails.length > 0 + ? bendingDetails.reduce((acc, d) => { + const calc = d.input + d.elongation; + return acc + calc; + }, 0).toFixed(1) + : '0.0'} mm +
+
+ ) : ( +
+ 전개도 상세 데이터가 없습니다. "행 추가" 버튼을 클릭하여 추가하세요. +
+ )} + +

+ * 전개도의 각 구간별 치수를 입력하여 정확한 전개 길이를 계산할 수 있습니다. +

+
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/src/components/items/ItemForm/FormHeader.tsx b/src/components/items/ItemForm/FormHeader.tsx new file mode 100644 index 00000000..aa79adda --- /dev/null +++ b/src/components/items/ItemForm/FormHeader.tsx @@ -0,0 +1,62 @@ +/** + * FormHeader - 품목 폼 헤더 컴포넌트 + */ + +import { Button } from '@/components/ui/button'; +import { Package, Save, X } from 'lucide-react'; +import type { ItemType } from '@/types/item'; + +interface FormHeaderProps { + mode: 'create' | 'edit'; + selectedItemType: ItemType | ''; + isSubmitting: boolean; + onCancel: () => void; +} + +export default function FormHeader({ + mode, + selectedItemType, + isSubmitting, + onCancel, +}: FormHeaderProps) { + return ( +
+
+
+ +
+
+

+ {mode === 'create' ? '품목 등록' : '품목 수정'} +

+

+ 품목 정보를 입력하세요 +

+
+
+ +
+ + +
+
+ ); +} \ No newline at end of file diff --git a/src/components/items/ItemForm/ValidationAlert.tsx b/src/components/items/ItemForm/ValidationAlert.tsx new file mode 100644 index 00000000..e00709a5 --- /dev/null +++ b/src/components/items/ItemForm/ValidationAlert.tsx @@ -0,0 +1,50 @@ +/** + * ValidationAlert - 폼 검증 에러 표시 컴포넌트 + */ + +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { FIELD_NAME_MAP } from './constants'; +import type { FieldErrors } from 'react-hook-form'; +import type { CreateItemFormData } from '@/lib/utils/validation'; + +interface ValidationAlertProps { + errors: FieldErrors; +} + +export default function ValidationAlert({ errors }: ValidationAlertProps) { + const errorCount = Object.keys(errors).length; + + if (errorCount === 0) { + return null; + } + + return ( + + +
+ ⚠️ +
+ + 입력 내용을 확인해주세요 ({errorCount}개 오류) + +
    + {Object.entries(errors).map(([field, error]) => { + const fieldName = FIELD_NAME_MAP[field] || field; + const errorMessage = error?.message || '입력 오류'; + + return ( +
  • + + + {fieldName}: {errorMessage} + +
  • + ); + })} +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/items/ItemForm/constants.ts b/src/components/items/ItemForm/constants.ts new file mode 100644 index 00000000..76d5e18e --- /dev/null +++ b/src/components/items/ItemForm/constants.ts @@ -0,0 +1,93 @@ +/** + * ItemForm 상수 정의 + */ + +// 부품 유형별 분류 체계 +export const PART_TYPE_CATEGORIES = { + ASSEMBLY: { + label: "조립 부품 (Assembly Part)", + categories: [ + { value: "guide_rail", label: "가이드레일", code: "R" }, + { value: "case", label: "케이스", code: "C" }, + { value: "bottom_finish", label: "하단마감재", code: "B" }, + ] + }, + BENDING: { + label: "절곡 부품 (Bending Part)", + categories: [ + { value: "guide_rail_wall", label: "가이드레일(벽면형)", code: "R" }, + { value: "guide_rail_side", label: "가이드레일(측면형)", code: "S" }, + { value: "case", label: "케이스", code: "C" }, + { value: "bottom_finish_screen", label: "하단마감재(스크린)", code: "B" }, + { value: "bottom_finish_steel", label: "하단마감재(철재)", code: "T" }, + { value: "l_bar", label: "L-Bar", code: "L" }, + { value: "smoke_barrier", label: "연기차단재", code: "G" }, + ] + }, + PURCHASED: { + label: "구매 부품 (Purchased Part)", + categories: [ + { value: "electric_opener", label: "전동개폐기", code: "E" }, + { value: "motor", label: "모터", code: "M" }, + { value: "chain", label: "체인", code: "CH" }, + ] + } +} as const; + +// 부품 분류별 종류 옵션 +export const PART_ITEM_NAMES: Record> = { + guide_rail_wall: [ + { value: "RM", label: "분체", code: "M" }, + { value: "RT", label: "분체(철재)", code: "T" }, + { value: "RC", label: "C형", code: "C" }, + { value: "RD", label: "D형", code: "D" }, + { value: "RS", label: "SUS 마감재", code: "S" }, + { value: "RM2", label: "분체티딩", code: "M" }, + ], + guide_rail_side: [ + { value: "SC", label: "C형", code: "C" }, + { value: "SD", label: "D형", code: "D" }, + { value: "SS", label: "SUS 마감재①", code: "S" }, + { value: "SU", label: "SUS 마감재②", code: "U" }, + { value: "SF", label: "전면부", code: "F" }, + { value: "SP", label: "점검구", code: "P" }, + ], + case: [ + { value: "CF", label: "전면부", code: "F" }, + { value: "CP", label: "점검구", code: "P" }, + { value: "CL", label: "린텔부", code: "L" }, + { value: "CB", label: "후면코너부", code: "B" }, + ], + bottom_finish_screen: [ + { value: "BS", label: "SUS", code: "S" }, + { value: "BE", label: "EGI", code: "E" }, + ], + bottom_finish_steel: [ + { value: "TS", label: "SUS", code: "S" }, + { value: "TE", label: "EGI", code: "E" }, + ], + l_bar: [ + { value: "LA", label: "스크린용", code: "A" }, + ], + smoke_barrier: [ + { value: "GI", label: "화이바원단(W50)", code: "I" }, + { value: "GI2", label: "화이바원단(W80)", code: "I" }, + ], +}; + +// 필드명 한글 매핑 (에러 메시지용) +export const FIELD_NAME_MAP: Record = { + 'productName': '상품명', + 'itemName': '품목명', + 'itemType': '품목 유형', + 'partType': '부품 유형', + 'category1': '품목명', + 'material': '재질', + 'length': '폭 합계', + 'bendingLength': '모양&길이', + 'sideSpecWidth': '측면 규격 (가로)', + 'sideSpecHeight': '측면 규격 (세로)', + 'assemblyLength': '길이', + 'specification': '규격', + 'unit': '단위', +}; \ No newline at end of file diff --git a/src/components/items/ItemForm/context/ItemFormContext.tsx b/src/components/items/ItemForm/context/ItemFormContext.tsx new file mode 100644 index 00000000..9bef3de0 --- /dev/null +++ b/src/components/items/ItemForm/context/ItemFormContext.tsx @@ -0,0 +1,77 @@ +/** + * ItemFormContext - 품목 폼 상태 컨텍스트 + * + * 하위 컴포넌트에서 공유되는 폼 상태 관리 + */ + +'use client'; + +import { createContext, useContext, ReactNode } from 'react'; +import type { UseFormReturn } from 'react-hook-form'; +import type { CreateItemFormData } from '@/lib/utils/validation'; +import type { ItemType } from '@/types/item'; +import type { UseItemFormStateReturn } from '../hooks/useItemFormState'; +import type { UseBOMManagementReturn } from '../hooks/useBOMManagement'; +import type { UseBendingDetailsReturn } from '../hooks/useBendingDetails'; + +export interface ItemFormContextType { + // React Hook Form + form: UseFormReturn; + + // 모드 + mode: 'create' | 'edit'; + + // 품목 유형 + selectedItemType: ItemType | ''; + setSelectedItemType: (type: ItemType | '') => void; + + // 부품 유형 + selectedPartType: string; + setSelectedPartType: (type: string) => void; + + // 상태 훅 + formState: UseItemFormStateReturn; + bomManagement: UseBOMManagementReturn; + bendingDetails: UseBendingDetailsReturn; + + // 품목코드 생성 + generateItemCode: () => string; + + // 품목 유형 변경 핸들러 + handleItemTypeChange: (type: ItemType) => void; + + // 제출 상태 + isSubmitting: boolean; +} + +const ItemFormContext = createContext(null); + +export interface ItemFormProviderProps { + children: ReactNode; + value: ItemFormContextType; +} + +export function ItemFormProvider({ children, value }: ItemFormProviderProps) { + return ( + + {children} + + ); +} + +export function useItemFormContext(): ItemFormContextType { + const context = useContext(ItemFormContext); + if (!context) { + throw new Error('useItemFormContext must be used within an ItemFormProvider'); + } + return context; +} + +/** + * 선택적으로 컨텍스트 사용 (컨텍스트가 없어도 에러 안 남) + */ +export function useOptionalItemFormContext(): ItemFormContextType | null { + return useContext(ItemFormContext); +} + +export default ItemFormContext; \ No newline at end of file diff --git a/src/components/items/ItemForm/context/index.ts b/src/components/items/ItemForm/context/index.ts new file mode 100644 index 00000000..00515ec3 --- /dev/null +++ b/src/components/items/ItemForm/context/index.ts @@ -0,0 +1,12 @@ +/** + * 품목 폼 컨텍스트 export + */ + +export { + ItemFormProvider, + useItemFormContext, + useOptionalItemFormContext, + default as ItemFormContext, +} from './ItemFormContext'; + +export type { ItemFormContextType, ItemFormProviderProps } from './ItemFormContext'; \ No newline at end of file diff --git a/src/components/items/ItemForm/forms/MaterialForm.tsx b/src/components/items/ItemForm/forms/MaterialForm.tsx new file mode 100644 index 00000000..4e1504fe --- /dev/null +++ b/src/components/items/ItemForm/forms/MaterialForm.tsx @@ -0,0 +1,354 @@ +/** + * 원자재/부자재/소모품 (RM/SM/CS) 폼 컴포넌트 + */ + +import { Label } from '@/components/ui/label'; +import { Input } from '@/components/ui/input'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import type { ItemType } from '@/types/item'; +import type { UseFormRegister, UseFormSetValue, UseFormGetValues, FieldErrors } from 'react-hook-form'; +import type { CreateItemFormData } from '@/lib/utils/validation'; + +interface MaterialFormProps { + selectedItemType: ItemType; + itemName: string; + setItemName: (value: string) => void; + selectedSpecification: string; + setSelectedSpecification: (value: string) => void; + materialStatus: string; + setMaterialStatus: (value: string) => void; + selectedUnit: string; + setSelectedUnit: (value: string) => void; + register: UseFormRegister; + setValue: UseFormSetValue; + getValues: UseFormGetValues; + errors: FieldErrors; +} + +export default function MaterialForm({ + selectedItemType, + itemName, + setItemName, + selectedSpecification, + setSelectedSpecification, + materialStatus, + setMaterialStatus, + selectedUnit, + setSelectedUnit, + register, + setValue, + getValues, + errors, +}: MaterialFormProps) { + return ( + <> +
+ + {/* 원자재/부자재는 목록에서 선택, 소모품은 직접 입력 */} + {selectedItemType === 'RM' ? ( + <> + + {errors.itemName && ( +

+ {errors.itemName.message} +

+ )} + + ) : selectedItemType === 'SM' ? ( + <> + + {errors.itemName && ( +

+ {errors.itemName.message} +

+ )} + + ) : ( + <> + { + const newName = e.target.value; + setItemName(newName); + setValue('itemName', newName); + // 품목코드 자동생성 + const spec = getValues('specification') || ''; + setValue('itemCode', spec ? `${newName}-${spec}` : newName); + }} + className={errors.itemName ? 'border-red-500' : ''} + /> + {errors.itemName && ( +

+ {errors.itemName.message} +

+ )} + + )} +
+ + {/* 규격(사양) */} + {selectedItemType === 'CS' ? ( +
+ + { + // 품목코드 자동생성 + const spec = e.target.value; + const name = itemName || ''; + setValue('itemCode', name && spec ? `${name}-${spec}` : name); + } + })} + className={errors.specification ? 'border-red-500' : ''} + /> + {errors.specification && ( +

+ {errors.specification.message} +

+ )} +
+ ) : ( +
+ + + {errors.specification && ( +

+ {errors.specification.message} +

+ )} + {!errors.specification && ( +

+ * 규격은 품목명 선택 시 자동으로 필터링됩니다 +

+ )} +
+ )} + + {/* 품목코드 (자동생성) */} +
+ + { + const name = itemName || ''; + const spec = getValues('specification') || ''; + return spec ? `${name}-${spec}` : name; + })()} + disabled + className="bg-muted text-muted-foreground" + /> +

+ * 품목코드는 '품목명-규격' 형식으로 자동 생성됩니다 +

+
+ + {/* 품목 상태 (RM/SM만) */} + {(selectedItemType === 'RM' || selectedItemType === 'SM') && ( +
+ + +

+ * 비활성 시 품목 사용이 제한됩니다 +

+
+ )} + + {/* 단위 (RM/SM/CS 공통) */} +
+ + + {errors.unit && ( +

+ {errors.unit.message} +

+ )} +
+ + ); +} \ No newline at end of file diff --git a/src/components/items/ItemForm/forms/PartForm.tsx b/src/components/items/ItemForm/forms/PartForm.tsx new file mode 100644 index 00000000..239cd03f --- /dev/null +++ b/src/components/items/ItemForm/forms/PartForm.tsx @@ -0,0 +1,273 @@ +/** + * 부품 (PT) 폼 컴포넌트 + * - ASSEMBLY (조립 부품) + * - BENDING (절곡 부품) + * - PURCHASED (구매 부품) + */ + +import { Label } from '@/components/ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import type { UseFormRegister, UseFormSetValue, UseFormClearErrors, FieldErrors } from 'react-hook-form'; +import type { CreateItemFormData } from '@/lib/utils/validation'; +import { AssemblyPartForm, BendingPartForm, PurchasedPartForm } from './parts'; + +export interface PartFormProps { + // Part Type + selectedPartType: string; + setSelectedPartType: (value: string) => void; + // Category + selectedCategory1: string; + setSelectedCategory1: (value: string) => void; + selectedInstallationType: string; + setSelectedInstallationType: (value: string) => void; + // ASSEMBLY + sideSpecWidth: string; + setSideSpecWidth: (value: string) => void; + sideSpecHeight: string; + setSideSpecHeight: (value: string) => void; + assemblyLength: string; + setAssemblyLength: (value: string) => void; + assemblyUnit: string; + setAssemblyUnit: (value: string) => void; + // BENDING + selectedBendingItemType: string; + setSelectedBendingItemType: (value: string) => void; + material: string; + setMaterial: (value: string) => void; + widthSum: string; + setWidthSum: (value: string) => void; + bendingLength: string; + setBendingLength: (value: string) => void; + partUnit: string; + setPartUnit: (value: string) => void; + bendingDetailsLength: number; + // PURCHASED + electricOpenerPower: string; + setElectricOpenerPower: (value: string) => void; + electricOpenerCapacity: string; + setElectricOpenerCapacity: (value: string) => void; + motorVoltage: string; + setMotorVoltage: (value: string) => void; + chainSpec: string; + setChainSpec: (value: string) => void; + // Common + partStatus: string; + setPartStatus: (value: string) => void; + needsBOM: boolean; + setNeedsBOM: (value: boolean) => void; + // Item Code Generator + generateItemCode: () => string; + // Form + register: UseFormRegister; + setValue: UseFormSetValue; + clearErrors: UseFormClearErrors; + errors: FieldErrors; +} + +export default function PartForm({ + selectedPartType, + setSelectedPartType, + selectedCategory1, + setSelectedCategory1, + selectedInstallationType, + setSelectedInstallationType, + sideSpecWidth, + setSideSpecWidth, + sideSpecHeight, + setSideSpecHeight, + assemblyLength, + setAssemblyLength, + assemblyUnit, + setAssemblyUnit, + selectedBendingItemType, + setSelectedBendingItemType, + material, + setMaterial, + widthSum, + setWidthSum, + bendingLength, + setBendingLength, + partUnit, + setPartUnit, + bendingDetailsLength, + electricOpenerPower, + setElectricOpenerPower, + electricOpenerCapacity, + setElectricOpenerCapacity, + motorVoltage, + setMotorVoltage, + chainSpec, + setChainSpec, + partStatus, + setPartStatus, + needsBOM, + setNeedsBOM, + generateItemCode, + register, + setValue, + clearErrors, + errors, +}: PartFormProps) { + // 부품 유형 변경 시 필드 초기화 핸들러 + const handlePartTypeChange = (value: string) => { + setSelectedPartType(value); + setValue('partType', value); + clearErrors('partType'); + + // 공통 필드 초기화 + setSelectedCategory1(''); + setValue('category1', undefined); + setPartUnit('EA'); + setValue('unit', 'EA'); + + // ASSEMBLY 부품 전용 필드 초기화 + setSelectedInstallationType(''); + setValue('installationType', undefined); + setSideSpecWidth(''); + setValue('sideSpecWidth', ''); + setSideSpecHeight(''); + setValue('sideSpecHeight', ''); + setAssemblyLength(''); + setValue('assemblyLength', ''); + setAssemblyUnit('EA'); + + // BENDING 부품 전용 필드 초기화 + setSelectedBendingItemType(''); + setValue('category2', undefined); + setMaterial(''); + setValue('material', ''); + setWidthSum(''); + setValue('length', ''); + setBendingLength(''); + setValue('bendingLength', ''); + + // PURCHASED 부품 전용 필드 초기화 + setElectricOpenerPower(''); + setValue('electricOpenerPower', ''); + setElectricOpenerCapacity(''); + setValue('electricOpenerCapacity', ''); + setMotorVoltage(''); + setValue('motorVoltage', ''); + setChainSpec(''); + setValue('chainSpec', ''); + + // BOM 설정 (절곡 부품은 BOM 없음, 조립 부품은 BOM 기본 true) + setNeedsBOM(value === 'BENDING' ? false : value === 'ASSEMBLY' ? true : needsBOM); + }; + + return ( + <> + {/* 부품 유형 선택 - 항상 표시 */} +
+ + + {errors.partType && ( +

+ {errors.partType.message} +

+ )} + {!errors.partType && selectedPartType === 'BENDING' && ( +

+ * 절곡 부품은 전개도(바라시)만 있으며, 부품 구성(BOM)은 사용하지 않습니다. +

+ )} +
+ + {/* ASSEMBLY 부품인 경우 */} + {selectedPartType === 'ASSEMBLY' && ( + + )} + + {/* BENDING 부품인 경우 */} + {selectedPartType === 'BENDING' && ( + + )} + + {/* PURCHASED 부품인 경우 */} + {selectedPartType === 'PURCHASED' && ( + + )} + + ); +} \ No newline at end of file diff --git a/src/components/items/ItemForm/forms/ProductForm.tsx b/src/components/items/ItemForm/forms/ProductForm.tsx new file mode 100644 index 00000000..6fa92a02 --- /dev/null +++ b/src/components/items/ItemForm/forms/ProductForm.tsx @@ -0,0 +1,337 @@ +/** + * 제품 (FG) 폼 컴포넌트 + */ + +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Textarea } from '@/components/ui/textarea'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { X } from 'lucide-react'; +import type { UseFormRegister, UseFormSetValue, UseFormGetValues, FieldErrors } from 'react-hook-form'; +import type { CreateItemFormData } from '@/lib/utils/validation'; + +interface ProductFormProps { + productName: string; + setProductName: (value: string) => void; + productStatus: string; + setProductStatus: (value: string) => void; + remarks: string; + setRemarks: (value: string) => void; + needsBOM: boolean; + setNeedsBOM: (value: boolean) => void; + specificationFile: File | null; + setSpecificationFile: (file: File | null) => void; + certificationFile: File | null; + setCertificationFile: (file: File | null) => void; + isSubmitting: boolean; + register: UseFormRegister; + setValue: UseFormSetValue; + getValues: UseFormGetValues; + errors: FieldErrors; +} + +export default function ProductForm({ + productName, + setProductName, + productStatus, + setProductStatus, + remarks, + setRemarks, + needsBOM, + setNeedsBOM, + specificationFile, + setSpecificationFile, + certificationFile, + setCertificationFile, + isSubmitting, + register, + setValue, + getValues, + errors, +}: ProductFormProps) { + return ( + <> + {/* 기본 정보 */} +
+ + { + const newName = e.target.value; + setProductName(newName); + setValue('productName', newName); + }} + className={errors.productName ? 'border-red-500' : ''} + /> + {errors.productName && ( +

+ {errors.productName.message} +

+ )} + {!errors.productName && ( +

+ 상품명을 입력해주세요 +

+ )} +
+ +
+ + + {errors.itemName && ( +

+ {errors.itemName.message} +

+ )} + {!errors.itemName && ( +

+ 품목명을 입력해주세요 +

+ )} +
+ +
+ + { + const pName = productName || ''; + const iName = getValues('itemName') || ''; + return pName && iName ? `${pName}-${iName}` : ''; + })()} + disabled + className="bg-muted text-muted-foreground" + placeholder="상품명과 품목명을 입력하면 자동으로 생성됩니다" + /> +

+ * 품목코드는 '상품명-품목명' 형식으로 자동 생성됩니다 +

+
+ +
+ + +

+ * 로트 번호 생성 시 사용되는 약자 (선택사항) +

+
+ +
+ + +

+ * 비활성 시 품목 사용이 제한됩니다 +

+
+ + ); +} + +/** + * FG 인정 정보 섹션 컴포넌트 + */ +export function ProductCertificationSection({ + remarks, + setRemarks, + needsBOM, + setNeedsBOM, + specificationFile, + setSpecificationFile, + certificationFile, + setCertificationFile, + isSubmitting, + register, +}: Pick) { + return ( +
+
+

인정 정보

+

+ 제품 인정서 및 시방서를 관리합니다 +

+
+ + {/* 인정번호, 유효기간, 파일 업로드, 비고 */} +
+
+ + +
+ +
+ + +
+ +
+ + +
+ + {/* 시방서 파일 */} +
+ +
+ { + const file = e.target.files?.[0]; + if (file) { + setSpecificationFile(file); + } + }} + className="flex-1" + disabled={isSubmitting} + /> + {specificationFile && ( + + )} +
+ {specificationFile && ( +

+ 첨부됨: {specificationFile.name} +

+ )} +
+ + {/* 인정서 파일 */} +
+ +
+ { + const file = e.target.files?.[0]; + if (file) { + setCertificationFile(file); + } + }} + className="flex-1" + disabled={isSubmitting} + /> + {certificationFile && ( + + )} +
+ {certificationFile && ( +

+ 첨부됨: {certificationFile.name} +

+ )} +
+ + {/* 비고 */} +
+ +