'use client' import React, { useState } from 'react' import { useSession } from 'next-auth/react' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Textarea } from '@/components/ui/textarea' import { Button } from '@/components/ui/button' import { Save, LoaderIcon } from 'lucide-react' import { updateContractBasicInfo, getContractBasicInfo } from '../service' import { toast } from 'sonner' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { GeneralContract } from '@/db/schema' import { ContractDocuments } from './general-contract-documents' import { getPaymentTermsForSelection, getIncotermsForSelection, getPlaceOfShippingForSelection, getPlaceOfDestinationForSelection } from '@/lib/procurement-select/service' import { TAX_CONDITIONS, getTaxConditionName } from '@/lib/tax-conditions/types' import { GENERAL_CONTRACT_SCOPES } from '@/lib/general-contracts/types' import { uploadContractAttachment, getContractAttachments, deleteContractAttachment, getContractAttachmentForDownload } from '../service' import { downloadFile } from '@/lib/file-download' import { FileText, Upload, Download, Trash2 } from 'lucide-react' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' interface ContractBasicInfoProps { contractId: number } export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) { const session = useSession() const [isLoading, setIsLoading] = useState(false) const [contract, setContract] = useState(null) const userId = session.data?.user?.id ? Number(session.data.user.id) : null // 독립적인 상태 관리 const [paymentDeliveryPercent, setPaymentDeliveryPercent] = useState('') // Procurement 데이터 상태들 const [paymentTermsOptions, setPaymentTermsOptions] = useState>([]) const [incotermsOptions, setIncotermsOptions] = useState>([]) const [shippingPlaces, setShippingPlaces] = useState>([]) const [destinationPlaces, setDestinationPlaces] = useState>([]) const [procurementLoading, setProcurementLoading] = useState(false) const [formData, setFormData] = useState({ contractScope: '', // 계약확정범위 specificationType: '', specificationManualText: '', unitPriceType: '', warrantyPeriod: { 납품후: { enabled: false, period: 0, maxPeriod: 0 }, 인도후: { enabled: false, period: 0, maxPeriod: 0 }, 작업후: { enabled: false, period: 0, maxPeriod: 0 }, 기타: { enabled: false, period: 0, maxPeriod: 0 }, }, contractAmount: null as number | null, currency: 'KRW', linkedPoNumber: '', linkedBidNumber: '', notes: '', // 개별 JSON 필드들 (스키마에 맞게) paymentBeforeDelivery: {} as any, paymentDelivery: '', // varchar 타입 paymentAfterDelivery: {} as any, paymentTerm: '', taxType: '', liquidatedDamages: false as boolean, liquidatedDamagesPercent: '', deliveryType: '', deliveryTerm: '', shippingLocation: '', dischargeLocation: '', contractDeliveryDate: '', contractEstablishmentConditions: { regularVendorRegistration: false, projectAward: false, ownerApproval: false, other: false, }, interlockingSystem: '', mandatoryDocuments: { technicalDataAgreement: false, nda: false, basicCompliance: false, safetyHealthAgreement: false, }, contractTerminationConditions: { standardTermination: false, projectNotAwarded: false, other: false, }, externalYardEntry: 'N' as 'Y' | 'N', // 사외업체 야드투입 (Y/N) contractAmountReason: '', // 합의계약 미확정 사유 }) const [errors] = useState>({}) const [specificationFiles, setSpecificationFiles] = useState>([]) const [isLoadingSpecFiles, setIsLoadingSpecFiles] = useState(false) const [showSpecFileDialog, setShowSpecFileDialog] = useState(false) const [unitPriceTypeOther, setUnitPriceTypeOther] = useState('') // 단가 유형 '기타' 수기입력 const [showYardEntryConfirmDialog, setShowYardEntryConfirmDialog] = useState(false) // 계약 데이터 로드 React.useEffect(() => { const loadContract = async () => { try { console.log('Loading contract with ID:', contractId) const contractData = await getContractBasicInfo(contractId) console.log('Contract data received:', contractData) setContract(contractData as GeneralContract) // JSON 필드들 파싱 (null 체크) - 스키마에 맞게 개별 필드로 접근 const paymentBeforeDelivery = (contractData?.paymentBeforeDelivery && typeof contractData.paymentBeforeDelivery === 'object') ? contractData.paymentBeforeDelivery as any : {} const paymentAfterDelivery = (contractData?.paymentAfterDelivery && typeof contractData.paymentAfterDelivery === 'object') ? contractData.paymentAfterDelivery as any : {} const warrantyPeriod = (contractData?.warrantyPeriod && typeof contractData.warrantyPeriod === 'object') ? contractData.warrantyPeriod as any : {} const contractEstablishmentConditions = (contractData?.contractEstablishmentConditions && typeof contractData.contractEstablishmentConditions === 'object') ? contractData.contractEstablishmentConditions as any : {} const mandatoryDocuments = (contractData?.mandatoryDocuments && typeof contractData.mandatoryDocuments === 'object') ? contractData.mandatoryDocuments as any : {} const contractTerminationConditions = (contractData?.contractTerminationConditions && typeof contractData.contractTerminationConditions === 'object') ? contractData.contractTerminationConditions as any : {} // paymentDelivery에서 퍼센트와 타입 분리 const paymentDeliveryValue = contractData?.paymentDelivery || '' let paymentDeliveryType = '' let paymentDeliveryPercentValue = '' if (paymentDeliveryValue.includes('%')) { const match = paymentDeliveryValue.match(/(\d+)%\s*(.+)/) if (match) { paymentDeliveryPercentValue = match[1] paymentDeliveryType = match[2] } } else { paymentDeliveryType = paymentDeliveryValue } setPaymentDeliveryPercent(paymentDeliveryPercentValue) // 합의계약(AD, AW)인 경우 인도조건 기본값 설정 const defaultDeliveryTerm = (contractData?.type === 'AD' || contractData?.type === 'AW') ? '본 표준하도급 계약에 따름' : (contractData?.deliveryTerm || '') setFormData({ contractScope: contractData?.contractScope || '', specificationType: contractData?.specificationType || '', specificationManualText: contractData?.specificationManualText || '', unitPriceType: contractData?.unitPriceType || '', warrantyPeriod: warrantyPeriod || { 납품후: { enabled: false, period: 0, maxPeriod: 0 }, 인도후: { enabled: false, period: 0, maxPeriod: 0 }, 작업후: { enabled: false, period: 0, maxPeriod: 0 }, 기타: { enabled: false, period: 0, maxPeriod: 0 }, }, contractAmount: contractData?.contractAmount || null, currency: contractData?.currency || 'KRW', linkedPoNumber: contractData?.linkedPoNumber || '', linkedBidNumber: contractData?.linkedBidNumber || '', notes: contractData?.notes || '', // 개별 JSON 필드들 paymentBeforeDelivery: paymentBeforeDelivery || {} as any, paymentDelivery: paymentDeliveryType, // 분리된 타입만 저장 paymentAfterDelivery: paymentAfterDelivery || {} as any, paymentTerm: contractData?.paymentTerm || '', taxType: contractData?.taxType || '', liquidatedDamages: Boolean(contractData?.liquidatedDamages), liquidatedDamagesPercent: contractData?.liquidatedDamagesPercent || '', deliveryType: contractData?.deliveryType || '', deliveryTerm: defaultDeliveryTerm, shippingLocation: contractData?.shippingLocation || '', dischargeLocation: contractData?.dischargeLocation || '', contractDeliveryDate: contractData?.contractDeliveryDate || '', paymentDeliveryAdditionalText: (contractData as any)?.paymentDeliveryAdditionalText || '', contractEstablishmentConditions: contractEstablishmentConditions || { regularVendorRegistration: false, projectAward: false, ownerApproval: false, other: false, }, interlockingSystem: contractData?.interlockingSystem || '', mandatoryDocuments: mandatoryDocuments || { technicalDataAgreement: false, nda: false, basicCompliance: false, safetyHealthAgreement: false, }, contractTerminationConditions: contractTerminationConditions || { standardTermination: false, projectNotAwarded: false, other: false, }, externalYardEntry: (contractData?.externalYardEntry as 'Y' | 'N') || 'N', contractAmountReason: (contractData as any)?.contractAmountReason || '', }) } catch (error) { console.error('Error loading contract:', error) toast.error('계약 정보를 불러오는 중 오류가 발생했습니다.') } } if (contractId) { loadContract() } }, [contractId]) // 사양 파일 목록 로드 React.useEffect(() => { const loadSpecificationFiles = async () => { if (!contractId || formData.specificationType !== '첨부서류 참조') return setIsLoadingSpecFiles(true) try { const attachments = await getContractAttachments(contractId) const specFiles = (attachments as Array<{ id: number; fileName: string; filePath: string; documentName: string; uploadedAt: Date }>) .filter(att => att.documentName === '사양 및 공급범위' || att.documentName === 'specification') .map(att => ({ id: att.id, fileName: att.fileName, filePath: att.filePath, uploadedAt: att.uploadedAt })) setSpecificationFiles(specFiles) } catch (error) { console.error('Error loading specification files:', error) } finally { setIsLoadingSpecFiles(false) } } loadSpecificationFiles() }, [contractId, formData.specificationType]) // Procurement 데이터 로드 함수들 const loadPaymentTerms = React.useCallback(async () => { setProcurementLoading(true); try { const data = await getPaymentTermsForSelection(); setPaymentTermsOptions(data); } catch (error) { console.error("Failed to load payment terms:", error); toast.error("결제조건 목록을 불러오는데 실패했습니다."); } finally { setProcurementLoading(false); } }, []); const loadIncoterms = React.useCallback(async () => { setProcurementLoading(true); try { const data = await getIncotermsForSelection(); setIncotermsOptions(data); } catch (error) { console.error("Failed to load incoterms:", error); toast.error("운송조건 목록을 불러오는데 실패했습니다."); } finally { setProcurementLoading(false); } }, []); const loadShippingPlaces = React.useCallback(async () => { setProcurementLoading(true); try { const data = await getPlaceOfShippingForSelection(); setShippingPlaces(data); } catch (error) { console.error("Failed to load shipping places:", error); toast.error("선적지 목록을 불러오는데 실패했습니다."); } finally { setProcurementLoading(false); } }, []); const loadDestinationPlaces = React.useCallback(async () => { setProcurementLoading(true); try { const data = await getPlaceOfDestinationForSelection(); setDestinationPlaces(data); } catch (error) { console.error("Failed to load destination places:", error); toast.error("하역지 목록을 불러오는데 실패했습니다."); } finally { setProcurementLoading(false); } }, []); // 컴포넌트 마운트 시 procurement 데이터 로드 React.useEffect(() => { loadPaymentTerms(); loadIncoterms(); loadShippingPlaces(); loadDestinationPlaces(); }, [loadPaymentTerms, loadIncoterms, loadShippingPlaces, loadDestinationPlaces]); const handleSaveContractInfo = async () => { if (!userId) { toast.error('사용자 정보를 찾을 수 없습니다.') return } try { setIsLoading(true) // 필수값 validation 체크 const validationErrors: string[] = [] if (!formData.contractScope) validationErrors.push('계약확정범위') if (!formData.specificationType) validationErrors.push('사양') // 첨부서류 참조 선택 시 사양 파일 필수 체크 if (formData.specificationType === '첨부서류 참조' && specificationFiles.length === 0) { validationErrors.push('사양 파일') } // LO 계약인 경우 계약체결유효기간 필수값 체크 if (contract?.type === 'LO' && !contract?.validityEndDate) { validationErrors.push('계약체결유효기간') } if (!formData.paymentDelivery) validationErrors.push('납품 지급조건') if (!formData.currency) validationErrors.push('계약통화') if (!formData.paymentTerm) validationErrors.push('지불조건') if (!formData.taxType) validationErrors.push('세금조건') if (validationErrors.length > 0) { toast.error(`다음 필수 항목을 입력해주세요: ${validationErrors.join(', ')}`) return } // paymentDelivery와 paymentDeliveryPercent 합쳐서 저장 const dataToSave = { ...formData, paymentDelivery: (formData.paymentDelivery === 'L/C' || formData.paymentDelivery === 'T/T') && paymentDeliveryPercent ? `${paymentDeliveryPercent}% ${formData.paymentDelivery}` : formData.paymentDelivery } await updateContractBasicInfo(contractId, dataToSave, userId as number) toast.success('계약 정보가 저장되었습니다.') } catch (error) { console.error('Error saving contract info:', error) toast.error('계약 정보 저장 중 오류가 발생했습니다.') } finally { setIsLoading(false) } } return ( 계약 기본 정보 기본 정보 지급/인도 조건 추가 조건 계약첨부문서 {/* 기본 정보 탭 */} {/* 계약확정범위 */} 계약확정범위
{/* 보증기간 및 단가유형 */} 보증기간 및 단가유형 {/* 3그리드: 보증기간, 사양, 단가 */}
{/* 보증기간 */}
setFormData(prev => ({ ...prev, warrantyPeriod: { ...prev.warrantyPeriod, 납품후: { ...prev.warrantyPeriod.납품후, enabled: e.target.checked } } }))} className="rounded" />
{formData.warrantyPeriod.납품후?.enabled && (
setFormData(prev => ({ ...prev, warrantyPeriod: { ...prev.warrantyPeriod, 납품후: { ...prev.warrantyPeriod.납품후, period: parseInt(e.target.value) || 0 } } }))} className="w-20 h-8 text-sm" /> 개월, 최대 setFormData(prev => ({ ...prev, warrantyPeriod: { ...prev.warrantyPeriod, 납품후: { ...prev.warrantyPeriod.납품후, maxPeriod: parseInt(e.target.value) || 0 } } }))} className="w-20 h-8 text-sm" /> 개월
)}
setFormData(prev => ({ ...prev, warrantyPeriod: { ...prev.warrantyPeriod, 인도후: { ...prev.warrantyPeriod.인도후, enabled: e.target.checked } } }))} className="rounded" />
{formData.warrantyPeriod.인도후?.enabled && (
setFormData(prev => ({ ...prev, warrantyPeriod: { ...prev.warrantyPeriod, 인도후: { ...prev.warrantyPeriod.인도후, period: parseInt(e.target.value) || 0 } } }))} className="w-20 h-8 text-sm" /> 개월, 최대 setFormData(prev => ({ ...prev, warrantyPeriod: { ...prev.warrantyPeriod, 인도후: { ...prev.warrantyPeriod.인도후, maxPeriod: parseInt(e.target.value) || 0 } } }))} className="w-20 h-8 text-sm" /> 개월
)}
setFormData(prev => ({ ...prev, warrantyPeriod: { ...prev.warrantyPeriod, 작업후: { ...prev.warrantyPeriod.작업후, enabled: e.target.checked } } }))} className="rounded" />
{formData.warrantyPeriod.작업후?.enabled && (
setFormData(prev => ({ ...prev, warrantyPeriod: { ...prev.warrantyPeriod, 작업후: { ...prev.warrantyPeriod.작업후, period: parseInt(e.target.value) || 0 } } }))} className="w-20 h-8 text-sm" /> 개월, 최대 setFormData(prev => ({ ...prev, warrantyPeriod: { ...prev.warrantyPeriod, 작업후: { ...prev.warrantyPeriod.작업후, maxPeriod: parseInt(e.target.value) || 0 } } }))} className="w-20 h-8 text-sm" /> 개월
)}
setFormData(prev => ({ ...prev, warrantyPeriod: { ...prev.warrantyPeriod, 기타: { ...prev.warrantyPeriod.기타, enabled: e.target.checked } } }))} className="rounded" />
{/* 사양 */}
{errors.specificationType && (

사양은 필수값입니다.

)}
{/* 단가 */}
{/* 단가 유형 '기타' 선택 시 수기입력 필드 */} {formData.unitPriceType === '기타' && (
setUnitPriceTypeOther(e.target.value)} placeholder="단가 유형을 수기로 입력하세요" className="mt-2" required />
)} {(() => { const contractType = contract?.type as string || '' const contractCategory = contract?.category as string || '' const unitPriceContractTypes = ['UP', 'LE', 'IL', 'AL', 'OS', 'OW'] const isUnitPriceRequired = contractCategory === 'unit_price' && unitPriceContractTypes.includes(contractType) return isUnitPriceRequired && !formData.unitPriceType ? (

단가 유형은 필수값입니다.

) : formData.unitPriceType === '기타' && !unitPriceTypeOther.trim() ? (

단가 유형(기타)을 입력해주세요.

) : null })()}
{/* 선택에 따른 폼: vertical로 출력 */} {/* 사양이 수기사양일 때 매뉴얼 텍스트 */} {formData.specificationType === '수기사양' && (