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 --- .../general-contract-approval-request-dialog.tsx | 141 +-- .../detail/general-contract-basic-info.tsx | 488 ++++++++- .../general-contract-communication-channel.tsx | 362 ------- .../detail/general-contract-detail.tsx | 81 +- .../detail/general-contract-documents.tsx | 11 +- .../detail/general-contract-field-service-rate.tsx | 288 ----- .../detail/general-contract-info-header.tsx | 5 +- .../detail/general-contract-items-table.tsx | 292 +++++- .../detail/general-contract-location.tsx | 480 --------- .../detail/general-contract-offset-details.tsx | 314 ------ .../detail/general-contract-review-comments.tsx | 194 ++++ .../general-contract-review-request-dialog.tsx | 891 ++++++++++++++++ .../detail/general-contract-storage-info.tsx | 249 +++++ .../general-contract-subcontract-checklist.tsx | 47 +- .../detail/general-contract-yard-entry-info.tsx | 232 +++++ .../main/create-general-contract-dialog.tsx | 156 ++- .../main/general-contract-update-sheet.tsx | 53 +- .../main/general-contracts-table-columns.tsx | 34 +- .../main/general-contracts-table.tsx | 5 +- lib/general-contracts/service.ts | 1102 +++++++++++++++++--- lib/general-contracts/types.ts | 8 +- 21 files changed, 3510 insertions(+), 1923 deletions(-) delete mode 100644 lib/general-contracts/detail/general-contract-communication-channel.tsx delete mode 100644 lib/general-contracts/detail/general-contract-field-service-rate.tsx delete mode 100644 lib/general-contracts/detail/general-contract-location.tsx delete mode 100644 lib/general-contracts/detail/general-contract-offset-details.tsx create mode 100644 lib/general-contracts/detail/general-contract-review-comments.tsx create mode 100644 lib/general-contracts/detail/general-contract-review-request-dialog.tsx create mode 100644 lib/general-contracts/detail/general-contract-storage-info.tsx create mode 100644 lib/general-contracts/detail/general-contract-yard-entry-info.tsx (limited to 'lib/general-contracts') diff --git a/lib/general-contracts/detail/general-contract-approval-request-dialog.tsx b/lib/general-contracts/detail/general-contract-approval-request-dialog.tsx index f05fe9ef..25c1fb9a 100644 --- a/lib/general-contracts/detail/general-contract-approval-request-dialog.tsx +++ b/lib/general-contracts/detail/general-contract-approval-request-dialog.tsx @@ -27,10 +27,6 @@ import { type BasicContractTemplate } from '@/db/schema' import { getBasicInfo, getContractItems, - getCommunicationChannel, - getLocation, - getFieldServiceRate, - getOffsetDetails, getSubcontractChecklist, uploadContractApprovalFile, sendContractApprovalRequest @@ -45,10 +41,6 @@ interface ContractApprovalRequestDialogProps { interface ContractSummary { basicInfo: Record items: Record[] - communicationChannel: Record | null - location: Record | null - fieldServiceRate: Record | null - offsetDetails: Record | null subcontractChecklist: Record | null } @@ -280,10 +272,6 @@ export function ContractApprovalRequestDialog({ const summary: ContractSummary = { basicInfo: {}, items: [], - communicationChannel: null, - location: null, - fieldServiceRate: null, - offsetDetails: null, subcontractChecklist: null } @@ -293,6 +281,14 @@ export function ContractApprovalRequestDialog({ if (basicInfoData && basicInfoData.success) { summary.basicInfo = basicInfoData.data || {} } + // externalYardEntry 정보도 추가로 가져오기 + const contractData = await getContractById(contractId) + if (contractData) { + summary.basicInfo = { + ...summary.basicInfo, + externalYardEntry: contractData.externalYardEntry || 'N' + } + } } catch { console.log('Basic Info 데이터 없음') } @@ -307,47 +303,6 @@ export function ContractApprovalRequestDialog({ console.log('품목 정보 데이터 없음') } - // 각 컴포넌트의 활성화 상태 및 데이터 확인 - try { - // Communication Channel 확인 - const commData = await getCommunicationChannel(contractId) - if (commData && commData.enabled) { - summary.communicationChannel = commData - } - } catch { - console.log('Communication Channel 데이터 없음') - } - - try { - // Location 확인 - const locationData = await getLocation(contractId) - if (locationData && locationData.enabled) { - summary.location = locationData - } - } catch { - console.log('Location 데이터 없음') - } - - try { - // Field Service Rate 확인 - const fieldServiceData = await getFieldServiceRate(contractId) - if (fieldServiceData && fieldServiceData.enabled) { - summary.fieldServiceRate = fieldServiceData - } - } catch { - console.log('Field Service Rate 데이터 없음') - } - - try { - // Offset Details 확인 - const offsetData = await getOffsetDetails(contractId) - if (offsetData && offsetData.enabled) { - summary.offsetDetails = offsetData - } - } catch { - console.log('Offset Details 데이터 없음') - } - try { // Subcontract Checklist 확인 const subcontractData = await getSubcontractChecklist(contractId) @@ -943,86 +898,6 @@ export function ContractApprovalRequestDialog({ )} - {/* 커뮤니케이션 채널 */} -
-
- - - 선택 -
-

- {contractSummary?.communicationChannel - ? '정보가 입력되어 있습니다.' - : '정보가 입력되지 않았습니다.'} -

-
- - {/* 위치 정보 */} -
-
- - - 선택 -
-

- {contractSummary?.location - ? '정보가 입력되어 있습니다.' - : '정보가 입력되지 않았습니다.'} -

-
- - {/* 현장 서비스 요율 */} -
-
- - - 선택 -
-

- {contractSummary?.fieldServiceRate - ? '정보가 입력되어 있습니다.' - : '정보가 입력되지 않았습니다.'} -

-
- - {/* 오프셋 세부사항 */} -
-
- - - 선택 -
-

- {contractSummary?.offsetDetails - ? '정보가 입력되어 있습니다.' - : '정보가 입력되지 않았습니다.'} -

-
- {/* 하도급 체크리스트 */}
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' ? ( +
+ +
+ +