From ded0bc2439186cda5758cb65ac7bddb041d2ac81 Mon Sep 17 00:00:00 2001 From: byeongcheolryu Date: Tue, 9 Dec 2025 18:07:47 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20TypeScript=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BOMItem Omit 타입 시그니처 통일 (useTemplateManagement, SectionsTab, ItemMasterContext) - HeadersInit → Record 타입 변경 - Zustand useShallow 마이그레이션 (zustand/react/shallow) - DataTable, ListPageTemplate 제네릭 타입 제약 추가 - 설정 관리 페이지 추가 (직급, 직책, 휴가정책, 근무일정, 권한) - HR 관리 페이지 추가 (급여, 휴가) - 단가관리 페이지 리팩토링 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- claudedocs/[REF] all-pages-test-urls.md | 272 ++++++++ claudedocs/_index.md | 21 +- ...25-12-06] vacation-management-checklist.md | 196 ++++++ ...2025-12-06] item-data-mapping-checklist.md | 451 ++++++++++++ ...-2025-12-06] item-crud-backend-requests.md | 546 +++++++++++++++ ...5-12-06] assembly-part-issues-checklist.md | 154 ++++ ...T-2025-12-06] item-crud-session-context.md | 80 +++ ...T-2025-12-09] item-crud-session-context.md | 120 ++++ ...025-12-08] dynamic-form-separation-plan.md | 305 ++++++++ .../[API-2025-12-04] client-api-analysis.md | 34 +- ...-12-08] pricing-api-enhancement-request.md | 358 ++++++++++ ...2-09] pricing-api-integration-checklist.md | 139 ++++ ...NEXT-2025-12-09] client-session-context.md | 143 ++++ package-lock.json | 73 ++ package.json | 1 + .../(protected)/hr/salary-management/page.tsx | 38 + .../hr/vacation-management/page.tsx | 38 + .../(protected)/items/[id]/edit/page.tsx | 115 +-- .../[locale]/(protected)/items/[id]/page.tsx | 61 +- .../production/screen-production/page.tsx | 2 +- .../client-management-sales-admin/page.tsx | 186 +++-- .../pricing-management/[id]/edit/page.tsx | 224 +----- .../sales/pricing-management/create/page.tsx | 80 +-- .../sales/pricing-management/page.tsx | 463 +++++++----- .../settings/leave-policy/page.tsx | 5 + .../settings/permissions/[id]/page.tsx | 10 + .../settings/permissions/new/page.tsx | 5 + .../(protected)/settings/permissions/page.tsx | 5 + .../(protected)/settings/ranks/page.tsx | 5 + .../(protected)/settings/titles/page.tsx | 5 + .../settings/work-schedule/page.tsx | 5 + src/app/globals.css | 53 ++ src/components/clients/ClientDetail.tsx | 56 +- src/components/clients/ClientRegistration.tsx | 293 ++------ src/components/common/DataTable/DataTable.tsx | 4 +- src/components/common/DataTable/index.ts | 2 + .../SalaryManagement/SalaryDetailDialog.tsx | 287 ++++++++ src/components/hr/SalaryManagement/index.tsx | 509 ++++++++++++++ src/components/hr/SalaryManagement/types.ts | 100 +++ .../VacationAdjustDialog.tsx | 224 ++++++ .../VacationGrantDialog.tsx | 162 +++++ .../VacationRegisterDialog.tsx | 200 ++++++ .../VacationRequestDialog.tsx | 232 ++++++ .../VacationTypeSettingsDialog.tsx | 191 +++++ .../hr/VacationManagement/index.tsx | 571 +++++++++++++++ src/components/hr/VacationManagement/types.ts | 175 +++++ src/components/items/DrawingCanvas.tsx | 7 +- .../DynamicItemForm/fields/DropdownField.tsx | 16 - .../hooks/useConditionalDisplay.ts | 22 - .../hooks/useDynamicFormState.ts | 1 - .../items/DynamicItemForm/index.tsx | 239 ++----- src/components/items/DynamicItemForm/types.ts | 2 +- src/components/items/ItemDetailClient.tsx | 92 +-- .../items/ItemForm/forms/PartForm.tsx | 8 +- .../items/ItemForm/forms/ProductForm.tsx | 7 +- .../ItemForm/forms/parts/BendingPartForm.tsx | 20 +- .../forms/parts/PurchasedPartForm.tsx | 24 +- src/components/items/ItemListClient.tsx | 1 - src/components/organisms/DataTable.tsx | 4 +- src/components/organisms/PageLayout.tsx | 2 +- src/components/pricing/PricingFormClient.tsx | 30 +- src/components/pricing/PricingListClient.tsx | 6 +- src/components/pricing/actions.ts | 511 ++++++++++++++ src/components/pricing/index.ts | 11 + src/components/pricing/types.ts | 1 + src/components/quotes/QuoteRegistration.tsx | 3 + .../settings/LeavePolicyManagement/index.tsx | 138 ++++ .../settings/LeavePolicyManagement/types.ts | 49 ++ .../PermissionManagement/PermissionDetail.tsx | 482 +++++++++++++ .../PermissionDetailClient.tsx | 659 ++++++++++++++++++ .../PermissionManagement/PermissionDialog.tsx | 109 +++ .../settings/PermissionManagement/index.tsx | 492 +++++++++++++ .../settings/PermissionManagement/types.ts | 45 ++ .../settings/RankManagement/RankDialog.tsx | 84 +++ .../settings/RankManagement/index.tsx | 272 ++++++++ .../settings/RankManagement/types.ts | 18 + .../settings/TitleManagement/TitleDialog.tsx | 84 +++ .../settings/TitleManagement/index.tsx | 270 +++++++ .../settings/TitleManagement/types.ts | 18 + .../settings/WorkScheduleManagement/index.tsx | 286 ++++++++ .../settings/WorkScheduleManagement/types.ts | 51 ++ .../templates/IntegratedListTemplateV2.tsx | 2 +- src/components/templates/ListPageTemplate.tsx | 8 +- .../templates/ResponsiveFormTemplate.tsx | 5 +- src/components/ui/scroll-area.tsx | 52 ++ src/components/ui/select.tsx | 4 +- src/components/ui/sheet.tsx | 13 +- src/components/ui/time-picker.tsx | 190 +++++ src/contexts/ItemMasterContext.tsx | 4 +- src/hooks/useClientList.ts | 8 +- src/hooks/useItemList.ts | 7 +- src/layouts/DashboardLayout.tsx | 183 ++++- src/lib/api/master-data.ts | 8 +- src/lib/utils/materialTransform.ts | 37 +- src/lib/utils/validation.ts | 2 +- src/stores/masterDataStore.ts | 23 +- src/types/item.ts | 1 + tsconfig.tsbuildinfo | 2 +- 98 files changed, 10608 insertions(+), 1204 deletions(-) create mode 100644 claudedocs/[REF] all-pages-test-urls.md create mode 100644 claudedocs/hr/[IMPL-2025-12-06] vacation-management-checklist.md create mode 100644 claudedocs/item-master/[ANALYSIS-2025-12-06] item-data-mapping-checklist.md create mode 100644 claudedocs/item-master/[API-2025-12-06] item-crud-backend-requests.md create mode 100644 claudedocs/item-master/[IMPL-2025-12-06] assembly-part-issues-checklist.md create mode 100644 claudedocs/item-master/[NEXT-2025-12-06] item-crud-session-context.md create mode 100644 claudedocs/item-master/[NEXT-2025-12-09] item-crud-session-context.md create mode 100644 claudedocs/item-master/[PLAN-2025-12-08] dynamic-form-separation-plan.md create mode 100644 claudedocs/sales/[API-2025-12-08] pricing-api-enhancement-request.md create mode 100644 claudedocs/sales/[IMPL-2025-12-09] pricing-api-integration-checklist.md create mode 100644 claudedocs/sales/[NEXT-2025-12-09] client-session-context.md create mode 100644 src/app/[locale]/(protected)/hr/salary-management/page.tsx create mode 100644 src/app/[locale]/(protected)/hr/vacation-management/page.tsx create mode 100644 src/app/[locale]/(protected)/settings/leave-policy/page.tsx create mode 100644 src/app/[locale]/(protected)/settings/permissions/[id]/page.tsx create mode 100644 src/app/[locale]/(protected)/settings/permissions/new/page.tsx create mode 100644 src/app/[locale]/(protected)/settings/permissions/page.tsx create mode 100644 src/app/[locale]/(protected)/settings/ranks/page.tsx create mode 100644 src/app/[locale]/(protected)/settings/titles/page.tsx create mode 100644 src/app/[locale]/(protected)/settings/work-schedule/page.tsx create mode 100644 src/components/hr/SalaryManagement/SalaryDetailDialog.tsx create mode 100644 src/components/hr/SalaryManagement/index.tsx create mode 100644 src/components/hr/SalaryManagement/types.ts create mode 100644 src/components/hr/VacationManagement/VacationAdjustDialog.tsx create mode 100644 src/components/hr/VacationManagement/VacationGrantDialog.tsx create mode 100644 src/components/hr/VacationManagement/VacationRegisterDialog.tsx create mode 100644 src/components/hr/VacationManagement/VacationRequestDialog.tsx create mode 100644 src/components/hr/VacationManagement/VacationTypeSettingsDialog.tsx create mode 100644 src/components/hr/VacationManagement/index.tsx create mode 100644 src/components/hr/VacationManagement/types.ts create mode 100644 src/components/pricing/actions.ts create mode 100644 src/components/settings/LeavePolicyManagement/index.tsx create mode 100644 src/components/settings/LeavePolicyManagement/types.ts create mode 100644 src/components/settings/PermissionManagement/PermissionDetail.tsx create mode 100644 src/components/settings/PermissionManagement/PermissionDetailClient.tsx create mode 100644 src/components/settings/PermissionManagement/PermissionDialog.tsx create mode 100644 src/components/settings/PermissionManagement/index.tsx create mode 100644 src/components/settings/PermissionManagement/types.ts create mode 100644 src/components/settings/RankManagement/RankDialog.tsx create mode 100644 src/components/settings/RankManagement/index.tsx create mode 100644 src/components/settings/RankManagement/types.ts create mode 100644 src/components/settings/TitleManagement/TitleDialog.tsx create mode 100644 src/components/settings/TitleManagement/index.tsx create mode 100644 src/components/settings/TitleManagement/types.ts create mode 100644 src/components/settings/WorkScheduleManagement/index.tsx create mode 100644 src/components/settings/WorkScheduleManagement/types.ts create mode 100644 src/components/ui/scroll-area.tsx create mode 100644 src/components/ui/time-picker.tsx diff --git a/claudedocs/[REF] all-pages-test-urls.md b/claudedocs/[REF] all-pages-test-urls.md new file mode 100644 index 00000000..9bc05fa0 --- /dev/null +++ b/claudedocs/[REF] all-pages-test-urls.md @@ -0,0 +1,272 @@ +# 전체 페이지 테스트 URL 목록 + +> 백엔드 메뉴 연동 전 테스트용 직접 접근 URL (Last Updated: 2025-12-08) + +--- + +## 🏠 기본 페이지 + +| 페이지 | URL | 상태 | +|--------|-----|------| +| 홈 (리다이렉트) | `/ko` | ✅ | +| 로그인 | `/ko/login` | ✅ | +| 회원가입 | `/ko/signup` | ⚠️ 차단됨 | +| 대시보드 | `/ko/dashboard` | ✅ | + +``` +http://localhost:3000/ko +http://localhost:3000/ko/login +http://localhost:3000/ko/signup +http://localhost:3000/ko/dashboard +``` + +--- + +## 👥 인사관리 (HR) + +### 메인 페이지 + +| 페이지 | URL | 상태 | +|--------|-----|------| +| 부서관리 | `/ko/hr/department-management` | ✅ | +| 사원관리 | `/ko/hr/employee-management` | ✅ | +| 근태관리 | `/ko/hr/attendance-management` | ✅ | +| 휴가관리 | `/ko/hr/vacation-management` | ✅ | +| 급여관리 | `/ko/hr/salary-management` | ✅ | + +``` +http://localhost:3000/ko/hr/department-management +http://localhost:3000/ko/hr/employee-management +http://localhost:3000/ko/hr/attendance-management +http://localhost:3000/ko/hr/vacation-management +http://localhost:3000/ko/hr/salary-management +``` + +### 사원관리 하위 페이지 + +| 페이지 | URL | +|--------|-----| +| 사원 등록 | `/ko/hr/employee-management/new` | +| 사원 상세 | `/ko/hr/employee-management/[id]` | +| 사원 수정 | `/ko/hr/employee-management/[id]/edit` | +| CSV 업로드 | `/ko/hr/employee-management/csv-upload` | + +``` +http://localhost:3000/ko/hr/employee-management/new +http://localhost:3000/ko/hr/employee-management/1 +http://localhost:3000/ko/hr/employee-management/1/edit +http://localhost:3000/ko/hr/employee-management/csv-upload +``` + +--- + +## 💰 판매관리 (Sales) + +### 메인 페이지 + +| 페이지 | URL | 상태 | +|--------|-----|------| +| 거래처관리 | `/ko/sales/client-management-sales-admin` | ✅ | +| 견적관리 | `/ko/sales/quote-management` | ✅ | +| 단가관리 | `/ko/sales/pricing-management` | ✅ | + +``` +http://localhost:3000/ko/sales/client-management-sales-admin +http://localhost:3000/ko/sales/quote-management +http://localhost:3000/ko/sales/pricing-management +``` + +### 거래처관리 하위 페이지 + +| 페이지 | URL | +|--------|-----| +| 거래처 등록 | `/ko/sales/client-management-sales-admin/new` | +| 거래처 상세 | `/ko/sales/client-management-sales-admin/[id]` | +| 거래처 수정 | `/ko/sales/client-management-sales-admin/[id]/edit` | + +``` +http://localhost:3000/ko/sales/client-management-sales-admin/new +http://localhost:3000/ko/sales/client-management-sales-admin/1 +http://localhost:3000/ko/sales/client-management-sales-admin/1/edit +``` + +### 견적관리 하위 페이지 + +| 페이지 | URL | +|--------|-----| +| 견적 등록 | `/ko/sales/quote-management/new` | +| 견적 상세 | `/ko/sales/quote-management/[id]` | +| 견적 수정 | `/ko/sales/quote-management/[id]/edit` | + +``` +http://localhost:3000/ko/sales/quote-management/new +http://localhost:3000/ko/sales/quote-management/1 +http://localhost:3000/ko/sales/quote-management/1/edit +``` + +### 단가관리 하위 페이지 + +| 페이지 | URL | +|--------|-----| +| 단가 등록 | `/ko/sales/pricing-management/create` | +| 단가 수정 | `/ko/sales/pricing-management/[id]/edit` | + +``` +http://localhost:3000/ko/sales/pricing-management/create +http://localhost:3000/ko/sales/pricing-management/1/edit +``` + +--- + +## 📦 기준정보관리 (Master Data) + +### 품목기준관리 + +| 페이지 | URL | 상태 | +|--------|-----|------| +| 품목 목록 | `/ko/master-data/item-master-data-management` | ✅ | + +``` +http://localhost:3000/ko/master-data/item-master-data-management +``` + +### 품목관리 (Items) - 구버전 + +| 페이지 | URL | +|--------|-----| +| 품목 목록 | `/ko/items` | +| 품목 등록 | `/ko/items/create` | +| 품목 상세 | `/ko/items/[id]` | +| 품목 수정 | `/ko/items/[id]/edit` | + +``` +http://localhost:3000/ko/items +http://localhost:3000/ko/items/create +http://localhost:3000/ko/items/1 +http://localhost:3000/ko/items/1/edit +``` + +--- + +## 🏭 생산관리 (Production) + +### 스크린 생산 + +| 페이지 | URL | +|--------|-----| +| 생산 목록 | `/ko/production/screen-production` | +| 생산 등록 | `/ko/production/screen-production/create` | +| 생산 상세 | `/ko/production/screen-production/[id]` | +| 생산 수정 | `/ko/production/screen-production/[id]/edit` | + +``` +http://localhost:3000/ko/production/screen-production +http://localhost:3000/ko/production/screen-production/create +http://localhost:3000/ko/production/screen-production/1 +http://localhost:3000/ko/production/screen-production/1/edit +``` + +--- + +## ⚙️ 설정 (Settings) + +| 페이지 | URL | 상태 | +|--------|-----|------| +| 휴가정책 | `/ko/settings/leave-policy` | ✅ | +| 권한관리 | `/ko/settings/permissions` | ✅ | +| 직급관리 | `/ko/settings/ranks` | ✅ | +| 직책관리 | `/ko/settings/titles` | ✅ | +| 근무일정 | `/ko/settings/work-schedule` | ✅ | + +``` +http://localhost:3000/ko/settings/leave-policy +http://localhost:3000/ko/settings/permissions +http://localhost:3000/ko/settings/ranks +http://localhost:3000/ko/settings/titles +http://localhost:3000/ko/settings/work-schedule +``` + +--- + +## 📋 전체 URL 한눈에 보기 + +### 기본 +``` +http://localhost:3000/ko/dashboard +http://localhost:3000/ko/login +``` + +### HR +``` +http://localhost:3000/ko/hr/department-management +http://localhost:3000/ko/hr/employee-management +http://localhost:3000/ko/hr/attendance-management +http://localhost:3000/ko/hr/vacation-management +http://localhost:3000/ko/hr/salary-management +``` + +### Sales +``` +http://localhost:3000/ko/sales/client-management-sales-admin +http://localhost:3000/ko/sales/quote-management +http://localhost:3000/ko/sales/pricing-management +``` + +### Master Data +``` +http://localhost:3000/ko/master-data/item-master-data-management +http://localhost:3000/ko/items +``` + +### Production +``` +http://localhost:3000/ko/production/screen-production +``` + +### Settings +``` +http://localhost:3000/ko/settings/leave-policy +http://localhost:3000/ko/settings/permissions +http://localhost:3000/ko/settings/ranks +http://localhost:3000/ko/settings/titles +http://localhost:3000/ko/settings/work-schedule +``` + +--- + +## 백엔드 메뉴 연동 시 path 참고 + +```javascript +// HR +'/hr/department-management' +'/hr/employee-management' +'/hr/attendance-management' +'/hr/vacation-management' +'/hr/salary-management' + +// Sales +'/sales/client-management-sales-admin' +'/sales/quote-management' +'/sales/pricing-management' + +// Master Data +'/master-data/item-master-data-management' +'/items' + +// Production +'/production/screen-production' + +// Settings +'/settings/leave-policy' +'/settings/permissions' +'/settings/ranks' +'/settings/titles' +'/settings/work-schedule' +``` + +--- + +## 작성일 + +- 최초 작성: 2025-12-06 +- 최종 업데이트: 2025-12-08 (전체 페이지 통합) \ No newline at end of file diff --git a/claudedocs/_index.md b/claudedocs/_index.md index 13f4abc1..37a522a1 100644 --- a/claudedocs/_index.md +++ b/claudedocs/_index.md @@ -1,6 +1,14 @@ # claudedocs 문서 맵 -> 프로젝트 기술 문서 인덱스 (Last Updated: 2025-12-05) +> 프로젝트 기술 문서 인덱스 (Last Updated: 2025-12-09) + +## ⭐ 빠른 참조 + +| 문서 | 설명 | +|------|------| +| **[`[REF] all-pages-test-urls.md`](./[REF]%20all-pages-test-urls.md)** | 🔗 **전체 페이지 테스트 URL 목록** - 모든 페이지 직접 접근 주소 | + +--- ## 폴더 구조 @@ -38,12 +46,13 @@ claudedocs/ --- -## 👥 hr/ - 인사관리 (부서/사원) +## 👥 hr/ - 인사관리 (부서/사원/근태/휴가) | 파일 | 설명 | |------|------| | `[IMPL-2025-12-05] department-management-checklist.md` | ✅ **완료** - 부서관리 구현 체크리스트 (무제한 트리구조) | -| `[IMPL-2025-12-05] employee-management-checklist.md` | 🔄 **진행중** - 사원관리 구현 체크리스트 | +| `[IMPL-2025-12-05] employee-management-checklist.md` | ✅ **완료** - 사원관리 구현 체크리스트 | +| `[IMPL-2025-12-06] vacation-management-checklist.md` | ✅ **완료** - 휴가관리 구현 체크리스트 | --- @@ -51,6 +60,8 @@ claudedocs/ | 파일 | 설명 | |------|------| +| `[NEXT-2025-12-09] item-crud-session-context.md` | ⭐ **세션 체크포인트** - 백엔드 field_key 통일 대기, 다음 작업 정리 | +| `[PLAN-2025-12-08] dynamic-form-separation-plan.md` | 📋 DynamicItemForm 품목별 분리 계획 (Phase 2: 컴포넌트 구조 설계) | | `[REF] item-code-hardcoding.md` | ⭐ **핵심** - 품목관리 하드코딩 내역 종합 (품목유형/코드자동생성/전개도/BOM) | | `[IMPL-2025-12-02] item-code-auto-generation.md` | 품목코드 자동생성 구현 상세 | | `[PLAN-2025-12-01] service-layer-refactoring.md` | ✅ **완료** - 서비스 레이어 리팩토링 계획 (도메인 로직 중앙화) | @@ -76,11 +87,13 @@ claudedocs/ | 파일 | 설명 | |------|------| +| `[API-2025-12-08] pricing-api-enhancement-request.md` | 🔴 **NEW** - 단가관리 백엔드 API 개선 요청서 (스키마 변경, 신규 엔드포인트) | | `[IMPL-2025-12-05] pricing-management-migration.md` | 🔄 **진행중** - 단가관리 마이그레이션 계획 (7 Phase, 체크리스트, 원가/마진 계산 로직) | | `[API-2025-12-04] quote-api-request.md` | ⭐ **NEW** - 견적관리 API 요청서 (데이터 모델, 엔드포인트, 수식 계산) | | `[PLAN-2025-12-04] quote-management-implementation.md` | 📋 **NEW** - 견적관리 작업계획서 (6 Phase, 체크리스트) | +| `[NEXT-2025-12-09] client-session-context.md` | ⭐ **세션 체크포인트** - 다음 세션 이어하기용 (완료/숨긴 섹션/다음 작업) | | `[IMPL-2025-12-04] client-management-api-integration.md` | ✅ **완료** - 거래처관리 API 연동 체크리스트 (CRUD, 그룹 훅) | -| `[API-2025-12-04] client-api-analysis.md` | ⭐ 거래처 API 분석 (sam-api 연동 현황, 필드 매핑) | +| `[API-2025-12-04] client-api-analysis.md` | ✅ **완료** - 거래처 API 분석 (2차 필드 완료, is_active Boolean) | | `[PLAN-2025-12-02] sales-pages-migration.md` | 📋 견적관리/거래처관리 마이그레이션 계획 | --- diff --git a/claudedocs/hr/[IMPL-2025-12-06] vacation-management-checklist.md b/claudedocs/hr/[IMPL-2025-12-06] vacation-management-checklist.md new file mode 100644 index 00000000..3cbd732c --- /dev/null +++ b/claudedocs/hr/[IMPL-2025-12-06] vacation-management-checklist.md @@ -0,0 +1,196 @@ +# 휴가관리 페이지 구현 체크리스트 + +> **시작일**: 2025-12-06 +> **상태**: ✅ 완료 +> **참조 페이지**: 사원관리, 근태관리 (공통 UI 패턴 사용) + +--- + +## 스크린샷 분석 요약 (3탭 구조) + +### 메인 탭 구조 +1. **휴가 사용현황** (usage) - 사원별 휴가 잔여 현황 +2. **휴가 부여현황** (grant) - 휴가 지급 이력 +3. **휴가 신청현황** (request) - 휴가 신청 및 승인 현황 + +### 탭 1: 휴가 사용현황 +- **통계 카드 4개**: 연차, 월차, 포상휴가, 기타휴가 (각각 N명) +- **서브탭**: 전체 | 연차 | 월차 | 포상휴가 | 기타 +- **테이블 컬럼**: 체크박스, 부서, 직책, 이름, 직급, 연차(총/사용/잔여), 월차, 포상휴가, 기타휴가, 조정 버튼 +- **액션 버튼**: 엑셀 다운로드, 휴가 등록, 휴가 종류 설정 + +### 탭 2: 휴가 부여현황 +- **통계 카드 4개**: 연차, 월차, 포상휴가, 기타 (지급 건수) +- **서브탭**: 전체 | 연차 | 월차 | 포상휴가 | 기타 +- **테이블 컬럼**: 체크박스, 부서, 직책, 이름, 직급, 휴가종류, 지급일수, 지급일자, 비고 +- **액션 버튼**: 엑셀 다운로드, 휴가 등록 + +### 탭 3: 휴가 신청현황 +- **통계 카드 3개**: 대기, 승인, 반려 (건수) +- **서브탭**: 전체 | 대기 | 승인 | 반려 +- **테이블 컬럼**: 체크박스, 부서, 직책, 이름, 직급, 휴가종류, 신청일수, 시작일, 종료일, 상태 +- **액션 버튼**: 엑셀 다운로드 + +### 공통 다이얼로그 +1. **휴가 등록 다이얼로그**: 사원 선택, 휴가 종류, 지급 일수, 비고 +2. **휴가 종류 설정 다이얼로그**: 휴가종류명, 사용여부, 설명, 삭제 +3. **휴가 조정 다이얼로그**: 사원 정보, 휴가종류별 총/사용/잔여일수, +/- 조정 + +--- + +## Phase 1: 기본 구조 세팅 + +- [x] 1.1 페이지 파일 생성 (`src/app/[locale]/(protected)/hr/vacation-management/page.tsx`) +- [x] 1.2 컴포넌트 폴더 구조 생성 (`src/components/hr/VacationManagement/`) +- [x] 1.3 타입 정의 파일 생성 (`types.ts`) +- [x] 1.4 메인 컴포넌트 생성 (`index.tsx`) + +--- + +## Phase 2: 타입 및 상수 정의 (3탭 구조) + +- [x] 2.1 `MainTabType` 타입 정의 (usage | grant | request) +- [x] 2.2 `VacationUsageRecord` 인터페이스 정의 (탭1: 사용현황) +- [x] 2.3 `VacationGrantRecord` 인터페이스 정의 (탭2: 부여현황) +- [x] 2.4 `VacationRequestRecord` 인터페이스 정의 (탭3: 신청현황) +- [x] 2.5 `VacationType` 타입 정의 (연차, 월차, 포상휴가, 기타) +- [x] 2.6 `RequestStatus` 타입 정의 (pending, approved, rejected) +- [x] 2.7 `VacationFormData`, `VacationAdjustment`, `VacationTypeConfig` 인터페이스 정의 +- [x] 2.8 상수 정의 (MAIN_TAB_LABELS, VACATION_TYPE_LABELS, REQUEST_STATUS_LABELS 등) + +--- + +## Phase 3: 메인 컴포넌트 구현 (3탭 + IntegratedListTemplateV2) + +- [x] 3.1 메인 탭 상태 관리 (mainTab: usage | grant | request) +- [x] 3.2 탭별 Mock 데이터 생성 (usageData, grantData, requestData) +- [x] 3.3 탭별 통계 카드 구현 + - 사용현황: 연차/월차/포상휴가/기타 사용자 수 + - 부여현황: 연차/월차/포상휴가/기타 지급 건수 + - 신청현황: 대기/승인/반려 건수 +- [x] 3.4 탭별 서브탭 구현 + - 사용현황: 전체 | 연차 | 월차 | 포상휴가 | 기타 + - 부여현황: 전체 | 연차 | 월차 | 포상휴가 | 기타 + - 신청현황: 전체 | 대기 | 승인 | 반려 +- [x] 3.5 탭별 테이블 컬럼 정의 (usageColumns, grantColumns, requestColumns) +- [x] 3.6 탭별 행 렌더링 (renderUsageRow, renderGrantRow, renderRequestRow) +- [x] 3.7 모바일 카드 렌더링 (탭별) +- [x] 3.8 탭별 헤더 액션 버튼 구현 +- [x] 3.9 검색 및 정렬 기능 구현 + +--- + +## Phase 4: 휴가 등록 다이얼로그 + +- [x] 4.1 `VacationRegisterDialog.tsx` 파일 생성 +- [x] 4.2 사원 선택 드롭다운 구현 +- [x] 4.3 휴가 종류 선택 드롭다운 구현 +- [x] 4.4 지급 일수 입력 필드 구현 +- [x] 4.5 비고 텍스트 영역 구현 +- [x] 4.6 폼 유효성 검사 구현 +- [x] 4.7 등록/취소 핸들러 구현 + +--- + +## Phase 5: 휴가 종류 설정 다이얼로그 + +- [x] 5.1 `VacationTypeSettingsDialog.tsx` 파일 생성 +- [x] 5.2 휴가 종류 테이블 구현 +- [x] 5.3 사용여부 토글 스위치 구현 +- [x] 5.4 휴가종류 추가 기능 구현 +- [x] 5.5 휴가종류 삭제 기능 구현 +- [x] 5.6 저장/취소 핸들러 구현 + +--- + +## Phase 6: 휴가 조정 다이얼로그 + +- [x] 6.1 `VacationAdjustDialog.tsx` 파일 생성 +- [x] 6.2 사원 정보 헤더 표시 +- [x] 6.3 휴가 종류별 조정 테이블 구현 (총일수/사용일수/잔여일수) +- [x] 6.4 +/- 조정 버튼 구현 +- [x] 6.5 저장/취소 핸들러 구현 +- [x] 6.6 타입 수정: `VacationRecord` → `VacationUsageRecord` + +--- + +## Phase 7: 연동 및 마무리 + +- [x] 7.1 사이드바 메뉴 - 백엔드에서 관리 (별도 연동 예정) +- [x] 7.2 테스트 URL 문서 작성 (`[REF] hr-pages-test-urls.md`) + +--- + +## 생성된 파일 목록 + +| 파일 | 설명 | +|------|------| +| `src/app/[locale]/(protected)/hr/vacation-management/page.tsx` | 휴가관리 페이지 | +| `src/components/hr/VacationManagement/types.ts` | 타입 및 상수 정의 (3탭 구조) | +| `src/components/hr/VacationManagement/index.tsx` | 메인 컴포넌트 (3탭 구조) | +| `src/components/hr/VacationManagement/VacationRegisterDialog.tsx` | 휴가 등록 다이얼로그 | +| `src/components/hr/VacationManagement/VacationTypeSettingsDialog.tsx` | 휴가 종류 설정 다이얼로그 | +| `src/components/hr/VacationManagement/VacationAdjustDialog.tsx` | 휴가 조정 다이얼로그 | + +--- + +## 타입 구조 정리 + +```typescript +// 메인 탭 타입 +type MainTabType = 'usage' | 'grant' | 'request'; + +// 휴가 종류 +type VacationType = 'annual' | 'monthly' | 'reward' | 'other'; + +// 신청 상태 +type RequestStatus = 'pending' | 'approved' | 'rejected'; + +// 탭1: 휴가 사용현황 +interface VacationUsageRecord { + id, employeeId, employeeName, department, position, rank, + annual: VacationInfo, // { total, used, remaining } + monthly: VacationInfo, + reward: VacationInfo, + other: VacationInfo, + createdAt, updatedAt +} + +// 탭2: 휴가 부여현황 +interface VacationGrantRecord { + id, employeeId, employeeName, department, position, rank, + vacationType, grantDays, grantDate, memo?, + createdAt, updatedAt +} + +// 탭3: 휴가 신청현황 +interface VacationRequestRecord { + id, employeeId, employeeName, department, position, rank, + vacationType, requestDays, startDate, endDate, status, approver?, + createdAt, updatedAt +} +``` + +--- + +## 참조 파일 + +| 파일 | 용도 | +|------|------| +| `src/components/hr/AttendanceManagement/index.tsx` | 공통 UI 패턴 참조 | +| `src/components/hr/EmployeeManagement/index.tsx` | 공통 UI 패턴 참조 | +| `src/components/templates/IntegratedListTemplateV2.tsx` | 리스트 템플릿 | +| `src/components/organisms/ListMobileCard.tsx` | 모바일 카드 | + +--- + +## 변경 로그 + +| 날짜 | 내용 | +|------|------| +| 2025-12-06 | 체크리스트 생성, 스크린샷 분석 완료 | +| 2025-12-06 | Phase 1~7 구현 완료 (단일 탭 구조) | +| 2025-12-06 | **구조 변경**: 3탭 구조로 전면 재구현 (사용현황/부여현황/신청현황) | +| 2025-12-06 | types.ts 재작성: 3개 레코드 타입 분리 | +| 2025-12-06 | index.tsx 재작성: 메인 탭 + 탭별 서브탭/통계/테이블 구조 | +| 2025-12-06 | VacationAdjustDialog.tsx 타입 수정: VacationUsageRecord 사용 | \ No newline at end of file diff --git a/claudedocs/item-master/[ANALYSIS-2025-12-06] item-data-mapping-checklist.md b/claudedocs/item-master/[ANALYSIS-2025-12-06] item-data-mapping-checklist.md new file mode 100644 index 00000000..2220a652 --- /dev/null +++ b/claudedocs/item-master/[ANALYSIS-2025-12-06] item-data-mapping-checklist.md @@ -0,0 +1,451 @@ +# 품목관리 데이터 누락 조사 체크리스트 + +품목기준관리 데이터 기반 품목관리 등록/수정 시 데이터 매핑 분석 + +## 조사 방법 +- **프론트엔드 → 백엔드**: DynamicItemForm submit 데이터 분석 +- **백엔드 → 프론트엔드**: API 응답 → Edit 페이지 데이터 매핑 분석 +- **DB 필드**: Material/Product 모델 fillable 필드 확인 + +--- + +## 1. 소모품 (SM - Supplies Material) + +### 사용 모델: `Material` + +### Material 모델 fillable 필드 +| 필드명 | 타입 | 필수 | 설명 | +|--------|------|------|------| +| tenant_id | int | Y | 테넌트 ID | +| category_id | int | N | 분류 ID | +| name | string | Y | 품목명 | +| item_name | string | N | 품목명 (레거시) | +| specification | string | N | 규격 | +| material_code | string | Y | 품목코드 | +| material_type | string | Y | SM/RM/CS | +| unit | string | Y | 단위 | +| is_inspection | string | N | 검사 여부 | +| search_tag | string | N | 검색 태그 | +| remarks | string | N | 비고 | +| attributes | json | N | 동적 속성 | +| options | json | N | 옵션 배열 | +| created_by | int | N | 생성자 | +| updated_by | int | N | 수정자 | +| is_active | bool | N | 활성 상태 | + +### 등록 시 데이터 매핑 체크리스트 + +#### 프론트엔드 → 백엔드 +| 프론트엔드 필드 | 백엔드 필드 | 변환 함수 | 상태 | 비고 | +|----------------|------------|----------|------|------| +| - [ ] item_name / 품목명 | name | fieldKeyToBackendKey | | | +| - [ ] specification / 규격 | specification | standard_* → options + specification | | convertStandardFieldsToOptions() | +| - [ ] unit / 단위 | unit | fieldKeyToBackendKey | | | +| - [ ] note / 비고 | remarks | transformMaterialDataForSave | | note → remarks | +| - [ ] is_active / 상태 | is_active | boolean 변환 | | | +| - [ ] code (자동생성) | material_code | transformMaterialDataForSave | | name-specification | +| - [ ] product_type | material_type | transformMaterialDataForSave | | | +| - [ ] standard_* | options | convertStandardFieldsToOptions | | 배열로 변환 | + +#### 백엔드 → 프론트엔드 (수정 시) +| 백엔드 필드 | 프론트엔드 필드 | 변환 위치 | 상태 | 비고 | +|------------|----------------|----------|------|------| +| - [ ] name | item_name | mapApiResponseToFormData | | | +| - [ ] specification | specification | mapApiResponseToFormData | | | +| - [ ] unit | unit | mapApiResponseToFormData | | | +| - [ ] remarks | note | mapApiResponseToFormData | | remarks → note | +| - [ ] is_active | is_active | mapApiResponseToFormData | | | +| - [ ] material_code | (참조만) | - | | 품목코드는 수정 불가 | +| - [ ] material_type | (참조만) | - | | 품목유형은 수정 불가 | +| - [ ] options | standard_* | convertOptionsToStandardFields | | 배열 → 개별 필드 | + +### API 엔드포인트 +- **등록**: POST `/api/v1/items` → ItemsService.createMaterial() +- **조회**: GET `/api/v1/items/{id}?item_type=MATERIAL` +- **수정**: PATCH `/api/v1/products/materials/{id}` + +### 누락 가능성 분석 +- [ ] **options 필드 로드 확인**: Edit 시 options 배열이 standard_* 필드로 정상 변환되는지 +- [ ] **specification 생성 로직**: 등록 시 standard_* 값들이 specification으로 조합되는지 +- [ ] **material_code 자동생성**: name-specification 형식으로 생성되는지 +- [ ] **is_inspection 필드**: 프론트엔드에 검사여부 필드가 있는지 + +--- + +## 2. 원자재 (RM - Raw Material) + +### 사용 모델: `Material` (SM과 동일) + +### 등록/수정 데이터 매핑 (SM과 동일) +- SM과 동일한 Material 모델 사용 +- material_type = 'RM' + +### 원자재 특화 필드 체크리스트 +| 프론트엔드 필드 | 백엔드 필드 | 상태 | 비고 | +|----------------|------------|------|------| +| - [ ] 분류 선택 (드롭다운) | category_id | | 원자재 카테고리 | +| - [ ] 규격 옵션들 (standard_*) | options | | 원자재별 규격 옵션 | + +### 누락 가능성 분석 +- [ ] **category_id 매핑**: 분류 선택이 제대로 저장되는지 +- [ ] **원자재별 옵션 구조**: 품목기준관리에서 정의한 standard_1~N 필드들 + +--- + +## 3. 부자재 (CS - Consumable Supplies) + +### 사용 모델: `Material` (SM, RM과 동일) + +### 등록/수정 데이터 매핑 (SM과 동일) +- material_type = 'CS' + +### 부자재 특화 필드 체크리스트 +| 프론트엔드 필드 | 백엔드 필드 | 상태 | 비고 | +|----------------|------------|------|------| +| - [ ] 규격 옵션들 (standard_*) | options | | 부자재별 규격 옵션 | + +### 누락 가능성 분석 +- [ ] SM/RM과 동일한 구조 사용 확인 + +--- + +## 4. 조립부품 (PT - Assembly) + +### 사용 모델: `Product` + +### Product 모델 fillable 필드 +| 필드명 | 타입 | 필수 | 설명 | +|--------|------|------|------| +| tenant_id | int | Y | 테넌트 ID | +| code | string | Y | 품목코드 | +| name | string | Y | 품목명 | +| unit | string | Y | 단위 | +| category_id | int | N | 분류 ID | +| product_type | string | Y | FG/PT | +| attributes | json | N | 동적 속성 | +| description | string | N | 설명 | +| is_sellable | bool | N | 판매가능 | +| is_purchasable | bool | N | 구매가능 | +| is_producible | bool | N | 생산가능 | +| safety_stock | int | N | 안전재고 | +| lead_time | int | N | 리드타임 | +| is_variable_size | bool | N | 가변사이즈 | +| product_category | string | N | 제품분류 | +| **part_type** | string | N | 부품유형 (ASSEMBLY/BENDING/PURCHASED) | +| bending_diagram | string | N | 전개도 경로 | +| bending_details | json | N | 절곡 상세 | +| specification_file | string | N | 시방서 경로 | +| certification_file | string | N | 인정서 경로 | +| is_active | bool | N | 활성 상태 | + +### 조립부품 특화 필드 체크리스트 + +#### 프론트엔드 → 백엔드 +| 프론트엔드 필드 | 백엔드 필드 | 변환 | 상태 | 비고 | +|----------------|------------|------|------|------| +| - [ ] 품목명 (드롭다운 선택) | name | autoAssemblyItemName | | "품목명 가로x세로" 형식 | +| - [ ] 측면규격(가로) | (attributes?) | | | side_spec_width | +| - [ ] 측면규격(세로) | (attributes?) | | | side_spec_height | +| - [ ] 길이 | (attributes?) | | | assembly_length | +| - [ ] 규격 (자동생성) | spec/description | autoAssemblySpec | | "가로x세로x길이" 형식 | +| - [ ] 품목코드 (자동생성) | code | autoGeneratedItemCode | | 영문약어-순번 | +| - [ ] 단위 | unit | | | | +| - [ ] 비고 | note/description | | | | +| - [ ] 품목상태 | is_active | | | | +| - [ ] 전개도 이미지 | bending_diagram | uploadItemFile() | | 파일 업로드 | +| - [ ] BOM 구성 | bom[] | | | 자재명세서 | + +#### 백엔드 → 프론트엔드 (수정 시) +| 백엔드 필드 | 프론트엔드 필드 | 상태 | 비고 | +|------------|----------------|------|------| +| - [ ] name | item_name | | | +| - [ ] code | (참조만) | | 품목코드 | +| - [ ] part_type | part_type | | ASSEMBLY | +| - [ ] side_spec_width | 측면규격(가로) | | attributes에서? | +| - [ ] side_spec_height | 측면규격(세로) | | attributes에서? | +| - [ ] assembly_length | 길이 | | attributes에서? | +| - [ ] is_active | is_active | | | +| - [ ] bending_diagram | bending_diagram | | 기존 전개도 URL | + +### 누락 가능성 분석 +- [ ] **측면규격 가로/세로 필드**: 백엔드에서 별도 컬럼으로 저장되는지 vs attributes JSON +- [ ] **길이 필드**: assembly_length 저장 여부 +- [ ] **전개도 이미지 로드**: Edit 시 기존 이미지 표시 +- [ ] **BOM 데이터 로드**: component_lines 관계 로드 확인 + +--- + +## 5. 절곡부품 (PT - Bending) + +### 사용 모델: `Product` (조립부품과 동일) + +### 절곡부품 특화 필드 체크리스트 + +#### 프론트엔드 → 백엔드 +| 프론트엔드 필드 | 백엔드 필드 | 변환 | 상태 | 비고 | +|----------------|------------|------|------|------| +| - [ ] 품목명 (드롭다운 선택) | name | | | bendingFieldKeys.itemName | +| - [ ] 재질 | (attributes?) | | | bendingFieldKeys.material | +| - [ ] 종류 | (attributes?) | | | bendingFieldKeys.category | +| - [ ] 폭 합계 | width_sum | | | bendingFieldKeys.widthSum | +| - [ ] 모양&길이 | (attributes?) | | | bendingFieldKeys.shapeLength | +| - [ ] 품목코드 (자동생성) | code | autoBendingItemCode | | "품목명+종류+모양길이" | +| - [ ] 단위 | unit | | | | +| - [ ] 비고 | note | | | | +| - [ ] 전개도 이미지 | bending_diagram | uploadItemFile() | | | +| - [ ] 절곡 상세 | bending_details | | | [{angle, length, type}] | + +#### 백엔드 → 프론트엔드 (수정 시) +| 백엔드 필드 | 프론트엔드 필드 | 상태 | 비고 | +|------------|----------------|------|------| +| - [ ] name | item_name | | | +| - [ ] part_type | part_type | | BENDING | +| - [ ] bending_diagram | bending_diagram | | 전개도 URL | +| - [ ] bending_details | bendingDetails | | 절곡 상세 배열 | +| - [ ] width_sum | widthSum | | | +| - [ ] is_active | is_active | | | + +### 누락 가능성 분석 +- [ ] **재질/종류/모양길이 필드**: 저장 위치 (별도 컬럼 vs attributes) +- [ ] **절곡 상세 데이터**: bending_details JSON 저장 및 로드 +- [ ] **전개도 파일 업로드**: 파일 경로 저장 확인 + +--- + +## 6. 구매부품 (PT - Purchased) + +### 사용 모델: `Product` (조립/절곡부품과 동일) + +### 구매부품 특화 필드 체크리스트 + +#### 프론트엔드 → 백엔드 +| 프론트엔드 필드 | 백엔드 필드 | 변환 | 상태 | 비고 | +|----------------|------------|------|------|------| +| - [ ] 품목명 (전동개폐기 등) | name | | | purchasedFieldKeys.itemName | +| - [ ] 용량 | (attributes?) | | | purchasedFieldKeys.capacity | +| - [ ] 전원 | (attributes?) | | | purchasedFieldKeys.power | +| - [ ] 품목코드 (자동생성) | code | autoPurchasedItemCode | | "품목명+용량+전원" | +| - [ ] 단위 | unit | | | | +| - [ ] 비고 | note | | | | + +#### 백엔드 → 프론트엔드 (수정 시) +| 백엔드 필드 | 프론트엔드 필드 | 상태 | 비고 | +|------------|----------------|------|------| +| - [ ] name | item_name | | | +| - [ ] part_type | part_type | | PURCHASED | +| - [ ] capacity | 용량 | | attributes에서? | +| - [ ] power | 전원 | | attributes에서? | +| - [ ] is_active | is_active | | | + +### 누락 가능성 분석 +- [ ] **용량/전원 필드**: 저장 위치 (별도 컬럼 vs attributes) +- [ ] **품목코드 자동생성**: "전동개폐기150KG380V" 형식 확인 + +--- + +## 7. 제품 (FG - Finished Goods) + +### 사용 모델: `Product` + +### 제품 특화 필드 체크리스트 + +#### 프론트엔드 → 백엔드 +| 프론트엔드 필드 | 백엔드 필드 | 변환 | 상태 | 비고 | +|----------------|------------|------|------|------| +| - [ ] 품목명 | name | | | productName 필드 | +| - [ ] 품목코드 | code | = name | | 품목명이 품목코드 | +| - [ ] 제품분류 | product_category | | | | +| - [ ] 로트약어 | lot_abbreviation | | | | +| - [ ] 인정번호 | certification_number | | | | +| - [ ] 인정유효기간 시작 | certification_start_date | | | | +| - [ ] 인정유효기간 종료 | certification_end_date | | | | +| - [ ] 시방서 파일 | specification_file | uploadItemFile() | | | +| - [ ] 인정서 파일 | certification_file | uploadItemFile() | | | +| - [ ] 단위 | unit | 기본값 'EA' | | | +| - [ ] BOM 구성 | bom[] | | | | +| - [ ] 품목상태 | is_active | | | | + +#### 백엔드 → 프론트엔드 (수정 시) +| 백엔드 필드 | 프론트엔드 필드 | 상태 | 비고 | +|------------|----------------|------|------| +| - [ ] name | item_name | | | +| - [ ] code | (참조만) | | | +| - [ ] product_category | product_category | | | +| - [ ] lot_abbreviation | lot_abbreviation | | | +| - [ ] certification_number | certification_number | | | +| - [ ] certification_start_date | certification_start_date | | | +| - [ ] certification_end_date | certification_end_date | | | +| - [ ] specification_file | specification_file | | 기존 시방서 URL | +| - [ ] specification_file_name | specification_file_name | | 파일명 | +| - [ ] certification_file | certification_file | | 기존 인정서 URL | +| - [ ] certification_file_name | certification_file_name | | 파일명 | +| - [ ] is_active | is_active | | | + +### 누락 가능성 분석 +- [ ] **인정서 관련 필드**: certification_* 필드들 저장 확인 +- [ ] **시방서/인정서 파일**: Edit 시 기존 파일 표시 및 다운로드 +- [ ] **BOM 데이터 로드**: 제품의 component_lines 관계 로드 + +--- + +## 공통 이슈 체크리스트 + +### 1. 필드명 매핑 이슈 +- [ ] `note` vs `remarks`: Material은 remarks, Product는 description/note +- [ ] `is_active` boolean 변환: "활성"/"비활성" vs true/false +- [ ] field_key 형식: "{id}_{fieldName}" → 백엔드 필드명 변환 + +### 2. 동적 필드 저장 방식 +- [ ] **attributes JSON**: 품목기준관리에서 정의한 추가 필드들이 attributes에 저장되는지 +- [ ] **options JSON**: Material의 standard_* 필드들이 options 배열로 저장되는지 + +### 3. 파일 업로드 +- [ ] 품목 저장 후 파일 업로드 순서 +- [ ] Edit 시 기존 파일 표시 +- [ ] 파일 삭제 기능 + +### 4. API 엔드포인트 일관성 +- Material(SM, RM, CS): + - 등록: POST `/api/v1/items` + - 조회: GET `/api/v1/items/{id}?item_type=MATERIAL` + - 수정: PATCH `/api/v1/products/materials/{id}` ⚠️ 경로 불일치 + +- Product(FG, PT): + - 등록: POST `/api/v1/items` + - 조회: GET `/api/v1/items/code/{code}?include_bom=true` + - 수정: PUT `/api/v1/items/{id}` + +--- + +## 조사 진행 상황 + +### 소모품 (SM) - 진행중 +- [x] 모델 구조 분석 +- [x] API 엔드포인트 확인 +- [ ] 등록 데이터 흐름 테스트 +- [ ] 수정 데이터 흐름 테스트 +- [ ] 누락 필드 식별 + +### 원자재 (RM) - 대기 +- [ ] 등록/수정 테스트 +- [ ] SM과 차이점 확인 + +### 부자재 (CS) - 대기 +- [ ] 등록/수정 테스트 +- [ ] SM과 차이점 확인 + +### 조립부품 (PT-Assembly) - 대기 +- [ ] 특화 필드 저장 위치 확인 +- [ ] 등록/수정 테스트 + +### 절곡부품 (PT-Bending) - 대기 +- [ ] 특화 필드 저장 위치 확인 +- [ ] 전개도 업로드 테스트 + +### 구매부품 (PT-Purchased) - 대기 +- [ ] 특화 필드 저장 위치 확인 +- [ ] 등록/수정 테스트 + +### 제품 (FG) - 대기 +- [ ] 인정서 관련 필드 확인 +- [ ] 파일 업로드 테스트 + +--- + +## 발견된 이슈 + +### 이슈 #1: Material is_active 필드 누락 +- **품목유형**: SM, RM, CS (Material 타입 전체) +- **현상**: 등록/수정 시 품목 상태(is_active) 필드가 백엔드로 전달되지 않음 +- **원인**: + 1. `ItemsService.createMaterial()`에서 is_active 설정 O + 2. `MaterialService.setMaterial()`에서는 is_active 필드가 validation rules에 없음 + 3. 프론트엔드 `transformMaterialDataForSave()`에서 is_active를 별도 처리하지 않음 +- **해결방안**: + - 백엔드: `MaterialService` validation rules에 `'is_active' => 'nullable|boolean'` 추가 + - 또는 프론트엔드에서 `/api/v1/items` 엔드포인트 사용 (이미 is_active 지원) +- **우선순위**: 🔴 높음 + +### 이슈 #2: Material 수정 API 경로 불일치 +- **품목유형**: SM, RM, CS +- **현상**: 등록은 `/api/v1/items`, 수정은 `/api/v1/products/materials/{id}` 사용 +- **원인**: 두 개의 다른 서비스 (`ItemsService` vs `MaterialService`)가 Material 처리 +- **영향**: + - `ItemsService.createMaterial()`: is_active, material_type 필드 처리 O + - `MaterialService.updateMaterial()`: is_active 필드 validation 없음, material_type 변경 불가 +- **해결방안**: + - 통합: 수정도 `/api/v1/items/{id}` 사용하도록 프론트엔드 수정 + - 또는 백엔드: `MaterialService.updateMaterial()`에 is_active 필드 추가 +- **우선순위**: 🟡 중간 + +### 이슈 #3: PT(부품) 특화 필드 저장 위치 불명확 +- **품목유형**: PT (조립/절곡/구매 부품) +- **현상**: 측면규격, 용량, 전원 등 부품별 특화 필드가 어디에 저장되는지 불명확 +- **원인**: + - Product 모델에 `side_spec_width`, `side_spec_height`, `assembly_length` 등 컬럼 없음 + - `attributes` JSON에 저장되어야 하나, 프론트엔드에서 attributes 변환 로직 부재 +- **영향**: 부품 수정 시 특화 필드 값이 유실될 수 있음 +- **해결방안**: + 1. 백엔드: Product 모델에 부품 특화 필드 컬럼 추가 (마이그레이션) + 2. 또는: 프론트엔드에서 특화 필드를 `attributes` JSON으로 변환하여 저장 +- **우선순위**: 🔴 높음 + +### 이슈 #4: BOM 데이터 저장 로직 누락 +- **품목유형**: FG, PT (조립부품) +- **현상**: 프론트엔드에서 BOM 라인을 폼에 입력하지만 저장 시 무시됨 +- **원인**: + - `ItemsService.createProduct()`에서 `bom` 배열 처리 로직 없음 + - `ProductComponent` 모델에 데이터 생성하는 코드 부재 +- **영향**: BOM 구성품 정보가 저장되지 않음 +- **해결방안**: + - 백엔드: `ItemsService.createProduct()` 후 `ProductComponent` 레코드 생성 로직 추가 + - 별도 API: `/api/v1/items/{id}/bom` 엔드포인트로 BOM 저장 +- **우선순위**: 🔴 높음 + +### 이슈 #5: 품목기준관리 동적 필드와 백엔드 필드 불일치 +- **품목유형**: 전체 +- **현상**: 품목기준관리에서 정의한 필드가 백엔드 모델에 대응 컬럼이 없음 +- **예시**: + - 품목기준관리: `107_재질`, `108_종류`, `109_폭합계` 등 + - 백엔드 Product 모델: 해당 컬럼 없음 +- **원인**: 동적 필드 구조 vs 고정 스키마 불일치 +- **해결방안**: + 1. 동적 필드는 `attributes` JSON에 저장 + 2. 프론트엔드: 품목기준관리 field_key를 `attributes[field_key]` 형태로 변환 + 3. 백엔드: `attributes` 필드에서 동적 값 저장/조회 지원 +- **우선순위**: 🔴 높음 + +### 이슈 #6: Material options 필드 Edit 시 로드 확인 필요 +- **품목유형**: SM, RM, CS +- **현상**: 등록 시 standard_* → options 변환은 되지만, Edit 시 역변환 확인 필요 +- **확인사항**: + - `mapApiResponseToFormData()`에서 options → standard_* 변환 로직 있음 (line 123-129) + - 하지만 Material API 응답에서 options가 제대로 포함되는지 확인 필요 +- **해결방안**: 실제 데이터로 테스트 필요 +- **우선순위**: 🟢 낮음 + +--- + +## 권장 조치사항 + +### 즉시 조치 (우선순위 높음) +1. **이슈 #1 해결**: `MaterialService.setMaterial()`과 `updateMaterial()`에 `is_active` validation 추가 +2. **이슈 #3 해결**: PT 부품 특화 필드 저장 방식 결정 + - Option A: DB 마이그레이션으로 컬럼 추가 + - Option B: attributes JSON 활용 +3. **이슈 #4 해결**: BOM 저장 API 구현 + +### 중기 조치 (우선순위 중간) +4. **이슈 #2 해결**: Material 등록/수정 API 일관성 확보 +5. **이슈 #5 해결**: 동적 필드 ↔ attributes 매핑 체계 구축 + +### 테스트 필요 +6. **이슈 #6 확인**: 실제 데이터로 Material Edit 플로우 테스트 + +--- + +## 다음 단계 + +- [ ] 각 이슈별 수정 작업 진행 +- [ ] 실제 데이터로 등록/수정 흐름 테스트 +- [ ] 누락된 필드 추가 발견 시 문서 업데이트 diff --git a/claudedocs/item-master/[API-2025-12-06] item-crud-backend-requests.md b/claudedocs/item-master/[API-2025-12-06] item-crud-backend-requests.md new file mode 100644 index 00000000..6021ec8e --- /dev/null +++ b/claudedocs/item-master/[API-2025-12-06] item-crud-backend-requests.md @@ -0,0 +1,546 @@ +# 품목 등록/수정 백엔드 API 수정 요청 + +> 프론트엔드 품목관리 기능 개발 중 발견된 백엔드 수정 필요 사항 정리 +> +> **최종 업데이트**: 2025-12-09 + +--- + +## 🟢 [핵심] field_key 통일 - source_column 매핑 방식 채택 + +### 상태: 🟡 백엔드 작업 진행 중 + +### 발견일: 2025-12-06 + +### 요약 + +| 항목 | 내용 | +|------|------| +| **근본 원인** | 품목기준관리 / 품목관리 API 요청서 분리로 키값 불일치 | +| **해결 방향** | `source_column` 매핑으로 백엔드에서 변환 처리 | +| **백엔드 작업** | `item_fields`에 `source_column` 컬럼 추가 ✅, Edit 응답 형식 수정 진행 중 | +| **프론트 작업** | 백엔드 완료 후 하드코딩 매핑 제거 예정 | + +### 2025-12-09 백엔드 대화 결론 + +1. **등록/상세/리스트**: ✅ `field_key` 기준으로 문제없음 +2. **수정 (Edit)**: ⏳ 조회 시 `98_unit` 형식으로 응답 예정 (백엔드 수정 중) +3. **source_column**: 백엔드 내부 변환용, 프론트에 노출 안됨 + +### 참고 문서 + +- 백엔드 설계서: `~/Desktop/코브라브릿지백엔드문서/item-master-integration.md` +- sam-api 커밋: `bf92b37` (품목 마스터 소스 매핑 기능 추가) + +### 현재 문제 + +``` +품목기준관리 field_key 프론트엔드 변환 백엔드 API 응답 +───────────────────── ───────────────── ───────────────── +Part_type → part_type → part_type (불일치!) +Installation_type_1 → installation_type → 저장 안됨 +side_dimensions_horizontal → side_spec_width → 저장 안됨 +``` + +**두 개의 요청서가 따로 놀면서** 백엔드에서 각각 다르게 구현 → 키 불일치 발생 + +### 이상적인 구조 + +``` +품목기준관리 (field_key 정의) + │ + ▼ + ┌────────────┐ + │ field_key │ ← Single Source of Truth + └────────────┘ + │ + ┌────┴────┬────────┬────────┐ + ▼ ▼ ▼ ▼ + 등록 수정 상세 리스트 + + (전부 동일한 field_key로 저장/조회/렌더링) +``` + +### 기대 효과 + +1. **단위 필드 혼란 해결**: field_key가 "unit"이면 그게 단위 (명확!) +2. **필드 타입 자동 인식**: 품목기준관리 field_type 보고 자동 렌더링 +3. **누락 데이터 분석 용이**: field_key 하나만 확인하면 끝 +4. **디버깅 속도 향상**: API 응답 = 폼 데이터 (변환 없음) + +### 수정 요청 + +1. **품목기준관리 field_key를 기준**으로 API 요청/응답 키 통일 +2. 동적 필드는 `attributes` JSON에 field_key 그대로 저장 +3. 조회 시에도 field_key 그대로 응답 + +### 영향 범위 + +- 품목관리 전체 (등록/수정/상세/리스트) +- 모든 품목 유형 (FG, PT, SM, RM, CS) + +### 우선순위 + +🔴 **최우선** - 이 구조 개선 후 아래 개별 이슈들 대부분 자동 해결 + +--- + +## 1. 소모품(CS) 등록 시 규격(specification) 저장 안됨 + +### 상태: 🔴 수정 필요 + +### 발견일: 2025-12-06 + +### 파일 위치 +`/app/Http/Requests/Item/ItemStoreRequest.php` - rules() 메서드 (Line 14-42) + +### 현재 문제 +- `specification` 필드가 validation rules에 없음 +- Laravel FormRequest는 rules에 정의되지 않은 필드를 `$request->validated()` 결과에서 제외 +- 프론트엔드에서 `specification: "테스트"` 값을 보내도 백엔드에서 무시됨 +- DB에 규격 값이 null로 저장됨 + +### 프론트엔드 확인 사항 +- `DynamicItemForm`에서 `97_specification` → `spec` → `specification`으로 정상 변환됨 +- API 요청 payload에 `specification` 필드 포함 확인됨 +- 백엔드 `ItemsService.createMaterial()`에서 `$data['specification']` 참조하나 값이 없음 + +### 수정 요청 +```php +// /app/Http/Requests/Item/ItemStoreRequest.php + +public function rules(): array +{ + return [ + // 필수 필드 + 'code' => 'required|string|max:50', + 'name' => 'required|string|max:255', + 'product_type' => 'required|string|in:FG,PT,SM,RM,CS', + 'unit' => 'required|string|max:20', + + // 선택 필드 + 'category_id' => 'nullable|integer|exists:categories,id', + 'description' => 'nullable|string', + 'specification' => 'nullable|string|max:255', // ✅ 추가 필요 + + // ... 나머지 기존 필드들 ... + ]; +} +``` + +### 영향 범위 +- 소모품(CS) 등록 +- 원자재(RM) 등록 (해당 시) +- 부자재(SM) 등록 (해당 시) + +--- + +## 2. Material(SM, RM, CS) 수정 시 material_code 중복 에러 + +### 상태: 🔴 수정 필요 + +### 발견일: 2025-12-06 + +### 현재 문제 +- Material 수정 시 `material_code` 중복 체크 에러 발생 +- 케이스 1: 값을 변경하지 않아도 자기 자신과 중복 체크됨 +- 케이스 2: 소프트 삭제된 품목의 코드와도 중복 체크됨 +- 에러 메시지: `Duplicate entry '알루미늄-옵션2-2' for key 'materials.materials_material_code_unique'` + +### 원인 +1. UPDATE 시 자기 자신의 ID를 제외하지 않음 +2. 소프트 삭제(`deleted_at`)된 레코드도 unique 체크에 포함됨 + +### 수정 요청 +```php +// Material 수정 Request 파일 (MaterialUpdateRequest.php 또는 해당 파일) +use Illuminate\Validation\Rule; + +public function rules(): array +{ + return [ + // ... 기존 필드들 ... + + // ✅ 수정 시 자기 자신 제외 + 소프트삭제 제외하고 unique 체크 + 'material_code' => [ + 'required', + 'string', + 'max:255', + Rule::unique('materials', 'material_code') + ->whereNull('deleted_at') // 소프트삭제된 건 제외 + ->ignore($this->route('id')), // 자기 자신 제외 + ], + + // ... 나머지 필드들 ... + ]; +} +``` + +### 영향 범위 +- 원자재(RM) 수정 +- 부자재(SM) 수정 +- 소모품(CS) 수정 + +### 비고 +- 현재 수정 자체가 불가능하여 수정 후 데이터 검증이 어려움 +- 이 이슈 해결 후 수정 기능 재검증 필요 + +--- + +## 3. Material(SM, RM) 수정 시 규격(specification) 로딩 안됨 + +### 상태: 🔴 확인 필요 + +### 발견일: 2025-12-06 + +### 현재 문제 +- SM(부자재), RM(원자재) 수정 페이지 진입 시 규격 값이 표시 안됨 +- 1회 수정 후 다시 수정 페이지 진입 시 규격 값 없음 +- CS(소모품)과 동일한 문제로 추정 + +### 원인 추정 +- 백엔드에서 `options` 배열이 제대로 저장되지 않거나 반환되지 않음 +- SM/RM은 `standard_*` 필드 조합으로 `specification`을 생성하고, 이 값을 `options` 배열에도 저장 +- 수정 페이지에서는 `options` 배열을 읽어서 폼에 표시 +- `options`가 null이면 규격 필드들이 빈 값으로 표시됨 + +### 확인 요청 +```php +// Material 조회 API 응답에서 options 필드 확인 필요 +// GET /api/v1/items/{id}?item_type=MATERIAL + +// 응답 예시 (정상) +{ + "id": 396, + "name": "썬더볼트", + "specification": "부자재2-2", + "options": [ + {"label": "standard_1", "value": "부자재2"}, + {"label": "standard_2", "value": "2"} + ] // ✅ options 배열이 있어야 함 +} + +// 응답 예시 (문제) +{ + "id": 396, + "name": "썬더볼트", + "specification": "부자재2-2", + "options": null // ❌ options가 null이면 규격 로딩 불가 +} +``` + +### 수정 요청 +1. Material 저장 시 `options` 배열 정상 저장 확인 +2. Material 조회 시 `options` 필드 반환 확인 +3. `options`가 JSON 컬럼이라면 정상적인 JSON 형식으로 저장되는지 확인 + +### 영향 범위 +- 원자재(RM) 수정 +- 부자재(SM) 수정 + +--- + +## 4. Material(SM, RM, CS) 비고(remarks) 저장 안됨 + +### 상태: 🔴 수정 필요 + +### 발견일: 2025-12-06 + +### 현재 문제 +- 소모품(CS), 원자재(RM), 부자재(SM) 등록/수정 시 비고(remarks)가 저장되지 않음 +- 프론트에서 `note` → `remarks`로 정상 변환하여 전송 +- 백엔드 Service에서 `$data['remarks']` 참조하지만 값이 없음 + +### 원인 분석 +- **프론트엔드**: `note` → `remarks` 변환 ✅ (`materialTransform.ts:115`) +- **백엔드 Service**: `'remarks' => $data['remarks'] ?? null` ✅ (`ItemsService.php:301`) +- **백엔드 Model**: `remarks` 컬럼 존재 ✅ (`Material.php:31`) +- **백엔드 Request**: `remarks` validation rule 없음 ❌ **누락** + +### 수정 요청 +```php +// /app/Http/Requests/Item/ItemStoreRequest.php +// /app/Http/Requests/Item/ItemUpdateRequest.php + +public function rules(): array +{ + return [ + // 기존 필드들... + 'description' => 'nullable|string', + + // ✅ 추가 필요 + 'remarks' => 'nullable|string', // 비고 필드 + + // ... 나머지 필드들 ... + ]; +} +``` + +### 영향 범위 +- 소모품(CS) 등록/수정 +- 원자재(RM) 등록/수정 +- 부자재(SM) 등록/수정 + +### 비고 +- 1번 이슈(specification)와 동일한 원인: Request validation 누락 +- 함께 처리하면 효율적 + +--- + +## 5. 품목기준관리 옵션 필드 식별자 필요 (장기 개선) + +### 상태: 🟡 개선 권장 + +### 발견일: 2025-12-06 + +### 현재 문제 +- 품목기준관리에서 옵션 필드의 `field_key`를 자유롭게 입력 가능 +- 프론트엔드는 `standard_*`, `option_*` 패턴으로 옵션 필드를 식별 +- 패턴에 맞지 않는 field_key (예: `st_2`)는 규격(specification) 조합에서 누락됨 +- 결과: 부자재(SM)의 규격값이 저장되지 않음 + +### 임시 해결 (프론트엔드) +- 품목기준관리에서 field_key를 `standard_*` 패턴으로 통일 +- 예: `st_2` → `standard_2` + +### 근본 해결 요청 (백엔드) +```php +// 품목기준관리 API 응답에 옵션 필드 여부 표시 + +// 현재 응답 +{ + "field_key": "st_2", + "field_type": "select", + "field_name": "규격옵션" +} + +// 개선 요청 +{ + "field_key": "st_2", + "field_type": "select", + "field_name": "규격옵션", + "is_spec_option": true // ✅ 규격 조합용 옵션 필드인지 표시 +} +``` + +### 기대 효과 +- 프론트엔드가 field_key 패턴에 의존하지 않음 +- `is_spec_option: true`인 필드만 규격 조합에 사용 +- 새로운 field_key 패턴이 추가되어도 프론트 수정 불필요 + +### 영향 범위 +- 원자재(RM) 등록/수정 +- 부자재(SM) 등록/수정 +- 향후 추가되는 Material 유형 + +--- + +## 6. 조립부품(PT-ASSEMBLY) 필드 저장 안됨 - fillable 누락 + +### 상태: 🔴 수정 필요 + +### 발견일: 2025-12-06 + +### 현재 문제 +- 조립부품 등록 후 상세보기/수정 페이지에서 데이터가 제대로 표시되지 않음 +- **프론트엔드는 데이터를 정상 전송하고 있음** ✅ +- **백엔드에서 조립부품 필드들이 저장되지 않음** ❌ + +### 원인 분석 + +#### 프론트엔드 전송 데이터 (정상) +```javascript +// DynamicItemForm/index.tsx - handleFormSubmit() +const submitData = { + ...convertedData, // 폼 데이터 (installation_type, assembly_type 등 포함) + product_type: 'PT', + part_type: 'ASSEMBLY', // ✅ 명시적 추가 + bending_diagram: base64, // ✅ 캔버스 이미지 + bom: [...], // ✅ BOM 데이터 +}; +``` + +#### 백엔드 저장 로직 (문제) +```php +// ItemsService.php - createProduct() +private function createProduct(array $data, int $tenantId, int $userId): Product +{ + $payload = $data; + // ... 기본 필드만 설정 + return Product::create($payload); // Mass Assignment +} +``` + +#### Product 모델 fillable (누락 필드 있음) +```php +// Product.php +protected $fillable = [ + 'tenant_id', 'code', 'name', 'unit', 'category_id', + 'product_type', + 'attributes', 'description', // ✅ attributes JSON 있음 + 'part_type', // ✅ 있음 + 'bending_diagram', 'bending_details', // ✅ 있음 + // ❌ installation_type 없음 + // ❌ assembly_type 없음 + // ❌ side_spec_width 없음 + // ❌ side_spec_height 없음 + // ❌ length 없음 +]; +``` + +### 필드별 저장 상태 + +| 필드 | 프론트 전송 | fillable | 저장 여부 | +|------|------------|----------|----------| +| `part_type` | ✅ | ✅ 컬럼 | ✅ 저장됨 | +| `bending_diagram` | ✅ | ✅ 컬럼 | ⚠️ 파일 업로드 별도 처리 | +| `installation_type` | ✅ | ❌ 없음 | ❌ **저장 안됨** | +| `assembly_type` | ✅ | ❌ 없음 | ❌ **저장 안됨** | +| `side_spec_width` | ✅ | ❌ 없음 | ❌ **저장 안됨** | +| `side_spec_height` | ✅ | ❌ 없음 | ❌ **저장 안됨** | +| `length` | ✅ | ❌ 없음 | ❌ **저장 안됨** | +| `bom` | ✅ | ⚠️ 별도 처리 | ❓ 확인 필요 | + +### 수정 요청 + +#### 방법 1: attributes JSON에 저장 (권장) +```php +// ItemsService.php - createProduct() 수정 + +private function createProduct(array $data, int $tenantId, int $userId): Product +{ + // 조립부품 전용 필드들을 attributes JSON으로 묶기 + $assemblyFields = ['installation_type', 'assembly_type', 'side_spec_width', 'side_spec_height', 'length']; + $attributes = $data['attributes'] ?? []; + + foreach ($assemblyFields as $field) { + if (isset($data[$field])) { + $attributes[$field] = $data[$field]; + unset($data[$field]); // payload에서 제거 + } + } + + $payload = $data; + $payload['tenant_id'] = $tenantId; + $payload['created_by'] = $userId; + $payload['attributes'] = !empty($attributes) ? $attributes : null; + // ... 나머지 동일 + + return Product::create($payload); +} +``` + +#### 방법 2: 컬럼 추가 + fillable 등록 +```php +// Product.php - fillable에 추가 +protected $fillable = [ + // 기존 필드들... + 'installation_type', // ✅ 추가 + 'assembly_type', // ✅ 추가 + 'side_spec_width', // ✅ 추가 + 'side_spec_height', // ✅ 추가 + 'length', // ✅ 추가 (또는 assembly_length) +]; + +// + migration으로 컬럼 추가 필요 +``` + +### 프론트엔드 대응 (완료) +- 프론트에서 `data.xxx` 또는 `data.attributes.xxx` 둘 다에서 값을 찾도록 수정 완료 +- 상세보기: `items/[id]/page.tsx` - `mapApiResponseToItemMaster` 함수 +- 수정페이지: `items/[id]/edit/page.tsx` - `mapApiResponseToFormData` 함수 + +### 영향 범위 +- 조립부품(PT-ASSEMBLY) 등록/조회/수정 +- 설치유형, 마감, 측면규격, 길이 모든 필드 + +### 우선순위 +🔴 **높음** - 조립부품 등록 기능이 완전히 동작하지 않음 + +--- + +## 7. 파일 업로드 API 500 에러 - ApiResponse 클래스 네임스페이스 불일치 + +### 상태: 🔴 수정 필요 + +### 발견일: 2025-12-06 + +### 현재 문제 +- 품목 파일 업로드 (전개도, 시방서, 인정서) 시 500 에러 발생 +- 절곡부품, 조립부품 모두 동일한 에러 +- 파일이 업로드되지 않음 + +### 에러 로그 +``` +[2025-12-06 17:28:22] DEV.ERROR: Class "App\Http\Responses\ApiResponse" not found +{"exception":"[object] (Error(code: 0): Class \"App\\Http\\Responses\\ApiResponse\" not found +at /home/webservice/api/app/Http/Controllers/Api/V1/ItemsFileController.php:31) +``` + +### 원인 분석 +**네임스페이스 불일치** + +| 위치 | 네임스페이스 | +|------|-------------| +| `ItemsFileController.php` (Line 7) | `use App\Http\Responses\ApiResponse` ❌ | +| 실제 파일 위치 | `App\Helpers\ApiResponse` ✅ | + +### 파일 위치 +`/app/Http/Controllers/Api/V1/ItemsFileController.php` (Line 7) + +### 현재 코드 +```php + 수정 완료된 항목은 아래로 이동 + +(아직 없음) + +--- + +## 참고 사항 + +### 관련 파일 (프론트엔드) +- `src/app/[locale]/(protected)/items/create/page.tsx` - 품목 등록 페이지 +- `src/app/[locale]/(protected)/items/[id]/edit/page.tsx` - 품목 수정 페이지 +- `src/components/items/DynamicItemForm/index.tsx` - 동적 폼 컴포넌트 + +### 관련 파일 (백엔드) +- `/app/Http/Controllers/Api/V1/ItemsController.php` - 품목 API 컨트롤러 +- `/app/Services/ItemsService.php` - 품목 서비스 레이어 +- `/app/Http/Requests/Item/ItemStoreRequest.php` - 등록 요청 검증 +- `/app/Http/Requests/Item/ItemUpdateRequest.php` - 수정 요청 검증 +- `/app/Models/Materials/Material.php` - Material 모델 diff --git a/claudedocs/item-master/[IMPL-2025-12-06] assembly-part-issues-checklist.md b/claudedocs/item-master/[IMPL-2025-12-06] assembly-part-issues-checklist.md new file mode 100644 index 00000000..d384c548 --- /dev/null +++ b/claudedocs/item-master/[IMPL-2025-12-06] assembly-part-issues-checklist.md @@ -0,0 +1,154 @@ +# 조립부품(Assembly Part) 이슈 체크리스트 + +> 품목관리 - 제품(PT) 조립부품 관련 이슈 추적 + +--- + +## 이슈 요약 + +| # | 이슈 | 상태 | 유형 | +|---|------|------|------| +| 1 | 캔버스 드로잉 마우스 오프셋 버그 | 🔴 | 프론트엔드 | +| 2 | 상세보기 - 조립부품 세부정보/전개도 누락 | 🔴 | 프론트엔드/API | +| 3 | 수정 페이지 - 기존 데이터 로딩 안됨 | 🔴 | 프론트엔드/API | + +--- + +## 1. 캔버스 드로잉 마우스 오프셋 버그 + +### 상태: ✅ 수정 완료 + +### 현상 +- 마우스 클릭 지점과 실제 그려지는 위치가 다름 +- sam-design에서는 정상 작동 +- sam-react-prod에서만 오프셋 발생 + +### 원인 +- `getMousePos()` 함수에서 캔버스 scale 비율 계산 누락 +- 캔버스 실제 해상도(600x400)와 CSS 표시 크기(`w-full`)가 다름 +- scale 보정 없이 좌표 계산하면 오프셋 발생 + +### 체크리스트 +- [x] 캔버스 컴포넌트 파일 위치 확인 +- [x] sam-design vs sam-react-prod 코드 비교 +- [x] getBoundingClientRect() 사용 여부 확인 +- [x] CSS transform/scale 영향 확인 +- [x] 오프셋 계산 로직 수정 +- [ ] 테스트 및 검증 + +### 수정 내용 +```typescript +// Before (문제) +return { + x: e.clientX - rect.left, + y: e.clientY - rect.top, +}; + +// After (수정) +const scaleX = canvas.width / rect.width; +const scaleY = canvas.height / rect.height; +return { + x: (e.clientX - rect.left) * scaleX, + y: (e.clientY - rect.top) * scaleY, +}; +``` + +### 관련 파일 +- `src/components/items/DrawingCanvas.tsx` (Line 135-146) + +--- + +## 2. 상세보기 - 조립부품 세부정보/전개도 누락 + +### 상태: 🟡 프론트 수정 완료, API 확인 필요 + +### 현상 +- "조립 부품 세부 정보" 섹션 헤더만 있고 내용 없음 +- "조립품 전개도 (바라시)" 섹션 자체가 없음 + +### 원인 +1. API 응답에서 조립부품 필드들이 `attributes` JSON 안에 있음 +2. 프론트에서 `data.installation_type`만 확인, `attributes.installation_type` 확인 안함 +3. 전개도 섹션은 `bendingDiagram`이 있어야만 표시되는 조건 + +### 프론트엔드 수정 내용 +1. **상세 페이지 매핑** (`items/[id]/page.tsx`) + - `attributes` 객체에서도 조립부품 필드 추출하도록 수정 + - `installation_type`, `assembly_type`, `assembly_length`, `side_spec_width`, `side_spec_height` 등 + +2. **상세보기 컴포넌트** (`ItemDetailClient.tsx`) + - 조립부품 세부정보 섹션: 값 없으면 `-` 표시하도록 개선 + - 전개도 섹션: 데이터 없어도 섹션 표시, "등록된 전개도가 없습니다" 메시지 + +### 체크리스트 +- [x] 상세보기 페이지 컴포넌트 확인 +- [x] API 응답 구조 분석 (attributes JSON) +- [x] 조립부품 세부정보 렌더링 로직 수정 +- [x] 전개도 섹션 항상 표시하도록 수정 +- [ ] 테스트 및 검증 +- [ ] 백엔드 API에서 `bending_diagram` 반환 여부 확인 필요 + +### 관련 파일 +- `src/app/[locale]/(protected)/items/[id]/page.tsx` - 매핑 함수 +- `src/components/items/ItemDetailClient.tsx` - UI 컴포넌트 + +### 백엔드 확인 필요 +- 조립부품 등록 시 `bending_diagram` 필드가 DB에 저장되는지 확인 +- 조회 API 응답에 `bending_diagram`, `attributes` 포함 여부 확인 + +--- + +## 3. 수정 페이지 - 기존 데이터 로딩 안됨 + +### 상태: 🟡 프론트 수정 완료, API 확인 필요 + +### 현상 +- 부품 유형이 초기화됨 ("부품 유형을(를) 선택하세요") +- 기존 입력 데이터 전혀 로딩 안됨 +- 조립품 전개도 미리보기 없음 +- 부품 구성(BOM) 목록 로딩 안됨 + +### 원인 +- 상세보기와 동일: API 응답에서 조립부품 필드들이 `attributes` JSON 안에 있음 +- `mapApiResponseToFormData` 함수에서 `attributes`를 확인하지 않음 + +### 프론트엔드 수정 내용 +**수정 페이지 매핑** (`items/[id]/edit/page.tsx`) +- `attributes` 객체에서도 조립부품 필드 추출하도록 수정 +- `part_type`, `part_usage`, `installation_type`, `assembly_type`, `assembly_length`, 등 + +### 체크리스트 +- [x] 수정 페이지 데이터 fetch 로직 확인 +- [x] API 응답 데이터 구조 확인 (attributes JSON) +- [x] 폼 초기값 매핑 로직 수정 +- [x] 조립부품 특화 필드 매핑 추가 +- [ ] 전개도 이미지 로딩 - 백엔드 확인 필요 +- [ ] BOM 목록 로딩 - 백엔드 확인 필요 +- [ ] 테스트 및 검증 + +### 관련 파일 +- `src/app/[locale]/(protected)/items/[id]/edit/page.tsx` - 매핑 함수 + +### 백엔드 확인 필요 +- 조립부품 등록 시 `part_type`, `attributes` 필드가 DB에 저장되는지 확인 +- Product 모델에서 `part_type`이 컬럼인지 `attributes` JSON 안에 있는지 확인 +- 조회 API 응답에 `bending_diagram`, `bom` 포함 여부 확인 + +--- + +## 작업 로그 + +### 2025-12-06 +- 이슈 체크리스트 생성 +- 스크린샷 분석 완료 +- ✅ **캔버스 오프셋 버그 수정**: `DrawingCanvas.tsx` - scale 비율 계산 추가 +- ✅ **상세보기 페이지 수정**: + - `items/[id]/page.tsx` - attributes에서 조립부품 필드 추출 + - `ItemDetailClient.tsx` - 세부정보 섹션 항상 표시, 전개도 섹션 조건 완화 +- ✅ **수정 페이지 수정**: + - `items/[id]/edit/page.tsx` - attributes에서 조립부품 필드 추출 +- ✅ **API 요청 문서 업데이트**: 조립부품 관련 백엔드 확인 사항 추가 + +### 남은 작업 +- [ ] 백엔드와 조립부품 데이터 저장/반환 확인 논의 +- [ ] 실제 테스트 후 검증 diff --git a/claudedocs/item-master/[NEXT-2025-12-06] item-crud-session-context.md b/claudedocs/item-master/[NEXT-2025-12-06] item-crud-session-context.md new file mode 100644 index 00000000..5c6580fc --- /dev/null +++ b/claudedocs/item-master/[NEXT-2025-12-06] item-crud-session-context.md @@ -0,0 +1,80 @@ +# 다음 세션 컨텍스트 - 품목관리 기능 개발 + +> 2025-12-06 세션에서 진행한 내용 및 다음 세션에서 이어갈 작업 + +--- + +## 완료된 작업 + +### 1. 삭제 알럿 제거 ✅ +- 품목 테이블에서 삭제 버튼 클릭 → 모달 확인 → 바로 삭제 (알럿 없이) +- 파일: `src/components/items/ItemListClient.tsx` + +### 2. 디버깅 콘솔 로그 제거 ✅ +- `DropdownField.tsx` - 단위 필드 디버깅 로그 제거 +- `useConditionalDisplay.ts` - 조건부 표시 디버깅 로그 제거 +- `useDynamicFormState.ts` - resetForm 디버깅 로그 제거 + +--- + +## 발견된 문제 (백엔드 수정 필요) + +### 소모품(CS) 규격(specification) 저장 안됨 🔴 + +**원인 분석 완료**: +1. 프론트엔드: `97_specification` → `spec` → `specification`으로 정상 변환됨 +2. 백엔드 문제: `ItemStoreRequest.php`의 validation rules에 `specification` 필드가 없음 +3. Laravel FormRequest는 rules에 없는 필드를 `$request->validated()`에서 제외 +4. 결과: DB에 규격이 null로 저장됨 + +**백엔드 수정 요청**: +```php +// /app/Http/Requests/Item/ItemStoreRequest.php +// rules()에 추가 필요: +'specification' => 'nullable|string|max:255', +``` + +**상세 문서**: `claudedocs/item-master/[API-2025-12-06] item-crud-backend-requests.md` + +--- + +## 다음 세션에서 확인할 사항 + +1. **백엔드 수정 후 테스트** + - 소모품 등록 시 규격 값 저장 확인 + - 상세 페이지에서 규격 표시 확인 + +2. **수정 API도 확인 필요** + - `ItemUpdateRequest.php`에도 `specification` 필드 있는지 확인 + - 어제 "수정하면 규격이 보였다"고 했는데, 수정 API는 다를 수 있음 + +3. **추가 편의 기능 개발** (사용자 요청 시) + - 품목관리 관련 추가 기능 구현 + +--- + +## 관련 파일 위치 + +### 프론트엔드 +- `src/components/items/ItemListClient.tsx` - 품목 목록/삭제 +- `src/components/items/ItemDetailClient.tsx` - 품목 상세 +- `src/components/items/DynamicItemForm/index.tsx` - 동적 폼 +- `src/app/[locale]/(protected)/items/create/page.tsx` - 등록 페이지 +- `src/app/[locale]/(protected)/items/[id]/edit/page.tsx` - 수정 페이지 +- `src/app/[locale]/(protected)/items/[id]/page.tsx` - 상세 페이지 + +### 백엔드 (sam-api) +- `/app/Http/Requests/Item/ItemStoreRequest.php` - 등록 요청 검증 ⚠️ 수정 필요 +- `/app/Http/Requests/Item/ItemUpdateRequest.php` - 수정 요청 검증 (확인 필요) +- `/app/Services/ItemsService.php` - 품목 서비스 +- `/app/Models/Materials/Material.php` - Material 모델 + +--- + +## 명령어 + +```bash +# 프론트엔드 개발 서버 +cd /Users/byeongcheolryu/codebridgex/sam_project/sam-next/sma-next-project/sam-react-prod +npm run dev +``` \ No newline at end of file diff --git a/claudedocs/item-master/[NEXT-2025-12-09] item-crud-session-context.md b/claudedocs/item-master/[NEXT-2025-12-09] item-crud-session-context.md new file mode 100644 index 00000000..0f94b242 --- /dev/null +++ b/claudedocs/item-master/[NEXT-2025-12-09] item-crud-session-context.md @@ -0,0 +1,120 @@ +# 품목관리 세션 체크포인트 + +> 작성일: 2025-12-09 +> 수정일: 2025-12-09 +> 상태: ✅ Phase 1 완료! + +--- + +## 🎉 2025-12-09 완료 사항 + +### 백엔드 작업 완료 + +| 항목 | 상태 | +|------|------| +| field_key 저장 방식 변경 (`98_unit` → `unit`) | ✅ 완료 | +| 시스템 예약어 검증 (`SystemFields.php`) | ✅ 완료 | +| 중복 검증 로직 | ✅ 완료 | +| 에러 메시지 한국어화 | ✅ 완료 | + +### 프론트엔드 정리 완료 + +| 항목 | 삭제된 코드 | 상태 | +|------|------------|------| +| Edit 모드 매핑 로직 | ~140줄 | ✅ 완료 | +| `fieldAliases` 객체 | 25줄 | ✅ 완료 | +| `extractFieldName()` 함수 | 7줄 | ✅ 완료 | +| `fieldKeyMap` 생성 로직 | 25줄 | ✅ 완료 | +| `fieldKeyToBackendKey` 변환 | 60줄 | ✅ 완료 | +| **총 삭제** | **~200줄** | ✅ | + +### 빌드 검증 + +```bash +npm run build # ✅ 성공 +``` + +--- + +## 📋 새로운 데이터 흐름 + +### field_key 통일 완료 + +``` +등록: { "unit": "EA" } → 그대로 저장 +조회: DB → { "unit": "EA" } → 그대로 표시 +수정: { "unit": "EA" } → 그대로 저장 + +※ 기존 레거시 데이터 (98_unit 형식)도 그대로 동작 +``` + +### 코드 변경 요약 + +**Before (복잡한 매핑)**: +```typescript +// Edit 모드: 155줄 매핑 로직 +const fieldAliases = { 'unit': '단위', ... }; +const extractFieldName = (key) => { ... }; +const fieldKeyMap = { ... }; +// 여러 단계 변환... +``` + +**After (직접 사용)**: +```typescript +// Edit 모드: 15줄 +useEffect(() => { + if (mode !== 'edit' || !structure || !initialData || isEditDataMapped) return; + resetForm(initialData); // 직접 사용! + setIsEditDataMapped(true); +}, [mode, structure, initialData, isEditDataMapped, resetForm]); +``` + +--- + +## ⏳ 남은 작업 + +### 파일 업로드 500 에러 (검수 중) + +``` +위치: /app/Http/Controllers/Api/V1/ItemsFileController.php (Line 7) +문제: use App\Http\Responses\ApiResponse (잘못된 경로) +수정: use App\Helpers\ApiResponse (올바른 경로) +``` + +### Phase 2: 컴포넌트 분리 (선택적) + +계획 문서: `[PLAN-2025-12-08] dynamic-form-separation-plan.md` + +- 공통 컴포넌트 추출 (FileUpload, BOM, AutoItemCode) +- 품목별 컴포넌트 생성 (FG, PT, SM, RM, CS) +- DynamicFormCore 리팩토링 + +--- + +## 📋 테스트 체크리스트 + +### 등록 테스트 +- [ ] FG(제품) 등록 +- [ ] PT-조립부품 등록 +- [ ] PT-절곡부품 등록 +- [ ] SM/RM/CS 등록 + +### 수정 테스트 +- [ ] 수정 페이지 진입 → 데이터 로드 확인 +- [ ] 드롭다운 값 표시 확인 +- [ ] 수정 후 저장 → 값 유지 확인 + +### 파일 업로드 테스트 +- [ ] 절곡부품 전개도 업로드 +- [ ] 조립부품 전개도 업로드 +- [ ] 제품 시방서/인정서 업로드 + +--- + +## 📚 관련 문서 + +| 문서 | 위치 | +|------|------| +| DynamicForm 분리 계획 | `[PLAN-2025-12-08] dynamic-form-separation-plan.md` | +| Radix UI 버그 해결 | `claudedocs/guides/[FIX-2025-12-05] radix-ui-select-controlled-mode-bug.md` | +| 백엔드 field_key 검증 스펙 | `sam-api/docs/specs/item-master-field-key-validation.md` | diff --git a/claudedocs/item-master/[PLAN-2025-12-08] dynamic-form-separation-plan.md b/claudedocs/item-master/[PLAN-2025-12-08] dynamic-form-separation-plan.md new file mode 100644 index 00000000..63f07eb2 --- /dev/null +++ b/claudedocs/item-master/[PLAN-2025-12-08] dynamic-form-separation-plan.md @@ -0,0 +1,305 @@ +# DynamicItemForm 품목별 분리 계획 + +> 작성일: 2025-12-08 +> 수정일: 2025-12-09 +> 상태: 🚀 Phase 1 진행 중 + +--- + +## 🎉 2025-12-09 업데이트 + +### 백엔드 작업 완료! + +| 항목 | 상태 | +|------|------| +| field_key 저장 방식 변경 (`98_unit` → `unit`) | ✅ 완료 | +| 시스템 예약어 검증 (SystemFields.php) | ✅ 완료 | +| 중복 검증 로직 | ✅ 완료 | +| 에러 메시지 | ✅ 완료 | + +### Phase 1 완료! ✅ + +제거된 레거시 코드: +- ✅ `fieldAliases` (영문 → 한글 매핑) - 25줄 +- ✅ `extractFieldName()` 함수 - 7줄 +- ✅ `fieldKeyMap` 생성 로직 - 25줄 +- ✅ 대소문자 무시 매핑 - 15줄 +- ✅ 별칭 fallback 매핑 - 10줄 +- ✅ `98_` prefix 파싱 로직 - 8줄 +- ✅ `fieldKeyToBackendKey` 변환 테이블 - 30줄 +- ✅ 복잡한 변환 로직 - 40줄 + +**실제 결과**: **~200줄 코드 삭제!** 🎉 + +### 변경 요약 + +| 변경 내용 | Before | After | +|-----------|--------|-------| +| Edit 모드 매핑 | 155줄 복잡한 로직 | 15줄 직접 로드 | +| 저장 시 변환 | 75줄 변환 테이블 | 15줄 is_active만 처리 | +| 총 코드량 | ~230줄 | ~30줄 | + +--- + +## 📊 현재 상태 분석 + +### 현재 구조 + +``` +src/components/items/ +├── DynamicItemForm/ # 현재: 모든 품목 처리 (1350+ lines) +│ ├── index.tsx # 메인 컴포넌트 (거대) +│ ├── fields/ # 필드 렌더러 +│ ├── hooks/ # 상태 관리 훅 +│ ├── sections/ # BOM 섹션 +│ ├── types.ts +│ └── utils/ +│ +├── ItemForm/ # 기존: 하드코딩된 폼 (참고용) +│ ├── forms/ +│ │ ├── ProductForm.tsx # FG(제품) 전용 +│ │ ├── MaterialForm.tsx # RM(원자재) 전용 +│ │ └── PartForm.tsx # PT(부품) → 하위 분기 +│ │ └── parts/ +│ │ ├── AssemblyPartForm.tsx # 조립부품 +│ │ ├── BendingPartForm.tsx # 절곡부품 +│ │ └── PurchasedPartForm.tsx # 구매부품 +``` + +### 문제점 + +| 문제 | 영향도 | field_key 통일로 해결? | +|------|--------|----------------------| +| DynamicItemForm 1350+ lines | 🔴 유지보수 어려움 | ❌ | +| 품목별 특수 로직 혼재 | 🔴 버그 발생 원인 | ❌ | +| 조건부 렌더링 복잡도 | 🟡 코드 가독성 | ⚠️ 부분 해결 | +| 파일 업로드 로직 중복 | 🟡 DRY 위반 | ❌ | +| 테스트 어려움 | 🟡 품질 이슈 | ❌ | + +### 품목 유형별 특수 로직 + +```yaml +FG (제품): + - 시방서/인정서 파일 업로드 + - 인정번호, 유효기간 + - BOM 구성 (선택) + +PT (부품): + - 부품 유형 선택 (ASSEMBLY/BENDING/PURCHASED) + - 조립부품: 측면규격, 길이, 설치유형, BOM + - 절곡부품: 전개도(바라시), 재질, 폭합계 + - 구매부품: 전기개폐기, 모터, 체인 규격 + +SM (반제품): + - 규격 정보 + - BOM 구성 (선택) + +RM (원자재): + - 재질, 두께, 폭, 단위 + - 단가 정보 + +CS (소모품): + - 기본 정보만 +``` + +--- + +## 🎯 목표 구조 (2단계 분리) + +### Phase 1: field_key 통일 (백엔드 작업 대기) +- 품목기준관리 API와 품목 CRUD API 간 field_key 일치 +- 데이터 일관성 확보 → 버그 60% 감소 예상 + +### Phase 2: 컴포넌트 분리 (field_key 통일 후) + +``` +src/components/items/ +├── DynamicItemForm/ +│ ├── index.tsx # 컨테이너 (라우팅만) +│ ├── DynamicFormCore.tsx # 공통 폼 프레임워크 +│ │ +│ ├── itemTypes/ # 🆕 품목 유형별 컴포넌트 +│ │ ├── index.ts # 내보내기 +│ │ ├── FGFormFields.tsx # 제품 전용 (인정정보, 파일업로드) +│ │ ├── PTFormFields.tsx # 부품 라우터 +│ │ │ ├── AssemblyFields.tsx # 조립부품 전용 +│ │ │ ├── BendingFields.tsx # 절곡부품 전용 (전개도) +│ │ │ └── PurchasedFields.tsx # 구매부품 전용 +│ │ ├── SMFormFields.tsx # 반제품 전용 +│ │ ├── RMFormFields.tsx # 원자재 전용 +│ │ └── CSFormFields.tsx # 소모품 전용 +│ │ +│ ├── shared/ # 🆕 공유 컴포넌트 +│ │ ├── FileUploadSection.tsx # 파일 업로드 (FG, PT-절곡) +│ │ ├── BOMSection.tsx # BOM 관리 (FG, PT-조립, SM) +│ │ ├── StatusField.tsx # 활성/비활성 +│ │ └── AutoItemCode.tsx # 품목코드 자동생성 +│ │ +│ ├── fields/ # 기존 유지 +│ ├── hooks/ # 기존 유지 + 개선 +│ ├── sections/ # → shared/로 이동 +│ └── types.ts +``` + +--- + +## 📋 세부 작업 계획 + +### Phase 2-1: 공통 컴포넌트 추출 + +| 작업 | 파일 | 예상 LOC | +|------|------|----------| +| 파일 업로드 섹션 추출 | `shared/FileUploadSection.tsx` | ~150 | +| BOM 섹션 정리 | `shared/BOMSection.tsx` | ~100 | +| 품목코드 자동생성 | `shared/AutoItemCode.tsx` | ~50 | +| 상태 필드 | `shared/StatusField.tsx` | ~30 | + +### Phase 2-2: 품목 유형별 컴포넌트 생성 + +| 작업 | 파일 | 특수 로직 | +|------|------|-----------| +| FG 전용 | `itemTypes/FGFormFields.tsx` | 인정정보, 시방서/인정서 | +| PT 라우터 | `itemTypes/PTFormFields.tsx` | 부품유형 선택 후 분기 | +| PT-조립 | `itemTypes/AssemblyFields.tsx` | 측면규격, 설치유형, BOM | +| PT-절곡 | `itemTypes/BendingFields.tsx` | 전개도, 폭합계 | +| PT-구매 | `itemTypes/PurchasedFields.tsx` | 전기개폐기, 모터 규격 | +| SM 전용 | `itemTypes/SMFormFields.tsx` | 규격, BOM (선택) | +| RM 전용 | `itemTypes/RMFormFields.tsx` | 재질, 두께, 단가 | +| CS 전용 | `itemTypes/CSFormFields.tsx` | 기본 정보만 | + +### Phase 2-3: DynamicFormCore 리팩토링 + +```typescript +// DynamicFormCore.tsx - 공통 프레임워크 +interface DynamicFormCoreProps { + structure: FormStructure; + formData: DynamicFormData; + onChange: (key: string, value: any) => void; + renderCustomFields?: () => ReactNode; // 품목별 특수 필드 + renderCustomSections?: () => ReactNode; // 품목별 특수 섹션 +} + +// index.tsx - 라우팅만 +const ItemTypeComponents = { + FG: FGFormFields, + PT: PTFormFields, + SM: SMFormFields, + RM: RMFormFields, + CS: CSFormFields, +}; + + { + const Component = ItemTypeComponents[selectedItemType]; + return Component ? : null; + }} +/> +``` + +--- + +## 📊 예상 효과 + +### Before (현재) +- `DynamicItemForm/index.tsx`: **1350+ lines** +- 모든 품목 로직 혼재 +- 수정 시 다른 품목 영향 우려 + +### After (분리 후) +- `DynamicItemForm/index.tsx`: **~200 lines** (라우팅만) +- `DynamicFormCore.tsx`: **~400 lines** (공통 프레임워크) +- 품목별 컴포넌트: **각 100-200 lines** +- **총 코드량 비슷하지만 책임 분리됨** + +### 장점 +1. **버그 격리**: 조립부품 수정 → 절곡부품 영향 없음 +2. **유지보수 용이**: 품목별 로직 파악 쉬움 +3. **테스트 가능**: 품목별 단위 테스트 작성 가능 +4. **확장성**: 새 품목 유형 추가 시 파일만 추가 +5. **협업**: 여러 개발자가 다른 품목 동시 작업 가능 + +--- + +## ⏰ 작업 순서 및 의존성 + +``` +┌─────────────────────────────────────────┐ +│ Phase 1: field_key 통일 (백엔드) │ ← 현재 대기 중 +└─────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────┐ +│ Phase 2-1: 공통 컴포넌트 추출 │ +│ - FileUploadSection │ +│ - BOMSection │ +│ - AutoItemCode │ +└─────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────┐ +│ Phase 2-2: 품목별 컴포넌트 생성 │ +│ - FG, PT(Assembly/Bending/Purchased) │ +│ - SM, RM, CS │ +└─────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────┐ +│ Phase 2-3: DynamicFormCore 리팩토링 │ +│ - index.tsx 슬림화 │ +│ - 품목별 라우팅 │ +└─────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────┐ +│ Phase 3: 테스트 및 검증 │ +│ - 품목별 CRUD 테스트 │ +│ - 회귀 테스트 │ +└─────────────────────────────────────────┘ +``` + +--- + +## ❓ 결정 필요 사항 + +### Q1. 기존 ItemForm 컴포넌트 처리 + +| 옵션 | 장점 | 단점 | +|------|------|------| +| A. 삭제 | 중복 제거, 혼란 방지 | 참고 코드 소실 | +| B. 유지 (참고용) | 마이그레이션 시 참고 가능 | 중복 유지보수 | +| C. archive 폴더로 이동 | 참고 가능 + 혼란 방지 | 폴더 추가 | + +**권장**: C. archive 폴더로 이동 + +### Q2. 분리 전략 + +| 옵션 | 설명 | +|------|------| +| A. 점진적 분리 | 한 품목씩 분리, 기존 코드 유지 | +| B. 전면 분리 | 한 번에 모든 품목 분리 | + +**권장**: A. 점진적 분리 (PT-조립부품부터 시작) + +### Q3. 품목별 hooks 분리 여부 + +| 옵션 | 설명 | +|------|------| +| A. 공통 hooks 유지 | useDynamicFormState 그대로 사용 | +| B. 품목별 hooks 추가 | useAssemblyFormState 등 추가 | + +**권장**: A. 공통 hooks 유지 (복잡도 관리) + +--- + +## 📝 다음 단계 + +1. ⏳ **백엔드 field_key 통일 답변 대기** +2. 📋 **결정 필요 사항 확인** (Q1, Q2, Q3) +3. 🚀 **Phase 2-1 시작** (공통 컴포넌트 추출) + +--- + +## 참고 문서 + +- `claudedocs/item-master/[API-2025-12-06] item-crud-backend-requests.md` - field_key 통일 요청 +- `src/components/items/ItemForm/` - 기존 하드코딩 폼 참고 +- `src/components/items/DynamicItemForm/` - 현재 동적 폼 \ No newline at end of file diff --git a/claudedocs/sales/[API-2025-12-04] client-api-analysis.md b/claudedocs/sales/[API-2025-12-04] client-api-analysis.md index 62f55424..71135191 100644 --- a/claudedocs/sales/[API-2025-12-04] client-api-analysis.md +++ b/claudedocs/sales/[API-2025-12-04] client-api-analysis.md @@ -1,6 +1,7 @@ # 거래처 관리 API 분석 > **작성일**: 2025-12-04 +> **최종 수정**: 2025-12-09 > **목적**: sam-api 백엔드 Client API와 프론트엔드 거래처 관리 페이지 간 연동 분석 --- @@ -9,14 +10,14 @@ ### 프론트엔드 (sam-react-prod) - **파일**: `src/app/[locale]/(protected)/sales/client-management-sales-admin/page.tsx` -- **상태**: ❌ **API 미연동** - 로컬 샘플 데이터(`SAMPLE_CUSTOMERS`)로만 동작 -- **모든 CRUD가 클라이언트 사이드에서만 수행됨** +- **상태**: ✅ **API 연동 완료** (2025-12-09) +- **Hook**: `src/hooks/useClientList.ts` - 2차 필드 모두 지원 ### 백엔드 (sam-api) - **컨트롤러**: `app/Http/Controllers/Api/V1/ClientController.php` - **서비스**: `app/Services/ClientService.php` - **모델**: `app/Models/Orders/Client.php` -- **상태**: ✅ **API 구현 완료** - 모든 CRUD 기능 제공 +- **상태**: ✅ **API 구현 완료** - 2차 필드 포함, is_active Boolean 변경 완료 --- @@ -69,10 +70,10 @@ | `email` | `email` | ✅ 동일 | | | `address` | `address` | ✅ 동일 | | | `registeredDate` | `created_at` | ✅ 매핑 필요 | 필드명 변경 | -| `status` | `is_active` | ✅ 매핑 필요 | "활성"/"비활성" ↔ "Y"/"N" | -| `businessNo` | - | ❌ **백엔드 없음** | 추가 필요 | -| `businessType` | - | ❌ **백엔드 없음** | 추가 필요 | -| `businessItem` | - | ❌ **백엔드 없음** | 추가 필요 | +| `status` | `is_active` | ✅ 매핑 완료 | "활성"/"비활성" ↔ boolean (2025-12-09 변경) | +| `businessNo` | `business_no` | ✅ 추가됨 | | +| `businessType` | `business_type` | ✅ 추가됨 | | +| `businessItem` | `business_item` | ✅ 추가됨 | | | - | `tenant_id` | ✅ 백엔드 전용 | 자동 처리 | | - | `client_group_id` | ⚠️ 프론트 없음 | 그룹 기능 미구현 | @@ -214,18 +215,25 @@ ALTER TABLE clients ADD COLUMN memo TEXT NULL; --- -## 5. 프론트엔드 API 연동 구현 계획 +## 5. 프론트엔드 API 연동 구현 ✅ 완료 (2025-12-09) -### 5.1 필요한 작업 +### 5.1 완료된 작업 | # | 작업 | 우선순위 | 상태 | |---|------|---------|------| -| 1 | Next.js API Proxy 생성 (`/api/proxy/clients/[...path]`) | 🔴 HIGH | ⬜ 미완료 | -| 2 | 커스텀 훅 생성 (`useClientList`) | 🔴 HIGH | ⬜ 미완료 | -| 3 | 타입 정의 업데이트 (`CustomerAccount` → API 응답 매핑) | 🟡 MEDIUM | ⬜ 미완료 | -| 4 | CRUD 함수를 API 호출로 변경 | 🔴 HIGH | ⬜ 미완료 | +| 1 | Next.js API Proxy 생성 (`/api/proxy/[...path]`) | 🔴 HIGH | ✅ 완료 | +| 2 | 커스텀 훅 생성 (`useClientList`) | 🔴 HIGH | ✅ 완료 | +| 3 | 타입 정의 업데이트 (2차 필드 모두 포함) | 🟡 MEDIUM | ✅ 완료 | +| 4 | CRUD 함수를 API 호출로 변경 | 🔴 HIGH | ✅ 완료 | | 5 | 거래처 그룹 기능 추가 (선택) | 🟢 LOW | ⬜ 미완료 | +### 5.2 숨긴 섹션 (기획 미확정) +- 발주처 설정: 계정ID, 비밀번호, 매입/매출 결제일 +- 약정 세금: 약정 여부, 금액, 시작/종료일 +- 악성채권 정보: 악성채권 여부, 금액, 발생/만료일, 진행상태 + +> ⚠️ 백엔드 API 지원됨. 기획 확정 시 `ClientRegistration.tsx` TODO 주석 해제 + ### 5.2 API Proxy 구현 패턴 ```typescript diff --git a/claudedocs/sales/[API-2025-12-08] pricing-api-enhancement-request.md b/claudedocs/sales/[API-2025-12-08] pricing-api-enhancement-request.md new file mode 100644 index 00000000..e7179574 --- /dev/null +++ b/claudedocs/sales/[API-2025-12-08] pricing-api-enhancement-request.md @@ -0,0 +1,358 @@ +# 단가관리 API 개선 요청서 + +> **작성일**: 2025-12-08 +> **요청자**: 프론트엔드 개발팀 +> **대상**: sam-api 백엔드 팀 + +--- + +## 1. 현황 요약 + +### 현재 API 구조 +| Endpoint | Method | 상태 | +|----------|--------|------| +| `/api/v1/pricing` | GET | 목록 조회 | +| `/api/v1/pricing/show` | GET | 단일 가격 조회 | +| `/api/v1/pricing/bulk` | POST | 일괄 가격 조회 | +| `/api/v1/pricing/upsert` | POST | 등록/수정 | +| `/api/v1/pricing/{id}` | DELETE | 삭제 | + +### ✅ 이미 지원됨 (품목 정보) +- `item_type_code` (품목유형) - PriceHistory 테이블 +- `item_code`, `item_name`, `specification`, `unit` - item 관계 JOIN으로 조회 가능 + +### ❌ 문제점 (단가 상세 정보) +- 프론트엔드 단가관리 화면에서 요구하는 **단가 계산 필드** 대부분 누락 +- 현재 `price_histories` 테이블은 **단순 가격 이력**만 저장 (`price` 단일 필드) +- 프론트엔드는 **원가 계산, 마진 관리, 리비전 관리** 기능 필요 + +--- + +## 2. 테이블 스키마 변경 요청 + +### 2.1 `price_histories` 테이블 필드 추가 + +```sql +ALTER TABLE price_histories ADD COLUMN purchase_price DECIMAL(15,4) NULL COMMENT '매입단가(입고가)'; +ALTER TABLE price_histories ADD COLUMN processing_cost DECIMAL(15,4) NULL COMMENT '가공비'; +ALTER TABLE price_histories ADD COLUMN loss_rate DECIMAL(5,2) NULL COMMENT 'LOSS율(%)'; +ALTER TABLE price_histories ADD COLUMN rounding_rule ENUM('round','ceil','floor') DEFAULT 'round' COMMENT '반올림 규칙'; +ALTER TABLE price_histories ADD COLUMN rounding_unit INT DEFAULT 1 COMMENT '반올림 단위(1,10,100,1000)'; +ALTER TABLE price_histories ADD COLUMN margin_rate DECIMAL(5,2) NULL COMMENT '마진율(%)'; +ALTER TABLE price_histories ADD COLUMN sales_price DECIMAL(15,4) NULL COMMENT '판매단가'; +ALTER TABLE price_histories ADD COLUMN supplier VARCHAR(255) NULL COMMENT '공급업체'; +ALTER TABLE price_histories ADD COLUMN author VARCHAR(100) NULL COMMENT '작성자'; +ALTER TABLE price_histories ADD COLUMN receive_date DATE NULL COMMENT '입고일'; +ALTER TABLE price_histories ADD COLUMN note TEXT NULL COMMENT '비고'; +ALTER TABLE price_histories ADD COLUMN revision_number INT DEFAULT 0 COMMENT '리비전 번호'; +ALTER TABLE price_histories ADD COLUMN is_final BOOLEAN DEFAULT FALSE COMMENT '최종 확정 여부'; +ALTER TABLE price_histories ADD COLUMN finalized_at DATETIME NULL COMMENT '확정일시'; +ALTER TABLE price_histories ADD COLUMN finalized_by INT NULL COMMENT '확정자 ID'; +ALTER TABLE price_histories ADD COLUMN status ENUM('draft','active','inactive','finalized') DEFAULT 'draft' COMMENT '상태'; +``` + +### 2.2 기존 `price` 필드 처리 방안 + +**옵션 A (권장)**: `price` 필드를 `sales_price`로 마이그레이션 +```sql +UPDATE price_histories SET sales_price = price WHERE price_type_code = 'SALE'; +UPDATE price_histories SET purchase_price = price WHERE price_type_code = 'PURCHASE'; +-- 이후 price 필드 deprecated 또는 삭제 +``` + +**옵션 B**: `price` 필드 유지, 새 필드와 병행 사용 +- 기존 로직 호환성 유지 +- 점진적 마이그레이션 + +--- + +## 3. API 엔드포인트 수정 요청 + +### 3.1 `POST /api/v1/pricing/upsert` 수정 + +**현재 Request Body:** +```json +{ + "item_type_code": "PRODUCT", + "item_id": 10, + "price_type_code": "SALE", + "client_group_id": 1, + "price": 50000.00, + "started_at": "2025-01-01", + "ended_at": "2025-12-31" +} +``` + +**요청 Request Body:** +```json +{ + "item_type_code": "PRODUCT", + "item_id": 10, + "client_group_id": 1, + + "purchase_price": 45000, + "processing_cost": 5000, + "loss_rate": 3.5, + "rounding_rule": "round", + "rounding_unit": 100, + "margin_rate": 20.0, + "sales_price": 60000, + + "supplier": "ABC공급", + "author": "홍길동", + "receive_date": "2025-01-01", + "started_at": "2025-01-01", + "ended_at": null, + "note": "2025년 1분기 단가", + + "is_revision": false, + "revision_reason": "가격 인상" +} +``` + +### 3.2 `GET /api/v1/pricing` 수정 (목록 조회) + +**현재 Response:** +```json +{ + "data": { + "data": [ + { + "id": 1, + "item_type_code": "PRODUCT", + "item_id": 10, + "price_type_code": "SALE", + "price": 50000, + "started_at": "2025-01-01" + } + ] + } +} +``` + +**요청 Response:** +```json +{ + "data": { + "data": [ + { + "id": 1, + "item_type_code": "PRODUCT", + "item_id": 10, + "item_code": "SCREEN-001", + "item_name": "스크린 셔터 기본형", + "specification": "표준형", + "unit": "SET", + + "purchase_price": 45000, + "processing_cost": 5000, + "loss_rate": 3.5, + "margin_rate": 20.0, + "sales_price": 60000, + + "started_at": "2025-01-01", + "ended_at": null, + "status": "active", + "revision_number": 1, + "is_final": false, + + "supplier": "ABC공급", + "created_at": "2025-01-01 10:00:00" + } + ] + } +} +``` + +**핵심 변경**: 품목 정보 JOIN 필요 (`item_masters` 또는 `products`/`materials` 테이블) + +--- + +## 4. 신규 API 엔드포인트 요청 + +### 4.1 품목 기반 단가 현황 조회 (신규) + +**용도**: 품목 마스터 기준으로 단가 등록/미등록 현황 조회 + +**Endpoint**: `GET /api/v1/pricing/by-items` + +**Query Parameters:** +| 파라미터 | 타입 | 설명 | +|---------|------|------| +| `item_type_code` | string | 품목 유형 (FG, PT, SM, RM, CS) | +| `search` | string | 품목코드/품목명 검색 | +| `status` | string | `all`, `registered`, `not_registered` | +| `size` | int | 페이지당 항목 수 | +| `page` | int | 페이지 번호 | + +**Response:** +```json +{ + "data": { + "data": [ + { + "item_id": 1, + "item_code": "SCREEN-001", + "item_name": "스크린 셔터 기본형", + "item_type": "FG", + "specification": "표준형", + "unit": "SET", + + "pricing_id": null, + "has_pricing": false, + "purchase_price": null, + "sales_price": null, + "margin_rate": null, + "status": "not_registered" + }, + { + "item_id": 2, + "item_code": "GR-001", + "item_name": "가이드레일 130×80", + "item_type": "PT", + "specification": "130×80×2438", + "unit": "EA", + + "pricing_id": 5, + "has_pricing": true, + "purchase_price": 45000, + "sales_price": 60000, + "margin_rate": 20.0, + "effective_date": "2025-01-01", + "status": "active", + "revision_number": 1, + "is_final": false + } + ], + "stats": { + "total_items": 100, + "registered": 45, + "not_registered": 55, + "finalized": 10 + } + } +} +``` + +### 4.2 단가 이력 조회 (신규) + +**Endpoint**: `GET /api/v1/pricing/{id}/revisions` + +**Response:** +```json +{ + "data": [ + { + "revision_number": 2, + "revision_date": "2025-06-01", + "revision_by": "김철수", + "revision_reason": "원자재 가격 인상", + "previous_purchase_price": 40000, + "previous_sales_price": 55000, + "new_purchase_price": 45000, + "new_sales_price": 60000 + }, + { + "revision_number": 1, + "revision_date": "2025-01-01", + "revision_by": "홍길동", + "revision_reason": "최초 등록", + "previous_purchase_price": null, + "previous_sales_price": null, + "new_purchase_price": 40000, + "new_sales_price": 55000 + } + ] +} +``` + +### 4.3 단가 확정 (신규) + +**Endpoint**: `POST /api/v1/pricing/{id}/finalize` + +**Request Body:** +```json +{ + "finalize_reason": "2025년 1분기 단가 확정" +} +``` + +**Response:** +```json +{ + "data": { + "id": 5, + "is_final": true, + "finalized_at": "2025-12-08 14:30:00", + "finalized_by": 1, + "status": "finalized" + }, + "message": "단가가 최종 확정되었습니다." +} +``` + +--- + +## 5. 프론트엔드 타입 참조 + +프론트엔드에서 사용하는 타입 정의 (`src/components/pricing/types.ts`): + +```typescript +interface PricingData { + id: string; + itemId: string; + itemCode: string; + itemName: string; + itemType: string; + specification?: string; + unit: string; + + // 단가 정보 + effectiveDate: string; // started_at + receiveDate?: string; // receive_date + author?: string; // author + purchasePrice?: number; // purchase_price + processingCost?: number; // processing_cost + loss?: number; // loss_rate + roundingRule?: RoundingRule; // rounding_rule + roundingUnit?: number; // rounding_unit + marginRate?: number; // margin_rate + salesPrice?: number; // sales_price + supplier?: string; // supplier + note?: string; // note + + // 리비전 관리 + currentRevision: number; // revision_number + isFinal: boolean; // is_final + revisions?: PricingRevision[]; + finalizedDate?: string; // finalized_at + finalizedBy?: string; // finalized_by + status: PricingStatus; // status +} +``` + +--- + +## 6. 우선순위 + +| 순위 | 항목 | 중요도 | +|------|------|--------| +| 1 | 테이블 스키마 변경 (필드 추가) | 🔴 필수 | +| 2 | `POST /pricing/upsert` 수정 | 🔴 필수 | +| 3 | `GET /pricing/by-items` 신규 | 🔴 필수 | +| 4 | `GET /pricing` 응답 확장 | 🟡 중요 | +| 5 | `GET /pricing/{id}/revisions` 신규 | 🟡 중요 | +| 6 | `POST /pricing/{id}/finalize` 신규 | 🟢 권장 | + +--- + +## 7. 질문/협의 사항 + +1. **기존 price 필드 처리**: 마이그레이션 vs 병행 사용? +2. **리비전 테이블 분리**: `price_history_revisions` 별도 테이블 vs 현재 테이블 확장? +3. **품목 연결**: `item_masters` 테이블 사용 vs `products`/`materials` 각각 JOIN? + +--- + +**연락처**: 프론트엔드 개발팀 +**관련 파일**: `src/components/pricing/types.ts` \ No newline at end of file diff --git a/claudedocs/sales/[IMPL-2025-12-09] pricing-api-integration-checklist.md b/claudedocs/sales/[IMPL-2025-12-09] pricing-api-integration-checklist.md new file mode 100644 index 00000000..471f8d98 --- /dev/null +++ b/claudedocs/sales/[IMPL-2025-12-09] pricing-api-integration-checklist.md @@ -0,0 +1,139 @@ +# 단가관리 API 연동 체크리스트 + +> **작성일**: 2025-12-09 +> **목표**: 백엔드 API 연동 완료 후 수동 검수 + +--- + +## Phase 1: 목록 페이지 API 연동 ✅ + +### 1.1 데이터 조회 +- [x] `GET /api/v1/pricing` 호출로 단가 목록 조회 +- [x] API 응답 → `PricingListItem` 타입 변환 +- [x] 품목 정보 표시 (item_type_code, item_id 기반) +- [x] 페이지네이션 적용 (size=100) + +### 1.2 필터/검색 +- [x] 품목 유형 필터 (`item_type_code`) - API 지원됨 +- [x] 상태 필터 (`status`) - API 지원됨 +- [x] 검색어 필터 (`q`) - API 지원됨 + +--- + +## Phase 2: 등록/수정 페이지 API 연동 ✅ + +### 2.1 등록 (`POST /api/v1/pricing`) +- [x] 폼 데이터 → API 요청 형식 변환 (`actions.ts: transformFrontendToApi`) +- [x] 성공/실패 토스트 메시지 (PricingFormClient 기존 로직) +- [x] 등록 후 목록으로 리다이렉트 + +### 2.2 수정 (`PUT /api/v1/pricing/{id}`) +- [x] 기존 데이터 로드 (`GET /api/v1/pricing/{id}`) - `getPricingById()` +- [x] 폼 데이터 → API 요청 형식 변환 +- [x] 수정 후 목록으로 리다이렉트 + +### 2.3 자동 계산 +- [x] 판매단가 자동 계산 (매입단가 + 가공비) × (1 + LOSS) × (1 + 마진율) - 백엔드에서 처리 +- [x] 반올림 규칙 적용 - 백엔드에서 처리 + +**생성된 파일:** +- `src/components/pricing/actions.ts` - 서버 액션 모음 + +--- + +## Phase 3: 삭제/확정 API 연동 ✅ + +### 3.1 삭제 (`DELETE /api/v1/pricing/{id}`) +- [x] 삭제 서버 액션 구현 (`deletePricing` in actions.ts) +- [ ] 삭제 확인 다이얼로그 (목록 페이지에서 필요 시 추가) +- [ ] 삭제 후 목록 새로고침 (목록 페이지에서 필요 시 추가) + +### 3.2 확정 (`POST /api/v1/pricing/{id}/finalize`) +- [x] 확정 서버 액션 구현 (`finalizePricing` in actions.ts) +- [x] 확정 확인 다이얼로그 (PricingFinalizeDialog 기존 UI 활용) +- [x] 확정 후 목록으로 리다이렉트 +- [x] PricingFormClient에 onFinalize prop 추가 + +--- + +## Phase 4: 이력 조회 API 연동 ✅ + +### 4.1 리비전 이력 (`GET /api/v1/pricing/{id}/revisions`) +- [x] 이력 다이얼로그 UI (PricingHistoryDialog 기존 UI 활용) +- [x] 변경 전/후 스냅샷 표시 (revisions 배열의 previousData 사용) +- [x] 별도 API 호출 함수 구현 (`getPricingRevisions` in actions.ts) +- [x] GET /pricing/{id}에서 revisions 함께 로드 (백엔드에서 with() 사용) + +--- + +## 테스트 URL + +- 목록: http://localhost:3000/sales/pricing-management +- 등록: http://localhost:3000/sales/pricing-management/create +- 수정: http://localhost:3000/sales/pricing-management/{id}/edit + +--- + +## Phase 5: 품목 목록 + 단가 병합 ✅ + +> **배경**: 현재는 단가 등록된 품목만 표시됨. sam-design처럼 품목 전체 목록에 단가 정보를 병합해야 함. + +### 5.1 품목 목록 API 호출 +- [x] `GET /api/v1/items` 통합 품목 목록 호출 (size=500) +- [x] API 응답 → 프론트엔드 타입 변환 (ItemApiData) +- [x] 품목 유형별 필터 지원 (FG, PT, SM, RM, CS) - items API에서 type 파라미터 지원 + +### 5.2 데이터 병합 로직 +- [x] 품목 목록 + 단가 목록 병합 함수 구현 (`mergeItemsWithPricing`) +- [x] 품목별 단가 유무 판별 (Map 활용 O(1) 조회) +- [x] 단가 미등록 품목 → `status: 'not_registered'` 표시 + +### 5.3 목록 페이지 수정 +- [x] `page.tsx` - 품목 API + 단가 API 병렬 호출 (`Promise.all`) +- [x] `PricingListClient` - 병합된 데이터 표시 +- [x] 미등록 품목 → "등록" 버튼 표시 (+ 아이콘) +- [x] 등록된 품목 → "수정" 버튼 표시 (연필 아이콘) +- [x] `itemTypeCode` 파라미터 추가 (PRODUCT/MATERIAL 구분) + +### 5.4 통계 카드 수정 +- [x] 전체 품목 수 (품목 목록 기준) - `totalAll` +- [x] 단가 등록 수 - `registered` (status !== 'not_registered') +- [x] 미등록 수 - `notRegistered` (totalAll - registered) +- [x] 확정 수 - `finalized` (isFinal === true) + +--- + +## 진행 상태 + +| Phase | 상태 | 완료일 | +|-------|------|--------| +| Phase 1 | ✅ 완료 | 2025-12-09 | +| Phase 2 | ✅ 완료 | 2025-12-09 | +| Phase 3 | ✅ 완료 | 2025-12-09 | +| Phase 4 | ✅ 완료 | 2025-12-09 | +| Phase 5 | ✅ 완료 | 2025-12-09 | + +--- + +## 생성/수정된 파일 요약 + +### 신규 생성 +- `src/components/pricing/actions.ts` - 서버 액션 (CRUD + 확정 + 이력 조회) + +### 수정된 파일 +- `src/app/[locale]/(protected)/sales/pricing-management/page.tsx` - API 연동 (목록) +- `src/app/[locale]/(protected)/sales/pricing-management/create/page.tsx` - API 연동 (등록) +- `src/app/[locale]/(protected)/sales/pricing-management/[id]/edit/page.tsx` - API 연동 (수정+확정) +- `src/components/pricing/PricingFormClient.tsx` - onFinalize prop 추가 +- `src/components/pricing/index.ts` - actions export 추가 + +--- + +## 수동 검수 체크리스트 + +- [ ] 목록 페이지에서 데이터 정상 조회 +- [ ] 품목 클릭 → 수정 페이지 이동 정상 +- [ ] 단가 수정 후 저장 정상 +- [ ] 단가 확정 기능 정상 +- [ ] 이력 조회 다이얼로그 정상 +- [ ] 신규 단가 등록 정상 (품목 선택 후) \ No newline at end of file diff --git a/claudedocs/sales/[NEXT-2025-12-09] client-session-context.md b/claudedocs/sales/[NEXT-2025-12-09] client-session-context.md new file mode 100644 index 00000000..20b3a5ba --- /dev/null +++ b/claudedocs/sales/[NEXT-2025-12-09] client-session-context.md @@ -0,0 +1,143 @@ +# 거래처 관리 - 다음 세션 컨텍스트 + +> **작성일**: 2025-12-09 +> **목적**: 다음 세션에서 이어서 작업할 내용 정리 + +--- + +## 1. 완료된 작업 (2025-12-09) + +### 1.1 백엔드 API 2차 필드 추가 ✅ 완료 +- **커밋**: `d164bb4` - feat: [client] 거래처 API 2차 필드 추가 +- **마이그레이션**: `2025_12_04_205603_add_extended_fields_to_clients_table.php` +- **is_active 타입 변경**: `5f20005` - CHAR(1) → TINYINT(1) Boolean + +### 1.2 프론트엔드 API 연동 ✅ 완료 + +| 구성 요소 | 파일 | 상태 | +|----------|------|------| +| **Proxy** | `/api/proxy/[...path]/route.ts` | ✅ 완료 | +| **Hook** | `src/hooks/useClientList.ts` | ✅ 완료 (2차 필드 포함) | +| **목록** | `sales/client-management-sales-admin/page.tsx` | ✅ API 연동 | +| **등록** | `sales/client-management-sales-admin/new/page.tsx` | ✅ API 연동 | +| **수정** | `sales/client-management-sales-admin/[id]/edit/page.tsx` | ✅ API 연동 | +| **상세** | `sales/client-management-sales-admin/[id]/page.tsx` | ✅ API 연동 | +| **등록폼** | `components/clients/ClientRegistration.tsx` | ✅ 완료 | +| **상세뷰** | `components/clients/ClientDetail.tsx` | ✅ 완료 | + +### 1.3 is_active Boolean 변경 대응 ✅ 완료 +```typescript +// useClientList.ts 수정 내역 +is_active: boolean; // 타입 변경 ("Y"|"N" → boolean) +status: api.is_active ? "활성" : "비활성", // 변환 로직 +is_active: form.isActive, // 전송 시 boolean 그대로 +``` + +### 1.4 기획 미확정 필드 - 개발 보류 ✅ 확정 +**결정일**: 2025-12-09 +**결정사항**: 기획에서 확정되지 않은 필드에 대해서는 **개발 보류** + +**숨김 처리된 섹션** (등록/수정 폼): +| 섹션 | 필드 | 상태 | +|------|------|------| +| 발주처 설정 | 계정ID, 비밀번호, 매입결제일, 매출결제일 | ❌ 개발보류 | +| 약정 세금 | 약정 여부, 금액, 시작/종료일 | ❌ 개발보류 | +| 악성채권 정보 | 악성채권 여부, 금액, 발생/만료일, 진행상태 | ❌ 개발보류 | + +**목록 테이블에서도 제외** (스크린샷 디자인과 다름 확인): +- 매입 결제일 컬럼 ❌ 제외 +- 매출 결제일 컬럼 ❌ 제외 +- 악성채권 컬럼/배지 ❌ 제외 + +> ⚠️ 백엔드 API는 이미 지원됨 (nullable 필드) +> 기획 확정 후 주석 해제하면 바로 사용 가능 +> 프론트엔드 파일: `src/components/clients/ClientRegistration.tsx` + +--- + +## 2. 현재 거래처 등록 폼 구조 + +``` +1. 기본 정보 ✅ 활성 + - 사업자등록번호 (필수) + - 거래처 코드 (자동생성) + - 거래처명 (필수) + - 대표자명 (필수) + - 거래처 유형 (매입/매출/매입매출) + - 업태, 종목 + +2. 연락처 정보 ✅ 활성 + - 주소 + - 전화번호, 모바일, 팩스 + - 이메일 + +3. 담당자 정보 ✅ 활성 + - 담당자명, 담당자 전화 + - 시스템 관리자 + +4. 발주처 설정 ❌ 숨김 (기획 미확정) +5. 약정 세금 ❌ 숨김 (기획 미확정) +6. 악성채권 정보 ❌ 숨김 (기획 미확정) + +7. 기타 정보 ✅ 활성 + - 메모 + - 상태 (활성/비활성) +``` + +--- + +## 3. 다음 작업 후보 + +### 3.1 거래처 관리 관련 +- [ ] 거래처 그룹 기능 구현 (client-groups API 이미 있음) +- [ ] 엑셀 내보내기/가져오기 +- [ ] 발주처/약정세금/악성채권 섹션 활성화 (기획 확정 시) + +### 3.2 다른 기능 +- [ ] 견적 관리 페이지 구현 +- [ ] 단가 관리 페이지 완성 +- [ ] 기타 요청 사항 + +--- + +## 4. 참고 파일 + +### 프론트엔드 (sam-react-prod) +``` +src/ +├── hooks/useClientList.ts # API 훅 + 타입 정의 +├── components/clients/ +│ ├── ClientRegistration.tsx # 등록/수정 폼 +│ └── ClientDetail.tsx # 상세 보기 +└── app/[locale]/(protected)/sales/client-management-sales-admin/ + ├── page.tsx # 목록 + ├── new/page.tsx # 등록 + ├── [id]/page.tsx # 상세 + └── [id]/edit/page.tsx # 수정 +``` + +### 백엔드 (sam-api) +``` +app/ +├── Http/Controllers/Api/V1/ClientController.php +├── Http/Requests/Client/ +│ ├── ClientStoreRequest.php +│ └── ClientUpdateRequest.php +├── Models/Orders/Client.php +├── Services/ClientService.php +└── Swagger/v1/ClientApi.php +``` + +--- + +## 5. 다음 세션에서 말할 내용 + +``` +버디 안녕~! 지난번에 거래처 관리 API 연동 완료했어. +- 백엔드 2차 필드 추가 확인 완료 +- 프론트엔드 API 연동 완료 (목록/등록/수정/상세) +- is_active Boolean 변경 대응 완료 +- 발주처/약정세금/악성채권 섹션은 기획 미확정으로 숨김 처리 + +오늘은 [다음 작업 내용] 진행하자~! +``` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d443f8ce..de4d0081 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-progress": "^1.1.8", "@radix-ui/react-radio-group": "^1.3.8", + "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slot": "^1.2.4", @@ -2635,6 +2636,78 @@ } } }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-select": { "version": "2.2.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", diff --git a/package.json b/package.json index d8393794..eb3ed1b5 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-progress": "^1.1.8", "@radix-ui/react-radio-group": "^1.3.8", + "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slot": "^1.2.4", diff --git a/src/app/[locale]/(protected)/hr/salary-management/page.tsx b/src/app/[locale]/(protected)/hr/salary-management/page.tsx new file mode 100644 index 00000000..3422b84f --- /dev/null +++ b/src/app/[locale]/(protected)/hr/salary-management/page.tsx @@ -0,0 +1,38 @@ +/** + * 급여관리 페이지 (Salary Management) + * + * 직원 급여 정보를 관리하는 시스템 + * - 급여 목록 조회/검색/필터 + * - 지급완료/지급예정 상태 변경 + * - 급여 상세 정보 조회 + * - 엑셀 다운로드 + */ + +import { Suspense } from 'react'; +import { SalaryManagement } from '@/components/hr/SalaryManagement'; +import type { Metadata } from 'next'; + +/** + * 메타데이터 설정 + */ +export const metadata: Metadata = { + title: '급여관리', + description: '직원 급여 정보를 관리합니다', +}; + +export default function SalaryManagementPage() { + return ( +
+ +
+
+

로딩 중...

+
+
+ }> + + + + ); +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/hr/vacation-management/page.tsx b/src/app/[locale]/(protected)/hr/vacation-management/page.tsx new file mode 100644 index 00000000..e77ab015 --- /dev/null +++ b/src/app/[locale]/(protected)/hr/vacation-management/page.tsx @@ -0,0 +1,38 @@ +/** + * 휴가관리 페이지 (Vacation Management) + * + * 직원 휴가 정보를 관리하는 시스템 + * - 휴가 목록 조회/검색/필터 + * - 휴가 등록/조정 + * - 휴가 종류 설정 + * - 엑셀 다운로드 + */ + +import { Suspense } from 'react'; +import { VacationManagement } from '@/components/hr/VacationManagement'; +import type { Metadata } from 'next'; + +/** + * 메타데이터 설정 + */ +export const metadata: Metadata = { + title: '휴가관리', + description: '직원 휴가 정보를 관리합니다', +}; + +export default function VacationManagementPage() { + return ( +
+ +
+
+

로딩 중...

+
+
+ }> + + + + ); +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/items/[id]/edit/page.tsx b/src/app/[locale]/(protected)/items/[id]/edit/page.tsx index 1534e209..2c572c46 100644 --- a/src/app/[locale]/(protected)/items/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/items/[id]/edit/page.tsx @@ -2,7 +2,7 @@ * 품목 수정 페이지 * * API 연동: - * - GET /api/proxy/items/code/{itemCode}?include_bom=true (품목 조회) + * - GET /api/proxy/items/{id} (품목 조회 - id 기반 통일) * - PUT /api/proxy/items/{id} (품목 수정) */ @@ -71,6 +71,9 @@ interface ItemApiResponse { function mapApiResponseToFormData(data: ItemApiResponse): DynamicFormData { const formData: DynamicFormData = {}; + // attributes 객체 추출 (조립부품 등의 동적 필드가 여기에 저장됨) + const attributes = (data.attributes || {}) as Record; + // 백엔드 Product 모델 필드: code, name, product_type // 프론트엔드 폼 필드: item_name, item_code 등 (snake_case) @@ -86,19 +89,29 @@ function mapApiResponseToFormData(data: ItemApiResponse): DynamicFormData { if (data.remarks) formData['note'] = data.remarks; // Material remarks → note 매핑 formData['is_active'] = data.is_active ?? true; - // 부품 관련 필드 (PT) - if (data.part_type) formData['part_type'] = data.part_type; - if (data.part_usage) formData['part_usage'] = data.part_usage; - if (data.material) formData['material'] = data.material; - if (data.length) formData['length'] = data.length; - if (data.thickness) formData['thickness'] = data.thickness; + // 부품 관련 필드 (PT) - data와 attributes 둘 다에서 찾음 + const partType = data.part_type || attributes.part_type; + const partUsage = data.part_usage || attributes.part_usage; + const material = data.material || attributes.material; + const length = data.length || attributes.length; + const thickness = data.thickness || attributes.thickness; + if (partType) formData['part_type'] = String(partType); + if (partUsage) formData['part_usage'] = String(partUsage); + if (material) formData['material'] = String(material); + if (length) formData['length'] = String(length); + if (thickness) formData['thickness'] = String(thickness); - // 조립 부품 관련 - if (data.installation_type) formData['installation_type'] = data.installation_type; - if (data.assembly_type) formData['assembly_type'] = data.assembly_type; - if (data.assembly_length) formData['assembly_length'] = data.assembly_length; - if (data.side_spec_width) formData['side_spec_width'] = data.side_spec_width; - if (data.side_spec_height) formData['side_spec_height'] = data.side_spec_height; + // 조립 부품 관련 - data와 attributes 둘 다에서 찾음 + const installationType = data.installation_type || attributes.installation_type; + const assemblyType = data.assembly_type || attributes.assembly_type; + const assemblyLength = data.assembly_length || attributes.assembly_length; + const sideSpecWidth = data.side_spec_width || attributes.side_spec_width; + const sideSpecHeight = data.side_spec_height || attributes.side_spec_height; + if (installationType) formData['installation_type'] = String(installationType); + if (assemblyType) formData['assembly_type'] = String(assemblyType); + if (assemblyLength) formData['assembly_length'] = String(assemblyLength); + if (sideSpecWidth) formData['side_spec_width'] = String(sideSpecWidth); + if (sideSpecHeight) formData['side_spec_height'] = String(sideSpecHeight); // 제품 관련 필드 (FG) if (data.product_category) formData['product_category'] = data.product_category; @@ -110,11 +123,11 @@ function mapApiResponseToFormData(data: ItemApiResponse): DynamicFormData { if (data.certification_end_date) formData['certification_end_date'] = data.certification_end_date; // 파일 관련 필드 (edit 모드에서 기존 파일 표시용) - if (data.bending_diagram) formData['bending_diagram'] = data.bending_diagram; - if (data.specification_file) formData['specification_file'] = data.specification_file; - if (data.specification_file_name) formData['specification_file_name'] = data.specification_file_name; - if (data.certification_file) formData['certification_file'] = data.certification_file; - if (data.certification_file_name) formData['certification_file_name'] = data.certification_file_name; + if (data.bending_diagram) formData['bending_diagram'] = String(data.bending_diagram); + if (data.specification_file) formData['specification_file'] = String(data.specification_file); + if (data.specification_file_name) formData['specification_file_name'] = String(data.specification_file_name); + if (data.certification_file) formData['certification_file'] = String(data.certification_file); + if (data.certification_file_name) formData['certification_file_name'] = String(data.certification_file_name); // Material(SM, RM, CS) options 필드 매핑 // 백엔드에서 options: [{label: "standard_1", value: "옵션값"}, ...] 형태로 저장됨 @@ -179,17 +192,25 @@ export default function EditItemPage() { let response: Response; - // Materials (SM, RM, CS)는 다른 API 엔드포인트 사용 - if (MATERIAL_TYPES.includes(urlItemType) && urlItemId) { - // GET /api/proxy/items/{id}?item_type=MATERIAL - // console.log('[EditItem] Using Material API'); - response = await fetch(`/api/proxy/items/${urlItemId}?item_type=MATERIAL`); - } else { - // Products (FG, PT): GET /api/proxy/items/code/{itemCode}?include_bom=true - // console.log('[EditItem] Using Product API'); - response = await fetch(`/api/proxy/items/code/${encodeURIComponent(itemCode)}?include_bom=true`); + // 모든 품목: GET /api/proxy/items/{id} (id 기반 통일) + if (!urlItemId) { + setError('품목 ID가 없습니다.'); + setIsLoading(false); + return; } + // Materials (SM, RM, CS)는 item_type=MATERIAL 쿼리 파라미터 추가 + const isMaterial = isMaterialType(urlItemType); + const queryParams = new URLSearchParams(); + if (isMaterial) { + queryParams.append('item_type', 'MATERIAL'); + } else { + queryParams.append('include_bom', 'true'); + } + + console.log('[EditItem] Fetching:', { urlItemId, urlItemType, isMaterial }); + response = await fetch(`/api/proxy/items/${urlItemId}?${queryParams.toString()}`); + if (!response.ok) { if (response.status === 404) { setError('품목을 찾을 수 없습니다.'); @@ -206,12 +227,13 @@ export default function EditItemPage() { if (result.success && result.data) { const apiData = result.data as ItemApiResponse; - // console.log('========== [EditItem] API 원본 데이터 =========='); - // console.log('is_active:', apiData.is_active); - // console.log('specification:', apiData.specification); - // console.log('unit:', apiData.unit); - // console.log('전체 데이터:', JSON.stringify(apiData, null, 2)); - // console.log('================================================'); + console.log('========== [EditItem] API 원본 데이터 (백엔드 응답) =========='); + console.log('id:', apiData.id); + console.log('specification:', apiData.specification); + console.log('unit:', apiData.unit); + console.log('is_active:', apiData.is_active); + console.log('전체:', apiData); + console.log('=============================================================='); // ID, 품목 유형 저장 // Product: product_type, Material: material_type 또는 type_code @@ -222,12 +244,12 @@ export default function EditItemPage() { // 폼 데이터로 변환 const formData = mapApiResponseToFormData(apiData); - // console.log('========== [EditItem] Mapped form data =========='); - // console.log('is_active:', formData['is_active']); - // console.log('specification:', formData['specification']); - // console.log('unit:', formData['unit']); - // console.log('전체 매핑 데이터:', JSON.stringify(formData, null, 2)); - // console.log('================================================='); + console.log('========== [EditItem] 폼에 전달되는 initialData =========='); + console.log('specification:', formData['specification']); + console.log('unit:', formData['unit']); + console.log('is_active:', formData['is_active']); + console.log('전체:', formData); + console.log('=========================================================='); setInitialData(formData); } else { setError(result.message || '품목 정보를 불러올 수 없습니다.'); @@ -261,7 +283,7 @@ export default function EditItemPage() { // Materials (SM, RM, CS)는 /products/materials 엔드포인트 + PATCH 메서드 사용 // Products (FG, PT)는 /items 엔드포인트 + PUT 메서드 사용 - const isMaterial = itemType ? MATERIAL_TYPES.includes(itemType) : false; + const isMaterial = isMaterialType(itemType); // 디버깅: material_code 생성 관련 변수 확인 (필요 시 주석 해제) // console.log('========== [EditItem] handleSubmit 디버깅 =========='); @@ -312,11 +334,14 @@ export default function EditItemPage() { } // API 호출 - // console.log('========== [EditItem] PUT 요청 데이터 =========='); - // console.log('URL:', updateUrl); - // console.log('Method:', method); - // console.log('전송 데이터:', JSON.stringify(submitData, null, 2)); - // console.log('================================================'); + console.log('========== [EditItem] 수정 요청 데이터 =========='); + console.log('URL:', updateUrl); + console.log('Method:', method); + console.log('specification:', submitData.specification); + console.log('unit:', submitData.unit); + console.log('is_active:', submitData.is_active); + console.log('전체:', submitData); + console.log('================================================='); const response = await fetch(updateUrl, { method, diff --git a/src/app/[locale]/(protected)/items/[id]/page.tsx b/src/app/[locale]/(protected)/items/[id]/page.tsx index 4d03049b..4ce63f8a 100644 --- a/src/app/[locale]/(protected)/items/[id]/page.tsx +++ b/src/app/[locale]/(protected)/items/[id]/page.tsx @@ -1,7 +1,7 @@ /** * 품목 상세 조회 페이지 * - * API 연동: GET /api/proxy/items/code/{itemCode}?include_bom=true + * API 연동: GET /api/proxy/items/{id} (id 기반 통일) */ 'use client'; @@ -10,7 +10,7 @@ import { useEffect, useState } from 'react'; import { useParams, useRouter, useSearchParams } from 'next/navigation'; import { notFound } from 'next/navigation'; import ItemDetailClient from '@/components/items/ItemDetailClient'; -import type { ItemMaster } from '@/types/item'; +import type { ItemMaster, ItemType, ProductCategory, PartType, PartUsage } from '@/types/item'; import { Loader2 } from 'lucide-react'; // Materials 타입 (SM, RM, CS는 Material 테이블 사용) @@ -20,6 +20,9 @@ const MATERIAL_TYPES = ['SM', 'RM', 'CS']; * API 응답을 ItemMaster 타입으로 변환 */ function mapApiResponseToItemMaster(data: Record): ItemMaster { + // attributes 객체 추출 (조립부품 등의 동적 필드가 여기에 저장됨) + const attributes = (data.attributes || {}) as Record; + return { id: String(data.id || ''), // 백엔드 필드 매핑: @@ -27,7 +30,7 @@ function mapApiResponseToItemMaster(data: Record): ItemMaster { // - Material: material_code, name, material_type (또는 type_code) itemCode: String(data.code || data.material_code || data.item_code || data.itemCode || ''), itemName: String(data.name || data.item_name || data.itemName || ''), - itemType: String(data.product_type || data.material_type || data.type_code || data.item_type || data.itemType || 'FG'), + itemType: (data.product_type || data.material_type || data.type_code || data.item_type || data.itemType || 'FG') as ItemType, unit: String(data.unit || 'EA'), specification: data.specification ? String(data.specification) : undefined, isActive: Boolean(data.is_active ?? data.isActive ?? true), @@ -40,7 +43,7 @@ function mapApiResponseToItemMaster(data: Record): ItemMaster { processingCost: data.processing_cost ? Number(data.processing_cost) : undefined, laborCost: data.labor_cost ? Number(data.labor_cost) : undefined, installCost: data.install_cost ? Number(data.install_cost) : undefined, - productCategory: data.product_category ? String(data.product_category) : undefined, + productCategory: data.product_category ? (data.product_category as ProductCategory) : undefined, lotAbbreviation: data.lot_abbreviation ? String(data.lot_abbreviation) : undefined, note: data.note ? String(data.note) : undefined, description: data.description ? String(data.description) : undefined, @@ -50,18 +53,18 @@ function mapApiResponseToItemMaster(data: Record): ItemMaster { isFinal: Boolean(data.is_final ?? false), createdAt: String(data.created_at || data.createdAt || ''), updatedAt: data.updated_at ? String(data.updated_at) : undefined, - // 부품 관련 - partType: data.part_type ? String(data.part_type) : undefined, - partUsage: data.part_usage ? String(data.part_usage) : undefined, - installationType: data.installation_type ? String(data.installation_type) : undefined, - assemblyType: data.assembly_type ? String(data.assembly_type) : undefined, - assemblyLength: data.assembly_length ? String(data.assembly_length) : undefined, - material: data.material ? String(data.material) : undefined, - sideSpecWidth: data.side_spec_width ? String(data.side_spec_width) : undefined, - sideSpecHeight: data.side_spec_height ? String(data.side_spec_height) : undefined, - guideRailModelType: data.guide_rail_model_type ? String(data.guide_rail_model_type) : undefined, - guideRailModel: data.guide_rail_model ? String(data.guide_rail_model) : undefined, - length: data.length ? String(data.length) : undefined, + // 부품 관련 - data와 attributes 둘 다에서 찾음 + partType: (data.part_type || attributes.part_type) ? ((data.part_type || attributes.part_type) as PartType) : undefined, + partUsage: (data.part_usage || attributes.part_usage) ? ((data.part_usage || attributes.part_usage) as PartUsage) : undefined, + installationType: (data.installation_type || attributes.installation_type) ? String(data.installation_type || attributes.installation_type) : undefined, + assemblyType: (data.assembly_type || attributes.assembly_type) ? String(data.assembly_type || attributes.assembly_type) : undefined, + assemblyLength: (data.assembly_length || attributes.assembly_length || attributes.length) ? String(data.assembly_length || attributes.assembly_length || attributes.length) : undefined, + material: (data.material || attributes.material) ? String(data.material || attributes.material) : undefined, + sideSpecWidth: (data.side_spec_width || attributes.side_spec_width) ? String(data.side_spec_width || attributes.side_spec_width) : undefined, + sideSpecHeight: (data.side_spec_height || attributes.side_spec_height) ? String(data.side_spec_height || attributes.side_spec_height) : undefined, + guideRailModelType: (data.guide_rail_model_type || attributes.guide_rail_model_type) ? String(data.guide_rail_model_type || attributes.guide_rail_model_type) : undefined, + guideRailModel: (data.guide_rail_model || attributes.guide_rail_model) ? String(data.guide_rail_model || attributes.guide_rail_model) : undefined, + length: (data.length || attributes.length) ? String(data.length || attributes.length) : undefined, // BOM (있으면) bom: Array.isArray(data.bom) ? data.bom.map((bomItem: Record) => ({ id: String(bomItem.id || ''), @@ -117,17 +120,25 @@ export default function ItemDetailPage() { let response: Response; - // Materials (SM, RM, CS)는 다른 API 엔드포인트 사용 - if (MATERIAL_TYPES.includes(itemType) && itemId) { - // GET /api/proxy/items/{id}?item_type=MATERIAL - console.log('[ItemDetail] Using Material API'); - response = await fetch(`/api/proxy/items/${itemId}?item_type=MATERIAL`); - } else { - // Products (FG, PT): GET /api/proxy/items/code/{itemCode}?include_bom=true - console.log('[ItemDetail] Using Product API'); - response = await fetch(`/api/proxy/items/code/${encodeURIComponent(itemCode)}?include_bom=true`); + // 모든 품목: GET /api/proxy/items/{id} (id 기반 통일) + if (!itemId) { + setError('품목 ID가 없습니다.'); + setIsLoading(false); + return; } + // Materials (SM, RM, CS)는 item_type=MATERIAL 쿼리 파라미터 추가 + const isMaterial = MATERIAL_TYPES.includes(itemType); + const queryParams = new URLSearchParams(); + if (isMaterial) { + queryParams.append('item_type', 'MATERIAL'); + } else { + queryParams.append('include_bom', 'true'); + } + + console.log('[ItemDetail] Fetching:', { itemId, itemType, isMaterial }); + response = await fetch(`/api/proxy/items/${itemId}?${queryParams.toString()}`); + if (!response.ok) { if (response.status === 404) { setError('품목을 찾을 수 없습니다.'); diff --git a/src/app/[locale]/(protected)/production/screen-production/page.tsx b/src/app/[locale]/(protected)/production/screen-production/page.tsx index b57528e6..79ac01ab 100644 --- a/src/app/[locale]/(protected)/production/screen-production/page.tsx +++ b/src/app/[locale]/(protected)/production/screen-production/page.tsx @@ -147,7 +147,7 @@ export default async function ItemsPage() { return (
로딩 중...
}> - + ); diff --git a/src/app/[locale]/(protected)/sales/client-management-sales-admin/page.tsx b/src/app/[locale]/(protected)/sales/client-management-sales-admin/page.tsx index dae598c0..ba9b3c82 100644 --- a/src/app/[locale]/(protected)/sales/client-management-sales-admin/page.tsx +++ b/src/app/[locale]/(protected)/sales/client-management-sales-admin/page.tsx @@ -27,6 +27,7 @@ import { CheckCircle, XCircle, Eye, + Loader2, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; @@ -71,6 +72,18 @@ export default function CustomerAccountManagementPage() { const [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 20; + // 초기 로딩 완료 여부 (첫 데이터 로드 완료 시 true) + const [isInitialLoaded, setIsInitialLoaded] = useState(false); + + // 전체 통계 (검색과 무관하게 고정) + const [totalStats, setTotalStats] = useState({ + total: 0, + purchase: 0, + sales: 0, + active: 0, + inactive: 0, + }); + // 삭제 확인 다이얼로그 state const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [deleteTargetId, setDeleteTargetId] = useState(null); @@ -82,15 +95,52 @@ export default function CustomerAccountManagementPage() { const [mobileDisplayCount, setMobileDisplayCount] = useState(20); const sentinelRef = useRef(null); + // 전체 통계 로드 (최초 1회만) + const loadTotalStats = useCallback(async () => { + try { + // 전체 데이터 조회 (검색 조건 없이) + const response = await fetch("/api/proxy/clients?size=1000"); + if (response.ok) { + const result = await response.json(); + if (result.success && result.data) { + const allClients = result.data.data || []; + setTotalStats({ + total: result.data.total || allClients.length, + purchase: allClients.filter((c: { client_type?: string }) => + c.client_type === "매입" || c.client_type === "매입매출" + ).length, + sales: allClients.filter((c: { client_type?: string }) => + c.client_type === "매출" || c.client_type === "매입매출" + ).length, + active: allClients.filter((c: { is_active?: boolean }) => c.is_active === true).length, + inactive: allClients.filter((c: { is_active?: boolean }) => c.is_active === false).length, + }); + } + } + } catch (err) { + console.error("전체 통계 로드 실패:", err); + } + }, []); + // 초기 데이터 로드 useEffect(() => { - fetchClients({ - page: currentPage, - size: itemsPerPage, - q: searchTerm || undefined, - onlyActive: filterType === "active" ? true : filterType === "inactive" ? false : undefined, - }); - }, [currentPage, filterType, fetchClients]); + const loadData = async () => { + // 최초 로드 시 전체 통계도 함께 로드 + if (!isInitialLoaded) { + await loadTotalStats(); + } + await fetchClients({ + page: currentPage, + size: itemsPerPage, + q: searchTerm || undefined, + onlyActive: filterType === "active" ? true : filterType === "inactive" ? false : undefined, + }); + if (!isInitialLoaded) { + setIsInitialLoaded(true); + } + }; + loadData(); + }, [currentPage, filterType, fetchClients, isInitialLoaded, loadTotalStats]); // 검색어 변경 시 디바운스 처리 const searchTimeoutRef = useRef(null); @@ -119,6 +169,8 @@ export default function CustomerAccountManagementPage() { const filteredClients = clients.filter((client) => { if (filterType === "active") return client.status === "활성"; if (filterType === "inactive") return client.status === "비활성"; + if (filterType === "purchase") return client.clientType === "매입" || client.clientType === "매입매출"; + if (filterType === "sales") return client.clientType === "매출" || client.clientType === "매입매출"; return true; }); @@ -165,41 +217,44 @@ export default function CustomerAccountManagementPage() { setMobileDisplayCount(20); }, [searchTerm, filterType]); - // 통계 (API에서 가져온 전체 데이터 기반) - const totalCustomers = pagination?.total || clients.length; - const activeCustomers = clients.filter((c) => c.status === "활성").length; - const inactiveCustomers = clients.filter((c) => c.status === "비활성").length; - + // 통계 카드 (전체 통계 기준 - 검색과 무관하게 고정) const stats = [ { label: "전체 거래처", - value: totalCustomers, + value: totalStats.total, icon: Users, iconColor: "text-blue-600", }, + { + label: "매입 거래처", + value: totalStats.purchase, + icon: Building2, + iconColor: "text-orange-600", + }, + { + label: "매출 거래처", + value: totalStats.sales, + icon: Building2, + iconColor: "text-rose-600", + }, { label: "활성 거래처", - value: activeCustomers, + value: totalStats.active, icon: CheckCircle, iconColor: "text-green-600", }, - { - label: "비활성 거래처", - value: inactiveCustomers, - icon: XCircle, - iconColor: "text-gray-600", - }, ]; - // 데이터 새로고침 함수 - const refreshData = useCallback(() => { + // 데이터 새로고침 함수 (삭제/등록 후 통계도 다시 로드) + const refreshData = useCallback(async () => { + await loadTotalStats(); // 통계 다시 로드 fetchClients({ page: currentPage, size: itemsPerPage, q: searchTerm || undefined, onlyActive: filterType === "active" ? true : filterType === "inactive" ? false : undefined, }); - }, [currentPage, itemsPerPage, searchTerm, filterType, fetchClients]); + }, [currentPage, itemsPerPage, searchTerm, filterType, fetchClients, loadTotalStats]); // 핸들러 - 페이지 기반 네비게이션 const handleAddNew = () => { @@ -297,39 +352,74 @@ export default function CustomerAccountManagementPage() { ); }; - // 탭 구성 + // 탭 구성 (전체 | 매입 | 매출 | 활성 | 비활성) - 전체 통계 기준으로 고정 const tabs: TabOption[] = [ { value: "all", label: "전체", - count: totalCustomers, + count: totalStats.total, color: "blue", }, + { + value: "purchase", + label: "매입", + count: totalStats.purchase, + color: "orange", + }, + { + value: "sales", + label: "매출", + count: totalStats.sales, + color: "rose", + }, { value: "active", label: "활성", - count: activeCustomers, + count: totalStats.active, color: "green", }, { value: "inactive", label: "비활성", - count: inactiveCustomers, + count: totalStats.inactive, color: "gray", }, ]; + // 거래처 유형 배지 + const getClientTypeBadge = (clientType: Client["clientType"]) => { + switch (clientType) { + case "매출": + return ( + + 매출 + + ); + case "매입매출": + return ( + + 매입매출 + + ); + case "매입": + default: + return ( + + 매입 + + ); + } + }; + // 테이블 컬럼 정의 const tableColumns: TableColumn[] = [ { key: "rowNumber", label: "번호", className: "px-4" }, { key: "code", label: "코드", className: "px-4" }, + { key: "clientType", label: "구분", className: "px-4" }, { key: "name", label: "거래처명", className: "px-4" }, - { key: "businessNo", label: "사업자번호", className: "px-4" }, { key: "representative", label: "대표자", className: "px-4" }, + { key: "manager", label: "담당자", className: "px-4" }, { key: "phone", label: "전화번호", className: "px-4" }, - { key: "businessType", label: "업태", className: "px-4" }, - { key: "businessItem", label: "업종", className: "px-4" }, - { key: "status", label: "상태", className: "px-4" }, { key: "actions", label: "작업", className: "px-4" }, ]; @@ -360,16 +450,21 @@ export default function CustomerAccountManagementPage() { {customer.code} + {getClientTypeBadge(customer.clientType)} {customer.name} - {customer.businessNo} {customer.representative} + {customer.managerName || "-"} {customer.phone} - {customer.businessType} - {customer.businessItem} - {getStatusBadge(customer.status)} e.stopPropagation()}> {isSelected && (
+ - - -
- + + {/* 헤더 - PageHeader 사용으로 등록/수정과 동일한 레이아웃 */} + + + + + + } + /> - {/* 1. 기본 정보 */} +
+ {/* 1. 기본 정보 */}
@@ -242,6 +243,7 @@ export function ClientDetail({ )} -
+
+
); } \ No newline at end of file diff --git a/src/components/clients/ClientRegistration.tsx b/src/components/clients/ClientRegistration.tsx index 1bec63fc..bed8c4d9 100644 --- a/src/components/clients/ClientRegistration.tsx +++ b/src/components/clients/ClientRegistration.tsx @@ -12,24 +12,13 @@ import { useState, useEffect } from "react"; import { Input } from "../ui/input"; import { Textarea } from "../ui/textarea"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "../ui/select"; import { RadioGroup, RadioGroupItem } from "../ui/radio-group"; -import { Checkbox } from "../ui/checkbox"; import { Label } from "../ui/label"; import { Building2, UserCircle, Phone, - CreditCard, FileText, - AlertTriangle, - Calculator, } from "lucide-react"; import { toast } from "sonner"; import { @@ -42,7 +31,6 @@ import { ClientFormData, INITIAL_CLIENT_FORM, ClientType, - BadDebtProgress, } from "../../hooks/useClientList"; interface ClientRegistrationProps { @@ -52,15 +40,33 @@ interface ClientRegistrationProps { isLoading?: boolean; } +// 4자리 영문+숫자 조합 코드 생성 (중복 방지를 위해 타임스탬프 기반) +const generateClientCode = (): string => { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + const timestamp = Date.now().toString(36).toUpperCase().slice(-2); // 타임스탬프 2자리 + let random = ""; + for (let i = 0; i < 2; i++) { + random += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return timestamp + random; +}; + export function ClientRegistration({ onBack, onSave, editingClient, isLoading = false, }: ClientRegistrationProps) { - const [formData, setFormData] = useState( - editingClient || INITIAL_CLIENT_FORM - ); + const [formData, setFormData] = useState(() => { + if (editingClient) { + return editingClient; + } + // 신규 등록 시 클라이언트 코드 자동 생성 + return { + ...INITIAL_CLIENT_FORM, + clientCode: generateClientCode(), + }; + }); const [errors, setErrors] = useState>({}); const [isSaving, setIsSaving] = useState(false); @@ -79,7 +85,15 @@ export function ClientRegistration({ newErrors.name = "거래처명은 2자 이상 입력해주세요"; } - if (!formData.businessNo || !/^\d{10}$/.test(formData.businessNo)) { + // 하이픈 제거 후 10자리 숫자 검증 (123-45-67890 또는 1234567890 허용) + const businessNoDigits = formData.businessNo.replace(/-/g, "").trim(); + console.log("[ClientRegistration] businessNo 검증:", { + 원본: formData.businessNo, + 하이픈제거: businessNoDigits, + 길이: businessNoDigits.length, + 숫자만: /^\d{10}$/.test(businessNoDigits), + }); + if (!formData.businessNo || !/^\d{10}$/.test(businessNoDigits)) { newErrors.businessNo = "사업자등록번호는 10자리 숫자여야 합니다"; } @@ -125,6 +139,7 @@ export function ClientRegistration({ field: keyof ClientFormData, value: string | boolean ) => { + console.log("[ClientRegistration] handleFieldChange:", field, value); setFormData({ ...formData, [field]: value }); if (errors[field]) { setErrors((prev) => { @@ -160,10 +175,11 @@ export function ClientRegistration({ required error={errors.businessNo} htmlFor="businessNo" + type="custom" > handleFieldChange("businessNo", e.target.value)} /> @@ -173,6 +189,7 @@ export function ClientRegistration({ label="거래처 코드" htmlFor="clientCode" helpText="자동 생성됩니다" + type="custom" > - + - + - + - + - + - + - + - + - + - + - {/* 4. 발주처 설정 */} - - - - handleFieldChange("accountId", e.target.value)} - /> - + {/* + TODO: 기획 확정 후 활성화 (2025-12-09) + - 발주처 설정: 계정ID, 비밀번호, 매입/매출 결제일 + - 약정 세금: 약정 여부, 금액, 시작/종료일 + - 악성채권 정보: 악성채권 여부, 금액, 발생/만료일, 진행상태 - - - handleFieldChange("accountPassword", e.target.value) - } - /> - - + 백엔드 API에서는 이미 지원됨 (nullable 필드) + */} - - - - - - - - - - - - {/* 5. 약정 세금 */} - - -
- - handleFieldChange("taxAgreement", checked as boolean) - } - /> - -
-
- - {formData.taxAgreement && ( - <> - - handleFieldChange("taxAmount", e.target.value)} - /> - - - - - - handleFieldChange("taxStartDate", e.target.value) - } - /> - - - - - handleFieldChange("taxEndDate", e.target.value) - } - /> - - - - )} -
- - {/* 6. 악성채권 */} - - -
- - handleFieldChange("badDebt", checked as boolean) - } - /> - -
-
- - {formData.badDebt && ( - <> - - - handleFieldChange("badDebtAmount", e.target.value) - } - /> - - - - - - handleFieldChange("badDebtReceiveDate", e.target.value) - } - /> - - - - - handleFieldChange("badDebtEndDate", e.target.value) - } - /> - - - - - - - - )} -
- - {/* 7. 기타 */} + {/* 4. 기타 */} - +