diff --git a/claudedocs/_index.md b/claudedocs/_index.md index f28b089c..2bcffd20 100644 --- a/claudedocs/_index.md +++ b/claudedocs/_index.md @@ -1,6 +1,6 @@ # claudedocs 문서 맵 -> 프로젝트 기술 문서 인덱스 (Last Updated: 2025-12-20) +> 프로젝트 기술 문서 인덱스 (Last Updated: 2025-12-24) ## ⭐ 빠른 참조 @@ -64,7 +64,10 @@ claudedocs/ | 파일 | 설명 | |------|------| -| `[PLAN-2025-12-16] dynamicitemform-hook-extraction.md` | 🔴 **NEW** - DynamicItemForm 훅 분리 계획서 (2161줄 → 900줄 목표, 6 Phase) | +| `[NEXT-2025-12-24] item-master-refactoring-session.md` | ⭐ **세션 체크포인트** - 훅 분리 Phase 1,2 완료, 커밋 대기 | +| `[PLAN-2025-12-24] hook-extraction-plan.md` | 🔴 **진행중** - ItemMasterDataManagement 훅 분리 계획서 (1,799줄 → 목표 ~500줄) | +| `[IMPL-2025-12-24] item-master-test-and-zustand.md` | 🔴 **진행중** - 훅 분리 테스트 및 Zustand 도입 체크리스트 | +| `[PLAN-2025-12-16] dynamicitemform-hook-extraction.md` | ✅ **완료** - DynamicItemForm 훅 분리 계획서 (2161줄 → 1050줄, 51% 감소) | | `[FIX-2025-12-16] options-details-duplicate-bug.md` | options vs item_details 중복 저장 버그 (bending_details 값 덮어쓰기 문제 해결) | | `[IMPL-2025-12-15] backend-item-api-migration.md` | 백엔드 품목 API 통합 (product/material → items), group_id 파라미터, **향후 동적 변경 예정** | | `[NEXT-2025-12-13] item-file-upload-session-context.md` | ⭐ **세션 체크포인트** - 파일 업로드 UI 개선 완료, 백엔드 대기 중, DynamicItemForm 분리 예정 | diff --git a/claudedocs/item-master/[IMPL-2025-12-24] item-master-test-and-zustand.md b/claudedocs/item-master/[IMPL-2025-12-24] item-master-test-and-zustand.md new file mode 100644 index 00000000..0974e5ad --- /dev/null +++ b/claudedocs/item-master/[IMPL-2025-12-24] item-master-test-and-zustand.md @@ -0,0 +1,120 @@ +# 품목기준관리 테스트 및 Zustand 도입 체크리스트 + +> **브랜치**: `feature/item-master-zustand` +> **작성일**: 2025-12-24 +> **목표**: 훅 분리 완료 후 수동 테스트 → Zustand 도입 + +--- + +## 현재 상태 + +| 항목 | 상태 | +|------|------| +| 훅 분리 작업 | ✅ 완료 (2025-12-16) | +| index.tsx 줄 수 | 2,161줄 → 1,050줄 (51% 감소) | +| 빌드 | ✅ 성공 | +| 수동 테스트 | ⏳ 진행 필요 | + +--- + +## Phase 1: 수동 테스트 + +### 1.1 품목 유형별 등록 테스트 + +| # | 품목 유형 | 테스트 URL | 상태 | +|---|----------|-----------|------| +| 1 | FG (제품) | `/ko/items/create` → 제품 선택 | ⬜ | +| 2 | PT (부품) - 절곡 | `/ko/items/create` → 부품 → 절곡부품 | ⬜ | +| 3 | PT (부품) - 조립 | `/ko/items/create` → 부품 → 조립부품 | ⬜ | +| 4 | PT (부품) - 구매 | `/ko/items/create` → 부품 → 구매부품 | ⬜ | +| 5 | SM (부자재) | `/ko/items/create` → 부자재 선택 | ⬜ | +| 6 | RM (원자재) | `/ko/items/create` → 원자재 선택 | ⬜ | +| 7 | CS (소모품) | `/ko/items/create` → 소모품 선택 | ⬜ | + +### 1.2 품목 수정 테스트 + +| # | 테스트 항목 | 상태 | +|---|------------|------| +| 1 | 기존 품목 불러오기 | ⬜ | +| 2 | 필드 값 수정 후 저장 | ⬜ | +| 3 | 수정 후 목록에서 확인 | ⬜ | + +### 1.3 핵심 기능 테스트 + +| # | 기능 | 테스트 방법 | 상태 | +|---|------|-----------|------| +| 1 | 품목코드 자동생성 | 절곡부품 선택 → 재질/두께 입력 → 코드 확인 | ⬜ | +| 2 | 조건부 필드 표시 | 부품 유형 변경 시 필드 변화 확인 | ⬜ | +| 3 | BOM 추가/수정/삭제 | BOM 섹션에서 CRUD 테스트 | ⬜ | +| 4 | 파일 업로드 | 시방서/인정서 파일 첨부 | ⬜ (백엔드 수정 대기) | +| 5 | 파일 다운로드 | 기존 파일 다운로드 | ⬜ | +| 6 | 파일 삭제 | 첨부된 파일 삭제 | ⬜ | + +### 1.4 부품 유형 변경 테스트 + +| # | 테스트 시나리오 | 상태 | +|---|---------------|------| +| 1 | 절곡 → 조립 변경 시 필드 초기화 확인 | ⬜ | +| 2 | 조립 → 구매 변경 시 필드 초기화 확인 | ⬜ | +| 3 | 폭 합계 자동 계산 (절곡부품) | ⬜ | + +--- + +## Phase 2: 테스트 결과 정리 + +### 발견된 이슈 + +| # | 이슈 | 심각도 | 상태 | 비고 | +|---|------|--------|------|------| +| - | - | - | - | - | + +### 테스트 완료 확인 + +- [ ] 모든 품목 유형 등록 테스트 완료 +- [ ] 모든 품목 수정 테스트 완료 +- [ ] 핵심 기능 테스트 완료 +- [ ] 발견된 이슈 수정 완료 +- [ ] **Phase 1 완료 승인** + +--- + +## Phase 3: Zustand 도입 + +### 3.1 사전 준비 + +- [ ] 기존 Zustand 스토어 분석 (`stores/item-master/`) +- [ ] 기존 품목기준관리 Context 분석 (`ItemMasterContext.tsx`) +- [ ] 마이그레이션 전략 수립 + +### 3.2 Zustand 적용 대상 + +| # | 대상 | 현재 상태 관리 | Zustand 적용 | +|---|------|---------------|-------------| +| 1 | 품목기준관리 설정 페이지 | Context | ⬜ | +| 2 | 품목관리 페이지 | 로컬 state | ⬜ | +| 3 | DynamicItemForm | 로컬 state + props | ⬜ | + +### 3.3 Zustand 마이그레이션 단계 + +- [ ] 1단계: 참조 데이터 (단위, 재질, 표면처리 등) Zustand로 이동 +- [ ] 2단계: 페이지/섹션/필드 CRUD Zustand로 이동 +- [ ] 3단계: 기존 Context 제거 +- [ ] 4단계: 테스트 및 검증 + +--- + +## 작업 로그 + +| 날짜 | 작업 내용 | 상태 | +|------|----------|------| +| 2025-12-24 | 체크리스트 문서 생성 | ✅ | +| - | 수동 테스트 시작 | ⬜ | +| - | Zustand 도입 | ⬜ | + +--- + +## 참고 문서 + +- `[PLAN-2025-12-16] dynamicitemform-hook-extraction.md` - 훅 분리 계획서 +- `[DESIGN-2025-12-20] item-master-zustand-refactoring.md` - Zustand 설계서 +- `[NEXT-2025-12-20] zustand-refactoring-session-context.md` - 세션 컨텍스트 \ No newline at end of file diff --git a/claudedocs/item-master/[NEXT-2025-12-24] item-master-refactoring-session.md b/claudedocs/item-master/[NEXT-2025-12-24] item-master-refactoring-session.md new file mode 100644 index 00000000..32d16fe9 --- /dev/null +++ b/claudedocs/item-master/[NEXT-2025-12-24] item-master-refactoring-session.md @@ -0,0 +1,134 @@ +# 품목기준관리 리팩토링 세션 컨텍스트 + +> **브랜치**: `feature/item-master-zustand` +> **날짜**: 2025-12-24 +> **상태**: Phase 2 완료, 커밋 대기 + +--- + +## 세션 요약 (12차 세션) + +### 완료된 작업 +- [x] 브랜치 상태 확인 (`feature/item-master-zustand`) +- [x] 기존 작업 혼동 정리 (품목관리 CRUD vs 품목기준관리 설정) +- [x] 작업 대상 파일 확인 (`ItemMasterDataManagement.tsx` - 1,799줄) +- [x] 기존 훅 분리 상태 파악 (7개 훅 이미 존재) +- [x] `ItemMasterDataManagement.tsx` 상세 분석 완료 +- [x] 훅 분리 계획서 작성 (`[PLAN-2025-12-24] hook-extraction-plan.md`) +- [x] **Phase 1: 신규 훅 4개 생성** + - `useInitialDataLoading.ts` - 초기 데이터 로딩 (~130줄) + - `useImportManagement.ts` - 섹션/필드 Import (~100줄) + - `useReorderManagement.ts` - 드래그앤드롭 순서 변경 (~80줄) + - `useDeleteManagement.ts` - 삭제/언링크 핸들러 (~100줄) +- [x] **Phase 2: UI 컴포넌트 2개 생성** + - `AttributeTabContent.tsx` - 속성 탭 콘텐츠 (~340줄) + - `ItemMasterDialogs.tsx` - 다이얼로그 통합 (~540줄) +- [x] 빌드 테스트 통과 + +### 현재 상태 +- **메인 컴포넌트**: 1,799줄 → ~1,478줄 (약 320줄 감소) +- **신규 훅**: 4개 생성 및 통합 +- **신규 UI 컴포넌트**: 2개 생성 (향후 추가 통합 가능) +- **빌드**: 통과 + +### 다음 TODO (커밋 후) +1. Git 커밋 (Phase 1, 2 변경사항) +2. Phase 3: 추가 코드 정리 (선택적) + - 속성 탭 내용을 `AttributeTabContent`로 완전 대체 (추가 ~500줄 감소 가능) + - 다이얼로그들을 `ItemMasterDialogs`로 완전 대체 +3. Zustand 도입 (3방향 동기화 문제 해결) + +--- + +## 핵심 정보 + +### 페이지 구분 (중요!) + +| 페이지 | URL | 컴포넌트 | 상태 | +|--------|-----|----------|------| +| 품목관리 CRUD | `/items/` | `DynamicItemForm` | ✅ 훅 분리 완료 (master 적용됨) | +| **품목기준관리 설정** | `/master-data/item-master-data-management` | `ItemMasterDataManagement` | ⏳ **훅 분리 진행 중** | + +### 현재 파일 구조 + +``` +src/components/items/ItemMasterDataManagement/ +├── ItemMasterDataManagement.tsx ← ~1,478줄 (리팩토링 후) +├── hooks/ (11개 - 7개 기존 + 4개 신규) +│ ├── usePageManagement.ts +│ ├── useSectionManagement.ts +│ ├── useFieldManagement.ts +│ ├── useMasterFieldManagement.ts +│ ├── useTemplateManagement.ts +│ ├── useAttributeManagement.ts +│ ├── useTabManagement.ts +│ ├── useInitialDataLoading.ts ← NEW +│ ├── useImportManagement.ts ← NEW +│ ├── useReorderManagement.ts ← NEW +│ └── useDeleteManagement.ts ← NEW +├── components/ (5개 - 3개 기존 + 2개 신규) +│ ├── DraggableSection.tsx +│ ├── DraggableField.tsx +│ ├── ConditionalDisplayUI.tsx +│ ├── AttributeTabContent.tsx ← NEW +│ └── ItemMasterDialogs.tsx ← NEW +├── services/ (6개) +├── dialogs/ (13개) +├── tabs/ (4개) +└── utils/ (1개) +``` + +### 브랜치 상태 + +``` +master (원본 보존) + │ + └── feature/item-master-zustand (현재) + ├── Zustand 테스트 페이지 (/items-management-test/) - 놔둠 + ├── Zustand 스토어 (stores/item-master/) - 나중에 사용 + └── 기존 품목기준관리 페이지 - 훅 분리 진행 중 +``` + +### 작업 진행률 + +``` +시작: ItemMasterDataManagement.tsx 1,799줄 + ↓ Phase 1: 훅 분리 (4개 신규 훅) +현재: ~1,478줄 (-321줄, -18%) + ↓ Phase 2: UI 컴포넌트 분리 (2개 신규 컴포넌트 생성) + ↓ Phase 3: 추가 통합 (선택적) +목표: ~500줄 (메인 컴포넌트) + ↓ Zustand 적용 +최종: 3방향 동기화 문제 해결 +``` + +--- + +## 생성된 파일 목록 + +### 신규 훅 (Phase 1) +1. `hooks/useInitialDataLoading.ts` - 초기 데이터 로딩, 에러 처리 +2. `hooks/useImportManagement.ts` - 섹션/필드 Import 다이얼로그 상태 및 핸들러 +3. `hooks/useReorderManagement.ts` - 드래그앤드롭 순서 변경 +4. `hooks/useDeleteManagement.ts` - 삭제, 언링크, 초기화 핸들러 + +### 신규 UI 컴포넌트 (Phase 2) +1. `components/AttributeTabContent.tsx` - 속성 탭 전체 UI +2. `components/ItemMasterDialogs.tsx` - 모든 다이얼로그 통합 렌더링 + +--- + +## 참고 문서 + +- `[PLAN-2025-12-24] hook-extraction-plan.md` - 훅 분리 계획서 (상세) +- `[DESIGN-2025-12-20] item-master-zustand-refactoring.md` - Zustand 설계서 +- `[IMPL-2025-12-24] item-master-test-and-zustand.md` - 테스트 체크리스트 + +--- + +## 다음 세션 시작 명령 + +``` +품목기준관리 설정 페이지(ItemMasterDataManagement.tsx) 추가 리팩토링 또는 Zustand 도입 진행해줘. +[NEXT-2025-12-24] item-master-refactoring-session.md 문서 확인하고 시작해. +``` diff --git a/claudedocs/item-master/[PLAN-2025-12-24] hook-extraction-plan.md b/claudedocs/item-master/[PLAN-2025-12-24] hook-extraction-plan.md new file mode 100644 index 00000000..b4a6bcc1 --- /dev/null +++ b/claudedocs/item-master/[PLAN-2025-12-24] hook-extraction-plan.md @@ -0,0 +1,270 @@ +# ItemMasterDataManagement 훅 분리 계획서 + +> **날짜**: 2025-12-24 +> **대상 파일**: `src/components/items/ItemMasterDataManagement.tsx` (1,799줄) +> **목표**: ~500줄로 축소 + +--- + +## 현재 구조 분석 + +### 파일 구성 + +| 구간 | 줄 수 | 내용 | +|------|-------|------| +| Import | 1-61 | React, UI, 다이얼로그, 훅 import | +| 상수 | 63-91 | ITEM_TYPE_OPTIONS, INPUT_TYPE_OPTIONS | +| Context 구조분해 | 94-124 | useItemMaster에서 20+개 함수/상태 | +| 훅 초기화 | 127-286 | 7개 훅 + 150+개 상태 구조분해 | +| useMemo | 298-372 | sectionsAsTemplates 변환 | +| useState/useEffect | 374-504 | 로딩, 에러, 모바일, Import 상태 | +| 핸들러 | 519-743 | Import, Clone, Delete, Reorder | +| UI 렌더링 | 746-1799 | Tabs + 13개 다이얼로그 | + +### 기존 훅 (7개) + +``` +src/components/items/ItemMasterDataManagement/hooks/ +├── usePageManagement.ts - 페이지 CRUD +├── useSectionManagement.ts - 섹션 CRUD +├── useFieldManagement.ts - 필드 CRUD +├── useMasterFieldManagement.ts - 마스터 필드 CRUD +├── useTemplateManagement.ts - 템플릿 관리 +├── useAttributeManagement.ts - 속성 관리 +└── useTabManagement.ts - 탭 관리 +``` + +--- + +## 분리 계획 + +### Phase 1: 신규 훅 생성 (4개) + +#### 1. `useInitialDataLoading` (~100줄 분리) + +**분리 대상:** +- 초기 데이터 로딩 useEffect (387-492줄) +- 로딩/에러 상태 (isInitialLoading, error) +- transformers 호출 로직 + +**반환값:** +```typescript +{ + isInitialLoading: boolean; + error: string | null; + reload: () => Promise; +} +``` + +#### 2. `useImportManagement` (~80줄 분리) + +**분리 대상:** +- Import 다이얼로그 상태 (512-516줄) +- handleImportSection (519-530줄) +- handleImportField (540-559줄) +- handleCloneSection (562-570줄) + +**반환값:** +```typescript +{ + // 상태 + isImportSectionDialogOpen, setIsImportSectionDialogOpen, + isImportFieldDialogOpen, setIsImportFieldDialogOpen, + selectedImportSectionId, setSelectedImportSectionId, + selectedImportFieldId, setSelectedImportFieldId, + importFieldTargetSectionId, setImportFieldTargetSectionId, + // 핸들러 + handleImportSection, + handleImportField, + handleCloneSection, +} +``` + +#### 3. `useReorderManagement` (~60줄 분리) + +**분리 대상:** +- moveSection (650-668줄) +- moveField (672-702줄) + +**반환값:** +```typescript +{ + moveSection: (dragIndex: number, hoverIndex: number) => Promise; + moveField: (sectionId: number, dragFieldId: number, hoverFieldId: number) => Promise; +} +``` + +#### 4. `useDeleteManagement` (~50줄 분리) + +**분리 대상:** +- handleDeletePageWithTracking (582-588줄) +- handleDeleteSectionWithTracking (591-597줄) +- handleUnlinkFieldWithTracking (601-609줄) +- handleResetAllData (705-743줄) + +**반환값:** +```typescript +{ + handleDeletePage: (pageId: number) => void; + handleDeleteSection: (pageId: number, sectionId: number) => void; + handleUnlinkField: (pageId: string, sectionId: string, fieldId: string) => Promise; + handleResetAllData: () => void; +} +``` + +### Phase 2: UI 컴포넌트 분리 (2개) + +#### 1. `AttributeTabContent` (~400줄 분리) + +**분리 대상:** +- 속성 탭 내용 (807-1331줄) +- 단위/재질/표면처리 반복 UI 통합 + +**Props:** +```typescript +interface AttributeTabContentProps { + activeAttributeTab: string; + attributeSubTabs: AttributeSubTab[]; + unitOptions: UnitOption[]; + materialOptions: MaterialOption[]; + surfaceTreatmentOptions: SurfaceTreatmentOption[]; + customAttributeOptions: Record; + attributeColumns: Record; + itemMasterFields: ItemMasterField[]; + // 핸들러들... +} +``` + +#### 2. `ItemMasterDialogs` (~280줄 분리) + +**분리 대상:** +- 13개 다이얼로그 렌더링 (1442-1797줄) + +**Props:** +```typescript +interface ItemMasterDialogsProps { + // 모든 다이얼로그 관련 props +} +``` + +### Phase 3: 코드 정리 + +1. **래퍼 함수 제거** (~30줄) + - `handleAddSectionWrapper` 등을 훅 내부로 이동 + - `selectedPage`를 훅 파라미터로 전달 + +2. **unused 변수 정리** + - `_mounted`, `_isLoading` 등 제거 + +3. **Import 최적화** + - 사용하지 않는 import 제거 + +--- + +## 예상 결과 + +### 줄 수 변화 + +| 항목 | 현재 | 분리 후 | +|------|------|---------| +| Import | 61 | 40 | +| 상수 | 28 | 28 | +| 훅 사용 | 160 | 60 | +| useMemo | 75 | 75 | +| 상태/Effect | 130 | 20 | +| 핸들러 | 225 | 30 | +| UI 렌더링 | 1,053 | 300 | +| **합계** | **1,799** | **~550** | + +### 새 파일 구조 + +``` +src/components/items/ItemMasterDataManagement/ +├── ItemMasterDataManagement.tsx ← ~550줄 (메인) +├── hooks/ +│ ├── index.ts +│ ├── usePageManagement.ts (기존) +│ ├── useSectionManagement.ts (기존) +│ ├── useFieldManagement.ts (기존) +│ ├── useMasterFieldManagement.ts (기존) +│ ├── useTemplateManagement.ts (기존) +│ ├── useAttributeManagement.ts (기존) +│ ├── useTabManagement.ts (기존) +│ ├── useInitialDataLoading.ts ← NEW +│ ├── useImportManagement.ts ← NEW +│ ├── useReorderManagement.ts ← NEW +│ └── useDeleteManagement.ts ← NEW +├── components/ +│ ├── AttributeTabContent.tsx ← NEW +│ └── ItemMasterDialogs.tsx ← NEW +├── dialogs/ (기존 13개) +├── tabs/ (기존 4개) +├── services/ (기존 6개) +└── utils/ (기존 1개) +``` + +--- + +## 작업 순서 + +### Step 1: useInitialDataLoading 훅 생성 +- [ ] 훅 파일 생성 +- [ ] 로딩/에러 상태 이동 +- [ ] useEffect 이동 +- [ ] 메인 컴포넌트에서 사용 + +### Step 2: useImportManagement 훅 생성 +- [ ] 훅 파일 생성 +- [ ] Import 상태 이동 +- [ ] 핸들러 이동 +- [ ] 메인 컴포넌트에서 사용 + +### Step 3: useReorderManagement 훅 생성 +- [ ] 훅 파일 생성 +- [ ] moveSection, moveField 이동 +- [ ] 메인 컴포넌트에서 사용 + +### Step 4: useDeleteManagement 훅 생성 +- [ ] 훅 파일 생성 +- [ ] Delete/Unlink 핸들러 이동 +- [ ] 메인 컴포넌트에서 사용 + +### Step 5: AttributeTabContent 컴포넌트 분리 +- [ ] 컴포넌트 파일 생성 +- [ ] 속성 탭 UI 이동 +- [ ] 반복 코드 통합 + +### Step 6: ItemMasterDialogs 컴포넌트 분리 +- [ ] 컴포넌트 파일 생성 +- [ ] 13개 다이얼로그 이동 + +### Step 7: 정리 및 테스트 +- [ ] 래퍼 함수 정리 +- [ ] unused 코드 제거 +- [ ] 빌드 확인 +- [ ] 수동 테스트 + +--- + +## 리스크 및 주의사항 + +1. **Context 의존성**: 훅들이 `useItemMaster` Context에 의존 + - 해결: 필요한 함수만 훅 파라미터로 전달 + +2. **상태 공유**: 여러 훅에서 동일 상태 사용 + - 해결: 공통 상태는 메인 컴포넌트에서 관리 + +3. **타입 호환성**: 기존 훅의 setter 타입 문제 + - 해결: `as any` 임시 사용 또는 타입 수정 + +4. **테스트**: 페이지 기능이 많아 수동 테스트 필요 + - 해결: 체크리스트 작성하여 순차 테스트 + +--- + +## 다음 단계 + +1. 이 계획서 확인 후 작업 시작 +2. Step 1부터 순차 진행 +3. 각 Step 완료 후 빌드 확인 +4. 최종 수동 테스트 \ No newline at end of file diff --git a/src/components/items/ItemMasterDataManagement.tsx b/src/components/items/ItemMasterDataManagement.tsx index eb869cf3..c0c7261d 100644 --- a/src/components/items/ItemMasterDataManagement.tsx +++ b/src/components/items/ItemMasterDataManagement.tsx @@ -6,42 +6,13 @@ import { PageHeader } from '@/components/organisms/PageHeader'; import { useItemMaster } from '@/contexts/ItemMasterContext'; import type { SectionTemplate, BOMItem, TemplateField } from '@/contexts/ItemMasterContext'; import { MasterFieldTab, HierarchyTab, SectionsTab } from './ItemMasterDataManagement/tabs'; -import { FieldDialog } from './ItemMasterDataManagement/dialogs/FieldDialog'; -// ConditionalFieldConfig type removed - not currently used -import { FieldDrawer } from './ItemMasterDataManagement/dialogs/FieldDrawer'; -import { TabManagementDialogs } from './ItemMasterDataManagement/dialogs/TabManagementDialogs'; -import { OptionDialog } from './ItemMasterDataManagement/dialogs/OptionDialog'; -import { ColumnManageDialog } from './ItemMasterDataManagement/dialogs/ColumnManageDialog'; -import { PathEditDialog } from './ItemMasterDataManagement/dialogs/PathEditDialog'; -import { PageDialog } from './ItemMasterDataManagement/dialogs/PageDialog'; -import { SectionDialog } from './ItemMasterDataManagement/dialogs/SectionDialog'; -import { MasterFieldDialog } from './ItemMasterDataManagement/dialogs/MasterFieldDialog'; -import { TemplateFieldDialog } from './ItemMasterDataManagement/dialogs/TemplateFieldDialog'; -import { LoadTemplateDialog } from './ItemMasterDataManagement/dialogs/LoadTemplateDialog'; -import { ColumnDialog } from './ItemMasterDataManagement/dialogs/ColumnDialog'; -import { SectionTemplateDialog } from './ItemMasterDataManagement/dialogs/SectionTemplateDialog'; -import { ImportSectionDialog } from './ItemMasterDataManagement/dialogs/ImportSectionDialog'; -import { ImportFieldDialog } from './ItemMasterDataManagement/dialogs/ImportFieldDialog'; -import { itemMasterApi } from '@/lib/api/item-master'; -import { getErrorMessage, ApiError } from '@/lib/api/error-handler'; import { LoadingSpinner } from '@/components/ui/loading-spinner'; import { ServerErrorPage } from '@/components/common/ServerErrorPage'; -import { - transformPagesResponse, - transformSectionsResponse, - transformSectionTemplatesResponse, - transformFieldsResponse, - transformCustomTabsResponse, - transformUnitOptionsResponse, - transformSectionTemplateFromSection, -} from '@/lib/api/transformers'; +// 2025-12-24: Phase 2 UI 컴포넌트 분리 +import { AttributeTabContent, ItemMasterDialogs } from './ItemMasterDataManagement/components'; import { Database, - Plus, - Trash2, FileText, - Settings, - Package, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; @@ -58,6 +29,11 @@ import { useTemplateManagement, useAttributeManagement, useTabManagement, + // 2025-12-24: 신규 훅 추가 + useInitialDataLoading, + useImportManagement, + useReorderManagement, + useDeleteManagement, } from './ItemMasterDataManagement/hooks'; const ITEM_TYPE_OPTIONS = [ @@ -133,6 +109,87 @@ export function ItemMasterDataManagement() { const attributeManagement = useAttributeManagement(); const tabManagement = useTabManagement(); + // 2025-12-24: 신규 훅 (먼저 tabManagement, attributeManagement에서 setter 추출 필요) + const { + customTabs, setCustomTabs, + activeTab, setActiveTab, + attributeSubTabs, setAttributeSubTabs, + activeAttributeTab, setActiveAttributeTab, + isAddTabDialogOpen, setIsAddTabDialogOpen, + isManageTabsDialogOpen, setIsManageTabsDialogOpen, + newTabLabel, setNewTabLabel, + editingTabId, setEditingTabId, + deletingTabId, setDeletingTabId, + isDeleteTabDialogOpen, setIsDeleteTabDialogOpen, + isManageAttributeTabsDialogOpen, setIsManageAttributeTabsDialogOpen, + isAddAttributeTabDialogOpen, setIsAddAttributeTabDialogOpen, + newAttributeTabLabel, setNewAttributeTabLabel, + editingAttributeTabId, setEditingAttributeTabId, + deletingAttributeTabId, setDeletingAttributeTabId, + isDeleteAttributeTabDialogOpen, setIsDeleteAttributeTabDialogOpen, + handleAddTab, handleUpdateTab, handleDeleteTab, confirmDeleteTab, + handleAddAttributeTab, handleUpdateAttributeTab, + handleDeleteAttributeTab, confirmDeleteAttributeTab, + moveTabUp, moveTabDown, + moveAttributeTabUp, moveAttributeTabDown, + getTabIcon, handleEditTabFromManage, + } = tabManagement; + + const { + unitOptions, setUnitOptions, + materialOptions, setMaterialOptions, + surfaceTreatmentOptions, setSurfaceTreatmentOptions, + customAttributeOptions, setCustomAttributeOptions, + isOptionDialogOpen, setIsOptionDialogOpen, + editingOptionType, setEditingOptionType, + newOptionValue, setNewOptionValue, + newOptionLabel, setNewOptionLabel, + newOptionColumnValues, setNewOptionColumnValues, + newOptionInputType, setNewOptionInputType, + newOptionRequired, setNewOptionRequired, + newOptionOptions, setNewOptionOptions, + newOptionPlaceholder, setNewOptionPlaceholder, + newOptionDefaultValue, setNewOptionDefaultValue, + isColumnManageDialogOpen, setIsColumnManageDialogOpen, + managingColumnType, setManagingColumnType, + attributeColumns, setAttributeColumns, + newColumnName, setNewColumnName, + newColumnKey, setNewColumnKey, + newColumnType, setNewColumnType, + newColumnRequired, setNewColumnRequired, + handleAddOption, handleDeleteOption, + handleAddColumn: _handleAddColumn, handleDeleteColumn: _handleDeleteColumn, + } = attributeManagement; + + // 2025-12-24: 신규 훅 초기화 + const { isInitialLoading, error } = useInitialDataLoading({ + setCustomTabs, + setUnitOptions, + }); + + const importManagement = useImportManagement(); + const { + isImportSectionDialogOpen, setIsImportSectionDialogOpen, + isImportFieldDialogOpen, setIsImportFieldDialogOpen, + selectedImportSectionId, setSelectedImportSectionId, + selectedImportFieldId, setSelectedImportFieldId, + importFieldTargetSectionId, setImportFieldTargetSectionId, + handleImportSection: handleImportSectionFromHook, + handleImportField: handleImportFieldFromHook, + handleCloneSection, + } = importManagement; + + const reorderManagement = useReorderManagement(); + const { moveSection: moveSectionFromHook, moveField: moveFieldFromHook } = reorderManagement; + + const deleteManagement = useDeleteManagement({ itemPages }); + const { + handleDeletePage: handleDeletePageWithTracking, + handleDeleteSection: _handleDeleteSectionWithTracking, + handleUnlinkField: handleUnlinkFieldWithTracking, + handleResetAllData: handleResetAllDataFromHook, + } = deleteManagement; + // 훅에서 필요한 값들 구조분해 const { selectedPageId, setSelectedPageId, selectedPage, @@ -233,57 +290,6 @@ export function ItemMasterDataManagement() { handleDeleteBOMItemFromTemplate, } = templateManagement; - const { - unitOptions, setUnitOptions, - materialOptions, setMaterialOptions, - surfaceTreatmentOptions, setSurfaceTreatmentOptions, - customAttributeOptions, setCustomAttributeOptions, - isOptionDialogOpen, setIsOptionDialogOpen, - editingOptionType, setEditingOptionType, - newOptionValue, setNewOptionValue, - newOptionLabel, setNewOptionLabel, - newOptionColumnValues, setNewOptionColumnValues, - newOptionInputType, setNewOptionInputType, - newOptionRequired, setNewOptionRequired, - newOptionOptions, setNewOptionOptions, - newOptionPlaceholder, setNewOptionPlaceholder, - newOptionDefaultValue, setNewOptionDefaultValue, - isColumnManageDialogOpen, setIsColumnManageDialogOpen, - managingColumnType, setManagingColumnType, - attributeColumns, setAttributeColumns, - newColumnName, setNewColumnName, - newColumnKey, setNewColumnKey, - newColumnType, setNewColumnType, - newColumnRequired, setNewColumnRequired, - handleAddOption, handleDeleteOption, - handleAddColumn: _handleAddColumn, handleDeleteColumn: _handleDeleteColumn, - } = attributeManagement; - - const { - customTabs, setCustomTabs, - activeTab, setActiveTab, - attributeSubTabs, setAttributeSubTabs, - activeAttributeTab, setActiveAttributeTab, - isAddTabDialogOpen, setIsAddTabDialogOpen, - isManageTabsDialogOpen, setIsManageTabsDialogOpen, - newTabLabel, setNewTabLabel, - editingTabId, setEditingTabId, - deletingTabId, setDeletingTabId, - isDeleteTabDialogOpen, setIsDeleteTabDialogOpen, - isManageAttributeTabsDialogOpen, setIsManageAttributeTabsDialogOpen, - isAddAttributeTabDialogOpen, setIsAddAttributeTabDialogOpen, - newAttributeTabLabel, setNewAttributeTabLabel, - editingAttributeTabId, setEditingAttributeTabId, - deletingAttributeTabId, setDeletingAttributeTabId, - isDeleteAttributeTabDialogOpen, setIsDeleteAttributeTabDialogOpen, - handleAddTab, handleUpdateTab, handleDeleteTab, confirmDeleteTab, - handleAddAttributeTab, handleUpdateAttributeTab, - handleDeleteAttributeTab, confirmDeleteAttributeTab, - moveTabUp, moveTabDown, - moveAttributeTabUp, moveAttributeTabDown, - getTabIcon, handleEditTabFromManage, - } = tabManagement; - // 모든 페이지의 섹션을 하나의 배열로 평탄화 const _itemSections = itemPages.flatMap(page => page.sections.map(section => ({ @@ -371,129 +377,6 @@ export function ItemMasterDataManagement() { return uniqueSections; }, [itemPages, independentSections]); - // 마운트 상태 추적 (SSR 호환) - const [_mounted, setMounted] = useState(false); - - useEffect(() => { - setMounted(true); - }, []); - - // API 로딩 및 에러 상태 관리 - const [isInitialLoading, setIsInitialLoading] = useState(true); // 초기 데이터 로딩 - const [_isLoading, _setIsLoading] = useState(false); // 개별 작업 로딩 - const [error, setError] = useState(null); // 에러 메시지 - - // 초기 데이터 로딩 - useEffect(() => { - const loadInitialData = async () => { - try { - setIsInitialLoading(true); - setError(null); - - const data = await itemMasterApi.init(); - - // 2025-11-26: 백엔드가 entity_relationships 기반으로 변경됨 - // - pages[].sections: entity_relationships 기반으로 연결된 섹션 (이미 포함) - // - sections: 모든 독립 섹션 (재사용 가능 목록) - // - sectionTemplates: 삭제됨 → sections로 대체 - - // 1. 페이지 데이터 로드 (섹션이 이미 포함되어 있음) - const transformedPages = transformPagesResponse(data.pages); - loadItemPages(transformedPages); - - // 2. 독립 섹션 로드 (모든 재사용 가능 섹션) - // 백엔드가 sections 배열로 모든 독립 섹션을 반환 - if (data.sections && data.sections.length > 0) { - const transformedSections = transformSectionsResponse(data.sections); - loadIndependentSections(transformedSections); - console.log('✅ 독립 섹션 로드:', transformedSections.length); - } - - // 3. 섹션 템플릿 로드 (sectionTemplates → sections로 통합됨) - // 기존 sectionTemplates가 있으면 호환성 유지, 없으면 sections 사용 - if (data.sectionTemplates && data.sectionTemplates.length > 0) { - const transformedTemplates = transformSectionTemplatesResponse(data.sectionTemplates); - loadSectionTemplates(transformedTemplates); - } else if (data.sections && data.sections.length > 0) { - // sectionTemplates가 없으면 sections에서 is_template=true인 것만 사용 - const templates = data.sections - .filter((s: { is_template?: boolean }) => s.is_template) - .map(transformSectionTemplateFromSection); - if (templates.length > 0) { - loadSectionTemplates(templates); - } - } - - // 필드 로드 (2025-11-27: masterFields가 fields로 통합됨) - // data.fields = 모든 필드 목록 (백엔드 init API에서 반환) - if (data.fields && data.fields.length > 0) { - const transformedFields = transformFieldsResponse(data.fields); - - // 2025-11-27: section_id가 null인 필드만 필터링 (독립 필드) - const independentOnlyFields = transformedFields.filter( - f => f.section_id === null || f.section_id === undefined - ); - - // 2025-11-27: 항목탭용 (itemMasterFields) - 모든 필드 로드 - // 계층구조에서 추가한 필드도 항목탭에 바로 표시되도록 함 - // addFieldToSection에서 setItemMasterFields를 호출하므로 일관성 유지 - loadItemMasterFields(transformedFields as any); - - // 독립 필드용 (independentFields) - section_id=null인 필드만 - loadIndependentFields(independentOnlyFields); - - console.log('✅ 필드 로드:', { - total: transformedFields.length, - independent: independentOnlyFields.length, - allFieldsForItemsTab: transformedFields.length, - }); - } - - // 커스텀 탭 로드 (local state) - 교체 방식 (복제 방지) - if (data.customTabs && data.customTabs.length > 0) { - const transformedTabs = transformCustomTabsResponse(data.customTabs); - setCustomTabs(transformedTabs); - } - - // 단위 옵션 로드 (local state) - if (data.unitOptions && data.unitOptions.length > 0) { - const transformedUnits = transformUnitOptionsResponse(data.unitOptions); - setUnitOptions(transformedUnits); - } - - console.log('✅ Initial data loaded:', { - pages: data.pages?.length || 0, - sections: data.sections?.length || 0, - fields: data.fields?.length || 0, - customTabs: data.customTabs?.length || 0, - unitOptions: data.unitOptions?.length || 0, - }); - - } catch (err) { - if (err instanceof ApiError && err.errors) { - // Validation 에러 (422) - const errorMessages = Object.entries(err.errors) - .map(([field, messages]) => `${field}: ${messages.join(', ')}`) - .join('\n'); - toast.error(errorMessages); - setError('입력값을 확인해주세요.'); - } else { - const errorMessage = getErrorMessage(err); - setError(errorMessage); - toast.error(errorMessage); - } - console.error('❌ Failed to load initial data:', err); - } finally { - setIsInitialLoading(false); - } - }; - - loadInitialData(); - }, []); - - // ===== 훅으로 이동된 상태들 (참조용 주석) ===== - // 탭, 속성, 페이지, 섹션 관련 상태는 위의 훅에서 관리됩니다. - // 모바일 체크 const [isMobile, setIsMobile] = useState(false); useEffect(() => { @@ -508,122 +391,21 @@ export function ItemMasterDataManagement() { // BOM 관리 상태 (훅에 없음) const [_bomItems, setBomItems] = useState([]); - // 2025-11-26 추가: 섹션/필드 불러오기 다이얼로그 상태 - const [isImportSectionDialogOpen, setIsImportSectionDialogOpen] = useState(false); - const [isImportFieldDialogOpen, setIsImportFieldDialogOpen] = useState(false); - const [selectedImportSectionId, setSelectedImportSectionId] = useState(null); - const [selectedImportFieldId, setSelectedImportFieldId] = useState(null); - const [importFieldTargetSectionId, setImportFieldTargetSectionId] = useState(null); - - // 2025-11-26 추가: 섹션 불러오기 핸들러 - const handleImportSection = async () => { - if (!selectedPageId || !selectedImportSectionId) return; - - try { - await linkSectionToPage(selectedPageId, selectedImportSectionId); - toast.success('섹션을 불러왔습니다.'); - setSelectedImportSectionId(null); - } catch (error) { - console.error('섹션 불러오기 실패:', error); - toast.error(getErrorMessage(error)); - } - }; - - /** - * 필드 불러오기 핸들러 - * - * @description 2025-11-27: API 변경으로 단순화 - * - 이전: source 파라미터로 'master' | 'independent' 구분 - * - 현재: 모든 필드가 item_fields로 통합 → linkFieldToSection만 사용 - * - section_id=NULL인 필드를 섹션에 연결하는 방식으로 통일 - */ - const handleImportField = async () => { - if (!importFieldTargetSectionId || !selectedImportFieldId) return; - - try { - // 2025-12-02: 섹션별 순서 종속 - 해당 섹션의 마지막 순서 + 1로 설정 - const targetSection = selectedPage?.sections.find(s => s.id === importFieldTargetSectionId); - const existingFieldsCount = targetSection?.fields?.length ?? 0; - const newOrderNo = existingFieldsCount; // 0-based로 마지막 다음 순서 - - // 2025-11-27: 통합된 필드 연결 방식 - await linkFieldToSection(importFieldTargetSectionId, selectedImportFieldId, newOrderNo); - toast.success('필드를 섹션에 연결했습니다.'); - - setSelectedImportFieldId(null); - setImportFieldTargetSectionId(null); - } catch (error) { - console.error('필드 불러오기 실패:', error); - toast.error(getErrorMessage(error)); - } - }; - - // 2025-11-26 추가: 섹션 복제 핸들러 - const handleCloneSection = async (sectionId: number) => { - try { - await cloneSection(sectionId); - toast.success('섹션이 복제되었습니다.'); - } catch (error) { - console.error('섹션 복제 실패:', error); - toast.error(getErrorMessage(error)); - } - }; - - // ===== 이하 핸들러들은 훅으로 이동되어 제거됨 ===== - // handleAddOption, handleDeleteOption → useAttributeManagement - // handleAddPage, handleDuplicatePage → usePageManagement - // handleAddSection, handleLinkTemplate, handleEditSectionTitle, handleSaveSectionTitle → useSectionManagement - // handleAddField, handleEditField → useFieldManagement - // handleAddMasterField, handleEditMasterField, handleUpdateMasterField, handleDeleteMasterField → useMasterFieldManagement - // handleAddSectionTemplate 등 템플릿 관련 → useTemplateManagement - // handleAddTab 등 탭 관련 → useTabManagement - - // 페이지 삭제 핸들러 (pendingChanges 제거 포함) - 훅에 없어 유지 - const handleDeletePageWithTracking = (pageId: number) => { - const pageToDelete = itemPages.find(p => p.id === pageId); - const sectionIds = pageToDelete?.sections.map(s => s.id) || []; - const fieldIds = pageToDelete?.sections.flatMap(s => s.fields?.map(f => f.id) || []) || []; - deleteItemPage(pageId); - console.log('페이지 삭제 완료:', { pageId, removedSections: sectionIds.length, removedFields: fieldIds.length }); - }; - - // 섹션 삭제 핸들러 (pendingChanges 제거 포함) - 훅에 없어 유지 - const _handleDeleteSectionWithTracking = (pageId: number, sectionId: number) => { - const page = itemPages.find(p => p.id === pageId); - const sectionToDelete = page?.sections.find(s => s.id === sectionId); - const fieldIds = sectionToDelete?.fields?.map(f => f.id) || []; - deleteSection(Number(sectionId)); - console.log('섹션 삭제 완료:', { sectionId, removedFields: fieldIds.length }); - }; - - // 필드 연결 해제 핸들러 (2025-11-27: 삭제 → unlink로 변경) - // 섹션에서 필드 연결만 해제하고, 필드 자체는 독립 필드 목록에 유지됨 - const handleUnlinkFieldWithTracking = async (_pageId: string, sectionId: string, fieldId: string) => { - try { - await unlinkFieldFromSection(Number(sectionId), Number(fieldId)); - console.log('필드 연결 해제 완료:', fieldId); - } catch (error) { - console.error('필드 연결 해제 실패:', error); - toast.error('필드 연결 해제에 실패했습니다'); - } - }; - - // 절대경로 업데이트 - 로컬에서 처리 - const _handleUpdateAbsolutePathLocal = (pageId: number, newPath: string) => { - updateItemPage(pageId, { absolute_path: newPath }); - toast.success('절대경로가 업데이트되었습니다'); - }; - - // 필드 순서 변경 - const _handleReorderFieldsLocal = (sectionId: number, orderedFieldIds: number[]) => { - reorderFields(sectionId, orderedFieldIds); - }; - - // 페이지 이름 업데이트 - const _handleUpdatePageNameLocal = (pageId: number, newName: string) => { - updateItemPage(pageId, { page_name: newName }); - toast.success('페이지 이름이 업데이트되었습니다'); - }; + // 2025-12-24: 신규 훅에서 가져온 핸들러 래퍼 + const handleImportSection = async () => handleImportSectionFromHook(selectedPageId); + const handleImportField = async () => handleImportFieldFromHook(selectedPage); + const moveSection = async (dragIndex: number, hoverIndex: number) => moveSectionFromHook(selectedPage, dragIndex, hoverIndex); + const moveField = async (sectionId: number, dragFieldId: number, hoverFieldId: number) => moveFieldFromHook(selectedPage, sectionId, dragFieldId, hoverFieldId); + const handleResetAllData = () => handleResetAllDataFromHook( + setUnitOptions, + setMaterialOptions, + setSurfaceTreatmentOptions, + setCustomAttributeOptions, + setAttributeColumns, + setBomItems, + setCustomTabs, + setAttributeSubTabs, + ); // ===== 래퍼 함수들 (훅 함수에 selectedPage 바인딩 및 타입 호환성) ===== const handleAddSectionWrapper = () => handleAddSection(selectedPage); @@ -638,110 +420,6 @@ export function ItemMasterDataManagement() { // eslint-disable-next-line @typescript-eslint/no-explicit-any const setNewPageItemTypeWrapper: React.Dispatch> = setNewPageItemType as any; - // ===== 유틸리티 함수들 ===== - // 현재 섹션의 모든 필드 가져오기 (조건부 필드 참조용) - const _getAllFieldsInSection = (sectionId: number) => { - if (!selectedPage) return []; - const section = selectedPage.sections.find(s => s.id === sectionId); - return section?.fields || []; - }; - - // 섹션 순서 변경 핸들러 (드래그앤드롭) - const moveSection = async (dragIndex: number, hoverIndex: number) => { - if (!selectedPage) return; - - const sections = [...selectedPage.sections]; - const [draggedSection] = sections.splice(dragIndex, 1); - sections.splice(hoverIndex, 0, draggedSection); - - // 새로운 순서의 섹션 ID 배열 생성 - const sectionIds = sections.map(s => s.id); - - try { - // API를 통해 섹션 순서 변경 (Context의 reorderSections 사용) - await reorderSections(selectedPage.id, sectionIds); - toast.success('섹션 순서가 변경되었습니다'); - } catch (error) { - console.error('섹션 순서 변경 실패:', error); - toast.error('섹션 순서 변경에 실패했습니다'); - } - }; - - // 필드 순서 변경 핸들러 - // 2025-12-03: ID 기반으로 변경 (index stale 문제 해결) - const moveField = async (sectionId: number, dragFieldId: number, hoverFieldId: number) => { - if (!selectedPage) return; - const section = selectedPage.sections.find(s => s.id === sectionId); - if (!section || !section.fields) return; - - // 동일 필드면 스킵 - if (dragFieldId === hoverFieldId) return; - - // 정렬된 배열에서 ID로 인덱스 찾기 - const sortedFields = [...section.fields].sort((a, b) => (a.order_no ?? 0) - (b.order_no ?? 0)); - const dragIndex = sortedFields.findIndex(f => f.id === dragFieldId); - const hoverIndex = sortedFields.findIndex(f => f.id === hoverFieldId); - - // 유효하지 않은 인덱스 체크 - if (dragIndex === -1 || hoverIndex === -1) { - return; - } - - // 드래그된 필드를 제거하고 새 위치에 삽입 - const [draggedField] = sortedFields.splice(dragIndex, 1); - sortedFields.splice(hoverIndex, 0, draggedField); - - const newFieldIds = sortedFields.map(f => f.id); - - try { - await reorderFields(sectionId, newFieldIds); - toast.success('항목 순서가 변경되었습니다'); - } catch (error) { - toast.error('항목 순서 변경에 실패했습니다'); - } - }; - - // 전체 데이터 초기화 핸들러 - const _handleResetAllData = () => { - if (!confirm('⚠️ 경고: 모든 품목기준관리 데이터(계층구조, 섹션, 항목, 속성)를 초기화하시겠습니까?\n\n이 작업은 되돌릴 수 없습니다!')) { - return; - } - - try { - // ItemMasterContext의 모든 데이터 및 캐시 초기화 - resetAllData(); - - // 로컬 상태 초기화 (ItemMasterContext가 관리하지 않는 컴포넌트 로컬 상태) - setUnitOptions([]); - setMaterialOptions([]); - setSurfaceTreatmentOptions([]); - setCustomAttributeOptions({}); - setAttributeColumns({}); - setBomItems([]); - - // 탭 상태 초기화 (기본 탭만 남김) - setCustomTabs([ - { id: 'hierarchy', label: '계층구조', icon: 'FolderTree', isDefault: true, order: 1 }, - { id: 'sections', label: '섹션', icon: 'Layers', isDefault: true, order: 2 }, - { id: 'items', label: '항목', icon: 'ListTree', isDefault: true, order: 3 }, - { id: 'attributes', label: '속성', icon: 'Settings', isDefault: true, order: 4 } - ]); - - setAttributeSubTabs([]); - - console.log('🗑️ 모든 품목기준관리 데이터가 초기화되었습니다'); - toast.success('✅ 모든 데이터가 초기화되었습니다!\n계층구조, 섹션, 항목, 속성이 모두 삭제되었습니다.'); - - // 페이지 새로고침하여 완전히 초기화된 상태 반영 - setTimeout(() => { - window.location.reload(); - }, 1500); - } catch (error) { - toast.error('초기화 중 오류가 발생했습니다'); - console.error('Reset error:', error); - } - }; - // 초기 로딩 중 UI if (isInitialLoading) { return ( diff --git a/src/components/items/ItemMasterDataManagement/components/AttributeTabContent.tsx b/src/components/items/ItemMasterDataManagement/components/AttributeTabContent.tsx new file mode 100644 index 00000000..ec048588 --- /dev/null +++ b/src/components/items/ItemMasterDataManagement/components/AttributeTabContent.tsx @@ -0,0 +1,472 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Plus, Trash2, Settings, Package } from 'lucide-react'; +import type { ItemMasterField } from '@/contexts/ItemMasterContext'; + +// 타입 정의 +export interface UnitOption { + id: string; + value: string; + label: string; + inputType?: string; + required?: boolean; + placeholder?: string; + defaultValue?: string; + options?: string[]; + columnValues?: Record; +} + +export interface AttributeSubTab { + id: string; + key: string; + label: string; + order: number; +} + +export interface AttributeColumn { + id: string; + name: string; + key: string; + type: string; + required: boolean; +} + +// 입력 타입 라벨 변환 헬퍼 함수 +const getInputTypeLabel = (inputType: string | undefined): string => { + const labels: Record = { + textbox: '텍스트박스', + number: '숫자', + dropdown: '드롭다운', + checkbox: '체크박스', + date: '날짜', + textarea: '텍스트영역', + }; + return labels[inputType || ''] || '텍스트박스'; +}; + +interface AttributeTabContentProps { + activeAttributeTab: string; + setActiveAttributeTab: (tab: string) => void; + attributeSubTabs: AttributeSubTab[]; + unitOptions: UnitOption[]; + materialOptions: UnitOption[]; + surfaceTreatmentOptions: UnitOption[]; + customAttributeOptions: Record; + attributeColumns: Record; + itemMasterFields: ItemMasterField[]; + // 다이얼로그 핸들러 + setIsManageAttributeTabsDialogOpen: (open: boolean) => void; + setIsOptionDialogOpen: (open: boolean) => void; + setEditingOptionType: (type: string) => void; + setNewOptionValue: (value: string) => void; + setNewOptionLabel: (value: string) => void; + setNewOptionColumnValues: (values: Record) => void; + setIsColumnManageDialogOpen: (open: boolean) => void; + setManagingColumnType: (type: string) => void; + setNewColumnName: (name: string) => void; + setNewColumnKey: (key: string) => void; + setNewColumnType: (type: string) => void; + setNewColumnRequired: (required: boolean) => void; + handleDeleteOption: (type: string, id: string) => void; +} + +export function AttributeTabContent({ + activeAttributeTab, + setActiveAttributeTab, + attributeSubTabs, + unitOptions, + materialOptions, + surfaceTreatmentOptions, + customAttributeOptions, + attributeColumns, + itemMasterFields, + setIsManageAttributeTabsDialogOpen, + setIsOptionDialogOpen, + setEditingOptionType, + setNewOptionValue, + setNewOptionLabel, + setNewOptionColumnValues, + setIsColumnManageDialogOpen, + setManagingColumnType, + setNewColumnName, + setNewColumnKey, + setNewColumnType, + setNewColumnRequired, + handleDeleteOption, +}: AttributeTabContentProps) { + // 옵션 목록 렌더링 헬퍼 + const renderOptionList = ( + options: UnitOption[], + optionType: string, + title: string + ) => { + const columns = attributeColumns[optionType] || []; + + return ( +
+
+

{title}

+
+ + +
+
+
+ {options.map((option) => { + const hasColumns = columns.length > 0 && option.columnValues; + + return ( +
+
+
+
+ {option.label} + {option.inputType && ( + {getInputTypeLabel(option.inputType)} + )} + {option.required && ( + 필수 + )} +
+ +
+
+ 값(Value): + {option.value} +
+ {option.placeholder && ( +
+ 플레이스홀더: + {option.placeholder} +
+ )} + {option.defaultValue && ( +
+ 기본값: + {option.defaultValue} +
+ )} + {option.inputType === 'dropdown' && option.options && ( +
+ 옵션: +
+ {option.options.map((opt, idx) => ( + {opt} + ))} +
+
+ )} +
+ + {hasColumns && ( +
+

추가 칼럼

+
+ {columns.map((column) => ( +
+ {column.name}: + {option.columnValues?.[column.key] || '-'} +
+ ))} +
+
+ )} +
+ +
+
+ ); + })} +
+
+ ); + }; + + // 마스터 필드 속성 렌더링 + const renderMasterFieldProperties = (masterField: ItemMasterField) => { + const propertiesArray = masterField?.properties + ? Object.entries(masterField.properties).map(([key, value]) => ({ key, ...value as object })) + : []; + + if (propertiesArray.length === 0) return null; + + return ( +
+
+
+

{masterField.field_name} 속성 목록

+

+ 항목 탭에서 추가한 "{masterField.field_name}" 항목의 속성값들입니다 +

+
+
+ +
+ {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} + {propertiesArray.map((property: any) => { + const inputTypeLabel = getInputTypeLabel(property.type); + + return ( +
+
+
+
+ {property.label} + {inputTypeLabel} + {property.required && ( + 필수 + )} +
+ +
+
+ 키(Key): + {property.key} +
+ {property.placeholder && ( +
+ 플레이스홀더: + {property.placeholder} +
+ )} + {property.defaultValue && ( +
+ 기본값: + {property.defaultValue} +
+ )} + {property.type === 'dropdown' && property.options && ( +
+ 옵션: +
+ {property.options.map((opt: string, idx: number) => ( + {opt} + ))} +
+
+ )} +
+
+
+
+ ); + })} +
+ +
+
+ +
+

+ 마스터 항목 속성 관리 +

+

+ 이 속성들은 항목 탭에서 "{masterField.field_name}" 항목을 편집하여 추가/수정/삭제할 수 있습니다. +

+
+
+
+
+ ); + }; + + // 사용자 정의 속성 렌더링 + const renderCustomAttributeTab = () => { + const currentTabKey = activeAttributeTab; + const currentOptions = customAttributeOptions[currentTabKey] || []; + const columns = attributeColumns[currentTabKey] || []; + + return ( +
+
+

+ {attributeSubTabs.find(t => t.key === activeAttributeTab)?.label || '사용자 정의'} 목록 +

+
+ + +
+
+ + {currentOptions.length > 0 ? ( +
+ {currentOptions.map((option) => { + const hasColumns = columns.length > 0 && option.columnValues; + const inputTypeLabel = getInputTypeLabel(option.inputType); + + return ( +
+
+
+
+ {option.label} + {inputTypeLabel} + {option.required && ( + 필수 + )} +
+ +
+
+ 값(Value): + {option.value} +
+ {option.placeholder && ( +
+ 플레이스홀더: + {option.placeholder} +
+ )} + {option.defaultValue && ( +
+ 기본값: + {option.defaultValue} +
+ )} + {option.inputType === 'dropdown' && option.options && ( +
+ 옵션: +
+ {option.options.map((opt, idx) => ( + {opt} + ))} +
+
+ )} +
+ + {hasColumns && ( +
+

추가 칼럼

+
+ {columns.map((column) => ( +
+ {column.name}: + {option.columnValues?.[column.key] || '-'} +
+ ))} +
+
+ )} +
+ +
+
+ ); + })} +
+ ) : ( +
+ +

아직 추가된 항목이 없습니다

+

위 "추가" 버튼을 클릭하여 새로운 속성을 추가할 수 있습니다

+
+ )} +
+ ); + }; + + return ( + + + 속성 관리 + 단위, 재질, 표면처리 등의 속성을 관리합니다 + + + {/* 속성 하위 탭 (칩 형태) */} +
+
+ {attributeSubTabs.sort((a, b) => a.order - b.order).map(tab => ( + + ))} +
+ +
+ + {/* 단위 관리 */} + {activeAttributeTab === 'units' && renderOptionList(unitOptions, 'units', '단위 목록')} + + {/* 재질 관리 */} + {activeAttributeTab === 'materials' && renderOptionList(materialOptions, 'materials', '재질 목록')} + + {/* 표면처리 관리 */} + {activeAttributeTab === 'surface' && renderOptionList(surfaceTreatmentOptions, 'surface', '표면처리 목록')} + + {/* 사용자 정의 속성 탭 및 마스터 항목 탭 */} + {!['units', 'materials', 'surface'].includes(activeAttributeTab) && (() => { + const currentTabKey = activeAttributeTab; + + // 마스터 항목인지 확인 + const masterField = itemMasterFields.find(f => f.id.toString() === currentTabKey); + + if (masterField) { + const propertiesArray = masterField?.properties + ? Object.entries(masterField.properties).map(([key, value]) => ({ key, ...value as object })) + : []; + + if (propertiesArray.length > 0) { + return renderMasterFieldProperties(masterField); + } + } + + // 사용자 정의 속성 탭 + return renderCustomAttributeTab(); + })()} +
+
+ ); +} diff --git a/src/components/items/ItemMasterDataManagement/components/ItemMasterDialogs.tsx b/src/components/items/ItemMasterDataManagement/components/ItemMasterDialogs.tsx new file mode 100644 index 00000000..52c88b45 --- /dev/null +++ b/src/components/items/ItemMasterDataManagement/components/ItemMasterDialogs.tsx @@ -0,0 +1,943 @@ +'use client'; + +import type { ItemPage, SectionTemplate, ItemMasterField, ItemSection, BOMItem } from '@/contexts/ItemMasterContext'; +import { FieldDialog } from '../dialogs/FieldDialog'; +import { FieldDrawer } from '../dialogs/FieldDrawer'; +import { TabManagementDialogs } from '../dialogs/TabManagementDialogs'; +import { OptionDialog } from '../dialogs/OptionDialog'; +import { ColumnManageDialog } from '../dialogs/ColumnManageDialog'; +import { PathEditDialog } from '../dialogs/PathEditDialog'; +import { PageDialog } from '../dialogs/PageDialog'; +import { SectionDialog } from '../dialogs/SectionDialog'; +import { MasterFieldDialog } from '../dialogs/MasterFieldDialog'; +import { TemplateFieldDialog } from '../dialogs/TemplateFieldDialog'; +import { LoadTemplateDialog } from '../dialogs/LoadTemplateDialog'; +import { ColumnDialog } from '../dialogs/ColumnDialog'; +import { SectionTemplateDialog } from '../dialogs/SectionTemplateDialog'; +import { ImportSectionDialog } from '../dialogs/ImportSectionDialog'; +import { ImportFieldDialog } from '../dialogs/ImportFieldDialog'; +import type { CustomTab, AttributeSubTab } from '../hooks/useTabManagement'; +import type { UnitOption } from '../hooks/useAttributeManagement'; + +interface TextboxColumn { + id: string; + name: string; + key: string; +} + +interface ConditionField { + fieldId: string; + fieldName: string; + operator: string; + value: string; + logicOperator?: 'AND' | 'OR'; +} + +interface ConditionSection { + sectionId: string; + sectionTitle: string; + operator: string; + value: string; + logicOperator?: 'AND' | 'OR'; +} + +interface AttributeColumn { + id: string; + name: string; + key: string; + type: string; + required: boolean; +} + +export interface ItemMasterDialogsProps { + isMobile: boolean; + selectedPage: ItemPage | null; + + // Tab Management + isManageTabsDialogOpen: boolean; + setIsManageTabsDialogOpen: (open: boolean) => void; + customTabs: CustomTab[]; + moveTabUp: (tabId: string) => void; + moveTabDown: (tabId: string) => void; + handleEditTabFromManage: (tabId: string) => void; + handleDeleteTab: (tabId: string) => void; + getTabIcon: (iconName: string) => React.ComponentType<{ className?: string }>; + setIsAddTabDialogOpen: (open: boolean) => void; + isDeleteTabDialogOpen: boolean; + setIsDeleteTabDialogOpen: (open: boolean) => void; + deletingTabId: string | null; + setDeletingTabId: (id: string | null) => void; + confirmDeleteTab: () => void; + isAddTabDialogOpen: boolean; + editingTabId: string | null; + setEditingTabId: (id: string | null) => void; + newTabLabel: string; + setNewTabLabel: (label: string) => void; + handleUpdateTab: () => void; + handleAddTab: () => void; + isManageAttributeTabsDialogOpen: boolean; + setIsManageAttributeTabsDialogOpen: (open: boolean) => void; + attributeSubTabs: AttributeSubTab[]; + moveAttributeTabUp: (tabId: string) => void; + moveAttributeTabDown: (tabId: string) => void; + handleDeleteAttributeTab: (tabId: string) => void; + isDeleteAttributeTabDialogOpen: boolean; + setIsDeleteAttributeTabDialogOpen: (open: boolean) => void; + deletingAttributeTabId: string | null; + setDeletingAttributeTabId: (id: string | null) => void; + confirmDeleteAttributeTab: () => void; + isAddAttributeTabDialogOpen: boolean; + setIsAddAttributeTabDialogOpen: (open: boolean) => void; + editingAttributeTabId: string | null; + setEditingAttributeTabId: (id: string | null) => void; + newAttributeTabLabel: string; + setNewAttributeTabLabel: (label: string) => void; + handleUpdateAttributeTab: () => void; + handleAddAttributeTab: () => void; + + // Option Dialog + isOptionDialogOpen: boolean; + setIsOptionDialogOpen: (open: boolean) => void; + newOptionValue: string; + setNewOptionValue: (value: string) => void; + newOptionLabel: string; + setNewOptionLabel: (value: string) => void; + newOptionColumnValues: Record; + setNewOptionColumnValues: (values: Record) => void; + newOptionInputType: string; + setNewOptionInputType: (type: string) => void; + newOptionRequired: boolean; + setNewOptionRequired: (required: boolean) => void; + newOptionOptions: string[]; + setNewOptionOptions: (options: string[]) => void; + newOptionPlaceholder: string; + setNewOptionPlaceholder: (placeholder: string) => void; + newOptionDefaultValue: string; + setNewOptionDefaultValue: (value: string) => void; + editingOptionType: string; + attributeColumns: Record; + handleAddOption: () => void; + + // Column Manage Dialog + isColumnManageDialogOpen: boolean; + setIsColumnManageDialogOpen: (open: boolean) => void; + managingColumnType: string; + setAttributeColumns: React.Dispatch>>; + newColumnName: string; + setNewColumnName: (name: string) => void; + newColumnKey: string; + setNewColumnKey: (key: string) => void; + newColumnType: string; + setNewColumnType: (type: string) => void; + newColumnRequired: boolean; + setNewColumnRequired: (required: boolean) => void; + + // Path Edit Dialog + editingPathPageId: number | null; + setEditingPathPageId: (id: number | null) => void; + editingAbsolutePath: string; + setEditingAbsolutePath: (path: string) => void; + updateItemPage: (id: number, updates: Partial) => void; + + // Page Dialog + isPageDialogOpen: boolean; + setIsPageDialogOpen: (open: boolean) => void; + newPageName: string; + setNewPageName: (name: string) => void; + newPageItemType: 'FG' | 'PT' | 'SM' | 'RM' | 'CS'; + setNewPageItemType: React.Dispatch>; + handleAddPage: () => void; + + // Section Dialog + isSectionDialogOpen: boolean; + setIsSectionDialogOpen: (open: boolean) => void; + newSectionType: 'fields' | 'bom'; + setNewSectionType: (type: 'fields' | 'bom') => void; + newSectionTitle: string; + setNewSectionTitle: (title: string) => void; + newSectionDescription: string; + setNewSectionDescription: (description: string) => void; + handleAddSection: () => void; + sectionInputMode: 'new' | 'existing'; + setSectionInputMode: (mode: 'new' | 'existing') => void; + sectionsAsTemplates: SectionTemplate[]; + selectedSectionTemplateId: number | null; + setSelectedSectionTemplateId: (id: number | null) => void; + handleLinkTemplate: () => void; + + // Field Dialog + isFieldDialogOpen: boolean; + setIsFieldDialogOpen: (open: boolean) => void; + selectedSectionForField: number | null; + editingFieldId: number | null; + setEditingFieldId: (id: number | null) => void; + fieldInputMode: 'new' | 'existing'; + setFieldInputMode: (mode: 'new' | 'existing') => void; + showMasterFieldList: boolean; + setShowMasterFieldList: (show: boolean) => void; + selectedMasterFieldId: number | null; + setSelectedMasterFieldId: (id: number | null) => void; + textboxColumns: TextboxColumn[]; + setTextboxColumns: React.Dispatch>; + newFieldConditionEnabled: boolean; + setNewFieldConditionEnabled: (enabled: boolean) => void; + newFieldConditionTargetType: 'field' | 'section'; + setNewFieldConditionTargetType: (type: 'field' | 'section') => void; + newFieldConditionFields: ConditionField[]; + setNewFieldConditionFields: React.Dispatch>; + newFieldConditionSections: ConditionSection[]; + setNewFieldConditionSections: React.Dispatch>; + tempConditionValue: string; + setTempConditionValue: (value: string) => void; + newFieldName: string; + setNewFieldName: (name: string) => void; + newFieldKey: string; + setNewFieldKey: (key: string) => void; + newFieldInputType: string; + setNewFieldInputType: (type: string) => void; + newFieldRequired: boolean; + setNewFieldRequired: (required: boolean) => void; + newFieldDescription: string; + setNewFieldDescription: (description: string) => void; + newFieldOptions: string[]; + setNewFieldOptions: React.Dispatch>; + itemMasterFields: ItemMasterField[]; + handleAddField: () => void; + isColumnDialogOpen: boolean; + setIsColumnDialogOpen: (open: boolean) => void; + editingColumnId: string | null; + setEditingColumnId: (id: string | null) => void; + columnName: string; + setColumnName: (name: string) => void; + columnKey: string; + setColumnKey: (key: string) => void; + + // Master Field Dialog + isMasterFieldDialogOpen: boolean; + setIsMasterFieldDialogOpen: (open: boolean) => void; + editingMasterFieldId: number | null; + setEditingMasterFieldId: (id: number | null) => void; + newMasterFieldName: string; + setNewMasterFieldName: (name: string) => void; + newMasterFieldKey: string; + setNewMasterFieldKey: (key: string) => void; + newMasterFieldInputType: string; + setNewMasterFieldInputType: (type: string) => void; + newMasterFieldRequired: boolean; + setNewMasterFieldRequired: (required: boolean) => void; + newMasterFieldCategory: string; + setNewMasterFieldCategory: (category: string) => void; + newMasterFieldDescription: string; + setNewMasterFieldDescription: (description: string) => void; + newMasterFieldOptions: string[]; + setNewMasterFieldOptions: React.Dispatch>; + newMasterFieldAttributeType: string; + setNewMasterFieldAttributeType: (type: string) => void; + newMasterFieldMultiColumn: boolean; + setNewMasterFieldMultiColumn: (multiColumn: boolean) => void; + newMasterFieldColumnCount: number; + setNewMasterFieldColumnCount: (count: number) => void; + newMasterFieldColumnNames: string[]; + setNewMasterFieldColumnNames: React.Dispatch>; + handleUpdateMasterField: () => void; + handleAddMasterField: () => void; + + // Section Template Dialog + isSectionTemplateDialogOpen: boolean; + setIsSectionTemplateDialogOpen: (open: boolean) => void; + editingSectionTemplateId: number | null; + setEditingSectionTemplateId: (id: number | null) => void; + newSectionTemplateTitle: string; + setNewSectionTemplateTitle: (title: string) => void; + newSectionTemplateDescription: string; + setNewSectionTemplateDescription: (description: string) => void; + newSectionTemplateCategory: string; + setNewSectionTemplateCategory: (category: string) => void; + newSectionTemplateType: 'fields' | 'bom'; + setNewSectionTemplateType: (type: 'fields' | 'bom') => void; + handleUpdateSectionTemplate: () => void; + handleAddSectionTemplate: () => void; + + // Template Field Dialog + isTemplateFieldDialogOpen: boolean; + setIsTemplateFieldDialogOpen: (open: boolean) => void; + editingTemplateFieldId: string | null; + setEditingTemplateFieldId: (id: string | null) => void; + templateFieldName: string; + setTemplateFieldName: (name: string) => void; + templateFieldKey: string; + setTemplateFieldKey: (key: string) => void; + templateFieldInputType: string; + setTemplateFieldInputType: (type: string) => void; + templateFieldRequired: boolean; + setTemplateFieldRequired: (required: boolean) => void; + templateFieldOptions: string[]; + setTemplateFieldOptions: React.Dispatch>; + templateFieldDescription: string; + setTemplateFieldDescription: (description: string) => void; + templateFieldMultiColumn: boolean; + setTemplateFieldMultiColumn: (multiColumn: boolean) => void; + templateFieldColumnCount: number; + setTemplateFieldColumnCount: (count: number) => void; + templateFieldColumnNames: string[]; + setTemplateFieldColumnNames: React.Dispatch>; + handleAddTemplateField: () => void; + templateFieldInputMode: 'new' | 'existing'; + setTemplateFieldInputMode: (mode: 'new' | 'existing') => void; + templateFieldShowMasterFieldList: boolean; + setTemplateFieldShowMasterFieldList: (show: boolean) => void; + templateFieldSelectedMasterFieldId: number | null; + setTemplateFieldSelectedMasterFieldId: (id: number | null) => void; + + // Load Template Dialog + isLoadTemplateDialogOpen: boolean; + setIsLoadTemplateDialogOpen: (open: boolean) => void; + sectionTemplates: SectionTemplate[]; + selectedTemplateId: number | null; + setSelectedTemplateId: (id: number | null) => void; + handleLoadTemplate: () => void; + + // Import Section Dialog + isImportSectionDialogOpen: boolean; + setIsImportSectionDialogOpen: (open: boolean) => void; + independentSections: ItemSection[]; + selectedImportSectionId: number | null; + setSelectedImportSectionId: (id: number | null) => void; + handleImportSection: () => Promise; + refreshIndependentSections: () => void; + getSectionUsage: (sectionId: number) => Promise<{ pages: { id: number; name: string }[] }>; + + // Import Field Dialog + isImportFieldDialogOpen: boolean; + setIsImportFieldDialogOpen: (open: boolean) => void; + selectedImportFieldId: number | null; + setSelectedImportFieldId: (id: number | null) => void; + handleImportField: () => Promise; + refreshIndependentFields: () => void; + getFieldUsage: (fieldId: number) => Promise<{ sections: { id: number; title: string }[] }>; + importFieldTargetSectionId: number | null; +} + +export function ItemMasterDialogs({ + isMobile, + selectedPage, + + // Tab Management + isManageTabsDialogOpen, + setIsManageTabsDialogOpen, + customTabs, + moveTabUp, + moveTabDown, + handleEditTabFromManage, + handleDeleteTab, + getTabIcon, + setIsAddTabDialogOpen, + isDeleteTabDialogOpen, + setIsDeleteTabDialogOpen, + deletingTabId, + setDeletingTabId, + confirmDeleteTab, + isAddTabDialogOpen, + editingTabId, + setEditingTabId, + newTabLabel, + setNewTabLabel, + handleUpdateTab, + handleAddTab, + isManageAttributeTabsDialogOpen, + setIsManageAttributeTabsDialogOpen, + attributeSubTabs, + moveAttributeTabUp, + moveAttributeTabDown, + handleDeleteAttributeTab, + isDeleteAttributeTabDialogOpen, + setIsDeleteAttributeTabDialogOpen, + deletingAttributeTabId, + setDeletingAttributeTabId, + confirmDeleteAttributeTab, + isAddAttributeTabDialogOpen, + setIsAddAttributeTabDialogOpen, + editingAttributeTabId, + setEditingAttributeTabId, + newAttributeTabLabel, + setNewAttributeTabLabel, + handleUpdateAttributeTab, + handleAddAttributeTab, + + // Option Dialog + isOptionDialogOpen, + setIsOptionDialogOpen, + newOptionValue, + setNewOptionValue, + newOptionLabel, + setNewOptionLabel, + newOptionColumnValues, + setNewOptionColumnValues, + newOptionInputType, + setNewOptionInputType, + newOptionRequired, + setNewOptionRequired, + newOptionOptions, + setNewOptionOptions, + newOptionPlaceholder, + setNewOptionPlaceholder, + newOptionDefaultValue, + setNewOptionDefaultValue, + editingOptionType, + attributeColumns, + handleAddOption, + + // Column Manage Dialog + isColumnManageDialogOpen, + setIsColumnManageDialogOpen, + managingColumnType, + setAttributeColumns, + newColumnName, + setNewColumnName, + newColumnKey, + setNewColumnKey, + newColumnType, + setNewColumnType, + newColumnRequired, + setNewColumnRequired, + + // Path Edit Dialog + editingPathPageId, + setEditingPathPageId, + editingAbsolutePath, + setEditingAbsolutePath, + updateItemPage, + + // Page Dialog + isPageDialogOpen, + setIsPageDialogOpen, + newPageName, + setNewPageName, + newPageItemType, + setNewPageItemType, + handleAddPage, + + // Section Dialog + isSectionDialogOpen, + setIsSectionDialogOpen, + newSectionType, + setNewSectionType, + newSectionTitle, + setNewSectionTitle, + newSectionDescription, + setNewSectionDescription, + handleAddSection, + sectionInputMode, + setSectionInputMode, + sectionsAsTemplates, + selectedSectionTemplateId, + setSelectedSectionTemplateId, + handleLinkTemplate, + + // Field Dialog + isFieldDialogOpen, + setIsFieldDialogOpen, + selectedSectionForField, + editingFieldId, + setEditingFieldId, + fieldInputMode, + setFieldInputMode, + showMasterFieldList, + setShowMasterFieldList, + selectedMasterFieldId, + setSelectedMasterFieldId, + textboxColumns, + setTextboxColumns, + newFieldConditionEnabled, + setNewFieldConditionEnabled, + newFieldConditionTargetType, + setNewFieldConditionTargetType, + newFieldConditionFields, + setNewFieldConditionFields, + newFieldConditionSections, + setNewFieldConditionSections, + tempConditionValue, + setTempConditionValue, + newFieldName, + setNewFieldName, + newFieldKey, + setNewFieldKey, + newFieldInputType, + setNewFieldInputType, + newFieldRequired, + setNewFieldRequired, + newFieldDescription, + setNewFieldDescription, + newFieldOptions, + setNewFieldOptions, + itemMasterFields, + handleAddField, + isColumnDialogOpen, + setIsColumnDialogOpen, + editingColumnId, + setEditingColumnId, + columnName, + setColumnName, + columnKey, + setColumnKey, + + // Master Field Dialog + isMasterFieldDialogOpen, + setIsMasterFieldDialogOpen, + editingMasterFieldId, + setEditingMasterFieldId, + newMasterFieldName, + setNewMasterFieldName, + newMasterFieldKey, + setNewMasterFieldKey, + newMasterFieldInputType, + setNewMasterFieldInputType, + newMasterFieldRequired, + setNewMasterFieldRequired, + newMasterFieldCategory, + setNewMasterFieldCategory, + newMasterFieldDescription, + setNewMasterFieldDescription, + newMasterFieldOptions, + setNewMasterFieldOptions, + newMasterFieldAttributeType, + setNewMasterFieldAttributeType, + newMasterFieldMultiColumn, + setNewMasterFieldMultiColumn, + newMasterFieldColumnCount, + setNewMasterFieldColumnCount, + newMasterFieldColumnNames, + setNewMasterFieldColumnNames, + handleUpdateMasterField, + handleAddMasterField, + + // Section Template Dialog + isSectionTemplateDialogOpen, + setIsSectionTemplateDialogOpen, + editingSectionTemplateId, + setEditingSectionTemplateId, + newSectionTemplateTitle, + setNewSectionTemplateTitle, + newSectionTemplateDescription, + setNewSectionTemplateDescription, + newSectionTemplateCategory, + setNewSectionTemplateCategory, + newSectionTemplateType, + setNewSectionTemplateType, + handleUpdateSectionTemplate, + handleAddSectionTemplate, + + // Template Field Dialog + isTemplateFieldDialogOpen, + setIsTemplateFieldDialogOpen, + editingTemplateFieldId, + setEditingTemplateFieldId, + templateFieldName, + setTemplateFieldName, + templateFieldKey, + setTemplateFieldKey, + templateFieldInputType, + setTemplateFieldInputType, + templateFieldRequired, + setTemplateFieldRequired, + templateFieldOptions, + setTemplateFieldOptions, + templateFieldDescription, + setTemplateFieldDescription, + templateFieldMultiColumn, + setTemplateFieldMultiColumn, + templateFieldColumnCount, + setTemplateFieldColumnCount, + templateFieldColumnNames, + setTemplateFieldColumnNames, + handleAddTemplateField, + templateFieldInputMode, + setTemplateFieldInputMode, + templateFieldShowMasterFieldList, + setTemplateFieldShowMasterFieldList, + templateFieldSelectedMasterFieldId, + setTemplateFieldSelectedMasterFieldId, + + // Load Template Dialog + isLoadTemplateDialogOpen, + setIsLoadTemplateDialogOpen, + sectionTemplates, + selectedTemplateId, + setSelectedTemplateId, + handleLoadTemplate, + + // Import Section Dialog + isImportSectionDialogOpen, + setIsImportSectionDialogOpen, + independentSections, + selectedImportSectionId, + setSelectedImportSectionId, + handleImportSection, + refreshIndependentSections, + getSectionUsage, + + // Import Field Dialog + isImportFieldDialogOpen, + setIsImportFieldDialogOpen, + selectedImportFieldId, + setSelectedImportFieldId, + handleImportField, + refreshIndependentFields, + getFieldUsage, + importFieldTargetSectionId, +}: ItemMasterDialogsProps) { + return ( + <> + + + + + + + {}} + /> + + + + + + {/* 항목 추가/수정 다이얼로그 - 데스크톱 */} + {!isMobile && ( + s.id === selectedSectionForField) || null} + selectedPage={selectedPage || null} + itemMasterFields={itemMasterFields} + handleAddField={handleAddField} + setIsColumnDialogOpen={setIsColumnDialogOpen} + setEditingColumnId={setEditingColumnId} + setColumnName={setColumnName} + setColumnKey={setColumnKey} + /> + )} + + {/* 항목 추가/수정 다이얼로그 - 모바일 (바텀시트) */} + {isMobile && ( + s.id === selectedSectionForField) || null} + selectedPage={selectedPage || null} + itemMasterFields={itemMasterFields} + handleAddField={handleAddField} + setIsColumnDialogOpen={setIsColumnDialogOpen} + setEditingColumnId={setEditingColumnId} + setColumnName={setColumnName} + setColumnKey={setColumnKey} + /> + )} + + {/* 텍스트박스 컬럼 추가/수정 다이얼로그 */} + + + + + + + + + + + {/* 섹션 불러오기 다이얼로그 */} + + + {/* 필드 불러오기 다이얼로그 */} + s.id === importFieldTargetSectionId)?.title + : undefined + } + /> + + ); +} diff --git a/src/components/items/ItemMasterDataManagement/components/index.ts b/src/components/items/ItemMasterDataManagement/components/index.ts index 8e7892bd..981acb35 100644 --- a/src/components/items/ItemMasterDataManagement/components/index.ts +++ b/src/components/items/ItemMasterDataManagement/components/index.ts @@ -1,2 +1,7 @@ export { DraggableSection } from './DraggableSection'; -export { DraggableField } from './DraggableField'; \ No newline at end of file +export { DraggableField } from './DraggableField'; + +// 2025-12-24: Phase 2 UI 컴포넌트 분리 +export { AttributeTabContent } from './AttributeTabContent'; +export { ItemMasterDialogs } from './ItemMasterDialogs'; +export type { ItemMasterDialogsProps } from './ItemMasterDialogs'; \ No newline at end of file diff --git a/src/components/items/ItemMasterDataManagement/hooks/index.ts b/src/components/items/ItemMasterDataManagement/hooks/index.ts index c5997da7..4ab043fa 100644 --- a/src/components/items/ItemMasterDataManagement/hooks/index.ts +++ b/src/components/items/ItemMasterDataManagement/hooks/index.ts @@ -17,4 +17,17 @@ export { useAttributeManagement } from './useAttributeManagement'; export type { UseAttributeManagementReturn } from './useAttributeManagement'; export { useTabManagement } from './useTabManagement'; -export type { UseTabManagementReturn, CustomTab, AttributeSubTab } from './useTabManagement'; \ No newline at end of file +export type { UseTabManagementReturn, CustomTab, AttributeSubTab } from './useTabManagement'; + +// 2025-12-24: 신규 훅 추가 +export { useInitialDataLoading } from './useInitialDataLoading'; +export type { UseInitialDataLoadingReturn } from './useInitialDataLoading'; + +export { useImportManagement } from './useImportManagement'; +export type { UseImportManagementReturn } from './useImportManagement'; + +export { useReorderManagement } from './useReorderManagement'; +export type { UseReorderManagementReturn } from './useReorderManagement'; + +export { useDeleteManagement } from './useDeleteManagement'; +export type { UseDeleteManagementReturn } from './useDeleteManagement'; \ No newline at end of file diff --git a/src/components/items/ItemMasterDataManagement/hooks/useDeleteManagement.ts b/src/components/items/ItemMasterDataManagement/hooks/useDeleteManagement.ts new file mode 100644 index 00000000..24546b95 --- /dev/null +++ b/src/components/items/ItemMasterDataManagement/hooks/useDeleteManagement.ts @@ -0,0 +1,119 @@ +'use client'; + +import { useCallback } from 'react'; +import { useItemMaster } from '@/contexts/ItemMasterContext'; +import { toast } from 'sonner'; +import type { ItemPage, BOMItem } from '@/contexts/ItemMasterContext'; +import type { CustomTab, AttributeSubTab } from './useTabManagement'; +import type { UnitOption, MaterialOption, SurfaceTreatmentOption } from './useAttributeManagement'; + +export interface UseDeleteManagementReturn { + handleDeletePage: (pageId: number) => void; + handleDeleteSection: (pageId: number, sectionId: number) => void; + handleUnlinkField: (pageId: string, sectionId: string, fieldId: string) => Promise; + handleResetAllData: ( + setUnitOptions: React.Dispatch>, + setMaterialOptions: React.Dispatch>, + setSurfaceTreatmentOptions: React.Dispatch>, + setCustomAttributeOptions: React.Dispatch>>, + setAttributeColumns: React.Dispatch>>, + setBomItems: React.Dispatch>, + setCustomTabs: React.Dispatch>, + setAttributeSubTabs: React.Dispatch>, + ) => void; +} + +interface UseDeleteManagementProps { + itemPages: ItemPage[]; +} + +export function useDeleteManagement({ itemPages }: UseDeleteManagementProps): UseDeleteManagementReturn { + const { + deleteItemPage, + deleteSection, + unlinkFieldFromSection, + resetAllData, + } = useItemMaster(); + + // 페이지 삭제 핸들러 + const handleDeletePage = useCallback((pageId: number) => { + const pageToDelete = itemPages.find(p => p.id === pageId); + const sectionIds = pageToDelete?.sections.map(s => s.id) || []; + const fieldIds = pageToDelete?.sections.flatMap(s => s.fields?.map(f => f.id) || []) || []; + deleteItemPage(pageId); + console.log('페이지 삭제 완료:', { pageId, removedSections: sectionIds.length, removedFields: fieldIds.length }); + }, [itemPages, deleteItemPage]); + + // 섹션 삭제 핸들러 + const handleDeleteSection = useCallback((pageId: number, sectionId: number) => { + const page = itemPages.find(p => p.id === pageId); + const sectionToDelete = page?.sections.find(s => s.id === sectionId); + const fieldIds = sectionToDelete?.fields?.map(f => f.id) || []; + deleteSection(Number(sectionId)); + console.log('섹션 삭제 완료:', { sectionId, removedFields: fieldIds.length }); + }, [itemPages, deleteSection]); + + // 필드 연결 해제 핸들러 + const handleUnlinkField = useCallback(async (_pageId: string, sectionId: string, fieldId: string) => { + try { + await unlinkFieldFromSection(Number(sectionId), Number(fieldId)); + console.log('필드 연결 해제 완료:', fieldId); + } catch (error) { + console.error('필드 연결 해제 실패:', error); + toast.error('필드 연결 해제에 실패했습니다'); + } + }, [unlinkFieldFromSection]); + + // 전체 데이터 초기화 핸들러 + const handleResetAllData = useCallback(( + setUnitOptions: React.Dispatch>, + setMaterialOptions: React.Dispatch>, + setSurfaceTreatmentOptions: React.Dispatch>, + setCustomAttributeOptions: React.Dispatch>>, + setAttributeColumns: React.Dispatch>>, + setBomItems: React.Dispatch>, + setCustomTabs: React.Dispatch>, + setAttributeSubTabs: React.Dispatch>, + ) => { + if (!confirm('⚠️ 경고: 모든 품목기준관리 데이터(계층구조, 섹션, 항목, 속성)를 초기화하시겠습니까?\n\n이 작업은 되돌릴 수 없습니다!')) { + return; + } + + try { + resetAllData(); + + setUnitOptions([]); + setMaterialOptions([]); + setSurfaceTreatmentOptions([]); + setCustomAttributeOptions({}); + setAttributeColumns({}); + setBomItems([]); + + setCustomTabs([ + { id: 'hierarchy', label: '계층구조', icon: 'FolderTree', isDefault: true, order: 1 }, + { id: 'sections', label: '섹션', icon: 'Layers', isDefault: true, order: 2 }, + { id: 'items', label: '항목', icon: 'ListTree', isDefault: true, order: 3 }, + { id: 'attributes', label: '속성', icon: 'Settings', isDefault: true, order: 4 } + ]); + + setAttributeSubTabs([]); + + console.log('🗑️ 모든 품목기준관리 데이터가 초기화되었습니다'); + toast.success('✅ 모든 데이터가 초기화되었습니다!\n계층구조, 섹션, 항목, 속성이 모두 삭제되었습니다.'); + + setTimeout(() => { + window.location.reload(); + }, 1500); + } catch (error) { + toast.error('초기화 중 오류가 발생했습니다'); + console.error('Reset error:', error); + } + }, [resetAllData]); + + return { + handleDeletePage, + handleDeleteSection, + handleUnlinkField, + handleResetAllData, + }; +} diff --git a/src/components/items/ItemMasterDataManagement/hooks/useImportManagement.ts b/src/components/items/ItemMasterDataManagement/hooks/useImportManagement.ts new file mode 100644 index 00000000..71db9bf3 --- /dev/null +++ b/src/components/items/ItemMasterDataManagement/hooks/useImportManagement.ts @@ -0,0 +1,112 @@ +'use client'; + +import { useState, useCallback } from 'react'; +import { useItemMaster } from '@/contexts/ItemMasterContext'; +import { getErrorMessage } from '@/lib/api/error-handler'; +import { toast } from 'sonner'; +import type { ItemPage } from '@/contexts/ItemMasterContext'; + +export interface UseImportManagementReturn { + // 섹션 Import 상태 + isImportSectionDialogOpen: boolean; + setIsImportSectionDialogOpen: React.Dispatch>; + selectedImportSectionId: number | null; + setSelectedImportSectionId: React.Dispatch>; + + // 필드 Import 상태 + isImportFieldDialogOpen: boolean; + setIsImportFieldDialogOpen: React.Dispatch>; + selectedImportFieldId: number | null; + setSelectedImportFieldId: React.Dispatch>; + importFieldTargetSectionId: number | null; + setImportFieldTargetSectionId: React.Dispatch>; + + // 핸들러 + handleImportSection: (selectedPageId: number | null) => Promise; + handleImportField: (selectedPage: ItemPage | null) => Promise; + handleCloneSection: (sectionId: number) => Promise; +} + +export function useImportManagement(): UseImportManagementReturn { + const { + linkSectionToPage, + linkFieldToSection, + cloneSection, + } = useItemMaster(); + + // 섹션 Import 상태 + const [isImportSectionDialogOpen, setIsImportSectionDialogOpen] = useState(false); + const [selectedImportSectionId, setSelectedImportSectionId] = useState(null); + + // 필드 Import 상태 + const [isImportFieldDialogOpen, setIsImportFieldDialogOpen] = useState(false); + const [selectedImportFieldId, setSelectedImportFieldId] = useState(null); + const [importFieldTargetSectionId, setImportFieldTargetSectionId] = useState(null); + + // 섹션 불러오기 핸들러 + const handleImportSection = useCallback(async (selectedPageId: number | null) => { + if (!selectedPageId || !selectedImportSectionId) return; + + try { + await linkSectionToPage(selectedPageId, selectedImportSectionId); + toast.success('섹션을 불러왔습니다.'); + setSelectedImportSectionId(null); + } catch (error) { + console.error('섹션 불러오기 실패:', error); + toast.error(getErrorMessage(error)); + } + }, [selectedImportSectionId, linkSectionToPage]); + + // 필드 불러오기 핸들러 + const handleImportField = useCallback(async (selectedPage: ItemPage | null) => { + if (!importFieldTargetSectionId || !selectedImportFieldId) return; + + try { + // 해당 섹션의 마지막 순서 + 1로 설정 + const targetSection = selectedPage?.sections.find(s => s.id === importFieldTargetSectionId); + const existingFieldsCount = targetSection?.fields?.length ?? 0; + const newOrderNo = existingFieldsCount; + + await linkFieldToSection(importFieldTargetSectionId, selectedImportFieldId, newOrderNo); + toast.success('필드를 섹션에 연결했습니다.'); + + setSelectedImportFieldId(null); + setImportFieldTargetSectionId(null); + } catch (error) { + console.error('필드 불러오기 실패:', error); + toast.error(getErrorMessage(error)); + } + }, [importFieldTargetSectionId, selectedImportFieldId, linkFieldToSection]); + + // 섹션 복제 핸들러 + const handleCloneSection = useCallback(async (sectionId: number) => { + try { + await cloneSection(sectionId); + toast.success('섹션이 복제되었습니다.'); + } catch (error) { + console.error('섹션 복제 실패:', error); + toast.error(getErrorMessage(error)); + } + }, [cloneSection]); + + return { + // 섹션 Import + isImportSectionDialogOpen, + setIsImportSectionDialogOpen, + selectedImportSectionId, + setSelectedImportSectionId, + + // 필드 Import + isImportFieldDialogOpen, + setIsImportFieldDialogOpen, + selectedImportFieldId, + setSelectedImportFieldId, + importFieldTargetSectionId, + setImportFieldTargetSectionId, + + // 핸들러 + handleImportSection, + handleImportField, + handleCloneSection, + }; +} \ No newline at end of file diff --git a/src/components/items/ItemMasterDataManagement/hooks/useInitialDataLoading.ts b/src/components/items/ItemMasterDataManagement/hooks/useInitialDataLoading.ts new file mode 100644 index 00000000..7340cfd7 --- /dev/null +++ b/src/components/items/ItemMasterDataManagement/hooks/useInitialDataLoading.ts @@ -0,0 +1,150 @@ +'use client'; + +import { useState, useEffect, useCallback } from 'react'; +import { useItemMaster } from '@/contexts/ItemMasterContext'; +import { itemMasterApi } from '@/lib/api/item-master'; +import { getErrorMessage, ApiError } from '@/lib/api/error-handler'; +import { + transformPagesResponse, + transformSectionsResponse, + transformSectionTemplatesResponse, + transformFieldsResponse, + transformCustomTabsResponse, + transformUnitOptionsResponse, + transformSectionTemplateFromSection, +} from '@/lib/api/transformers'; +import { toast } from 'sonner'; +import type { CustomTab } from './useTabManagement'; +import type { UnitOption } from './useAttributeManagement'; + +export interface UseInitialDataLoadingReturn { + isInitialLoading: boolean; + error: string | null; + reload: () => Promise; +} + +interface UseInitialDataLoadingProps { + setCustomTabs: React.Dispatch>; + setUnitOptions: React.Dispatch>; +} + +export function useInitialDataLoading({ + setCustomTabs, + setUnitOptions, +}: UseInitialDataLoadingProps): UseInitialDataLoadingReturn { + const { + loadItemPages, + loadSectionTemplates, + loadItemMasterFields, + loadIndependentSections, + loadIndependentFields, + } = useItemMaster(); + + const [isInitialLoading, setIsInitialLoading] = useState(true); + const [error, setError] = useState(null); + + const loadInitialData = useCallback(async () => { + try { + setIsInitialLoading(true); + setError(null); + + const data = await itemMasterApi.init(); + + // 1. 페이지 데이터 로드 (섹션이 이미 포함되어 있음) + const transformedPages = transformPagesResponse(data.pages); + loadItemPages(transformedPages); + + // 2. 독립 섹션 로드 (모든 재사용 가능 섹션) + if (data.sections && data.sections.length > 0) { + const transformedSections = transformSectionsResponse(data.sections); + loadIndependentSections(transformedSections); + console.log('✅ 독립 섹션 로드:', transformedSections.length); + } + + // 3. 섹션 템플릿 로드 + if (data.sectionTemplates && data.sectionTemplates.length > 0) { + const transformedTemplates = transformSectionTemplatesResponse(data.sectionTemplates); + loadSectionTemplates(transformedTemplates); + } else if (data.sections && data.sections.length > 0) { + const templates = data.sections + .filter((s: { is_template?: boolean }) => s.is_template) + .map(transformSectionTemplateFromSection); + if (templates.length > 0) { + loadSectionTemplates(templates); + } + } + + // 4. 필드 로드 + if (data.fields && data.fields.length > 0) { + const transformedFields = transformFieldsResponse(data.fields); + + const independentOnlyFields = transformedFields.filter( + f => f.section_id === null || f.section_id === undefined + ); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + loadItemMasterFields(transformedFields as any); + loadIndependentFields(independentOnlyFields); + + console.log('✅ 필드 로드:', { + total: transformedFields.length, + independent: independentOnlyFields.length, + }); + } + + // 5. 커스텀 탭 로드 + if (data.customTabs && data.customTabs.length > 0) { + const transformedTabs = transformCustomTabsResponse(data.customTabs); + setCustomTabs(transformedTabs); + } + + // 6. 단위 옵션 로드 + if (data.unitOptions && data.unitOptions.length > 0) { + const transformedUnits = transformUnitOptionsResponse(data.unitOptions); + setUnitOptions(transformedUnits); + } + + console.log('✅ Initial data loaded:', { + pages: data.pages?.length || 0, + sections: data.sections?.length || 0, + fields: data.fields?.length || 0, + customTabs: data.customTabs?.length || 0, + unitOptions: data.unitOptions?.length || 0, + }); + + } catch (err) { + if (err instanceof ApiError && err.errors) { + const errorMessages = Object.entries(err.errors) + .map(([field, messages]) => `${field}: ${messages.join(', ')}`) + .join('\n'); + toast.error(errorMessages); + setError('입력값을 확인해주세요.'); + } else { + const errorMessage = getErrorMessage(err); + setError(errorMessage); + toast.error(errorMessage); + } + console.error('❌ Failed to load initial data:', err); + } finally { + setIsInitialLoading(false); + } + }, [ + loadItemPages, + loadSectionTemplates, + loadItemMasterFields, + loadIndependentSections, + loadIndependentFields, + setCustomTabs, + setUnitOptions, + ]); + + useEffect(() => { + loadInitialData(); + }, [loadInitialData]); + + return { + isInitialLoading, + error, + reload: loadInitialData, + }; +} \ No newline at end of file diff --git a/src/components/items/ItemMasterDataManagement/hooks/useReorderManagement.ts b/src/components/items/ItemMasterDataManagement/hooks/useReorderManagement.ts new file mode 100644 index 00000000..82fb36b2 --- /dev/null +++ b/src/components/items/ItemMasterDataManagement/hooks/useReorderManagement.ts @@ -0,0 +1,84 @@ +'use client'; + +import { useCallback } from 'react'; +import { useItemMaster } from '@/contexts/ItemMasterContext'; +import { toast } from 'sonner'; +import type { ItemPage } from '@/contexts/ItemMasterContext'; + +export interface UseReorderManagementReturn { + moveSection: (selectedPage: ItemPage | null, dragIndex: number, hoverIndex: number) => Promise; + moveField: (selectedPage: ItemPage | null, sectionId: number, dragFieldId: number, hoverFieldId: number) => Promise; +} + +export function useReorderManagement(): UseReorderManagementReturn { + const { + reorderSections, + reorderFields, + } = useItemMaster(); + + // 섹션 순서 변경 핸들러 (드래그앤드롭) + const moveSection = useCallback(async ( + selectedPage: ItemPage | null, + dragIndex: number, + hoverIndex: number + ) => { + if (!selectedPage) return; + + const sections = [...selectedPage.sections]; + const [draggedSection] = sections.splice(dragIndex, 1); + sections.splice(hoverIndex, 0, draggedSection); + + const sectionIds = sections.map(s => s.id); + + try { + await reorderSections(selectedPage.id, sectionIds); + toast.success('섹션 순서가 변경되었습니다'); + } catch (error) { + console.error('섹션 순서 변경 실패:', error); + toast.error('섹션 순서 변경에 실패했습니다'); + } + }, [reorderSections]); + + // 필드 순서 변경 핸들러 + const moveField = useCallback(async ( + selectedPage: ItemPage | null, + sectionId: number, + dragFieldId: number, + hoverFieldId: number + ) => { + if (!selectedPage) return; + const section = selectedPage.sections.find(s => s.id === sectionId); + if (!section || !section.fields) return; + + // 동일 필드면 스킵 + if (dragFieldId === hoverFieldId) return; + + // 정렬된 배열에서 ID로 인덱스 찾기 + const sortedFields = [...section.fields].sort((a, b) => (a.order_no ?? 0) - (b.order_no ?? 0)); + const dragIndex = sortedFields.findIndex(f => f.id === dragFieldId); + const hoverIndex = sortedFields.findIndex(f => f.id === hoverFieldId); + + // 유효하지 않은 인덱스 체크 + if (dragIndex === -1 || hoverIndex === -1) { + return; + } + + // 드래그된 필드를 제거하고 새 위치에 삽입 + const [draggedField] = sortedFields.splice(dragIndex, 1); + sortedFields.splice(hoverIndex, 0, draggedField); + + const newFieldIds = sortedFields.map(f => f.id); + + try { + await reorderFields(sectionId, newFieldIds); + toast.success('항목 순서가 변경되었습니다'); + } catch (error) { + toast.error('항목 순서 변경에 실패했습니다'); + } + }, [reorderFields]); + + return { + moveSection, + moveField, + }; +} \ No newline at end of file