refactor: 품목기준관리 hooks 분리 및 다이얼로그 개선
- ItemMasterDataManagement 컴포넌트에서 hooks 분리 - 다이얼로그 컴포넌트들 타입 및 구조 개선 - BOMManagementSection 개선 - HierarchyTab 업데이트 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -12,13 +12,62 @@
|
||||
### 1.1 목적
|
||||
품목기준관리 화면에서 품목의 메타데이터(페이지, 섹션, 필드)를 동적으로 정의하기 위한 백엔드 API 개발 요청
|
||||
|
||||
### 1.2 프론트엔드 구현 현황
|
||||
### 1.2 핵심 개념 정의
|
||||
|
||||
#### "페이지"의 의미
|
||||
- **❌ 실제 라우팅 경로(URL path)가 아님**
|
||||
- **✅ 품목유형별 필드 구성 템플릿**
|
||||
|
||||
```
|
||||
품목기준관리의 "페이지" = 품목유형(FG, PT, SM, RM, CS)별 입력 필드 구성 정의
|
||||
|
||||
예시:
|
||||
├─ 페이지: "제품 등록" (item_type: FG)
|
||||
│ └─ 품목유형이 "제품(FG)"일 때 보여줄 섹션/필드 정의
|
||||
│
|
||||
└─ 페이지: "부품 등록" (item_type: PT)
|
||||
└─ 품목유형이 "부품(PT)"일 때 보여줄 섹션/필드 정의
|
||||
```
|
||||
|
||||
#### 실제 사용 흐름
|
||||
```
|
||||
[품목 등록 페이지] /items/create
|
||||
│
|
||||
├─ 사용자가 품목유형 선택 (예: "제품(FG)")
|
||||
│
|
||||
├─ API 호출: item_type='FG'인 페이지 설정 로드
|
||||
│
|
||||
└─ 해당 페이지에 정의된 섹션/필드로 입력 폼 동적 렌더링
|
||||
```
|
||||
|
||||
#### 멀티테넌트 연동 구조
|
||||
```
|
||||
Tenant A (회사 A)
|
||||
├─ 품목기준관리 설정
|
||||
│ ├─ 제품(FG): 섹션 3개, 필드 15개
|
||||
│ └─ 부품(PT): 섹션 2개, 필드 8개
|
||||
│
|
||||
└─ 품목관리 (/items)
|
||||
└─ 품목유형 선택 시 Tenant A의 설정으로 폼 렌더링
|
||||
|
||||
Tenant B (회사 B)
|
||||
├─ 품목기준관리 설정
|
||||
│ ├─ 제품(FG): 섹션 5개, 필드 25개 ← 다른 구성
|
||||
│ └─ 부품(PT): 섹션 1개, 필드 5개
|
||||
│
|
||||
└─ 품목관리 (/items)
|
||||
└─ 품목유형 선택 시 Tenant B의 설정으로 폼 렌더링
|
||||
```
|
||||
|
||||
**핵심**: 모든 테이블에 `tenant_id`가 있어 테넌트별 독립적인 설정 관리 가능
|
||||
|
||||
### 1.3 프론트엔드 구현 현황
|
||||
- 프론트엔드 UI 구현 완료
|
||||
- API 클라이언트 코드 작성 완료 (`src/lib/api/item-master.ts`)
|
||||
- 타입 정의 완료 (`src/types/item-master-api.ts`)
|
||||
- Next.js API 프록시 구조 적용 (HttpOnly 쿠키 인증)
|
||||
|
||||
### 1.3 API 기본 정보
|
||||
### 1.4 API 기본 정보
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| Base URL | `/api/v1/item-master` |
|
||||
@@ -26,7 +75,7 @@
|
||||
| Content-Type | `application/json` |
|
||||
| 응답 형식 | 표준 API 응답 래퍼 사용 |
|
||||
|
||||
### 1.4 표준 응답 형식
|
||||
### 1.5 표준 응답 형식
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
|
||||
116
claudedocs/[NEXT-2025-11-26] item-master-api-pending-tasks.md
Normal file
116
claudedocs/[NEXT-2025-11-26] item-master-api-pending-tasks.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# 품목기준관리 - API 대기 및 다음 작업
|
||||
|
||||
**작성일**: 2025-11-26
|
||||
**상태**: API 대기 중
|
||||
|
||||
---
|
||||
|
||||
## 1. 현재 대기 중인 API 작업
|
||||
|
||||
### 1.1 계층구조(페이지) 탭
|
||||
|
||||
| 기능 | 설명 | 상태 |
|
||||
|------|------|------|
|
||||
| 생성 데이터 연결 | 최하위 항목(필드)까지 공통으로 연결 | ⏳ 대기 |
|
||||
| 페이지 삭제 | 실제 삭제 (Soft Delete) | ⏳ 대기 |
|
||||
| 섹션 연결 끊기 | 삭제가 아닌 연결만 해제 (`page_id = null`) | ⏳ 대기 |
|
||||
| 항목 연결 끊기 | 삭제가 아닌 연결만 해제 | ⏳ 대기 |
|
||||
| 섹션 불러오기 | 기존 섹션 목록에서 선택하여 연결 | ⏳ 대기 |
|
||||
| 섹션 리스트 조회 | 연결 가능한 섹션 목록 표시 | ⏳ 대기 |
|
||||
| 항목 불러오기 | 기존 항목 목록에서 선택하여 연결 | ⏳ 대기 |
|
||||
| 항목 리스트 조회 | 연결 가능한 항목 목록 표시 | ⏳ 대기 |
|
||||
|
||||
### 1.2 섹션 탭
|
||||
|
||||
| 기능 | 설명 | 상태 |
|
||||
|------|------|------|
|
||||
| 항목 불러오기 | 마스터 항목에서 선택하여 추가 | ⏳ 대기 |
|
||||
| 항목 리스트 조회 | 추가 가능한 마스터 항목 목록 | ⏳ 대기 |
|
||||
|
||||
### 1.3 데이터 동기화
|
||||
|
||||
| 기능 | 설명 | 상태 |
|
||||
|------|------|------|
|
||||
| 개별 수정 시 연결된 데이터 동기화 | 마스터 항목 수정 → 연결된 모든 필드에 반영 | ⏳ 대기 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 삭제 vs 연결 끊기 정리
|
||||
|
||||
```
|
||||
[계층구조 탭에서]
|
||||
├─ 페이지 삭제 → 실제 삭제 (Soft Delete)
|
||||
├─ 섹션 제거 → 연결만 끊기 (page_id = null), 섹션 데이터는 유지
|
||||
└─ 항목 제거 → 연결만 끊기 (section_id = null), 항목 데이터는 유지
|
||||
|
||||
[섹션 탭에서]
|
||||
├─ 섹션 삭제 → 실제 삭제 (Soft Delete)
|
||||
└─ 항목 삭제 → 실제 삭제 (Soft Delete)
|
||||
|
||||
[마스터 항목 탭에서]
|
||||
└─ 마스터 항목 삭제 → 실제 삭제 (참조된 필드는 master_field_id = null)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 데이터 연결 구조
|
||||
|
||||
```
|
||||
마스터 항목 (master_fields)
|
||||
↓ 참조 (master_field_id)
|
||||
섹션 템플릿 항목 (template_fields) ←──┐
|
||||
↓ 복사 │
|
||||
섹션 내 항목 (fields) ───────────────┘
|
||||
↑ 소속 (section_id)
|
||||
섹션 (sections)
|
||||
↑ 소속 (page_id) - 연결/해제 가능
|
||||
페이지 (pages) = 품목유형별 필드 구성
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 작업 순서
|
||||
|
||||
### Step 1: 품목기준관리 API 연동 (현재 대기)
|
||||
- 위 1~3번 항목 API 연동
|
||||
- 품목기준관리 페이지 최종 완료
|
||||
|
||||
### Step 2: 품목관리 동적 렌더링 API 검토
|
||||
- 품목기준관리 완료 시점에 필요한 API 다시 검토
|
||||
- 추가 API 필요 여부 확인
|
||||
- `[API-2025-11-24] item-management-dynamic-api-spec.md` 업데이트
|
||||
|
||||
### Step 3: 품목관리 페이지 동적 렌더링 구현
|
||||
|
||||
```
|
||||
품목 등록 페이지 (/items/create)
|
||||
│
|
||||
├─ 품목유형 선택 (FG, PT, SM, RM, CS)
|
||||
│
|
||||
├─ GET /api/v1/item-master/pages?item_type={선택된유형}
|
||||
│
|
||||
└─ 응답받은 섹션/필드 구조로 동적 폼 생성
|
||||
```
|
||||
|
||||
### 4.2 참고 문서
|
||||
|
||||
- `claudedocs/[API-2025-11-25] item-master-data-management-api-request.md`
|
||||
- `claudedocs/[API-2025-11-24] item-management-dynamic-api-spec.md`
|
||||
- `src/types/item-master-api.ts`
|
||||
- `src/lib/api/item-master.ts`
|
||||
|
||||
---
|
||||
|
||||
## 5. 핵심 개념 (잊지 말 것!)
|
||||
|
||||
> **"페이지"는 실제 URL 경로가 아니라, 품목유형별 필드 구성 템플릿이다!**
|
||||
|
||||
```
|
||||
품목기준관리의 "페이지"
|
||||
= 품목유형(FG, PT, SM, RM, CS)별로
|
||||
= 품목 등록 시 어떤 섹션/필드를 보여줄지 정의하는 템플릿
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**다음 세션 시작 시**: 이 문서 확인 후 API 상태 체크하고 작업 진행
|
||||
427
claudedocs/[REF-2025-11-26] item-master-hooks-refactoring.md
Normal file
427
claudedocs/[REF-2025-11-26] item-master-hooks-refactoring.md
Normal file
@@ -0,0 +1,427 @@
|
||||
# ItemMasterDataManagement 훅 분리 리팩토링
|
||||
|
||||
## 개요
|
||||
- **목표**: 2000줄+ 컴포넌트를 도메인별 커스텀 훅으로 분리
|
||||
- **예상 결과**: 메인 컴포넌트 ~200줄로 축소 (90% 감소)
|
||||
- **시작일**: 2025-11-26
|
||||
|
||||
## 진행 상태
|
||||
|
||||
### Phase 1: 커스텀 훅 생성 ✅ 완료
|
||||
|
||||
| # | 훅 이름 | 상태 수 | 핸들러 수 | 상태 | 비고 |
|
||||
|---|---------|---------|----------|------|------|
|
||||
| 1 | usePageManagement | ~8 | 3 | [x] 완료 | 가장 단순, 먼저 시작 |
|
||||
| 2 | useSectionManagement | ~10 | 5 | [x] 완료 | Page에 의존 |
|
||||
| 3 | useFieldManagement | ~15 | 4 | [x] 완료 | Section에 의존 |
|
||||
| 4 | useMasterFieldManagement | ~12 | 4 | [x] 완료 | 독립적 |
|
||||
| 5 | useTemplateManagement | ~18 | 11 | [x] 완료 | 가장 복잡 (BOM 포함) |
|
||||
| 6 | useAttributeManagement | ~15 | 6 | [x] 완료 | 옵션/칼럼 관리 |
|
||||
| 7 | useTabManagement | ~12 | 14 | [x] 완료 | 탭 상태 관리 |
|
||||
|
||||
### Phase 2: 메인 컴포넌트 정리 ✅ 완료
|
||||
|
||||
| # | 작업 | 상태 | 비고 |
|
||||
|---|------|------|------|
|
||||
| 2.1 | 훅 import 추가 | [x] 완료 | 7개 훅 import |
|
||||
| 2.2 | 훅 초기화 코드 추가 | [x] 완료 | 컴포넌트 상단에 훅 호출 |
|
||||
| 2.3 | useState 제거 - 페이지 관련 | [x] 완료 | usePageManagement로 대체 |
|
||||
| 2.4 | useState 제거 - 섹션 관련 | [x] 완료 | useSectionManagement로 대체 |
|
||||
| 2.5 | useState 제거 - 필드 관련 | [x] 완료 | useFieldManagement로 대체 |
|
||||
| 2.6 | useState 제거 - 마스터필드 관련 | [x] 완료 | useMasterFieldManagement로 대체 |
|
||||
| 2.7 | useState 제거 - 템플릿 관련 | [x] 완료 | useTemplateManagement로 대체 |
|
||||
| 2.8 | useState 제거 - 속성 관련 | [x] 완료 | useAttributeManagement로 대체 |
|
||||
| 2.9 | useState 제거 - 탭 관련 | [x] 완료 | useTabManagement로 대체 |
|
||||
| 2.10 | 핸들러 함수 제거 | [x] 완료 | 훅에서 가져온 핸들러로 대체 |
|
||||
| 2.11 | useEffect 제거 | [x] 완료 | 훅 내부로 이동된 것들 |
|
||||
| 2.12 | 하위 컴포넌트 props 연결 | [x] 완료 | 다이얼로그/탭 컴포넌트에 훅 값 전달 + wrapper 함수 |
|
||||
| 2.13 | 타입 체크 | [x] 완료 | 기존 타입 에러만 남음 (훅과 무관) |
|
||||
| 2.14 | 빌드 테스트 | [x] 완료 | npm run build 성공! |
|
||||
| 2.15 | 기능 테스트 | [ ] 대기 | 브라우저에서 동작 확인 필요 |
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 상세 가이드
|
||||
|
||||
### 2.1 훅 Import 추가
|
||||
```typescript
|
||||
// ItemMasterDataManagement.tsx 상단에 추가
|
||||
import {
|
||||
usePageManagement,
|
||||
useSectionManagement,
|
||||
useFieldManagement,
|
||||
useMasterFieldManagement,
|
||||
useTemplateManagement,
|
||||
useAttributeManagement,
|
||||
useTabManagement,
|
||||
} from './ItemMasterDataManagement/hooks';
|
||||
```
|
||||
|
||||
### 2.2 훅 초기화 코드
|
||||
```typescript
|
||||
// 컴포넌트 함수 내부 상단에 추가
|
||||
export function ItemMasterDataManagement() {
|
||||
const pageManagement = usePageManagement();
|
||||
const sectionManagement = useSectionManagement();
|
||||
const fieldManagement = useFieldManagement();
|
||||
const masterFieldManagement = useMasterFieldManagement();
|
||||
const templateManagement = useTemplateManagement();
|
||||
const attributeManagement = useAttributeManagement();
|
||||
const tabManagement = useTabManagement();
|
||||
|
||||
// ... 기존 코드
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3~2.9 useState 제거 패턴
|
||||
**변경 전:**
|
||||
```typescript
|
||||
const [selectedPageId, setSelectedPageId] = useState<number | null>(null);
|
||||
```
|
||||
|
||||
**변경 후:**
|
||||
```typescript
|
||||
const { selectedPageId, setSelectedPageId } = pageManagement;
|
||||
// 또는 구조분해 없이 pageManagement.selectedPageId 사용
|
||||
```
|
||||
|
||||
### 2.10 핸들러 교체 패턴
|
||||
**변경 전:**
|
||||
```typescript
|
||||
const handleAddPage = async () => {
|
||||
// 100줄의 핸들러 코드
|
||||
};
|
||||
```
|
||||
|
||||
**변경 후:**
|
||||
```typescript
|
||||
// 삭제하고, pageManagement.handleAddPage 사용
|
||||
// 또는 상단에서 구조분해
|
||||
const { handleAddPage } = pageManagement;
|
||||
```
|
||||
|
||||
### 2.12 하위 컴포넌트 Props 연결 예시
|
||||
```typescript
|
||||
<PageDialog
|
||||
isOpen={pageManagement.isPageDialogOpen}
|
||||
onClose={() => pageManagement.setIsPageDialogOpen(false)}
|
||||
onSubmit={pageManagement.handleAddPage}
|
||||
pageName={pageManagement.newPageName}
|
||||
setPageName={pageManagement.setNewPageName}
|
||||
// ...
|
||||
/>
|
||||
```
|
||||
|
||||
### Phase 2 완료 기준 (실제 결과)
|
||||
- [x] 메인 컴포넌트 줄 수: 2893줄 → 1575줄 (45% 감소) *목표는 90%였으나 렌더링 로직이 많아 45% 달성*
|
||||
- [x] 모든 useState가 훅으로 이동 (핵심 상태 ~100개)
|
||||
- [x] 모든 핸들러가 훅에서 제공 (~50개 핸들러)
|
||||
- [x] 타입 에러: 훅 관련 새 에러 없음 (기존 에러만 존재)
|
||||
- [x] 빌드 성공
|
||||
- [ ] 기능 동작 정상 (브라우저 테스트 대기)
|
||||
|
||||
---
|
||||
|
||||
## 상세 작업 내역
|
||||
|
||||
### 1. usePageManagement
|
||||
|
||||
**포함될 상태:**
|
||||
```typescript
|
||||
- selectedPageId
|
||||
- editingPageId
|
||||
- editingPageName
|
||||
- isPageDialogOpen
|
||||
- newPageName
|
||||
- newPageItemType
|
||||
- editingPathPageId
|
||||
- editingAbsolutePath
|
||||
```
|
||||
|
||||
**포함될 핸들러:**
|
||||
```typescript
|
||||
- handleAddPage
|
||||
- handleDuplicatePage
|
||||
- handleDeletePageWithTracking
|
||||
```
|
||||
|
||||
**작업 체크리스트:**
|
||||
- [ ] hooks/usePageManagement.ts 파일 생성
|
||||
- [ ] 상태 이동
|
||||
- [ ] 핸들러 이동
|
||||
- [ ] 메인 컴포넌트에서 훅 사용으로 변경
|
||||
- [ ] 기능 테스트
|
||||
|
||||
---
|
||||
|
||||
### 2. useSectionManagement
|
||||
|
||||
**포함될 상태:**
|
||||
```typescript
|
||||
- editingSectionId
|
||||
- editingSectionTitle
|
||||
- isSectionDialogOpen
|
||||
- newSectionTitle
|
||||
- newSectionDescription
|
||||
- newSectionType
|
||||
- sectionInputMode
|
||||
- selectedSectionTemplateId
|
||||
- expandedSections
|
||||
```
|
||||
|
||||
**포함될 핸들러:**
|
||||
```typescript
|
||||
- handleAddSection
|
||||
- handleLinkTemplate
|
||||
- handleEditSectionTitle
|
||||
- handleSaveSectionTitle
|
||||
- handleDeleteSectionWithTracking
|
||||
```
|
||||
|
||||
**작업 체크리스트:**
|
||||
- [ ] hooks/useSectionManagement.ts 파일 생성
|
||||
- [ ] 상태 이동
|
||||
- [ ] 핸들러 이동
|
||||
- [ ] 메인 컴포넌트에서 훅 사용으로 변경
|
||||
- [ ] 기능 테스트
|
||||
|
||||
---
|
||||
|
||||
### 3. useFieldManagement
|
||||
|
||||
**포함될 상태:**
|
||||
```typescript
|
||||
- isFieldDialogOpen
|
||||
- selectedSectionForField
|
||||
- editingFieldId
|
||||
- fieldInputMode
|
||||
- showMasterFieldList
|
||||
- selectedMasterFieldId
|
||||
- newFieldName, newFieldKey, newFieldInputType, newFieldRequired
|
||||
- newFieldOptions, newFieldDescription
|
||||
- textboxColumns, isColumnDialogOpen, editingColumnId, columnName, columnKey
|
||||
- newFieldConditionEnabled, newFieldConditionTargetType
|
||||
- newFieldConditionFields, newFieldConditionSections
|
||||
- tempConditionFieldKey, tempConditionValue
|
||||
```
|
||||
|
||||
**포함될 핸들러:**
|
||||
```typescript
|
||||
- handleAddField
|
||||
- handleEditField
|
||||
- handleDeleteFieldWithTracking
|
||||
- (useEffect for masterField selection)
|
||||
```
|
||||
|
||||
**작업 체크리스트:**
|
||||
- [ ] hooks/useFieldManagement.ts 파일 생성
|
||||
- [ ] 상태 이동
|
||||
- [ ] 핸들러 이동
|
||||
- [ ] 메인 컴포넌트에서 훅 사용으로 변경
|
||||
- [ ] 기능 테스트
|
||||
|
||||
---
|
||||
|
||||
### 4. useMasterFieldManagement
|
||||
|
||||
**포함될 상태:**
|
||||
```typescript
|
||||
- isMasterFieldDialogOpen
|
||||
- editingMasterFieldId
|
||||
- newMasterFieldName, newMasterFieldKey, newMasterFieldInputType
|
||||
- newMasterFieldRequired, newMasterFieldCategory, newMasterFieldDescription
|
||||
- newMasterFieldOptions, newMasterFieldAttributeType
|
||||
- newMasterFieldMultiColumn, newMasterFieldColumnCount, newMasterFieldColumnNames
|
||||
```
|
||||
|
||||
**포함될 핸들러:**
|
||||
```typescript
|
||||
- handleAddMasterField
|
||||
- handleEditMasterField
|
||||
- handleUpdateMasterField
|
||||
- handleDeleteMasterField
|
||||
```
|
||||
|
||||
**작업 체크리스트:**
|
||||
- [ ] hooks/useMasterFieldManagement.ts 파일 생성
|
||||
- [ ] 상태 이동
|
||||
- [ ] 핸들러 이동
|
||||
- [ ] 메인 컴포넌트에서 훅 사용으로 변경
|
||||
- [ ] 기능 테스트
|
||||
|
||||
---
|
||||
|
||||
### 5. useTemplateManagement
|
||||
|
||||
**포함될 상태:**
|
||||
```typescript
|
||||
- isSectionTemplateDialogOpen
|
||||
- editingSectionTemplateId
|
||||
- newSectionTemplateTitle, newSectionTemplateDescription
|
||||
- newSectionTemplateCategory, newSectionTemplateType
|
||||
- isLoadTemplateDialogOpen, selectedTemplateId
|
||||
- expandedTemplateId
|
||||
- isTemplateFieldDialogOpen, currentTemplateId, editingTemplateFieldId
|
||||
- templateFieldName, templateFieldKey, templateFieldInputType
|
||||
- templateFieldRequired, templateFieldOptions, templateFieldDescription
|
||||
- templateFieldMultiColumn, templateFieldColumnCount, templateFieldColumnNames
|
||||
- templateFieldInputMode, templateFieldShowMasterFieldList, templateFieldSelectedMasterFieldId
|
||||
```
|
||||
|
||||
**포함될 핸들러:**
|
||||
```typescript
|
||||
- handleAddSectionTemplate
|
||||
- handleEditSectionTemplate
|
||||
- handleUpdateSectionTemplate
|
||||
- handleDeleteSectionTemplate
|
||||
- handleLoadTemplate
|
||||
- handleAddTemplateField
|
||||
- handleEditTemplateField
|
||||
- handleDeleteTemplateField
|
||||
- handleAddBOMItemToTemplate
|
||||
- handleUpdateBOMItemInTemplate
|
||||
- handleDeleteBOMItemFromTemplate
|
||||
```
|
||||
|
||||
**작업 체크리스트:**
|
||||
- [ ] hooks/useTemplateManagement.ts 파일 생성
|
||||
- [ ] 상태 이동
|
||||
- [ ] 핸들러 이동
|
||||
- [ ] 메인 컴포넌트에서 훅 사용으로 변경
|
||||
- [ ] 기능 테스트
|
||||
|
||||
---
|
||||
|
||||
### 6. useAttributeManagement
|
||||
|
||||
**포함될 상태:**
|
||||
```typescript
|
||||
- unitOptions, materialOptions, surfaceTreatmentOptions
|
||||
- customAttributeOptions
|
||||
- isOptionDialogOpen, editingOptionType
|
||||
- newOptionValue, newOptionLabel, newOptionColumnValues
|
||||
- newOptionInputType, newOptionRequired, newOptionOptions
|
||||
- newOptionPlaceholder, newOptionDefaultValue
|
||||
- isColumnManageDialogOpen, managingColumnType
|
||||
- attributeColumns
|
||||
- newColumnName, newColumnKey, newColumnType, newColumnRequired
|
||||
```
|
||||
|
||||
**포함될 핸들러:**
|
||||
```typescript
|
||||
- handleAddOption
|
||||
- handleDeleteOption
|
||||
- (useEffect for attribute sync)
|
||||
```
|
||||
|
||||
**작업 체크리스트:**
|
||||
- [ ] hooks/useAttributeManagement.ts 파일 생성
|
||||
- [ ] 상태 이동
|
||||
- [ ] 핸들러 이동
|
||||
- [ ] 메인 컴포넌트에서 훅 사용으로 변경
|
||||
- [ ] 기능 테스트
|
||||
|
||||
---
|
||||
|
||||
### 7. useTabManagement
|
||||
|
||||
**포함될 상태:**
|
||||
```typescript
|
||||
- customTabs
|
||||
- activeTab
|
||||
- attributeSubTabs, activeAttributeTab
|
||||
- isAddTabDialogOpen, isManageTabsDialogOpen
|
||||
- newTabLabel, editingTabId, deletingTabId, isDeleteTabDialogOpen
|
||||
- isManageAttributeTabsDialogOpen, isAddAttributeTabDialogOpen
|
||||
- newAttributeTabLabel, editingAttributeTabId
|
||||
- deletingAttributeTabId, isDeleteAttributeTabDialogOpen
|
||||
```
|
||||
|
||||
**포함될 핸들러:**
|
||||
```typescript
|
||||
- handleAddTab
|
||||
- handleEditTab
|
||||
- handleDeleteTab
|
||||
- handleAddAttributeTab
|
||||
- (useEffect for attributeSubTabs sync)
|
||||
```
|
||||
|
||||
**작업 체크리스트:**
|
||||
- [ ] hooks/useTabManagement.ts 파일 생성
|
||||
- [ ] 상태 이동
|
||||
- [ ] 핸들러 이동
|
||||
- [ ] 메인 컴포넌트에서 훅 사용으로 변경
|
||||
- [ ] 기능 테스트
|
||||
|
||||
---
|
||||
|
||||
## 작업 로그
|
||||
|
||||
### 2025-11-26
|
||||
- 체크리스트 문서 생성
|
||||
- 분석 완료: 총 7개 훅으로 분리 예정
|
||||
- ✅ usePageManagement.ts 생성 완료 (타입 에러 없음)
|
||||
- ✅ useSectionManagement.ts 생성 완료 (타입 에러 없음)
|
||||
- ✅ useFieldManagement.ts 생성 완료 (타입 에러 수정 완료)
|
||||
- ✅ useMasterFieldManagement.ts 생성 완료 (타입 에러 수정: 'ㄹ' 오타 제거)
|
||||
- ✅ useTemplateManagement.ts 생성 완료 (타입 에러 없음, ~350줄)
|
||||
- ✅ useAttributeManagement.ts 생성 완료 (타입 에러 없음, ~250줄)
|
||||
- ✅ useTabManagement.ts 생성 완료 (타입 에러 없음, ~330줄)
|
||||
- 🎉 **Phase 1 완료!** 모든 7개 훅 생성 완료
|
||||
- 📁 hooks/index.ts에 모든 훅 export 완료
|
||||
|
||||
### Phase 2 완료 (2025-11-26 추가)
|
||||
- ✅ 메인 컴포넌트에서 훅 import 및 연결 완료
|
||||
- ✅ props drilling 정리 - wrapper 함수 패턴 적용
|
||||
- ✅ 렌더링 로직만 남기기 - 2893줄 → 1575줄 (45% 감소)
|
||||
- ✅ 빌드 테스트 성공
|
||||
- ⏳ 기능 테스트 - 브라우저에서 확인 필요
|
||||
|
||||
### 주요 기술 결정 (Phase 2)
|
||||
1. **Wrapper 함수 패턴**: 훅 함수에 selectedPage 바인딩 필요한 경우 wrapper 사용
|
||||
- `handleAddSectionWrapper`, `handleLinkTemplateWrapper` 등
|
||||
2. **타입 호환성**: setter 함수 타입 불일치는 `as any`로 우회
|
||||
- `setNewSectionTypeWrapper`, `setNewPageItemTypeWrapper`
|
||||
3. **기존 타입 에러**: `default_properties` 등 기존 타입 정의 문제는 Phase 2 범위 외
|
||||
|
||||
### 다음 단계
|
||||
- [ ] 기능 테스트 (브라우저)
|
||||
- [ ] 추가 최적화 (선택)
|
||||
|
||||
---
|
||||
|
||||
## 참고 사항
|
||||
|
||||
### 훅 간 의존성
|
||||
```
|
||||
usePageManagement (독립)
|
||||
↓
|
||||
useSectionManagement (selectedPageId 필요)
|
||||
↓
|
||||
useFieldManagement (selectedSectionId 필요)
|
||||
|
||||
useMasterFieldManagement (독립)
|
||||
useTemplateManagement (독립, but masterFields 참조)
|
||||
useAttributeManagement (독립, but masterFields와 연동)
|
||||
useTabManagement (독립)
|
||||
```
|
||||
|
||||
### 파일 구조 (목표)
|
||||
```
|
||||
ItemMasterDataManagement/
|
||||
├── hooks/
|
||||
│ ├── index.ts
|
||||
│ ├── usePageManagement.ts
|
||||
│ ├── useSectionManagement.ts
|
||||
│ ├── useFieldManagement.ts
|
||||
│ ├── useMasterFieldManagement.ts
|
||||
│ ├── useTemplateManagement.ts
|
||||
│ ├── useAttributeManagement.ts
|
||||
│ └── useTabManagement.ts
|
||||
├── tabs/
|
||||
├── dialogs/
|
||||
├── components/
|
||||
├── utils/
|
||||
├── types.ts
|
||||
└── index.tsx (메인 컴포넌트, ~200줄 목표)
|
||||
```
|
||||
@@ -41,6 +41,13 @@ export function BOMManagementSection({
|
||||
const [unit, setUnit] = useState('EA');
|
||||
const [itemType, setItemType] = useState('part');
|
||||
const [note, setNote] = useState('');
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
// 유효성 검사
|
||||
const isItemCodeEmpty = !itemCode.trim();
|
||||
const isItemNameEmpty = !itemName.trim();
|
||||
const qty = parseFloat(quantity);
|
||||
const isQuantityInvalid = isNaN(qty) || qty <= 0;
|
||||
|
||||
const handleOpenDialog = (item?: BOMItem) => {
|
||||
if (item) {
|
||||
@@ -61,16 +68,25 @@ export function BOMManagementSection({
|
||||
setNote('');
|
||||
}
|
||||
setIsDialogOpen(true);
|
||||
setIsSubmitted(false);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setIsDialogOpen(false);
|
||||
setEditingId(null);
|
||||
setItemCode('');
|
||||
setItemName('');
|
||||
setQuantity('1');
|
||||
setUnit('EA');
|
||||
setItemType('part');
|
||||
setNote('');
|
||||
setIsSubmitted(false);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!itemCode.trim() || !itemName.trim()) {
|
||||
return toast.error('품목코드와 품목명을 입력해주세요');
|
||||
}
|
||||
|
||||
const qty = parseFloat(quantity);
|
||||
if (isNaN(qty) || qty <= 0) {
|
||||
return toast.error('올바른 수량을 입력해주세요');
|
||||
setIsSubmitted(true);
|
||||
if (isItemCodeEmpty || isItemNameEmpty || isQuantityInvalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemData = {
|
||||
@@ -89,7 +105,7 @@ export function BOMManagementSection({
|
||||
toast.success('BOM 품목이 추가되었습니다');
|
||||
}
|
||||
|
||||
setIsDialogOpen(false);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
@@ -170,10 +186,7 @@ export function BOMManagementSection({
|
||||
<Dialog
|
||||
open={isDialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
setIsDialogOpen(open);
|
||||
if (!open) {
|
||||
setEditingId(null);
|
||||
}
|
||||
if (!open) handleClose();
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-w-2xl">
|
||||
@@ -191,7 +204,11 @@ export function BOMManagementSection({
|
||||
value={itemCode}
|
||||
onChange={(e) => setItemCode(e.target.value)}
|
||||
placeholder="예: PART-001"
|
||||
className={isSubmitted && isItemCodeEmpty ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
||||
/>
|
||||
{isSubmitted && isItemCodeEmpty && (
|
||||
<p className="text-xs text-red-500 mt-1">품목코드를 입력해주세요</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Label>품목명 *</Label>
|
||||
@@ -199,7 +216,11 @@ export function BOMManagementSection({
|
||||
value={itemName}
|
||||
onChange={(e) => setItemName(e.target.value)}
|
||||
placeholder="예: 샤프트"
|
||||
className={isSubmitted && isItemNameEmpty ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
||||
/>
|
||||
{isSubmitted && isItemNameEmpty && (
|
||||
<p className="text-xs text-red-500 mt-1">품목명을 입력해주세요</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -213,7 +234,11 @@ export function BOMManagementSection({
|
||||
placeholder="1"
|
||||
min="0"
|
||||
step="0.01"
|
||||
className={isSubmitted && isQuantityInvalid ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
||||
/>
|
||||
{isSubmitted && isQuantityInvalid && (
|
||||
<p className="text-xs text-red-500 mt-1">올바른 수량을 입력해주세요 (0보다 큰 숫자)</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Label>단위 *</Label>
|
||||
@@ -257,9 +282,7 @@ export function BOMManagementSection({
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleClose}>취소</Button>
|
||||
<Button onClick={handleSave}>저장</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -31,9 +32,24 @@ export function ColumnDialog({
|
||||
textboxColumns,
|
||||
setTextboxColumns,
|
||||
}: ColumnDialogProps) {
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
// 유효성 검사
|
||||
const isNameEmpty = !columnName.trim();
|
||||
const isKeyEmpty = !columnKey.trim();
|
||||
|
||||
const handleClose = () => {
|
||||
setIsColumnDialogOpen(false);
|
||||
setEditingColumnId(null);
|
||||
setColumnName('');
|
||||
setColumnKey('');
|
||||
setIsSubmitted(false);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!columnName.trim() || !columnKey.trim()) {
|
||||
return toast.error('모든 필드를 입력해주세요');
|
||||
setIsSubmitted(true);
|
||||
if (isNameEmpty || isKeyEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (editingColumnId) {
|
||||
@@ -54,20 +70,14 @@ export function ColumnDialog({
|
||||
toast.success('컬럼이 추가되었습니다');
|
||||
}
|
||||
|
||||
setIsColumnDialogOpen(false);
|
||||
setEditingColumnId(null);
|
||||
setColumnName('');
|
||||
setColumnKey('');
|
||||
handleClose();
|
||||
setIsSubmitted(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isColumnDialogOpen} onOpenChange={(open) => {
|
||||
setIsColumnDialogOpen(open);
|
||||
if (!open) {
|
||||
setEditingColumnId(null);
|
||||
setColumnName('');
|
||||
setColumnKey('');
|
||||
}
|
||||
if (!open) handleClose();
|
||||
else setIsColumnDialogOpen(open);
|
||||
}}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
@@ -83,7 +93,11 @@ export function ColumnDialog({
|
||||
value={columnName}
|
||||
onChange={(e) => setColumnName(e.target.value)}
|
||||
placeholder="예: 가로"
|
||||
className={isSubmitted && isNameEmpty ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
||||
/>
|
||||
{isSubmitted && isNameEmpty && (
|
||||
<p className="text-xs text-red-500 mt-1">컬럼명을 입력해주세요</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Label>컬럼 키 *</Label>
|
||||
@@ -91,11 +105,15 @@ export function ColumnDialog({
|
||||
value={columnKey}
|
||||
onChange={(e) => setColumnKey(e.target.value)}
|
||||
placeholder="예: width"
|
||||
className={isSubmitted && isKeyEmpty ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
||||
/>
|
||||
{isSubmitted && isKeyEmpty && (
|
||||
<p className="text-xs text-red-500 mt-1">컬럼 키를 입력해주세요</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsColumnDialogOpen(false)}>취소</Button>
|
||||
<Button variant="outline" onClick={handleClose}>취소</Button>
|
||||
<Button onClick={handleSubmit}>
|
||||
{editingColumnId ? '수정' : '추가'}
|
||||
</Button>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -121,7 +122,14 @@ export function FieldDialog({
|
||||
setColumnName,
|
||||
setColumnKey,
|
||||
}: FieldDialogProps) {
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
// 유효성 검사
|
||||
const isNameEmpty = !newFieldName.trim();
|
||||
const isKeyEmpty = !newFieldKey.trim();
|
||||
|
||||
const handleClose = () => {
|
||||
setIsSubmitted(false);
|
||||
onOpenChange(false);
|
||||
setEditingFieldId(null);
|
||||
setFieldInputMode('custom');
|
||||
@@ -268,7 +276,11 @@ export function FieldDialog({
|
||||
value={newFieldName}
|
||||
onChange={(e) => setNewFieldName(e.target.value)}
|
||||
placeholder="예: 품목명"
|
||||
className={isSubmitted && isNameEmpty ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
||||
/>
|
||||
{isSubmitted && isNameEmpty && (
|
||||
<p className="text-xs text-red-500 mt-1">항목명을 입력해주세요</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Label>필드 키 *</Label>
|
||||
@@ -276,7 +288,11 @@ export function FieldDialog({
|
||||
value={newFieldKey}
|
||||
onChange={(e) => setNewFieldKey(e.target.value)}
|
||||
placeholder="예: itemName"
|
||||
className={isSubmitted && isKeyEmpty ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
||||
/>
|
||||
{isSubmitted && isKeyEmpty && (
|
||||
<p className="text-xs text-red-500 mt-1">필드 키를 입력해주세요</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -402,7 +418,12 @@ export function FieldDialog({
|
||||
</div>
|
||||
<DialogFooter className="shrink-0 bg-white z-10 px-6 py-4 border-t">
|
||||
<Button variant="outline" onClick={handleClose}>취소</Button>
|
||||
<Button onClick={handleAddField}>저장</Button>
|
||||
<Button onClick={() => {
|
||||
setIsSubmitted(true);
|
||||
if ((fieldInputMode === 'custom' || editingFieldId) && (isNameEmpty || isKeyEmpty)) return;
|
||||
handleAddField();
|
||||
setIsSubmitted(false);
|
||||
}}>저장</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -189,21 +189,22 @@ export function FieldDrawer({
|
||||
<div
|
||||
key={field.id}
|
||||
className={`p-3 border rounded cursor-pointer transition-colors ${
|
||||
selectedMasterFieldId === field.id
|
||||
selectedMasterFieldId === String(field.id)
|
||||
? 'bg-blue-50 border-blue-300'
|
||||
: 'hover:bg-gray-50'
|
||||
}`}
|
||||
onClick={() => {
|
||||
setSelectedMasterFieldId(field.id);
|
||||
setNewFieldName(field.name);
|
||||
setNewFieldKey(field.fieldKey);
|
||||
setNewFieldInputType(field.property.inputType);
|
||||
setNewFieldRequired(field.property.required);
|
||||
setSelectedMasterFieldId(String(field.id));
|
||||
setNewFieldName(field.field_name);
|
||||
setNewFieldKey(`field_${field.id}`);
|
||||
setNewFieldInputType(field.field_type);
|
||||
setNewFieldRequired((field.properties as any)?.required ?? false);
|
||||
setNewFieldDescription(field.description || '');
|
||||
setNewFieldOptions(field.property.options?.join(', ') || '');
|
||||
if (field.property.multiColumn && field.property.columnNames) {
|
||||
setNewFieldOptions(field.options?.map(o => o.value).join(', ') || '');
|
||||
const props = field.properties as any;
|
||||
if (props?.multiColumn && props?.columnNames) {
|
||||
setTextboxColumns(
|
||||
field.property.columnNames.map((name, idx) => ({
|
||||
props.columnNames.map((name: string, idx: number) => ({
|
||||
id: `col-${idx}`,
|
||||
name,
|
||||
key: `column${idx + 1}`
|
||||
@@ -215,28 +216,26 @@ export function FieldDrawer({
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{field.name}</span>
|
||||
<span className="font-medium">{field.field_name}</span>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{INPUT_TYPE_OPTIONS.find(o => o.value === field.property.inputType)?.label}
|
||||
{INPUT_TYPE_OPTIONS.find(o => o.value === field.field_type)?.label}
|
||||
</Badge>
|
||||
{field.property.required && (
|
||||
{(field.properties as any)?.required && (
|
||||
<Badge variant="destructive" className="text-xs">필수</Badge>
|
||||
)}
|
||||
</div>
|
||||
{field.description && (
|
||||
<p className="text-xs text-muted-foreground mt-1">{field.description}</p>
|
||||
)}
|
||||
{Array.isArray(field.category) && field.category.length > 0 && (
|
||||
{field.category && (
|
||||
<div className="flex gap-1 mt-1">
|
||||
{field.category.map((cat, idx) => (
|
||||
<Badge key={idx} variant="secondary" className="text-xs">
|
||||
{cat}
|
||||
</Badge>
|
||||
))}
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{field.category}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{selectedMasterFieldId === field.id && (
|
||||
{selectedMasterFieldId === String(field.id) && (
|
||||
<Check className="h-5 w-5 text-blue-600" />
|
||||
)}
|
||||
</div>
|
||||
@@ -585,22 +584,23 @@ export function FieldDrawer({
|
||||
</p>
|
||||
<div className="space-y-2 max-h-60 overflow-y-auto">
|
||||
{selectedPage.sections
|
||||
.filter(section => section.type !== 'bom')
|
||||
.filter(section => section.section_type !== 'BOM')
|
||||
.map(section => (
|
||||
<label key={section.id} className="flex items-center gap-2 p-2 bg-muted rounded cursor-pointer hover:bg-muted/80">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={newFieldConditionSections.includes(section.id)}
|
||||
checked={newFieldConditionSections.includes(String(section.id))}
|
||||
onChange={(e) => {
|
||||
const sectionIdStr = String(section.id);
|
||||
if (e.target.checked) {
|
||||
setNewFieldConditionSections(prev => [...prev, section.id]);
|
||||
setNewFieldConditionSections(prev => [...prev, sectionIdStr]);
|
||||
} else {
|
||||
setNewFieldConditionSections(prev => prev.filter(id => id !== section.id));
|
||||
setNewFieldConditionSections(prev => prev.filter(id => id !== sectionIdStr));
|
||||
}
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
<span className="flex-1 text-sm">{section.title}</span>
|
||||
<span className="flex-1 text-sm">{section.section_name}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -79,23 +80,45 @@ export function MasterFieldDialog({
|
||||
handleUpdateMasterField,
|
||||
handleAddMasterField,
|
||||
}: MasterFieldDialogProps) {
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
// 유효성 검사
|
||||
const isNameEmpty = !newMasterFieldName.trim();
|
||||
const isKeyEmpty = !newMasterFieldKey.trim();
|
||||
|
||||
const handleClose = () => {
|
||||
setIsMasterFieldDialogOpen(false);
|
||||
setEditingMasterFieldId(null);
|
||||
setNewMasterFieldName('');
|
||||
setNewMasterFieldKey('');
|
||||
setNewMasterFieldInputType('textbox');
|
||||
setNewMasterFieldRequired(false);
|
||||
setNewMasterFieldCategory('공통');
|
||||
setNewMasterFieldDescription('');
|
||||
setNewMasterFieldOptions('');
|
||||
setNewMasterFieldAttributeType('custom');
|
||||
setNewMasterFieldMultiColumn(false);
|
||||
setNewMasterFieldColumnCount(2);
|
||||
setNewMasterFieldColumnNames(['컬럼1', '컬럼2']);
|
||||
setIsSubmitted(false);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
setIsSubmitted(true);
|
||||
if (!isNameEmpty && !isKeyEmpty) {
|
||||
if (editingMasterFieldId) {
|
||||
handleUpdateMasterField();
|
||||
} else {
|
||||
handleAddMasterField();
|
||||
}
|
||||
setIsSubmitted(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isMasterFieldDialogOpen} onOpenChange={(open) => {
|
||||
setIsMasterFieldDialogOpen(open);
|
||||
if (!open) {
|
||||
setEditingMasterFieldId(null);
|
||||
setNewMasterFieldName('');
|
||||
setNewMasterFieldKey('');
|
||||
setNewMasterFieldInputType('textbox');
|
||||
setNewMasterFieldRequired(false);
|
||||
setNewMasterFieldCategory('공통');
|
||||
setNewMasterFieldDescription('');
|
||||
setNewMasterFieldOptions('');
|
||||
setNewMasterFieldAttributeType('custom');
|
||||
setNewMasterFieldMultiColumn(false);
|
||||
setNewMasterFieldColumnCount(2);
|
||||
setNewMasterFieldColumnNames(['컬럼1', '컬럼2']);
|
||||
}
|
||||
if (!open) handleClose();
|
||||
else setIsMasterFieldDialogOpen(open);
|
||||
}}>
|
||||
<DialogContent className="max-w-[95vw] sm:max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
@@ -112,7 +135,11 @@ export function MasterFieldDialog({
|
||||
value={newMasterFieldName}
|
||||
onChange={(e) => setNewMasterFieldName(e.target.value)}
|
||||
placeholder="예: 품목명"
|
||||
className={isSubmitted && isNameEmpty ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
||||
/>
|
||||
{isSubmitted && isNameEmpty && (
|
||||
<p className="text-xs text-red-500 mt-1">항목명을 입력해주세요</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Label>필드 키 *</Label>
|
||||
@@ -120,7 +147,11 @@ export function MasterFieldDialog({
|
||||
value={newMasterFieldKey}
|
||||
onChange={(e) => setNewMasterFieldKey(e.target.value)}
|
||||
placeholder="예: itemName"
|
||||
className={isSubmitted && isKeyEmpty ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
||||
/>
|
||||
{isSubmitted && isKeyEmpty && (
|
||||
<p className="text-xs text-red-500 mt-1">필드 키를 입력해주세요</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -251,8 +282,8 @@ export function MasterFieldDialog({
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsMasterFieldDialogOpen(false)}>취소</Button>
|
||||
<Button onClick={editingMasterFieldId ? handleUpdateMasterField : handleAddMasterField}>
|
||||
<Button variant="outline" onClick={handleClose}>취소</Button>
|
||||
<Button onClick={handleSubmit}>
|
||||
{editingMasterFieldId ? '수정' : '추가'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -72,19 +73,38 @@ export function OptionDialog({
|
||||
attributeColumns,
|
||||
handleAddOption,
|
||||
}: OptionDialogProps) {
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
// 유효성 검사
|
||||
const isValueEmpty = !newOptionValue.trim();
|
||||
const isLabelEmpty = !newOptionLabel.trim();
|
||||
const isDropdownOptionsEmpty = newOptionInputType === 'dropdown' && !newOptionOptions.trim();
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false);
|
||||
setNewOptionValue('');
|
||||
setNewOptionLabel('');
|
||||
setNewOptionColumnValues({});
|
||||
setNewOptionInputType('textbox');
|
||||
setNewOptionRequired(false);
|
||||
setNewOptionOptions('');
|
||||
setNewOptionPlaceholder('');
|
||||
setNewOptionDefaultValue('');
|
||||
setIsSubmitted(false);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
setIsSubmitted(true);
|
||||
if (!isValueEmpty && !isLabelEmpty && !isDropdownOptionsEmpty) {
|
||||
handleAddOption();
|
||||
setIsSubmitted(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={(open) => {
|
||||
setIsOpen(open);
|
||||
if (!open) {
|
||||
setNewOptionValue('');
|
||||
setNewOptionLabel('');
|
||||
setNewOptionColumnValues({});
|
||||
setNewOptionInputType('textbox');
|
||||
setNewOptionRequired(false);
|
||||
setNewOptionOptions('');
|
||||
setNewOptionPlaceholder('');
|
||||
setNewOptionDefaultValue('');
|
||||
}
|
||||
if (!open) handleClose();
|
||||
else setIsOpen(open);
|
||||
}}>
|
||||
<DialogContent className="max-w-[95vw] sm:max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
@@ -109,7 +129,11 @@ export function OptionDialog({
|
||||
value={newOptionValue}
|
||||
onChange={(e) => setNewOptionValue(e.target.value)}
|
||||
placeholder="예: kg, stainless"
|
||||
className={isSubmitted && isValueEmpty ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
||||
/>
|
||||
{isSubmitted && isValueEmpty && (
|
||||
<p className="text-xs text-red-500 mt-1">값을 입력해주세요</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Label>라벨 (표시명) *</Label>
|
||||
@@ -117,7 +141,11 @@ export function OptionDialog({
|
||||
value={newOptionLabel}
|
||||
onChange={(e) => setNewOptionLabel(e.target.value)}
|
||||
placeholder="예: 킬로그램, 스테인리스"
|
||||
className={isSubmitted && isLabelEmpty ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
||||
/>
|
||||
{isSubmitted && isLabelEmpty && (
|
||||
<p className="text-xs text-red-500 mt-1">라벨을 입력해주세요</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -151,10 +179,15 @@ export function OptionDialog({
|
||||
value={newOptionOptions}
|
||||
onChange={(e) => setNewOptionOptions(e.target.value)}
|
||||
placeholder="옵션1, 옵션2, 옵션3 (쉼표로 구분)"
|
||||
className={isSubmitted && isDropdownOptionsEmpty ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
쉼표로 구분하여 여러 옵션을 입력하세요
|
||||
</p>
|
||||
{isSubmitted && isDropdownOptionsEmpty ? (
|
||||
<p className="text-xs text-red-500 mt-1">드롭다운 옵션을 입력해주세요</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
쉼표로 구분하여 여러 옵션을 입력하세요
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -213,8 +246,8 @@ export function OptionDialog({
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsOpen(false)}>취소</Button>
|
||||
<Button onClick={handleAddOption}>추가</Button>
|
||||
<Button variant="outline" onClick={handleClose}>취소</Button>
|
||||
<Button onClick={handleSubmit}>추가</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -33,27 +34,53 @@ export function PageDialog({
|
||||
setNewPageItemType,
|
||||
handleAddPage,
|
||||
}: PageDialogProps) {
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
// 유효성 검사
|
||||
const isPageNameEmpty = !newPageName.trim();
|
||||
|
||||
const handleSubmit = () => {
|
||||
setIsSubmitted(true);
|
||||
if (!isPageNameEmpty) {
|
||||
handleAddPage();
|
||||
setIsSubmitted(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setIsPageDialogOpen(false);
|
||||
setNewPageName('');
|
||||
setNewPageItemType('FG');
|
||||
setIsSubmitted(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isPageDialogOpen} onOpenChange={(open) => {
|
||||
setIsPageDialogOpen(open);
|
||||
if (!open) {
|
||||
setNewPageName('');
|
||||
setNewPageItemType('FG');
|
||||
handleClose();
|
||||
} else {
|
||||
setIsPageDialogOpen(open);
|
||||
}
|
||||
}}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>섹션 추가</DialogTitle>
|
||||
<DialogDescription>새로운 품목 섹션을 생성합니다</DialogDescription>
|
||||
<DialogTitle>페이지 추가</DialogTitle>
|
||||
<DialogDescription>품목 유형별 관리 페이지를 생성합니다</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>섹션명 *</Label>
|
||||
<Label>페이지명 *</Label>
|
||||
<Input
|
||||
value={newPageName}
|
||||
onChange={(e) => setNewPageName(e.target.value)}
|
||||
placeholder="예: 품목 등록"
|
||||
placeholder="예: 제품 등록"
|
||||
className={isSubmitted && isPageNameEmpty ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
||||
/>
|
||||
{isSubmitted && isPageNameEmpty && (
|
||||
<p className="text-xs text-red-500 mt-1">
|
||||
페이지명을 입력해주세요
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Label>품목유형 *</Label>
|
||||
@@ -70,8 +97,8 @@ export function PageDialog({
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsPageDialogOpen(false)}>취소</Button>
|
||||
<Button onClick={handleAddPage}>추가</Button>
|
||||
<Button variant="outline" onClick={handleClose}>취소</Button>
|
||||
<Button onClick={handleSubmit}>추가</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -23,12 +24,21 @@ export function PathEditDialog({
|
||||
updateItemPage,
|
||||
trackChange,
|
||||
}: PathEditDialogProps) {
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
// 유효성 검사
|
||||
const isPathEmpty = !editingAbsolutePath.trim();
|
||||
const isPathInvalid = editingAbsolutePath.trim() && !editingAbsolutePath.startsWith('/');
|
||||
|
||||
const handleClose = () => {
|
||||
setEditingPathPageId(null);
|
||||
setEditingAbsolutePath('');
|
||||
setIsSubmitted(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={editingPathPageId !== null} onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setEditingPathPageId(null);
|
||||
setEditingAbsolutePath('');
|
||||
}
|
||||
if (!open) handleClose();
|
||||
}}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
@@ -42,25 +52,30 @@ export function PathEditDialog({
|
||||
value={editingAbsolutePath}
|
||||
onChange={(e) => setEditingAbsolutePath(e.target.value)}
|
||||
placeholder="/제품관리/제품등록"
|
||||
className={isSubmitted && (isPathEmpty || isPathInvalid) ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">슬래시(/)로 시작하며, 경로를 슬래시로 구분합니다</p>
|
||||
{isSubmitted && isPathEmpty && (
|
||||
<p className="text-xs text-red-500 mt-1">절대경로를 입력해주세요</p>
|
||||
)}
|
||||
{isSubmitted && isPathInvalid && (
|
||||
<p className="text-xs text-red-500 mt-1">절대경로는 슬래시(/)로 시작해야 합니다</p>
|
||||
)}
|
||||
{!isSubmitted || (!isPathEmpty && !isPathInvalid) ? (
|
||||
<p className="text-xs text-gray-500 mt-1">슬래시(/)로 시작하며, 경로를 슬래시로 구분합니다</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setEditingPathPageId(null)}>취소</Button>
|
||||
<Button variant="outline" onClick={handleClose}>취소</Button>
|
||||
<Button onClick={() => {
|
||||
if (!editingAbsolutePath.trim()) {
|
||||
toast.error('절대경로를 입력해주세요');
|
||||
return;
|
||||
}
|
||||
if (!editingAbsolutePath.startsWith('/')) {
|
||||
toast.error('절대경로는 슬래시(/)로 시작해야 합니다');
|
||||
setIsSubmitted(true);
|
||||
if (isPathEmpty || isPathInvalid) {
|
||||
return;
|
||||
}
|
||||
if (editingPathPageId) {
|
||||
updateItemPage(editingPathPageId, { absolutePath: editingAbsolutePath });
|
||||
trackChange('pages', editingPathPageId, 'update', { absolutePath: editingAbsolutePath });
|
||||
setEditingPathPageId(null);
|
||||
trackChange('pages', String(editingPathPageId), 'update', { absolutePath: editingAbsolutePath });
|
||||
handleClose();
|
||||
toast.success('절대경로가 수정되었습니다 (저장 필요)');
|
||||
}
|
||||
}}>저장</Button>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { FileText, Package, Check, X } from 'lucide-react';
|
||||
import { FileText, Package, Check } from 'lucide-react';
|
||||
import type { SectionTemplate } from '@/contexts/ItemMasterContext';
|
||||
|
||||
interface SectionDialogProps {
|
||||
@@ -45,6 +46,11 @@ export function SectionDialog({
|
||||
setSelectedTemplateId,
|
||||
handleLinkTemplate,
|
||||
}: SectionDialogProps) {
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
// 유효성 검사
|
||||
const isTitleEmpty = !newSectionTitle.trim();
|
||||
|
||||
const handleClose = () => {
|
||||
setIsSectionDialogOpen(false);
|
||||
setNewSectionType('fields');
|
||||
@@ -52,6 +58,15 @@ export function SectionDialog({
|
||||
setNewSectionDescription('');
|
||||
setSectionInputMode('custom');
|
||||
setSelectedTemplateId(null);
|
||||
setIsSubmitted(false);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
setIsSubmitted(true);
|
||||
if (sectionInputMode === 'custom' && !isTitleEmpty) {
|
||||
handleAddSection();
|
||||
setIsSubmitted(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 템플릿 선택 시 폼에 값 채우기
|
||||
@@ -220,7 +235,11 @@ export function SectionDialog({
|
||||
onChange={(e) => setNewSectionTitle(e.target.value)}
|
||||
placeholder={newSectionType === 'bom' ? '예: BOM 구성' : '예: 기본 정보'}
|
||||
disabled={sectionInputMode === 'template'}
|
||||
className={isSubmitted && isTitleEmpty && sectionInputMode === 'custom' ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
||||
/>
|
||||
{isSubmitted && isTitleEmpty && sectionInputMode === 'custom' && (
|
||||
<p className="text-xs text-red-500 mt-1">섹션 제목을 입력해주세요</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Label>설명 (선택)</Label>
|
||||
@@ -269,7 +288,7 @@ export function SectionDialog({
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={handleAddSection}
|
||||
onClick={handleSubmit}
|
||||
className="w-full sm:w-auto"
|
||||
disabled={sectionInputMode === 'template' && !selectedTemplateId}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -47,16 +48,37 @@ export function SectionTemplateDialog({
|
||||
handleUpdateSectionTemplate,
|
||||
handleAddSectionTemplate,
|
||||
}: SectionTemplateDialogProps) {
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
// 유효성 검사
|
||||
const isTitleEmpty = !newSectionTemplateTitle.trim();
|
||||
|
||||
const handleClose = () => {
|
||||
setIsSectionTemplateDialogOpen(false);
|
||||
setEditingSectionTemplateId(null);
|
||||
setNewSectionTemplateTitle('');
|
||||
setNewSectionTemplateDescription('');
|
||||
setNewSectionTemplateCategory([]);
|
||||
setNewSectionTemplateType('fields');
|
||||
setIsSubmitted(false);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
setIsSubmitted(true);
|
||||
if (!isTitleEmpty) {
|
||||
if (editingSectionTemplateId) {
|
||||
handleUpdateSectionTemplate();
|
||||
} else {
|
||||
handleAddSectionTemplate();
|
||||
}
|
||||
setIsSubmitted(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isSectionTemplateDialogOpen} onOpenChange={(open) => {
|
||||
setIsSectionTemplateDialogOpen(open);
|
||||
if (!open) {
|
||||
setEditingSectionTemplateId(null);
|
||||
setNewSectionTemplateTitle('');
|
||||
setNewSectionTemplateDescription('');
|
||||
setNewSectionTemplateCategory([]);
|
||||
setNewSectionTemplateType('fields');
|
||||
}
|
||||
if (!open) handleClose();
|
||||
else setIsSectionTemplateDialogOpen(open);
|
||||
}}>
|
||||
<DialogContent className="max-w-[95vw] sm:max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
@@ -72,7 +94,11 @@ export function SectionTemplateDialog({
|
||||
value={newSectionTemplateTitle}
|
||||
onChange={(e) => setNewSectionTemplateTitle(e.target.value)}
|
||||
placeholder="예: 기본 정보"
|
||||
className={isSubmitted && isTitleEmpty ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
||||
/>
|
||||
{isSubmitted && isTitleEmpty && (
|
||||
<p className="text-xs text-red-500 mt-1">섹션 제목을 입력해주세요</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -136,8 +162,8 @@ export function SectionTemplateDialog({
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsSectionTemplateDialogOpen(false)}>취소</Button>
|
||||
<Button onClick={editingSectionTemplateId ? handleUpdateSectionTemplate : handleAddSectionTemplate}>
|
||||
<Button variant="outline" onClick={handleClose}>취소</Button>
|
||||
<Button onClick={handleSubmit}>
|
||||
{editingSectionTemplateId ? '수정' : '추가'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -87,7 +88,14 @@ export function TemplateFieldDialog({
|
||||
selectedMasterFieldId = '',
|
||||
setSelectedMasterFieldId,
|
||||
}: TemplateFieldDialogProps) {
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
// 유효성 검사
|
||||
const isNameEmpty = !templateFieldName.trim();
|
||||
const isKeyEmpty = !templateFieldKey.trim();
|
||||
|
||||
const handleClose = () => {
|
||||
setIsSubmitted(false);
|
||||
setIsTemplateFieldDialogOpen(false);
|
||||
setEditingTemplateFieldId(null);
|
||||
setTemplateFieldName('');
|
||||
@@ -230,7 +238,11 @@ export function TemplateFieldDialog({
|
||||
value={templateFieldName}
|
||||
onChange={(e) => setTemplateFieldName(e.target.value)}
|
||||
placeholder="예: 품목명"
|
||||
className={isSubmitted && isNameEmpty ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
||||
/>
|
||||
{isSubmitted && isNameEmpty && (
|
||||
<p className="text-xs text-red-500 mt-1">항목명을 입력해주세요</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Label>필드 키 *</Label>
|
||||
@@ -238,7 +250,11 @@ export function TemplateFieldDialog({
|
||||
value={templateFieldKey}
|
||||
onChange={(e) => setTemplateFieldKey(e.target.value)}
|
||||
placeholder="예: itemName"
|
||||
className={isSubmitted && isKeyEmpty ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
||||
/>
|
||||
{isSubmitted && isKeyEmpty && (
|
||||
<p className="text-xs text-red-500 mt-1">필드 키를 입력해주세요</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -334,8 +350,14 @@ export function TemplateFieldDialog({
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsTemplateFieldDialogOpen(false)}>취소</Button>
|
||||
<Button onClick={handleAddTemplateField}>
|
||||
<Button variant="outline" onClick={handleClose}>취소</Button>
|
||||
<Button onClick={() => {
|
||||
setIsSubmitted(true);
|
||||
const shouldValidate = templateFieldInputMode === 'custom' || editingTemplateFieldId || !setTemplateFieldInputMode;
|
||||
if (shouldValidate && (isNameEmpty || isKeyEmpty)) return;
|
||||
handleAddTemplateField();
|
||||
setIsSubmitted(false);
|
||||
}}>
|
||||
{editingTemplateFieldId ? '수정' : '추가'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
20
src/components/items/ItemMasterDataManagement/hooks/index.ts
Normal file
20
src/components/items/ItemMasterDataManagement/hooks/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export { usePageManagement } from './usePageManagement';
|
||||
export type { UsePageManagementReturn } from './usePageManagement';
|
||||
|
||||
export { useSectionManagement } from './useSectionManagement';
|
||||
export type { UseSectionManagementReturn } from './useSectionManagement';
|
||||
|
||||
export { useFieldManagement } from './useFieldManagement';
|
||||
export type { UseFieldManagementReturn } from './useFieldManagement';
|
||||
|
||||
export { useMasterFieldManagement } from './useMasterFieldManagement';
|
||||
export type { UseMasterFieldManagementReturn } from './useMasterFieldManagement';
|
||||
|
||||
export { useTemplateManagement } from './useTemplateManagement';
|
||||
export type { UseTemplateManagementReturn } from './useTemplateManagement';
|
||||
|
||||
export { useAttributeManagement } from './useAttributeManagement';
|
||||
export type { UseAttributeManagementReturn } from './useAttributeManagement';
|
||||
|
||||
export { useTabManagement } from './useTabManagement';
|
||||
export type { UseTabManagementReturn, CustomTab, AttributeSubTab } from './useTabManagement';
|
||||
@@ -0,0 +1,351 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { useItemMaster } from '@/contexts/ItemMasterContext';
|
||||
import type { MasterOption, OptionColumn } from '../types';
|
||||
|
||||
export interface UseAttributeManagementReturn {
|
||||
// 속성 옵션 상태
|
||||
unitOptions: MasterOption[];
|
||||
setUnitOptions: React.Dispatch<React.SetStateAction<MasterOption[]>>;
|
||||
materialOptions: MasterOption[];
|
||||
setMaterialOptions: React.Dispatch<React.SetStateAction<MasterOption[]>>;
|
||||
surfaceTreatmentOptions: MasterOption[];
|
||||
setSurfaceTreatmentOptions: React.Dispatch<React.SetStateAction<MasterOption[]>>;
|
||||
customAttributeOptions: Record<string, MasterOption[]>;
|
||||
setCustomAttributeOptions: React.Dispatch<React.SetStateAction<Record<string, MasterOption[]>>>;
|
||||
|
||||
// 옵션 다이얼로그 상태
|
||||
isOptionDialogOpen: boolean;
|
||||
setIsOptionDialogOpen: (open: boolean) => void;
|
||||
editingOptionType: string | null;
|
||||
setEditingOptionType: (type: string | null) => void;
|
||||
|
||||
// 옵션 폼 상태
|
||||
newOptionValue: string;
|
||||
setNewOptionValue: (value: string) => void;
|
||||
newOptionLabel: string;
|
||||
setNewOptionLabel: (label: string) => void;
|
||||
newOptionColumnValues: Record<string, string>;
|
||||
setNewOptionColumnValues: React.Dispatch<React.SetStateAction<Record<string, string>>>;
|
||||
newOptionInputType: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea';
|
||||
setNewOptionInputType: (type: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea') => 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;
|
||||
|
||||
// 칼럼 관리 상태
|
||||
isColumnManageDialogOpen: boolean;
|
||||
setIsColumnManageDialogOpen: (open: boolean) => void;
|
||||
managingColumnType: string | null;
|
||||
setManagingColumnType: (type: string | null) => void;
|
||||
attributeColumns: Record<string, OptionColumn[]>;
|
||||
setAttributeColumns: React.Dispatch<React.SetStateAction<Record<string, OptionColumn[]>>>;
|
||||
|
||||
// 칼럼 폼 상태
|
||||
newColumnName: string;
|
||||
setNewColumnName: (name: string) => void;
|
||||
newColumnKey: string;
|
||||
setNewColumnKey: (key: string) => void;
|
||||
newColumnType: 'text' | 'number';
|
||||
setNewColumnType: (type: 'text' | 'number') => void;
|
||||
newColumnRequired: boolean;
|
||||
setNewColumnRequired: (required: boolean) => void;
|
||||
|
||||
// 핸들러
|
||||
handleAddOption: () => void;
|
||||
handleDeleteOption: (type: string, id: string) => void;
|
||||
handleAddColumn: () => void;
|
||||
handleDeleteColumn: (columnKey: string) => void;
|
||||
resetOptionForm: () => void;
|
||||
resetColumnForm: () => void;
|
||||
}
|
||||
|
||||
export function useAttributeManagement(): UseAttributeManagementReturn {
|
||||
const {
|
||||
itemMasterFields,
|
||||
updateItemMasterField
|
||||
} = useItemMaster();
|
||||
|
||||
// 속성 옵션 상태
|
||||
const [unitOptions, setUnitOptions] = useState<MasterOption[]>([]);
|
||||
const [materialOptions, setMaterialOptions] = useState<MasterOption[]>([]);
|
||||
const [surfaceTreatmentOptions, setSurfaceTreatmentOptions] = useState<MasterOption[]>([]);
|
||||
const [customAttributeOptions, setCustomAttributeOptions] = useState<Record<string, MasterOption[]>>({});
|
||||
|
||||
// 옵션 다이얼로그 상태
|
||||
const [isOptionDialogOpen, setIsOptionDialogOpen] = useState(false);
|
||||
const [editingOptionType, setEditingOptionType] = useState<string | null>(null);
|
||||
|
||||
// 옵션 폼 상태
|
||||
const [newOptionValue, setNewOptionValue] = useState('');
|
||||
const [newOptionLabel, setNewOptionLabel] = useState('');
|
||||
const [newOptionColumnValues, setNewOptionColumnValues] = useState<Record<string, string>>({});
|
||||
const [newOptionInputType, setNewOptionInputType] = useState<'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea'>('textbox');
|
||||
const [newOptionRequired, setNewOptionRequired] = useState(false);
|
||||
const [newOptionOptions, setNewOptionOptions] = useState('');
|
||||
const [newOptionPlaceholder, setNewOptionPlaceholder] = useState('');
|
||||
const [newOptionDefaultValue, setNewOptionDefaultValue] = useState('');
|
||||
|
||||
// 칼럼 관리 상태
|
||||
const [isColumnManageDialogOpen, setIsColumnManageDialogOpen] = useState(false);
|
||||
const [managingColumnType, setManagingColumnType] = useState<string | null>(null);
|
||||
const [attributeColumns, setAttributeColumns] = useState<Record<string, OptionColumn[]>>({});
|
||||
|
||||
// 칼럼 폼 상태
|
||||
const [newColumnName, setNewColumnName] = useState('');
|
||||
const [newColumnKey, setNewColumnKey] = useState('');
|
||||
const [newColumnType, setNewColumnType] = useState<'text' | 'number'>('text');
|
||||
const [newColumnRequired, setNewColumnRequired] = useState(false);
|
||||
|
||||
// 이전 옵션 값 추적용 ref (무한 루프 방지)
|
||||
const prevOptionsRef = useRef<string>('');
|
||||
|
||||
// 속성 변경 시 연동된 마스터 항목의 옵션 자동 업데이트
|
||||
// 주의: itemMasterFields를 의존성에서 제거하여 무한 루프 방지
|
||||
useEffect(() => {
|
||||
// 현재 옵션 상태를 문자열로 직렬화
|
||||
const currentOptionsState = JSON.stringify({
|
||||
unit: unitOptions.map(o => o.label).sort(),
|
||||
material: materialOptions.map(o => o.label).sort(),
|
||||
surface: surfaceTreatmentOptions.map(o => o.label).sort(),
|
||||
custom: Object.keys(customAttributeOptions).reduce((acc, key) => {
|
||||
acc[key] = (customAttributeOptions[key] || []).map(o => o.label).sort();
|
||||
return acc;
|
||||
}, {} as Record<string, string[]>)
|
||||
});
|
||||
|
||||
// 이전 상태와 동일하면 업데이트 스킵
|
||||
if (prevOptionsRef.current === currentOptionsState) {
|
||||
return;
|
||||
}
|
||||
prevOptionsRef.current = currentOptionsState;
|
||||
|
||||
// 실제 업데이트가 필요한 경우만 처리
|
||||
itemMasterFields.forEach(field => {
|
||||
// properties가 null/undefined인 경우 스킵
|
||||
if (!field.properties) return;
|
||||
const attributeType = (field.properties as any).attributeType;
|
||||
if (attributeType && attributeType !== 'custom' && (field.properties as any)?.inputType === 'dropdown') {
|
||||
let newOptions: string[] = [];
|
||||
|
||||
if (attributeType === 'unit') {
|
||||
newOptions = unitOptions.map(opt => opt.label);
|
||||
} else if (attributeType === 'material') {
|
||||
newOptions = materialOptions.map(opt => opt.label);
|
||||
} else if (attributeType === 'surface') {
|
||||
newOptions = surfaceTreatmentOptions.map(opt => opt.label);
|
||||
} else {
|
||||
const customOpts = customAttributeOptions[attributeType] || [];
|
||||
newOptions = customOpts.map(opt => opt.label);
|
||||
}
|
||||
|
||||
const currentOptions = (field.properties as any)?.options || [];
|
||||
const optionsChanged = JSON.stringify([...currentOptions].sort()) !== JSON.stringify([...newOptions].sort());
|
||||
|
||||
if (optionsChanged && newOptions.length > 0) {
|
||||
updateItemMasterField(field.id, {
|
||||
properties: {
|
||||
...(field.properties || {}),
|
||||
options: newOptions
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [unitOptions, materialOptions, surfaceTreatmentOptions, customAttributeOptions]);
|
||||
|
||||
// 옵션 추가
|
||||
const handleAddOption = () => {
|
||||
if (!editingOptionType || !newOptionValue.trim() || !newOptionLabel.trim()) {
|
||||
toast.error('모든 항목을 입력해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
// dropdown일 경우 옵션 필수 체크
|
||||
if (newOptionInputType === 'dropdown' && !newOptionOptions.trim()) {
|
||||
toast.error('드롭다운 옵션을 입력해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
// 칼럼 필수 값 체크
|
||||
const currentColumns = attributeColumns[editingOptionType] || [];
|
||||
for (const column of currentColumns) {
|
||||
if (column.required && !newOptionColumnValues[column.key]?.trim()) {
|
||||
toast.error(`${column.name}은(는) 필수 입력 항목입니다`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const newOption: MasterOption = {
|
||||
id: `${editingOptionType}-${Date.now()}`,
|
||||
value: newOptionValue,
|
||||
label: newOptionLabel,
|
||||
isActive: true,
|
||||
inputType: newOptionInputType,
|
||||
required: newOptionRequired,
|
||||
options: newOptionInputType === 'dropdown' ? newOptionOptions.split(',').map(o => o.trim()).filter(o => o) : undefined,
|
||||
placeholder: newOptionPlaceholder || undefined,
|
||||
defaultValue: newOptionDefaultValue || undefined,
|
||||
columnValues: Object.keys(newOptionColumnValues).length > 0 ? { ...newOptionColumnValues } : undefined
|
||||
};
|
||||
|
||||
if (editingOptionType === 'unit') {
|
||||
setUnitOptions(prev => [...prev, newOption]);
|
||||
} else if (editingOptionType === 'material') {
|
||||
setMaterialOptions(prev => [...prev, newOption]);
|
||||
} else if (editingOptionType === 'surface') {
|
||||
setSurfaceTreatmentOptions(prev => [...prev, newOption]);
|
||||
} else {
|
||||
setCustomAttributeOptions(prev => ({
|
||||
...prev,
|
||||
[editingOptionType]: [...(prev[editingOptionType] || []), newOption]
|
||||
}));
|
||||
}
|
||||
|
||||
resetOptionForm();
|
||||
toast.success('속성이 추가되었습니다 (저장 필요)');
|
||||
};
|
||||
|
||||
// 옵션 삭제
|
||||
const handleDeleteOption = (type: string, id: string) => {
|
||||
if (type === 'unit') {
|
||||
setUnitOptions(prev => prev.filter(o => o.id !== id));
|
||||
} else if (type === 'material') {
|
||||
setMaterialOptions(prev => prev.filter(o => o.id !== id));
|
||||
} else if (type === 'surface') {
|
||||
setSurfaceTreatmentOptions(prev => prev.filter(o => o.id !== id));
|
||||
} else {
|
||||
setCustomAttributeOptions(prev => ({
|
||||
...prev,
|
||||
[type]: (prev[type] || []).filter(o => o.id !== id)
|
||||
}));
|
||||
}
|
||||
toast.success('삭제되었습니다');
|
||||
};
|
||||
|
||||
// 칼럼 추가
|
||||
const handleAddColumn = () => {
|
||||
if (!managingColumnType || !newColumnName.trim() || !newColumnKey.trim()) {
|
||||
toast.error('칼럼명과 키를 입력해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
const newColumn: OptionColumn = {
|
||||
id: `col-${Date.now()}`,
|
||||
key: newColumnKey,
|
||||
name: newColumnName,
|
||||
type: newColumnType,
|
||||
required: newColumnRequired
|
||||
};
|
||||
|
||||
setAttributeColumns(prev => ({
|
||||
...prev,
|
||||
[managingColumnType]: [...(prev[managingColumnType] || []), newColumn]
|
||||
}));
|
||||
|
||||
resetColumnForm();
|
||||
toast.success('칼럼이 추가되었습니다');
|
||||
};
|
||||
|
||||
// 칼럼 삭제
|
||||
const handleDeleteColumn = (columnKey: string) => {
|
||||
if (!managingColumnType) return;
|
||||
|
||||
setAttributeColumns(prev => ({
|
||||
...prev,
|
||||
[managingColumnType]: (prev[managingColumnType] || []).filter(c => c.key !== columnKey)
|
||||
}));
|
||||
toast.success('칼럼이 삭제되었습니다');
|
||||
};
|
||||
|
||||
// 옵션 폼 초기화
|
||||
const resetOptionForm = () => {
|
||||
setNewOptionValue('');
|
||||
setNewOptionLabel('');
|
||||
setNewOptionColumnValues({});
|
||||
setNewOptionInputType('textbox');
|
||||
setNewOptionRequired(false);
|
||||
setNewOptionOptions('');
|
||||
setNewOptionPlaceholder('');
|
||||
setNewOptionDefaultValue('');
|
||||
setIsOptionDialogOpen(false);
|
||||
};
|
||||
|
||||
// 칼럼 폼 초기화
|
||||
const resetColumnForm = () => {
|
||||
setNewColumnName('');
|
||||
setNewColumnKey('');
|
||||
setNewColumnType('text');
|
||||
setNewColumnRequired(false);
|
||||
};
|
||||
|
||||
return {
|
||||
// 속성 옵션 상태
|
||||
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,
|
||||
handleDeleteColumn,
|
||||
resetOptionForm,
|
||||
resetColumnForm,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { useItemMaster } from '@/contexts/ItemMasterContext';
|
||||
import type { ItemPage, ItemField, ItemMasterField, FieldDisplayCondition } from '@/contexts/ItemMasterContext';
|
||||
import { type ConditionalFieldConfig } from '../components/ConditionalDisplayUI';
|
||||
|
||||
export interface UseFieldManagementReturn {
|
||||
// 다이얼로그 상태
|
||||
isFieldDialogOpen: boolean;
|
||||
setIsFieldDialogOpen: (open: boolean) => void;
|
||||
selectedSectionForField: number | null;
|
||||
setSelectedSectionForField: (id: number | null) => void;
|
||||
editingFieldId: number | null;
|
||||
setEditingFieldId: (id: number | null) => void;
|
||||
|
||||
// 입력 모드
|
||||
fieldInputMode: 'master' | 'custom';
|
||||
setFieldInputMode: (mode: 'master' | 'custom') => void;
|
||||
showMasterFieldList: boolean;
|
||||
setShowMasterFieldList: (show: boolean) => void;
|
||||
selectedMasterFieldId: string;
|
||||
setSelectedMasterFieldId: (id: string) => void;
|
||||
|
||||
// 필드 폼 상태
|
||||
newFieldName: string;
|
||||
setNewFieldName: (name: string) => void;
|
||||
newFieldKey: string;
|
||||
setNewFieldKey: (key: string) => void;
|
||||
newFieldInputType: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea';
|
||||
setNewFieldInputType: (type: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea') => void;
|
||||
newFieldRequired: boolean;
|
||||
setNewFieldRequired: (required: boolean) => void;
|
||||
newFieldOptions: string;
|
||||
setNewFieldOptions: (options: string) => void;
|
||||
newFieldDescription: string;
|
||||
setNewFieldDescription: (desc: string) => void;
|
||||
|
||||
// 텍스트박스 컬럼
|
||||
textboxColumns: Array<{ id: string; name: string; key: string }>;
|
||||
setTextboxColumns: React.Dispatch<React.SetStateAction<Array<{ id: string; name: string; key: string }>>>;
|
||||
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;
|
||||
|
||||
// 조건부 필드
|
||||
newFieldConditionEnabled: boolean;
|
||||
setNewFieldConditionEnabled: (enabled: boolean) => void;
|
||||
newFieldConditionTargetType: 'field' | 'section';
|
||||
setNewFieldConditionTargetType: (type: 'field' | 'section') => void;
|
||||
newFieldConditionFields: ConditionalFieldConfig[];
|
||||
setNewFieldConditionFields: React.Dispatch<React.SetStateAction<ConditionalFieldConfig[]>>;
|
||||
newFieldConditionSections: string[];
|
||||
setNewFieldConditionSections: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
tempConditionValue: string;
|
||||
setTempConditionValue: (value: string) => void;
|
||||
|
||||
// 핸들러
|
||||
handleAddField: (selectedPage: ItemPage | undefined) => void;
|
||||
handleEditField: (sectionId: string, field: ItemField) => void;
|
||||
handleDeleteField: (pageId: string, sectionId: string, fieldId: string) => void;
|
||||
resetFieldForm: () => void;
|
||||
}
|
||||
|
||||
export function useFieldManagement(): UseFieldManagementReturn {
|
||||
const {
|
||||
itemMasterFields,
|
||||
addFieldToSection,
|
||||
updateField,
|
||||
deleteField,
|
||||
addItemMasterField,
|
||||
updateItemMasterField,
|
||||
} = useItemMaster();
|
||||
|
||||
// 다이얼로그 상태
|
||||
const [isFieldDialogOpen, setIsFieldDialogOpen] = useState(false);
|
||||
const [selectedSectionForField, setSelectedSectionForField] = useState<number | null>(null);
|
||||
const [editingFieldId, setEditingFieldId] = useState<number | null>(null);
|
||||
|
||||
// 입력 모드
|
||||
const [fieldInputMode, setFieldInputMode] = useState<'master' | 'custom'>('custom');
|
||||
const [showMasterFieldList, setShowMasterFieldList] = useState(false);
|
||||
const [selectedMasterFieldId, setSelectedMasterFieldId] = useState('');
|
||||
|
||||
// 필드 폼 상태
|
||||
const [newFieldName, setNewFieldName] = useState('');
|
||||
const [newFieldKey, setNewFieldKey] = useState('');
|
||||
const [newFieldInputType, setNewFieldInputType] = useState<'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea'>('textbox');
|
||||
const [newFieldRequired, setNewFieldRequired] = useState(false);
|
||||
const [newFieldOptions, setNewFieldOptions] = useState('');
|
||||
const [newFieldDescription, setNewFieldDescription] = useState('');
|
||||
|
||||
// 텍스트박스 컬럼
|
||||
const [textboxColumns, setTextboxColumns] = useState<Array<{ id: string; name: string; key: string }>>([]);
|
||||
const [isColumnDialogOpen, setIsColumnDialogOpen] = useState(false);
|
||||
const [editingColumnId, setEditingColumnId] = useState<string | null>(null);
|
||||
const [columnName, setColumnName] = useState('');
|
||||
const [columnKey, setColumnKey] = useState('');
|
||||
|
||||
// 조건부 필드
|
||||
const [newFieldConditionEnabled, setNewFieldConditionEnabled] = useState(false);
|
||||
const [newFieldConditionTargetType, setNewFieldConditionTargetType] = useState<'field' | 'section'>('field');
|
||||
const [newFieldConditionFields, setNewFieldConditionFields] = useState<ConditionalFieldConfig[]>([]);
|
||||
const [newFieldConditionSections, setNewFieldConditionSections] = useState<string[]>([]);
|
||||
const [tempConditionValue, setTempConditionValue] = useState('');
|
||||
|
||||
// 마스터 필드 선택 시 폼 자동 채우기
|
||||
useEffect(() => {
|
||||
if (fieldInputMode === 'master' && selectedMasterFieldId) {
|
||||
const masterField = itemMasterFields.find(f => f.id === Number(selectedMasterFieldId));
|
||||
if (masterField) {
|
||||
setNewFieldName(masterField.field_name);
|
||||
setNewFieldKey(masterField.id.toString());
|
||||
setNewFieldInputType(masterField.field_type || 'textbox');
|
||||
// properties에서 required 확인, 또는 validation_rules에서 확인
|
||||
const isRequired = (masterField.properties as any)?.required || false;
|
||||
setNewFieldRequired(isRequired);
|
||||
setNewFieldOptions(masterField.options?.map(o => o.label).join(', ') || '');
|
||||
setNewFieldDescription(masterField.description || '');
|
||||
}
|
||||
} else if (fieldInputMode === 'custom') {
|
||||
// 직접 입력 모드로 전환 시 폼 초기화
|
||||
setNewFieldName('');
|
||||
setNewFieldKey('');
|
||||
setNewFieldInputType('textbox');
|
||||
setNewFieldRequired(false);
|
||||
setNewFieldOptions('');
|
||||
setNewFieldDescription('');
|
||||
}
|
||||
}, [fieldInputMode, selectedMasterFieldId, itemMasterFields]);
|
||||
|
||||
// 필드 추가
|
||||
const handleAddField = (selectedPage: ItemPage | undefined) => {
|
||||
if (!selectedPage || !selectedSectionForField || !newFieldName.trim() || !newFieldKey.trim()) {
|
||||
toast.error('모든 필수 항목을 입력해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
// 조건부 표시 설정
|
||||
const displayCondition: FieldDisplayCondition | undefined = newFieldConditionEnabled
|
||||
? {
|
||||
targetType: newFieldConditionTargetType,
|
||||
fieldConditions: newFieldConditionTargetType === 'field' && newFieldConditionFields.length > 0
|
||||
? newFieldConditionFields
|
||||
: undefined,
|
||||
sectionIds: newFieldConditionTargetType === 'section' && newFieldConditionSections.length > 0
|
||||
? newFieldConditionSections
|
||||
: undefined
|
||||
}
|
||||
: undefined;
|
||||
|
||||
// 텍스트박스 컬럼 설정
|
||||
const hasColumns = newFieldInputType === 'textbox' && textboxColumns.length > 0;
|
||||
|
||||
// 마스터 항목에서 가져온 경우 master_field_id 설정
|
||||
const masterFieldId = fieldInputMode === 'master' && selectedMasterFieldId
|
||||
? Number(selectedMasterFieldId)
|
||||
: null;
|
||||
|
||||
const newField: ItemField = {
|
||||
id: editingFieldId ? Number(editingFieldId) : Date.now(),
|
||||
section_id: Number(selectedSectionForField),
|
||||
master_field_id: masterFieldId,
|
||||
field_name: newFieldName,
|
||||
field_type: newFieldInputType,
|
||||
order_no: 0,
|
||||
is_required: newFieldRequired,
|
||||
placeholder: newFieldDescription || null,
|
||||
default_value: null,
|
||||
display_condition: displayCondition as Record<string, any> | null || null,
|
||||
validation_rules: null,
|
||||
options: newFieldInputType === 'dropdown' && newFieldOptions.trim()
|
||||
? newFieldOptions.split(',').map(o => ({ label: o.trim(), value: o.trim() }))
|
||||
: null,
|
||||
properties: hasColumns ? {
|
||||
multiColumn: true,
|
||||
columnCount: textboxColumns.length,
|
||||
columnNames: textboxColumns.map(c => c.name)
|
||||
} : null,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
if (editingFieldId) {
|
||||
console.log('Updating field:', { pageId: selectedPage.id, sectionId: selectedSectionForField, fieldId: editingFieldId, fieldName: newField.field_name });
|
||||
updateField(Number(editingFieldId), newField);
|
||||
|
||||
// 항목관리 탭의 마스터 항목도 업데이트 (동일한 fieldKey가 있으면)
|
||||
const existingMasterField = itemMasterFields.find(mf => mf.id.toString() === newField.field_name);
|
||||
if (existingMasterField) {
|
||||
const updatedMasterField: Partial<ItemMasterField> = {
|
||||
field_name: newField.field_name,
|
||||
description: newField.placeholder ?? null,
|
||||
properties: newField.properties,
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
updateItemMasterField(existingMasterField.id, updatedMasterField);
|
||||
}
|
||||
|
||||
toast.success('항목이 섹션에 수정되었습니다!');
|
||||
} else {
|
||||
console.log('Adding field to section:', { pageId: selectedPage.id, sectionId: selectedSectionForField, fieldName: newField.field_name });
|
||||
|
||||
// 1. 섹션에 항목 추가
|
||||
addFieldToSection(Number(selectedSectionForField), newField);
|
||||
|
||||
// 2. 마스터 항목 선택이 아닌 경우에만 새 마스터 항목 자동 생성
|
||||
const isFromMasterField = masterFieldId !== null;
|
||||
const existingMasterField = itemMasterFields.find(mf => mf.id.toString() === newField.field_name);
|
||||
if (!isFromMasterField && !existingMasterField) {
|
||||
// ItemMasterField 타입에 맞게 필수 필드 포함
|
||||
const newMasterFieldData: Omit<ItemMasterField, 'id' | 'tenant_id' | 'created_by' | 'updated_by' | 'created_at' | 'updated_at'> = {
|
||||
field_name: newField.field_name,
|
||||
field_type: newField.field_type,
|
||||
description: newField.placeholder ?? null,
|
||||
category: selectedPage.item_type,
|
||||
is_common: false,
|
||||
default_value: null,
|
||||
options: newField.options ?? null,
|
||||
validation_rules: null,
|
||||
properties: newField.properties ?? null,
|
||||
};
|
||||
addItemMasterField(newMasterFieldData as any);
|
||||
|
||||
console.log('Field added to both section and master fields:', {
|
||||
fieldId: newField.id,
|
||||
fieldName: newMasterFieldData.field_name
|
||||
});
|
||||
|
||||
toast.success('항목이 섹션에 추가되고 마스터 항목으로도 등록되었습니다!');
|
||||
} else {
|
||||
toast.success('항목이 섹션에 추가되었습니다!');
|
||||
}
|
||||
}
|
||||
|
||||
resetFieldForm();
|
||||
};
|
||||
|
||||
// 필드 수정
|
||||
const handleEditField = (sectionId: string, field: ItemField) => {
|
||||
setSelectedSectionForField(Number(sectionId));
|
||||
setEditingFieldId(field.id);
|
||||
setNewFieldName(field.field_name);
|
||||
setNewFieldKey(field.id.toString());
|
||||
setNewFieldInputType(field.field_type);
|
||||
setNewFieldRequired(field.is_required);
|
||||
setNewFieldOptions(field.options?.map(opt => opt.value).join(', ') || '');
|
||||
setNewFieldDescription('');
|
||||
|
||||
// 조건부 표시 설정 로드
|
||||
if (field.display_condition) {
|
||||
setNewFieldConditionEnabled(true);
|
||||
setNewFieldConditionTargetType(field.display_condition.targetType);
|
||||
setNewFieldConditionFields(field.display_condition.fieldConditions || []);
|
||||
setNewFieldConditionSections(field.display_condition.sectionIds || []);
|
||||
} else {
|
||||
setNewFieldConditionEnabled(false);
|
||||
setNewFieldConditionTargetType('field');
|
||||
setNewFieldConditionFields([]);
|
||||
setNewFieldConditionSections([]);
|
||||
}
|
||||
|
||||
setIsFieldDialogOpen(true);
|
||||
};
|
||||
|
||||
// 필드 삭제
|
||||
const handleDeleteField = (pageId: string, sectionId: string, fieldId: string) => {
|
||||
deleteField(Number(fieldId));
|
||||
console.log('필드 삭제 완료:', fieldId);
|
||||
};
|
||||
|
||||
// 폼 초기화
|
||||
const resetFieldForm = () => {
|
||||
setNewFieldName('');
|
||||
setNewFieldKey('');
|
||||
setNewFieldInputType('textbox');
|
||||
setNewFieldRequired(false);
|
||||
setNewFieldOptions('');
|
||||
setNewFieldDescription('');
|
||||
setNewFieldConditionEnabled(false);
|
||||
setNewFieldConditionTargetType('field');
|
||||
setNewFieldConditionFields([]);
|
||||
setNewFieldConditionSections([]);
|
||||
setTempConditionValue('');
|
||||
setEditingFieldId(null);
|
||||
setSelectedSectionForField(null);
|
||||
setFieldInputMode('custom');
|
||||
setSelectedMasterFieldId('');
|
||||
setTextboxColumns([]);
|
||||
setIsFieldDialogOpen(false);
|
||||
};
|
||||
|
||||
return {
|
||||
// 다이얼로그 상태
|
||||
isFieldDialogOpen,
|
||||
setIsFieldDialogOpen,
|
||||
selectedSectionForField,
|
||||
setSelectedSectionForField,
|
||||
editingFieldId,
|
||||
setEditingFieldId,
|
||||
|
||||
// 입력 모드
|
||||
fieldInputMode,
|
||||
setFieldInputMode,
|
||||
showMasterFieldList,
|
||||
setShowMasterFieldList,
|
||||
selectedMasterFieldId,
|
||||
setSelectedMasterFieldId,
|
||||
|
||||
// 필드 폼 상태
|
||||
newFieldName,
|
||||
setNewFieldName,
|
||||
newFieldKey,
|
||||
setNewFieldKey,
|
||||
newFieldInputType,
|
||||
setNewFieldInputType,
|
||||
newFieldRequired,
|
||||
setNewFieldRequired,
|
||||
newFieldOptions,
|
||||
setNewFieldOptions,
|
||||
newFieldDescription,
|
||||
setNewFieldDescription,
|
||||
|
||||
// 텍스트박스 컬럼
|
||||
textboxColumns,
|
||||
setTextboxColumns,
|
||||
isColumnDialogOpen,
|
||||
setIsColumnDialogOpen,
|
||||
editingColumnId,
|
||||
setEditingColumnId,
|
||||
columnName,
|
||||
setColumnName,
|
||||
columnKey,
|
||||
setColumnKey,
|
||||
|
||||
// 조건부 필드
|
||||
newFieldConditionEnabled,
|
||||
setNewFieldConditionEnabled,
|
||||
newFieldConditionTargetType,
|
||||
setNewFieldConditionTargetType,
|
||||
newFieldConditionFields,
|
||||
setNewFieldConditionFields,
|
||||
newFieldConditionSections,
|
||||
setNewFieldConditionSections,
|
||||
tempConditionValue,
|
||||
setTempConditionValue,
|
||||
|
||||
// 핸들러
|
||||
handleAddField,
|
||||
handleEditField,
|
||||
handleDeleteField,
|
||||
resetFieldForm,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { useItemMaster } from '@/contexts/ItemMasterContext';
|
||||
import type { ItemMasterField } from '@/contexts/ItemMasterContext';
|
||||
|
||||
export interface UseMasterFieldManagementReturn {
|
||||
// 다이얼로그 상태
|
||||
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: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea';
|
||||
setNewMasterFieldInputType: (type: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea') => void;
|
||||
newMasterFieldRequired: boolean;
|
||||
setNewMasterFieldRequired: (required: boolean) => void;
|
||||
newMasterFieldCategory: string;
|
||||
setNewMasterFieldCategory: (category: string) => void;
|
||||
newMasterFieldDescription: string;
|
||||
setNewMasterFieldDescription: (desc: string) => void;
|
||||
newMasterFieldOptions: string;
|
||||
setNewMasterFieldOptions: (options: string) => void;
|
||||
newMasterFieldAttributeType: 'custom' | 'unit' | 'material' | 'surface';
|
||||
setNewMasterFieldAttributeType: (type: 'custom' | 'unit' | 'material' | 'surface') => void;
|
||||
newMasterFieldMultiColumn: boolean;
|
||||
setNewMasterFieldMultiColumn: (multi: boolean) => void;
|
||||
newMasterFieldColumnCount: number;
|
||||
setNewMasterFieldColumnCount: (count: number) => void;
|
||||
newMasterFieldColumnNames: string[];
|
||||
setNewMasterFieldColumnNames: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
|
||||
// 핸들러
|
||||
handleAddMasterField: () => void;
|
||||
handleEditMasterField: (field: ItemMasterField) => void;
|
||||
handleUpdateMasterField: () => void;
|
||||
handleDeleteMasterField: (id: number) => void;
|
||||
resetMasterFieldForm: () => void;
|
||||
}
|
||||
|
||||
export function useMasterFieldManagement(): UseMasterFieldManagementReturn {
|
||||
const {
|
||||
itemMasterFields,
|
||||
addItemMasterField,
|
||||
updateItemMasterField,
|
||||
deleteItemMasterField,
|
||||
} = useItemMaster();
|
||||
|
||||
// 다이얼로그 상태
|
||||
const [isMasterFieldDialogOpen, setIsMasterFieldDialogOpen] = useState(false);
|
||||
const [editingMasterFieldId, setEditingMasterFieldId] = useState<number | null>(null);
|
||||
|
||||
// 폼 상태
|
||||
const [newMasterFieldName, setNewMasterFieldName] = useState('');
|
||||
const [newMasterFieldKey, setNewMasterFieldKey] = useState('');
|
||||
const [newMasterFieldInputType, setNewMasterFieldInputType] = useState<'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea'>('textbox');
|
||||
const [newMasterFieldRequired, setNewMasterFieldRequired] = useState(false);
|
||||
const [newMasterFieldCategory, setNewMasterFieldCategory] = useState('공통');
|
||||
const [newMasterFieldDescription, setNewMasterFieldDescription] = useState('');
|
||||
const [newMasterFieldOptions, setNewMasterFieldOptions] = useState('');
|
||||
const [newMasterFieldAttributeType, setNewMasterFieldAttributeType] = useState<'custom' | 'unit' | 'material' | 'surface'>('custom');
|
||||
const [newMasterFieldMultiColumn, setNewMasterFieldMultiColumn] = useState(false);
|
||||
const [newMasterFieldColumnCount, setNewMasterFieldColumnCount] = useState(2);
|
||||
const [newMasterFieldColumnNames, setNewMasterFieldColumnNames] = useState<string[]>(['컬럼1', '컬럼2']);
|
||||
|
||||
// 마스터 항목 추가
|
||||
const handleAddMasterField = () => {
|
||||
if (!newMasterFieldName.trim() || !newMasterFieldKey.trim()) {
|
||||
toast.error('항목명과 필드 키를 입력해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
// ItemMasterField 타입에 맞게 필수 필드 포함
|
||||
const newMasterFieldData: Omit<ItemMasterField, 'id' | 'tenant_id' | 'created_by' | 'updated_by' | 'created_at' | 'updated_at'> = {
|
||||
field_name: newMasterFieldName,
|
||||
field_type: newMasterFieldInputType,
|
||||
category: newMasterFieldCategory || null,
|
||||
description: newMasterFieldDescription || null,
|
||||
is_common: false,
|
||||
default_value: null,
|
||||
options: newMasterFieldInputType === 'dropdown' && newMasterFieldOptions.trim()
|
||||
? newMasterFieldOptions.split(',').map(o => ({ label: o.trim(), value: o.trim() }))
|
||||
: null,
|
||||
validation_rules: null,
|
||||
properties: {
|
||||
required: newMasterFieldRequired,
|
||||
attributeType: newMasterFieldInputType === 'dropdown' ? newMasterFieldAttributeType : undefined,
|
||||
multiColumn: (newMasterFieldInputType === 'textbox' || newMasterFieldInputType === 'textarea') ? newMasterFieldMultiColumn : undefined,
|
||||
columnCount: (newMasterFieldInputType === 'textbox' || newMasterFieldInputType === 'textarea') && newMasterFieldMultiColumn ? newMasterFieldColumnCount : undefined,
|
||||
columnNames: (newMasterFieldInputType === 'textbox' || newMasterFieldInputType === 'textarea') && newMasterFieldMultiColumn ? newMasterFieldColumnNames : undefined
|
||||
},
|
||||
};
|
||||
|
||||
addItemMasterField(newMasterFieldData as any);
|
||||
resetMasterFieldForm();
|
||||
toast.success('마스터 항목이 추가되었습니다 (저장 필요)');
|
||||
};
|
||||
|
||||
// 마스터 항목 수정 시작
|
||||
const handleEditMasterField = (field: ItemMasterField) => {
|
||||
setEditingMasterFieldId(field.id);
|
||||
setNewMasterFieldName(field.field_name);
|
||||
setNewMasterFieldKey(field.id.toString());
|
||||
setNewMasterFieldInputType(field.field_type || 'textbox');
|
||||
setNewMasterFieldRequired((field.properties as any)?.required || false);
|
||||
setNewMasterFieldCategory(field.category || '공통');
|
||||
setNewMasterFieldDescription(field.description || '');
|
||||
setNewMasterFieldOptions(field.options?.map(o => o.label).join(', ') || '');
|
||||
setNewMasterFieldAttributeType((field.properties as any)?.attributeType || 'custom');
|
||||
setNewMasterFieldMultiColumn((field.properties as any)?.multiColumn || false);
|
||||
setNewMasterFieldColumnCount((field.properties as any)?.columnCount || 2);
|
||||
setNewMasterFieldColumnNames((field.properties as any)?.columnNames || ['컬럼1', '컬럼2']);
|
||||
setIsMasterFieldDialogOpen(true);
|
||||
};
|
||||
|
||||
// 마스터 항목 업데이트
|
||||
const handleUpdateMasterField = () => {
|
||||
if (!editingMasterFieldId || !newMasterFieldName.trim() || !newMasterFieldKey.trim()) {
|
||||
toast.error('항목명과 필드 키를 입력해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
const updateData: Partial<ItemMasterField> = {
|
||||
field_name: newMasterFieldName,
|
||||
field_type: newMasterFieldInputType,
|
||||
category: newMasterFieldCategory || null,
|
||||
description: newMasterFieldDescription || null,
|
||||
options: newMasterFieldInputType === 'dropdown' && newMasterFieldOptions.trim()
|
||||
? newMasterFieldOptions.split(',').map(o => ({ label: o.trim(), value: o.trim() }))
|
||||
: null,
|
||||
properties: {
|
||||
required: newMasterFieldRequired,
|
||||
attributeType: newMasterFieldInputType === 'dropdown' ? newMasterFieldAttributeType : undefined,
|
||||
multiColumn: (newMasterFieldInputType === 'textbox' || newMasterFieldInputType === 'textarea') ? newMasterFieldMultiColumn : undefined,
|
||||
columnCount: (newMasterFieldInputType === 'textbox' || newMasterFieldInputType === 'textarea') && newMasterFieldMultiColumn ? newMasterFieldColumnCount : undefined,
|
||||
columnNames: (newMasterFieldInputType === 'textbox' || newMasterFieldInputType === 'textarea') && newMasterFieldMultiColumn ? newMasterFieldColumnNames : undefined
|
||||
},
|
||||
};
|
||||
|
||||
updateItemMasterField(editingMasterFieldId, updateData);
|
||||
resetMasterFieldForm();
|
||||
toast.success('마스터 항목이 수정되었습니다 (저장 필요)');
|
||||
};
|
||||
|
||||
// 마스터 항목 삭제
|
||||
const handleDeleteMasterField = (id: number) => {
|
||||
if (confirm('이 마스터 항목을 삭제하시겠습니까?')) {
|
||||
deleteItemMasterField(id);
|
||||
toast.success('마스터 항목이 삭제되었습니다');
|
||||
}
|
||||
};
|
||||
|
||||
// 폼 초기화
|
||||
const resetMasterFieldForm = () => {
|
||||
setEditingMasterFieldId(null);
|
||||
setNewMasterFieldName('');
|
||||
setNewMasterFieldKey('');
|
||||
setNewMasterFieldInputType('textbox');
|
||||
setNewMasterFieldRequired(false);
|
||||
setNewMasterFieldCategory('공통');
|
||||
setNewMasterFieldDescription('');
|
||||
setNewMasterFieldOptions('');
|
||||
setNewMasterFieldAttributeType('custom');
|
||||
setNewMasterFieldMultiColumn(false);
|
||||
setNewMasterFieldColumnCount(2);
|
||||
setNewMasterFieldColumnNames(['컬럼1', '컬럼2']);
|
||||
setIsMasterFieldDialogOpen(false);
|
||||
};
|
||||
|
||||
return {
|
||||
// 다이얼로그 상태
|
||||
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,
|
||||
|
||||
// 핸들러
|
||||
handleAddMasterField,
|
||||
handleEditMasterField,
|
||||
handleUpdateMasterField,
|
||||
handleDeleteMasterField,
|
||||
resetMasterFieldForm,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { useItemMaster } from '@/contexts/ItemMasterContext';
|
||||
import type { ItemPage } from '@/contexts/ItemMasterContext';
|
||||
import { ApiError, getErrorMessage } from '@/lib/api/error-handler';
|
||||
import { generateAbsolutePath } from '../utils/pathUtils';
|
||||
|
||||
export interface UsePageManagementReturn {
|
||||
// 상태
|
||||
selectedPageId: number | null;
|
||||
setSelectedPageId: (id: number | null) => void;
|
||||
selectedPage: ItemPage | undefined;
|
||||
editingPageId: number | null;
|
||||
setEditingPageId: (id: number | null) => void;
|
||||
editingPageName: string;
|
||||
setEditingPageName: (name: string) => void;
|
||||
isPageDialogOpen: boolean;
|
||||
setIsPageDialogOpen: (open: boolean) => void;
|
||||
newPageName: string;
|
||||
setNewPageName: (name: string) => void;
|
||||
newPageItemType: 'FG' | 'PT' | 'SM' | 'RM' | 'CS';
|
||||
setNewPageItemType: (type: 'FG' | 'PT' | 'SM' | 'RM' | 'CS') => void;
|
||||
editingPathPageId: number | null;
|
||||
setEditingPathPageId: (id: number | null) => void;
|
||||
editingAbsolutePath: string;
|
||||
setEditingAbsolutePath: (path: string) => void;
|
||||
isLoading: boolean;
|
||||
|
||||
// 핸들러
|
||||
handleAddPage: () => Promise<void>;
|
||||
handleDuplicatePage: (pageId: number) => Promise<void>;
|
||||
handleDeletePage: (pageId: number) => void;
|
||||
handleUpdatePageName: (pageId: number, newName: string) => Promise<void>;
|
||||
handleUpdateAbsolutePath: (pageId: number, newPath: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function usePageManagement(): UsePageManagementReturn {
|
||||
const {
|
||||
itemPages,
|
||||
addItemPage,
|
||||
updateItemPage,
|
||||
deleteItemPage,
|
||||
} = useItemMaster();
|
||||
|
||||
// 상태
|
||||
const [selectedPageId, setSelectedPageId] = useState<number | null>(itemPages[0]?.id || null);
|
||||
const [editingPageId, setEditingPageId] = useState<number | null>(null);
|
||||
const [editingPageName, setEditingPageName] = useState('');
|
||||
const [isPageDialogOpen, setIsPageDialogOpen] = useState(false);
|
||||
const [newPageName, setNewPageName] = useState('');
|
||||
const [newPageItemType, setNewPageItemType] = useState<'FG' | 'PT' | 'SM' | 'RM' | 'CS'>('FG');
|
||||
const [editingPathPageId, setEditingPathPageId] = useState<number | null>(null);
|
||||
const [editingAbsolutePath, setEditingAbsolutePath] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// 선택된 페이지
|
||||
const selectedPage = itemPages.find(p => p.id === selectedPageId);
|
||||
|
||||
// 마이그레이션 완료 추적용 ref
|
||||
const migrationDoneRef = useRef<Set<number>>(new Set());
|
||||
|
||||
// 기존 페이지들에 절대경로 자동 생성 (마이그레이션)
|
||||
useEffect(() => {
|
||||
// itemPages가 비어있으면 스킵
|
||||
if (itemPages.length === 0) return;
|
||||
|
||||
const pagesToMigrate = itemPages.filter(
|
||||
page => !page.absolute_path && !migrationDoneRef.current.has(page.id)
|
||||
);
|
||||
|
||||
// 마이그레이션할 페이지가 없으면 스킵
|
||||
if (pagesToMigrate.length === 0) return;
|
||||
|
||||
// 마이그레이션 실행 (한 번에 처리)
|
||||
pagesToMigrate.forEach(page => {
|
||||
const absolutePath = generateAbsolutePath(page.item_type, page.page_name);
|
||||
updateItemPage(page.id, { absolute_path: absolutePath });
|
||||
migrationDoneRef.current.add(page.id);
|
||||
});
|
||||
|
||||
console.log(`절대경로가 자동으로 생성되었습니다 (${pagesToMigrate.length}개 페이지)`);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [itemPages.length]); // itemPages 길이가 변경될 때만 체크
|
||||
|
||||
// 페이지 추가
|
||||
const handleAddPage = async () => {
|
||||
if (!newPageName.trim()) {
|
||||
toast.error('섹션명을 입력해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const absolutePath = generateAbsolutePath(newPageItemType, newPageName);
|
||||
|
||||
const newPage = await addItemPage({
|
||||
page_name: newPageName,
|
||||
item_type: newPageItemType,
|
||||
absolute_path: absolutePath,
|
||||
is_active: true,
|
||||
sections: [],
|
||||
order_no: 0,
|
||||
});
|
||||
|
||||
// 새로 생성된 페이지를 선택
|
||||
setSelectedPageId(newPage.id);
|
||||
|
||||
// 폼 초기화
|
||||
setNewPageName('');
|
||||
setNewPageItemType('FG');
|
||||
setIsPageDialogOpen(false);
|
||||
|
||||
toast.success('페이지가 추가되었습니다');
|
||||
} 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);
|
||||
} else {
|
||||
const errorMessage = getErrorMessage(err);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
console.error('❌ Failed to create page:', err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 페이지 복제
|
||||
const handleDuplicatePage = async (pageId: number) => {
|
||||
const originalPage = itemPages.find(p => p.id === pageId);
|
||||
if (!originalPage) {
|
||||
toast.error('페이지를 찾을 수 없습니다');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const duplicatedPageName = `${originalPage.page_name} (복제)`;
|
||||
const absolutePath = generateAbsolutePath(originalPage.item_type, duplicatedPageName);
|
||||
|
||||
const newPage = await addItemPage({
|
||||
page_name: duplicatedPageName,
|
||||
item_type: originalPage.item_type,
|
||||
sections: [], // 섹션은 별도 API로 복제해야 함
|
||||
is_active: true,
|
||||
absolute_path: absolutePath,
|
||||
order_no: 0,
|
||||
});
|
||||
|
||||
setSelectedPageId(newPage.id);
|
||||
toast.success('페이지가 복제되었습니다');
|
||||
|
||||
// TODO: 원본 페이지의 섹션들도 복제 필요 (별도 API 호출)
|
||||
} catch (err) {
|
||||
const errorMessage = getErrorMessage(err);
|
||||
toast.error(errorMessage);
|
||||
console.error('❌ Failed to duplicate page:', err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 페이지 삭제
|
||||
const handleDeletePage = (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);
|
||||
|
||||
// 삭제된 페이지가 선택된 페이지였다면 다른 페이지 선택
|
||||
if (selectedPageId === pageId) {
|
||||
const remainingPages = itemPages.filter(p => p.id !== pageId);
|
||||
setSelectedPageId(remainingPages[0]?.id || null);
|
||||
}
|
||||
|
||||
console.log('페이지 삭제 완료:', {
|
||||
pageId,
|
||||
removedSections: sectionIds.length,
|
||||
removedFields: fieldIds.length
|
||||
});
|
||||
};
|
||||
|
||||
// 페이지명 수정
|
||||
const handleUpdatePageName = async (pageId: number, newName: string) => {
|
||||
if (!newName.trim()) {
|
||||
toast.error('페이지명을 입력해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await updateItemPage(pageId, { page_name: newName });
|
||||
setEditingPageId(null);
|
||||
setEditingPageName('');
|
||||
toast.success('페이지명이 수정되었습니다');
|
||||
} catch (err) {
|
||||
const errorMessage = getErrorMessage(err);
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 절대경로 수정
|
||||
const handleUpdateAbsolutePath = async (pageId: number, newPath: string) => {
|
||||
if (!newPath.trim()) {
|
||||
toast.error('절대경로를 입력해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await updateItemPage(pageId, { absolute_path: newPath });
|
||||
setEditingPathPageId(null);
|
||||
setEditingAbsolutePath('');
|
||||
toast.success('절대경로가 수정되었습니다');
|
||||
} catch (err) {
|
||||
const errorMessage = getErrorMessage(err);
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
// 상태
|
||||
selectedPageId,
|
||||
setSelectedPageId,
|
||||
selectedPage,
|
||||
editingPageId,
|
||||
setEditingPageId,
|
||||
editingPageName,
|
||||
setEditingPageName,
|
||||
isPageDialogOpen,
|
||||
setIsPageDialogOpen,
|
||||
newPageName,
|
||||
setNewPageName,
|
||||
newPageItemType,
|
||||
setNewPageItemType,
|
||||
editingPathPageId,
|
||||
setEditingPathPageId,
|
||||
editingAbsolutePath,
|
||||
setEditingAbsolutePath,
|
||||
isLoading,
|
||||
|
||||
// 핸들러
|
||||
handleAddPage,
|
||||
handleDuplicatePage,
|
||||
handleDeletePage,
|
||||
handleUpdatePageName,
|
||||
handleUpdateAbsolutePath,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { useItemMaster } from '@/contexts/ItemMasterContext';
|
||||
import type { ItemPage, ItemSection, SectionTemplate } from '@/contexts/ItemMasterContext';
|
||||
|
||||
export interface UseSectionManagementReturn {
|
||||
// 상태
|
||||
editingSectionId: number | null;
|
||||
setEditingSectionId: (id: number | null) => void;
|
||||
editingSectionTitle: string;
|
||||
setEditingSectionTitle: (title: string) => void;
|
||||
isSectionDialogOpen: boolean;
|
||||
setIsSectionDialogOpen: (open: boolean) => void;
|
||||
newSectionTitle: string;
|
||||
setNewSectionTitle: (title: string) => void;
|
||||
newSectionDescription: string;
|
||||
setNewSectionDescription: (desc: string) => void;
|
||||
newSectionType: 'fields' | 'bom';
|
||||
setNewSectionType: (type: 'fields' | 'bom') => void;
|
||||
sectionInputMode: 'custom' | 'template';
|
||||
setSectionInputMode: (mode: 'custom' | 'template') => void;
|
||||
selectedSectionTemplateId: number | null;
|
||||
setSelectedSectionTemplateId: (id: number | null) => void;
|
||||
expandedSections: Record<string, boolean>;
|
||||
setExpandedSections: React.Dispatch<React.SetStateAction<Record<string, boolean>>>;
|
||||
|
||||
// 핸들러
|
||||
handleAddSection: (selectedPage: ItemPage | undefined) => void;
|
||||
handleLinkTemplate: (template: SectionTemplate, selectedPage: ItemPage | undefined) => void;
|
||||
handleEditSectionTitle: (sectionId: number, currentTitle: string) => void;
|
||||
handleSaveSectionTitle: (selectedPage: ItemPage | undefined) => void;
|
||||
handleDeleteSection: (pageId: number, sectionId: number) => void;
|
||||
toggleSection: (sectionId: string) => void;
|
||||
resetSectionForm: () => void;
|
||||
}
|
||||
|
||||
export function useSectionManagement(): UseSectionManagementReturn {
|
||||
const {
|
||||
itemPages,
|
||||
addSectionToPage,
|
||||
updateSection,
|
||||
deleteSection,
|
||||
addSectionTemplate,
|
||||
tenantId,
|
||||
} = useItemMaster();
|
||||
|
||||
// 상태
|
||||
const [editingSectionId, setEditingSectionId] = useState<number | null>(null);
|
||||
const [editingSectionTitle, setEditingSectionTitle] = useState('');
|
||||
const [isSectionDialogOpen, setIsSectionDialogOpen] = useState(false);
|
||||
const [newSectionTitle, setNewSectionTitle] = useState('');
|
||||
const [newSectionDescription, setNewSectionDescription] = useState('');
|
||||
const [newSectionType, setNewSectionType] = useState<'fields' | 'bom'>('fields');
|
||||
const [sectionInputMode, setSectionInputMode] = useState<'custom' | 'template'>('custom');
|
||||
const [selectedSectionTemplateId, setSelectedSectionTemplateId] = useState<number | null>(null);
|
||||
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({});
|
||||
|
||||
// 섹션 추가
|
||||
const handleAddSection = (selectedPage: ItemPage | undefined) => {
|
||||
if (!selectedPage || !newSectionTitle.trim()) {
|
||||
toast.error('하위섹션 제목을 입력해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
const sectionType: 'BASIC' | 'BOM' | 'CUSTOM' = newSectionType === 'bom' ? 'BOM' : 'BASIC';
|
||||
const newSection: ItemSection = {
|
||||
id: Date.now(),
|
||||
page_id: selectedPage.id,
|
||||
section_name: newSectionTitle,
|
||||
section_type: sectionType,
|
||||
description: newSectionDescription || undefined,
|
||||
order_no: selectedPage.sections.length + 1,
|
||||
is_collapsible: true,
|
||||
is_default_open: true,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
fields: [],
|
||||
bomItems: sectionType === 'BOM' ? [] : undefined
|
||||
};
|
||||
|
||||
console.log('Adding section to page:', {
|
||||
pageId: selectedPage.id,
|
||||
page_name: selectedPage.page_name,
|
||||
sectionTitle: newSection.section_name,
|
||||
sectionType: newSection.section_type,
|
||||
currentSectionCount: selectedPage.sections.length,
|
||||
newSection: newSection
|
||||
});
|
||||
|
||||
// 1. 페이지에 섹션 추가
|
||||
addSectionToPage(selectedPage.id, newSection);
|
||||
|
||||
// 2. 섹션관리 탭에도 템플릿으로 자동 추가
|
||||
const newTemplateData = {
|
||||
tenant_id: tenantId ?? 0,
|
||||
template_name: newSection.section_name,
|
||||
section_type: newSection.section_type as 'BASIC' | 'BOM' | 'CUSTOM',
|
||||
description: newSection.description ?? null,
|
||||
default_fields: null,
|
||||
created_by: null,
|
||||
updated_by: null,
|
||||
};
|
||||
addSectionTemplate(newTemplateData);
|
||||
|
||||
console.log('Section added to both page and template:', {
|
||||
sectionId: newSection.id,
|
||||
templateTitle: newTemplateData.template_name
|
||||
});
|
||||
|
||||
resetSectionForm();
|
||||
toast.success(`${newSectionType === 'bom' ? 'BOM' : '일반'} 섹션이 페이지에 추가되고 템플릿으로도 등록되었습니다!`);
|
||||
};
|
||||
|
||||
// 섹션 템플릿을 페이지에 연결
|
||||
const handleLinkTemplate = (template: SectionTemplate, selectedPage: ItemPage | undefined) => {
|
||||
if (!selectedPage) {
|
||||
toast.error('페이지를 먼저 선택해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
// 템플릿을 섹션으로 변환하여 페이지에 추가
|
||||
const newSection: Omit<ItemSection, 'id' | 'created_at' | 'updated_at'> = {
|
||||
page_id: selectedPage.id,
|
||||
section_name: template.template_name,
|
||||
section_type: template.section_type,
|
||||
description: template.description || undefined,
|
||||
order_no: selectedPage.sections.length + 1,
|
||||
is_collapsible: true,
|
||||
is_default_open: true,
|
||||
fields: template.fields ? template.fields.map((field, idx) => ({
|
||||
id: Date.now() + idx,
|
||||
section_id: 0, // 추후 업데이트됨
|
||||
field_name: field.name,
|
||||
field_type: field.property.inputType,
|
||||
order_no: idx + 1,
|
||||
is_required: field.property.required,
|
||||
placeholder: field.description || null,
|
||||
default_value: null,
|
||||
display_condition: null,
|
||||
validation_rules: null,
|
||||
options: field.property.options
|
||||
? field.property.options.map(opt => ({ label: opt, value: opt }))
|
||||
: null,
|
||||
properties: field.property.multiColumn ? {
|
||||
multiColumn: true,
|
||||
columnCount: field.property.columnCount,
|
||||
columnNames: field.property.columnNames
|
||||
} : null,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
})) : [],
|
||||
bomItems: template.section_type === 'BOM' ? (template.bomItems || []) : undefined
|
||||
};
|
||||
|
||||
console.log('Linking template to page:', {
|
||||
templateId: template.id,
|
||||
templateName: template.template_name,
|
||||
pageId: selectedPage.id,
|
||||
newSection
|
||||
});
|
||||
|
||||
addSectionToPage(selectedPage.id, newSection);
|
||||
resetSectionForm();
|
||||
toast.success(`"${template.template_name}" 템플릿이 페이지에 연결되었습니다!`);
|
||||
};
|
||||
|
||||
// 섹션 제목 수정 시작
|
||||
const handleEditSectionTitle = (sectionId: number, currentTitle: string) => {
|
||||
setEditingSectionId(sectionId);
|
||||
setEditingSectionTitle(currentTitle);
|
||||
};
|
||||
|
||||
// 섹션 제목 저장
|
||||
const handleSaveSectionTitle = (selectedPage: ItemPage | undefined) => {
|
||||
if (!selectedPage || !editingSectionId || !editingSectionTitle.trim()) {
|
||||
toast.error('하위섹션 제목을 입력해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
updateSection(editingSectionId, { section_name: editingSectionTitle });
|
||||
setEditingSectionId(null);
|
||||
setEditingSectionTitle('');
|
||||
toast.success('하위섹션 제목이 수정되었습니다 (저장 필요)');
|
||||
};
|
||||
|
||||
// 섹션 삭제
|
||||
const handleDeleteSection = (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(sectionId);
|
||||
|
||||
console.log('섹션 삭제 완료:', {
|
||||
sectionId,
|
||||
removedFields: fieldIds.length
|
||||
});
|
||||
};
|
||||
|
||||
// 섹션 확장/축소 토글
|
||||
const toggleSection = (sectionId: string) => {
|
||||
setExpandedSections(prev => ({ ...prev, [sectionId]: !prev[sectionId] }));
|
||||
};
|
||||
|
||||
// 폼 초기화
|
||||
const resetSectionForm = () => {
|
||||
setNewSectionTitle('');
|
||||
setNewSectionDescription('');
|
||||
setNewSectionType('fields');
|
||||
setSectionInputMode('custom');
|
||||
setSelectedSectionTemplateId(null);
|
||||
setIsSectionDialogOpen(false);
|
||||
};
|
||||
|
||||
return {
|
||||
// 상태
|
||||
editingSectionId,
|
||||
setEditingSectionId,
|
||||
editingSectionTitle,
|
||||
setEditingSectionTitle,
|
||||
isSectionDialogOpen,
|
||||
setIsSectionDialogOpen,
|
||||
newSectionTitle,
|
||||
setNewSectionTitle,
|
||||
newSectionDescription,
|
||||
setNewSectionDescription,
|
||||
newSectionType,
|
||||
setNewSectionType,
|
||||
sectionInputMode,
|
||||
setSectionInputMode,
|
||||
selectedSectionTemplateId,
|
||||
setSelectedSectionTemplateId,
|
||||
expandedSections,
|
||||
setExpandedSections,
|
||||
|
||||
// 핸들러
|
||||
handleAddSection,
|
||||
handleLinkTemplate,
|
||||
handleEditSectionTitle,
|
||||
handleSaveSectionTitle,
|
||||
handleDeleteSection,
|
||||
toggleSection,
|
||||
resetSectionForm,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,441 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { useItemMaster } from '@/contexts/ItemMasterContext';
|
||||
import {
|
||||
FolderTree,
|
||||
ListTree,
|
||||
FileText,
|
||||
Settings,
|
||||
Layers,
|
||||
Database,
|
||||
Plus,
|
||||
Folder
|
||||
} from 'lucide-react';
|
||||
|
||||
export interface CustomTab {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
isDefault: boolean;
|
||||
order: number;
|
||||
}
|
||||
|
||||
export interface AttributeSubTab {
|
||||
id: string;
|
||||
label: string;
|
||||
key: string;
|
||||
isDefault: boolean;
|
||||
order: number;
|
||||
}
|
||||
|
||||
export interface UseTabManagementReturn {
|
||||
// 메인 탭 상태
|
||||
customTabs: CustomTab[];
|
||||
setCustomTabs: React.Dispatch<React.SetStateAction<CustomTab[]>>;
|
||||
activeTab: string;
|
||||
setActiveTab: (tab: string) => void;
|
||||
|
||||
// 속성 하위 탭 상태
|
||||
attributeSubTabs: AttributeSubTab[];
|
||||
setAttributeSubTabs: React.Dispatch<React.SetStateAction<AttributeSubTab[]>>;
|
||||
activeAttributeTab: string;
|
||||
setActiveAttributeTab: (tab: string) => void;
|
||||
|
||||
// 메인 탭 다이얼로그 상태
|
||||
isAddTabDialogOpen: boolean;
|
||||
setIsAddTabDialogOpen: (open: boolean) => void;
|
||||
isManageTabsDialogOpen: boolean;
|
||||
setIsManageTabsDialogOpen: (open: boolean) => void;
|
||||
newTabLabel: string;
|
||||
setNewTabLabel: (label: string) => void;
|
||||
editingTabId: string | null;
|
||||
setEditingTabId: (id: string | null) => void;
|
||||
deletingTabId: string | null;
|
||||
setDeletingTabId: (id: string | null) => void;
|
||||
isDeleteTabDialogOpen: boolean;
|
||||
setIsDeleteTabDialogOpen: (open: boolean) => void;
|
||||
|
||||
// 속성 하위 탭 다이얼로그 상태
|
||||
isManageAttributeTabsDialogOpen: boolean;
|
||||
setIsManageAttributeTabsDialogOpen: (open: boolean) => void;
|
||||
isAddAttributeTabDialogOpen: boolean;
|
||||
setIsAddAttributeTabDialogOpen: (open: boolean) => void;
|
||||
newAttributeTabLabel: string;
|
||||
setNewAttributeTabLabel: (label: string) => void;
|
||||
editingAttributeTabId: string | null;
|
||||
setEditingAttributeTabId: (id: string | null) => void;
|
||||
deletingAttributeTabId: string | null;
|
||||
setDeletingAttributeTabId: (id: string | null) => void;
|
||||
isDeleteAttributeTabDialogOpen: boolean;
|
||||
setIsDeleteAttributeTabDialogOpen: (open: boolean) => void;
|
||||
|
||||
// 핸들러
|
||||
handleAddTab: () => void;
|
||||
handleUpdateTab: () => void;
|
||||
handleDeleteTab: (tabId: string) => void;
|
||||
confirmDeleteTab: () => void;
|
||||
handleAddAttributeTab: () => void;
|
||||
handleUpdateAttributeTab: () => void;
|
||||
handleDeleteAttributeTab: (tabId: string) => void;
|
||||
confirmDeleteAttributeTab: () => void;
|
||||
moveTabUp: (tabId: string) => void;
|
||||
moveTabDown: (tabId: string) => void;
|
||||
moveAttributeTabUp: (tabId: string) => void;
|
||||
moveAttributeTabDown: (tabId: string) => void;
|
||||
getTabIcon: (iconName: string) => any;
|
||||
handleEditTabFromManage: (tab: CustomTab) => void;
|
||||
}
|
||||
|
||||
export function useTabManagement(): UseTabManagementReturn {
|
||||
const { itemMasterFields } = useItemMaster();
|
||||
|
||||
// 메인 탭 상태
|
||||
const [customTabs, setCustomTabs] = useState<CustomTab[]>([
|
||||
{ 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 }
|
||||
]);
|
||||
const [activeTab, setActiveTab] = useState('hierarchy');
|
||||
|
||||
// 속성 하위 탭 상태
|
||||
const [attributeSubTabs, setAttributeSubTabs] = useState<AttributeSubTab[]>([]);
|
||||
const [activeAttributeTab, setActiveAttributeTab] = useState('units');
|
||||
|
||||
// 메인 탭 다이얼로그 상태
|
||||
const [isAddTabDialogOpen, setIsAddTabDialogOpen] = useState(false);
|
||||
const [isManageTabsDialogOpen, setIsManageTabsDialogOpen] = useState(false);
|
||||
const [newTabLabel, setNewTabLabel] = useState('');
|
||||
const [editingTabId, setEditingTabId] = useState<string | null>(null);
|
||||
const [deletingTabId, setDeletingTabId] = useState<string | null>(null);
|
||||
const [isDeleteTabDialogOpen, setIsDeleteTabDialogOpen] = useState(false);
|
||||
|
||||
// 속성 하위 탭 다이얼로그 상태
|
||||
const [isManageAttributeTabsDialogOpen, setIsManageAttributeTabsDialogOpen] = useState(false);
|
||||
const [isAddAttributeTabDialogOpen, setIsAddAttributeTabDialogOpen] = useState(false);
|
||||
const [newAttributeTabLabel, setNewAttributeTabLabel] = useState('');
|
||||
const [editingAttributeTabId, setEditingAttributeTabId] = useState<string | null>(null);
|
||||
const [deletingAttributeTabId, setDeletingAttributeTabId] = useState<string | null>(null);
|
||||
const [isDeleteAttributeTabDialogOpen, setIsDeleteAttributeTabDialogOpen] = useState(false);
|
||||
|
||||
// 이전 필드 상태 추적용 ref (무한 루프 방지)
|
||||
const prevFieldsRef = useRef<string>('');
|
||||
|
||||
// 마스터 항목이 추가/수정될 때 속성 탭 자동 생성
|
||||
useEffect(() => {
|
||||
// 현재 필드 상태를 문자열로 직렬화
|
||||
const currentFieldsState = JSON.stringify(
|
||||
itemMasterFields.map(f => ({ id: f.id, name: f.field_name })).sort((a, b) => a.id - b.id)
|
||||
);
|
||||
|
||||
// 이전 상태와 동일하면 업데이트 스킵
|
||||
if (prevFieldsRef.current === currentFieldsState) {
|
||||
return;
|
||||
}
|
||||
prevFieldsRef.current = currentFieldsState;
|
||||
|
||||
setAttributeSubTabs(prev => {
|
||||
const newTabs: AttributeSubTab[] = [];
|
||||
const updates: { key: string; label: string }[] = [];
|
||||
|
||||
itemMasterFields.forEach(field => {
|
||||
const existingTab = prev.find(tab => tab.key === field.id.toString());
|
||||
|
||||
if (!existingTab) {
|
||||
const maxOrder = Math.max(...prev.map(t => t.order), ...newTabs.map(t => t.order), -1);
|
||||
newTabs.push({
|
||||
id: `attr-${field.id.toString()}`,
|
||||
label: field.field_name,
|
||||
key: field.id.toString(),
|
||||
isDefault: false,
|
||||
order: maxOrder + 1
|
||||
});
|
||||
} else if (existingTab.label !== field.field_name) {
|
||||
updates.push({ key: existingTab.key, label: field.field_name });
|
||||
}
|
||||
});
|
||||
|
||||
// 변경사항 없으면 이전 상태 그대로 반환
|
||||
if (newTabs.length === 0 && updates.length === 0) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
let result = prev.map(tab => {
|
||||
const update = updates.find(u => u.key === tab.key);
|
||||
return update ? { ...tab, label: update.label } : tab;
|
||||
});
|
||||
result = [...result, ...newTabs];
|
||||
|
||||
// 중복 제거
|
||||
return result.filter((tab, index, self) =>
|
||||
index === self.findIndex(t => t.key === tab.key)
|
||||
);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [itemMasterFields]);
|
||||
|
||||
// 메인 탭 핸들러
|
||||
const handleAddTab = () => {
|
||||
if (!newTabLabel.trim()) {
|
||||
toast.error('탭 이름을 입력해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
const newTab: CustomTab = {
|
||||
id: Date.now().toString(),
|
||||
label: newTabLabel,
|
||||
icon: 'FileText',
|
||||
isDefault: false,
|
||||
order: customTabs.length + 1
|
||||
};
|
||||
|
||||
setCustomTabs(prev => [...prev, newTab]);
|
||||
setNewTabLabel('');
|
||||
setIsAddTabDialogOpen(false);
|
||||
toast.success('탭이 추가되었습니다');
|
||||
};
|
||||
|
||||
const handleUpdateTab = () => {
|
||||
if (!newTabLabel.trim() || !editingTabId) {
|
||||
toast.error('탭 이름을 입력해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
setCustomTabs(prev => prev.map(tab =>
|
||||
tab.id === editingTabId ? { ...tab, label: newTabLabel } : tab
|
||||
));
|
||||
|
||||
setEditingTabId(null);
|
||||
setNewTabLabel('');
|
||||
setIsAddTabDialogOpen(false);
|
||||
setIsManageTabsDialogOpen(true);
|
||||
toast.success('탭이 수정되었습니다');
|
||||
};
|
||||
|
||||
const handleDeleteTab = (tabId: string) => {
|
||||
const tab = customTabs.find(t => t.id === tabId);
|
||||
if (!tab || tab.isDefault) {
|
||||
toast.error('기본 탭은 삭제할 수 없습니다');
|
||||
return;
|
||||
}
|
||||
|
||||
setDeletingTabId(tabId);
|
||||
setIsDeleteTabDialogOpen(true);
|
||||
};
|
||||
|
||||
const confirmDeleteTab = () => {
|
||||
if (!deletingTabId) return;
|
||||
|
||||
setCustomTabs(prev => prev.filter(t => t.id !== deletingTabId));
|
||||
if (activeTab === deletingTabId) {
|
||||
setActiveTab('hierarchy');
|
||||
}
|
||||
|
||||
setIsDeleteTabDialogOpen(false);
|
||||
setDeletingTabId(null);
|
||||
toast.success('탭이 삭제되었습니다');
|
||||
};
|
||||
|
||||
// 속성 하위 탭 핸들러
|
||||
const handleAddAttributeTab = () => {
|
||||
if (!newAttributeTabLabel.trim()) {
|
||||
toast.error('탭 이름을 입력해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
const newTab: AttributeSubTab = {
|
||||
id: `attr-${Date.now()}`,
|
||||
label: newAttributeTabLabel,
|
||||
key: `custom-${Date.now()}`,
|
||||
isDefault: false,
|
||||
order: attributeSubTabs.length
|
||||
};
|
||||
|
||||
setAttributeSubTabs(prev => [...prev, newTab]);
|
||||
setNewAttributeTabLabel('');
|
||||
setIsAddAttributeTabDialogOpen(false);
|
||||
toast.success('속성 탭이 추가되었습니다');
|
||||
};
|
||||
|
||||
const handleUpdateAttributeTab = () => {
|
||||
if (!newAttributeTabLabel.trim() || !editingAttributeTabId) {
|
||||
toast.error('탭 이름을 입력해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
setAttributeSubTabs(prev => prev.map(tab =>
|
||||
tab.id === editingAttributeTabId ? { ...tab, label: newAttributeTabLabel } : tab
|
||||
));
|
||||
|
||||
setEditingAttributeTabId(null);
|
||||
setNewAttributeTabLabel('');
|
||||
setIsAddAttributeTabDialogOpen(false);
|
||||
setIsManageAttributeTabsDialogOpen(true);
|
||||
toast.success('속성 탭이 수정되었습니다');
|
||||
};
|
||||
|
||||
const handleDeleteAttributeTab = (tabId: string) => {
|
||||
const tab = attributeSubTabs.find(t => t.id === tabId);
|
||||
if (!tab || tab.isDefault) {
|
||||
toast.error('기본 속성 탭은 삭제할 수 없습니다');
|
||||
return;
|
||||
}
|
||||
|
||||
setDeletingAttributeTabId(tabId);
|
||||
setIsDeleteAttributeTabDialogOpen(true);
|
||||
};
|
||||
|
||||
const confirmDeleteAttributeTab = () => {
|
||||
if (!deletingAttributeTabId) return;
|
||||
|
||||
setAttributeSubTabs(prev => prev.filter(t => t.id !== deletingAttributeTabId));
|
||||
if (activeAttributeTab === deletingAttributeTabId) {
|
||||
const firstTab = attributeSubTabs.find(t => t.id !== deletingAttributeTabId);
|
||||
if (firstTab) {
|
||||
setActiveAttributeTab(firstTab.key);
|
||||
}
|
||||
}
|
||||
|
||||
setIsDeleteAttributeTabDialogOpen(false);
|
||||
setDeletingAttributeTabId(null);
|
||||
toast.success('속성 탭이 삭제되었습니다');
|
||||
};
|
||||
|
||||
// 탭 순서 변경 핸들러
|
||||
const moveTabUp = (tabId: string) => {
|
||||
const tabIndex = customTabs.findIndex(t => t.id === tabId);
|
||||
if (tabIndex <= 0) return;
|
||||
|
||||
const newTabs = [...customTabs];
|
||||
const temp = newTabs[tabIndex - 1].order;
|
||||
newTabs[tabIndex - 1].order = newTabs[tabIndex].order;
|
||||
newTabs[tabIndex].order = temp;
|
||||
|
||||
setCustomTabs(newTabs.sort((a, b) => a.order - b.order));
|
||||
toast.success('탭 순서가 변경되었습니다');
|
||||
};
|
||||
|
||||
const moveTabDown = (tabId: string) => {
|
||||
const tabIndex = customTabs.findIndex(t => t.id === tabId);
|
||||
if (tabIndex >= customTabs.length - 1) return;
|
||||
|
||||
const newTabs = [...customTabs];
|
||||
const temp = newTabs[tabIndex + 1].order;
|
||||
newTabs[tabIndex + 1].order = newTabs[tabIndex].order;
|
||||
newTabs[tabIndex].order = temp;
|
||||
|
||||
setCustomTabs(newTabs.sort((a, b) => a.order - b.order));
|
||||
toast.success('탭 순서가 변경되었습니다');
|
||||
};
|
||||
|
||||
const moveAttributeTabUp = (tabId: string) => {
|
||||
const tabIndex = attributeSubTabs.findIndex(t => t.id === tabId);
|
||||
if (tabIndex <= 0) return;
|
||||
|
||||
const newTabs = [...attributeSubTabs];
|
||||
const temp = newTabs[tabIndex - 1].order;
|
||||
newTabs[tabIndex - 1].order = newTabs[tabIndex].order;
|
||||
newTabs[tabIndex].order = temp;
|
||||
|
||||
setAttributeSubTabs(newTabs.sort((a, b) => a.order - b.order));
|
||||
};
|
||||
|
||||
const moveAttributeTabDown = (tabId: string) => {
|
||||
const tabIndex = attributeSubTabs.findIndex(t => t.id === tabId);
|
||||
if (tabIndex >= attributeSubTabs.length - 1) return;
|
||||
|
||||
const newTabs = [...attributeSubTabs];
|
||||
const temp = newTabs[tabIndex + 1].order;
|
||||
newTabs[tabIndex + 1].order = newTabs[tabIndex].order;
|
||||
newTabs[tabIndex].order = temp;
|
||||
|
||||
setAttributeSubTabs(newTabs.sort((a, b) => a.order - b.order));
|
||||
};
|
||||
|
||||
// 아이콘 헬퍼
|
||||
const getTabIcon = (iconName: string) => {
|
||||
const icons: Record<string, any> = {
|
||||
FolderTree,
|
||||
ListTree,
|
||||
FileText,
|
||||
Settings,
|
||||
Layers,
|
||||
Database,
|
||||
Plus,
|
||||
Folder
|
||||
};
|
||||
return icons[iconName] || FileText;
|
||||
};
|
||||
|
||||
// 탭 관리에서 수정 시작
|
||||
const handleEditTabFromManage = (tab: CustomTab) => {
|
||||
if (tab.isDefault) {
|
||||
toast.error('기본 탭은 수정할 수 없습니다');
|
||||
return;
|
||||
}
|
||||
setEditingTabId(tab.id);
|
||||
setNewTabLabel(tab.label);
|
||||
setIsManageTabsDialogOpen(false);
|
||||
setIsAddTabDialogOpen(true);
|
||||
};
|
||||
|
||||
return {
|
||||
// 메인 탭 상태
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { useItemMaster } from '@/contexts/ItemMasterContext';
|
||||
import type { ItemPage, SectionTemplate, TemplateField, BOMItem, ItemMasterField } from '@/contexts/ItemMasterContext';
|
||||
|
||||
export interface UseTemplateManagementReturn {
|
||||
// 섹션 템플릿 다이얼로그 상태
|
||||
isSectionTemplateDialogOpen: boolean;
|
||||
setIsSectionTemplateDialogOpen: (open: boolean) => void;
|
||||
editingSectionTemplateId: number | null;
|
||||
setEditingSectionTemplateId: (id: number | null) => void;
|
||||
|
||||
// 섹션 템플릿 폼 상태
|
||||
newSectionTemplateTitle: string;
|
||||
setNewSectionTemplateTitle: (title: string) => void;
|
||||
newSectionTemplateDescription: string;
|
||||
setNewSectionTemplateDescription: (desc: string) => void;
|
||||
newSectionTemplateCategory: string[];
|
||||
setNewSectionTemplateCategory: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
newSectionTemplateType: 'fields' | 'bom';
|
||||
setNewSectionTemplateType: (type: 'fields' | 'bom') => void;
|
||||
|
||||
// 템플릿 불러오기 다이얼로그
|
||||
isLoadTemplateDialogOpen: boolean;
|
||||
setIsLoadTemplateDialogOpen: (open: boolean) => void;
|
||||
selectedTemplateId: string | null;
|
||||
setSelectedTemplateId: (id: string | null) => void;
|
||||
|
||||
// 템플릿 필드 다이얼로그 상태
|
||||
isTemplateFieldDialogOpen: boolean;
|
||||
setIsTemplateFieldDialogOpen: (open: boolean) => void;
|
||||
currentTemplateId: number | null;
|
||||
setCurrentTemplateId: (id: number | null) => void;
|
||||
editingTemplateFieldId: number | null;
|
||||
setEditingTemplateFieldId: (id: number | null) => void;
|
||||
|
||||
// 템플릿 필드 폼 상태
|
||||
templateFieldName: string;
|
||||
setTemplateFieldName: (name: string) => void;
|
||||
templateFieldKey: string;
|
||||
setTemplateFieldKey: (key: string) => void;
|
||||
templateFieldInputType: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea';
|
||||
setTemplateFieldInputType: (type: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea') => void;
|
||||
templateFieldRequired: boolean;
|
||||
setTemplateFieldRequired: (required: boolean) => void;
|
||||
templateFieldOptions: string;
|
||||
setTemplateFieldOptions: (options: string) => void;
|
||||
templateFieldDescription: string;
|
||||
setTemplateFieldDescription: (desc: string) => void;
|
||||
templateFieldMultiColumn: boolean;
|
||||
setTemplateFieldMultiColumn: (multi: boolean) => void;
|
||||
templateFieldColumnCount: number;
|
||||
setTemplateFieldColumnCount: (count: number) => void;
|
||||
templateFieldColumnNames: string[];
|
||||
setTemplateFieldColumnNames: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
|
||||
// 템플릿 필드 마스터 항목 관련
|
||||
templateFieldInputMode: 'custom' | 'master';
|
||||
setTemplateFieldInputMode: (mode: 'custom' | 'master') => void;
|
||||
templateFieldShowMasterFieldList: boolean;
|
||||
setTemplateFieldShowMasterFieldList: (show: boolean) => void;
|
||||
templateFieldSelectedMasterFieldId: string;
|
||||
setTemplateFieldSelectedMasterFieldId: (id: string) => void;
|
||||
|
||||
// 핸들러
|
||||
handleAddSectionTemplate: () => void;
|
||||
handleEditSectionTemplate: (template: SectionTemplate) => void;
|
||||
handleUpdateSectionTemplate: () => void;
|
||||
handleDeleteSectionTemplate: (id: number) => void;
|
||||
handleLoadTemplate: (selectedPage: ItemPage | undefined) => void;
|
||||
handleAddTemplateField: () => void;
|
||||
handleEditTemplateField: (templateId: number, field: TemplateField) => void;
|
||||
handleDeleteTemplateField: (templateId: number, fieldId: string) => void;
|
||||
handleAddBOMItemToTemplate: (templateId: number, item: Omit<BOMItem, 'id' | 'created_at' | 'updated_at' | 'tenant_id' | 'section_id'>) => void;
|
||||
handleUpdateBOMItemInTemplate: (templateId: number, itemId: number, item: Partial<BOMItem>) => void;
|
||||
handleDeleteBOMItemFromTemplate: (templateId: number, itemId: number) => void;
|
||||
resetSectionTemplateForm: () => void;
|
||||
resetTemplateFieldForm: () => void;
|
||||
}
|
||||
|
||||
export function useTemplateManagement(): UseTemplateManagementReturn {
|
||||
const {
|
||||
sectionTemplates,
|
||||
addSectionTemplate,
|
||||
updateSectionTemplate,
|
||||
deleteSectionTemplate,
|
||||
addSectionToPage,
|
||||
addItemMasterField,
|
||||
itemMasterFields,
|
||||
tenantId
|
||||
} = useItemMaster();
|
||||
|
||||
// 섹션 템플릿 다이얼로그 상태
|
||||
const [isSectionTemplateDialogOpen, setIsSectionTemplateDialogOpen] = useState(false);
|
||||
const [editingSectionTemplateId, setEditingSectionTemplateId] = useState<number | null>(null);
|
||||
|
||||
// 섹션 템플릿 폼 상태
|
||||
const [newSectionTemplateTitle, setNewSectionTemplateTitle] = useState('');
|
||||
const [newSectionTemplateDescription, setNewSectionTemplateDescription] = useState('');
|
||||
const [newSectionTemplateCategory, setNewSectionTemplateCategory] = useState<string[]>([]);
|
||||
const [newSectionTemplateType, setNewSectionTemplateType] = useState<'fields' | 'bom'>('fields');
|
||||
|
||||
// 템플릿 불러오기 다이얼로그
|
||||
const [isLoadTemplateDialogOpen, setIsLoadTemplateDialogOpen] = useState(false);
|
||||
const [selectedTemplateId, setSelectedTemplateId] = useState<string | null>(null);
|
||||
|
||||
// 템플릿 필드 다이얼로그 상태
|
||||
const [isTemplateFieldDialogOpen, setIsTemplateFieldDialogOpen] = useState(false);
|
||||
const [currentTemplateId, setCurrentTemplateId] = useState<number | null>(null);
|
||||
const [editingTemplateFieldId, setEditingTemplateFieldId] = useState<number | null>(null);
|
||||
|
||||
// 템플릿 필드 폼 상태
|
||||
const [templateFieldName, setTemplateFieldName] = useState('');
|
||||
const [templateFieldKey, setTemplateFieldKey] = useState('');
|
||||
const [templateFieldInputType, setTemplateFieldInputType] = useState<'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea'>('textbox');
|
||||
const [templateFieldRequired, setTemplateFieldRequired] = useState(false);
|
||||
const [templateFieldOptions, setTemplateFieldOptions] = useState('');
|
||||
const [templateFieldDescription, setTemplateFieldDescription] = useState('');
|
||||
const [templateFieldMultiColumn, setTemplateFieldMultiColumn] = useState(false);
|
||||
const [templateFieldColumnCount, setTemplateFieldColumnCount] = useState(2);
|
||||
const [templateFieldColumnNames, setTemplateFieldColumnNames] = useState<string[]>(['컬럼1', '컬럼2']);
|
||||
|
||||
// 템플릿 필드 마스터 항목 관련
|
||||
const [templateFieldInputMode, setTemplateFieldInputMode] = useState<'custom' | 'master'>('custom');
|
||||
const [templateFieldShowMasterFieldList, setTemplateFieldShowMasterFieldList] = useState(false);
|
||||
const [templateFieldSelectedMasterFieldId, setTemplateFieldSelectedMasterFieldId] = useState('');
|
||||
|
||||
// 섹션 템플릿 추가
|
||||
const handleAddSectionTemplate = () => {
|
||||
if (!newSectionTemplateTitle.trim()) {
|
||||
toast.error('섹션 제목을 입력해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
const newTemplateData = {
|
||||
tenant_id: tenantId ?? 0,
|
||||
template_name: newSectionTemplateTitle,
|
||||
section_type: (newSectionTemplateType === 'bom' ? 'BOM' : 'BASIC') as 'BASIC' | 'BOM' | 'CUSTOM',
|
||||
description: newSectionTemplateDescription || null,
|
||||
default_fields: null,
|
||||
category: newSectionTemplateCategory,
|
||||
created_by: null,
|
||||
updated_by: null,
|
||||
};
|
||||
|
||||
console.log('Adding section template:', newTemplateData);
|
||||
addSectionTemplate(newTemplateData);
|
||||
resetSectionTemplateForm();
|
||||
toast.success('섹션 템플릿이 추가되었습니다!');
|
||||
};
|
||||
|
||||
// 섹션 템플릿 수정 시작
|
||||
const handleEditSectionTemplate = (template: SectionTemplate) => {
|
||||
setEditingSectionTemplateId(template.id);
|
||||
setNewSectionTemplateTitle(template.template_name);
|
||||
setNewSectionTemplateDescription(template.description || '');
|
||||
setNewSectionTemplateCategory(template.category || []);
|
||||
setNewSectionTemplateType(template.section_type === 'BOM' ? 'bom' : 'fields');
|
||||
setIsSectionTemplateDialogOpen(true);
|
||||
};
|
||||
|
||||
// 섹션 템플릿 업데이트
|
||||
const handleUpdateSectionTemplate = () => {
|
||||
if (!editingSectionTemplateId || !newSectionTemplateTitle.trim()) {
|
||||
toast.error('섹션 제목을 입력해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
const updateData = {
|
||||
template_name: newSectionTemplateTitle,
|
||||
description: newSectionTemplateDescription || undefined,
|
||||
category: newSectionTemplateCategory.length > 0 ? newSectionTemplateCategory : undefined,
|
||||
section_type: (newSectionTemplateType === 'bom' ? 'BOM' : 'BASIC') as 'BASIC' | 'BOM' | 'CUSTOM'
|
||||
};
|
||||
|
||||
console.log('Updating section template:', { id: editingSectionTemplateId, updateData });
|
||||
updateSectionTemplate(editingSectionTemplateId, updateData);
|
||||
resetSectionTemplateForm();
|
||||
toast.success('섹션이 수정되었습니다 (저장 필요)');
|
||||
};
|
||||
|
||||
// 섹션 템플릿 삭제
|
||||
const handleDeleteSectionTemplate = (id: number) => {
|
||||
if (confirm('이 섹션을 삭제하시겠습니까?')) {
|
||||
deleteSectionTemplate(id);
|
||||
toast.success('섹션이 삭제되었습니다');
|
||||
}
|
||||
};
|
||||
|
||||
// 템플릿 불러오기
|
||||
const handleLoadTemplate = (selectedPage: ItemPage | undefined) => {
|
||||
if (!selectedTemplateId || !selectedPage) {
|
||||
toast.error('템플릿을 선택해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
const template = sectionTemplates.find(t => t.id === Number(selectedTemplateId));
|
||||
if (!template) {
|
||||
toast.error('템플릿을 찾을 수 없습니다');
|
||||
return;
|
||||
}
|
||||
|
||||
const newSection = {
|
||||
page_id: selectedPage.id,
|
||||
section_name: template.template_name,
|
||||
section_type: template.section_type === 'BOM' ? 'BOM' as const : 'BASIC' as const,
|
||||
description: template.description || undefined,
|
||||
order_no: selectedPage.sections.length + 1,
|
||||
is_collapsible: true,
|
||||
is_default_open: true,
|
||||
fields: [],
|
||||
bomItems: template.section_type === 'BOM' ? [] : undefined
|
||||
};
|
||||
|
||||
console.log('Loading template to section:', template.template_name, 'newSection:', newSection);
|
||||
addSectionToPage(selectedPage.id, newSection);
|
||||
setSelectedTemplateId(null);
|
||||
setIsLoadTemplateDialogOpen(false);
|
||||
toast.success('섹션이 불러와졌습니다');
|
||||
};
|
||||
|
||||
// 템플릿 필드 추가
|
||||
const handleAddTemplateField = () => {
|
||||
if (!currentTemplateId || !templateFieldName.trim() || !templateFieldKey.trim()) {
|
||||
toast.error('모든 필수 항목을 입력해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
const template = sectionTemplates.find(t => t.id === currentTemplateId);
|
||||
if (!template) return;
|
||||
|
||||
// 마스터 필드에 없으면 자동 추가
|
||||
const existingMasterField = itemMasterFields.find(f => f.id.toString() === templateFieldKey);
|
||||
if (!existingMasterField && !editingTemplateFieldId) {
|
||||
const newMasterFieldData: Omit<ItemMasterField, 'id' | 'tenant_id' | 'created_by' | 'updated_by' | 'created_at' | 'updated_at'> = {
|
||||
field_name: templateFieldName,
|
||||
field_type: templateFieldInputType,
|
||||
category: '공통',
|
||||
description: templateFieldDescription || null,
|
||||
is_common: false,
|
||||
default_value: null,
|
||||
options: templateFieldInputType === 'dropdown' && templateFieldOptions.trim()
|
||||
? templateFieldOptions.split(',').map(o => ({ label: o.trim(), value: o.trim() }))
|
||||
: null,
|
||||
validation_rules: null,
|
||||
properties: {
|
||||
inputType: templateFieldInputType,
|
||||
required: templateFieldRequired,
|
||||
multiColumn: (templateFieldInputType === 'textbox' || templateFieldInputType === 'textarea') ? templateFieldMultiColumn : undefined,
|
||||
columnCount: (templateFieldInputType === 'textbox' || templateFieldInputType === 'textarea') && templateFieldMultiColumn ? templateFieldColumnCount : undefined,
|
||||
columnNames: (templateFieldInputType === 'textbox' || templateFieldInputType === 'textarea') && templateFieldMultiColumn ? templateFieldColumnNames : undefined
|
||||
},
|
||||
};
|
||||
addItemMasterField(newMasterFieldData as any);
|
||||
toast.success('항목 탭에 자동으로 추가되었습니다');
|
||||
}
|
||||
|
||||
// TemplateField 형식으로 생성
|
||||
const newField: TemplateField = {
|
||||
id: String(editingTemplateFieldId || Date.now()),
|
||||
name: templateFieldName,
|
||||
fieldKey: templateFieldKey,
|
||||
property: {
|
||||
inputType: templateFieldInputType,
|
||||
required: templateFieldRequired,
|
||||
options: templateFieldInputType === 'dropdown' && templateFieldOptions.trim()
|
||||
? templateFieldOptions.split(',').map(o => o.trim())
|
||||
: undefined,
|
||||
multiColumn: (templateFieldInputType === 'textbox' || templateFieldInputType === 'textarea') ? templateFieldMultiColumn : undefined,
|
||||
columnCount: (templateFieldInputType === 'textbox' || templateFieldInputType === 'textarea') && templateFieldMultiColumn ? templateFieldColumnCount : undefined,
|
||||
columnNames: (templateFieldInputType === 'textbox' || templateFieldInputType === 'textarea') && templateFieldMultiColumn ? templateFieldColumnNames : undefined
|
||||
},
|
||||
description: templateFieldDescription || undefined
|
||||
};
|
||||
|
||||
let updatedFields;
|
||||
const currentFields = template.default_fields
|
||||
? (typeof template.default_fields === 'string' ? JSON.parse(template.default_fields) : template.default_fields)
|
||||
: [];
|
||||
|
||||
if (editingTemplateFieldId) {
|
||||
updatedFields = Array.isArray(currentFields)
|
||||
? currentFields.map((f: any) => String(f.id) === String(editingTemplateFieldId) ? newField : f)
|
||||
: [];
|
||||
toast.success('항목이 수정되었습니다');
|
||||
} else {
|
||||
updatedFields = Array.isArray(currentFields) ? [...currentFields, newField] : [newField];
|
||||
toast.success('항목이 추가되었습니다');
|
||||
}
|
||||
|
||||
updateSectionTemplate(currentTemplateId, { default_fields: updatedFields });
|
||||
resetTemplateFieldForm();
|
||||
};
|
||||
|
||||
// 템플릿 필드 수정 시작
|
||||
const handleEditTemplateField = (templateId: number, field: TemplateField) => {
|
||||
setCurrentTemplateId(templateId);
|
||||
setEditingTemplateFieldId(Number(field.id));
|
||||
setTemplateFieldName(field.name);
|
||||
setTemplateFieldKey(field.fieldKey);
|
||||
setTemplateFieldInputType(field.property.inputType);
|
||||
setTemplateFieldRequired(field.property.required);
|
||||
setTemplateFieldOptions(field.property.options?.join(', ') || '');
|
||||
setTemplateFieldDescription(field.description || '');
|
||||
setTemplateFieldMultiColumn(field.property.multiColumn || false);
|
||||
setTemplateFieldColumnCount(field.property.columnCount || 2);
|
||||
setTemplateFieldColumnNames(field.property.columnNames || ['컬럼1', '컬럼2']);
|
||||
setIsTemplateFieldDialogOpen(true);
|
||||
};
|
||||
|
||||
// 템플릿 필드 삭제
|
||||
const handleDeleteTemplateField = (templateId: number, fieldId: string) => {
|
||||
if (!confirm('이 항목을 삭제하시겠습니까?')) return;
|
||||
|
||||
const template = sectionTemplates.find(t => t.id === templateId);
|
||||
if (!template) return;
|
||||
|
||||
const currentFields = template.default_fields
|
||||
? (typeof template.default_fields === 'string' ? JSON.parse(template.default_fields) : template.default_fields)
|
||||
: [];
|
||||
const updatedFields = Array.isArray(currentFields)
|
||||
? currentFields.filter((f: any) => String(f.id) !== String(fieldId))
|
||||
: [];
|
||||
updateSectionTemplate(templateId, { default_fields: updatedFields });
|
||||
toast.success('항목이 삭제되었습니다');
|
||||
};
|
||||
|
||||
// BOM 항목 추가
|
||||
const handleAddBOMItemToTemplate = (templateId: number, item: Omit<BOMItem, 'id' | 'created_at' | 'updated_at' | 'tenant_id' | 'section_id'>) => {
|
||||
const newItem: BOMItem = {
|
||||
...item,
|
||||
id: Date.now(),
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
tenant_id: tenantId ?? 0,
|
||||
section_id: 0
|
||||
};
|
||||
|
||||
const template = sectionTemplates.find(t => t.id === templateId);
|
||||
if (!template) return;
|
||||
|
||||
const updatedBomItems = [...(template.bomItems || []), newItem];
|
||||
updateSectionTemplate(templateId, { bomItems: updatedBomItems });
|
||||
};
|
||||
|
||||
// BOM 항목 수정
|
||||
const handleUpdateBOMItemInTemplate = (templateId: number, itemId: number, item: Partial<BOMItem>) => {
|
||||
const template = sectionTemplates.find(t => t.id === templateId);
|
||||
if (!template || !template.bomItems) return;
|
||||
|
||||
const updatedBomItems = template.bomItems.map(bom =>
|
||||
bom.id === itemId ? { ...bom, ...item } : bom
|
||||
);
|
||||
updateSectionTemplate(templateId, { bomItems: updatedBomItems });
|
||||
};
|
||||
|
||||
// BOM 항목 삭제
|
||||
const handleDeleteBOMItemFromTemplate = (templateId: number, itemId: number) => {
|
||||
const template = sectionTemplates.find(t => t.id === templateId);
|
||||
if (!template || !template.bomItems) return;
|
||||
|
||||
const updatedBomItems = template.bomItems.filter(bom => bom.id !== itemId);
|
||||
updateSectionTemplate(templateId, { bomItems: updatedBomItems });
|
||||
};
|
||||
|
||||
// 섹션 템플릿 폼 초기화
|
||||
const resetSectionTemplateForm = () => {
|
||||
setEditingSectionTemplateId(null);
|
||||
setNewSectionTemplateTitle('');
|
||||
setNewSectionTemplateDescription('');
|
||||
setNewSectionTemplateCategory([]);
|
||||
setNewSectionTemplateType('fields');
|
||||
setIsSectionTemplateDialogOpen(false);
|
||||
};
|
||||
|
||||
// 템플릿 필드 폼 초기화
|
||||
const resetTemplateFieldForm = () => {
|
||||
setTemplateFieldName('');
|
||||
setTemplateFieldKey('');
|
||||
setTemplateFieldInputType('textbox');
|
||||
setTemplateFieldRequired(false);
|
||||
setTemplateFieldOptions('');
|
||||
setTemplateFieldDescription('');
|
||||
setTemplateFieldMultiColumn(false);
|
||||
setTemplateFieldColumnCount(2);
|
||||
setTemplateFieldColumnNames(['컬럼1', '컬럼2']);
|
||||
setEditingTemplateFieldId(null);
|
||||
setTemplateFieldInputMode('custom');
|
||||
setTemplateFieldShowMasterFieldList(false);
|
||||
setTemplateFieldSelectedMasterFieldId('');
|
||||
setIsTemplateFieldDialogOpen(false);
|
||||
};
|
||||
|
||||
return {
|
||||
// 섹션 템플릿 다이얼로그 상태
|
||||
isSectionTemplateDialogOpen,
|
||||
setIsSectionTemplateDialogOpen,
|
||||
editingSectionTemplateId,
|
||||
setEditingSectionTemplateId,
|
||||
|
||||
// 섹션 템플릿 폼 상태
|
||||
newSectionTemplateTitle,
|
||||
setNewSectionTemplateTitle,
|
||||
newSectionTemplateDescription,
|
||||
setNewSectionTemplateDescription,
|
||||
newSectionTemplateCategory,
|
||||
setNewSectionTemplateCategory,
|
||||
newSectionTemplateType,
|
||||
setNewSectionTemplateType,
|
||||
|
||||
// 템플릿 불러오기 다이얼로그
|
||||
isLoadTemplateDialogOpen,
|
||||
setIsLoadTemplateDialogOpen,
|
||||
selectedTemplateId,
|
||||
setSelectedTemplateId,
|
||||
|
||||
// 템플릿 필드 다이얼로그 상태
|
||||
isTemplateFieldDialogOpen,
|
||||
setIsTemplateFieldDialogOpen,
|
||||
currentTemplateId,
|
||||
setCurrentTemplateId,
|
||||
editingTemplateFieldId,
|
||||
setEditingTemplateFieldId,
|
||||
|
||||
// 템플릿 필드 폼 상태
|
||||
templateFieldName,
|
||||
setTemplateFieldName,
|
||||
templateFieldKey,
|
||||
setTemplateFieldKey,
|
||||
templateFieldInputType,
|
||||
setTemplateFieldInputType,
|
||||
templateFieldRequired,
|
||||
setTemplateFieldRequired,
|
||||
templateFieldOptions,
|
||||
setTemplateFieldOptions,
|
||||
templateFieldDescription,
|
||||
setTemplateFieldDescription,
|
||||
templateFieldMultiColumn,
|
||||
setTemplateFieldMultiColumn,
|
||||
templateFieldColumnCount,
|
||||
setTemplateFieldColumnCount,
|
||||
templateFieldColumnNames,
|
||||
setTemplateFieldColumnNames,
|
||||
|
||||
// 템플릿 필드 마스터 항목 관련
|
||||
templateFieldInputMode,
|
||||
setTemplateFieldInputMode,
|
||||
templateFieldShowMasterFieldList,
|
||||
setTemplateFieldShowMasterFieldList,
|
||||
templateFieldSelectedMasterFieldId,
|
||||
setTemplateFieldSelectedMasterFieldId,
|
||||
|
||||
// 핸들러
|
||||
handleAddSectionTemplate,
|
||||
handleEditSectionTemplate,
|
||||
handleUpdateSectionTemplate,
|
||||
handleDeleteSectionTemplate,
|
||||
handleLoadTemplate,
|
||||
handleAddTemplateField,
|
||||
handleEditTemplateField,
|
||||
handleDeleteTemplateField,
|
||||
handleAddBOMItemToTemplate,
|
||||
handleUpdateBOMItemInTemplate,
|
||||
handleDeleteBOMItemFromTemplate,
|
||||
resetSectionTemplateForm,
|
||||
resetTemplateFieldForm,
|
||||
};
|
||||
}
|
||||
@@ -25,8 +25,8 @@ interface HierarchyTabProps {
|
||||
setEditingPathPageId: (id: number | null) => void;
|
||||
editingAbsolutePath: string;
|
||||
setEditingAbsolutePath: (path: string) => void;
|
||||
editingSectionId: string | null;
|
||||
setEditingSectionId: (id: string | null) => void;
|
||||
editingSectionId: number | null;
|
||||
setEditingSectionId: (id: number | null) => void;
|
||||
editingSectionTitle: string;
|
||||
setEditingSectionTitle: (title: string) => void;
|
||||
hasUnsavedChanges: boolean;
|
||||
@@ -51,7 +51,7 @@ interface HierarchyTabProps {
|
||||
setIsPageDialogOpen: (open: boolean) => void;
|
||||
setIsSectionDialogOpen: (open: boolean) => void;
|
||||
setIsFieldDialogOpen: (open: boolean) => void;
|
||||
handleEditSectionTitle: (sectionId: string, title: string) => void;
|
||||
handleEditSectionTitle: (sectionId: number, title: string) => void;
|
||||
handleSaveSectionTitle: () => void;
|
||||
moveSection: (dragIndex: number, hoverIndex: number) => void;
|
||||
deleteSection: (pageId: number, sectionId: number) => void;
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user