refactor: 품목기준관리 설정 페이지 훅/컴포넌트 분리
- Phase 1: 신규 훅 4개 생성 - useInitialDataLoading.ts (초기 데이터 로딩) - useImportManagement.ts (섹션/필드 Import) - useReorderManagement.ts (드래그앤드롭 순서 변경) - useDeleteManagement.ts (삭제/언링크 핸들러) - Phase 2: UI 컴포넌트 2개 생성 - AttributeTabContent.tsx (속성 탭 콘텐츠) - ItemMasterDialogs.tsx (다이얼로그 통합) - 메인 컴포넌트 1,799줄 → ~1,478줄 (약 320줄 감소) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 분리 예정 |
|
||||
|
||||
@@ -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` - 세션 컨텍스트
|
||||
@@ -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 문서 확인하고 시작해.
|
||||
```
|
||||
270
claudedocs/item-master/[PLAN-2025-12-24] hook-extraction-plan.md
Normal file
270
claudedocs/item-master/[PLAN-2025-12-24] hook-extraction-plan.md
Normal file
@@ -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<void>;
|
||||
}
|
||||
```
|
||||
|
||||
#### 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<void>;
|
||||
moveField: (sectionId: number, dragFieldId: number, hoverFieldId: number) => Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
#### 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<void>;
|
||||
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<string, Option[]>;
|
||||
attributeColumns: Record<string, Column[]>;
|
||||
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. 최종 수동 테스트
|
||||
@@ -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<string | null>(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<BOMItem[]>([]);
|
||||
|
||||
// 2025-11-26 추가: 섹션/필드 불러오기 다이얼로그 상태
|
||||
const [isImportSectionDialogOpen, setIsImportSectionDialogOpen] = useState(false);
|
||||
const [isImportFieldDialogOpen, setIsImportFieldDialogOpen] = useState(false);
|
||||
const [selectedImportSectionId, setSelectedImportSectionId] = useState<number | null>(null);
|
||||
const [selectedImportFieldId, setSelectedImportFieldId] = useState<number | null>(null);
|
||||
const [importFieldTargetSectionId, setImportFieldTargetSectionId] = useState<number | null>(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<React.SetStateAction<'FG' | 'PT' | 'SM' | 'RM' | 'CS'>> = 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 (
|
||||
|
||||
@@ -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<string, string>;
|
||||
}
|
||||
|
||||
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<string, string> = {
|
||||
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<string, UnitOption[]>;
|
||||
attributeColumns: Record<string, AttributeColumn[]>;
|
||||
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<string, string>) => 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 (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="font-medium">{title}</h3>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => {
|
||||
setManagingColumnType(optionType);
|
||||
setNewColumnName('');
|
||||
setNewColumnKey('');
|
||||
setNewColumnType('text');
|
||||
setNewColumnRequired(false);
|
||||
setIsColumnManageDialogOpen(true);
|
||||
}}>
|
||||
<Settings className="w-4 h-4 mr-2" />칼럼 관리
|
||||
</Button>
|
||||
<Button size="sm" onClick={() => {
|
||||
setEditingOptionType(optionType === 'units' ? 'unit' : optionType === 'materials' ? 'material' : optionType === 'surface' ? 'surface' : optionType);
|
||||
setIsOptionDialogOpen(true);
|
||||
}}>
|
||||
<Plus className="w-4 h-4 mr-2" />추가
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{options.map((option) => {
|
||||
const hasColumns = columns.length > 0 && option.columnValues;
|
||||
|
||||
return (
|
||||
<div key={option.id} className="p-4 border rounded hover:bg-gray-50 transition-colors">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-base">{option.label}</span>
|
||||
{option.inputType && (
|
||||
<Badge variant="outline" className="text-xs">{getInputTypeLabel(option.inputType)}</Badge>
|
||||
)}
|
||||
{option.required && (
|
||||
<Badge variant="destructive" className="text-xs">필수</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-muted-foreground space-y-1">
|
||||
<div className="flex gap-2">
|
||||
<span className="font-medium min-w-16">값(Value):</span>
|
||||
<span>{option.value}</span>
|
||||
</div>
|
||||
{option.placeholder && (
|
||||
<div className="flex gap-2">
|
||||
<span className="font-medium min-w-16">플레이스홀더:</span>
|
||||
<span>{option.placeholder}</span>
|
||||
</div>
|
||||
)}
|
||||
{option.defaultValue && (
|
||||
<div className="flex gap-2">
|
||||
<span className="font-medium min-w-16">기본값:</span>
|
||||
<span>{option.defaultValue}</span>
|
||||
</div>
|
||||
)}
|
||||
{option.inputType === 'dropdown' && option.options && (
|
||||
<div className="flex gap-2">
|
||||
<span className="font-medium min-w-16">옵션:</span>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{option.options.map((opt, idx) => (
|
||||
<Badge key={idx} variant="secondary" className="text-xs">{opt}</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hasColumns && (
|
||||
<div className="mt-3 pt-3 border-t">
|
||||
<p className="text-xs font-medium text-muted-foreground mb-2">추가 칼럼</p>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
{columns.map((column) => (
|
||||
<div key={column.id} className="flex gap-2">
|
||||
<span className="text-muted-foreground">{column.name}:</span>
|
||||
<span>{option.columnValues?.[column.key] || '-'}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={() => handleDeleteOption(optionType === 'units' ? 'unit' : optionType === 'materials' ? 'material' : optionType === 'surface' ? 'surface' : optionType, option.id)}>
|
||||
<Trash2 className="w-4 h-4 text-red-500" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 마스터 필드 속성 렌더링
|
||||
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 (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="font-medium">{masterField.field_name} 속성 목록</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
항목 탭에서 추가한 "{masterField.field_name}" 항목의 속성값들입니다
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
||||
{propertiesArray.map((property: any) => {
|
||||
const inputTypeLabel = getInputTypeLabel(property.type);
|
||||
|
||||
return (
|
||||
<div key={property.id} className="p-4 border rounded hover:bg-gray-50 transition-colors">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-base">{property.label}</span>
|
||||
<Badge variant="outline" className="text-xs">{inputTypeLabel}</Badge>
|
||||
{property.required && (
|
||||
<Badge variant="destructive" className="text-xs">필수</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-muted-foreground space-y-1">
|
||||
<div className="flex gap-2">
|
||||
<span className="font-medium min-w-24">키(Key):</span>
|
||||
<code className="bg-gray-100 px-2 py-0.5 rounded text-xs">{property.key}</code>
|
||||
</div>
|
||||
{property.placeholder && (
|
||||
<div className="flex gap-2">
|
||||
<span className="font-medium min-w-24">플레이스홀더:</span>
|
||||
<span>{property.placeholder}</span>
|
||||
</div>
|
||||
)}
|
||||
{property.defaultValue && (
|
||||
<div className="flex gap-2">
|
||||
<span className="font-medium min-w-24">기본값:</span>
|
||||
<span>{property.defaultValue}</span>
|
||||
</div>
|
||||
)}
|
||||
{property.type === 'dropdown' && property.options && (
|
||||
<div className="flex gap-2">
|
||||
<span className="font-medium min-w-24">옵션:</span>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{property.options.map((opt: string, idx: number) => (
|
||||
<Badge key={idx} variant="secondary" className="text-xs">{opt}</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<div className="flex items-start gap-2">
|
||||
<Package className="w-5 h-5 text-blue-600 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-blue-900">
|
||||
마스터 항목 속성 관리
|
||||
</p>
|
||||
<p className="text-xs text-blue-700 mt-1">
|
||||
이 속성들은 <strong>항목 탭</strong>에서 "{masterField.field_name}" 항목을 편집하여 추가/수정/삭제할 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 사용자 정의 속성 렌더링
|
||||
const renderCustomAttributeTab = () => {
|
||||
const currentTabKey = activeAttributeTab;
|
||||
const currentOptions = customAttributeOptions[currentTabKey] || [];
|
||||
const columns = attributeColumns[currentTabKey] || [];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="font-medium">
|
||||
{attributeSubTabs.find(t => t.key === activeAttributeTab)?.label || '사용자 정의'} 목록
|
||||
</h3>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => {
|
||||
setManagingColumnType(currentTabKey);
|
||||
setNewColumnName('');
|
||||
setNewColumnKey('');
|
||||
setNewColumnType('text');
|
||||
setNewColumnRequired(false);
|
||||
setIsColumnManageDialogOpen(true);
|
||||
}}>
|
||||
<Settings className="w-4 h-4 mr-2" />칼럼 관리
|
||||
</Button>
|
||||
<Button size="sm" onClick={() => {
|
||||
setEditingOptionType(activeAttributeTab);
|
||||
setNewOptionValue('');
|
||||
setNewOptionLabel('');
|
||||
setNewOptionColumnValues({});
|
||||
setIsOptionDialogOpen(true);
|
||||
}}>
|
||||
<Plus className="w-4 h-4 mr-2" />추가
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{currentOptions.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{currentOptions.map((option) => {
|
||||
const hasColumns = columns.length > 0 && option.columnValues;
|
||||
const inputTypeLabel = getInputTypeLabel(option.inputType);
|
||||
|
||||
return (
|
||||
<div key={option.id} className="p-4 border rounded hover:bg-gray-50 transition-colors">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-base">{option.label}</span>
|
||||
<Badge variant="outline" className="text-xs">{inputTypeLabel}</Badge>
|
||||
{option.required && (
|
||||
<Badge variant="destructive" className="text-xs">필수</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-muted-foreground space-y-1">
|
||||
<div className="flex gap-2">
|
||||
<span className="font-medium min-w-16">값(Value):</span>
|
||||
<span>{option.value}</span>
|
||||
</div>
|
||||
{option.placeholder && (
|
||||
<div className="flex gap-2">
|
||||
<span className="font-medium min-w-16">플레이스홀더:</span>
|
||||
<span>{option.placeholder}</span>
|
||||
</div>
|
||||
)}
|
||||
{option.defaultValue && (
|
||||
<div className="flex gap-2">
|
||||
<span className="font-medium min-w-16">기본값:</span>
|
||||
<span>{option.defaultValue}</span>
|
||||
</div>
|
||||
)}
|
||||
{option.inputType === 'dropdown' && option.options && (
|
||||
<div className="flex gap-2">
|
||||
<span className="font-medium min-w-16">옵션:</span>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{option.options.map((opt, idx) => (
|
||||
<Badge key={idx} variant="secondary" className="text-xs">{opt}</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hasColumns && (
|
||||
<div className="mt-3 pt-3 border-t">
|
||||
<p className="text-xs font-medium text-muted-foreground mb-2">추가 칼럼</p>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
{columns.map((column) => (
|
||||
<div key={column.id} className="flex gap-2">
|
||||
<span className="text-muted-foreground">{column.name}:</span>
|
||||
<span>{option.columnValues?.[column.key] || '-'}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={() => handleDeleteOption(currentTabKey, option.id)}>
|
||||
<Trash2 className="w-4 h-4 text-red-500" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="border-2 border-dashed rounded-lg p-8 text-center text-gray-500">
|
||||
<Settings className="w-12 h-12 mx-auto mb-4 text-gray-400" />
|
||||
<p className="mb-2">아직 추가된 항목이 없습니다</p>
|
||||
<p className="text-sm">위 "추가" 버튼을 클릭하여 새로운 속성을 추가할 수 있습니다</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>속성 관리</CardTitle>
|
||||
<CardDescription>단위, 재질, 표면처리 등의 속성을 관리합니다</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{/* 속성 하위 탭 (칩 형태) */}
|
||||
<div className="flex items-center gap-2 mb-6 border-b pb-2">
|
||||
<div className="flex gap-2 flex-1 flex-wrap">
|
||||
{attributeSubTabs.sort((a, b) => a.order - b.order).map(tab => (
|
||||
<Button
|
||||
key={tab.id}
|
||||
variant={activeAttributeTab === tab.key ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setActiveAttributeTab(tab.key)}
|
||||
className="rounded-full"
|
||||
>
|
||||
{tab.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setIsManageAttributeTabsDialogOpen(true)}
|
||||
className="shrink-0"
|
||||
>
|
||||
<Settings className="w-4 h-4 mr-1" />
|
||||
항목 관리
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 단위 관리 */}
|
||||
{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();
|
||||
})()}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -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<string, string>;
|
||||
setNewOptionColumnValues: (values: Record<string, string>) => 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<string, AttributeColumn[]>;
|
||||
handleAddOption: () => void;
|
||||
|
||||
// Column Manage Dialog
|
||||
isColumnManageDialogOpen: boolean;
|
||||
setIsColumnManageDialogOpen: (open: boolean) => void;
|
||||
managingColumnType: string;
|
||||
setAttributeColumns: React.Dispatch<React.SetStateAction<Record<string, AttributeColumn[]>>>;
|
||||
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<ItemPage>) => void;
|
||||
|
||||
// Page Dialog
|
||||
isPageDialogOpen: boolean;
|
||||
setIsPageDialogOpen: (open: boolean) => void;
|
||||
newPageName: string;
|
||||
setNewPageName: (name: string) => void;
|
||||
newPageItemType: 'FG' | 'PT' | 'SM' | 'RM' | 'CS';
|
||||
setNewPageItemType: React.Dispatch<React.SetStateAction<'FG' | 'PT' | 'SM' | 'RM' | 'CS'>>;
|
||||
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<React.SetStateAction<TextboxColumn[]>>;
|
||||
newFieldConditionEnabled: boolean;
|
||||
setNewFieldConditionEnabled: (enabled: boolean) => void;
|
||||
newFieldConditionTargetType: 'field' | 'section';
|
||||
setNewFieldConditionTargetType: (type: 'field' | 'section') => void;
|
||||
newFieldConditionFields: ConditionField[];
|
||||
setNewFieldConditionFields: React.Dispatch<React.SetStateAction<ConditionField[]>>;
|
||||
newFieldConditionSections: ConditionSection[];
|
||||
setNewFieldConditionSections: React.Dispatch<React.SetStateAction<ConditionSection[]>>;
|
||||
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<React.SetStateAction<string[]>>;
|
||||
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<React.SetStateAction<string[]>>;
|
||||
newMasterFieldAttributeType: string;
|
||||
setNewMasterFieldAttributeType: (type: string) => void;
|
||||
newMasterFieldMultiColumn: boolean;
|
||||
setNewMasterFieldMultiColumn: (multiColumn: boolean) => void;
|
||||
newMasterFieldColumnCount: number;
|
||||
setNewMasterFieldColumnCount: (count: number) => void;
|
||||
newMasterFieldColumnNames: string[];
|
||||
setNewMasterFieldColumnNames: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
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<React.SetStateAction<string[]>>;
|
||||
templateFieldDescription: string;
|
||||
setTemplateFieldDescription: (description: string) => void;
|
||||
templateFieldMultiColumn: boolean;
|
||||
setTemplateFieldMultiColumn: (multiColumn: boolean) => void;
|
||||
templateFieldColumnCount: number;
|
||||
setTemplateFieldColumnCount: (count: number) => void;
|
||||
templateFieldColumnNames: string[];
|
||||
setTemplateFieldColumnNames: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
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<void>;
|
||||
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<void>;
|
||||
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 (
|
||||
<>
|
||||
<TabManagementDialogs
|
||||
isManageTabsDialogOpen={isManageTabsDialogOpen}
|
||||
setIsManageTabsDialogOpen={setIsManageTabsDialogOpen}
|
||||
customTabs={customTabs}
|
||||
moveTabUp={moveTabUp}
|
||||
moveTabDown={moveTabDown}
|
||||
handleEditTabFromManage={handleEditTabFromManage}
|
||||
handleDeleteTab={handleDeleteTab}
|
||||
getTabIcon={getTabIcon}
|
||||
setIsAddTabDialogOpen={setIsAddTabDialogOpen}
|
||||
isDeleteTabDialogOpen={isDeleteTabDialogOpen}
|
||||
setIsDeleteTabDialogOpen={setIsDeleteTabDialogOpen}
|
||||
deletingTabId={deletingTabId}
|
||||
setDeletingTabId={setDeletingTabId}
|
||||
confirmDeleteTab={confirmDeleteTab}
|
||||
isAddTabDialogOpen={isAddTabDialogOpen}
|
||||
editingTabId={editingTabId}
|
||||
setEditingTabId={setEditingTabId}
|
||||
newTabLabel={newTabLabel}
|
||||
setNewTabLabel={setNewTabLabel}
|
||||
handleUpdateTab={handleUpdateTab}
|
||||
handleAddTab={handleAddTab}
|
||||
isManageAttributeTabsDialogOpen={isManageAttributeTabsDialogOpen}
|
||||
setIsManageAttributeTabsDialogOpen={setIsManageAttributeTabsDialogOpen}
|
||||
attributeSubTabs={attributeSubTabs}
|
||||
moveAttributeTabUp={moveAttributeTabUp}
|
||||
moveAttributeTabDown={moveAttributeTabDown}
|
||||
handleDeleteAttributeTab={handleDeleteAttributeTab}
|
||||
isDeleteAttributeTabDialogOpen={isDeleteAttributeTabDialogOpen}
|
||||
setIsDeleteAttributeTabDialogOpen={setIsDeleteAttributeTabDialogOpen}
|
||||
deletingAttributeTabId={deletingAttributeTabId}
|
||||
setDeletingAttributeTabId={setDeletingAttributeTabId}
|
||||
confirmDeleteAttributeTab={confirmDeleteAttributeTab}
|
||||
isAddAttributeTabDialogOpen={isAddAttributeTabDialogOpen}
|
||||
setIsAddAttributeTabDialogOpen={setIsAddAttributeTabDialogOpen}
|
||||
editingAttributeTabId={editingAttributeTabId}
|
||||
setEditingAttributeTabId={setEditingAttributeTabId}
|
||||
newAttributeTabLabel={newAttributeTabLabel}
|
||||
setNewAttributeTabLabel={setNewAttributeTabLabel}
|
||||
handleUpdateAttributeTab={handleUpdateAttributeTab}
|
||||
handleAddAttributeTab={handleAddAttributeTab}
|
||||
/>
|
||||
|
||||
<OptionDialog
|
||||
isOpen={isOptionDialogOpen}
|
||||
setIsOpen={setIsOptionDialogOpen}
|
||||
newOptionValue={newOptionValue}
|
||||
setNewOptionValue={setNewOptionValue}
|
||||
newOptionLabel={newOptionLabel}
|
||||
setNewOptionLabel={setNewOptionLabel}
|
||||
newOptionColumnValues={newOptionColumnValues}
|
||||
setNewOptionColumnValues={setNewOptionColumnValues}
|
||||
newOptionInputType={newOptionInputType}
|
||||
setNewOptionInputType={setNewOptionInputType}
|
||||
newOptionRequired={newOptionRequired}
|
||||
setNewOptionRequired={setNewOptionRequired}
|
||||
newOptionOptions={newOptionOptions}
|
||||
setNewOptionOptions={setNewOptionOptions}
|
||||
newOptionPlaceholder={newOptionPlaceholder}
|
||||
setNewOptionPlaceholder={setNewOptionPlaceholder}
|
||||
newOptionDefaultValue={newOptionDefaultValue}
|
||||
setNewOptionDefaultValue={setNewOptionDefaultValue}
|
||||
editingOptionType={editingOptionType}
|
||||
attributeSubTabs={attributeSubTabs}
|
||||
attributeColumns={attributeColumns}
|
||||
handleAddOption={handleAddOption}
|
||||
/>
|
||||
|
||||
<ColumnManageDialog
|
||||
isOpen={isColumnManageDialogOpen}
|
||||
setIsOpen={setIsColumnManageDialogOpen}
|
||||
managingColumnType={managingColumnType}
|
||||
attributeSubTabs={attributeSubTabs}
|
||||
attributeColumns={attributeColumns}
|
||||
setAttributeColumns={setAttributeColumns}
|
||||
newColumnName={newColumnName}
|
||||
setNewColumnName={setNewColumnName}
|
||||
newColumnKey={newColumnKey}
|
||||
setNewColumnKey={setNewColumnKey}
|
||||
newColumnType={newColumnType}
|
||||
setNewColumnType={setNewColumnType}
|
||||
newColumnRequired={newColumnRequired}
|
||||
setNewColumnRequired={setNewColumnRequired}
|
||||
/>
|
||||
|
||||
<PathEditDialog
|
||||
editingPathPageId={editingPathPageId}
|
||||
setEditingPathPageId={setEditingPathPageId}
|
||||
editingAbsolutePath={editingAbsolutePath}
|
||||
setEditingAbsolutePath={setEditingAbsolutePath}
|
||||
updateItemPage={updateItemPage}
|
||||
trackChange={() => {}}
|
||||
/>
|
||||
|
||||
<PageDialog
|
||||
isPageDialogOpen={isPageDialogOpen}
|
||||
setIsPageDialogOpen={setIsPageDialogOpen}
|
||||
newPageName={newPageName}
|
||||
setNewPageName={setNewPageName}
|
||||
newPageItemType={newPageItemType}
|
||||
setNewPageItemType={setNewPageItemType}
|
||||
handleAddPage={handleAddPage}
|
||||
/>
|
||||
|
||||
<SectionDialog
|
||||
isSectionDialogOpen={isSectionDialogOpen}
|
||||
setIsSectionDialogOpen={setIsSectionDialogOpen}
|
||||
newSectionType={newSectionType}
|
||||
setNewSectionType={setNewSectionType}
|
||||
newSectionTitle={newSectionTitle}
|
||||
setNewSectionTitle={setNewSectionTitle}
|
||||
newSectionDescription={newSectionDescription}
|
||||
setNewSectionDescription={setNewSectionDescription}
|
||||
handleAddSection={handleAddSection}
|
||||
sectionInputMode={sectionInputMode}
|
||||
setSectionInputMode={setSectionInputMode}
|
||||
sectionTemplates={sectionsAsTemplates}
|
||||
selectedTemplateId={selectedSectionTemplateId}
|
||||
setSelectedTemplateId={setSelectedSectionTemplateId}
|
||||
handleLinkTemplate={handleLinkTemplate}
|
||||
/>
|
||||
|
||||
{/* 항목 추가/수정 다이얼로그 - 데스크톱 */}
|
||||
{!isMobile && (
|
||||
<FieldDialog
|
||||
isOpen={isFieldDialogOpen}
|
||||
onOpenChange={setIsFieldDialogOpen}
|
||||
editingFieldId={editingFieldId}
|
||||
setEditingFieldId={setEditingFieldId}
|
||||
fieldInputMode={fieldInputMode}
|
||||
setFieldInputMode={setFieldInputMode}
|
||||
showMasterFieldList={showMasterFieldList}
|
||||
setShowMasterFieldList={setShowMasterFieldList}
|
||||
selectedMasterFieldId={selectedMasterFieldId}
|
||||
setSelectedMasterFieldId={setSelectedMasterFieldId}
|
||||
textboxColumns={textboxColumns}
|
||||
setTextboxColumns={setTextboxColumns}
|
||||
newFieldConditionEnabled={newFieldConditionEnabled}
|
||||
setNewFieldConditionEnabled={setNewFieldConditionEnabled}
|
||||
newFieldConditionTargetType={newFieldConditionTargetType}
|
||||
setNewFieldConditionTargetType={setNewFieldConditionTargetType}
|
||||
newFieldConditionFields={newFieldConditionFields}
|
||||
setNewFieldConditionFields={setNewFieldConditionFields}
|
||||
newFieldConditionSections={newFieldConditionSections}
|
||||
setNewFieldConditionSections={setNewFieldConditionSections}
|
||||
tempConditionValue={tempConditionValue}
|
||||
setTempConditionValue={setTempConditionValue}
|
||||
newFieldName={newFieldName}
|
||||
setNewFieldName={setNewFieldName}
|
||||
newFieldKey={newFieldKey}
|
||||
setNewFieldKey={setNewFieldKey}
|
||||
newFieldInputType={newFieldInputType}
|
||||
setNewFieldInputType={setNewFieldInputType}
|
||||
newFieldRequired={newFieldRequired}
|
||||
setNewFieldRequired={setNewFieldRequired}
|
||||
newFieldDescription={newFieldDescription}
|
||||
setNewFieldDescription={setNewFieldDescription}
|
||||
newFieldOptions={newFieldOptions}
|
||||
setNewFieldOptions={setNewFieldOptions}
|
||||
selectedSectionForField={selectedPage?.sections.find(s => s.id === selectedSectionForField) || null}
|
||||
selectedPage={selectedPage || null}
|
||||
itemMasterFields={itemMasterFields}
|
||||
handleAddField={handleAddField}
|
||||
setIsColumnDialogOpen={setIsColumnDialogOpen}
|
||||
setEditingColumnId={setEditingColumnId}
|
||||
setColumnName={setColumnName}
|
||||
setColumnKey={setColumnKey}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 항목 추가/수정 다이얼로그 - 모바일 (바텀시트) */}
|
||||
{isMobile && (
|
||||
<FieldDrawer
|
||||
isOpen={isFieldDialogOpen}
|
||||
onOpenChange={setIsFieldDialogOpen}
|
||||
editingFieldId={editingFieldId}
|
||||
setEditingFieldId={setEditingFieldId}
|
||||
fieldInputMode={fieldInputMode}
|
||||
setFieldInputMode={setFieldInputMode}
|
||||
showMasterFieldList={showMasterFieldList}
|
||||
setShowMasterFieldList={setShowMasterFieldList}
|
||||
selectedMasterFieldId={selectedMasterFieldId}
|
||||
setSelectedMasterFieldId={setSelectedMasterFieldId}
|
||||
textboxColumns={textboxColumns}
|
||||
setTextboxColumns={setTextboxColumns}
|
||||
newFieldConditionEnabled={newFieldConditionEnabled}
|
||||
setNewFieldConditionEnabled={setNewFieldConditionEnabled}
|
||||
newFieldConditionTargetType={newFieldConditionTargetType}
|
||||
setNewFieldConditionTargetType={setNewFieldConditionTargetType}
|
||||
newFieldConditionFields={newFieldConditionFields}
|
||||
setNewFieldConditionFields={setNewFieldConditionFields}
|
||||
newFieldConditionSections={newFieldConditionSections}
|
||||
setNewFieldConditionSections={setNewFieldConditionSections}
|
||||
tempConditionValue={tempConditionValue}
|
||||
setTempConditionValue={setTempConditionValue}
|
||||
newFieldName={newFieldName}
|
||||
setNewFieldName={setNewFieldName}
|
||||
newFieldKey={newFieldKey}
|
||||
setNewFieldKey={setNewFieldKey}
|
||||
newFieldInputType={newFieldInputType}
|
||||
setNewFieldInputType={setNewFieldInputType}
|
||||
newFieldRequired={newFieldRequired}
|
||||
setNewFieldRequired={setNewFieldRequired}
|
||||
newFieldDescription={newFieldDescription}
|
||||
setNewFieldDescription={setNewFieldDescription}
|
||||
newFieldOptions={newFieldOptions}
|
||||
setNewFieldOptions={setNewFieldOptions}
|
||||
selectedSectionForField={selectedPage?.sections.find(s => s.id === selectedSectionForField) || null}
|
||||
selectedPage={selectedPage || null}
|
||||
itemMasterFields={itemMasterFields}
|
||||
handleAddField={handleAddField}
|
||||
setIsColumnDialogOpen={setIsColumnDialogOpen}
|
||||
setEditingColumnId={setEditingColumnId}
|
||||
setColumnName={setColumnName}
|
||||
setColumnKey={setColumnKey}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 텍스트박스 컬럼 추가/수정 다이얼로그 */}
|
||||
<ColumnDialog
|
||||
isColumnDialogOpen={isColumnDialogOpen}
|
||||
setIsColumnDialogOpen={setIsColumnDialogOpen}
|
||||
editingColumnId={editingColumnId}
|
||||
setEditingColumnId={setEditingColumnId}
|
||||
columnName={columnName}
|
||||
setColumnName={setColumnName}
|
||||
columnKey={columnKey}
|
||||
setColumnKey={setColumnKey}
|
||||
textboxColumns={textboxColumns}
|
||||
setTextboxColumns={setTextboxColumns}
|
||||
/>
|
||||
|
||||
<MasterFieldDialog
|
||||
isMasterFieldDialogOpen={isMasterFieldDialogOpen}
|
||||
setIsMasterFieldDialogOpen={setIsMasterFieldDialogOpen}
|
||||
editingMasterFieldId={editingMasterFieldId}
|
||||
setEditingMasterFieldId={setEditingMasterFieldId}
|
||||
newMasterFieldName={newMasterFieldName}
|
||||
setNewMasterFieldName={setNewMasterFieldName}
|
||||
newMasterFieldKey={newMasterFieldKey}
|
||||
setNewMasterFieldKey={setNewMasterFieldKey}
|
||||
newMasterFieldInputType={newMasterFieldInputType}
|
||||
setNewMasterFieldInputType={setNewMasterFieldInputType}
|
||||
newMasterFieldRequired={newMasterFieldRequired}
|
||||
setNewMasterFieldRequired={setNewMasterFieldRequired}
|
||||
newMasterFieldCategory={newMasterFieldCategory}
|
||||
setNewMasterFieldCategory={setNewMasterFieldCategory}
|
||||
newMasterFieldDescription={newMasterFieldDescription}
|
||||
setNewMasterFieldDescription={setNewMasterFieldDescription}
|
||||
newMasterFieldOptions={newMasterFieldOptions}
|
||||
setNewMasterFieldOptions={setNewMasterFieldOptions}
|
||||
newMasterFieldAttributeType={newMasterFieldAttributeType}
|
||||
setNewMasterFieldAttributeType={setNewMasterFieldAttributeType}
|
||||
newMasterFieldMultiColumn={newMasterFieldMultiColumn}
|
||||
setNewMasterFieldMultiColumn={setNewMasterFieldMultiColumn}
|
||||
newMasterFieldColumnCount={newMasterFieldColumnCount}
|
||||
setNewMasterFieldColumnCount={setNewMasterFieldColumnCount}
|
||||
newMasterFieldColumnNames={newMasterFieldColumnNames}
|
||||
setNewMasterFieldColumnNames={setNewMasterFieldColumnNames}
|
||||
handleUpdateMasterField={handleUpdateMasterField}
|
||||
handleAddMasterField={handleAddMasterField}
|
||||
/>
|
||||
|
||||
<SectionTemplateDialog
|
||||
isSectionTemplateDialogOpen={isSectionTemplateDialogOpen}
|
||||
setIsSectionTemplateDialogOpen={setIsSectionTemplateDialogOpen}
|
||||
editingSectionTemplateId={editingSectionTemplateId}
|
||||
setEditingSectionTemplateId={setEditingSectionTemplateId}
|
||||
newSectionTemplateTitle={newSectionTemplateTitle}
|
||||
setNewSectionTemplateTitle={setNewSectionTemplateTitle}
|
||||
newSectionTemplateDescription={newSectionTemplateDescription}
|
||||
setNewSectionTemplateDescription={setNewSectionTemplateDescription}
|
||||
newSectionTemplateCategory={newSectionTemplateCategory}
|
||||
setNewSectionTemplateCategory={setNewSectionTemplateCategory}
|
||||
newSectionTemplateType={newSectionTemplateType}
|
||||
setNewSectionTemplateType={setNewSectionTemplateType}
|
||||
handleUpdateSectionTemplate={handleUpdateSectionTemplate}
|
||||
handleAddSectionTemplate={handleAddSectionTemplate}
|
||||
/>
|
||||
|
||||
<TemplateFieldDialog
|
||||
isTemplateFieldDialogOpen={isTemplateFieldDialogOpen}
|
||||
setIsTemplateFieldDialogOpen={setIsTemplateFieldDialogOpen}
|
||||
editingTemplateFieldId={editingTemplateFieldId}
|
||||
setEditingTemplateFieldId={setEditingTemplateFieldId}
|
||||
templateFieldName={templateFieldName}
|
||||
setTemplateFieldName={setTemplateFieldName}
|
||||
templateFieldKey={templateFieldKey}
|
||||
setTemplateFieldKey={setTemplateFieldKey}
|
||||
templateFieldInputType={templateFieldInputType}
|
||||
setTemplateFieldInputType={setTemplateFieldInputType}
|
||||
templateFieldRequired={templateFieldRequired}
|
||||
setTemplateFieldRequired={setTemplateFieldRequired}
|
||||
templateFieldOptions={templateFieldOptions}
|
||||
setTemplateFieldOptions={setTemplateFieldOptions}
|
||||
templateFieldDescription={templateFieldDescription}
|
||||
setTemplateFieldDescription={setTemplateFieldDescription}
|
||||
templateFieldMultiColumn={templateFieldMultiColumn}
|
||||
setTemplateFieldMultiColumn={setTemplateFieldMultiColumn}
|
||||
templateFieldColumnCount={templateFieldColumnCount}
|
||||
setTemplateFieldColumnCount={setTemplateFieldColumnCount}
|
||||
templateFieldColumnNames={templateFieldColumnNames}
|
||||
setTemplateFieldColumnNames={setTemplateFieldColumnNames}
|
||||
handleAddTemplateField={handleAddTemplateField}
|
||||
itemMasterFields={itemMasterFields}
|
||||
templateFieldInputMode={templateFieldInputMode}
|
||||
setTemplateFieldInputMode={setTemplateFieldInputMode}
|
||||
showMasterFieldList={templateFieldShowMasterFieldList}
|
||||
setShowMasterFieldList={setTemplateFieldShowMasterFieldList}
|
||||
selectedMasterFieldId={templateFieldSelectedMasterFieldId}
|
||||
setSelectedMasterFieldId={setTemplateFieldSelectedMasterFieldId}
|
||||
/>
|
||||
|
||||
<LoadTemplateDialog
|
||||
isLoadTemplateDialogOpen={isLoadTemplateDialogOpen}
|
||||
setIsLoadTemplateDialogOpen={setIsLoadTemplateDialogOpen}
|
||||
sectionTemplates={sectionTemplates}
|
||||
selectedTemplateId={selectedTemplateId}
|
||||
setSelectedTemplateId={setSelectedTemplateId}
|
||||
handleLoadTemplate={handleLoadTemplate}
|
||||
/>
|
||||
|
||||
{/* 섹션 불러오기 다이얼로그 */}
|
||||
<ImportSectionDialog
|
||||
isOpen={isImportSectionDialogOpen}
|
||||
setIsOpen={setIsImportSectionDialogOpen}
|
||||
independentSections={independentSections}
|
||||
selectedSectionId={selectedImportSectionId}
|
||||
setSelectedSectionId={setSelectedImportSectionId}
|
||||
onImport={handleImportSection}
|
||||
onRefresh={refreshIndependentSections}
|
||||
onGetUsage={getSectionUsage}
|
||||
/>
|
||||
|
||||
{/* 필드 불러오기 다이얼로그 */}
|
||||
<ImportFieldDialog
|
||||
isOpen={isImportFieldDialogOpen}
|
||||
setIsOpen={setIsImportFieldDialogOpen}
|
||||
fields={itemMasterFields}
|
||||
selectedFieldId={selectedImportFieldId}
|
||||
setSelectedFieldId={setSelectedImportFieldId}
|
||||
onImport={handleImportField}
|
||||
onRefresh={refreshIndependentFields}
|
||||
onGetUsage={getFieldUsage}
|
||||
targetSectionTitle={
|
||||
importFieldTargetSectionId
|
||||
? selectedPage?.sections.find(s => s.id === importFieldTargetSectionId)?.title
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +1,7 @@
|
||||
export { DraggableSection } from './DraggableSection';
|
||||
export { DraggableField } from './DraggableField';
|
||||
export { DraggableField } from './DraggableField';
|
||||
|
||||
// 2025-12-24: Phase 2 UI 컴포넌트 분리
|
||||
export { AttributeTabContent } from './AttributeTabContent';
|
||||
export { ItemMasterDialogs } from './ItemMasterDialogs';
|
||||
export type { ItemMasterDialogsProps } from './ItemMasterDialogs';
|
||||
@@ -17,4 +17,17 @@ export { useAttributeManagement } from './useAttributeManagement';
|
||||
export type { UseAttributeManagementReturn } from './useAttributeManagement';
|
||||
|
||||
export { useTabManagement } from './useTabManagement';
|
||||
export type { UseTabManagementReturn, CustomTab, AttributeSubTab } from './useTabManagement';
|
||||
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';
|
||||
@@ -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<void>;
|
||||
handleResetAllData: (
|
||||
setUnitOptions: React.Dispatch<React.SetStateAction<UnitOption[]>>,
|
||||
setMaterialOptions: React.Dispatch<React.SetStateAction<MaterialOption[]>>,
|
||||
setSurfaceTreatmentOptions: React.Dispatch<React.SetStateAction<SurfaceTreatmentOption[]>>,
|
||||
setCustomAttributeOptions: React.Dispatch<React.SetStateAction<Record<string, UnitOption[]>>>,
|
||||
setAttributeColumns: React.Dispatch<React.SetStateAction<Record<string, { id: string; name: string; key: string; type: string; required: boolean }[]>>>,
|
||||
setBomItems: React.Dispatch<React.SetStateAction<BOMItem[]>>,
|
||||
setCustomTabs: React.Dispatch<React.SetStateAction<CustomTab[]>>,
|
||||
setAttributeSubTabs: React.Dispatch<React.SetStateAction<AttributeSubTab[]>>,
|
||||
) => 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<React.SetStateAction<UnitOption[]>>,
|
||||
setMaterialOptions: React.Dispatch<React.SetStateAction<MaterialOption[]>>,
|
||||
setSurfaceTreatmentOptions: React.Dispatch<React.SetStateAction<SurfaceTreatmentOption[]>>,
|
||||
setCustomAttributeOptions: React.Dispatch<React.SetStateAction<Record<string, UnitOption[]>>>,
|
||||
setAttributeColumns: React.Dispatch<React.SetStateAction<Record<string, { id: string; name: string; key: string; type: string; required: boolean }[]>>>,
|
||||
setBomItems: React.Dispatch<React.SetStateAction<BOMItem[]>>,
|
||||
setCustomTabs: React.Dispatch<React.SetStateAction<CustomTab[]>>,
|
||||
setAttributeSubTabs: React.Dispatch<React.SetStateAction<AttributeSubTab[]>>,
|
||||
) => {
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -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<React.SetStateAction<boolean>>;
|
||||
selectedImportSectionId: number | null;
|
||||
setSelectedImportSectionId: React.Dispatch<React.SetStateAction<number | null>>;
|
||||
|
||||
// 필드 Import 상태
|
||||
isImportFieldDialogOpen: boolean;
|
||||
setIsImportFieldDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
selectedImportFieldId: number | null;
|
||||
setSelectedImportFieldId: React.Dispatch<React.SetStateAction<number | null>>;
|
||||
importFieldTargetSectionId: number | null;
|
||||
setImportFieldTargetSectionId: React.Dispatch<React.SetStateAction<number | null>>;
|
||||
|
||||
// 핸들러
|
||||
handleImportSection: (selectedPageId: number | null) => Promise<void>;
|
||||
handleImportField: (selectedPage: ItemPage | null) => Promise<void>;
|
||||
handleCloneSection: (sectionId: number) => Promise<void>;
|
||||
}
|
||||
|
||||
export function useImportManagement(): UseImportManagementReturn {
|
||||
const {
|
||||
linkSectionToPage,
|
||||
linkFieldToSection,
|
||||
cloneSection,
|
||||
} = useItemMaster();
|
||||
|
||||
// 섹션 Import 상태
|
||||
const [isImportSectionDialogOpen, setIsImportSectionDialogOpen] = useState(false);
|
||||
const [selectedImportSectionId, setSelectedImportSectionId] = useState<number | null>(null);
|
||||
|
||||
// 필드 Import 상태
|
||||
const [isImportFieldDialogOpen, setIsImportFieldDialogOpen] = useState(false);
|
||||
const [selectedImportFieldId, setSelectedImportFieldId] = useState<number | null>(null);
|
||||
const [importFieldTargetSectionId, setImportFieldTargetSectionId] = useState<number | null>(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,
|
||||
};
|
||||
}
|
||||
@@ -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<void>;
|
||||
}
|
||||
|
||||
interface UseInitialDataLoadingProps {
|
||||
setCustomTabs: React.Dispatch<React.SetStateAction<CustomTab[]>>;
|
||||
setUnitOptions: React.Dispatch<React.SetStateAction<UnitOption[]>>;
|
||||
}
|
||||
|
||||
export function useInitialDataLoading({
|
||||
setCustomTabs,
|
||||
setUnitOptions,
|
||||
}: UseInitialDataLoadingProps): UseInitialDataLoadingReturn {
|
||||
const {
|
||||
loadItemPages,
|
||||
loadSectionTemplates,
|
||||
loadItemMasterFields,
|
||||
loadIndependentSections,
|
||||
loadIndependentFields,
|
||||
} = useItemMaster();
|
||||
|
||||
const [isInitialLoading, setIsInitialLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(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,
|
||||
};
|
||||
}
|
||||
@@ -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<void>;
|
||||
moveField: (selectedPage: ItemPage | null, sectionId: number, dragFieldId: number, hoverFieldId: number) => Promise<void>;
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user