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>
This commit is contained in:
byeongcheolryu
2025-11-28 20:14:43 +09:00
parent 8fd9cf2d40
commit 6ed5d4ffb3
28 changed files with 5359 additions and 2 deletions

View File

@@ -0,0 +1,208 @@
/**
* DynamicItemForm Component
*
* 품목기준관리 설정에 따라 동적으로 폼을 렌더링하는 메인 컴포넌트
*
* 특징:
* - API에서 폼 구조 로드 (품목 유형별)
* - 조건부 섹션/필드 표시
* - 동적 유효성 검증
* - 기존 특수 컴포넌트 (BOM, 전개도) 통합 가능
*
* @example
* // 신규 등록
* <DynamicItemForm
* itemType="FG"
* onSubmit={handleCreate}
* onCancel={handleCancel}
* />
*
* @example
* // 수정
* <DynamicItemForm
* itemType="PT"
* partType="BENDING"
* initialValues={existingItem}
* onSubmit={handleUpdate}
* onCancel={handleCancel}
* />
*/
'use client';
import { useEffect } from 'react';
import { Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { DynamicFormRenderer } from './DynamicFormRenderer';
import { useFormStructure, useDynamicFormState } from './hooks';
import type { ItemType, PartType } from '@/types/item';
import type { FormData } from './types';
interface DynamicItemFormProps {
/** 품목 유형 */
itemType: ItemType;
/** 부품 유형 (PT인 경우) */
partType?: PartType;
/** 초기 값 (수정 모드) */
initialValues?: FormData;
/** 제출 핸들러 */
onSubmit: (data: FormData) => Promise<void>;
/** 취소 핸들러 */
onCancel?: () => void;
/** 폼 비활성화 */
disabled?: boolean;
/** Mock 데이터 사용 (API 미구현 시) */
useMock?: boolean;
}
export function DynamicItemForm({
itemType,
partType,
initialValues,
onSubmit,
onCancel,
disabled = false,
useMock = true, // 기본적으로 Mock 사용 (API 구현 후 false로 변경)
}: DynamicItemFormProps) {
// 폼 구조 로드
const {
formStructure,
isLoading: isLoadingStructure,
error: structureError,
refetch: refetchStructure,
} = useFormStructure({
itemType,
partType,
useMock,
});
// 폼 상태 관리
const {
state,
setValue,
setTouched,
validate,
reset,
handleSubmit,
} = useDynamicFormState({
sections: formStructure?.sections || [],
initialValues,
});
// 폼 구조가 변경되면 값 초기화
useEffect(() => {
if (formStructure) {
reset(initialValues);
}
}, [formStructure, initialValues, reset]);
// 로딩 상태
if (isLoadingStructure) {
return (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<span className="ml-3 text-muted-foreground"> ...</span>
</div>
);
}
// 에러 상태
if (structureError) {
return (
<Alert variant="destructive">
<AlertDescription className="flex items-center justify-between">
<span> : {structureError.message}</span>
<Button variant="outline" size="sm" onClick={refetchStructure}>
</Button>
</AlertDescription>
</Alert>
);
}
// 폼 구조가 없는 경우
if (!formStructure) {
return (
<Alert>
<AlertDescription>
({itemType}) .
.
</AlertDescription>
</Alert>
);
}
// 폼 제출
const onFormSubmit = handleSubmit(onSubmit);
return (
<form onSubmit={onFormSubmit} className="space-y-6">
{/* 페이지 정보 (디버그용, 필요시 제거) */}
{process.env.NODE_ENV === 'development' && (
<div className="text-xs text-muted-foreground bg-gray-50 p-2 rounded">
📄 {formStructure.page.page_name} | : {formStructure.page.item_type}
{formStructure.page.part_type && ` | 부품유형: ${formStructure.page.part_type}`}
</div>
)}
{/* 동적 폼 렌더러 */}
<DynamicFormRenderer
sections={formStructure.sections}
conditionalSections={formStructure.conditionalSections}
conditionalFields={formStructure.conditionalFields}
values={state.values}
errors={state.errors}
onChange={(fieldKey, value) => {
setValue(fieldKey, value);
}}
onBlur={(fieldKey) => {
setTouched(fieldKey);
}}
disabled={disabled || state.isSubmitting}
/>
{/* 버튼 영역 */}
<div className="flex items-center justify-end gap-3 pt-4 border-t">
{onCancel && (
<Button
type="button"
variant="outline"
onClick={onCancel}
disabled={state.isSubmitting}
>
</Button>
)}
<Button
type="submit"
disabled={disabled || state.isSubmitting}
>
{state.isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
'저장'
)}
</Button>
</div>
</form>
);
}
// 하위 컴포넌트 및 훅 re-export
export { DynamicFormRenderer } from './DynamicFormRenderer';
export { DynamicSection } from './DynamicSection';
export { DynamicField } from './DynamicField';
export {
useFormStructure,
useConditionalFields,
useDynamicFormState,
clearFormStructureCache,
invalidateFormStructureCache,
} from './hooks';
export * from './types';
export default DynamicItemForm;