/** * 단가 등록/수정 폼 클라이언트 컴포넌트 * * 기능: * - 품목 정보 표시 (읽기전용) * - 단가 정보 입력 * - 원가/마진 자동 계산 * - 반올림 규칙 적용 * - 수정 이력 관리 */ 'use client'; import { useState, useEffect, useCallback, useMemo } from 'react'; import { useRouter } from 'next/navigation'; import { DollarSign, Package, ArrowLeft, Save, Calculator, TrendingUp, AlertCircle, History, CheckCircle2, Lock, } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Badge } from '@/components/ui/badge'; import { Separator } from '@/components/ui/separator'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { toast } from 'sonner'; import type { PricingData, PricingFormData, ItemInfo, RoundingRule, ItemType, } from './types'; import { ITEM_TYPE_LABELS, UNIT_OPTIONS, ROUNDING_RULE_OPTIONS, ROUNDING_UNIT_OPTIONS, } from './types'; // 다이얼로그 컴포넌트들 (추후 분리) import { PricingHistoryDialog } from './PricingHistoryDialog'; import { PricingRevisionDialog } from './PricingRevisionDialog'; import { PricingFinalizeDialog } from './PricingFinalizeDialog'; interface PricingFormClientProps { mode: 'create' | 'edit'; itemInfo?: ItemInfo; initialData?: PricingData; onSave?: (data: PricingData, isRevision?: boolean, revisionReason?: string) => Promise; onFinalize?: (id: string) => Promise; } export function PricingFormClient({ mode, itemInfo, initialData, onSave, onFinalize, }: PricingFormClientProps) { const router = useRouter(); const isEditMode = mode === 'edit'; // 품목 정보 (신규: itemInfo, 수정: initialData) const displayItemCode = initialData?.itemCode || itemInfo?.itemCode || ''; const displayItemName = initialData?.itemName || itemInfo?.itemName || ''; const displayItemType = initialData?.itemType || itemInfo?.itemType || ''; const displaySpecification = initialData?.specification || itemInfo?.specification || ''; const displayUnit = initialData?.unit || itemInfo?.unit || 'EA'; // 폼 상태 const [effectiveDate, setEffectiveDate] = useState( initialData?.effectiveDate || new Date().toISOString().split('T')[0] ); const [receiveDate, setReceiveDate] = useState(initialData?.receiveDate || ''); const [author, setAuthor] = useState(initialData?.author || ''); const [purchasePrice, setPurchasePrice] = useState(initialData?.purchasePrice || 0); const [processingCost, setProcessingCost] = useState(initialData?.processingCost || 0); const [loss, setLoss] = useState(initialData?.loss || 0); const [roundingRule, setRoundingRule] = useState( initialData?.roundingRule || 'round' ); const [roundingUnit, setRoundingUnit] = useState(initialData?.roundingUnit || 1); const [marginRate, setMarginRate] = useState(initialData?.marginRate || 0); const [salesPrice, setSalesPrice] = useState(initialData?.salesPrice || 0); const [supplier, setSupplier] = useState(initialData?.supplier || ''); const [note, setNote] = useState(initialData?.note || ''); const [unit, setUnit] = useState(displayUnit); // 에러 상태 const [errors, setErrors] = useState>({}); // 다이얼로그 상태 const [showHistoryDialog, setShowHistoryDialog] = useState(false); const [showRevisionDialog, setShowRevisionDialog] = useState(false); const [showFinalizeDialog, setShowFinalizeDialog] = useState(false); // 로딩 상태 const [isSaving, setIsSaving] = useState(false); // 반올림 적용 함수 const applyRounding = useCallback( (value: number, rule: RoundingRule, unit: number): number => { if (unit <= 0) return Math.round(value); switch (rule) { case 'ceil': return Math.ceil(value / unit) * unit; case 'floor': return Math.floor(value / unit) * unit; default: return Math.round(value / unit) * unit; } }, [] ); // LOSS 적용 원가 계산 const costWithLoss = useMemo(() => { const basePrice = (purchasePrice || 0) + (processingCost || 0); const lossRate = (loss || 0) / 100; return Math.round(basePrice * (1 + lossRate)); }, [purchasePrice, processingCost, loss]); // 마진율 → 판매단가 계산 const handleMarginRateChange = useCallback( (rate: number) => { setMarginRate(rate); if (costWithLoss > 0) { const calculatedPrice = costWithLoss * (1 + rate / 100); const roundedPrice = applyRounding(calculatedPrice, roundingRule, roundingUnit); setSalesPrice(Math.round(roundedPrice)); } else { setSalesPrice(0); } }, [costWithLoss, roundingRule, roundingUnit, applyRounding] ); // 판매단가 → 마진율 계산 const handleSalesPriceChange = useCallback( (price: number) => { setSalesPrice(price); if (costWithLoss > 0) { const calculatedMarginRate = ((price - costWithLoss) / costWithLoss) * 100; setMarginRate(parseFloat(calculatedMarginRate.toFixed(1))); } else { setMarginRate(0); } }, [costWithLoss] ); // 원가 변경 시 판매가 자동 재계산 useEffect(() => { if (marginRate > 0 && (purchasePrice > 0 || processingCost > 0)) { const calculatedPrice = costWithLoss * (1 + marginRate / 100); const roundedPrice = applyRounding(calculatedPrice, roundingRule, roundingUnit); const finalPrice = Math.round(roundedPrice); if (finalPrice !== salesPrice) { setSalesPrice(finalPrice); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [purchasePrice, processingCost, loss, roundingRule, roundingUnit]); // 마진 금액 계산 const marginAmount = useMemo(() => { return salesPrice - costWithLoss; }, [salesPrice, costWithLoss]); // 유효성 검사 const validateForm = useCallback(() => { const newErrors: Record = {}; if (!effectiveDate) newErrors.effectiveDate = true; if (purchasePrice <= 0 && salesPrice <= 0) { newErrors.purchasePrice = true; newErrors.salesPrice = true; } setErrors(newErrors); return Object.keys(newErrors).length === 0; }, [effectiveDate, purchasePrice, salesPrice]); // 저장 처리 const handleSave = async (isRevision = false, revisionReason = '') => { if (!validateForm()) { toast.error('필수 항목을 입력해주세요.'); return; } // 수정 모드이고 리비전 있으면 수정 이력 다이얼로그 if (isEditMode && initialData && !isRevision && (initialData.currentRevision > 0 || initialData.isFinal)) { setShowRevisionDialog(true); return; } setIsSaving(true); try { const pricingData: PricingData = { id: initialData?.id || `PR-${Date.now()}`, itemId: initialData?.itemId || itemInfo?.id || '', itemCode: displayItemCode, itemName: displayItemName, itemType: displayItemType, specification: displaySpecification, unit: unit, effectiveDate, receiveDate: receiveDate || undefined, author: author || undefined, purchasePrice: purchasePrice || undefined, processingCost: processingCost || undefined, loss: loss || undefined, roundingRule: roundingRule || undefined, roundingUnit: roundingUnit || undefined, marginRate: marginRate || undefined, salesPrice: salesPrice || undefined, supplier: supplier || undefined, note: note || undefined, currentRevision: isRevision ? (initialData?.currentRevision || 0) + 1 : initialData?.currentRevision || 0, isFinal: initialData?.isFinal || false, revisions: initialData?.revisions || [], status: isRevision ? 'active' : initialData?.status || 'draft', createdAt: initialData?.createdAt || new Date().toISOString(), createdBy: initialData?.createdBy || '관리자', updatedAt: new Date().toISOString(), updatedBy: '관리자', }; if (onSave) { await onSave(pricingData, isRevision, revisionReason); } toast.success(isEditMode ? '단가가 수정되었습니다.' : '단가가 등록되었습니다.'); router.push('/sales/pricing-management'); } catch (error) { toast.error('저장 중 오류가 발생했습니다.'); console.error(error); } finally { setIsSaving(false); } }; // 최종 확정 처리 const handleFinalize = async () => { if (!initialData) return; setIsSaving(true); try { if (onFinalize) { // 서버 액션으로 확정 처리 await onFinalize(initialData.id); } toast.success('단가가 최종 확정되었습니다.'); setShowFinalizeDialog(false); router.push('/sales/pricing-management'); } catch (error) { toast.error('확정 중 오류가 발생했습니다.'); console.error(error); } finally { setIsSaving(false); } }; return (
{/* 헤더 */}

단가 {isEditMode ? '수정' : '등록'}

{isEditMode ? '품목의 단가 정보를 수정합니다' : '새로운 품목의 단가 정보를 등록합니다'}

{/* 상태 표시 (수정 모드) */} {isEditMode && initialData && (
{initialData.isFinal && ( 최종 확정됨 )} {initialData.currentRevision > 0 && ( 수정 {initialData.currentRevision}차 )} {initialData.status === 'active' && !initialData.isFinal && ( 활성 )}
)} {/* 품목 정보 카드 */} 품목 정보
{displayItemCode}
{displayItemName}
{ITEM_TYPE_LABELS[displayItemType as ItemType] || displayItemType}
{displayUnit}
{displaySpecification && (
{displaySpecification}
)}
{/* 단가 정보 카드 */} 단가 정보 {/* 적용일 */}
{ setEffectiveDate(e.target.value); setErrors((prev) => ({ ...prev, effectiveDate: false })); }} className={errors.effectiveDate ? 'border-red-500' : ''} /> {errors.effectiveDate && (

적용일을 선택해주세요

)}
{/* 공급업체 및 기본 정보 */}
setSupplier(e.target.value)} placeholder="공급업체명을 입력하세요" />
setReceiveDate(e.target.value)} />
setAuthor(e.target.value)} placeholder="작성자명을 입력하세요" />
{/* 입고가 및 단위 */}
{ setPurchasePrice(parseInt(e.target.value) || 0); setErrors((prev) => ({ ...prev, purchasePrice: false, salesPrice: false })); }} placeholder="0" className={errors.purchasePrice ? 'border-red-500 pr-12' : 'pr-12'} />

단가 적용 시 사용될 단위

setLoss(parseFloat(e.target.value) || 0)} placeholder="0" className="pr-12" /> %

제조 과정에서 발생하는 손실율

setProcessingCost(parseInt(e.target.value) || 0)} placeholder="0" className="pr-12" />
{/* 원가 계산 섹션 */} {(purchasePrice > 0 || processingCost > 0) && (
입고가: {(purchasePrice || 0).toLocaleString()}원
가공비: {(processingCost || 0).toLocaleString()}원
소계: {((purchasePrice || 0) + (processingCost || 0)).toLocaleString()}원
{loss > 0 && (
LOSS ({loss}%): +{(((purchasePrice || 0) + (processingCost || 0)) * (loss / 100)).toLocaleString()}원
)}
LOSS 적용 원가: {costWithLoss.toLocaleString()}원
)} {/* 반올림 설정 */}

단가 계산 시 적용될 반올림 단위

{/* 마진율/판매단가 */}
handleMarginRateChange(parseFloat(e.target.value) || 0)} placeholder="0" className="pr-12" /> %

마진율을 입력하면 판매단가가 자동 계산됩니다

{ handleSalesPriceChange(parseInt(e.target.value) || 0); setErrors((prev) => ({ ...prev, purchasePrice: false, salesPrice: false })); }} placeholder="0" className={errors.salesPrice ? 'border-red-500 pr-12' : 'pr-12'} />

판매단가를 직접 입력하면 마진율이 자동 계산됩니다

{(errors.purchasePrice || errors.salesPrice) && (

입고가 또는 판매단가 중 최소 하나를 입력해주세요

)} {/* 마진 계산 섹션 */} {salesPrice > 0 && (purchasePrice > 0 || processingCost > 0) && (
LOSS 적용 원가: {costWithLoss.toLocaleString()}원
판매단가: {salesPrice.toLocaleString()}원
마진: {marginAmount.toLocaleString()}원 ({marginRate.toFixed(1)}%)
)} {/* 비고 */}