feat: 품목관리 동적 렌더링 시스템 구현
- DynamicItemForm 컴포넌트 구조 생성
- DynamicField: 필드 타입별 렌더링
- DynamicSection: 섹션 단위 렌더링
- DynamicFormRenderer: 페이지 전체 렌더링
- 필드 타입별 컴포넌트 (TextField, NumberField, DropdownField, CheckboxField, DateField, FileField, CustomField)
- 커스텀 훅 (useDynamicFormState, useFormStructure, useConditionalFields)
- DataTable 공통 컴포넌트 (테이블, 페이지네이션, 검색, 탭필터, 통계카드)
- ItemFormWrapper: Feature Flag 기반 폼 선택
- 타입 정의 및 문서화
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 20:14:43 +09:00
|
|
|
|
/**
|
2025-12-04 12:48:41 +09:00
|
|
|
|
* DynamicItemForm - 품목기준관리 API 기반 동적 품목 등록 폼
|
feat: 품목관리 동적 렌더링 시스템 구현
- DynamicItemForm 컴포넌트 구조 생성
- DynamicField: 필드 타입별 렌더링
- DynamicSection: 섹션 단위 렌더링
- DynamicFormRenderer: 페이지 전체 렌더링
- 필드 타입별 컴포넌트 (TextField, NumberField, DropdownField, CheckboxField, DateField, FileField, CustomField)
- 커스텀 훅 (useDynamicFormState, useFormStructure, useConditionalFields)
- DataTable 공통 컴포넌트 (테이블, 페이지네이션, 검색, 탭필터, 통계카드)
- ItemFormWrapper: Feature Flag 기반 폼 선택
- 타입 정의 및 문서화
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 20:14:43 +09:00
|
|
|
|
*
|
2025-12-04 12:48:41 +09:00
|
|
|
|
* 기존 ItemForm과 100% 동일한 디자인 유지
|
feat: 품목관리 동적 렌더링 시스템 구현
- DynamicItemForm 컴포넌트 구조 생성
- DynamicField: 필드 타입별 렌더링
- DynamicSection: 섹션 단위 렌더링
- DynamicFormRenderer: 페이지 전체 렌더링
- 필드 타입별 컴포넌트 (TextField, NumberField, DropdownField, CheckboxField, DateField, FileField, CustomField)
- 커스텀 훅 (useDynamicFormState, useFormStructure, useConditionalFields)
- DataTable 공통 컴포넌트 (테이블, 페이지네이션, 검색, 탭필터, 통계카드)
- ItemFormWrapper: Feature Flag 기반 폼 선택
- 타입 정의 및 문서화
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 20:14:43 +09:00
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
'use client';
|
|
|
|
|
|
|
2025-12-16 17:40:55 +09:00
|
|
|
|
import { useState, useEffect, useMemo } from 'react';
|
2025-12-04 12:48:41 +09:00
|
|
|
|
import { useRouter } from 'next/navigation';
|
|
|
|
|
|
import { cn } from '@/lib/utils';
|
|
|
|
|
|
import { Label } from '@/components/ui/label';
|
|
|
|
|
|
import { Input } from '@/components/ui/input';
|
|
|
|
|
|
import { PageLoadingSpinner } from '@/components/ui/loading-spinner';
|
feat: 품목관리 동적 렌더링 시스템 구현
- DynamicItemForm 컴포넌트 구조 생성
- DynamicField: 필드 타입별 렌더링
- DynamicSection: 섹션 단위 렌더링
- DynamicFormRenderer: 페이지 전체 렌더링
- 필드 타입별 컴포넌트 (TextField, NumberField, DropdownField, CheckboxField, DateField, FileField, CustomField)
- 커스텀 훅 (useDynamicFormState, useFormStructure, useConditionalFields)
- DataTable 공통 컴포넌트 (테이블, 페이지네이션, 검색, 탭필터, 통계카드)
- ItemFormWrapper: Feature Flag 기반 폼 선택
- 타입 정의 및 문서화
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 20:14:43 +09:00
|
|
|
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
2025-12-04 12:48:41 +09:00
|
|
|
|
import {
|
|
|
|
|
|
Card,
|
|
|
|
|
|
CardContent,
|
|
|
|
|
|
CardHeader,
|
|
|
|
|
|
CardTitle,
|
|
|
|
|
|
} from '@/components/ui/card';
|
|
|
|
|
|
import ItemTypeSelect from '../ItemTypeSelect';
|
|
|
|
|
|
import BendingDiagramSection from '../ItemForm/BendingDiagramSection';
|
|
|
|
|
|
import { DrawingCanvas } from '../DrawingCanvas';
|
2025-12-16 17:40:55 +09:00
|
|
|
|
import { useFormStructure, useDynamicFormState, useConditionalDisplay, useFieldDetection, useItemCodeGeneration, usePartTypeHandling, useFileHandling } from './hooks';
|
2025-12-04 12:48:41 +09:00
|
|
|
|
import { DynamicFieldRenderer } from './fields';
|
|
|
|
|
|
import { DynamicBOMSection } from './sections';
|
2025-12-16 17:40:55 +09:00
|
|
|
|
import { FormHeader, ValidationAlert, FileUploadFields, DuplicateCodeDialog } from './components';
|
|
|
|
|
|
import type { DynamicItemFormProps, DynamicFormData, BOMLine, BOMSearchState } from './types';
|
2025-12-04 12:48:41 +09:00
|
|
|
|
import type { ItemType, BendingDetail } from '@/types/item';
|
|
|
|
|
|
import type { ItemFieldResponse } from '@/types/item-master-api';
|
2025-12-16 17:40:55 +09:00
|
|
|
|
import { uploadItemFile, ItemFileType, checkItemCodeDuplicate, DuplicateCheckResult } from '@/lib/api/items';
|
2025-12-12 18:35:43 +09:00
|
|
|
|
import { DuplicateCodeError } from '@/lib/api/error-handler';
|
feat: 품목관리 동적 렌더링 시스템 구현
- DynamicItemForm 컴포넌트 구조 생성
- DynamicField: 필드 타입별 렌더링
- DynamicSection: 섹션 단위 렌더링
- DynamicFormRenderer: 페이지 전체 렌더링
- 필드 타입별 컴포넌트 (TextField, NumberField, DropdownField, CheckboxField, DateField, FileField, CustomField)
- 커스텀 훅 (useDynamicFormState, useFormStructure, useConditionalFields)
- DataTable 공통 컴포넌트 (테이블, 페이지네이션, 검색, 탭필터, 통계카드)
- ItemFormWrapper: Feature Flag 기반 폼 선택
- 타입 정의 및 문서화
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 20:14:43 +09:00
|
|
|
|
|
2025-12-04 12:48:41 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 메인 DynamicItemForm 컴포넌트
|
|
|
|
|
|
*/
|
|
|
|
|
|
export default function DynamicItemForm({
|
|
|
|
|
|
mode,
|
|
|
|
|
|
itemType: initialItemType,
|
2025-12-06 11:36:38 +09:00
|
|
|
|
itemId: propItemId,
|
2025-12-04 12:48:41 +09:00
|
|
|
|
initialData,
|
2025-12-12 18:35:43 +09:00
|
|
|
|
initialBomLines,
|
feat: 품목관리 동적 렌더링 시스템 구현
- DynamicItemForm 컴포넌트 구조 생성
- DynamicField: 필드 타입별 렌더링
- DynamicSection: 섹션 단위 렌더링
- DynamicFormRenderer: 페이지 전체 렌더링
- 필드 타입별 컴포넌트 (TextField, NumberField, DropdownField, CheckboxField, DateField, FileField, CustomField)
- 커스텀 훅 (useDynamicFormState, useFormStructure, useConditionalFields)
- DataTable 공통 컴포넌트 (테이블, 페이지네이션, 검색, 탭필터, 통계카드)
- ItemFormWrapper: Feature Flag 기반 폼 선택
- 타입 정의 및 문서화
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 20:14:43 +09:00
|
|
|
|
onSubmit,
|
|
|
|
|
|
}: DynamicItemFormProps) {
|
2025-12-04 12:48:41 +09:00
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
|
|
|
|
|
|
// 품목 유형 상태 (변경 가능)
|
|
|
|
|
|
const [selectedItemType, setSelectedItemType] = useState<ItemType | ''>(initialItemType || '');
|
|
|
|
|
|
|
|
|
|
|
|
// 폼 구조 로드 (품목 유형에 따라)
|
|
|
|
|
|
const { structure, isLoading, error: structureError, unitOptions } = useFormStructure(
|
|
|
|
|
|
selectedItemType as 'FG' | 'PT' | 'SM' | 'RM' | 'CS'
|
|
|
|
|
|
);
|
feat: 품목관리 동적 렌더링 시스템 구현
- DynamicItemForm 컴포넌트 구조 생성
- DynamicField: 필드 타입별 렌더링
- DynamicSection: 섹션 단위 렌더링
- DynamicFormRenderer: 페이지 전체 렌더링
- 필드 타입별 컴포넌트 (TextField, NumberField, DropdownField, CheckboxField, DateField, FileField, CustomField)
- 커스텀 훅 (useDynamicFormState, useFormStructure, useConditionalFields)
- DataTable 공통 컴포넌트 (테이블, 페이지네이션, 검색, 탭필터, 통계카드)
- ItemFormWrapper: Feature Flag 기반 폼 선택
- 타입 정의 및 문서화
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 20:14:43 +09:00
|
|
|
|
|
|
|
|
|
|
// 폼 상태 관리
|
|
|
|
|
|
const {
|
2025-12-04 12:48:41 +09:00
|
|
|
|
formData,
|
|
|
|
|
|
errors,
|
|
|
|
|
|
isSubmitting,
|
|
|
|
|
|
setFieldValue,
|
|
|
|
|
|
validateAll,
|
feat: 품목관리 동적 렌더링 시스템 구현
- DynamicItemForm 컴포넌트 구조 생성
- DynamicField: 필드 타입별 렌더링
- DynamicSection: 섹션 단위 렌더링
- DynamicFormRenderer: 페이지 전체 렌더링
- 필드 타입별 컴포넌트 (TextField, NumberField, DropdownField, CheckboxField, DateField, FileField, CustomField)
- 커스텀 훅 (useDynamicFormState, useFormStructure, useConditionalFields)
- DataTable 공통 컴포넌트 (테이블, 페이지네이션, 검색, 탭필터, 통계카드)
- ItemFormWrapper: Feature Flag 기반 폼 선택
- 타입 정의 및 문서화
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 20:14:43 +09:00
|
|
|
|
handleSubmit,
|
2025-12-04 12:48:41 +09:00
|
|
|
|
resetForm,
|
|
|
|
|
|
} = useDynamicFormState(initialData);
|
|
|
|
|
|
|
|
|
|
|
|
// BOM 상태 관리
|
|
|
|
|
|
const [bomLines, setBomLines] = useState<BOMLine[]>([]);
|
|
|
|
|
|
const [bomSearchStates, setBomSearchStates] = useState<Record<string, BOMSearchState>>({});
|
|
|
|
|
|
|
|
|
|
|
|
// 절곡품 전개도 상태 관리 (PT - 절곡 부품 전용)
|
|
|
|
|
|
const [bendingDiagramInputMethod, setBendingDiagramInputMethod] = useState<'file' | 'drawing'>('file');
|
|
|
|
|
|
const [bendingDiagram, setBendingDiagram] = useState<string>('');
|
|
|
|
|
|
const [bendingDiagramFile, setBendingDiagramFile] = useState<File | null>(null);
|
|
|
|
|
|
const [isDrawingOpen, setIsDrawingOpen] = useState(false);
|
|
|
|
|
|
const [bendingDetails, setBendingDetails] = useState<BendingDetail[]>([]);
|
|
|
|
|
|
const [widthSum, setWidthSum] = useState<string>('');
|
|
|
|
|
|
|
2025-12-04 20:52:42 +09:00
|
|
|
|
// FG(제품) 전용 파일 업로드 상태 관리
|
|
|
|
|
|
const [specificationFile, setSpecificationFile] = useState<File | null>(null);
|
|
|
|
|
|
const [certificationFile, setCertificationFile] = useState<File | null>(null);
|
|
|
|
|
|
|
2025-12-16 17:40:55 +09:00
|
|
|
|
// 파일 처리 훅 (기존 파일 로드, 다운로드, 삭제)
|
|
|
|
|
|
const {
|
|
|
|
|
|
existingBendingDiagram,
|
|
|
|
|
|
existingBendingDiagramFileId,
|
|
|
|
|
|
existingSpecificationFile,
|
|
|
|
|
|
existingSpecificationFileName,
|
|
|
|
|
|
existingSpecificationFileId,
|
|
|
|
|
|
existingCertificationFile,
|
|
|
|
|
|
existingCertificationFileName,
|
|
|
|
|
|
existingCertificationFileId,
|
|
|
|
|
|
isDeletingFile,
|
|
|
|
|
|
setExistingBendingDiagram: _setExistingBendingDiagram,
|
|
|
|
|
|
setExistingBendingDiagramFileId: _setExistingBendingDiagramFileId,
|
|
|
|
|
|
handleFileDownload,
|
|
|
|
|
|
handleDeleteFile: handleDeleteFileFromHook,
|
|
|
|
|
|
loadedBendingDetails,
|
|
|
|
|
|
loadedWidthSum,
|
|
|
|
|
|
} = useFileHandling({
|
|
|
|
|
|
mode,
|
|
|
|
|
|
initialData,
|
|
|
|
|
|
propItemId,
|
|
|
|
|
|
selectedItemType,
|
|
|
|
|
|
});
|
2025-12-06 11:36:38 +09:00
|
|
|
|
|
2025-12-12 18:35:43 +09:00
|
|
|
|
// 품목코드 중복 체크 상태 관리
|
|
|
|
|
|
const [showDuplicateDialog, setShowDuplicateDialog] = useState(false);
|
|
|
|
|
|
const [duplicateCheckResult, setDuplicateCheckResult] = useState<DuplicateCheckResult | null>(null);
|
2025-12-16 17:40:55 +09:00
|
|
|
|
const [_pendingSubmitData, setPendingSubmitData] = useState<DynamicFormData | null>(null);
|
2025-12-12 18:35:43 +09:00
|
|
|
|
|
2025-12-16 17:40:55 +09:00
|
|
|
|
// 훅에서 로드한 bendingDetails/widthSum을 로컬 상태와 동기화 (edit 모드)
|
2025-12-06 11:36:38 +09:00
|
|
|
|
useEffect(() => {
|
2025-12-16 17:40:55 +09:00
|
|
|
|
if (loadedBendingDetails.length > 0) {
|
|
|
|
|
|
setBendingDetails(loadedBendingDetails);
|
2025-12-06 11:36:38 +09:00
|
|
|
|
}
|
2025-12-16 17:40:55 +09:00
|
|
|
|
if (loadedWidthSum) {
|
|
|
|
|
|
setWidthSum(loadedWidthSum);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [loadedBendingDetails, loadedWidthSum]);
|
2025-12-06 11:36:38 +09:00
|
|
|
|
|
2025-12-12 18:35:43 +09:00
|
|
|
|
// initialBomLines prop으로 BOM 데이터 로드 (edit 모드)
|
|
|
|
|
|
// 2025-12-12: edit 페이지에서 별도로 전달받은 BOM 데이터 사용
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (mode === 'edit' && initialBomLines && initialBomLines.length > 0) {
|
|
|
|
|
|
setBomLines(initialBomLines);
|
|
|
|
|
|
console.log('[DynamicItemForm] initialBomLines로 BOM 데이터 로드:', initialBomLines.length, '건');
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [mode, initialBomLines]);
|
|
|
|
|
|
|
2025-12-16 17:40:55 +09:00
|
|
|
|
// 파일 삭제 래퍼 (훅의 handleDeleteFile에 콜백 전달)
|
2025-12-06 11:36:38 +09:00
|
|
|
|
const handleDeleteFile = async (fileType: ItemFileType) => {
|
2025-12-16 17:40:55 +09:00
|
|
|
|
await handleDeleteFileFromHook(fileType, {
|
|
|
|
|
|
onBendingDiagramDeleted: () => setBendingDiagram(''),
|
2025-12-16 11:01:25 +09:00
|
|
|
|
});
|
2025-12-06 11:36:38 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-04 12:48:41 +09:00
|
|
|
|
// 조건부 표시 관리
|
|
|
|
|
|
const { shouldShowSection, shouldShowField } = useConditionalDisplay(structure, formData);
|
|
|
|
|
|
|
|
|
|
|
|
// PT(부품) 품목코드 자동생성용 - 기존 품목코드 목록
|
|
|
|
|
|
const [existingItemCodes, setExistingItemCodes] = useState<string[]>([]);
|
|
|
|
|
|
|
|
|
|
|
|
// PT(부품) 선택 시 기존 품목코드 목록 조회
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (selectedItemType === 'PT') {
|
|
|
|
|
|
// PT 품목 목록 조회하여 기존 코드 수집
|
|
|
|
|
|
const fetchExistingCodes = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/proxy/items?type=PT&size=1000');
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
|
if (result.success && result.data?.data) {
|
|
|
|
|
|
const codes = result.data.data
|
|
|
|
|
|
.map((item: { code?: string; item_code?: string }) => item.code || item.item_code || '')
|
|
|
|
|
|
.filter((code: string) => code);
|
|
|
|
|
|
setExistingItemCodes(codes);
|
2025-12-04 20:52:42 +09:00
|
|
|
|
// console.log('[DynamicItemForm] PT 기존 품목코드 로드:', codes.length, '개');
|
2025-12-04 12:48:41 +09:00
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('[DynamicItemForm] PT 품목코드 조회 실패:', err);
|
|
|
|
|
|
setExistingItemCodes([]);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
fetchExistingCodes();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setExistingItemCodes([]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [selectedItemType]);
|
|
|
|
|
|
|
2025-12-04 20:52:42 +09:00
|
|
|
|
// 품목 유형 변경 시 폼 초기화 (create 모드)
|
2025-12-04 12:48:41 +09:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (selectedItemType && mode === 'create' && structure) {
|
|
|
|
|
|
// 기본값 설정
|
|
|
|
|
|
const defaults: DynamicFormData = {
|
|
|
|
|
|
item_type: selectedItemType,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 구조에서 기본값 추출
|
|
|
|
|
|
structure.sections.forEach((section) => {
|
|
|
|
|
|
section.fields.forEach((f) => {
|
|
|
|
|
|
const field = f.field;
|
|
|
|
|
|
const fieldKey = field.field_key || `field_${field.id}`;
|
|
|
|
|
|
if (field.default_value !== null && field.default_value !== undefined) {
|
|
|
|
|
|
defaults[fieldKey] = field.default_value;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
structure.directFields.forEach((f) => {
|
|
|
|
|
|
const field = f.field;
|
|
|
|
|
|
const fieldKey = field.field_key || `field_${field.id}`;
|
|
|
|
|
|
if (field.default_value !== null && field.default_value !== undefined) {
|
|
|
|
|
|
defaults[fieldKey] = field.default_value;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
resetForm(defaults);
|
|
|
|
|
|
|
|
|
|
|
|
// BOM 상태 초기화 - 빈 상태로 시작 (사용자가 추가 버튼으로 행 추가)
|
|
|
|
|
|
setBomLines([]);
|
|
|
|
|
|
setBomSearchStates({});
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [selectedItemType, structure, mode, resetForm]);
|
|
|
|
|
|
|
2025-12-09 18:07:47 +09:00
|
|
|
|
// Edit 모드: initialData를 폼에 직접 로드
|
|
|
|
|
|
// 2025-12-09: field_key 통일로 복잡한 매핑 로직 제거
|
|
|
|
|
|
// 백엔드에서 field_key 그대로 응답하므로 직접 사용 가능
|
2025-12-04 20:52:42 +09:00
|
|
|
|
const [isEditDataMapped, setIsEditDataMapped] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2025-12-09 18:07:47 +09:00
|
|
|
|
console.log('[DynamicItemForm] Edit useEffect 체크:', {
|
|
|
|
|
|
mode,
|
|
|
|
|
|
hasStructure: !!structure,
|
|
|
|
|
|
hasInitialData: !!initialData,
|
|
|
|
|
|
isEditDataMapped,
|
|
|
|
|
|
structureSections: structure?.sections?.length,
|
|
|
|
|
|
});
|
2025-12-04 20:52:42 +09:00
|
|
|
|
|
2025-12-09 18:07:47 +09:00
|
|
|
|
if (mode !== 'edit' || !structure || !initialData || isEditDataMapped) return;
|
2025-12-06 11:36:38 +09:00
|
|
|
|
|
2025-12-09 18:07:47 +09:00
|
|
|
|
console.log('[DynamicItemForm] Edit mode: initialData 직접 로드 (field_key 통일됨)');
|
2025-12-06 11:36:38 +09:00
|
|
|
|
console.log('[DynamicItemForm] initialData:', initialData);
|
2025-12-04 20:52:42 +09:00
|
|
|
|
|
2025-12-09 18:07:47 +09:00
|
|
|
|
// structure의 field_key들 확인
|
|
|
|
|
|
const fieldKeys: string[] = [];
|
2025-12-04 20:52:42 +09:00
|
|
|
|
structure.sections.forEach((section) => {
|
|
|
|
|
|
section.fields.forEach((f) => {
|
2025-12-09 18:07:47 +09:00
|
|
|
|
fieldKeys.push(f.field.field_key || `field_${f.field.id}`);
|
2025-12-04 20:52:42 +09:00
|
|
|
|
});
|
|
|
|
|
|
});
|
2025-12-09 18:07:47 +09:00
|
|
|
|
console.log('[DynamicItemForm] structure field_keys:', fieldKeys);
|
|
|
|
|
|
console.log('[DynamicItemForm] initialData keys:', Object.keys(initialData));
|
2025-12-04 20:52:42 +09:00
|
|
|
|
|
2025-12-09 18:07:47 +09:00
|
|
|
|
// field_key가 통일되었으므로 initialData를 그대로 사용
|
|
|
|
|
|
// 기존 레거시 데이터(98_unit 형식)도 그대로 동작
|
|
|
|
|
|
resetForm(initialData);
|
2025-12-04 20:52:42 +09:00
|
|
|
|
setIsEditDataMapped(true);
|
|
|
|
|
|
}, [mode, structure, initialData, isEditDataMapped, resetForm]);
|
|
|
|
|
|
|
2025-12-04 12:48:41 +09:00
|
|
|
|
// 모든 필드 목록 (밸리데이션용) - 숨겨진 섹션/필드 제외
|
|
|
|
|
|
const allFields = useMemo<ItemFieldResponse[]>(() => {
|
|
|
|
|
|
if (!structure) return [];
|
|
|
|
|
|
|
|
|
|
|
|
const fields: ItemFieldResponse[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
// 표시되는 섹션의 표시되는 필드만 포함
|
|
|
|
|
|
structure.sections.forEach((section) => {
|
|
|
|
|
|
// 섹션이 숨겨져 있으면 스킵 (조건부 표시)
|
|
|
|
|
|
if (!shouldShowSection(section.section.id)) return;
|
|
|
|
|
|
|
|
|
|
|
|
section.fields.forEach((f) => {
|
|
|
|
|
|
// 필드가 숨겨져 있으면 스킵 (조건부 표시)
|
|
|
|
|
|
if (!shouldShowField(f.field.id)) return;
|
|
|
|
|
|
fields.push(f.field);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 직접 필드도 필터링 (조건부 표시)
|
|
|
|
|
|
structure.directFields.forEach((f) => {
|
|
|
|
|
|
if (!shouldShowField(f.field.id)) return;
|
|
|
|
|
|
fields.push(f.field);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return fields;
|
|
|
|
|
|
}, [structure, shouldShowSection, shouldShowField]);
|
|
|
|
|
|
|
2025-12-16 17:40:55 +09:00
|
|
|
|
// 부품 유형 및 BOM 필드 탐지 (커스텀 훅으로 분리)
|
|
|
|
|
|
const {
|
|
|
|
|
|
partTypeFieldKey,
|
|
|
|
|
|
selectedPartType,
|
|
|
|
|
|
isBendingPart,
|
|
|
|
|
|
isAssemblyPart,
|
|
|
|
|
|
isPurchasedPart,
|
|
|
|
|
|
bomRequiredFieldKey,
|
|
|
|
|
|
} = useFieldDetection({
|
|
|
|
|
|
structure,
|
|
|
|
|
|
selectedItemType,
|
|
|
|
|
|
formData,
|
|
|
|
|
|
});
|
2025-12-04 12:48:41 +09:00
|
|
|
|
|
2025-12-16 17:40:55 +09:00
|
|
|
|
// 품목코드 자동생성 관련 정보 (커스텀 훅으로 분리)
|
|
|
|
|
|
const {
|
|
|
|
|
|
hasAutoItemCode,
|
|
|
|
|
|
itemNameKey,
|
|
|
|
|
|
allSpecificationKeys: _allSpecificationKeys,
|
|
|
|
|
|
statusFieldKey,
|
|
|
|
|
|
activeSpecificationKey,
|
|
|
|
|
|
bendingFieldKeys,
|
|
|
|
|
|
autoBendingItemCode,
|
|
|
|
|
|
allCategoryKeysWithIds,
|
|
|
|
|
|
hasAssemblyFields: _hasAssemblyFields,
|
|
|
|
|
|
assemblyFieldKeys: _assemblyFieldKeys,
|
|
|
|
|
|
autoAssemblyItemName,
|
|
|
|
|
|
autoAssemblySpec,
|
|
|
|
|
|
purchasedFieldKeys,
|
|
|
|
|
|
autoPurchasedItemCode,
|
|
|
|
|
|
autoGeneratedItemCode,
|
|
|
|
|
|
} = useItemCodeGeneration({
|
|
|
|
|
|
structure,
|
|
|
|
|
|
selectedItemType,
|
|
|
|
|
|
formData,
|
|
|
|
|
|
isBendingPart,
|
|
|
|
|
|
isAssemblyPart,
|
|
|
|
|
|
isPurchasedPart,
|
|
|
|
|
|
existingItemCodes,
|
|
|
|
|
|
shouldShowSection,
|
|
|
|
|
|
shouldShowField,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 부품 유형 변경 시 필드 초기화 처리 (커스텀 훅으로 분리)
|
|
|
|
|
|
usePartTypeHandling({
|
|
|
|
|
|
structure,
|
|
|
|
|
|
selectedItemType,
|
|
|
|
|
|
partTypeFieldKey,
|
|
|
|
|
|
selectedPartType,
|
|
|
|
|
|
itemNameKey,
|
|
|
|
|
|
setFieldValue,
|
|
|
|
|
|
formData,
|
|
|
|
|
|
bendingFieldKeys,
|
|
|
|
|
|
isBendingPart,
|
|
|
|
|
|
allCategoryKeysWithIds,
|
|
|
|
|
|
mode,
|
|
|
|
|
|
bendingDetails,
|
|
|
|
|
|
});
|
2025-12-04 12:48:41 +09:00
|
|
|
|
|
|
|
|
|
|
// 품목 유형 변경 핸들러
|
|
|
|
|
|
const handleItemTypeChange = (type: ItemType) => {
|
|
|
|
|
|
setSelectedItemType(type);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-12 18:35:43 +09:00
|
|
|
|
// 실제 저장 로직 (중복 체크 후 호출)
|
|
|
|
|
|
const executeSubmit = async (submitData: DynamicFormData) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await handleSubmit(async () => {
|
|
|
|
|
|
// 품목 저장 (ID 반환)
|
|
|
|
|
|
const result = await onSubmit(submitData);
|
|
|
|
|
|
const itemId = result?.id;
|
|
|
|
|
|
|
|
|
|
|
|
// 파일 업로드 (품목 ID가 있을 때만)
|
|
|
|
|
|
if (itemId) {
|
|
|
|
|
|
const fileUploadErrors: string[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
// PT (절곡/조립) 전개도 이미지 업로드
|
|
|
|
|
|
if (selectedItemType === 'PT' && (isBendingPart || isAssemblyPart) && bendingDiagramFile) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log('[DynamicItemForm] 전개도 파일 업로드 시작:', bendingDiagramFile.name);
|
|
|
|
|
|
await uploadItemFile(itemId, bendingDiagramFile, 'bending_diagram', {
|
|
|
|
|
|
fieldKey: 'bending_diagram',
|
2025-12-16 11:01:25 +09:00
|
|
|
|
// 수정 모드: 기존 파일 ID가 있으면 덮어쓰기, 없으면 새 파일 등록
|
|
|
|
|
|
fileId: existingBendingDiagramFileId ?? undefined,
|
2025-12-12 18:35:43 +09:00
|
|
|
|
bendingDetails: bendingDetails.length > 0 ? bendingDetails.map(d => ({
|
|
|
|
|
|
angle: d.aAngle || 0,
|
|
|
|
|
|
length: d.input || 0,
|
|
|
|
|
|
type: d.shaded ? 'shaded' : 'normal',
|
|
|
|
|
|
})) : undefined,
|
|
|
|
|
|
});
|
|
|
|
|
|
console.log('[DynamicItemForm] 전개도 파일 업로드 성공');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[DynamicItemForm] 전개도 파일 업로드 실패:', error);
|
|
|
|
|
|
fileUploadErrors.push('전개도 이미지');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// FG (제품) 시방서 업로드
|
|
|
|
|
|
if (selectedItemType === 'FG' && specificationFile) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log('[DynamicItemForm] 시방서 파일 업로드 시작:', specificationFile.name);
|
|
|
|
|
|
await uploadItemFile(itemId, specificationFile, 'specification', {
|
|
|
|
|
|
fieldKey: 'specification_file',
|
2025-12-16 11:01:25 +09:00
|
|
|
|
// 수정 모드: 기존 파일 ID가 있으면 덮어쓰기, 없으면 새 파일 등록
|
|
|
|
|
|
fileId: existingSpecificationFileId ?? undefined,
|
2025-12-12 18:35:43 +09:00
|
|
|
|
});
|
|
|
|
|
|
console.log('[DynamicItemForm] 시방서 파일 업로드 성공');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[DynamicItemForm] 시방서 파일 업로드 실패:', error);
|
|
|
|
|
|
fileUploadErrors.push('시방서');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// FG (제품) 인정서 업로드
|
|
|
|
|
|
if (selectedItemType === 'FG' && certificationFile) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log('[DynamicItemForm] 인정서 파일 업로드 시작:', certificationFile.name);
|
|
|
|
|
|
// formData에서 인정서 관련 필드 추출
|
|
|
|
|
|
const certNumber = Object.entries(formData).find(([key]) =>
|
|
|
|
|
|
key.includes('certification_number') || key.includes('인정번호')
|
|
|
|
|
|
)?.[1] as string | undefined;
|
|
|
|
|
|
const certStartDate = Object.entries(formData).find(([key]) =>
|
|
|
|
|
|
key.includes('certification_start') || key.includes('인정_유효기간_시작')
|
|
|
|
|
|
)?.[1] as string | undefined;
|
|
|
|
|
|
const certEndDate = Object.entries(formData).find(([key]) =>
|
|
|
|
|
|
key.includes('certification_end') || key.includes('인정_유효기간_종료')
|
|
|
|
|
|
)?.[1] as string | undefined;
|
|
|
|
|
|
|
|
|
|
|
|
await uploadItemFile(itemId, certificationFile, 'certification', {
|
|
|
|
|
|
fieldKey: 'certification_file',
|
2025-12-16 11:01:25 +09:00
|
|
|
|
// 수정 모드: 기존 파일 ID가 있으면 덮어쓰기, 없으면 새 파일 등록
|
|
|
|
|
|
fileId: existingCertificationFileId ?? undefined,
|
2025-12-12 18:35:43 +09:00
|
|
|
|
certificationNumber: certNumber,
|
|
|
|
|
|
certificationStartDate: certStartDate,
|
|
|
|
|
|
certificationEndDate: certEndDate,
|
|
|
|
|
|
});
|
|
|
|
|
|
console.log('[DynamicItemForm] 인정서 파일 업로드 성공');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[DynamicItemForm] 인정서 파일 업로드 실패:', error);
|
|
|
|
|
|
fileUploadErrors.push('인정서');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 파일 업로드 실패 경고 (품목은 저장됨)
|
|
|
|
|
|
if (fileUploadErrors.length > 0) {
|
|
|
|
|
|
console.warn('[DynamicItemForm] 일부 파일 업로드 실패:', fileUploadErrors.join(', '));
|
|
|
|
|
|
// 품목은 저장되었으므로 경고만 표시하고 진행
|
|
|
|
|
|
alert(`품목이 저장되었습니다.\n\n일부 파일 업로드에 실패했습니다: ${fileUploadErrors.join(', ')}\n수정 화면에서 다시 업로드해 주세요.`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
router.push('/items');
|
|
|
|
|
|
router.refresh();
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
// 2025-12-11: 백엔드에서 중복 에러 반환 시 다이얼로그 표시
|
|
|
|
|
|
// 사전 체크를 우회하거나 동시 등록 시에도 안전하게 처리
|
|
|
|
|
|
if (error instanceof DuplicateCodeError) {
|
|
|
|
|
|
console.warn('[DynamicItemForm] 저장 시점 중복 에러 감지:', error);
|
|
|
|
|
|
setDuplicateCheckResult({
|
|
|
|
|
|
isDuplicate: true,
|
|
|
|
|
|
duplicateId: error.duplicateId,
|
|
|
|
|
|
});
|
|
|
|
|
|
setPendingSubmitData(submitData);
|
|
|
|
|
|
setShowDuplicateDialog(true);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 그 외 에러는 상위로 전파
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-04 12:48:41 +09:00
|
|
|
|
// 폼 제출 핸들러
|
|
|
|
|
|
const handleFormSubmit = async (e: React.FormEvent) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
|
|
// 밸리데이션 - 조건부 표시로 숨겨진 필드는 이미 allFields에서 제외됨
|
|
|
|
|
|
// 2025-12-03: 연동 드롭다운 로직 제거 - 단순화
|
|
|
|
|
|
const isValid = validateAll(allFields);
|
|
|
|
|
|
if (!isValid) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 18:07:47 +09:00
|
|
|
|
// 2025-12-09: field_key 통일로 변환 로직 제거
|
|
|
|
|
|
// formData의 field_key가 백엔드 필드명과 일치하므로 직접 사용
|
|
|
|
|
|
console.log('[DynamicItemForm] 저장 시 formData:', formData);
|
2025-12-04 12:48:41 +09:00
|
|
|
|
|
2025-12-09 18:07:47 +09:00
|
|
|
|
// is_active 필드만 boolean 변환 (드롭다운 값 → boolean)
|
2025-12-04 12:48:41 +09:00
|
|
|
|
const convertedData: Record<string, any> = {};
|
|
|
|
|
|
Object.entries(formData).forEach(([key, value]) => {
|
2025-12-09 18:07:47 +09:00
|
|
|
|
if (key === 'is_active' || key.endsWith('_is_active')) {
|
|
|
|
|
|
// "활성", true, "true", "1", 1 등을 true로, 나머지는 false로
|
|
|
|
|
|
const isActive = value === true || value === 'true' || value === '1' ||
|
|
|
|
|
|
value === 1 || value === '활성' || value === 'active';
|
|
|
|
|
|
convertedData[key] = isActive;
|
2025-12-04 12:48:41 +09:00
|
|
|
|
} else {
|
2025-12-09 18:07:47 +09:00
|
|
|
|
convertedData[key] = value;
|
2025-12-04 12:48:41 +09:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 품목명 값 추출 (품목코드와 품목명 모두 필요)
|
|
|
|
|
|
// 2025-12-04: 절곡 부품은 별도 품목명 필드(bendingFieldKeys.itemName) 사용
|
|
|
|
|
|
const effectiveItemNameKeyForSubmit = isBendingPart && bendingFieldKeys.itemName
|
|
|
|
|
|
? bendingFieldKeys.itemName
|
|
|
|
|
|
: itemNameKey;
|
|
|
|
|
|
const itemNameValue = effectiveItemNameKeyForSubmit
|
|
|
|
|
|
? (formData[effectiveItemNameKeyForSubmit] as string) || ''
|
|
|
|
|
|
: '';
|
|
|
|
|
|
|
2025-12-04 20:52:42 +09:00
|
|
|
|
// 조립/절곡/구매 부품 자동생성 값 결정
|
2025-12-04 12:48:41 +09:00
|
|
|
|
// 조립 부품: 품목명 = "품목명 가로x세로", 규격 = "가로x세로x길이"
|
|
|
|
|
|
// 절곡 부품: 품목명 = bendingFieldKeys.itemName에서 선택한 값, 규격 = 없음 (품목코드로 대체)
|
2025-12-04 20:52:42 +09:00
|
|
|
|
// 구매 부품: 품목명 = purchasedFieldKeys.itemName에서 선택한 값
|
2025-12-04 12:48:41 +09:00
|
|
|
|
let finalName: string;
|
|
|
|
|
|
let finalSpec: string | undefined;
|
|
|
|
|
|
|
|
|
|
|
|
if (isAssemblyPart && autoAssemblyItemName) {
|
|
|
|
|
|
// 조립 부품: 자동생성 품목명/규격 사용
|
|
|
|
|
|
finalName = autoAssemblyItemName;
|
|
|
|
|
|
finalSpec = autoAssemblySpec;
|
|
|
|
|
|
} else if (isBendingPart) {
|
|
|
|
|
|
// 절곡 부품: bendingFieldKeys.itemName의 값 사용
|
|
|
|
|
|
finalName = itemNameValue || convertedData.name || '';
|
|
|
|
|
|
finalSpec = convertedData.spec;
|
2025-12-04 20:52:42 +09:00
|
|
|
|
} else if (isPurchasedPart) {
|
|
|
|
|
|
// 구매 부품: purchasedFieldKeys.itemName의 값 사용
|
|
|
|
|
|
const purchasedItemNameValue = purchasedFieldKeys.itemName
|
|
|
|
|
|
? (formData[purchasedFieldKeys.itemName] as string) || ''
|
|
|
|
|
|
: '';
|
|
|
|
|
|
finalName = purchasedItemNameValue || convertedData.name || '';
|
|
|
|
|
|
finalSpec = convertedData.spec;
|
2025-12-04 12:48:41 +09:00
|
|
|
|
} else {
|
|
|
|
|
|
// 기타: 기존 로직
|
|
|
|
|
|
finalName = convertedData.name || itemNameValue;
|
|
|
|
|
|
finalSpec = convertedData.spec;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-04 20:52:42 +09:00
|
|
|
|
// console.log('[DynamicItemForm] 품목명/규격 결정:', { finalName, finalSpec });
|
2025-12-04 12:48:41 +09:00
|
|
|
|
|
|
|
|
|
|
// 품목코드 결정
|
2025-12-12 18:35:43 +09:00
|
|
|
|
// 2025-12-11: 수정 모드에서는 기존 코드 유지 (자동생성으로 코드가 변경되는 버그 수정)
|
|
|
|
|
|
// 생성 모드에서만 자동생성 코드 사용
|
2025-12-04 12:48:41 +09:00
|
|
|
|
let finalCode: string;
|
2025-12-12 18:35:43 +09:00
|
|
|
|
if (mode === 'edit' && initialData?.code) {
|
|
|
|
|
|
// 수정 모드: DB에서 받은 기존 코드 유지
|
|
|
|
|
|
finalCode = initialData.code as string;
|
|
|
|
|
|
} else if (isBendingPart && autoBendingItemCode) {
|
|
|
|
|
|
// 생성 모드: 절곡 부품 자동생성
|
2025-12-04 12:48:41 +09:00
|
|
|
|
finalCode = autoBendingItemCode;
|
2025-12-04 20:52:42 +09:00
|
|
|
|
} else if (isPurchasedPart && autoPurchasedItemCode) {
|
2025-12-12 18:35:43 +09:00
|
|
|
|
// 생성 모드: 구매 부품 자동생성
|
2025-12-04 20:52:42 +09:00
|
|
|
|
finalCode = autoPurchasedItemCode;
|
2025-12-04 12:48:41 +09:00
|
|
|
|
} else if (hasAutoItemCode && autoGeneratedItemCode) {
|
2025-12-12 18:35:43 +09:00
|
|
|
|
// 생성 모드: 일반 자동생성
|
2025-12-04 12:48:41 +09:00
|
|
|
|
finalCode = autoGeneratedItemCode;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
finalCode = convertedData.code || itemNameValue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 품목 유형 및 BOM 데이터 추가
|
2025-12-09 18:07:47 +09:00
|
|
|
|
const submitData = {
|
2025-12-04 12:48:41 +09:00
|
|
|
|
...convertedData,
|
|
|
|
|
|
// 백엔드 필드명 사용
|
|
|
|
|
|
product_type: selectedItemType, // item_type → product_type
|
|
|
|
|
|
// 2025-12-03: 조립 부품 자동생성 품목명/규격 사용
|
|
|
|
|
|
// 2025-12-04: 절곡 부품도 자동생성 품목코드 사용
|
|
|
|
|
|
name: finalName, // 조립 부품: 품목명 가로x세로, 절곡 부품: 품목명 필드값, 기타: 품목명 필드값
|
|
|
|
|
|
spec: finalSpec, // 조립 부품: 가로x세로x길이, 기타: 규격 필드값
|
|
|
|
|
|
code: finalCode, // 절곡 부품: autoBendingItemCode, 기타: autoGeneratedItemCode
|
2025-12-12 18:35:43 +09:00
|
|
|
|
// BOM 데이터를 배열로 포함 (백엔드는 child_item_id, child_item_type, quantity만 저장)
|
2025-12-04 12:48:41 +09:00
|
|
|
|
bom: bomLines.map((line) => ({
|
2025-12-12 18:35:43 +09:00
|
|
|
|
child_item_id: line.childItemId ? Number(line.childItemId) : null,
|
|
|
|
|
|
child_item_type: line.childItemType || 'PRODUCT', // PRODUCT(FG/PT) 또는 MATERIAL(SM/RM/CS)
|
|
|
|
|
|
quantity: line.quantity || 1,
|
|
|
|
|
|
})).filter(item => item.child_item_id !== null), // child_item_id 없는 항목 제외
|
2025-12-04 12:48:41 +09:00
|
|
|
|
// 절곡품 전개도 데이터 (PT - 절곡 부품 전용)
|
|
|
|
|
|
...(selectedItemType === 'PT' && isBendingPart ? {
|
|
|
|
|
|
part_type: 'BENDING',
|
|
|
|
|
|
bending_diagram: bendingDiagram || null,
|
|
|
|
|
|
bending_details: bendingDetails.length > 0 ? bendingDetails : null,
|
|
|
|
|
|
width_sum: widthSum || null,
|
|
|
|
|
|
} : {}),
|
|
|
|
|
|
// 조립품 전개도 데이터 (PT - 조립 부품 전용)
|
|
|
|
|
|
...(selectedItemType === 'PT' && isAssemblyPart ? {
|
|
|
|
|
|
part_type: 'ASSEMBLY',
|
|
|
|
|
|
bending_diagram: bendingDiagram || null, // 조립품도 동일한 전개도 필드 사용
|
|
|
|
|
|
} : {}),
|
2025-12-04 20:52:42 +09:00
|
|
|
|
// 구매품 데이터 (PT - 구매 부품 전용)
|
|
|
|
|
|
...(selectedItemType === 'PT' && isPurchasedPart ? {
|
|
|
|
|
|
part_type: 'PURCHASED',
|
|
|
|
|
|
} : {}),
|
|
|
|
|
|
// FG(제품)은 단위 필드가 없으므로 기본값 'EA' 설정
|
|
|
|
|
|
...(selectedItemType === 'FG' && !convertedData.unit ? {
|
|
|
|
|
|
unit: 'EA',
|
|
|
|
|
|
} : {}),
|
2025-12-09 18:07:47 +09:00
|
|
|
|
} as DynamicFormData;
|
2025-12-04 12:48:41 +09:00
|
|
|
|
|
2025-12-04 20:52:42 +09:00
|
|
|
|
// console.log('[DynamicItemForm] 제출 데이터:', submitData);
|
2025-12-04 12:48:41 +09:00
|
|
|
|
|
2025-12-12 18:35:43 +09:00
|
|
|
|
// 2025-12-11: 품목코드 중복 체크 (조립/절곡 부품만 해당)
|
|
|
|
|
|
// PT-조립부품, PT-절곡부품은 품목코드가 자동생성되므로 중복 체크 필요
|
|
|
|
|
|
const needsDuplicateCheck = selectedItemType === 'PT' && (isAssemblyPart || isBendingPart) && finalCode;
|
2025-12-06 11:36:38 +09:00
|
|
|
|
|
2025-12-12 18:35:43 +09:00
|
|
|
|
if (needsDuplicateCheck) {
|
|
|
|
|
|
console.log('[DynamicItemForm] 품목코드 중복 체크:', finalCode);
|
2025-12-06 11:36:38 +09:00
|
|
|
|
|
2025-12-12 18:35:43 +09:00
|
|
|
|
// 수정 모드에서는 자기 자신 제외 (propItemId)
|
|
|
|
|
|
const excludeId = mode === 'edit' ? propItemId : undefined;
|
|
|
|
|
|
const duplicateResult = await checkItemCodeDuplicate(finalCode, excludeId);
|
2025-12-06 11:36:38 +09:00
|
|
|
|
|
2025-12-12 18:35:43 +09:00
|
|
|
|
console.log('[DynamicItemForm] 중복 체크 결과:', duplicateResult);
|
2025-12-06 11:36:38 +09:00
|
|
|
|
|
2025-12-12 18:35:43 +09:00
|
|
|
|
if (duplicateResult.isDuplicate) {
|
|
|
|
|
|
// 중복 발견 → 다이얼로그 표시
|
|
|
|
|
|
setDuplicateCheckResult(duplicateResult);
|
|
|
|
|
|
setPendingSubmitData(submitData);
|
|
|
|
|
|
setShowDuplicateDialog(true);
|
|
|
|
|
|
return; // 저장 중단, 사용자 선택 대기
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-06 11:36:38 +09:00
|
|
|
|
|
2025-12-12 18:35:43 +09:00
|
|
|
|
// 중복 없음 → 바로 저장
|
|
|
|
|
|
await executeSubmit(submitData);
|
|
|
|
|
|
};
|
2025-12-06 11:36:38 +09:00
|
|
|
|
|
2025-12-12 18:35:43 +09:00
|
|
|
|
// 중복 다이얼로그에서 "중복 품목 수정" 버튼 클릭 핸들러
|
|
|
|
|
|
const handleGoToEditDuplicate = () => {
|
|
|
|
|
|
if (duplicateCheckResult?.duplicateId) {
|
|
|
|
|
|
setShowDuplicateDialog(false);
|
|
|
|
|
|
// 2025-12-11: 수정 페이지 URL 형식 맞춤
|
|
|
|
|
|
// /items/{code}/edit?type={itemType}&id={itemId}
|
|
|
|
|
|
// duplicateItemType이 없으면 현재 선택된 품목 유형 사용
|
|
|
|
|
|
const itemType = duplicateCheckResult.duplicateItemType || selectedItemType || 'PT';
|
|
|
|
|
|
const itemId = duplicateCheckResult.duplicateId;
|
|
|
|
|
|
// code는 없으므로 id를 path에 사용 (edit 페이지에서 id 쿼리 파라미터로 조회)
|
|
|
|
|
|
router.push(`/items/${itemId}/edit?type=${itemType}&id=${itemId}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-12-06 11:36:38 +09:00
|
|
|
|
|
2025-12-12 18:35:43 +09:00
|
|
|
|
// 중복 다이얼로그에서 "취소" 버튼 클릭 핸들러
|
|
|
|
|
|
const handleCancelDuplicate = () => {
|
|
|
|
|
|
setShowDuplicateDialog(false);
|
|
|
|
|
|
setDuplicateCheckResult(null);
|
|
|
|
|
|
setPendingSubmitData(null);
|
2025-12-04 12:48:41 +09:00
|
|
|
|
};
|
feat: 품목관리 동적 렌더링 시스템 구현
- DynamicItemForm 컴포넌트 구조 생성
- DynamicField: 필드 타입별 렌더링
- DynamicSection: 섹션 단위 렌더링
- DynamicFormRenderer: 페이지 전체 렌더링
- 필드 타입별 컴포넌트 (TextField, NumberField, DropdownField, CheckboxField, DateField, FileField, CustomField)
- 커스텀 훅 (useDynamicFormState, useFormStructure, useConditionalFields)
- DataTable 공통 컴포넌트 (테이블, 페이지네이션, 검색, 탭필터, 통계카드)
- ItemFormWrapper: Feature Flag 기반 폼 선택
- 타입 정의 및 문서화
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 20:14:43 +09:00
|
|
|
|
|
|
|
|
|
|
// 로딩 상태
|
2025-12-04 12:48:41 +09:00
|
|
|
|
if (isLoading && selectedItemType) {
|
feat: 품목관리 동적 렌더링 시스템 구현
- DynamicItemForm 컴포넌트 구조 생성
- DynamicField: 필드 타입별 렌더링
- DynamicSection: 섹션 단위 렌더링
- DynamicFormRenderer: 페이지 전체 렌더링
- 필드 타입별 컴포넌트 (TextField, NumberField, DropdownField, CheckboxField, DateField, FileField, CustomField)
- 커스텀 훅 (useDynamicFormState, useFormStructure, useConditionalFields)
- DataTable 공통 컴포넌트 (테이블, 페이지네이션, 검색, 탭필터, 통계카드)
- ItemFormWrapper: Feature Flag 기반 폼 선택
- 타입 정의 및 문서화
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 20:14:43 +09:00
|
|
|
|
return (
|
2025-12-04 12:48:41 +09:00
|
|
|
|
<PageLoadingSpinner
|
|
|
|
|
|
text="폼 구조를 불러오는 중..."
|
|
|
|
|
|
minHeight="min-h-[40vh]"
|
|
|
|
|
|
/>
|
feat: 품목관리 동적 렌더링 시스템 구현
- DynamicItemForm 컴포넌트 구조 생성
- DynamicField: 필드 타입별 렌더링
- DynamicSection: 섹션 단위 렌더링
- DynamicFormRenderer: 페이지 전체 렌더링
- 필드 타입별 컴포넌트 (TextField, NumberField, DropdownField, CheckboxField, DateField, FileField, CustomField)
- 커스텀 훅 (useDynamicFormState, useFormStructure, useConditionalFields)
- DataTable 공통 컴포넌트 (테이블, 페이지네이션, 검색, 탭필터, 통계카드)
- ItemFormWrapper: Feature Flag 기반 폼 선택
- 타입 정의 및 문서화
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 20:14:43 +09:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 에러 상태
|
|
|
|
|
|
if (structureError) {
|
|
|
|
|
|
return (
|
2025-12-04 12:48:41 +09:00
|
|
|
|
<Alert className="bg-red-50 border-red-200">
|
|
|
|
|
|
<AlertDescription className="text-red-900">
|
|
|
|
|
|
⚠️ 폼 구조를 불러오는데 실패했습니다: {structureError}
|
feat: 품목관리 동적 렌더링 시스템 구현
- DynamicItemForm 컴포넌트 구조 생성
- DynamicField: 필드 타입별 렌더링
- DynamicSection: 섹션 단위 렌더링
- DynamicFormRenderer: 페이지 전체 렌더링
- 필드 타입별 컴포넌트 (TextField, NumberField, DropdownField, CheckboxField, DateField, FileField, CustomField)
- 커스텀 훅 (useDynamicFormState, useFormStructure, useConditionalFields)
- DataTable 공통 컴포넌트 (테이블, 페이지네이션, 검색, 탭필터, 통계카드)
- ItemFormWrapper: Feature Flag 기반 폼 선택
- 타입 정의 및 문서화
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 20:14:43 +09:00
|
|
|
|
</AlertDescription>
|
|
|
|
|
|
</Alert>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-04 12:48:41 +09:00
|
|
|
|
// 섹션 정렬
|
|
|
|
|
|
const sortedSections = structure
|
|
|
|
|
|
? [...structure.sections].sort((a, b) => a.orderNo - b.orderNo)
|
|
|
|
|
|
: [];
|
|
|
|
|
|
|
|
|
|
|
|
// 직접 필드 정렬
|
|
|
|
|
|
const sortedDirectFields = structure
|
|
|
|
|
|
? [...structure.directFields].sort((a, b) => a.orderNo - b.orderNo)
|
|
|
|
|
|
: [];
|
|
|
|
|
|
|
|
|
|
|
|
// 일반 섹션들 (BOM 제외) - 기본 정보 카드에 통합할 섹션들
|
|
|
|
|
|
const normalSections = sortedSections.filter((s) => s.section.type !== 'bom');
|
|
|
|
|
|
|
|
|
|
|
|
// BOM 섹션 - 별도 카드로 렌더링
|
|
|
|
|
|
const bomSection = sortedSections.find((s) => s.section.type === 'bom');
|
feat: 품목관리 동적 렌더링 시스템 구현
- DynamicItemForm 컴포넌트 구조 생성
- DynamicField: 필드 타입별 렌더링
- DynamicSection: 섹션 단위 렌더링
- DynamicFormRenderer: 페이지 전체 렌더링
- 필드 타입별 컴포넌트 (TextField, NumberField, DropdownField, CheckboxField, DateField, FileField, CustomField)
- 커스텀 훅 (useDynamicFormState, useFormStructure, useConditionalFields)
- DataTable 공통 컴포넌트 (테이블, 페이지네이션, 검색, 탭필터, 통계카드)
- ItemFormWrapper: Feature Flag 기반 폼 선택
- 타입 정의 및 문서화
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 20:14:43 +09:00
|
|
|
|
|
2025-12-04 12:48:41 +09:00
|
|
|
|
// 첫 번째 일반 섹션 (기본 필드용)
|
|
|
|
|
|
const firstDefaultSection = normalSections[0];
|
|
|
|
|
|
|
|
|
|
|
|
// 나머지 일반 섹션들 (하위 섹션으로 렌더링)
|
|
|
|
|
|
const additionalSections = normalSections.slice(1);
|
|
|
|
|
|
|
|
|
|
|
|
// 통합 섹션의 필드 정렬
|
|
|
|
|
|
const firstSectionFields = firstDefaultSection
|
|
|
|
|
|
? [...firstDefaultSection.fields].sort((a, b) => a.orderNo - b.orderNo)
|
|
|
|
|
|
: [];
|
feat: 품목관리 동적 렌더링 시스템 구현
- DynamicItemForm 컴포넌트 구조 생성
- DynamicField: 필드 타입별 렌더링
- DynamicSection: 섹션 단위 렌더링
- DynamicFormRenderer: 페이지 전체 렌더링
- 필드 타입별 컴포넌트 (TextField, NumberField, DropdownField, CheckboxField, DateField, FileField, CustomField)
- 커스텀 훅 (useDynamicFormState, useFormStructure, useConditionalFields)
- DataTable 공통 컴포넌트 (테이블, 페이지네이션, 검색, 탭필터, 통계카드)
- ItemFormWrapper: Feature Flag 기반 폼 선택
- 타입 정의 및 문서화
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 20:14:43 +09:00
|
|
|
|
|
|
|
|
|
|
return (
|
2025-12-04 12:48:41 +09:00
|
|
|
|
<form onSubmit={handleFormSubmit} className="space-y-6">
|
|
|
|
|
|
{/* Validation 에러 Alert */}
|
|
|
|
|
|
<ValidationAlert errors={errors} />
|
|
|
|
|
|
|
|
|
|
|
|
{/* 헤더 */}
|
|
|
|
|
|
<FormHeader
|
|
|
|
|
|
mode={mode}
|
|
|
|
|
|
selectedItemType={selectedItemType}
|
|
|
|
|
|
isSubmitting={isSubmitting}
|
|
|
|
|
|
onCancel={() => router.back()}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 기본 정보 - 목업과 동일한 레이아웃 (모든 필드 한 줄씩) */}
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader className="pb-4">
|
|
|
|
|
|
<CardTitle className="text-base font-medium">기본 정보</CardTitle>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent className="space-y-4">
|
|
|
|
|
|
{/* 품목 유형 선택 */}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<ItemTypeSelect
|
|
|
|
|
|
value={selectedItemType}
|
|
|
|
|
|
onChange={handleItemTypeChange}
|
|
|
|
|
|
disabled={mode === 'edit'}
|
|
|
|
|
|
required
|
|
|
|
|
|
/>
|
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
|
|
|
|
* 품목 유형에 따라 입력 항목이 다릅니다
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 직접 필드 (페이지에 직접 연결된 필드) */}
|
|
|
|
|
|
{selectedItemType && sortedDirectFields.map((dynamicField) => {
|
|
|
|
|
|
const field = dynamicField.field;
|
|
|
|
|
|
const fieldKey = field.field_key || `field_${field.id}`;
|
|
|
|
|
|
|
|
|
|
|
|
// 필드 조건부 표시 체크
|
|
|
|
|
|
if (!shouldShowField(field.id)) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<DynamicFieldRenderer
|
|
|
|
|
|
key={field.id}
|
|
|
|
|
|
field={field}
|
|
|
|
|
|
value={formData[fieldKey]}
|
|
|
|
|
|
onChange={(value) => setFieldValue(fieldKey, value)}
|
|
|
|
|
|
error={errors[fieldKey]}
|
|
|
|
|
|
disabled={isSubmitting}
|
|
|
|
|
|
unitOptions={unitOptions}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 첫 번째 섹션의 필드 렌더링 */}
|
|
|
|
|
|
{selectedItemType && firstSectionFields.map((dynamicField) => {
|
|
|
|
|
|
const field = dynamicField.field;
|
|
|
|
|
|
const fieldKey = field.field_key || `field_${field.id}`;
|
|
|
|
|
|
|
|
|
|
|
|
// 필드 조건부 표시 체크 (백엔드 설정 그대로 유지)
|
|
|
|
|
|
if (!shouldShowField(field.id)) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const isSpecField = fieldKey === activeSpecificationKey;
|
|
|
|
|
|
const isStatusField = fieldKey === statusFieldKey;
|
2025-12-04 20:52:42 +09:00
|
|
|
|
// 품목명 필드인지 체크 (FG 품목코드 자동생성 위치)
|
|
|
|
|
|
const isItemNameField = fieldKey === itemNameKey;
|
2025-12-04 12:48:41 +09:00
|
|
|
|
// 비고 필드인지 체크 (절곡부품 품목코드 자동생성 위치)
|
|
|
|
|
|
const fieldName = field.field_name || '';
|
|
|
|
|
|
const isNoteField = fieldKey.includes('note') || fieldKey.includes('비고') ||
|
|
|
|
|
|
fieldName.includes('비고') || fieldName === '비고';
|
2025-12-04 20:52:42 +09:00
|
|
|
|
// 인정 유효기간 종료일 필드인지 체크 (FG 시방서/인정서 파일 업로드 위치)
|
|
|
|
|
|
const isCertEndDateField = fieldKey.includes('certification_end') ||
|
|
|
|
|
|
fieldKey.includes('인정_유효기간_종료') ||
|
|
|
|
|
|
fieldName.includes('인정 유효기간 종료') ||
|
|
|
|
|
|
fieldName.includes('유효기간 종료');
|
2025-12-04 12:48:41 +09:00
|
|
|
|
|
|
|
|
|
|
// 절곡부품 박스 스타일링 (재질, 폭합계, 모양&길이)
|
|
|
|
|
|
const isBendingBoxField = isBendingPart && (
|
|
|
|
|
|
fieldKey === bendingFieldKeys.material ||
|
|
|
|
|
|
fieldKey === bendingFieldKeys.widthSum ||
|
|
|
|
|
|
fieldKey === bendingFieldKeys.shapeLength
|
|
|
|
|
|
);
|
|
|
|
|
|
const isFirstBendingBoxField = isBendingPart && fieldKey === bendingFieldKeys.material;
|
|
|
|
|
|
const isLastBendingBoxField = isBendingPart && fieldKey === bendingFieldKeys.shapeLength;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={field.id}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
isBendingBoxField && 'border-x border-gray-200 px-4 bg-gray-50/30',
|
|
|
|
|
|
isFirstBendingBoxField && 'border-t rounded-t-lg pt-4 mt-4',
|
|
|
|
|
|
isBendingBoxField && !isFirstBendingBoxField && '-mt-4 pt-4',
|
|
|
|
|
|
isLastBendingBoxField && 'border-b rounded-b-lg pb-4'
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<DynamicFieldRenderer
|
|
|
|
|
|
field={field}
|
|
|
|
|
|
value={formData[fieldKey]}
|
|
|
|
|
|
onChange={(value) => setFieldValue(fieldKey, value)}
|
|
|
|
|
|
error={errors[fieldKey]}
|
|
|
|
|
|
disabled={isSubmitting}
|
|
|
|
|
|
unitOptions={unitOptions}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{/* 규격 필드 바로 다음에 품목코드 자동생성 필드 표시 (절곡부품 제외) */}
|
|
|
|
|
|
{isSpecField && hasAutoItemCode && !isBendingPart && (
|
|
|
|
|
|
<div className="mt-4">
|
|
|
|
|
|
<Label htmlFor="item_code_auto">품목코드 (자동생성)</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="item_code_auto"
|
|
|
|
|
|
value={autoGeneratedItemCode || ''}
|
|
|
|
|
|
placeholder="품목명과 규격이 입력되면 자동으로 생성됩니다"
|
|
|
|
|
|
disabled
|
|
|
|
|
|
className="bg-muted text-muted-foreground"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
|
|
|
|
{selectedItemType === 'PT'
|
|
|
|
|
|
? "* 품목코드는 '영문약어-순번' 형식으로 자동 생성됩니다"
|
|
|
|
|
|
: "* 품목코드는 '품목명-규격' 형식으로 자동 생성됩니다"}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{/* 품목 상태 필드 하단 안내 메시지 */}
|
|
|
|
|
|
{isStatusField && (
|
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
|
|
|
|
* 비활성 시 품목 사용이 제한됩니다
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{/* 비고 필드 다음에 절곡부품 품목코드 자동생성 */}
|
|
|
|
|
|
{isNoteField && isBendingPart && (
|
|
|
|
|
|
<div className="mt-4">
|
|
|
|
|
|
<Label htmlFor="bending_item_code_auto">품목코드 (자동생성)</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="bending_item_code_auto"
|
|
|
|
|
|
value={autoBendingItemCode || ''}
|
|
|
|
|
|
placeholder="품목명과 종류가 입력되면 자동으로 생성됩니다"
|
|
|
|
|
|
disabled
|
|
|
|
|
|
className="bg-muted text-muted-foreground"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
|
|
|
|
* 절곡 부품 품목코드는 '품목명+종류+길이축약' 형식으로 자동 생성됩니다 (예: RM30)
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2025-12-04 20:52:42 +09:00
|
|
|
|
{/* 비고 필드 다음에 구매부품(전동개폐기) 품목코드 자동생성 */}
|
|
|
|
|
|
{isNoteField && isPurchasedPart && (
|
|
|
|
|
|
<div className="mt-4">
|
|
|
|
|
|
<Label htmlFor="purchased_item_code_auto">품목코드 (자동생성)</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="purchased_item_code_auto"
|
|
|
|
|
|
value={autoPurchasedItemCode || ''}
|
|
|
|
|
|
placeholder="품목명, 용량, 전원을 선택하면 자동으로 생성됩니다"
|
|
|
|
|
|
disabled
|
|
|
|
|
|
className="bg-muted text-muted-foreground"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
|
|
|
|
* 품목코드는 '품목명+용량+전원' 형식으로 자동 생성됩니다 (예: 전동개폐기150KG380V)
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{/* FG(제품) 전용: 품목명 필드 다음에 품목코드 자동생성 */}
|
|
|
|
|
|
{isItemNameField && selectedItemType === 'FG' && (
|
|
|
|
|
|
<div className="mt-4">
|
|
|
|
|
|
<Label htmlFor="fg_item_code_auto">품목코드 (자동생성)</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="fg_item_code_auto"
|
|
|
|
|
|
value={(formData[itemNameKey] as string) || ''}
|
|
|
|
|
|
placeholder="품목명이 입력되면 자동으로 동일하게 생성됩니다"
|
|
|
|
|
|
disabled
|
|
|
|
|
|
className="bg-muted text-muted-foreground"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
|
|
|
|
* 제품(FG)의 품목코드는 품목명과 동일하게 설정됩니다
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{/* FG(제품) 전용: 인정 유효기간 종료일 다음에 시방서/인정서 파일 업로드 */}
|
|
|
|
|
|
{isCertEndDateField && selectedItemType === 'FG' && (
|
2025-12-16 17:40:55 +09:00
|
|
|
|
<FileUploadFields
|
|
|
|
|
|
mode={mode}
|
|
|
|
|
|
isSubmitting={isSubmitting}
|
|
|
|
|
|
specificationFile={specificationFile}
|
|
|
|
|
|
setSpecificationFile={setSpecificationFile}
|
|
|
|
|
|
existingSpecificationFile={existingSpecificationFile}
|
|
|
|
|
|
existingSpecificationFileName={existingSpecificationFileName}
|
|
|
|
|
|
existingSpecificationFileId={existingSpecificationFileId}
|
|
|
|
|
|
certificationFile={certificationFile}
|
|
|
|
|
|
setCertificationFile={setCertificationFile}
|
|
|
|
|
|
existingCertificationFile={existingCertificationFile}
|
|
|
|
|
|
existingCertificationFileName={existingCertificationFileName}
|
|
|
|
|
|
existingCertificationFileId={existingCertificationFileId}
|
|
|
|
|
|
onFileDownload={handleFileDownload}
|
|
|
|
|
|
onDeleteFile={handleDeleteFile}
|
|
|
|
|
|
isDeletingFile={isDeletingFile}
|
|
|
|
|
|
/>
|
2025-12-04 20:52:42 +09:00
|
|
|
|
)}
|
2025-12-04 12:48:41 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 추가 섹션들 (기본 정보 카드 내에 하위 섹션으로 통합) */}
|
|
|
|
|
|
{selectedItemType && additionalSections.map((section) => {
|
|
|
|
|
|
// 조건부 표시 체크
|
|
|
|
|
|
if (!shouldShowSection(section.section.id)) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 부품 유형에 따른 섹션 필터링
|
|
|
|
|
|
const sectionTitle = section.section.title || '';
|
|
|
|
|
|
const isPurchaseSection = sectionTitle.includes('구매 부품');
|
|
|
|
|
|
const isAssemblySectionTitle = sectionTitle.includes('조립 부품');
|
|
|
|
|
|
|
|
|
|
|
|
// 조립 부품 선택 시 구매 부품 섹션 숨김
|
|
|
|
|
|
if (isAssemblyPart && isPurchaseSection) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 구매 부품 선택 시 조립 부품 섹션 숨김
|
|
|
|
|
|
if (!isAssemblyPart && !isBendingPart && isAssemblySectionTitle) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 섹션 필드 정렬
|
|
|
|
|
|
const sectionFields = [...section.fields].sort((a, b) => a.orderNo - b.orderNo);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div key={section.section.id} className="pt-4 border-t">
|
|
|
|
|
|
{/* 하위 섹션 제목 */}
|
|
|
|
|
|
<h3 className="text-sm font-medium mb-4">{section.section.title}</h3>
|
|
|
|
|
|
{section.section.description && (
|
|
|
|
|
|
<p className="text-xs text-muted-foreground mb-4">
|
|
|
|
|
|
{section.section.description}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{/* 하위 섹션 필드들 */}
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
|
{sectionFields.map((dynamicField) => {
|
|
|
|
|
|
const field = dynamicField.field;
|
|
|
|
|
|
const fieldKey = field.field_key || `field_${field.id}`;
|
|
|
|
|
|
|
|
|
|
|
|
// 필드 조건부 표시 체크
|
|
|
|
|
|
if (!shouldShowField(field.id)) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<DynamicFieldRenderer
|
|
|
|
|
|
key={field.id}
|
|
|
|
|
|
field={field}
|
|
|
|
|
|
value={formData[fieldKey]}
|
|
|
|
|
|
onChange={(value) => setFieldValue(fieldKey, value)}
|
|
|
|
|
|
error={errors[fieldKey]}
|
|
|
|
|
|
disabled={isSubmitting}
|
|
|
|
|
|
unitOptions={unitOptions}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 품목 유형 선택 안내 경고 */}
|
|
|
|
|
|
{!selectedItemType && (
|
|
|
|
|
|
<Alert className="bg-orange-50 border-orange-200">
|
|
|
|
|
|
<AlertDescription className="text-orange-900">
|
|
|
|
|
|
⚠️ 품목 유형을 먼저 선택해주세요
|
|
|
|
|
|
</AlertDescription>
|
|
|
|
|
|
</Alert>
|
feat: 품목관리 동적 렌더링 시스템 구현
- DynamicItemForm 컴포넌트 구조 생성
- DynamicField: 필드 타입별 렌더링
- DynamicSection: 섹션 단위 렌더링
- DynamicFormRenderer: 페이지 전체 렌더링
- 필드 타입별 컴포넌트 (TextField, NumberField, DropdownField, CheckboxField, DateField, FileField, CustomField)
- 커스텀 훅 (useDynamicFormState, useFormStructure, useConditionalFields)
- DataTable 공통 컴포넌트 (테이블, 페이지네이션, 검색, 탭필터, 통계카드)
- ItemFormWrapper: Feature Flag 기반 폼 선택
- 타입 정의 및 문서화
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 20:14:43 +09:00
|
|
|
|
)}
|
|
|
|
|
|
|
2025-12-04 12:48:41 +09:00
|
|
|
|
{/* 조립품 전개도 섹션 (PT - 조립 부품 전용) - 품목명 선택 시 표시 */}
|
|
|
|
|
|
{selectedItemType === 'PT' && isAssemblyPart && (
|
|
|
|
|
|
<BendingDiagramSection
|
|
|
|
|
|
selectedPartType="ASSEMBLY"
|
|
|
|
|
|
bendingDiagramInputMethod={bendingDiagramInputMethod}
|
|
|
|
|
|
setBendingDiagramInputMethod={setBendingDiagramInputMethod}
|
|
|
|
|
|
bendingDiagram={bendingDiagram}
|
|
|
|
|
|
setBendingDiagram={setBendingDiagram}
|
|
|
|
|
|
setBendingDiagramFile={setBendingDiagramFile}
|
|
|
|
|
|
setIsDrawingOpen={setIsDrawingOpen}
|
|
|
|
|
|
bendingDetails={bendingDetails}
|
|
|
|
|
|
setBendingDetails={setBendingDetails}
|
|
|
|
|
|
setWidthSum={setWidthSum}
|
2025-12-12 18:35:43 +09:00
|
|
|
|
widthSumFieldKey={bendingFieldKeys.widthSum}
|
2025-12-04 12:48:41 +09:00
|
|
|
|
setValue={(key, value) => setFieldValue(key, value)}
|
|
|
|
|
|
isSubmitting={isSubmitting}
|
2025-12-16 11:01:25 +09:00
|
|
|
|
existingBendingDiagram={existingBendingDiagram}
|
|
|
|
|
|
existingBendingDiagramFileId={existingBendingDiagramFileId}
|
|
|
|
|
|
onDeleteExistingFile={() => handleDeleteFile('bending_diagram')}
|
|
|
|
|
|
isDeletingFile={isDeletingFile === 'bending_diagram'}
|
2025-12-04 12:48:41 +09:00
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 절곡품 전개도 섹션 (PT - 절곡 부품 전용) */}
|
|
|
|
|
|
{selectedItemType === 'PT' && isBendingPart && (
|
|
|
|
|
|
<BendingDiagramSection
|
|
|
|
|
|
selectedPartType="BENDING"
|
|
|
|
|
|
bendingDiagramInputMethod={bendingDiagramInputMethod}
|
|
|
|
|
|
setBendingDiagramInputMethod={setBendingDiagramInputMethod}
|
|
|
|
|
|
bendingDiagram={bendingDiagram}
|
|
|
|
|
|
setBendingDiagram={setBendingDiagram}
|
|
|
|
|
|
setBendingDiagramFile={setBendingDiagramFile}
|
|
|
|
|
|
setIsDrawingOpen={setIsDrawingOpen}
|
|
|
|
|
|
bendingDetails={bendingDetails}
|
|
|
|
|
|
setBendingDetails={setBendingDetails}
|
|
|
|
|
|
setWidthSum={setWidthSum}
|
2025-12-12 18:35:43 +09:00
|
|
|
|
widthSumFieldKey={bendingFieldKeys.widthSum}
|
2025-12-04 12:48:41 +09:00
|
|
|
|
setValue={(key, value) => setFieldValue(key, value)}
|
|
|
|
|
|
isSubmitting={isSubmitting}
|
2025-12-16 11:01:25 +09:00
|
|
|
|
existingBendingDiagram={existingBendingDiagram}
|
|
|
|
|
|
existingBendingDiagramFileId={existingBendingDiagramFileId}
|
|
|
|
|
|
onDeleteExistingFile={() => handleDeleteFile('bending_diagram')}
|
|
|
|
|
|
isDeletingFile={isDeletingFile === 'bending_diagram'}
|
2025-12-04 12:48:41 +09:00
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* BOM 섹션 (부품구성 필요 체크 시에만 표시) */}
|
|
|
|
|
|
{selectedItemType && bomSection && (() => {
|
|
|
|
|
|
// bomRequiredFieldKey는 useMemo에서 structure 기반으로 미리 계산됨
|
|
|
|
|
|
const bomValue = bomRequiredFieldKey ? formData[bomRequiredFieldKey] : undefined;
|
|
|
|
|
|
const isBomRequired = bomValue === true || bomValue === 'true' || bomValue === '1' || bomValue === 1;
|
|
|
|
|
|
|
|
|
|
|
|
// 디버깅 로그
|
2025-12-04 20:52:42 +09:00
|
|
|
|
// console.log('[DynamicItemForm] BOM 체크 디버깅:', { bomRequiredFieldKey, bomValue, isBomRequired });
|
2025-12-04 12:48:41 +09:00
|
|
|
|
|
|
|
|
|
|
if (!isBomRequired) return null;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<DynamicBOMSection
|
|
|
|
|
|
section={bomSection}
|
|
|
|
|
|
bomLines={bomLines}
|
|
|
|
|
|
setBomLines={setBomLines}
|
|
|
|
|
|
bomSearchStates={bomSearchStates}
|
|
|
|
|
|
setBomSearchStates={setBomSearchStates}
|
|
|
|
|
|
isSubmitting={isSubmitting}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
})()}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 전개도 그리기 다이얼로그 (절곡품/조립품 공용) */}
|
|
|
|
|
|
<DrawingCanvas
|
|
|
|
|
|
open={isDrawingOpen}
|
|
|
|
|
|
onOpenChange={setIsDrawingOpen}
|
|
|
|
|
|
onSave={(imageData) => {
|
|
|
|
|
|
setBendingDiagram(imageData);
|
2025-12-09 18:07:47 +09:00
|
|
|
|
|
|
|
|
|
|
// Base64 string을 File 객체로 변환 (업로드용)
|
|
|
|
|
|
// 2025-12-06: 드로잉 방식에서도 파일 업로드 지원
|
|
|
|
|
|
try {
|
2025-12-16 17:40:55 +09:00
|
|
|
|
// eslint-disable-next-line no-undef
|
2025-12-09 18:07:47 +09:00
|
|
|
|
const byteString = atob(imageData.split(',')[1]);
|
|
|
|
|
|
const mimeType = imageData.split(',')[0].split(':')[1].split(';')[0];
|
|
|
|
|
|
const arrayBuffer = new ArrayBuffer(byteString.length);
|
|
|
|
|
|
const uint8Array = new Uint8Array(arrayBuffer);
|
|
|
|
|
|
for (let i = 0; i < byteString.length; i++) {
|
|
|
|
|
|
uint8Array[i] = byteString.charCodeAt(i);
|
|
|
|
|
|
}
|
2025-12-16 17:40:55 +09:00
|
|
|
|
// eslint-disable-next-line no-undef
|
2025-12-09 18:07:47 +09:00
|
|
|
|
const blob = new Blob([uint8Array], { type: mimeType });
|
|
|
|
|
|
const file = new File([blob], `bending_diagram_${Date.now()}.png`, { type: mimeType });
|
|
|
|
|
|
setBendingDiagramFile(file);
|
|
|
|
|
|
console.log('[DynamicItemForm] 드로잉 캔버스 → File 변환 성공:', file.name);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[DynamicItemForm] 드로잉 캔버스 → File 변환 실패:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-04 12:48:41 +09:00
|
|
|
|
setIsDrawingOpen(false);
|
feat: 품목관리 동적 렌더링 시스템 구현
- DynamicItemForm 컴포넌트 구조 생성
- DynamicField: 필드 타입별 렌더링
- DynamicSection: 섹션 단위 렌더링
- DynamicFormRenderer: 페이지 전체 렌더링
- 필드 타입별 컴포넌트 (TextField, NumberField, DropdownField, CheckboxField, DateField, FileField, CustomField)
- 커스텀 훅 (useDynamicFormState, useFormStructure, useConditionalFields)
- DataTable 공통 컴포넌트 (테이블, 페이지네이션, 검색, 탭필터, 통계카드)
- ItemFormWrapper: Feature Flag 기반 폼 선택
- 타입 정의 및 문서화
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 20:14:43 +09:00
|
|
|
|
}}
|
2025-12-04 12:48:41 +09:00
|
|
|
|
initialImage={bendingDiagram}
|
|
|
|
|
|
title={isAssemblyPart ? "조립품 전개도" : "절곡품 전개도"}
|
|
|
|
|
|
description={isAssemblyPart
|
|
|
|
|
|
? "조립품 전개도(바라시)를 그리거나 편집합니다."
|
|
|
|
|
|
: "절곡품 전개도를 그리거나 편집합니다."
|
|
|
|
|
|
}
|
feat: 품목관리 동적 렌더링 시스템 구현
- DynamicItemForm 컴포넌트 구조 생성
- DynamicField: 필드 타입별 렌더링
- DynamicSection: 섹션 단위 렌더링
- DynamicFormRenderer: 페이지 전체 렌더링
- 필드 타입별 컴포넌트 (TextField, NumberField, DropdownField, CheckboxField, DateField, FileField, CustomField)
- 커스텀 훅 (useDynamicFormState, useFormStructure, useConditionalFields)
- DataTable 공통 컴포넌트 (테이블, 페이지네이션, 검색, 탭필터, 통계카드)
- ItemFormWrapper: Feature Flag 기반 폼 선택
- 타입 정의 및 문서화
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 20:14:43 +09:00
|
|
|
|
/>
|
2025-12-12 18:35:43 +09:00
|
|
|
|
|
|
|
|
|
|
{/* 품목코드 중복 확인 다이얼로그 */}
|
2025-12-16 17:40:55 +09:00
|
|
|
|
<DuplicateCodeDialog
|
|
|
|
|
|
open={showDuplicateDialog}
|
|
|
|
|
|
onOpenChange={setShowDuplicateDialog}
|
|
|
|
|
|
onCancel={handleCancelDuplicate}
|
|
|
|
|
|
onGoToEdit={handleGoToEditDuplicate}
|
|
|
|
|
|
/>
|
feat: 품목관리 동적 렌더링 시스템 구현
- DynamicItemForm 컴포넌트 구조 생성
- DynamicField: 필드 타입별 렌더링
- DynamicSection: 섹션 단위 렌더링
- DynamicFormRenderer: 페이지 전체 렌더링
- 필드 타입별 컴포넌트 (TextField, NumberField, DropdownField, CheckboxField, DateField, FileField, CustomField)
- 커스텀 훅 (useDynamicFormState, useFormStructure, useConditionalFields)
- DataTable 공통 컴포넌트 (테이블, 페이지네이션, 검색, 탭필터, 통계카드)
- ItemFormWrapper: Feature Flag 기반 폼 선택
- 타입 정의 및 문서화
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 20:14:43 +09:00
|
|
|
|
</form>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|