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-04 12:48:41 +09:00
import { useState , useEffect , useMemo , useRef } from 'react' ;
import { useRouter } from 'next/navigation' ;
2025-12-16 11:01:25 +09:00
import { Package , Save , X , FileText , Trash2 , Download , Pencil , Upload } from 'lucide-react' ;
2025-12-04 12:48:41 +09:00
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 { Button } from '@/components/ui/button' ;
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' ;
import { useFormStructure , useDynamicFormState , useConditionalDisplay } from './hooks' ;
import { DynamicFieldRenderer } from './fields' ;
import { DynamicBOMSection } from './sections' ;
import {
generateItemCode ,
generateAssemblyItemNameSimple ,
generateAssemblySpecification ,
generateBendingItemCodeSimple ,
2025-12-04 20:52:42 +09:00
generatePurchasedItemCode ,
2025-12-04 12:48:41 +09:00
} from './utils/itemCodeGenerator' ;
2025-12-06 11:36:38 +09:00
import type { DynamicItemFormProps , DynamicFormData , DynamicSection , DynamicFieldValue , BOMLine , BOMSearchState , ItemSaveResult } 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-12 18:35:43 +09:00
import { uploadItemFile , deleteItemFile , ItemFileType , checkItemCodeDuplicate , DuplicateCheckResult } from '@/lib/api/items' ;
2025-12-16 11:01:25 +09:00
import { downloadFileById } from '@/lib/utils/fileDownload' ;
2025-12-12 18:35:43 +09:00
import { DuplicateCodeError } from '@/lib/api/error-handler' ;
import {
AlertDialog ,
AlertDialogAction ,
AlertDialogCancel ,
AlertDialogContent ,
AlertDialogDescription ,
AlertDialogFooter ,
AlertDialogHeader ,
AlertDialogTitle ,
} from '@/components/ui/alert-dialog' ;
2025-12-04 12:48:41 +09:00
/ * *
* 헤 더 컴 포 넌 트 - 기 존 FormHeader와 동 일 한 디 자 인
* /
function FormHeader ( {
mode ,
selectedItemType ,
isSubmitting ,
onCancel ,
} : {
mode : 'create' | 'edit' ;
selectedItemType : string ;
isSubmitting : boolean ;
onCancel : ( ) = > void ;
} ) {
return (
< div className = "flex flex-col md:flex-row md:items-center justify-between gap-4" >
< div className = "flex items-start gap-3" >
< div className = "p-2 bg-primary/10 rounded-lg hidden md:block" >
< Package className = "w-6 h-6 text-primary" / >
< / div >
< div >
< h1 className = "text-xl md:text-2xl" >
{ mode === 'create' ? '품목 등록' : '품목 수정' }
< / h1 >
< p className = "text-sm text-muted-foreground mt-1" >
품 목 정 보 를 입 력 하 세 요
< / p >
< / div >
< / div >
< div className = "flex gap-1 sm:gap-2" >
< Button
type = "button"
variant = "outline"
size = "sm"
onClick = { onCancel }
className = "gap-1 sm:gap-2"
disabled = { isSubmitting }
>
< X className = "h-4 w-4" / >
< span className = "hidden sm:inline" > 취 소 < / span >
< / Button >
< Button
type = "submit"
size = "sm"
disabled = { ! selectedItemType || isSubmitting }
className = "gap-1 sm:gap-2"
>
< Save className = "h-4 w-4" / >
< span className = "hidden sm:inline" > { isSubmitting ? '저장 중...' : '저장' } < / span >
< / Button >
< / div >
< / div >
) ;
}
/ * *
* 밸 리 데 이 션 에 러 Alert - 기 존 ValidationAlert와 동 일 한 디 자 인
* /
function ValidationAlert ( { errors } : { errors : Record < string , string > } ) {
const errorCount = Object . keys ( errors ) . length ;
if ( errorCount === 0 ) {
return null ;
}
return (
< Alert className = "bg-red-50 border-red-200" >
< AlertDescription className = "text-red-900" >
< div className = "flex items-start gap-2" >
< span className = "text-lg" > ⚠ ️ < / span >
< div className = "flex-1" >
< strong className = "block mb-2" >
입 력 내 용 을 확 인 해 주 세 요 ( { errorCount } 개 오 류 )
< / strong >
< ul className = "space-y-1 text-sm" >
{ Object . entries ( errors ) . map ( ( [ fieldKey , errorMessage ] ) = > (
< li key = { fieldKey } className = "flex items-start gap-1" >
< span > • < / span >
< span > { errorMessage } < / span >
< / li >
) ) }
< / ul >
< / div >
< / div >
< / AlertDescription >
< / Alert >
) ;
}
/ * *
* 동 적 섹 션 렌 더 러
* /
function DynamicSectionRenderer ( {
section ,
formData ,
errors ,
onChange ,
disabled ,
unitOptions ,
autoGeneratedItemCode ,
shouldShowField ,
} : {
section : DynamicSection ;
formData : DynamicFormData ;
errors : Record < string , string > ;
onChange : ( fieldKey : string , value : DynamicFieldValue ) = > void ;
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
disabled? : boolean ;
2025-12-04 12:48:41 +09:00
unitOptions : { label : string ; value : string } [ ] ;
autoGeneratedItemCode? : string ;
shouldShowField ? : ( fieldId : number ) = > boolean ;
} ) {
// 필드를 order_no 기준 정렬
const sortedFields = [ . . . section . fields ] . sort ( ( a , b ) = > a . orderNo - b . orderNo ) ;
// 이 섹션에 item_name과 specification 필드가 둘 다 있는지 체크
// field_key가 "{id}_item_name" 형식으로 올 수 있어서 includes로 체크
const fieldKeys = sortedFields . map ( ( f ) = > f . field . field_key || ` field_ ${ f . field . id } ` ) ;
const hasItemName = fieldKeys . some ( ( k ) = > k . includes ( 'item_name' ) ) ;
const hasSpecification = fieldKeys . some ( ( k ) = > k . includes ( 'specification' ) ) ;
const shouldShowItemCode = hasItemName && hasSpecification && autoGeneratedItemCode !== undefined ;
return (
< Card >
< CardHeader >
< CardTitle > { section . section . title } < / CardTitle >
{ section . section . description && (
< p className = "text-sm text-muted-foreground" >
{ section . section . description }
< / p >
) }
< / CardHeader >
< CardContent className = "space-y-4" >
{ sortedFields . map ( ( dynamicField ) = > {
const field = dynamicField . field ;
const fieldKey = field . field_key || ` field_ ${ field . id } ` ;
// 필드 조건부 표시 체크
if ( shouldShowField && ! shouldShowField ( field . id ) ) {
return null ;
}
return (
< DynamicFieldRenderer
key = { field . id }
field = { field }
value = { formData [ fieldKey ] }
onChange = { ( value ) = > onChange ( fieldKey , value ) }
error = { errors [ fieldKey ] }
disabled = { disabled }
unitOptions = { unitOptions }
/ >
) ;
} ) }
{ /* 품목코드 자동생성 필드 (item_name + specification 있는 섹션에만 표시) */ }
{ shouldShowItemCode && (
< div >
< 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" >
* 품 목 코 드 는 & apos ; 품 목 명 - 규 격 & apos ; 형 식 으 로 자 동 생 성 됩 니 다
< / p >
< / div >
) }
< / CardContent >
< / Card >
) ;
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-06 11:36:38 +09:00
// 기존 파일 URL 상태 (edit 모드에서 사용)
const [ existingBendingDiagram , setExistingBendingDiagram ] = useState < string > ( '' ) ;
2025-12-12 18:35:43 +09:00
const [ existingBendingDiagramFileId , setExistingBendingDiagramFileId ] = useState < number | null > ( null ) ;
2025-12-06 11:36:38 +09:00
const [ existingSpecificationFile , setExistingSpecificationFile ] = useState < string > ( '' ) ;
const [ existingSpecificationFileName , setExistingSpecificationFileName ] = useState < string > ( '' ) ;
2025-12-12 18:35:43 +09:00
const [ existingSpecificationFileId , setExistingSpecificationFileId ] = useState < number | null > ( null ) ;
2025-12-06 11:36:38 +09:00
const [ existingCertificationFile , setExistingCertificationFile ] = useState < string > ( '' ) ;
const [ existingCertificationFileName , setExistingCertificationFileName ] = useState < string > ( '' ) ;
2025-12-12 18:35:43 +09:00
const [ existingCertificationFileId , setExistingCertificationFileId ] = useState < number | null > ( null ) ;
2025-12-06 11:36:38 +09:00
const [ isDeletingFile , setIsDeletingFile ] = useState < string | null > ( null ) ;
2025-12-12 18:35:43 +09:00
// 품목코드 중복 체크 상태 관리
const [ showDuplicateDialog , setShowDuplicateDialog ] = useState ( false ) ;
const [ duplicateCheckResult , setDuplicateCheckResult ] = useState < DuplicateCheckResult | null > ( null ) ;
const [ pendingSubmitData , setPendingSubmitData ] = useState < DynamicFormData | null > ( null ) ;
// initialData에서 기존 파일 정보 및 전개도 상세 데이터 로드 (edit 모드)
2025-12-06 11:36:38 +09:00
useEffect ( ( ) = > {
if ( mode === 'edit' && initialData ) {
2025-12-16 11:01:25 +09:00
// files 객체에서 파일 정보 추출 (단수: specification_file, certification_file)
// 2025-12-15: files가 JSON 문자열로 올 수 있으므로 파싱 처리
2025-12-12 18:35:43 +09:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2025-12-16 11:01:25 +09:00
let filesRaw = ( initialData as any ) . files ;
// JSON 문자열인 경우 파싱
if ( typeof filesRaw === 'string' ) {
try {
filesRaw = JSON . parse ( filesRaw ) ;
console . log ( '[DynamicItemForm] files JSON 문자열 파싱 완료' ) ;
} catch ( e ) {
console . error ( '[DynamicItemForm] files JSON 파싱 실패:' , e ) ;
filesRaw = undefined ;
}
}
const files = filesRaw as {
2025-12-12 18:35:43 +09:00
bending_diagram? : Array < { id : number ; file_name : string ; file_path : string } > ;
2025-12-16 11:01:25 +09:00
specification_file? : Array < { id : number ; file_name : string ; file_path : string } > ;
certification_file? : Array < { id : number ; file_name : string ; file_path : string } > ;
2025-12-12 18:35:43 +09:00
} | undefined ;
2025-12-16 11:01:25 +09:00
// 2025-12-15: 파일 로드 디버깅
console . log ( '[DynamicItemForm] 파일 로드 시작' ) ;
console . log ( '[DynamicItemForm] initialData.files (raw):' , ( initialData as any ) . files ) ;
console . log ( '[DynamicItemForm] filesRaw 타입:' , typeof filesRaw ) ;
console . log ( '[DynamicItemForm] files 변수:' , files ) ;
console . log ( '[DynamicItemForm] specification_file:' , files ? . specification_file ) ;
console . log ( '[DynamicItemForm] certification_file:' , files ? . certification_file ) ;
// 전개도 파일 (배열의 마지막 파일 = 최신 파일을 가져옴)
// 2025-12-15: .at(-1) 대신 slice(-1)[0] 사용 (ES2022 이전 호환성)
const bendingFileArr = files ? . bending_diagram ;
const bendingFile = bendingFileArr && bendingFileArr . length > 0
? bendingFileArr [ bendingFileArr . length - 1 ]
: undefined ;
if ( bendingFile ) {
console . log ( '[DynamicItemForm] bendingFile 전체 객체:' , bendingFile ) ;
console . log ( '[DynamicItemForm] bendingFile 키 목록:' , Object . keys ( bendingFile ) ) ;
2025-12-12 18:35:43 +09:00
setExistingBendingDiagram ( bendingFile . file_path ) ;
2025-12-16 11:01:25 +09:00
// API에서 id 또는 file_id로 올 수 있음
const bendingFileId = ( bendingFile as Record < string , unknown > ) . id || ( bendingFile as Record < string , unknown > ) . file_id ;
console . log ( '[DynamicItemForm] bendingFile ID 추출:' , { id : ( bendingFile as Record < string , unknown > ) . id , file_id : ( bendingFile as Record < string , unknown > ) . file_id , final : bendingFileId } ) ;
setExistingBendingDiagramFileId ( bendingFileId as number ) ;
2025-12-12 18:35:43 +09:00
} else if ( initialData . bending_diagram ) {
2025-12-06 11:36:38 +09:00
setExistingBendingDiagram ( initialData . bending_diagram as string ) ;
}
2025-12-12 18:35:43 +09:00
2025-12-16 11:01:25 +09:00
// 시방서 파일 (배열의 마지막 파일 = 최신 파일을 가져옴)
// 2025-12-15: .at(-1) 대신 배열 인덱스 사용 (ES2022 이전 호환성)
const specFileArr = files ? . specification_file ;
const specFile = specFileArr && specFileArr . length > 0
? specFileArr [ specFileArr . length - 1 ]
: undefined ;
console . log ( '[DynamicItemForm] specFile 전체 객체:' , specFile ) ;
console . log ( '[DynamicItemForm] specFile 키 목록:' , specFile ? Object . keys ( specFile ) : 'undefined' ) ;
if ( specFile ? . file_path ) {
2025-12-12 18:35:43 +09:00
setExistingSpecificationFile ( specFile . file_path ) ;
2025-12-16 11:01:25 +09:00
setExistingSpecificationFileName ( specFile . file_name || '시방서' ) ;
// API에서 id 또는 file_id로 올 수 있음
const specFileId = ( specFile as Record < string , unknown > ) . id || ( specFile as Record < string , unknown > ) . file_id ;
console . log ( '[DynamicItemForm] specFile ID 추출:' , { id : ( specFile as Record < string , unknown > ) . id , file_id : ( specFile as Record < string , unknown > ) . file_id , final : specFileId } ) ;
setExistingSpecificationFileId ( specFileId as number || null ) ;
} else {
// 파일이 없으면 상태 초기화 (이전 값 제거)
setExistingSpecificationFile ( '' ) ;
setExistingSpecificationFileName ( '' ) ;
setExistingSpecificationFileId ( null ) ;
2025-12-06 11:36:38 +09:00
}
2025-12-12 18:35:43 +09:00
2025-12-16 11:01:25 +09:00
// 인정서 파일 (배열의 마지막 파일 = 최신 파일을 가져옴)
// 2025-12-15: .at(-1) 대신 배열 인덱스 사용 (ES2022 이전 호환성)
const certFileArr = files ? . certification_file ;
const certFile = certFileArr && certFileArr . length > 0
? certFileArr [ certFileArr . length - 1 ]
: undefined ;
console . log ( '[DynamicItemForm] certFile 전체 객체:' , certFile ) ;
console . log ( '[DynamicItemForm] certFile 키 목록:' , certFile ? Object . keys ( certFile ) : 'undefined' ) ;
if ( certFile ? . file_path ) {
2025-12-12 18:35:43 +09:00
setExistingCertificationFile ( certFile . file_path ) ;
2025-12-16 11:01:25 +09:00
setExistingCertificationFileName ( certFile . file_name || '인정서' ) ;
// API에서 id 또는 file_id로 올 수 있음
const certFileId = ( certFile as Record < string , unknown > ) . id || ( certFile as Record < string , unknown > ) . file_id ;
console . log ( '[DynamicItemForm] certFile ID 추출:' , { id : ( certFile as Record < string , unknown > ) . id , file_id : ( certFile as Record < string , unknown > ) . file_id , final : certFileId } ) ;
setExistingCertificationFileId ( certFileId as number || null ) ;
} else {
// 파일이 없으면 상태 초기화 (이전 값 제거)
setExistingCertificationFile ( '' ) ;
setExistingCertificationFileName ( '' ) ;
setExistingCertificationFileId ( null ) ;
2025-12-06 11:36:38 +09:00
}
2025-12-12 18:35:43 +09:00
// 전개도 상세 데이터 로드 (bending_details)
if ( initialData . bending_details ) {
const details = Array . isArray ( initialData . bending_details )
? initialData . bending_details
: ( typeof initialData . bending_details === 'string'
? JSON . parse ( initialData . bending_details )
: [ ] ) ;
if ( details . length > 0 ) {
// BendingDetail 형식으로 변환
2025-12-16 11:01:25 +09:00
// 2025-12-16: 명시적 Number() 변환 추가 - TypeScript 타입 캐스팅은 런타임 변환을 하지 않음
// 백엔드에서 문자열로 올 수 있으므로 명시적 숫자 변환 필수
2025-12-12 18:35:43 +09:00
const mappedDetails : BendingDetail [ ] = details . map ( ( d : Record < string , unknown > , index : number ) = > ( {
id : ( d . id as string ) || ` detail- ${ Date . now ( ) } - ${ index } ` ,
2025-12-16 11:01:25 +09:00
no : Number ( d . no ) || index + 1 ,
input : Number ( d . input ) || 0 ,
// elongation은 0이 유효한 값이므로 NaN 체크 필요
elongation : ! isNaN ( Number ( d . elongation ) ) ? Number ( d . elongation ) : - 1 ,
calculated : Number ( d . calculated ) || 0 ,
sum : Number ( d . sum ) || 0 ,
shaded : Boolean ( d . shaded ) ,
aAngle : d.aAngle !== undefined ? Number ( d . aAngle ) : undefined ,
2025-12-12 18:35:43 +09:00
} ) ) ;
setBendingDetails ( mappedDetails ) ;
// 폭 합계도 계산하여 설정
const totalSum = mappedDetails . reduce ( ( acc , detail ) = > {
return acc + detail . input + detail . elongation ;
} , 0 ) ;
setWidthSum ( totalSum . toString ( ) ) ;
}
}
2025-12-06 11:36:38 +09:00
}
} , [ mode , initialData ] ) ;
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 11:01:25 +09:00
// 파일 다운로드 핸들러 (Blob 방식)
const handleFileDownload = async ( fileId : number | null , fileName? : string ) = > {
if ( ! fileId ) return ;
try {
await downloadFileById ( fileId , fileName ) ;
} catch ( error ) {
console . error ( '[DynamicItemForm] 다운로드 실패:' , error ) ;
alert ( '파일 다운로드에 실패했습니다.' ) ;
2025-12-06 11:36:38 +09:00
}
} ;
// 파일 삭제 핸들러
const handleDeleteFile = async ( fileType : ItemFileType ) = > {
2025-12-16 11:01:25 +09:00
console . log ( '[DynamicItemForm] handleDeleteFile 호출:' , {
fileType ,
propItemId ,
existingBendingDiagramFileId ,
existingSpecificationFileId ,
existingCertificationFileId ,
} ) ;
if ( ! propItemId ) {
console . error ( '[DynamicItemForm] propItemId가 없습니다' ) ;
return ;
}
// 파일 ID 가져오기
let fileId : number | null = null ;
if ( fileType === 'bending_diagram' ) {
fileId = existingBendingDiagramFileId ;
} else if ( fileType === 'specification' ) {
fileId = existingSpecificationFileId ;
} else if ( fileType === 'certification' ) {
fileId = existingCertificationFileId ;
}
console . log ( '[DynamicItemForm] 삭제할 파일 ID:' , fileId ) ;
if ( ! fileId ) {
console . error ( '[DynamicItemForm] 파일 ID를 찾을 수 없습니다:' , fileType ) ;
alert ( '파일 ID를 찾을 수 없습니다.' ) ;
return ;
}
2025-12-06 11:36:38 +09:00
const confirmMessage = fileType === 'bending_diagram' ? '전개도 이미지를' :
fileType === 'specification' ? '시방서 파일을' : '인정서 파일을' ;
if ( ! confirm ( ` ${ confirmMessage } 삭제하시겠습니까? ` ) ) return ;
try {
setIsDeletingFile ( fileType ) ;
2025-12-16 11:01:25 +09:00
await deleteItemFile ( propItemId , fileId , selectedItemType || 'FG' ) ;
2025-12-06 11:36:38 +09:00
// 상태 업데이트
if ( fileType === 'bending_diagram' ) {
setExistingBendingDiagram ( '' ) ;
setBendingDiagram ( '' ) ;
2025-12-16 11:01:25 +09:00
setExistingBendingDiagramFileId ( null ) ;
2025-12-06 11:36:38 +09:00
} else if ( fileType === 'specification' ) {
setExistingSpecificationFile ( '' ) ;
setExistingSpecificationFileName ( '' ) ;
2025-12-16 11:01:25 +09:00
setExistingSpecificationFileId ( null ) ;
2025-12-06 11:36:38 +09:00
} else if ( fileType === 'certification' ) {
setExistingCertificationFile ( '' ) ;
setExistingCertificationFileName ( '' ) ;
2025-12-16 11:01:25 +09:00
setExistingCertificationFileId ( null ) ;
2025-12-06 11:36:38 +09:00
}
alert ( '파일이 삭제되었습니다.' ) ;
} catch ( error ) {
console . error ( '[DynamicItemForm] 파일 삭제 실패:' , error ) ;
alert ( '파일 삭제에 실패했습니다.' ) ;
} finally {
setIsDeletingFile ( null ) ;
}
} ;
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 ] ) ;
// 품목코드 자동생성 관련 필드 정보
// field_key 또는 field_name 기준으로 품목명/규격 필드 탐지
// 2025-12-03: 연동 드롭다운 로직 제거 - 조건부 섹션 표시로 대체
const { hasAutoItemCode , itemNameKey , allSpecificationKeys , statusFieldKey } = useMemo ( ( ) = > {
if ( ! structure ) return { hasAutoItemCode : false , itemNameKey : '' , allSpecificationKeys : [ ] as string [ ] , statusFieldKey : '' } ;
let foundItemNameKey = '' ;
let foundStatusFieldKey = '' ;
const specificationKeys : string [ ] = [ ] ; // 모든 규격 필드 키 수집
const checkField = ( fieldKey : string , field : ItemFieldResponse ) = > {
const fieldName = field . field_name || '' ;
// 품목명 필드 탐지 (field_key 또는 field_name 기준)
const isItemName = fieldKey . includes ( 'item_name' ) || fieldName . includes ( '품목명' ) ;
if ( isItemName && ! foundItemNameKey ) {
foundItemNameKey = fieldKey ;
}
// 규격 필드 탐지
// specification, standard, 규격, 사양 모두 지원
const isSpecification = fieldKey . includes ( 'specification' ) || fieldKey . includes ( 'standard' ) ||
fieldKey . includes ( '규격' ) || fieldName . includes ( '규격' ) || fieldName . includes ( '사양' ) ;
if ( isSpecification ) {
specificationKeys . push ( fieldKey ) ;
}
// 품목 상태 필드 탐지 (is_active, status, 품목상태, 품목 상태)
const isStatusField = fieldKey . includes ( 'is_active' ) || fieldKey . includes ( 'status' ) ||
fieldKey . includes ( 'active' ) || fieldName . includes ( '품목상태' ) ||
fieldName . includes ( '품목 상태' ) || fieldName === '상태' ;
if ( isStatusField && ! foundStatusFieldKey ) {
foundStatusFieldKey = fieldKey ;
}
} ;
structure . sections . forEach ( ( section ) = > {
section . fields . forEach ( ( f ) = > {
const key = f . field . field_key || ` field_ ${ f . field . id } ` ;
checkField ( key , f . field ) ;
} ) ;
} ) ;
structure . directFields . forEach ( ( f ) = > {
const key = f . field . field_key || ` field_ ${ f . field . id } ` ;
checkField ( key , f . field ) ;
} ) ;
return {
// PT(부품)도 품목코드 자동생성 포함
hasAutoItemCode : ! ! foundItemNameKey ,
itemNameKey : foundItemNameKey ,
allSpecificationKeys : specificationKeys ,
statusFieldKey : foundStatusFieldKey ,
} ;
} , [ structure ] ) ;
// 현재 표시 중인 규격 필드 키 (조건부 표시 고려)
// 2025-12-03: 조건부 표시로 숨겨진 필드는 제외하고, 실제 표시되는 규격 필드만 선택
const activeSpecificationKey = useMemo ( ( ) = > {
if ( ! structure || allSpecificationKeys . length === 0 ) return '' ;
// 모든 규격 필드 중 현재 표시 중인 첫 번째 필드 찾기
for ( const section of structure . sections ) {
// 섹션이 숨겨져 있으면 스킵
if ( ! shouldShowSection ( section . section . id ) ) continue ;
for ( const f of section . fields ) {
const fieldKey = f . field . field_key || ` field_ ${ f . field . id } ` ;
// 필드가 숨겨져 있으면 스킵
if ( ! shouldShowField ( f . field . id ) ) continue ;
// 규격 필드인지 확인
if ( allSpecificationKeys . includes ( fieldKey ) ) {
return fieldKey ;
}
}
}
// 직접 필드에서도 찾기
for ( const f of structure . directFields ) {
const fieldKey = f . field . field_key || ` field_ ${ f . field . id } ` ;
if ( ! shouldShowField ( f . field . id ) ) continue ;
if ( allSpecificationKeys . includes ( fieldKey ) ) {
return fieldKey ;
}
}
// 표시 중인 규격 필드가 없으면 첫 번째 규격 필드 반환 (fallback)
return allSpecificationKeys [ 0 ] || '' ;
} , [ structure , allSpecificationKeys , shouldShowSection , shouldShowField ] ) ;
2025-12-04 20:52:42 +09:00
// 부품 유형 필드 탐지 (PT 품목에서 절곡/조립/구매 부품 판별용)
const { partTypeFieldKey , selectedPartType , isBendingPart , isAssemblyPart , isPurchasedPart } = useMemo ( ( ) = > {
2025-12-04 12:48:41 +09:00
if ( ! structure || selectedItemType !== 'PT' ) {
2025-12-04 20:52:42 +09:00
return { partTypeFieldKey : '' , selectedPartType : '' , isBendingPart : false , isAssemblyPart : false , isPurchasedPart : false } ;
2025-12-04 12:48:41 +09:00
}
let foundPartTypeKey = '' ;
// 모든 필드에서 부품 유형 필드 찾기
const checkField = ( fieldKey : string , field : ItemFieldResponse ) = > {
const fieldName = field . field_name || '' ;
// part_type, 부품유형, 부품 유형 등 탐지
const isPartType = fieldKey . includes ( 'part_type' ) ||
fieldName . includes ( '부품유형' ) ||
fieldName . includes ( '부품 유형' ) ;
if ( isPartType && ! foundPartTypeKey ) {
foundPartTypeKey = fieldKey ;
}
} ;
structure . sections . forEach ( ( section ) = > {
section . fields . forEach ( ( f ) = > {
const key = f . field . field_key || ` field_ ${ f . field . id } ` ;
checkField ( key , f . field ) ;
} ) ;
} ) ;
structure . directFields . forEach ( ( f ) = > {
const key = f . field . field_key || ` field_ ${ f . field . id } ` ;
checkField ( key , f . field ) ;
} ) ;
const currentPartType = ( formData [ foundPartTypeKey ] as string ) || '' ;
// "절곡 부품", "BENDING", "절곡부품" 등 다양한 형태 지원
const isBending = currentPartType . includes ( '절곡' ) || currentPartType . toUpperCase ( ) === 'BENDING' ;
// "조립 부품", "ASSEMBLY", "조립부품" 등 다양한 형태 지원
const isAssembly = currentPartType . includes ( '조립' ) || currentPartType . toUpperCase ( ) === 'ASSEMBLY' ;
2025-12-04 20:52:42 +09:00
// "구매 부품", "PURCHASED", "구매부품" 등 다양한 형태 지원
const isPurchased = currentPartType . includes ( '구매' ) || currentPartType . toUpperCase ( ) === 'PURCHASED' ;
2025-12-04 12:48:41 +09:00
2025-12-04 20:52:42 +09:00
// console.log('[DynamicItemForm] 부품 유형 감지:', { partTypeFieldKey: foundPartTypeKey, currentPartType, isBending, isAssembly, isPurchased });
2025-12-04 12:48:41 +09:00
return {
partTypeFieldKey : foundPartTypeKey ,
selectedPartType : currentPartType ,
isBendingPart : isBending ,
isAssemblyPart : isAssembly ,
2025-12-04 20:52:42 +09:00
isPurchasedPart : isPurchased ,
2025-12-04 12:48:41 +09:00
} ;
} , [ structure , selectedItemType , formData ] ) ;
// 이전 부품 유형 값 추적 (부품 유형 변경 감지용)
const prevPartTypeRef = useRef < string > ( '' ) ;
// 부품 유형 변경 시 조건부 표시 관련 필드 초기화
// 2025-12-04: 절곡 ↔ 조립 부품 전환 시 formData 값이 유지되어
// 조건부 표시가 잘못 트리거되는 버그 수정
// 2025-12-04: setTimeout으로 초기화를 다음 틱으로 미뤄서 Select 두 번 클릭 문제 해결
useEffect ( ( ) = > {
if ( selectedItemType !== 'PT' || ! partTypeFieldKey ) return ;
const currentPartType = selectedPartType ;
const prevPartType = prevPartTypeRef . current ;
// 이전 값이 있고, 현재 값과 다른 경우에만 초기화
if ( prevPartType && prevPartType !== currentPartType ) {
2025-12-04 20:52:42 +09:00
// console.log('[DynamicItemForm] 부품 유형 변경 감지:', prevPartType, '→', currentPartType);
2025-12-04 12:48:41 +09:00
// setTimeout으로 다음 틱에서 초기화 실행
// → 부품 유형 Select 값 변경이 먼저 완료된 후 초기화
setTimeout ( ( ) = > {
// 조건부 표시 대상이 될 수 있는 필드들 수집 및 초기화
// (품목명, 재질, 종류, 폭 합계, 모양&길이 등)
const fieldsToReset : string [ ] = [ ] ;
// structure에서 조건부 표시 설정이 있는 필드들 찾기
if ( structure ) {
structure . sections . forEach ( ( section ) = > {
section . fields . forEach ( ( f ) = > {
const field = f . field ;
const fieldKey = field . field_key || ` field_ ${ field . id } ` ;
const fieldName = field . field_name || '' ;
// 부품 유형 필드는 초기화에서 제외
if ( fieldKey === partTypeFieldKey ) return ;
// 조건부 표시 트리거 필드 (display_condition이 있는 필드)
if ( field . display_condition ) {
fieldsToReset . push ( fieldKey ) ;
}
// 조건부 표시 대상 필드 (재질, 종류, 폭 합계, 모양&길이 등)
const isBendingRelated =
fieldName . includes ( '재질' ) || fieldName . includes ( '종류' ) ||
fieldName . includes ( '폭' ) || fieldName . includes ( '모양' ) ||
fieldName . includes ( '길이' ) || fieldKey . includes ( 'material' ) ||
fieldKey . includes ( 'category' ) || fieldKey . includes ( 'width' ) ||
fieldKey . includes ( 'shape' ) || fieldKey . includes ( 'length' ) ;
if ( isBendingRelated ) {
fieldsToReset . push ( fieldKey ) ;
}
} ) ;
} ) ;
// 품목명 필드도 초기화 (조건부 표시 트리거 역할)
if ( itemNameKey ) {
fieldsToReset . push ( itemNameKey ) ;
}
}
// 중복 제거 후 초기화
const uniqueFields = [ . . . new Set ( fieldsToReset ) ] ;
2025-12-04 20:52:42 +09:00
// console.log('[DynamicItemForm] 초기화할 필드:', uniqueFields);
2025-12-04 12:48:41 +09:00
uniqueFields . forEach ( ( fieldKey ) = > {
setFieldValue ( fieldKey , '' ) ;
} ) ;
} , 0 ) ;
}
// 현재 값을 이전 값으로 저장
prevPartTypeRef . current = currentPartType ;
} , [ selectedItemType , partTypeFieldKey , selectedPartType , structure , itemNameKey , setFieldValue ] ) ;
// 절곡부품 전용 필드 탐지 (재질, 종류, 폭 합계, 모양&길이)
// 2025-12-04: 조건부 표시 고려하여 종류 필드 선택 로직 개선
const { bendingFieldKeys , autoBendingItemCode , allCategoryKeysWithIds } = useMemo ( ( ) = > {
if ( ! structure || selectedItemType !== 'PT' || ! isBendingPart ) {
return {
bendingFieldKeys : {
material : '' , // 재질
category : '' , // 종류
widthSum : '' , // 폭 합계
shapeLength : '' , // 모양&길이
itemName : '' , // 품목명 (절곡부품 코드 생성용)
} ,
autoBendingItemCode : '' ,
allCategoryKeysWithIds : [ ] as Array < { key : string ; id : number } > ,
} ;
}
let materialKey = '' ;
const categoryKeysWithIds : Array < { key : string ; id : number } > = [ ] ; // 종류 필드 + ID
let widthSumKey = '' ;
let shapeLengthKey = '' ;
let bendingItemNameKey = '' ; // 절곡부품용 품목명 키
const checkField = ( fieldKey : string , field : ItemFieldResponse ) = > {
const fieldName = field . field_name || '' ;
const lowerKey = fieldKey . toLowerCase ( ) ;
// 절곡부품 품목명 필드 탐지 - bending_parts 우선
// 2025-12-04: 조립부품/절곡부품 품목명 필드가 모두 있을 때 절곡부품용 우선 선택
const isBendingItemNameField =
lowerKey . includes ( 'bending_parts' ) ||
lowerKey . includes ( 'bending_item' ) ||
lowerKey . includes ( '절곡부품' ) ||
lowerKey . includes ( '절곡_부품' ) ||
fieldName . includes ( '절곡부품' ) ||
fieldName . includes ( '절곡 부품' ) ;
const isGeneralItemNameField =
lowerKey . includes ( 'item_name' ) ||
lowerKey . includes ( '품목명' ) ||
fieldName . includes ( '품목명' ) ||
fieldName === '품목명' ;
// bending_parts는 무조건 우선 (덮어쓰기)
if ( isBendingItemNameField ) {
2025-12-04 20:52:42 +09:00
// console.log('[checkField] 절곡부품 품목명 필드 발견!', { fieldKey, fieldName });
2025-12-04 12:48:41 +09:00
bendingItemNameKey = fieldKey ;
}
// 일반 품목명은 아직 없을 때만
else if ( isGeneralItemNameField && ! bendingItemNameKey ) {
2025-12-04 20:52:42 +09:00
// console.log('[checkField] 일반 품목명 필드 발견!', { fieldKey, fieldName });
2025-12-04 12:48:41 +09:00
bendingItemNameKey = fieldKey ;
}
// 재질 필드
if ( lowerKey . includes ( 'material' ) || lowerKey . includes ( '재질' ) ||
lowerKey . includes ( 'texture' ) || fieldName . includes ( '재질' ) ) {
if ( ! materialKey ) materialKey = fieldKey ;
}
// 종류 필드 (type_1, type_2, type_3 등 모두 수집) - ID와 함께 저장
if ( ( lowerKey . includes ( 'category' ) || lowerKey . includes ( '종류' ) ||
lowerKey . includes ( 'type_' ) || fieldName === '종류' || fieldName . includes ( '종류' ) ) &&
! lowerKey . includes ( 'item_name' ) && ! lowerKey . includes ( 'item_type' ) &&
! lowerKey . includes ( 'part_type' ) && ! fieldName . includes ( '품목명' ) ) {
categoryKeysWithIds . push ( { key : fieldKey , id : field.id } ) ;
}
// 폭 합계 필드
if ( lowerKey . includes ( 'width_sum' ) || lowerKey . includes ( '폭합계' ) ||
lowerKey . includes ( '폭_합계' ) || lowerKey . includes ( 'width_total' ) ||
fieldName . includes ( '폭 합계' ) || fieldName . includes ( '폭합계' ) ) {
if ( ! widthSumKey ) widthSumKey = fieldKey ;
}
// 모양&길이 필드
if ( lowerKey . includes ( 'shape_length' ) || lowerKey . includes ( '모양' ) ||
fieldName . includes ( '모양' ) || fieldName . includes ( '길이' ) ) {
if ( ! shapeLengthKey ) shapeLengthKey = fieldKey ;
}
} ;
// 모든 필드 검사
structure . sections . forEach ( ( section ) = > {
section . fields . forEach ( ( f ) = > {
const key = f . field . field_key || ` field_ ${ f . field . id } ` ;
checkField ( key , f . field ) ;
} ) ;
} ) ;
structure . directFields . forEach ( ( f ) = > {
const key = f . field . field_key || ` field_ ${ f . field . id } ` ;
checkField ( key , f . field ) ;
} ) ;
// 품목코드 자동생성 (품목명 + 종류 + 모양&길이)
// itemNameKey 또는 직접 탐지한 bendingItemNameKey 사용
const effectiveItemNameKey = bendingItemNameKey || itemNameKey ;
const itemNameValue = effectiveItemNameKey ? ( formData [ effectiveItemNameKey ] as string ) || '' : '' ;
// 2025-12-04: 종류 필드 선택 - 조건부 표시로 현재 보이는 필드만 검사
// shouldShowField를 직접 호출할 수 없으므로, 값이 있는 필드 중 마지막 것을 선택
// (품목명 변경 시 이전 종류는 초기화되므로, 현재 표시되는 종류만 값이 있음)
let activeCategoryKey = '' ;
let categoryValue = '' ;
for ( const { key : catKey , id : catId } of categoryKeysWithIds ) {
const val = ( formData [ catKey ] as string ) || '' ;
if ( val ) {
// 마지막으로 선택된 종류 필드를 사용 (최신 값)
activeCategoryKey = catKey ;
categoryValue = val ;
// break 제거 - 마지막 값이 있는 필드 사용
}
}
const shapeLengthValue = shapeLengthKey ? ( formData [ shapeLengthKey ] as string ) || '' : '' ;
const autoCode = generateBendingItemCodeSimple ( itemNameValue , categoryValue , shapeLengthValue ) ;
2025-12-04 20:52:42 +09:00
// console.log('[DynamicItemForm] 절곡부품 필드 탐지:', { bendingItemNameKey, materialKey, activeCategoryKey, autoCode });
2025-12-04 12:48:41 +09:00
return {
bendingFieldKeys : {
material : materialKey ,
category : activeCategoryKey , // 현재 활성화된 종류 필드
widthSum : widthSumKey ,
shapeLength : shapeLengthKey ,
itemName : effectiveItemNameKey ,
} ,
autoBendingItemCode : autoCode ,
allCategoryKeysWithIds : categoryKeysWithIds , // 모든 종류 필드 키+ID (초기화용)
} ;
} , [ structure , selectedItemType , isBendingPart , formData , itemNameKey ] ) ;
2025-12-16 11:01:25 +09:00
// 2025-12-16: bendingDetails 로드 후 폭 합계를 formData에 동기화
// bendingFieldKeys.widthSum이 결정된 후에 실행되어야 함
const bendingWidthSumSyncedRef = useRef ( false ) ;
useEffect ( ( ) = > {
// edit 모드이고, bendingDetails가 있고, widthSum 필드 키가 결정되었을 때만 실행
if ( mode !== 'edit' || bendingDetails . length === 0 || ! bendingFieldKeys . widthSum ) {
return ;
}
// 이미 동기화했으면 스킵 (중복 실행 방지)
if ( bendingWidthSumSyncedRef . current ) {
return ;
}
const totalSum = bendingDetails . reduce ( ( acc , detail ) = > {
return acc + detail . input + detail . elongation ;
} , 0 ) ;
const sumString = totalSum . toString ( ) ;
console . log ( '[DynamicItemForm] bendingDetails 폭 합계 → formData 동기화:' , {
widthSumKey : bendingFieldKeys.widthSum ,
totalSum ,
bendingDetailsCount : bendingDetails.length ,
} ) ;
setFieldValue ( bendingFieldKeys . widthSum , sumString ) ;
bendingWidthSumSyncedRef . current = true ;
} , [ mode , bendingDetails , bendingFieldKeys . widthSum , setFieldValue ] ) ;
2025-12-04 12:48:41 +09:00
// 2025-12-04: 품목명 변경 시 종류 필드 값 초기화
// 품목명(A)→종류(A1) 선택 후 품목명(B)로 변경 시, 이전 종류(A1) 값이 남아있어서
// 새 종류(B1) 선택해도 이전 값이 품목코드에 적용되는 버그 수정
const prevItemNameValueRef = useRef < string > ( '' ) ;
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
useEffect ( ( ) = > {
2025-12-04 12:48:41 +09:00
if ( ! isBendingPart || ! bendingFieldKeys . itemName ) return ;
const currentItemNameValue = ( formData [ bendingFieldKeys . itemName ] as string ) || '' ;
const prevItemNameValue = prevItemNameValueRef . current ;
// 품목명이 변경되었고, 이전 값이 있었을 때만 종류 필드 초기화
if ( prevItemNameValue && prevItemNameValue !== currentItemNameValue ) {
2025-12-04 20:52:42 +09:00
// console.log('[DynamicItemForm] 품목명 변경 감지:', prevItemNameValue, '→', currentItemNameValue);
2025-12-04 12:48:41 +09:00
// 모든 종류 필드 값 초기화
allCategoryKeysWithIds . forEach ( ( { key } ) = > {
const currentVal = ( formData [ key ] as string ) || '' ;
if ( currentVal ) {
2025-12-04 20:52:42 +09:00
// console.log('[DynamicItemForm] 종류 필드 초기화:', key);
2025-12-04 12:48:41 +09:00
setFieldValue ( key , '' ) ;
}
} ) ;
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
// 현재 값을 이전 값으로 저장
prevItemNameValueRef . current = currentItemNameValue ;
} , [ isBendingPart , bendingFieldKeys . itemName , formData , allCategoryKeysWithIds , setFieldValue ] ) ;
// BOM 필요 체크박스 필드 키 탐지 (structure에서 직접 검색)
const bomRequiredFieldKey = useMemo ( ( ) = > {
if ( ! structure ) return '' ;
// 모든 섹션의 필드에서 BOM 관련 체크박스 필드 찾기
for ( const section of structure . sections ) {
for ( const f of section . fields ) {
const field = f . field ;
const fieldKey = field . field_key || '' ;
const fieldName = field . field_name || '' ;
const fieldType = field . field_type || '' ;
// 체크박스 타입이고 BOM 관련 필드인지 확인
const isCheckbox = fieldType . toLowerCase ( ) === 'checkbox' || fieldType . toLowerCase ( ) === 'boolean' ;
const isBomRelated =
fieldKey . toLowerCase ( ) . includes ( 'bom' ) ||
fieldName . toLowerCase ( ) . includes ( 'bom' ) ||
fieldName . includes ( '부품구성' ) ||
fieldKey . includes ( '부품구성' ) ;
if ( isCheckbox && isBomRelated ) {
2025-12-04 20:52:42 +09:00
// console.log('[DynamicItemForm] BOM 체크박스 필드 발견:', { fieldKey, fieldName });
2025-12-04 12:48:41 +09:00
return field . field_key || ` field_ ${ field . id } ` ;
}
}
}
// 직접 필드에서도 찾기
for ( const f of structure . directFields ) {
const field = f . field ;
const fieldKey = field . field_key || '' ;
const fieldName = field . field_name || '' ;
const fieldType = field . field_type || '' ;
const isCheckbox = fieldType . toLowerCase ( ) === 'checkbox' || fieldType . toLowerCase ( ) === 'boolean' ;
const isBomRelated =
fieldKey . toLowerCase ( ) . includes ( 'bom' ) ||
fieldName . toLowerCase ( ) . includes ( 'bom' ) ||
fieldName . includes ( '부품구성' ) ||
fieldKey . includes ( '부품구성' ) ;
if ( isCheckbox && isBomRelated ) {
2025-12-04 20:52:42 +09:00
// console.log('[DynamicItemForm] BOM 체크박스 필드 발견 (직접필드):', { fieldKey, fieldName });
2025-12-04 12:48:41 +09:00
return field . field_key || ` field_ ${ field . id } ` ;
}
}
2025-12-04 20:52:42 +09:00
// console.log('[DynamicItemForm] BOM 체크박스 필드를 찾지 못함');
2025-12-04 12:48:41 +09:00
return '' ;
} , [ structure ] ) ;
// 조립 부품 필드 탐지 (측면규격 가로/세로, 길이) - 자동생성용
// 2025-12-03: 필드 탐지 조건 개선 - 더 정확한 매칭
const { hasAssemblyFields , assemblyFieldKeys , autoAssemblyItemName , autoAssemblySpec } = useMemo ( ( ) = > {
if ( ! structure || selectedItemType !== 'PT' ) {
return {
hasAssemblyFields : false ,
assemblyFieldKeys : { sideSpecWidth : '' , sideSpecHeight : '' , assemblyLength : '' } ,
autoAssemblyItemName : '' ,
autoAssemblySpec : '' ,
} ;
}
let sideSpecWidthKey = '' ;
let sideSpecHeightKey = '' ;
let assemblyLengthKey = '' ;
const checkField = ( fieldKey : string , field : ItemFieldResponse ) = > {
const fieldName = field . field_name || '' ;
const lowerKey = fieldKey . toLowerCase ( ) ;
// 측면규격 가로 - 더 정확한 조건 (측면 + 가로 조합)
const isWidthField = lowerKey . includes ( 'side_spec_width' ) || lowerKey . includes ( 'sidespecwidth' ) ||
fieldName . includes ( '측면규격(가로)' ) || fieldName . includes ( '측면 규격(가로)' ) ||
fieldName . includes ( '측면규격 가로' ) || fieldName . includes ( '측면 가로' ) ||
( fieldName . includes ( '측면' ) && fieldName . includes ( '가로' ) ) ;
if ( isWidthField && ! sideSpecWidthKey ) {
sideSpecWidthKey = fieldKey ;
}
// 측면규격 세로 - 더 정확한 조건 (측면 + 세로 조합)
const isHeightField = lowerKey . includes ( 'side_spec_height' ) || lowerKey . includes ( 'sidespecheight' ) ||
fieldName . includes ( '측면규격(세로)' ) || fieldName . includes ( '측면 규격(세로)' ) ||
fieldName . includes ( '측면규격 세로' ) || fieldName . includes ( '측면 세로' ) ||
( fieldName . includes ( '측면' ) && fieldName . includes ( '세로' ) ) ;
if ( isHeightField && ! sideSpecHeightKey ) {
sideSpecHeightKey = fieldKey ;
}
// 길이 - 조립 부품 길이 필드
const isLengthField = lowerKey . includes ( 'assembly_length' ) || lowerKey . includes ( 'assemblylength' ) ||
lowerKey === 'length' || lowerKey . endsWith ( '_length' ) ||
fieldName === '길이' || fieldName . includes ( '조립' ) && fieldName . includes ( '길이' ) ;
if ( isLengthField && ! assemblyLengthKey ) {
assemblyLengthKey = fieldKey ;
}
} ;
// 모든 필드 검사
structure . sections . forEach ( ( section ) = > {
section . fields . forEach ( ( f ) = > {
const key = f . field . field_key || ` field_ ${ f . field . id } ` ;
checkField ( key , f . field ) ;
} ) ;
} ) ;
structure . directFields . forEach ( ( f ) = > {
const key = f . field . field_key || ` field_ ${ f . field . id } ` ;
checkField ( key , f . field ) ;
} ) ;
// 조립 부품 여부: 측면규격 가로/세로, 길이 필드가 모두 있어야 함
const isAssembly = ! ! ( sideSpecWidthKey && sideSpecHeightKey && assemblyLengthKey ) ;
// 자동생성 값 계산
const selectedItemName = itemNameKey ? ( formData [ itemNameKey ] as string ) || '' : '' ;
const sideSpecWidth = sideSpecWidthKey ? ( formData [ sideSpecWidthKey ] as string ) || '' : '' ;
const sideSpecHeight = sideSpecHeightKey ? ( formData [ sideSpecHeightKey ] as string ) || '' : '' ;
const assemblyLength = assemblyLengthKey ? ( formData [ assemblyLengthKey ] as string ) || '' : '' ;
// 품목명: 선택한 품목명 가로x세로
const autoItemName = generateAssemblyItemNameSimple ( selectedItemName , sideSpecWidth , sideSpecHeight ) ;
// 규격: 가로x세로x길이(네자리)
const autoSpec = generateAssemblySpecification ( sideSpecWidth , sideSpecHeight , assemblyLength ) ;
2025-12-04 20:52:42 +09:00
// console.log('[DynamicItemForm] 조립 부품 필드 탐지:', { isAssembly, autoItemName, autoSpec });
2025-12-04 12:48:41 +09:00
return {
hasAssemblyFields : isAssembly ,
assemblyFieldKeys : {
sideSpecWidth : sideSpecWidthKey ,
sideSpecHeight : sideSpecHeightKey ,
assemblyLength : assemblyLengthKey ,
} ,
autoAssemblyItemName : autoItemName ,
autoAssemblySpec : autoSpec ,
} ;
} , [ structure , selectedItemType , formData , itemNameKey ] ) ;
2025-12-04 20:52:42 +09:00
// 구매 부품(전동개폐기) 필드 탐지 - 품목명, 용량, 전원
// 2025-12-04: 구매 부품 품목코드 자동생성 추가
const { purchasedFieldKeys , autoPurchasedItemCode } = useMemo ( ( ) = > {
if ( ! structure || selectedItemType !== 'PT' || ! isPurchasedPart ) {
return {
purchasedFieldKeys : {
itemName : '' , // 품목명 (전동개폐기 등)
capacity : '' , // 용량 (150, 300, etc.)
power : '' , // 전원 (220V, 380V)
} ,
autoPurchasedItemCode : '' ,
} ;
}
let purchasedItemNameKey = '' ;
let capacityKey = '' ;
let powerKey = '' ;
const checkField = ( fieldKey : string , field : ItemFieldResponse ) = > {
const fieldName = field . field_name || '' ;
const lowerKey = fieldKey . toLowerCase ( ) ;
// 구매 부품 품목명 필드 탐지 - PurchasedItemName 우선 탐지
const isPurchasedItemNameField = lowerKey . includes ( 'purchaseditemname' ) ;
const isItemNameField =
isPurchasedItemNameField ||
lowerKey . includes ( 'item_name' ) ||
lowerKey . includes ( '품목명' ) ||
fieldName . includes ( '품목명' ) ||
fieldName === '품목명' ;
// PurchasedItemName을 우선적으로 사용 (더 정확한 매칭)
if ( isPurchasedItemNameField ) {
purchasedItemNameKey = fieldKey ; // 덮어쓰기 (우선순위 높음)
} else if ( isItemNameField && ! purchasedItemNameKey ) {
purchasedItemNameKey = fieldKey ;
}
// 용량 필드 탐지
const isCapacityField =
lowerKey . includes ( 'capacity' ) ||
lowerKey . includes ( '용량' ) ||
fieldName . includes ( '용량' ) ||
fieldName === '용량' ;
if ( isCapacityField && ! capacityKey ) {
capacityKey = fieldKey ;
}
// 전원 필드 탐지
const isPowerField =
lowerKey . includes ( 'power' ) ||
lowerKey . includes ( '전원' ) ||
fieldName . includes ( '전원' ) ||
fieldName === '전원' ;
if ( isPowerField && ! powerKey ) {
powerKey = fieldKey ;
}
} ;
// 모든 필드 검사
structure . sections . forEach ( ( section ) = > {
section . fields . forEach ( ( f ) = > {
const key = f . field . field_key || ` field_ ${ f . field . id } ` ;
checkField ( key , f . field ) ;
} ) ;
} ) ;
structure . directFields . forEach ( ( f ) = > {
const key = f . field . field_key || ` field_ ${ f . field . id } ` ;
checkField ( key , f . field ) ;
} ) ;
// 품목코드 자동생성: 품목명 + 용량 + 전원
const itemNameValue = purchasedItemNameKey ? ( formData [ purchasedItemNameKey ] as string ) || '' : '' ;
const capacityValue = capacityKey ? ( formData [ capacityKey ] as string ) || '' : '' ;
const powerValue = powerKey ? ( formData [ powerKey ] as string ) || '' : '' ;
const autoCode = generatePurchasedItemCode ( itemNameValue , capacityValue , powerValue ) ;
// console.log('[DynamicItemForm] 구매 부품 필드 탐지:', { purchasedItemNameKey, autoCode });
return {
purchasedFieldKeys : {
itemName : purchasedItemNameKey ,
capacity : capacityKey ,
power : powerKey ,
} ,
autoPurchasedItemCode : autoCode ,
} ;
} , [ structure , selectedItemType , isPurchasedPart , formData ] ) ;
2025-12-04 12:48:41 +09:00
// 품목코드 자동생성 값
// PT(부품): 영문약어-순번 (예: GR-001, MOTOR-002)
// 기타 품목: 품목명-규격 (기존 방식)
// 2025-12-03: 연동 드롭다운 로직 제거 - 단순화
// 2025-12-03: activeSpecificationKey 사용하여 조건부 표시 고려
const autoGeneratedItemCode = useMemo ( ( ) = > {
if ( ! hasAutoItemCode ) return '' ;
// field_key가 "{id}_item_name" 형식일 수 있어서 동적으로 키 사용
const itemName = ( formData [ itemNameKey ] as string ) || '' ;
// 현재 표시 중인 규격 필드 값 사용 (조건부 표시 고려)
const specification = activeSpecificationKey ? ( formData [ activeSpecificationKey ] as string ) || '' : '' ;
if ( ! itemName ) return '' ;
// PT(부품)인 경우: 영문약어-순번 형식 사용
if ( selectedItemType === 'PT' ) {
// generateItemCode는 품목명을 기반으로 영문약어를 찾고 순번을 계산
const generatedCode = generateItemCode ( itemName , existingItemCodes ) ;
return generatedCode ;
}
// 기타 품목: 기존 방식 (품목명-규격)
if ( ! specification ) return itemName ;
return ` ${ itemName } - ${ specification } ` ;
} , [ hasAutoItemCode , itemNameKey , activeSpecificationKey , formData , selectedItemType , existingItemCodes ] ) ;
// 품목 유형 변경 핸들러
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
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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" >
* 절 곡 부 품 품 목 코 드 는 & apos ; 품 목 명 + 종 류 + 길 이 축 약 & apos ; 형 식 으 로 자 동 생 성 됩 니 다 ( 예 : 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" >
* 품 목 코 드 는 & apos ; 품 목 명 + 용 량 + 전 원 & apos ; 형 식 으 로 자 동 생 성 됩 니 다 ( 예 : 전동개폐기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' && (
< div className = "mt-4 space-y-4" >
2025-12-06 11:36:38 +09:00
{ /* 시방서 파일 */ }
2025-12-04 20:52:42 +09:00
< div >
< Label htmlFor = "specification_file" > 시 방 서 ( PDF ) < / Label >
2025-12-12 18:35:43 +09:00
< div className = "mt-1.5" >
{ /* 기존 파일이 있고, 새 파일 선택 안한 경우: 파일명 + 버튼들 */ }
{ mode === 'edit' && existingSpecificationFile && ! specificationFile ? (
< div className = "flex items-center gap-2" >
< div className = "flex items-center gap-2 flex-1 px-3 py-2 bg-gray-50 rounded-md border text-sm" >
< FileText className = "h-4 w-4 text-blue-600 shrink-0" / >
< span className = "truncate" > { existingSpecificationFileName } < / span >
< / div >
2025-12-16 11:01:25 +09:00
< button
type = "button"
onClick = { ( ) = > handleFileDownload ( existingSpecificationFileId , existingSpecificationFileName ) }
2025-12-12 18:35:43 +09:00
className = "inline-flex items-center justify-center h-9 w-9 rounded-md border border-input bg-background hover:bg-accent hover:text-accent-foreground text-blue-600"
title = "다운로드"
2025-12-06 11:36:38 +09:00
>
2025-12-12 18:35:43 +09:00
< Download className = "h-4 w-4" / >
2025-12-16 11:01:25 +09:00
< / button >
2025-12-12 18:35:43 +09:00
< label
htmlFor = "specification_file_edit"
className = "inline-flex items-center justify-center h-9 w-9 rounded-md border border-input bg-background hover:bg-accent hover:text-accent-foreground text-gray-600 cursor-pointer"
title = "수정"
>
< Pencil className = "h-4 w-4" / >
< input
id = "specification_file_edit"
type = "file"
accept = ".pdf"
onChange = { ( e ) = > {
const file = e . target . files ? . [ 0 ] || null ;
setSpecificationFile ( file ) ;
} }
disabled = { isSubmitting }
className = "hidden"
/ >
< / label >
2025-12-06 11:36:38 +09:00
< Button
type = "button"
2025-12-12 18:35:43 +09:00
variant = "outline"
size = "icon"
2025-12-06 11:36:38 +09:00
onClick = { ( ) = > handleDeleteFile ( 'specification' ) }
disabled = { isDeletingFile === 'specification' || isSubmitting }
2025-12-12 18:35:43 +09:00
className = "h-9 w-9 text-red-500 hover:text-red-700 hover:bg-red-50"
title = "삭제"
2025-12-06 11:36:38 +09:00
>
2025-12-12 18:35:43 +09:00
< Trash2 className = "h-4 w-4" / >
2025-12-06 11:36:38 +09:00
< / Button >
< / div >
2025-12-16 11:01:25 +09:00
) : specificationFile ? (
/* 새 파일 선택된 경우: 커스텀 UI로 파일명 표시 */
< div className = "flex items-center gap-2" >
< div className = "flex items-center gap-2 flex-1 px-3 py-2 bg-blue-50 rounded-md border border-blue-200 text-sm" >
< FileText className = "h-4 w-4 text-blue-600 shrink-0" / >
< span className = "truncate" > { specificationFile . name } < / span >
< span className = "text-xs text-blue-500" > ( 새 파 일 ) < / span >
< / div >
< Button
type = "button"
variant = "outline"
size = "icon"
onClick = { ( ) = > setSpecificationFile ( null ) }
disabled = { isSubmitting }
className = "h-9 w-9 text-red-500 hover:text-red-700 hover:bg-red-50"
title = "취소"
>
< Trash2 className = "h-4 w-4" / >
< / Button >
< / div >
2025-12-12 18:35:43 +09:00
) : (
2025-12-16 11:01:25 +09:00
/* 파일 없는 경우: 파일 선택 버튼 */
< div className = "flex items-center gap-2" >
< label
htmlFor = "specification_file"
className = "flex items-center gap-2 flex-1 px-3 py-2 bg-gray-50 rounded-md border text-sm cursor-pointer hover:bg-gray-100 transition-colors"
>
< Upload className = "h-4 w-4 text-gray-500 shrink-0" / >
< span className = "text-gray-500" > PDF 파 일 을 선 택 하 세 요 < / span >
< / label >
< input
2025-12-12 18:35:43 +09:00
id = "specification_file"
type = "file"
accept = ".pdf"
onChange = { ( e ) = > {
const file = e . target . files ? . [ 0 ] || null ;
setSpecificationFile ( file ) ;
} }
disabled = { isSubmitting }
2025-12-16 11:01:25 +09:00
className = "hidden"
2025-12-12 18:35:43 +09:00
/ >
< / div >
2025-12-04 20:52:42 +09:00
) }
< / div >
< / div >
2025-12-06 11:36:38 +09:00
{ /* 인정서 파일 */ }
2025-12-04 20:52:42 +09:00
< div >
< Label htmlFor = "certification_file" > 인 정 서 ( PDF ) < / Label >
2025-12-12 18:35:43 +09:00
< div className = "mt-1.5" >
{ /* 기존 파일이 있고, 새 파일 선택 안한 경우: 파일명 + 버튼들 */ }
{ mode === 'edit' && existingCertificationFile && ! certificationFile ? (
< div className = "flex items-center gap-2" >
< div className = "flex items-center gap-2 flex-1 px-3 py-2 bg-gray-50 rounded-md border text-sm" >
< FileText className = "h-4 w-4 text-green-600 shrink-0" / >
< span className = "truncate" > { existingCertificationFileName } < / span >
< / div >
2025-12-16 11:01:25 +09:00
< button
type = "button"
onClick = { ( ) = > handleFileDownload ( existingCertificationFileId , existingCertificationFileName ) }
2025-12-12 18:35:43 +09:00
className = "inline-flex items-center justify-center h-9 w-9 rounded-md border border-input bg-background hover:bg-accent hover:text-accent-foreground text-green-600"
title = "다운로드"
2025-12-06 11:36:38 +09:00
>
2025-12-12 18:35:43 +09:00
< Download className = "h-4 w-4" / >
2025-12-16 11:01:25 +09:00
< / button >
2025-12-12 18:35:43 +09:00
< label
htmlFor = "certification_file_edit"
className = "inline-flex items-center justify-center h-9 w-9 rounded-md border border-input bg-background hover:bg-accent hover:text-accent-foreground text-gray-600 cursor-pointer"
title = "수정"
>
< Pencil className = "h-4 w-4" / >
< input
id = "certification_file_edit"
type = "file"
accept = ".pdf"
onChange = { ( e ) = > {
const file = e . target . files ? . [ 0 ] || null ;
setCertificationFile ( file ) ;
} }
disabled = { isSubmitting }
className = "hidden"
/ >
< / label >
2025-12-06 11:36:38 +09:00
< Button
type = "button"
2025-12-12 18:35:43 +09:00
variant = "outline"
size = "icon"
2025-12-06 11:36:38 +09:00
onClick = { ( ) = > handleDeleteFile ( 'certification' ) }
disabled = { isDeletingFile === 'certification' || isSubmitting }
2025-12-12 18:35:43 +09:00
className = "h-9 w-9 text-red-500 hover:text-red-700 hover:bg-red-50"
title = "삭제"
2025-12-06 11:36:38 +09:00
>
2025-12-12 18:35:43 +09:00
< Trash2 className = "h-4 w-4" / >
2025-12-06 11:36:38 +09:00
< / Button >
< / div >
2025-12-16 11:01:25 +09:00
) : certificationFile ? (
/* 새 파일 선택된 경우: 커스텀 UI로 파일명 표시 */
< div className = "flex items-center gap-2" >
< div className = "flex items-center gap-2 flex-1 px-3 py-2 bg-green-50 rounded-md border border-green-200 text-sm" >
< FileText className = "h-4 w-4 text-green-600 shrink-0" / >
< span className = "truncate" > { certificationFile . name } < / span >
< span className = "text-xs text-green-500" > ( 새 파 일 ) < / span >
< / div >
< Button
type = "button"
variant = "outline"
size = "icon"
onClick = { ( ) = > setCertificationFile ( null ) }
disabled = { isSubmitting }
className = "h-9 w-9 text-red-500 hover:text-red-700 hover:bg-red-50"
title = "취소"
>
< Trash2 className = "h-4 w-4" / >
< / Button >
< / div >
2025-12-12 18:35:43 +09:00
) : (
2025-12-16 11:01:25 +09:00
/* 파일 없는 경우: 파일 선택 버튼 */
< div className = "flex items-center gap-2" >
< label
htmlFor = "certification_file"
className = "flex items-center gap-2 flex-1 px-3 py-2 bg-gray-50 rounded-md border text-sm cursor-pointer hover:bg-gray-100 transition-colors"
>
< Upload className = "h-4 w-4 text-gray-500 shrink-0" / >
< span className = "text-gray-500" > PDF 파 일 을 선 택 하 세 요 < / span >
< / label >
< input
2025-12-12 18:35:43 +09:00
id = "certification_file"
type = "file"
accept = ".pdf"
onChange = { ( e ) = > {
const file = e . target . files ? . [ 0 ] || null ;
setCertificationFile ( file ) ;
} }
disabled = { isSubmitting }
2025-12-16 11:01:25 +09:00
className = "hidden"
2025-12-12 18:35:43 +09:00
/ >
< / div >
2025-12-04 20:52:42 +09:00
) }
< / div >
< / div >
< / div >
) }
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 {
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 ) ;
}
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
{ /* 품목코드 중복 확인 다이얼로그 */ }
< AlertDialog open = { showDuplicateDialog } onOpenChange = { setShowDuplicateDialog } >
< AlertDialogContent >
< AlertDialogHeader >
< AlertDialogTitle > 품 목 코 드 중 복 < / AlertDialogTitle >
< AlertDialogDescription >
입 력 하 신 조 건 의 품 목 코 드 가 이 미 존 재 합 니 다 .
< span className = "block mt-2 font-medium text-foreground" >
기 존 품 목 을 수 정 하 시 겠 습 니 까 ?
< / span >
< / AlertDialogDescription >
< / AlertDialogHeader >
< AlertDialogFooter >
< AlertDialogCancel onClick = { handleCancelDuplicate } >
취 소
< / AlertDialogCancel >
< AlertDialogAction onClick = { handleGoToEditDuplicate } >
중 복 품 목 수 정 하 러 가 기
< / AlertDialogAction >
< / AlertDialogFooter >
< / AlertDialogContent >
< / AlertDialog >
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 >
) ;
}