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:
byeongcheolryu
2025-12-24 14:35:29 +09:00
parent 5ecb0c3925
commit a823ae0777
13 changed files with 2532 additions and 429 deletions

View File

@@ -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 분리 예정 |

View File

@@ -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` - 세션 컨텍스트

View File

@@ -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 문서 확인하고 시작해.
```

View 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. 최종 수동 테스트

View File

@@ -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 (

View File

@@ -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">
&quot;{masterField.field_name}&quot;
</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> &quot;{masterField.field_name}&quot; // .
</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"> &quot;&quot; </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>
);
}

View File

@@ -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
}
/>
</>
);
}

View File

@@ -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';

View File

@@ -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';

View File

@@ -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,
};
}

View File

@@ -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,
};
}

View File

@@ -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,
};
}

View File

@@ -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,
};
}