Files
sam-react-prod/src/components/items/ItemForm/index.tsx
유병철 a2c3e4c41e refactor(WEB): 프론트엔드 대규모 코드 정리 및 리팩토링
- 미사용 코드 삭제: ThemeContext, itemStore, utils/date.ts, utils/formatAmount.ts
- 유틸리티 이동: date, formatAmount → src/lib/utils/ (중앙 집중화)
- 다수 page.tsx 클라이언트 컴포넌트 패턴 통일
- DateRangeSelector 리팩토링 및 date-range-picker UI 컴포넌트 추가
- ThemeSelect/themeStore Zustand 직접 연동으로 전환
- 건설/회계/영업/품목/출하 등 전반적 컴포넌트 개선
- UniversalListPage, IntegratedListTemplateV2 타입 확장
- 프론트엔드 종합 리뷰 문서 및 개선 체크리스트 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 16:30:07 +09:00

419 lines
15 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 품목 등록/수정 폼 컴포넌트
*
* react-hook-form + Zod 검증
*/
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import type { ItemType } from '@/types/item';
import { createItemFormSchema, type CreateItemFormData } from '@/lib/utils/validation';
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Alert, AlertDescription } from '@/components/ui/alert';
import ItemTypeSelect from '../ItemTypeSelect';
import { DrawingCanvas } from '../DrawingCanvas';
// Local imports
import { PART_TYPE_CATEGORIES, PART_ITEM_NAMES } from './constants';
import type { ItemFormProps } from './types';
import ValidationAlert from './ValidationAlert';
import FormHeader from './FormHeader';
import BendingDiagramSection from './BendingDiagramSection';
import BOMSection from './BOMSection';
import { MaterialForm, ProductForm, ProductCertificationSection, PartForm } from './forms';
import { useItemFormState } from './hooks';
import { toast } from 'sonner';
export default function ItemForm({ mode, initialData, onSubmit }: ItemFormProps) {
const router = useRouter();
// 커스텀 훅으로 상태 관리 통합
const {
// 기본 상태
isSubmitting, setIsSubmitting,
selectedItemType, setSelectedItemType,
// BOM 상태
bomLines, setBomLines,
bomSearchStates, setBomSearchStates,
// 파일 상태
specificationFile, setSpecificationFile,
certificationFile, setCertificationFile,
setBendingDiagramFile,
bendingDiagram, setBendingDiagram,
bendingDiagramInputMethod, setBendingDiagramInputMethod,
isDrawingOpen, setIsDrawingOpen,
// FG(제품) 상태
productName, setProductName,
productStatus, setProductStatus,
// PT(부품) 상태
selectedPartType, setSelectedPartType,
partStatus, setPartStatus,
// SM/RM/CS 상태
itemName, setItemName,
selectedCategory1, setSelectedCategory1,
selectedInstallationType, setSelectedInstallationType,
materialStatus, setMaterialStatus,
selectedSpecification, setSelectedSpecification,
selectedUnit, setSelectedUnit,
// ASSEMBLY 부품 상태
sideSpecWidth, setSideSpecWidth,
sideSpecHeight, setSideSpecHeight,
assemblyLength, setAssemblyLength,
assemblyUnit, setAssemblyUnit,
// 전동개폐기 상태
electricOpenerPower, setElectricOpenerPower,
electricOpenerCapacity, setElectricOpenerCapacity,
// 모터/체인 상태
motorVoltage, setMotorVoltage,
chainSpec, setChainSpec,
// BENDING 부품 상태
selectedBendingItemType, setSelectedBendingItemType,
material, setMaterial,
bendingLength, setBendingLength,
widthSum, setWidthSum,
partUnit, setPartUnit,
bendingDetails, setBendingDetails,
// BOM 필요 여부
needsBOM, setNeedsBOM,
// 비고
remarks, setRemarks,
// 헬퍼 함수
resetAllStates,
} = useItemFormState({ mode, initialData });
const {
register,
handleSubmit,
formState: { errors },
setValue,
getValues,
clearErrors,
} = useForm<CreateItemFormData>({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
resolver: zodResolver(createItemFormSchema) as any,
defaultValues: initialData || {
itemType: 'FG',
unit: 'EA',
isActive: true,
currentRevision: 0,
isFinal: false,
},
});
// 전개도 상세 입력 변경 시 폭 합계 자동 업데이트
useEffect(() => {
if (bendingDetails.length > 0) {
const totalSum = bendingDetails.reduce((acc, d) => {
const calc = d.input + d.elongation;
return acc + calc;
}, 0);
setWidthSum(totalSum.toFixed(1));
setValue('length', totalSum.toFixed(1));
}
}, [bendingDetails, setValue, setWidthSum]);
// 품목코드 자동 생성 함수
const generateItemCode = () => {
if (selectedPartType === "BENDING" && selectedCategory1 && selectedBendingItemType) {
const categoryData = PART_TYPE_CATEGORIES.BENDING.categories.find(
cat => cat.value === selectedCategory1
);
const categoryCode = categoryData?.code || '';
const itemTypeData = PART_ITEM_NAMES[selectedCategory1]?.find(
item => item.label === selectedBendingItemType
);
const itemTypeCode = itemTypeData?.code || '';
let lengthCode = "";
if (bendingLength) {
if (bendingLength.startsWith("W")) {
if (bendingLength === "W50x3000") lengthCode = "53";
else if (bendingLength === "W50x4000") lengthCode = "54";
else if (bendingLength === "W80x3000") lengthCode = "83";
else if (bendingLength === "W80x4000") lengthCode = "84";
else {
const match = bendingLength.match(/W(\d+)x(\d+)/);
if (match) {
const width = parseInt(match[1]);
const length = parseInt(match[2]);
lengthCode = `${Math.floor(width / 10)}${Math.floor(length / 1000)}`;
}
}
} else {
const lengthNum = parseInt(bendingLength);
const lengthMap: Record<number, string> = {
1219: "12", 2438: "24", 3000: "30", 3500: "35",
4000: "40", 4150: "41", 4200: "42", 4300: "43"
};
lengthCode = lengthMap[lengthNum] || Math.floor(lengthNum / 100).toString().padStart(2, '0');
}
}
return `${categoryCode}${itemTypeCode}${lengthCode}`;
}
return "";
};
const handleFormSubmit = async (data: CreateItemFormData) => {
setIsSubmitting(true);
try {
const finalData = {
...data,
bom: bomLines.length > 0 ? bomLines : undefined,
bendingDetails: bendingDetails.length > 0 ? bendingDetails : undefined,
specificationFileName: specificationFile?.name,
certificationFileName: certificationFile?.name,
};
await onSubmit(finalData);
router.push('/production/screen-production');
router.refresh();
} catch {
toast.error('품목 저장에 실패했습니다.');
} finally {
setIsSubmitting(false);
}
};
const handleItemTypeChange = (type: ItemType) => {
setSelectedItemType(type);
setValue('itemType', type);
resetAllStates(setValue, clearErrors, type);
};
return (
<form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-6">
{/* Validation 에러 Alert */}
<ValidationAlert errors={errors} />
{/* 헤더 */}
<FormHeader
mode={mode}
selectedItemType={selectedItemType}
isSubmitting={isSubmitting}
onCancel={() => router.back()}
/>
{/* 기본 정보 */}
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* 1단계: 품목 유형 먼저 선택 */}
<div className="md:col-span-2">
<ItemTypeSelect
value={selectedItemType}
onChange={handleItemTypeChange}
disabled={mode === 'edit'}
required
/>
{errors.itemType && (
<p className="text-sm text-red-500 mt-1">
{errors.itemType.message}
</p>
)}
<p className="text-xs text-muted-foreground mt-1">
*
</p>
</div>
{/* 2단계: 품목 유형이 선택된 경우에만 표시 */}
{selectedItemType && (
<>
{/* 제품(FG)인 경우 */}
{selectedItemType === 'FG' && (
<ProductForm
productName={productName}
setProductName={setProductName}
productStatus={productStatus}
setProductStatus={setProductStatus}
remarks={remarks}
setRemarks={setRemarks}
needsBOM={needsBOM}
setNeedsBOM={setNeedsBOM}
specificationFile={specificationFile}
setSpecificationFile={setSpecificationFile}
certificationFile={certificationFile}
setCertificationFile={setCertificationFile}
isSubmitting={isSubmitting}
register={register}
setValue={setValue}
getValues={getValues}
errors={errors}
/>
)}
{/* 부품(PT)인 경우 */}
{selectedItemType === 'PT' && (
<PartForm
selectedPartType={selectedPartType}
setSelectedPartType={setSelectedPartType}
selectedCategory1={selectedCategory1}
setSelectedCategory1={setSelectedCategory1}
selectedInstallationType={selectedInstallationType}
setSelectedInstallationType={setSelectedInstallationType}
sideSpecWidth={sideSpecWidth}
setSideSpecWidth={setSideSpecWidth}
sideSpecHeight={sideSpecHeight}
setSideSpecHeight={setSideSpecHeight}
assemblyLength={assemblyLength}
setAssemblyLength={setAssemblyLength}
assemblyUnit={assemblyUnit}
setAssemblyUnit={setAssemblyUnit}
selectedBendingItemType={selectedBendingItemType}
setSelectedBendingItemType={setSelectedBendingItemType}
material={material}
setMaterial={setMaterial}
widthSum={widthSum}
setWidthSum={setWidthSum}
bendingLength={bendingLength}
setBendingLength={setBendingLength}
partUnit={partUnit}
setPartUnit={setPartUnit}
bendingDetailsLength={bendingDetails.length}
electricOpenerPower={electricOpenerPower}
setElectricOpenerPower={setElectricOpenerPower}
electricOpenerCapacity={electricOpenerCapacity}
setElectricOpenerCapacity={setElectricOpenerCapacity}
motorVoltage={motorVoltage}
setMotorVoltage={setMotorVoltage}
chainSpec={chainSpec}
setChainSpec={setChainSpec}
partStatus={partStatus}
setPartStatus={setPartStatus}
needsBOM={needsBOM}
setNeedsBOM={setNeedsBOM}
generateItemCode={generateItemCode}
register={register}
setValue={setValue}
clearErrors={clearErrors}
errors={errors}
/>
)}
{/* SM/RM/CS 공통 섹션 */}
{(selectedItemType === 'RM' || selectedItemType === 'SM' || selectedItemType === 'CS') && (
<MaterialForm
selectedItemType={selectedItemType}
itemName={itemName}
setItemName={setItemName}
selectedSpecification={selectedSpecification}
setSelectedSpecification={setSelectedSpecification}
materialStatus={materialStatus}
setMaterialStatus={setMaterialStatus}
selectedUnit={selectedUnit}
setSelectedUnit={setSelectedUnit}
register={register}
setValue={setValue}
getValues={getValues}
errors={errors}
/>
)}
</>
)}
</div>
{/* 비고 필드 (PT를 제외한 모든 품목 유형) */}
{selectedItemType && selectedItemType !== 'PT' && (
<div className="mt-4">
<Label htmlFor="note"></Label>
<Input
id="note"
placeholder="비고사항을 입력하세요"
{...register('note')}
/>
</div>
)}
{/* 인정 정보 (FG only) */}
{selectedItemType === 'FG' && (
<ProductCertificationSection
remarks={remarks}
setRemarks={setRemarks}
needsBOM={needsBOM}
setNeedsBOM={setNeedsBOM}
specificationFile={specificationFile}
setSpecificationFile={setSpecificationFile}
certificationFile={certificationFile}
setCertificationFile={setCertificationFile}
isSubmitting={isSubmitting}
register={register}
setValue={setValue}
getValues={getValues}
/>
)}
</CardContent>
</Card>
{/* 품목 유형 선택 안내 경고 */}
{!selectedItemType && (
<Alert className="bg-orange-50 border-orange-200">
<AlertDescription className="text-orange-900">
</AlertDescription>
</Alert>
)}
{/* 절곡품 전개도 - PT ASSEMBLY(category1 선택 시) 또는 PT BENDING */}
{selectedItemType === 'PT' && (
(selectedPartType === 'ASSEMBLY' && selectedCategory1) ||
selectedPartType === 'BENDING'
) && (
<BendingDiagramSection
selectedPartType={selectedPartType}
bendingDiagramInputMethod={bendingDiagramInputMethod}
setBendingDiagramInputMethod={setBendingDiagramInputMethod}
bendingDiagram={bendingDiagram}
setBendingDiagram={setBendingDiagram}
setBendingDiagramFile={setBendingDiagramFile}
setIsDrawingOpen={setIsDrawingOpen}
bendingDetails={bendingDetails}
setBendingDetails={setBendingDetails}
setWidthSum={setWidthSum}
setValue={setValue}
isSubmitting={isSubmitting}
/>
)}
{/* 부품 구성 (BOM) - 제품 또는 부품(needsBOM 체크 시) */}
{(
selectedItemType === 'FG' ||
(selectedItemType === 'PT' && selectedPartType === 'ASSEMBLY' && selectedCategory1) ||
(selectedItemType === 'PT' && selectedPartType === 'PURCHASED')
) && needsBOM && (
<BOMSection
bomLines={bomLines}
setBomLines={setBomLines}
bomSearchStates={bomSearchStates}
setBomSearchStates={setBomSearchStates}
isSubmitting={isSubmitting}
/>
)}
{/* 전개도 그리기 캔버스 */}
<DrawingCanvas
open={isDrawingOpen}
onOpenChange={setIsDrawingOpen}
onSave={(imageData) => {
setBendingDiagram(imageData);
setIsDrawingOpen(false);
}}
initialImage={bendingDiagram}
title={selectedPartType === 'ASSEMBLY' ? '조립품 전개도 그리기' : '절곡품 전개도 그리기'}
description="전개도를 직접 그리거나 편집합니다."
/>
</form>
);
}