From ba8cd44a0ed2c613a5f2cee06bfc9bd0f61f21c7 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 7 Nov 2025 08:39:04 +0000 Subject: (최겸) 입찰/견적 수정사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../detail/general-contract-basic-info.tsx | 488 +++++++++++++++++++-- 1 file changed, 461 insertions(+), 27 deletions(-) (limited to 'lib/general-contracts/detail/general-contract-basic-info.tsx') diff --git a/lib/general-contracts/detail/general-contract-basic-info.tsx b/lib/general-contracts/detail/general-contract-basic-info.tsx index d891fe63..4071b2e0 100644 --- a/lib/general-contracts/detail/general-contract-basic-info.tsx +++ b/lib/general-contracts/detail/general-contract-basic-info.tsx @@ -16,6 +16,11 @@ 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 @@ -38,6 +43,7 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) { const [procurementLoading, setProcurementLoading] = useState(false) const [formData, setFormData] = useState({ + contractScope: '', // 계약확정범위 specificationType: '', specificationManualText: '', unitPriceType: '', @@ -83,9 +89,16 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) { 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(() => { @@ -121,7 +134,13 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) { 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 || '', @@ -145,10 +164,11 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) { liquidatedDamages: Boolean(contractData?.liquidatedDamages), liquidatedDamagesPercent: contractData?.liquidatedDamagesPercent || '', deliveryType: contractData?.deliveryType || '', - deliveryTerm: contractData?.deliveryTerm || '', + deliveryTerm: defaultDeliveryTerm, shippingLocation: contractData?.shippingLocation || '', dischargeLocation: contractData?.dischargeLocation || '', contractDeliveryDate: contractData?.contractDeliveryDate || '', + paymentDeliveryAdditionalText: (contractData as any)?.paymentDeliveryAdditionalText || '', contractEstablishmentConditions: contractEstablishmentConditions || { regularVendorRegistration: false, projectAward: false, @@ -167,6 +187,8 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) { projectNotAwarded: false, other: false, }, + externalYardEntry: (contractData?.externalYardEntry as 'Y' | 'N') || 'N', + contractAmountReason: (contractData as any)?.contractAmountReason || '', }) } catch (error) { console.error('Error loading contract:', error) @@ -179,6 +201,33 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) { } }, [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); @@ -249,7 +298,16 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) { // 필수값 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('지불조건') @@ -294,6 +352,35 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) { {/* 기본 정보 탭 */} + {/* 계약확정범위 */} + + + 계약확정범위 + + +
+
+ + +
+
+
+
+ {/* 보증기간 및 단가유형 */} @@ -509,7 +596,7 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) { - 첨부파일 + 첨부서류 참조 표준사양 수기사양 @@ -520,9 +607,26 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) { {/* 단가 */}
- + + {/* 단가 유형 '기타' 선택 시 수기입력 필드 */} + {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로 출력 */} @@ -552,6 +679,187 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) { )} + {/* 사양이 첨부서류 참조일 때 파일 업로드 */} + {formData.specificationType === '첨부서류 참조' && ( +
+
+ + + + + + + + 사양 파일 업로드 + +
+
+ + { + const file = e.target.files?.[0] + if (!file || !userId) return + + try { + setIsLoadingSpecFiles(true) + const result = await uploadContractAttachment( + contractId, + file, + userId.toString(), + '사양 및 공급범위' + ) + + if (result.success) { + toast.success('사양 파일이 업로드되었습니다.') + // 파일 목록 새로고침 + 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) + setShowSpecFileDialog(false) + e.target.value = '' + } else { + toast.error(result.error || '파일 업로드에 실패했습니다.') + } + } catch (error) { + console.error('Error uploading file:', error) + toast.error('파일 업로드 중 오류가 발생했습니다.') + } finally { + setIsLoadingSpecFiles(false) + } + }} + disabled={isLoadingSpecFiles} + /> +
+ + {/* 업로드된 파일 목록 */} + {specificationFiles.length > 0 && ( +
+ +
+ {specificationFiles.map((file) => ( +
+
+ + {file.fileName} + + ({new Date(file.uploadedAt).toLocaleDateString()}) + +
+
+ + +
+
+ ))} +
+
+ )} +
+
+
+
+ + {specificationFiles.length === 0 && ( +

사양 파일을 업로드해주세요.

+ )} + + {specificationFiles.length > 0 && ( +
+ {specificationFiles.map((file) => ( +
+
+ + {file.fileName} +
+
+ + +
+
+ ))} +
+ )} +
+ )} + @@ -664,6 +972,37 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) { disabled={!formData.paymentBeforeDelivery.materialPurchase} /> +
+ setFormData(prev => ({ + ...prev, + paymentBeforeDelivery: { + ...prev.paymentBeforeDelivery, + additionalCondition: e.target.checked + } + }))} + className="rounded" + /> + + setFormData(prev => ({ + ...prev, + paymentBeforeDelivery: { + ...prev.paymentBeforeDelivery, + additionalConditionPercent: e.target.value + } + }))} + disabled={!formData.paymentBeforeDelivery.additionalCondition} + /> +
@@ -678,26 +1017,26 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) { - L/C - T/T - 거래명세서 기반 정기지급조건 - 작업 및 입고 검사 완료 - 청구내역서 제출 및 승인 - 정규금액 월 단위 정산(지정일 지급) + {/* Payment term 검색 옵션들 */} + {paymentTermsOptions.map((term) => ( + + {term.code} - {term.description} + + ))} + 납품완료일로부터 60일 이내 지급 + 추가조건 - {/* L/C 또는 T/T 선택 시 퍼센트 입력 필드 */} - {(formData.paymentDelivery === 'L/C' || formData.paymentDelivery === 'T/T') && ( -
+ {/* 추가조건 선택 시 수기 입력 필드 */} + {formData.paymentDelivery === '추가조건' && ( +
setPaymentDeliveryPercent(e.target.value)} - placeholder="퍼센트" - className="w-20 h-8 text-sm" + type="text" + value={formData.paymentDeliveryAdditionalText || ''} + onChange={(e) => setFormData(prev => ({ ...prev, paymentDeliveryAdditionalText: e.target.value }))} + placeholder="추가조건을 입력하세요" + className="w-full" /> - %
)} {errors.paymentDelivery && ( @@ -1036,13 +1375,34 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) {
- + {contract?.type === 'AD' || contract?.type === 'AW' ? ( +
+ +
+ +