summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/general-contracts/detail/general-contract-basic-info.tsx28
-rw-r--r--lib/general-contracts/main/create-general-contract-dialog.tsx5
-rw-r--r--lib/general-contracts/main/general-contract-update-sheet.tsx20
-rw-r--r--lib/general-contracts_old/detail/general-contract-approval-request-dialog.tsx1312
-rw-r--r--lib/general-contracts_old/detail/general-contract-basic-info.tsx1250
-rw-r--r--lib/general-contracts_old/detail/general-contract-communication-channel.tsx362
-rw-r--r--lib/general-contracts_old/detail/general-contract-detail.tsx186
-rw-r--r--lib/general-contracts_old/detail/general-contract-documents.tsx383
-rw-r--r--lib/general-contracts_old/detail/general-contract-field-service-rate.tsx288
-rw-r--r--lib/general-contracts_old/detail/general-contract-info-header.tsx211
-rw-r--r--lib/general-contracts_old/detail/general-contract-items-table.tsx602
-rw-r--r--lib/general-contracts_old/detail/general-contract-location.tsx480
-rw-r--r--lib/general-contracts_old/detail/general-contract-offset-details.tsx314
-rw-r--r--lib/general-contracts_old/detail/general-contract-subcontract-checklist.tsx610
-rw-r--r--lib/general-contracts_old/main/create-general-contract-dialog.tsx413
-rw-r--r--lib/general-contracts_old/main/general-contract-update-sheet.tsx401
-rw-r--r--lib/general-contracts_old/main/general-contracts-table-columns.tsx571
-rw-r--r--lib/general-contracts_old/main/general-contracts-table-toolbar-actions.tsx124
-rw-r--r--lib/general-contracts_old/main/general-contracts-table.tsx217
-rw-r--r--lib/general-contracts_old/service.ts1933
-rw-r--r--lib/general-contracts_old/types.ts125
-rw-r--r--lib/general-contracts_old/validation.ts82
-rw-r--r--lib/mail/templates/risks-notification.hbs2
-rw-r--r--lib/pq/service.ts89
-rw-r--r--lib/pq/table/copy-pq-list-dialog.tsx2
-rw-r--r--lib/rfq-last/attachment/vendor-response-table.tsx2
-rw-r--r--lib/rfq-last/quotation-compare-view.tsx46
-rw-r--r--lib/rfq-last/table/rfq-table-columns.tsx2
-rw-r--r--lib/rfq-last/table/rfq-table.tsx2
-rw-r--r--lib/rfq-last/validations.ts2
-rw-r--r--lib/rfq-last/vendor/batch-update-conditions-dialog.tsx112
-rw-r--r--lib/rfq-last/vendor/rfq-vendor-table.tsx56
-rw-r--r--lib/rfq-last/vendor/send-rfq-dialog.tsx8
-rw-r--r--lib/site-visit/client-site-visit-wrapper.tsx16
-rw-r--r--lib/site-visit/vendor-info-sheet.tsx66
-rw-r--r--lib/site-visit/vendor-info-view-dialog.tsx22
-rw-r--r--lib/vendors/rfq-history-table/rfq-history-table.tsx4
-rw-r--r--lib/vendors/table/request-pq-dialog.tsx67
38 files changed, 317 insertions, 10098 deletions
diff --git a/lib/general-contracts/detail/general-contract-basic-info.tsx b/lib/general-contracts/detail/general-contract-basic-info.tsx
index 4071b2e0..fb08da87 100644
--- a/lib/general-contracts/detail/general-contract-basic-info.tsx
+++ b/lib/general-contracts/detail/general-contract-basic-info.tsx
@@ -53,18 +53,19 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) {
작업후: { enabled: false, period: 0, maxPeriod: 0 },
기타: { enabled: false, period: 0, maxPeriod: 0 },
},
- contractAmount: null as number | null,
+ contractAmount: null,
currency: 'KRW',
linkedPoNumber: '',
linkedBidNumber: '',
notes: '',
// 개별 JSON 필드들 (스키마에 맞게)
- paymentBeforeDelivery: {} as any,
+ paymentBeforeDelivery: {},
paymentDelivery: '', // varchar 타입
- paymentAfterDelivery: {} as any,
+ paymentDeliveryAdditionalText: '',
+ paymentAfterDelivery: {},
paymentTerm: '',
taxType: '',
- liquidatedDamages: false as boolean,
+ liquidatedDamages: false,
liquidatedDamagesPercent: '',
deliveryType: '',
deliveryTerm: '',
@@ -89,7 +90,7 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) {
projectNotAwarded: false,
other: false,
},
- externalYardEntry: 'N' as 'Y' | 'N', // 사외업체 야드투입 (Y/N)
+ externalYardEntry: 'N', // 사외업체 야드투입 (Y/N)
contractAmountReason: '', // 합의계약 미확정 사유
})
@@ -309,7 +310,10 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) {
validationErrors.push('계약체결유효기간')
}
if (!formData.paymentDelivery) validationErrors.push('납품 지급조건')
- if (!formData.currency) validationErrors.push('계약통화')
+ // 계약확정범위가 '단가' 또는 '물량(실적)'이 아닌 경우에만 계약통화 필수값 체크
+ if (formData.contractScope !== '단가' && formData.contractScope !== '물량(실적)' && !formData.currency) {
+ validationErrors.push('계약통화')
+ }
if (!formData.paymentTerm) validationErrors.push('지불조건')
if (!formData.taxType) validationErrors.push('세금조건')
@@ -1375,7 +1379,7 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="contractAmount">계약금액 (자동계산)</Label>
- {contract?.type === 'AD' || contract?.type === 'AW' ? (
+ {(contract?.type === 'AD' || contract?.type === 'AW') || formData.contractScope === '단가' || formData.contractScope === '물량(실적)' ? (
<div className="space-y-2">
<Input
type="text"
@@ -1405,15 +1409,19 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) {
)}
</div>
<div className="space-y-2">
- <Label htmlFor="currency">계약통화 <span className="text-red-600">*</span></Label>
+ <Label htmlFor="currency">
+ 계약통화
+ {formData.contractScope !== '단가' && formData.contractScope !== '물량(실적)' && <span className="text-red-600">*</span>}
+ </Label>
<Input
type="text"
value={formData.currency}
onChange={(e) => setFormData(prev => ({ ...prev, currency: e.target.value }))}
placeholder="계약통화를 입력하세요"
- className={errors.currency ? 'border-red-500' : ''}
+ className={`${errors.currency ? 'border-red-500' : ''} ${formData.contractScope === '단가' || formData.contractScope === '물량(실적)' ? 'bg-gray-50' : ''}`}
+ disabled={formData.contractScope === '단가' || formData.contractScope === '물량(실적)'}
/>
- {errors.currency && (
+ {errors.currency && formData.contractScope !== '단가' && formData.contractScope !== '물량(실적)' && (
<p className="text-sm text-red-600">계약통화는 필수값입니다.</p>
)}
</div>
diff --git a/lib/general-contracts/main/create-general-contract-dialog.tsx b/lib/general-contracts/main/create-general-contract-dialog.tsx
index 168b8cbc..04f70834 100644
--- a/lib/general-contracts/main/create-general-contract-dialog.tsx
+++ b/lib/general-contracts/main/create-general-contract-dialog.tsx
@@ -205,6 +205,11 @@ export function CreateGeneralContractDialog() {
onChange={(e) => setForm(prev => ({ ...prev, name: e.target.value }))}
placeholder="계약명을 입력하세요"
/>
+ {form.type === 'SC' && (
+ <p className="text-sm text-blue-600 mt-1">
+ 납품예정 품목 및 수량을 명기하세요. 납품 품목 또는 작업 내용은 구체적으로 작성하되, 수량(물량)이 정확하지 않을 경우, 상호협의하에 변경 가능하며, 수량(물량) 등은 개별계약(PO)시 명기하세요
+ </p>
+ )}
</div>
</div>
diff --git a/lib/general-contracts/main/general-contract-update-sheet.tsx b/lib/general-contracts/main/general-contract-update-sheet.tsx
index 18095516..074558ec 100644
--- a/lib/general-contracts/main/general-contract-update-sheet.tsx
+++ b/lib/general-contracts/main/general-contract-update-sheet.tsx
@@ -46,8 +46,8 @@ const updateContractSchema = z.object({
name: z.string().min(1, "계약명을 입력해주세요"),
startDate: z.string().optional(), // AD, LO, OF 계약인 경우 선택사항
endDate: z.string().optional(), // AD, LO, OF 계약인 경우 선택사항
- validityEndDate: z.string().optional(), // LO 계약인 경우에만 필수값으로 처리
- contractScope: z.string().min(1, "계약확정범위를 선택해주세요"),
+ validityEndDate: z.string().optional(),
+ contractScope: z.string().optional(),
notes: z.string().optional(),
linkedRfqOrItb: z.string().optional(),
linkedPoNumber: z.string().optional(),
@@ -70,15 +70,6 @@ const updateContractSchema = z.object({
})
}
}
-
- // LO 계약인 경우 계약체결유효기간 필수값 체크
- if (data.type === 'LO' && !data.validityEndDate) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: "LO 계약의 경우 계약체결유효기간은 필수 항목입니다",
- path: ["validityEndDate"],
- })
- }
})
type UpdateContractFormData = z.infer<typeof updateContractSchema>
@@ -355,10 +346,7 @@ export function GeneralContractUpdateSheet({
name="validityEndDate"
render={({ field }) => (
<FormItem>
- <FormLabel>
- 유효기간종료일
- {form.watch('type') === 'LO' && <span className="text-red-600 ml-1">*</span>}
- </FormLabel>
+ <FormLabel>유효기간종료일</FormLabel>
<FormControl>
<Input type="date" {...field} />
</FormControl>
@@ -373,7 +361,7 @@ export function GeneralContractUpdateSheet({
name="contractScope"
render={({ field }) => (
<FormItem>
- <FormLabel>계약확정범위 *</FormLabel>
+ <FormLabel>계약확정범위</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
diff --git a/lib/general-contracts_old/detail/general-contract-approval-request-dialog.tsx b/lib/general-contracts_old/detail/general-contract-approval-request-dialog.tsx
deleted file mode 100644
index f05fe9ef..00000000
--- a/lib/general-contracts_old/detail/general-contract-approval-request-dialog.tsx
+++ /dev/null
@@ -1,1312 +0,0 @@
-'use client'
-
-import React, { useState, useEffect } from 'react'
-import { useSession } from 'next-auth/react'
-import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
-import { Button } from '@/components/ui/button'
-import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
-import { Badge } from '@/components/ui/badge'
-import { Checkbox } from '@/components/ui/checkbox'
-import { Label } from '@/components/ui/label'
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
-import { Input } from '@/components/ui/input'
-import { toast } from 'sonner'
-import {
- FileText,
- Upload,
- Eye,
- Send,
- CheckCircle,
- Download,
- AlertCircle
-} from 'lucide-react'
-import { ContractDocuments } from './general-contract-documents'
-import { getActiveContractTemplates } from '@/lib/bidding/service'
-import { type BasicContractTemplate } from '@/db/schema'
-import {
- getBasicInfo,
- getContractItems,
- getCommunicationChannel,
- getLocation,
- getFieldServiceRate,
- getOffsetDetails,
- getSubcontractChecklist,
- uploadContractApprovalFile,
- sendContractApprovalRequest
-} from '../service'
-
-interface ContractApprovalRequestDialogProps {
- contract: Record<string, unknown>
- open: boolean
- onOpenChange: (open: boolean) => void
-}
-
-interface ContractSummary {
- basicInfo: Record<string, unknown>
- items: Record<string, unknown>[]
- communicationChannel: Record<string, unknown> | null
- location: Record<string, unknown> | null
- fieldServiceRate: Record<string, unknown> | null
- offsetDetails: Record<string, unknown> | null
- subcontractChecklist: Record<string, unknown> | null
-}
-
-export function ContractApprovalRequestDialog({
- contract,
- open,
- onOpenChange
-}: ContractApprovalRequestDialogProps) {
- const { data: session } = useSession()
- const [currentStep, setCurrentStep] = useState(1)
- const [contractSummary, setContractSummary] = useState<ContractSummary | null>(null)
- const [uploadedFile, setUploadedFile] = useState<File | null>(null)
- const [generatedPdfUrl, setGeneratedPdfUrl] = useState<string | null>(null)
- const [generatedPdfBuffer, setGeneratedPdfBuffer] = useState<Uint8Array | null>(null)
- const [isLoading, setIsLoading] = useState(false)
- const [pdfViewerInstance, setPdfViewerInstance] = useState<any>(null)
- const [isPdfPreviewVisible, setIsPdfPreviewVisible] = useState(false)
-
- // 기본계약 관련 상태
- const [selectedBasicContracts, setSelectedBasicContracts] = useState<Array<{
- type: string;
- templateName: string;
- checked: boolean;
- }>>([])
- const [isLoadingBasicContracts, setIsLoadingBasicContracts] = useState(false)
-
- const contractId = contract.id as number
- const userId = session?.user?.id || ''
-
- // LOI 템플릿용 변수 매핑 함수
- const mapContractSummaryToLOITemplate = (contractSummary: ContractSummary) => {
- const { basicInfo, items } = contractSummary
- const firstItem = items && items.length > 0 ? items[0] : {}
-
- // 날짜 포맷팅 헬퍼 함수
- const formatDate = (date: any) => {
- if (!date) return ''
- try {
- const d = new Date(date)
- return d.toLocaleDateString('ko-KR')
- } catch {
- return ''
- }
- }
-
- return {
- // 날짜 관련 (템플릿에서 {{todayDate}} 형식으로 사용)
- todayDate: new Date().toLocaleDateString('ko-KR'),
-
- // 벤더 정보
- vendorName: basicInfo?.vendorName || '',
- representativeName: '', // 벤더 대표자 이름 - 현재 데이터에 없음, 향후 확장 가능
-
- // 계약 기본 정보
- contractNumber: basicInfo?.contractNumber || '',
-
- // 프로젝트 정보
- projectNumber: '', // 프로젝트 코드 - 현재 데이터에 없음, 향후 확장 가능
- projectName: basicInfo?.projectName || '',
- project: basicInfo?.projectName || '',
-
- // 아이템 정보
- item: firstItem?.itemInfo || '',
-
- // 무역 조건
- incoterms: basicInfo?.deliveryTerm || '', // Incoterms 대신 deliveryTerm 사용
- shippingLocation: basicInfo?.shippingLocation || '',
-
- // 금액 및 통화
- contractCurrency: basicInfo?.currency || '',
- contractAmount: basicInfo?.contractAmount || '',
- totalAmount: basicInfo?.contractAmount || '', // totalAmount가 없으면 contractAmount 사용
-
- // 수량
- quantity: firstItem?.quantity || '',
-
- // 납기일
- contractDeliveryDate: formatDate(basicInfo?.contractDeliveryDate),
-
- // 지급 조건
- paymentTerm: basicInfo?.paymentTerm || '',
-
- // 유효기간
- validityEndDate: formatDate(basicInfo?.endDate), // validityEndDate 대신 endDate 사용
- }
- }
-
- // 기본계약 생성 함수 (최종 전송 시점에 호출)
- const generateBasicContractPdf = async (
- vendorId: number,
- contractType: string,
- templateName: string
- ): Promise<{ buffer: number[], fileName: string }> => {
- try {
- // 1. 템플릿 데이터 준비 (서버 액션 호출)
- const prepareResponse = await fetch("/api/contracts/prepare-template", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- templateName,
- vendorId,
- }),
- });
-
- if (!prepareResponse.ok) {
- const errorText = await prepareResponse.text();
- throw new Error(`템플릿 준비 실패 (${prepareResponse.status}): ${errorText}`);
- }
-
- const { template, templateData } = await prepareResponse.json();
-
- // 2. 템플릿 파일 다운로드
- const templateResponse = await fetch("/api/contracts/get-template", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ templatePath: template.filePath }),
- });
-
- const templateBlob = await templateResponse.blob();
- const templateFile = new window.File([templateBlob], "template.docx", {
- type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
- });
-
- // 3. PDFTron WebViewer로 PDF 변환
- const { default: WebViewer } = await import("@pdftron/webviewer");
-
- const tempDiv = document.createElement('div');
- tempDiv.style.display = 'none';
- document.body.appendChild(tempDiv);
-
- try {
- const instance = await WebViewer(
- {
- path: "/pdftronWeb",
- licenseKey: process.env.NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY,
- fullAPI: true,
- enableOfficeEditing: true,
- },
- tempDiv
- );
-
- const { Core } = instance;
- const { createDocument } = Core;
-
- const templateDoc = await createDocument(templateFile, {
- filename: templateFile.name,
- extension: 'docx',
- });
-
- // 변수 치환 적용
- await templateDoc.applyTemplateValues(templateData);
- await new Promise(resolve => setTimeout(resolve, 3000));
-
- const fileData = await templateDoc.getFileData();
- const pdfBuffer = await Core.officeToPDFBuffer(fileData, { extension: 'docx' });
-
- const fileName = `${contractType}_${contractSummary?.basicInfo?.vendorCode || vendorId}_${Date.now()}.pdf`;
-
- instance.UI.dispose();
- return {
- buffer: Array.from(pdfBuffer),
- fileName
- };
-
- } finally {
- if (tempDiv.parentNode) {
- document.body.removeChild(tempDiv);
- }
- }
-
- } catch (error) {
- console.error(`기본계약 PDF 생성 실패 (${contractType}):`, error);
- throw error;
- }
- };
-
- // 기본계약 생성 및 선택 초기화
- const initializeBasicContracts = React.useCallback(async () => {
- if (!contractSummary?.basicInfo) return;
-
- setIsLoadingBasicContracts(true);
- try {
- // 기본적으로 사용할 수 있는 계약서 타입들
- const availableContracts: Array<{
- type: string;
- templateName: string;
- checked: boolean;
- }> = [
- { type: "NDA", templateName: "비밀", checked: false },
- { type: "General_GTC", templateName: "General GTC", checked: false },
- { type: "기술자료", templateName: "기술", checked: false }
- ];
-
- // 프로젝트 코드가 있으면 Project GTC도 추가
- if (contractSummary.basicInfo.projectCode) {
- availableContracts.push({
- type: "Project_GTC",
- templateName: contractSummary.basicInfo.projectCode as string,
- checked: false
- });
- }
-
- setSelectedBasicContracts(availableContracts);
- } catch (error) {
- console.error('기본계약 초기화 실패:', error);
- toast.error('기본계약 초기화에 실패했습니다.');
- } finally {
- setIsLoadingBasicContracts(false);
- }
- }, [contractSummary]);
-
- // 기본계약 선택 토글
- const toggleBasicContract = (type: string) => {
- setSelectedBasicContracts(prev =>
- prev.map(contract =>
- contract.type === type
- ? { ...contract, checked: !contract.checked }
- : contract
- )
- );
- };
-
-
- // 1단계: 계약 현황 수집
- const collectContractSummary = React.useCallback(async () => {
- setIsLoading(true)
- try {
- // 각 컴포넌트에서 활성화된 데이터만 수집
- const summary: ContractSummary = {
- basicInfo: {},
- items: [],
- communicationChannel: null,
- location: null,
- fieldServiceRate: null,
- offsetDetails: null,
- subcontractChecklist: null
- }
-
- // Basic Info 확인 (항상 활성화)
- try {
- const basicInfoData = await getBasicInfo(contractId)
- if (basicInfoData && basicInfoData.success) {
- summary.basicInfo = basicInfoData.data || {}
- }
- } catch {
- console.log('Basic Info 데이터 없음')
- }
-
- // 품목 정보 확인
- try {
- const itemsData = await getContractItems(contractId)
- if (itemsData && itemsData.length > 0) {
- summary.items = itemsData
- }
- } catch {
- 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)
- if (subcontractData && subcontractData.success && subcontractData.enabled) {
- summary.subcontractChecklist = subcontractData.data
- }
- } catch {
- console.log('Subcontract Checklist 데이터 없음')
- }
-
- console.log('contractSummary 구조:', summary)
- console.log('basicInfo 내용:', summary.basicInfo)
- setContractSummary(summary)
- } catch (error) {
- console.error('Error collecting contract summary:', error)
- toast.error('계약 정보를 수집하는 중 오류가 발생했습니다.')
- } finally {
- setIsLoading(false)
- }
- }, [contractId])
-
- // 3단계: 파일 업로드 처리
- const handleFileUpload = async (file: File) => {
- // 파일 확장자 검증
- const allowedExtensions = ['.doc', '.docx']
- const fileExtension = file.name.toLowerCase().substring(file.name.lastIndexOf('.'))
-
- if (!allowedExtensions.includes(fileExtension)) {
- toast.error('Word 문서(.doc, .docx) 파일만 업로드 가능합니다.')
- return
- }
-
- if (!userId) {
- toast.error('로그인이 필요합니다.')
- return
- }
-
- setIsLoading(true)
- try {
- // 서버액션을 사용하여 파일 저장 (본 계약문서로 고정)
- const result = await uploadContractApprovalFile(
- contractId,
- file,
- userId
- )
-
- if (result.success) {
- setUploadedFile(file)
- toast.success('파일이 업로드되었습니다.')
- } else {
- throw new Error(result.error || '파일 업로드 실패')
- }
- } catch (error) {
- console.error('Error uploading file:', error)
- toast.error('파일 업로드 중 오류가 발생했습니다.')
- } finally {
- setIsLoading(false)
- }
- }
-
- // 4단계: PDF 생성 및 미리보기 (PDFTron 사용)
- const generatePdf = async () => {
- if (!uploadedFile || !contractSummary) {
- toast.error('업로드된 파일과 계약 정보가 필요합니다.')
- return
- }
-
- setIsLoading(true)
- try {
- // PDFTron을 사용해서 변수 치환 및 PDF 변환
- // @ts-ignore
- const WebViewer = await import("@pdftron/webviewer").then(({ default: WebViewer }) => WebViewer)
-
- // 임시 WebViewer 인스턴스 생성 (DOM에 추가하지 않음)
- const tempDiv = document.createElement('div')
- tempDiv.style.display = 'none'
- document.body.appendChild(tempDiv)
-
- const instance = await WebViewer(
- {
- path: "/pdftronWeb",
- licenseKey: process.env.NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY,
- fullAPI: true,
- },
- tempDiv
- )
-
- try {
- const { Core } = instance
- const { createDocument } = Core
-
- // 템플릿 문서 생성 및 변수 치환
- const templateDoc = await createDocument(uploadedFile, {
- filename: uploadedFile.name,
- extension: 'docx',
- })
-
- // LOI 템플릿용 변수 매핑
- const mappedTemplateData = mapContractSummaryToLOITemplate(contractSummary)
-
- console.log("🔄 변수 치환 시작:", mappedTemplateData)
- await templateDoc.applyTemplateValues(mappedTemplateData as any)
- console.log("✅ 변수 치환 완료")
-
- // PDF 변환
- const fileData = await templateDoc.getFileData()
- const pdfBuffer = await (Core as any).officeToPDFBuffer(fileData, { extension: 'docx' })
-
- console.log(`✅ PDF 변환 완료: ${uploadedFile.name}`, `크기: ${pdfBuffer.byteLength} bytes`)
-
- // PDF 버퍼를 Blob URL로 변환하여 미리보기
- const pdfBlob = new Blob([pdfBuffer], { type: 'application/pdf' })
- const pdfUrl = URL.createObjectURL(pdfBlob)
- setGeneratedPdfUrl(pdfUrl)
-
- // PDF 버퍼를 상태에 저장 (최종 전송 시 사용)
- setGeneratedPdfBuffer(new Uint8Array(pdfBuffer))
-
- toast.success('PDF가 생성되었습니다.')
-
- } finally {
- // 임시 WebViewer 정리
- instance.UI.dispose()
- document.body.removeChild(tempDiv)
- }
-
- } catch (error) {
- console.error('❌ PDF 생성 실패:', error)
- toast.error('PDF 생성 중 오류가 발생했습니다.')
- } finally {
- setIsLoading(false)
- }
- }
-
- // PDF 미리보기 기능
- const openPdfPreview = async () => {
- if (!generatedPdfBuffer) {
- toast.error('생성된 PDF가 없습니다.')
- return
- }
-
- setIsLoading(true)
- try {
- // @ts-ignore
- const WebViewer = await import("@pdftron/webviewer").then(({ default: WebViewer }) => WebViewer)
-
- // 기존 인스턴스가 있다면 정리
- if (pdfViewerInstance) {
- console.log("🔄 기존 WebViewer 인스턴스 정리")
- try {
- pdfViewerInstance.UI.dispose()
- } catch (error) {
- console.warn('기존 WebViewer 정리 중 오류:', error)
- }
- setPdfViewerInstance(null)
- }
-
- // 미리보기용 컨테이너 확인
- let previewDiv = document.getElementById('pdf-preview-container')
- if (!previewDiv) {
- console.log("🔄 컨테이너 생성")
- previewDiv = document.createElement('div')
- previewDiv.id = 'pdf-preview-container'
- previewDiv.className = 'w-full h-full'
- previewDiv.style.width = '100%'
- previewDiv.style.height = '100%'
-
- // 실제 컨테이너에 추가
- const actualContainer = document.querySelector('[data-pdf-container]')
- if (actualContainer) {
- actualContainer.appendChild(previewDiv)
- }
- }
-
- console.log("🔄 WebViewer 인스턴스 생성 시작")
-
- // WebViewer 인스턴스 생성 (문서 없이)
- const instance = await Promise.race([
- WebViewer(
- {
- path: "/pdftronWeb",
- licenseKey: process.env.NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY,
- fullAPI: true,
- },
- previewDiv
- ),
- new Promise((_, reject) =>
- setTimeout(() => reject(new Error('WebViewer 초기화 타임아웃')), 30000)
- )
- ])
-
- console.log("🔄 WebViewer 인스턴스 생성 완료")
- setPdfViewerInstance(instance)
-
- // PDF 버퍼를 Blob으로 변환
- const pdfBlob = new Blob([generatedPdfBuffer], { type: 'application/pdf' })
- const pdfUrl = URL.createObjectURL(pdfBlob)
- console.log("🔄 PDF Blob URL 생성:", pdfUrl)
-
- // 문서 로드
- console.log("🔄 문서 로드 시작")
- const { documentViewer } = instance.Core
-
- // 문서 로드 이벤트 대기
- await new Promise((resolve, reject) => {
- const timeout = setTimeout(() => {
- reject(new Error('문서 로드 타임아웃'))
- }, 20000)
-
- const onDocumentLoaded = () => {
- clearTimeout(timeout)
- documentViewer.removeEventListener('documentLoaded', onDocumentLoaded)
- documentViewer.removeEventListener('documentError', onDocumentError)
- console.log("🔄 문서 로드 완료")
- resolve(true)
- }
-
- const onDocumentError = (error: any) => {
- clearTimeout(timeout)
- documentViewer.removeEventListener('documentLoaded', onDocumentLoaded)
- documentViewer.removeEventListener('documentError', onDocumentError)
- console.error('문서 로드 오류:', error)
- reject(error)
- }
-
- documentViewer.addEventListener('documentLoaded', onDocumentLoaded)
- documentViewer.addEventListener('documentError', onDocumentError)
-
- // 문서 로드 시작
- documentViewer.loadDocument(pdfUrl)
- })
-
- setIsPdfPreviewVisible(true)
- toast.success('PDF 미리보기가 준비되었습니다.')
-
- } catch (error) {
- console.error('PDF 미리보기 실패:', error)
- toast.error(`PDF 미리보기 중 오류가 발생했습니다: ${error.message}`)
- } finally {
- setIsLoading(false)
- }
- }
-
- // PDF 다운로드 기능
- const downloadPdf = () => {
- if (!generatedPdfBuffer) {
- toast.error('다운로드할 PDF가 없습니다.')
- return
- }
-
- const pdfBlob = new Blob([generatedPdfBuffer], { type: 'application/pdf' })
- const pdfUrl = URL.createObjectURL(pdfBlob)
-
- const link = document.createElement('a')
- link.href = pdfUrl
- link.download = `contract_${contractId}_${Date.now()}.pdf`
- document.body.appendChild(link)
- link.click()
- document.body.removeChild(link)
-
- URL.revokeObjectURL(pdfUrl)
- toast.success('PDF가 다운로드되었습니다.')
- }
-
- // PDF 미리보기 닫기
- const closePdfPreview = () => {
- console.log("🔄 PDF 미리보기 닫기 시작")
- if (pdfViewerInstance) {
- try {
- console.log("🔄 WebViewer 인스턴스 정리")
- pdfViewerInstance.UI.dispose()
- } catch (error) {
- console.warn('WebViewer 정리 중 오류:', error)
- }
- setPdfViewerInstance(null)
- }
-
- // 컨테이너 정리
- const previewDiv = document.getElementById('pdf-preview-container')
- if (previewDiv) {
- try {
- previewDiv.innerHTML = ''
- } catch (error) {
- console.warn('컨테이너 정리 중 오류:', error)
- }
- }
-
- setIsPdfPreviewVisible(false)
- console.log("🔄 PDF 미리보기 닫기 완료")
- }
-
- // 최종 전송
- const handleFinalSubmit = async () => {
- if (!generatedPdfUrl || !contractSummary || !generatedPdfBuffer) {
- toast.error('생성된 PDF가 필요합니다.')
- return
- }
-
- if (!userId) {
- toast.error('로그인이 필요합니다.')
- return
- }
-
- setIsLoading(true)
- try {
- // 기본계약서 생성 (최종 전송 시점에)
- let generatedBasicContractPdfs: Array<{ key: string; buffer: number[]; fileName: string }> = [];
-
- const contractsToGenerate = selectedBasicContracts.filter(c => c.checked);
- if (contractsToGenerate.length > 0) {
- // vendorId 조회
- let vendorId: number | undefined;
- try {
- const basicInfoData = await getBasicInfo(contractId);
- if (basicInfoData && basicInfoData.success && basicInfoData.data) {
- vendorId = basicInfoData.data.vendorId;
- }
- } catch (error) {
- console.error('vendorId 조회 실패:', error);
- }
-
- if (vendorId) {
- toast.info('기본계약서를 생성하는 중입니다...');
-
- for (const contract of contractsToGenerate) {
- try {
- const pdf = await generateBasicContractPdf(vendorId, contract.type, contract.templateName);
- generatedBasicContractPdfs.push({
- key: `${vendorId}_${contract.type}_${contract.templateName}`,
- ...pdf
- });
- } catch (error) {
- console.error(`${contract.type} 계약서 생성 실패:`, error);
- // 개별 실패는 전체를 중단하지 않음
- }
- }
-
- if (generatedBasicContractPdfs.length > 0) {
- toast.success(`${generatedBasicContractPdfs.length}개의 기본계약서가 생성되었습니다.`);
- }
- }
- }
-
- // 서버액션을 사용하여 계약승인요청 전송
- const result = await sendContractApprovalRequest(
- contractSummary,
- generatedPdfBuffer,
- 'contractDocument',
- userId,
- generatedBasicContractPdfs
- )
-
- if (result.success) {
- toast.success('계약승인요청이 전송되었습니다.')
- onOpenChange(false)
- } else {
- // 서버에서 이미 처리된 에러 메시지 표시
- toast.error(result.error || '계약승인요청 전송 실패')
- return
- }
- } catch (error: any) {
- console.error('Error submitting approval request:', error)
-
- // 데이터베이스 중복 키 오류 처리
- if (error.message && error.message.includes('duplicate key value violates unique constraint')) {
- toast.error('이미 존재하는 계약번호입니다. 다른 계약번호를 사용해주세요.')
- return
- }
-
- // 다른 오류에 대한 일반적인 처리
- toast.error('계약승인요청 전송 중 오류가 발생했습니다.')
- } finally {
- setIsLoading(false)
- }
- }
-
- // 다이얼로그가 열릴 때 1단계 데이터 수집
- useEffect(() => {
- if (open && currentStep === 1) {
- collectContractSummary()
- }
- }, [open, currentStep, collectContractSummary])
-
- // 계약 요약이 준비되면 기본계약 초기화
- useEffect(() => {
- if (contractSummary && currentStep === 2) {
- const loadBasicContracts = async () => {
- await initializeBasicContracts()
- }
- loadBasicContracts()
- }
- }, [contractSummary, currentStep, initializeBasicContracts])
-
- // 다이얼로그가 닫힐 때 PDF 뷰어 정리
- useEffect(() => {
- if (!open) {
- closePdfPreview()
- }
- }, [open])
-
-
- return (
- <Dialog open={open} onOpenChange={onOpenChange}>
- <DialogContent className="max-w-6xl max-h-[90vh] overflow-y-auto">
- <DialogHeader>
- <DialogTitle className="flex items-center gap-2">
- <FileText className="h-5 w-5" />
- 계약승인요청
- </DialogTitle>
- </DialogHeader>
-
- <Tabs value={currentStep.toString()} className="w-full">
- <TabsList className="grid w-full grid-cols-4">
- <TabsTrigger value="1" disabled={currentStep < 1}>
- 1. 계약 현황 정리
- </TabsTrigger>
- <TabsTrigger value="2" disabled={currentStep < 2}>
- 2. 기본계약 체크
- </TabsTrigger>
- <TabsTrigger value="3" disabled={currentStep < 3}>
- 3. 문서 업로드
- </TabsTrigger>
- <TabsTrigger value="4" disabled={currentStep < 4}>
- 4. PDF 미리보기
- </TabsTrigger>
- </TabsList>
-
- {/* 1단계: 계약 현황 정리 */}
- <TabsContent value="1" className="space-y-4">
- <Card>
- <CardHeader>
- <CardTitle className="flex items-center gap-2">
- <CheckCircle className="h-5 w-5 text-green-600" />
- 작성된 계약 현황
- </CardTitle>
- </CardHeader>
- <CardContent className="space-y-4">
- {isLoading ? (
- <div className="text-center py-4">
- <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
- <p className="mt-2 text-sm text-muted-foreground">계약 정보를 수집하는 중...</p>
- </div>
- ) : (
- <div className="space-y-4">
- {/* 기본 정보 (필수) */}
- <div className="border rounded-lg p-4">
- <div className="flex items-center gap-2 mb-3">
- <CheckCircle className="h-4 w-4 text-green-600" />
- <Label className="font-medium">기본 정보</Label>
- <Badge variant="secondary">필수</Badge>
- </div>
- <div className="grid grid-cols-2 gap-4 text-sm">
- <div>
- <span className="font-medium">계약번호:</span> {String(contractSummary?.basicInfo?.contractNumber || '')}
- </div>
- <div>
- <span className="font-medium">계약명:</span> {String(contractSummary?.basicInfo?.contractName || '')}
- </div>
- <div>
- <span className="font-medium">벤더:</span> {String(contractSummary?.basicInfo?.vendorName || '')}
- </div>
- <div>
- <span className="font-medium">프로젝트:</span> {String(contractSummary?.basicInfo?.projectName || '')}
- </div>
- <div>
- <span className="font-medium">계약유형:</span> {String(contractSummary?.basicInfo?.contractType || '')}
- </div>
- <div>
- <span className="font-medium">계약상태:</span> {String(contractSummary?.basicInfo?.contractStatus || '')}
- </div>
- <div>
- <span className="font-medium">계약금액:</span> {String(contractSummary?.basicInfo?.contractAmount || '')} {String(contractSummary?.basicInfo?.currency || '')}
- </div>
- <div>
- <span className="font-medium">계약기간:</span> {String(contractSummary?.basicInfo?.startDate || '')} ~ {String(contractSummary?.basicInfo?.endDate || '')}
- </div>
- <div>
- <span className="font-medium">사양서 유형:</span> {String(contractSummary?.basicInfo?.specificationType || '')}
- </div>
- <div>
- <span className="font-medium">단가 유형:</span> {String(contractSummary?.basicInfo?.unitPriceType || '')}
- </div>
- <div>
- <span className="font-medium">연결 PO번호:</span> {String(contractSummary?.basicInfo?.linkedPoNumber || '')}
- </div>
- <div>
- <span className="font-medium">연결 입찰번호:</span> {String(contractSummary?.basicInfo?.linkedBidNumber || '')}
- </div>
- </div>
- </div>
-
- {/* 지급/인도 조건 */}
- <div className="border rounded-lg p-4">
- <div className="flex items-center gap-2 mb-3">
- <CheckCircle className="h-4 w-4 text-green-600" />
- <Label className="font-medium">지급/인도 조건</Label>
- <Badge variant="secondary">필수</Badge>
- </div>
- <div className="grid grid-cols-2 gap-4 text-sm">
- <div>
- <span className="font-medium">지급조건:</span> {String(contractSummary?.basicInfo?.paymentTerm || '')}
- </div>
- <div>
- <span className="font-medium">세금 유형:</span> {String(contractSummary?.basicInfo?.taxType || '')}
- </div>
- <div>
- <span className="font-medium">인도조건:</span> {String(contractSummary?.basicInfo?.deliveryTerm || '')}
- </div>
- <div>
- <span className="font-medium">인도유형:</span> {String(contractSummary?.basicInfo?.deliveryType || '')}
- </div>
- <div>
- <span className="font-medium">선적지:</span> {String(contractSummary?.basicInfo?.shippingLocation || '')}
- </div>
- <div>
- <span className="font-medium">하역지:</span> {String(contractSummary?.basicInfo?.dischargeLocation || '')}
- </div>
- <div>
- <span className="font-medium">계약납기:</span> {String(contractSummary?.basicInfo?.contractDeliveryDate || '')}
- </div>
- <div>
- <span className="font-medium">위약금:</span> {contractSummary?.basicInfo?.liquidatedDamages ? '적용' : '미적용'}
- </div>
- </div>
- </div>
-
- {/* 추가 조건 */}
- <div className="border rounded-lg p-4">
- <div className="flex items-center gap-2 mb-3">
- <CheckCircle className="h-4 w-4 text-green-600" />
- <Label className="font-medium">추가 조건</Label>
- <Badge variant="secondary">필수</Badge>
- </div>
- <div className="grid grid-cols-2 gap-4 text-sm">
- <div>
- <span className="font-medium">연동제 정보:</span> {String(contractSummary?.basicInfo?.interlockingSystem || '')}
- </div>
- <div>
- <span className="font-medium">계약성립조건:</span>
- {contractSummary?.basicInfo?.contractEstablishmentConditions &&
- Object.entries(contractSummary.basicInfo.contractEstablishmentConditions)
- .filter(([, value]) => value === true)
- .map(([key]) => key)
- .join(', ') || '없음'}
- </div>
- <div>
- <span className="font-medium">계약해지조건:</span>
- {contractSummary?.basicInfo?.contractTerminationConditions &&
- Object.entries(contractSummary.basicInfo.contractTerminationConditions)
- .filter(([, value]) => value === true)
- .map(([key]) => key)
- .join(', ') || '없음'}
- </div>
- </div>
- </div>
-
- {/* 품목 정보 */}
- <div className="border rounded-lg p-4">
- <div className="flex items-center gap-2 mb-3">
- <Checkbox
- id="items-enabled"
- checked={contractSummary?.items && contractSummary.items.length > 0}
- disabled
- />
- <Label htmlFor="items-enabled" className="font-medium">품목 정보</Label>
- <Badge variant="outline">선택</Badge>
- </div>
- {contractSummary?.items && contractSummary.items.length > 0 ? (
- <div className="space-y-2">
- <p className="text-sm text-muted-foreground">
- 총 {contractSummary.items.length}개 품목이 입력되어 있습니다.
- </p>
- <div className="max-h-32 overflow-y-auto">
- {contractSummary.items.slice(0, 3).map((item: Record<string, unknown>, index: number) => (
- <div key={index} className="text-xs bg-gray-50 p-2 rounded">
- <div className="font-medium">{item.itemInfo || item.description || `품목 ${index + 1}`}</div>
- <div className="text-muted-foreground">
- 수량: {item.quantity || 0} | 단가: {item.contractUnitPrice || item.unitPrice || 0}
- </div>
- </div>
- ))}
- {contractSummary.items.length > 3 && (
- <div className="text-xs text-muted-foreground text-center">
- ... 외 {contractSummary.items.length - 3}개 품목
- </div>
- )}
- </div>
- </div>
- ) : (
- <p className="text-sm text-muted-foreground">
- 품목 정보가 입력되지 않았습니다.
- </p>
- )}
- </div>
-
- {/* 커뮤니케이션 채널 */}
- <div className="border rounded-lg p-4">
- <div className="flex items-center gap-2 mb-3">
- <Checkbox
- id="communication-enabled"
- checked={!!contractSummary?.communicationChannel}
- disabled
- />
- <Label htmlFor="communication-enabled" className="font-medium">
- 커뮤니케이션 채널
- </Label>
- <Badge variant="outline">선택</Badge>
- </div>
- <p className="text-sm text-muted-foreground">
- {contractSummary?.communicationChannel
- ? '정보가 입력되어 있습니다.'
- : '정보가 입력되지 않았습니다.'}
- </p>
- </div>
-
- {/* 위치 정보 */}
- <div className="border rounded-lg p-4">
- <div className="flex items-center gap-2 mb-3">
- <Checkbox
- id="location-enabled"
- checked={!!contractSummary?.location}
- disabled
- />
- <Label htmlFor="location-enabled" className="font-medium">
- 위치 정보
- </Label>
- <Badge variant="outline">선택</Badge>
- </div>
- <p className="text-sm text-muted-foreground">
- {contractSummary?.location
- ? '정보가 입력되어 있습니다.'
- : '정보가 입력되지 않았습니다.'}
- </p>
- </div>
-
- {/* 현장 서비스 요율 */}
- <div className="border rounded-lg p-4">
- <div className="flex items-center gap-2 mb-3">
- <Checkbox
- id="fieldService-enabled"
- checked={!!contractSummary?.fieldServiceRate}
- disabled
- />
- <Label htmlFor="fieldService-enabled" className="font-medium">
- 현장 서비스 요율
- </Label>
- <Badge variant="outline">선택</Badge>
- </div>
- <p className="text-sm text-muted-foreground">
- {contractSummary?.fieldServiceRate
- ? '정보가 입력되어 있습니다.'
- : '정보가 입력되지 않았습니다.'}
- </p>
- </div>
-
- {/* 오프셋 세부사항 */}
- <div className="border rounded-lg p-4">
- <div className="flex items-center gap-2 mb-3">
- <Checkbox
- id="offset-enabled"
- checked={!!contractSummary?.offsetDetails}
- disabled
- />
- <Label htmlFor="offset-enabled" className="font-medium">
- 오프셋 세부사항
- </Label>
- <Badge variant="outline">선택</Badge>
- </div>
- <p className="text-sm text-muted-foreground">
- {contractSummary?.offsetDetails
- ? '정보가 입력되어 있습니다.'
- : '정보가 입력되지 않았습니다.'}
- </p>
- </div>
-
- {/* 하도급 체크리스트 */}
- <div className="border rounded-lg p-4">
- <div className="flex items-center gap-2 mb-3">
- <Checkbox
- id="subcontract-enabled"
- checked={!!contractSummary?.subcontractChecklist}
- disabled
- />
- <Label htmlFor="subcontract-enabled" className="font-medium">
- 하도급 체크리스트
- </Label>
- <Badge variant="outline">선택</Badge>
- </div>
- <p className="text-sm text-muted-foreground">
- {contractSummary?.subcontractChecklist
- ? '정보가 입력되어 있습니다.'
- : '정보가 입력되지 않았습니다.'}
- </p>
- </div>
- </div>
- )}
- </CardContent>
- </Card>
-
- <div className="flex justify-end">
- <Button
- onClick={() => setCurrentStep(2)}
- disabled={isLoading}
- >
- 다음 단계
- </Button>
- </div>
- </TabsContent>
-
- {/* 2단계: 기본계약 체크 */}
- <TabsContent value="2" className="space-y-4">
- <Card>
- <CardHeader>
- <CardTitle className="flex items-center gap-2">
- <FileText className="h-5 w-5 text-blue-600" />
- 기본계약서 선택
- </CardTitle>
- <p className="text-sm text-muted-foreground">
- 벤더에게 발송할 기본계약서를 선택해주세요. (템플릿이 있는 계약서만 선택 가능합니다.)
- </p>
- </CardHeader>
- <CardContent className="space-y-4">
- {isLoadingBasicContracts ? (
- <div className="text-center py-8">
- <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
- <p className="mt-2 text-sm text-muted-foreground">기본계약 템플릿을 불러오는 중...</p>
- </div>
- ) : (
- <div className="space-y-4">
- {selectedBasicContracts.length > 0 ? (
- <div className="space-y-3">
- <div className="flex items-center justify-between">
- <h4 className="font-medium">필요한 기본계약서</h4>
- <Badge variant="outline">
- {selectedBasicContracts.filter(c => c.checked).length}개 선택됨
- </Badge>
- </div>
-
- <div className="grid gap-3">
- {selectedBasicContracts.map((contract) => (
- <div
- key={contract.type}
- className="flex items-center justify-between p-3 border rounded-lg hover:bg-muted/50"
- >
- <div className="flex items-center gap-3">
- <Checkbox
- id={`contract-${contract.type}`}
- checked={contract.checked}
- onCheckedChange={() => toggleBasicContract(contract.type)}
- />
- <div>
- <Label
- htmlFor={`contract-${contract.type}`}
- className="font-medium cursor-pointer"
- >
- {contract.type}
- </Label>
- <p className="text-sm text-muted-foreground">
- 템플릿: {contract.templateName}
- </p>
- </div>
- </div>
- <Badge
- variant="secondary"
- className="text-xs"
- >
- {contract.checked ? "선택됨" : "미선택"}
- </Badge>
- </div>
- ))}
- </div>
-
- </div>
- ) : (
- <div className="text-center py-8 text-muted-foreground">
- <FileText className="h-12 w-12 mx-auto mb-4 opacity-50" />
- <p>기본계약서 목록을 불러올 수 없습니다.</p>
- <p className="text-sm">잠시 후 다시 시도해주세요.</p>
- </div>
- )}
-
- </div>
- )}
- </CardContent>
- </Card>
-
- <div className="flex justify-between">
- <Button variant="outline" onClick={() => setCurrentStep(1)}>
- 이전 단계
- </Button>
- <Button
- onClick={() => setCurrentStep(3)}
- disabled={isLoadingBasicContracts}
- >
- 다음 단계
- </Button>
- </div>
- </TabsContent>
-
- {/* 3단계: 문서 업로드 */}
- <TabsContent value="3" className="space-y-4">
- <Card>
- <CardHeader>
- <CardTitle className="flex items-center gap-2">
- <Upload className="h-5 w-5 text-blue-600" />
- 계약서 업로드
- </CardTitle>
- </CardHeader>
- <CardContent className="space-y-4">
- <div className="space-y-4">
- <p className="text-lg text-muted-foreground">일반계약 표준문서 관리 페이지에 접속하여, 원하는 양식의 계약서를 다운받아 수정 후 업로드하세요.</p>
- <div>
- <Label htmlFor="file-upload">파일 업로드</Label>
- <Input
- id="file-upload"
- type="file"
- accept=".doc,.docx"
- onChange={(e) => {
- const file = e.target.files?.[0]
- if (file) handleFileUpload(file)
- }}
- />
- <p className="text-sm text-muted-foreground mt-1">
- Word 문서(.doc, .docx) 파일만 업로드 가능합니다.
- </p>
- </div>
-
- {/* ContractDocuments 컴포넌트 사용 */}
- {/* <div className="mt-4">
- <Label>업로드된 문서</Label>
- <ContractDocuments
- contractId={contractId}
- userId={userId}
- readOnly={false}
- />
- </div> */}
-
- {uploadedFile && (
- <div className="border rounded-lg p-4 bg-green-50">
- <div className="flex items-center gap-2">
- <CheckCircle className="h-4 w-4 text-green-600" />
- <span className="font-medium text-green-900">업로드 완료</span>
- </div>
- <p className="text-sm text-green-800 mt-1">{uploadedFile.name}</p>
- </div>
- )}
- </div>
- </CardContent>
- </Card>
-
- <div className="flex justify-between">
- <Button variant="outline" onClick={() => setCurrentStep(2)}>
- 이전 단계
- </Button>
- <Button
- onClick={() => setCurrentStep(4)}
- disabled={!uploadedFile}
- >
- 다음 단계
- </Button>
- </div>
- </TabsContent>
-
- {/* 4단계: PDF 미리보기 */}
- <TabsContent value="4" className="space-y-4">
- <Card>
- <CardHeader>
- <CardTitle className="flex items-center gap-2">
- <Eye className="h-5 w-5 text-purple-600" />
- PDF 미리보기
- </CardTitle>
- </CardHeader>
- <CardContent className="space-y-4">
- {!generatedPdfUrl ? (
- <div className="text-center py-8">
- <Button onClick={generatePdf} disabled={isLoading}>
- {isLoading ? 'PDF 생성 중...' : 'PDF 생성하기'}
- </Button>
- </div>
- ) : (
- <div className="space-y-4">
- <div className="border rounded-lg p-4 bg-green-50">
- <div className="flex items-center gap-2">
- <CheckCircle className="h-4 w-4 text-green-600" />
- <span className="font-medium text-green-900">PDF 생성 완료</span>
- </div>
- </div>
-
- <div className="border rounded-lg p-4">
- <div className="flex items-center justify-between mb-4">
- <h4 className="font-medium">생성된 PDF</h4>
- <div className="flex gap-2">
- <Button
- variant="outline"
- size="sm"
- onClick={downloadPdf}
- disabled={isLoading}
- >
- <Download className="h-4 w-4 mr-2" />
- 다운로드
- </Button>
- <Button
- variant="outline"
- size="sm"
- onClick={openPdfPreview}
- disabled={isLoading}
- >
- <Eye className="h-4 w-4 mr-2" />
- 미리보기
- </Button>
- </div>
- </div>
-
- {/* PDF 미리보기 영역 */}
- <div className="border rounded-lg h-96 bg-gray-50 relative" data-pdf-container>
- {isPdfPreviewVisible ? (
- <>
- <div className="absolute top-2 right-2 z-10">
- <Button
- variant="outline"
- size="sm"
- onClick={closePdfPreview}
- className="bg-white/90 hover:bg-white"
- >
- ✕ 닫기
- </Button>
- </div>
- <div id="pdf-preview-container" className="w-full h-full" />
- </>
- ) : (
- <div className="flex items-center justify-center h-full">
- <div className="text-center text-muted-foreground">
- <FileText className="h-12 w-12 mx-auto mb-2" />
- <p>미리보기 버튼을 클릭하여 PDF를 확인하세요</p>
- </div>
- </div>
- )}
- </div>
- </div>
- </div>
- )}
- </CardContent>
- </Card>
-
- <div className="flex justify-between">
- <Button variant="outline" onClick={() => setCurrentStep(3)}>
- 이전 단계
- </Button>
- <Button
- onClick={handleFinalSubmit}
- disabled={!generatedPdfUrl || isLoading}
- className="bg-green-600 hover:bg-green-700"
- >
- <Send className="h-4 w-4 mr-2" />
- {isLoading ? '전송 중...' : '최종 전송'}
- </Button>
- </div>
- </TabsContent>
- </Tabs>
- </DialogContent>
- </Dialog>
- )} \ No newline at end of file
diff --git a/lib/general-contracts_old/detail/general-contract-basic-info.tsx b/lib/general-contracts_old/detail/general-contract-basic-info.tsx
deleted file mode 100644
index d891fe63..00000000
--- a/lib/general-contracts_old/detail/general-contract-basic-info.tsx
+++ /dev/null
@@ -1,1250 +0,0 @@
-'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'
-
-interface ContractBasicInfoProps {
- contractId: number
-}
-
-export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) {
- const session = useSession()
- const [isLoading, setIsLoading] = useState(false)
- const [contract, setContract] = useState<GeneralContract | null>(null)
- const userId = session.data?.user?.id ? Number(session.data.user.id) : null
-
- // 독립적인 상태 관리
- const [paymentDeliveryPercent, setPaymentDeliveryPercent] = useState('')
-
- // Procurement 데이터 상태들
- const [paymentTermsOptions, setPaymentTermsOptions] = useState<Array<{code: string, description: string}>>([])
- const [incotermsOptions, setIncotermsOptions] = useState<Array<{code: string, description: string}>>([])
- const [shippingPlaces, setShippingPlaces] = useState<Array<{code: string, description: string}>>([])
- const [destinationPlaces, setDestinationPlaces] = useState<Array<{code: string, description: string}>>([])
- const [procurementLoading, setProcurementLoading] = useState(false)
-
- const [formData, setFormData] = useState({
- 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,
- },
- })
-
- const [errors] = useState<Record<string, string>>({})
-
- // 계약 데이터 로드
- 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)
-
- setFormData({
- 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: contractData?.deliveryTerm || '',
- shippingLocation: contractData?.shippingLocation || '',
- dischargeLocation: contractData?.dischargeLocation || '',
- contractDeliveryDate: contractData?.contractDeliveryDate || '',
- 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,
- },
- })
- } catch (error) {
- console.error('Error loading contract:', error)
- toast.error('계약 정보를 불러오는 중 오류가 발생했습니다.')
- }
- }
-
- if (contractId) {
- loadContract()
- }
- }, [contractId])
-
- // 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.specificationType) 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 (
- <Card className="w-full">
- <CardHeader>
- <CardTitle>계약 기본 정보</CardTitle>
- </CardHeader>
- <CardContent>
- <Tabs defaultValue="basic" className="w-full">
- <TabsList className="grid w-full grid-cols-4 h-auto overflow-x-auto">
- <TabsTrigger value="basic" className="text-xs px-2 py-2 whitespace-nowrap">기본 정보</TabsTrigger>
- <TabsTrigger value="conditions" className="text-xs px-2 py-2 whitespace-nowrap">지급/인도 조건</TabsTrigger>
- <TabsTrigger value="additional" className="text-xs px-2 py-2 whitespace-nowrap">추가 조건</TabsTrigger>
- <TabsTrigger value="documents" className="text-xs px-2 py-2 whitespace-nowrap">계약첨부문서</TabsTrigger>
- </TabsList>
-
- {/* 기본 정보 탭 */}
- <TabsContent value="basic" className="space-y-6">
- <Card>
- {/* 보증기간 및 단가유형 */}
- <CardHeader>
- <CardTitle>보증기간 및 단가유형</CardTitle>
- </CardHeader>
- <CardContent className="space-y-4">
- {/* 3그리드: 보증기간, 사양, 단가 */}
- <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
- {/* 보증기간 */}
- <div className="flex flex-col gap-2">
- <Label htmlFor="warrantyPeriod">품질/하자 보증기간</Label>
- <div className="space-y-3">
- <div className="flex items-center space-x-2">
- <input
- type="checkbox"
- id="warrantyAfterDelivery"
- checked={formData.warrantyPeriod.납품후?.enabled || false}
- onChange={(e) => setFormData(prev => ({
- ...prev,
- warrantyPeriod: {
- ...prev.warrantyPeriod,
- 납품후: {
- ...prev.warrantyPeriod.납품후,
- enabled: e.target.checked
- }
- }
- }))}
- className="rounded"
- />
- <Label htmlFor="warrantyAfterDelivery" className="text-sm">납품 후</Label>
- </div>
- {formData.warrantyPeriod.납품후?.enabled && (
- <div className="ml-6 flex items-center space-x-2">
- <Input
- type="number"
- placeholder="보증기간"
- value={formData.warrantyPeriod.납품후?.period || ''}
- onChange={(e) => setFormData(prev => ({
- ...prev,
- warrantyPeriod: {
- ...prev.warrantyPeriod,
- 납품후: {
- ...prev.warrantyPeriod.납품후,
- period: parseInt(e.target.value) || 0
- }
- }
- }))}
- className="w-20 h-8 text-sm"
- />
- <span className="text-xs text-muted-foreground">개월, 최대</span>
- <Input
- type="number"
- placeholder="최대"
- value={formData.warrantyPeriod.납품후?.maxPeriod || ''}
- onChange={(e) => setFormData(prev => ({
- ...prev,
- warrantyPeriod: {
- ...prev.warrantyPeriod,
- 납품후: {
- ...prev.warrantyPeriod.납품후,
- maxPeriod: parseInt(e.target.value) || 0
- }
- }
- }))}
- className="w-20 h-8 text-sm"
- />
- <span className="text-xs text-muted-foreground">개월</span>
- </div>
- )}
-
- <div className="flex items-center space-x-2">
- <input
- type="checkbox"
- id="warrantyAfterHandover"
- checked={formData.warrantyPeriod.인도후?.enabled || false}
- onChange={(e) => setFormData(prev => ({
- ...prev,
- warrantyPeriod: {
- ...prev.warrantyPeriod,
- 인도후: {
- ...prev.warrantyPeriod.인도후,
- enabled: e.target.checked
- }
- }
- }))}
- className="rounded"
- />
- <Label htmlFor="warrantyAfterHandover" className="text-sm">인도 후</Label>
- </div>
- {formData.warrantyPeriod.인도후?.enabled && (
- <div className="ml-6 flex items-center space-x-2">
- <Input
- type="number"
- placeholder="보증기간"
- value={formData.warrantyPeriod.인도후?.period || ''}
- onChange={(e) => setFormData(prev => ({
- ...prev,
- warrantyPeriod: {
- ...prev.warrantyPeriod,
- 인도후: {
- ...prev.warrantyPeriod.인도후,
- period: parseInt(e.target.value) || 0
- }
- }
- }))}
- className="w-20 h-8 text-sm"
- />
- <span className="text-xs text-muted-foreground">개월, 최대</span>
- <Input
- type="number"
- placeholder="최대"
- value={formData.warrantyPeriod.인도후?.maxPeriod || ''}
- onChange={(e) => setFormData(prev => ({
- ...prev,
- warrantyPeriod: {
- ...prev.warrantyPeriod,
- 인도후: {
- ...prev.warrantyPeriod.인도후,
- maxPeriod: parseInt(e.target.value) || 0
- }
- }
- }))}
- className="w-20 h-8 text-sm"
- />
- <span className="text-xs text-muted-foreground">개월</span>
- </div>
- )}
-
- <div className="flex items-center space-x-2">
- <input
- type="checkbox"
- id="warrantyAfterWork"
- checked={formData.warrantyPeriod.작업후?.enabled || false}
- onChange={(e) => setFormData(prev => ({
- ...prev,
- warrantyPeriod: {
- ...prev.warrantyPeriod,
- 작업후: {
- ...prev.warrantyPeriod.작업후,
- enabled: e.target.checked
- }
- }
- }))}
- className="rounded"
- />
- <Label htmlFor="warrantyAfterWork" className="text-sm">작업 후</Label>
- </div>
- {formData.warrantyPeriod.작업후?.enabled && (
- <div className="ml-6 flex items-center space-x-2">
- <Input
- type="number"
- placeholder="보증기간"
- value={formData.warrantyPeriod.작업후?.period || ''}
- onChange={(e) => setFormData(prev => ({
- ...prev,
- warrantyPeriod: {
- ...prev.warrantyPeriod,
- 작업후: {
- ...prev.warrantyPeriod.작업후,
- period: parseInt(e.target.value) || 0
- }
- }
- }))}
- className="w-20 h-8 text-sm"
- />
- <span className="text-xs text-muted-foreground">개월, 최대</span>
- <Input
- type="number"
- placeholder="최대"
- value={formData.warrantyPeriod.작업후?.maxPeriod || ''}
- onChange={(e) => setFormData(prev => ({
- ...prev,
- warrantyPeriod: {
- ...prev.warrantyPeriod,
- 작업후: {
- ...prev.warrantyPeriod.작업후,
- maxPeriod: parseInt(e.target.value) || 0
- }
- }
- }))}
- className="w-20 h-8 text-sm"
- />
- <span className="text-xs text-muted-foreground">개월</span>
- </div>
- )}
-
- <div className="flex items-center space-x-2">
- <input
- type="checkbox"
- id="warrantyOther"
- checked={formData.warrantyPeriod.기타?.enabled || false}
- onChange={(e) => setFormData(prev => ({
- ...prev,
- warrantyPeriod: {
- ...prev.warrantyPeriod,
- 기타: {
- ...prev.warrantyPeriod.기타,
- enabled: e.target.checked
- }
- }
- }))}
- className="rounded"
- />
- <Label htmlFor="warrantyOther" className="text-sm">기타/미적용</Label>
- </div>
- </div>
- </div>
- {/* 사양 */}
- <div className="flex flex-col gap-2">
- <Label htmlFor="specificationType">사양 <span className="text-red-600">*</span></Label>
- <Select value={formData.specificationType} onValueChange={(value) => setFormData(prev => ({ ...prev, specificationType: value }))}>
- <SelectTrigger className={errors.specificationType ? 'border-red-500' : ''}>
- <SelectValue placeholder="사양을 선택하세요" />
- </SelectTrigger>
- <SelectContent>
- <SelectItem value="첨부파일">첨부파일</SelectItem>
- <SelectItem value="표준사양">표준사양</SelectItem>
- <SelectItem value="수기사양">수기사양</SelectItem>
- </SelectContent>
- </Select>
- {errors.specificationType && (
- <p className="text-sm text-red-600">사양은 필수값입니다.</p>
- )}
- </div>
- {/* 단가 */}
- <div className="flex flex-col gap-2">
- <Label htmlFor="unitPriceType">단가 유형</Label>
- <Select value={formData.unitPriceType} onValueChange={(value) => setFormData(prev => ({ ...prev, unitPriceType: value }))}>
- <SelectTrigger>
- <SelectValue placeholder="단가 유형을 선택하세요" />
- </SelectTrigger>
- <SelectContent>
- <SelectItem value="자재개별단가">자재개별단가</SelectItem>
- <SelectItem value="서비스용역단가">서비스용역단가</SelectItem>
- <SelectItem value="프로젝트단가">프로젝트단가</SelectItem>
- <SelectItem value="지역별단가">지역별단가</SelectItem>
- <SelectItem value="직무직급단가">직무직급단가</SelectItem>
- <SelectItem value="단계별단가">단계별단가</SelectItem>
- <SelectItem value="기타">기타</SelectItem>
- </SelectContent>
- </Select>
- </div>
- {/* 선택에 따른 폼: vertical로 출력 */}
-
-
- {/* 사양이 수기사양일 때 매뉴얼 텍스트 */}
- {formData.specificationType === '수기사양' && (
- <div className="flex flex-col gap-2">
- <Label htmlFor="specificationManualText">사양 매뉴얼 텍스트</Label>
- <Textarea
- value={formData.specificationManualText}
- onChange={(e) => setFormData(prev => ({ ...prev, specificationManualText: e.target.value }))}
- placeholder="사양 매뉴얼 텍스트를 입력하세요"
- rows={3}
- />
- </div>
- )}
-
- </div>
-
-
- </CardContent>
- </Card>
- </TabsContent>
-
- {/* 지급/인도 조건 탭 */}
- <TabsContent value="conditions" className="space-y-6">
- <Card>
- <CardHeader>
- <CardTitle>Payment & Delivery Conditions (지급/인도 조건)</CardTitle>
- </CardHeader>
- <CardContent className="space-y-6">
- <div className="grid grid-cols-5 gap-6">
- {/* 납품 전 지급조건 */}
- <div className="space-y-4">
- <Label className="text-base font-medium">납품 전</Label>
- <div className="space-y-3">
- <div className="flex items-center space-x-2">
- <input
- type="checkbox"
- id="apBond"
- checked={formData.paymentBeforeDelivery.apBond || false}
- onChange={(e) => setFormData(prev => ({
- ...prev,
- paymentBeforeDelivery: {
- ...prev.paymentBeforeDelivery,
- apBond: e.target.checked
- }
- }))}
- className="rounded"
- />
- <Label htmlFor="apBond" className="text-sm">AP Bond & Performance Bond</Label>
- <Input
- type="number"
- min="0"
- placeholder="%"
- className="w-16"
- value={formData.paymentBeforeDelivery.apBondPercent || ''}
- onChange={(e) => setFormData(prev => ({
- ...prev,
- paymentBeforeDelivery: {
- ...prev.paymentBeforeDelivery,
- apBondPercent: e.target.value
- }
- }))}
- disabled={!formData.paymentBeforeDelivery.apBond}
- />
- </div>
- <div className="flex items-center space-x-2">
- <input
- type="checkbox"
- id="drawingSubmission"
- checked={formData.paymentBeforeDelivery.drawingSubmission || false}
- onChange={(e) => setFormData(prev => ({
- ...prev,
- paymentBeforeDelivery: {
- ...prev.paymentBeforeDelivery,
- drawingSubmission: e.target.checked
- }
- }))}
- className="rounded"
- />
- <Label htmlFor="drawingSubmission" className="text-sm">도면제출</Label>
- <Input
- type="number"
- min="0"
- placeholder="%"
- className="w-16"
- value={formData.paymentBeforeDelivery.drawingSubmissionPercent || ''}
- onChange={(e) => setFormData(prev => ({
- ...prev,
- paymentBeforeDelivery: {
- ...prev.paymentBeforeDelivery,
- drawingSubmissionPercent: e.target.value
- }
- }))}
- disabled={!formData.paymentBeforeDelivery.drawingSubmission}
- />
- </div>
- <div className="flex items-center space-x-2">
- <input
- type="checkbox"
- id="materialPurchase"
- checked={formData.paymentBeforeDelivery.materialPurchase || false}
- onChange={(e) => setFormData(prev => ({
- ...prev,
- paymentBeforeDelivery: {
- ...prev.paymentBeforeDelivery,
- materialPurchase: e.target.checked
- }
- }))}
- className="rounded"
- />
- <Label htmlFor="materialPurchase" className="text-sm">소재구매 문서</Label>
- <Input
- type="number"
- min="0"
- placeholder="%"
- className="w-16"
- value={formData.paymentBeforeDelivery.materialPurchasePercent || ''}
- onChange={(e) => setFormData(prev => ({
- ...prev,
- paymentBeforeDelivery: {
- ...prev.paymentBeforeDelivery,
- materialPurchasePercent: e.target.value
- }
- }))}
- disabled={!formData.paymentBeforeDelivery.materialPurchase}
- />
- </div>
- </div>
- </div>
-
- {/* 납품 지급조건 */}
- <div className="space-y-4">
- <Label className="text-base font-medium">납품</Label>
- <div className="space-y-3">
- <div className="space-y-2">
- <Label htmlFor="paymentDelivery">납품 지급조건 <span className="text-red-600">*</span></Label>
- <Select value={formData.paymentDelivery} onValueChange={(value) => setFormData(prev => ({ ...prev, paymentDelivery: value }))}>
- <SelectTrigger className={errors.paymentDelivery ? 'border-red-500' : ''}>
- <SelectValue placeholder="납품 지급조건을 선택하세요" />
- </SelectTrigger>
- <SelectContent>
- <SelectItem value="L/C">L/C</SelectItem>
- <SelectItem value="T/T">T/T</SelectItem>
- <SelectItem value="거래명세서 기반 정기지급조건">거래명세서 기반 정기지급조건</SelectItem>
- <SelectItem value="작업 및 입고 검사 완료">작업 및 입고 검사 완료</SelectItem>
- <SelectItem value="청구내역서 제출 및 승인">청구내역서 제출 및 승인</SelectItem>
- <SelectItem value="정규금액 월 단위 정산(지정일 지급)">정규금액 월 단위 정산(지정일 지급)</SelectItem>
- </SelectContent>
- </Select>
- {/* L/C 또는 T/T 선택 시 퍼센트 입력 필드 */}
- {(formData.paymentDelivery === 'L/C' || formData.paymentDelivery === 'T/T') && (
- <div className="flex items-center gap-2 mt-2">
- <Input
- type="number"
- min="0"
- value={paymentDeliveryPercent}
- onChange={(e) => setPaymentDeliveryPercent(e.target.value)}
- placeholder="퍼센트"
- className="w-20 h-8 text-sm"
- />
- <span className="text-sm text-gray-600">%</span>
- </div>
- )}
- {errors.paymentDelivery && (
- <p className="text-sm text-red-600">납품 지급조건은 필수값입니다.</p>
- )}
- </div>
- </div>
- </div>
-
- {/* 납품 외 지급조건 */}
- <div className="space-y-4">
- <Label className="text-base font-medium">납품 외</Label>
- <div className="space-y-3">
- <div className="flex items-center space-x-2">
- <input
- type="checkbox"
- id="commissioning"
- checked={formData.paymentAfterDelivery.commissioning || false}
- onChange={(e) => setFormData(prev => ({
- ...prev,
- paymentAfterDelivery: {
- ...prev.paymentAfterDelivery,
- commissioning: e.target.checked
- }
- }))}
- className="rounded"
- />
- <Label htmlFor="commissioning" className="text-sm">Commissioning 완료</Label>
- <Input
- type="number"
- min="0"
- placeholder="%"
- className="w-16"
- value={formData.paymentAfterDelivery.commissioningPercent || ''}
- onChange={(e) => setFormData(prev => ({
- ...prev,
- paymentAfterDelivery: {
- ...prev.paymentAfterDelivery,
- commissioningPercent: e.target.value
- }
- }))}
- disabled={!formData.paymentAfterDelivery.commissioning}
- />
- </div>
- <div className="flex items-center space-x-2">
- <input
- type="checkbox"
- id="finalDocument"
- checked={formData.paymentAfterDelivery.finalDocument || false}
- onChange={(e) => setFormData(prev => ({
- ...prev,
- paymentAfterDelivery: {
- ...prev.paymentAfterDelivery,
- finalDocument: e.target.checked
- }
- }))}
- className="rounded"
- />
- <Label htmlFor="finalDocument" className="text-sm">최종문서 승인</Label>
- <Input
- type="number"
- min="0"
- placeholder="%"
- className="w-16"
- value={formData.paymentAfterDelivery.finalDocumentPercent || ''}
- onChange={(e) => setFormData(prev => ({
- ...prev,
- paymentAfterDelivery: {
- ...prev.paymentAfterDelivery,
- finalDocumentPercent: e.target.value
- }
- }))}
- disabled={!formData.paymentAfterDelivery.finalDocument}
- />
- </div>
- <div className="flex items-center space-x-2">
- <input
- type="checkbox"
- id="other"
- checked={formData.paymentAfterDelivery.other || false}
- onChange={(e) => setFormData(prev => ({
- ...prev,
- paymentAfterDelivery: {
- ...prev.paymentAfterDelivery,
- other: e.target.checked
- }
- }))}
- className="rounded"
- />
- <Label htmlFor="other" className="text-sm">기타</Label>
- <Input
- type="text"
- placeholder="기타 조건을 입력하세요"
- className="w-48"
- value={formData.paymentAfterDelivery.otherText || ''}
- onChange={(e) => setFormData(prev => ({
- ...prev,
- paymentAfterDelivery: {
- ...prev.paymentAfterDelivery,
- otherText: e.target.value
- }
- }))}
- disabled={!formData.paymentAfterDelivery.other}
- />
- </div>
- </div>
- </div>
-
- {/* 지불조건 */}
- <div className="space-y-4">
- <Label className="text-base font-medium">지불조건</Label>
- <div className="space-y-3">
- <div className="space-y-2">
- <Label htmlFor="paymentTerm">지불조건 <span className="text-red-600">*</span></Label>
- <Select
- value={formData.paymentTerm}
- onValueChange={(value) => setFormData(prev => ({ ...prev, paymentTerm: value }))}
- >
- <SelectTrigger className={errors.paymentTerm ? 'border-red-500' : ''}>
- <SelectValue placeholder="지불조건을 선택하세요" />
- </SelectTrigger>
- <SelectContent>
- {paymentTermsOptions.length > 0 ? (
- paymentTermsOptions.map((option) => (
- <SelectItem key={option.code} value={option.code}>
- {option.code} {option.description && `(${option.description})`}
- </SelectItem>
- ))
- ) : (
- <SelectItem value="loading" disabled>
- 데이터를 불러오는 중...
- </SelectItem>
- )}
- </SelectContent>
- </Select>
- {errors.paymentTerm && (
- <p className="text-sm text-red-600">지불조건은 필수값입니다.</p>
- )}
- </div>
- <div className="space-y-2">
- <Label htmlFor="taxType">세금조건 <span className="text-red-600">*</span></Label>
- <Select
- value={formData.taxType}
- onValueChange={(value) => setFormData(prev => ({ ...prev, taxType: value }))}
- >
- <SelectTrigger className={errors.taxType ? 'border-red-500' : ''}>
- <SelectValue placeholder="세금조건을 선택하세요" />
- </SelectTrigger>
- <SelectContent>
- {TAX_CONDITIONS.map((condition) => (
- <SelectItem key={condition.code} value={condition.code}>
- {condition.name}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- {errors.taxType && (
- <p className="text-sm text-red-600">세금조건은 필수값입니다.</p>
- )}
- </div>
- </div>
- </div>
-
- {/* 클레임금액 */}
- <div className="space-y-4">
- <Label className="text-base font-medium">클레임금액</Label>
- <div className="space-y-3">
- <div className="flex items-center space-x-2">
- <input
- type="checkbox"
- id="liquidatedDamages"
- checked={formData.liquidatedDamages || false}
- onChange={(e) => setFormData(prev => ({
- ...prev,
- liquidatedDamages: e.target.checked
- }))}
- className="rounded"
- />
- <Label htmlFor="liquidatedDamages" className="text-sm">지체상금</Label>
- <Input
- type="number"
- min="0"
- placeholder="%"
- className="w-16"
- value={formData.liquidatedDamagesPercent || ''}
- onChange={(e) => setFormData(prev => ({
- ...prev,
- liquidatedDamagesPercent: e.target.value
- }))}
- disabled={!formData.liquidatedDamages}
- />
- </div>
- </div>
- </div>
- </div>
-
- {/* 인도조건 섹션 */}
- <div className="mt-8">
- <h3 className="text-lg font-semibold mb-4">인도조건</h3>
- <div className="grid grid-cols-5 gap-6">
- {/* 납기종류 */}
- <div className="space-y-4">
- <div className="space-y-3">
- <div className="space-y-2">
- <Label htmlFor="deliveryType">납기종류</Label>
- <Select value={formData.deliveryType} onValueChange={(value) => setFormData(prev => ({ ...prev, deliveryType: value }))}>
- <SelectTrigger>
- <SelectValue placeholder="납기종류를 선택하세요" />
- </SelectTrigger>
- <SelectContent>
- <SelectItem value="단일납기">단일납기</SelectItem>
- <SelectItem value="분할납기">분할납기</SelectItem>
- <SelectItem value="구간납기">구간납기</SelectItem>
- </SelectContent>
- </Select>
- </div>
- </div>
- </div>
-
- {/* 인도조건 */}
- <div className="space-y-4">
- <div className="space-y-3">
- <div className="space-y-2">
- <Label htmlFor="deliveryTerm">인도조건</Label>
- <Select
- value={formData.deliveryTerm}
- onValueChange={(value) => setFormData(prev => ({ ...prev, deliveryTerm: value }))}
- >
- <SelectTrigger>
- <SelectValue placeholder="인도조건을 선택하세요" />
- </SelectTrigger>
- <SelectContent>
- {incotermsOptions.length > 0 ? (
- incotermsOptions.map((option) => (
- <SelectItem key={option.code} value={option.code}>
- {option.code} {option.description && `(${option.description})`}
- </SelectItem>
- ))
- ) : (
- <SelectItem value="loading" disabled>
- 데이터를 불러오는 중...
- </SelectItem>
- )}
- </SelectContent>
- </Select>
- </div>
- </div>
- </div>
-
- {/* 선적지 */}
- <div className="space-y-4">
- <div className="space-y-3">
- <div className="space-y-2">
- <Label htmlFor="shippingLocation">선적지</Label>
- <Select
- value={formData.shippingLocation}
- onValueChange={(value) => setFormData(prev => ({ ...prev, shippingLocation: value }))}
- >
- <SelectTrigger>
- <SelectValue placeholder="선적지를 선택하세요" />
- </SelectTrigger>
- <SelectContent>
- {shippingPlaces.length > 0 ? (
- shippingPlaces.map((place) => (
- <SelectItem key={place.code} value={place.code}>
- {place.code} {place.description && `(${place.description})`}
- </SelectItem>
- ))
- ) : (
- <SelectItem value="loading" disabled>
- 데이터를 불러오는 중...
- </SelectItem>
- )}
- </SelectContent>
- </Select>
- </div>
- </div>
- </div>
-
- {/* 하역지 */}
- <div className="space-y-4">
- <div className="space-y-3">
- <div className="space-y-2">
- <Label htmlFor="dischargeLocation">하역지</Label>
- <Select
- value={formData.dischargeLocation}
- onValueChange={(value) => setFormData(prev => ({ ...prev, dischargeLocation: value }))}
- >
- <SelectTrigger>
- <SelectValue placeholder="하역지를 선택하세요" />
- </SelectTrigger>
- <SelectContent>
- {destinationPlaces.length > 0 ? (
- destinationPlaces.map((place) => (
- <SelectItem key={place.code} value={place.code}>
- {place.code} {place.description && `(${place.description})`}
- </SelectItem>
- ))
- ) : (
- <SelectItem value="loading" disabled>
- 데이터를 불러오는 중...
- </SelectItem>
- )}
- </SelectContent>
- </Select>
- </div>
- </div>
- </div>
-
- {/* 계약납기일 */}
- <div className="space-y-4">
- <div className="space-y-3">
- <div className="space-y-2">
- <Label htmlFor="contractDeliveryDate">계약납기일</Label>
- <Input
- type="date"
- value={formData.contractDeliveryDate}
- onChange={(e) => setFormData(prev => ({ ...prev, contractDeliveryDate: e.target.value }))}
- />
- </div>
- </div>
- </div>
- </div>
- </div>
- </CardContent>
- </Card>
- </TabsContent>
-
- {/* 추가 조건 탭 */}
- <TabsContent value="additional" className="space-y-6">
- <Card>
- <CardHeader>
- <CardTitle>Additional Conditions (추가조건)</CardTitle>
- </CardHeader>
- <CardContent className="space-y-6">
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
- <div className="space-y-2">
- <Label htmlFor="contractAmount">계약금액 (자동계산)</Label>
- <Input
- type="text"
- value={contract?.contractAmount ? new Intl.NumberFormat('ko-KR').format(Number(contract.contractAmount)) : '품목정보 없음'}
- readOnly
- className="bg-gray-50"
- placeholder="품목정보에서 자동 계산됩니다"
- />
- </div>
- <div className="space-y-2">
- <Label htmlFor="currency">계약통화 <span className="text-red-600">*</span></Label>
- <Input
- type="text"
- value={formData.currency}
- onChange={(e) => setFormData(prev => ({ ...prev, currency: e.target.value }))}
- placeholder="계약통화를 입력하세요"
- className={errors.currency ? 'border-red-500' : ''}
- />
- {errors.currency && (
- <p className="text-sm text-red-600">계약통화는 필수값입니다.</p>
- )}
- </div>
-
- {/* 계약성립조건 */}
- <div className="space-y-4 col-span-2">
- <Label className="text-base font-medium">계약성립조건</Label>
- <div className="space-y-3">
- <div className="flex items-center space-x-2">
- <input
- type="checkbox"
- id="regularVendorRegistration"
- checked={formData.contractEstablishmentConditions.regularVendorRegistration}
- onChange={(e) => setFormData(prev => ({ ...prev, contractEstablishmentConditions: { ...prev.contractEstablishmentConditions, regularVendorRegistration: e.target.checked } }))}
- className="rounded"
- />
- <Label htmlFor="regularVendorRegistration">정규업체 등록(실사 포함) 시</Label>
- </div>
- <div className="flex items-center space-x-2">
- <input
- type="checkbox"
- id="projectAward"
- checked={formData.contractEstablishmentConditions.projectAward}
- onChange={(e) => setFormData(prev => ({ ...prev, contractEstablishmentConditions: { ...prev.contractEstablishmentConditions, projectAward: e.target.checked } }))}
- className="rounded"
- />
- <Label htmlFor="projectAward">프로젝트 수주 시</Label>
- </div>
- <div className="flex items-center space-x-2">
- <input
- type="checkbox"
- id="ownerApproval"
- checked={formData.contractEstablishmentConditions.ownerApproval}
- onChange={(e) => setFormData(prev => ({ ...prev, contractEstablishmentConditions: { ...prev.contractEstablishmentConditions, ownerApproval: e.target.checked } }))}
- className="rounded"
- />
- <Label htmlFor="ownerApproval">선주 승인 시</Label>
- </div>
- <div className="flex items-center space-x-2">
- <input
- type="checkbox"
- id="establishmentOther"
- checked={formData.contractEstablishmentConditions.other}
- onChange={(e) => setFormData(prev => ({ ...prev, contractEstablishmentConditions: { ...prev.contractEstablishmentConditions, other: e.target.checked } }))}
- className="rounded"
- />
- <Label htmlFor="establishmentOther">기타</Label>
- </div>
- </div>
- </div>
-
- {/* 연동제적용 */}
- <div className="space-y-4">
- <Label className="text-base font-medium">연동제적용</Label>
- <div className="space-y-2">
- <Select value={formData.interlockingSystem} onValueChange={(value) => setFormData(prev => ({ ...prev, interlockingSystem: value }))}>
- <SelectTrigger>
- <SelectValue placeholder="연동제적용을 선택하세요" />
- </SelectTrigger>
- <SelectContent>
- <SelectItem value="Y">Y</SelectItem>
- <SelectItem value="N">N</SelectItem>
- </SelectContent>
- </Select>
- </div>
- </div>
-
- {/* 필수문서동의 */}
- {/* <div className="space-y-4">
- <Label className="text-base font-medium">필수문서동의</Label>
- <div className="space-y-3">
- <div className="flex items-center space-x-2">
- <input
- type="checkbox"
- id="technicalDataAgreement"
- checked={formData.mandatoryDocuments.technicalDataAgreement}
- onChange={(e) => setFormData(prev => ({ ...prev, mandatoryDocuments: { ...prev.mandatoryDocuments, technicalDataAgreement: e.target.checked } }))}
- className="rounded"
- />
- <Label htmlFor="technicalDataAgreement">기술자료제공동의서</Label>
- </div>
- <div className="flex items-center space-x-2">
- <input
- type="checkbox"
- id="nda"
- checked={formData.mandatoryDocuments.nda}
- onChange={(e) => setFormData(prev => ({ ...prev, mandatoryDocuments: { ...prev.mandatoryDocuments, nda: e.target.checked } }))}
- className="rounded"
- />
- <Label htmlFor="nda">비밀유지계약서(NDA)</Label>
- </div>
- <div className="flex items-center space-x-2">
- <input
- type="checkbox"
- id="basicCompliance"
- checked={formData.mandatoryDocuments.basicCompliance}
- onChange={(e) => setFormData(prev => ({ ...prev, mandatoryDocuments: { ...prev.mandatoryDocuments, basicCompliance: e.target.checked } }))}
- className="rounded"
- />
- <Label htmlFor="basicCompliance">기본준수서약서</Label>
- </div>
- <div className="flex items-center space-x-2">
- <input
- type="checkbox"
- id="safetyHealthAgreement"
- checked={formData.mandatoryDocuments.safetyHealthAgreement}
- onChange={(e) => setFormData(prev => ({ ...prev, mandatoryDocuments: { ...prev.mandatoryDocuments, safetyHealthAgreement: e.target.checked } }))}
- className="rounded"
- />
- <Label htmlFor="safetyHealthAgreement">안전보건관리 약정서</Label>
- </div>
- </div>
- </div> */}
-
- {/* 계약해지조건 */}
- <div className="space-y-4">
- <Label className="text-base font-medium">계약해지조건</Label>
- <div className="space-y-3">
- <div className="flex items-center space-x-2">
- <input
- type="checkbox"
- id="standardTermination"
- checked={formData.contractTerminationConditions.standardTermination}
- onChange={(e) => setFormData(prev => ({ ...prev, contractTerminationConditions: { ...prev.contractTerminationConditions, standardTermination: e.target.checked } }))}
- className="rounded"
- />
- <Label htmlFor="standardTermination">표준 계약해지조건</Label>
- </div>
- <div className="flex items-center space-x-2">
- <input
- type="checkbox"
- id="projectNotAwarded"
- checked={formData.contractTerminationConditions.projectNotAwarded}
- onChange={(e) => setFormData(prev => ({ ...prev, contractTerminationConditions: { ...prev.contractTerminationConditions, projectNotAwarded: e.target.checked } }))}
- className="rounded"
- />
- <Label htmlFor="projectNotAwarded">프로젝트 미수주 시</Label>
- </div>
- <div className="flex items-center space-x-2">
- <input
- type="checkbox"
- id="terminationOther"
- checked={formData.contractTerminationConditions.other}
- onChange={(e) => setFormData(prev => ({ ...prev, contractTerminationConditions: { ...prev.contractTerminationConditions, other: e.target.checked } }))}
- className="rounded"
- />
- <Label htmlFor="terminationOther">기타</Label>
- </div>
- </div>
- </div>
-
- <div className="space-y-2 col-span-2">
- <Label htmlFor="notes">비고</Label>
- <Textarea
- value={formData.notes}
- onChange={(e) => setFormData(prev => ({ ...prev, notes: e.target.value }))}
- placeholder="비고사항을 입력하세요"
- rows={4}
- />
- </div>
- </div>
- </CardContent>
- </Card>
- </TabsContent>
-
-
- {/* 계약첨부문서 탭 */}
- <TabsContent value="documents" className="space-y-6">
- <ContractDocuments
- contractId={contractId}
- userId={userId?.toString() || "1"}
- />
- </TabsContent>
- </Tabs>
-
- {/* 저장 버튼 */}
- <div className="flex justify-end mt-6 pt-4 border-t border-gray-200">
- <Button
- onClick={handleSaveContractInfo}
- disabled={isLoading}
- className="flex items-center gap-2"
- >
- {isLoading ? (
- <LoaderIcon className="w-4 h-4 animate-spin" />
- ) : (
- <Save className="w-4 h-4" />
- )}
- 계약 정보 저장
- </Button>
- </div>
- </CardContent>
- </Card>
- )
-}
diff --git a/lib/general-contracts_old/detail/general-contract-communication-channel.tsx b/lib/general-contracts_old/detail/general-contract-communication-channel.tsx
deleted file mode 100644
index f5cd79b2..00000000
--- a/lib/general-contracts_old/detail/general-contract-communication-channel.tsx
+++ /dev/null
@@ -1,362 +0,0 @@
-'use client'
-
-import React, { useState, useEffect } from 'react'
-import { useSession } from 'next-auth/react'
-import { Input } from '@/components/ui/input'
-import { Button } from '@/components/ui/button'
-import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
-import { Checkbox } from '@/components/ui/checkbox'
-import { Plus, Trash2, Save, LoaderIcon, MessageSquare } from 'lucide-react'
-import { updateCommunicationChannel, getCommunicationChannel } from '../service'
-import { toast } from 'sonner'
-
-interface CommunicationChannelProps {
- contractType?: string
- contractId: number
-}
-
-interface Representative {
- id: string
- position: string
- name: string
- telNo: string
- email: string
- isActive: boolean
-}
-
-export function CommunicationChannel({ contractId }: CommunicationChannelProps) {
- const session = useSession()
- const [isLoading, setIsLoading] = useState(false)
- const [isEnabled, setIsEnabled] = useState(true)
-
- // 일단 모든 계약종류에서 활성화
- const isDisabled = false
-
- const [contractorReps, setContractorReps] = useState<Representative[]>([])
- const [supplierReps, setSupplierReps] = useState<Representative[]>([])
-
- // 초기 데이터 로드
- useEffect(() => {
- const loadCommunicationChannel = async () => {
- try {
- const data = await getCommunicationChannel(contractId)
- if (data && data.enabled !== undefined) {
- setIsEnabled(data.enabled)
- setContractorReps(data.contractorRepresentatives || [])
- setSupplierReps(data.supplierRepresentatives || [])
- }
- } catch (error) {
- console.error('Error loading communication channel:', error)
-
- }
- }
-
- loadCommunicationChannel()
- }, [contractId])
-
- const addContractorRow = () => {
- const newId = (contractorReps.length + 1).toString()
- setContractorReps([...contractorReps, {
- id: newId,
- position: '',
- name: '',
- telNo: '',
- email: '',
- isActive: false
- }])
- }
-
- const removeContractorRow = () => {
- const selectedRows = contractorReps.filter(rep => rep.isActive)
- if (selectedRows.length > 0) {
- setContractorReps(contractorReps.filter(rep => !rep.isActive))
- }
- }
-
- const addSupplierRow = () => {
- const newId = (supplierReps.length + 1).toString()
- setSupplierReps([...supplierReps, {
- id: newId,
- position: '',
- name: '',
- telNo: '',
- email: '',
- isActive: false
- }])
- }
-
- const removeSupplierRow = () => {
- const selectedRows = supplierReps.filter(rep => rep.isActive)
- if (selectedRows.length > 0) {
- setSupplierReps(supplierReps.filter(rep => !rep.isActive))
- }
- }
-
- const updateContractorRep = (id: string, field: keyof Representative, value: string | boolean) => {
- setContractorReps(contractorReps.map(rep =>
- rep.id === id ? { ...rep, [field]: value } : rep
- ))
- }
-
- const updateSupplierRep = (id: string, field: keyof Representative, value: string | boolean) => {
- setSupplierReps(supplierReps.map(rep =>
- rep.id === id ? { ...rep, [field]: value } : rep
- ))
- }
-
- const handleSaveCommunicationChannel = async () => {
- const userId = session.data?.user?.id ? Number(session.data.user.id) : null
-
- if (!userId) {
- toast.error('사용자 정보를 찾을 수 없습니다.')
- return
- }
-
- try {
- setIsLoading(true)
-
- const communicationData = {
- enabled: isEnabled,
- contractorRepresentatives: contractorReps,
- supplierRepresentatives: supplierReps
- }
-
- await updateCommunicationChannel(contractId, communicationData, userId)
- toast.success('커뮤니케이션 채널이 저장되었습니다.')
- } catch (error) {
- console.error('Error saving communication channel:', error)
- toast.error('커뮤니케이션 채널 저장에 실패했습니다.')
- } finally {
- setIsLoading(false)
- }
- }
-
- return (
- <div className="w-full">
- <Accordion type="single" collapsible className="w-full">
- {/* Communication Channel 활성화 */}
- <AccordionItem value="communication-channel">
- <AccordionTrigger className="hover:no-underline">
- <div className="flex items-center gap-3 w-full">
- <MessageSquare className="w-5 h-5" />
- <span className="font-medium">Communication Channel</span>
- </div>
- </AccordionTrigger>
- <AccordionContent>
- <div className="space-y-6">
- {/* 체크박스 */}
- <div className="flex items-center gap-2">
- <Checkbox
- checked={isEnabled}
- disabled={isDisabled}
- onCheckedChange={(checked) => {
- if (!isDisabled) {
- setIsEnabled(checked as boolean)
- }
- }}
- />
- <span className="text-sm font-medium">Communication Channel 활성화</span>
- </div>
-
- {/* Table 1: The Contractor's Representatives */}
- <div className="space-y-4">
- <div className="flex items-center justify-between">
- <h3 className="text-lg font-medium">Table 1: The Contractor &apos;s Representatives</h3>
- <div className="flex gap-2">
- <Button
- type="button"
- variant="outline"
- size="sm"
- onClick={addContractorRow}
- disabled={isDisabled || !isEnabled}
- >
- <Plus className="w-4 h-4 mr-1" />
- 행 추가
- </Button>
- <Button
- type="button"
- variant="outline"
- size="sm"
- onClick={removeContractorRow}
- disabled={isDisabled || !isEnabled}
- >
- <Trash2 className="w-4 h-4 mr-1" />
- 행 삭제
- </Button>
- </div>
- </div>
-
- <div className="overflow-x-auto">
- <table className={`w-full border-collapse border border-gray-300 ${!isEnabled ? 'opacity-50' : ''}`}>
- <thead>
- <tr className="bg-yellow-100">
- <th className="border border-gray-300 p-2 w-12"></th>
- <th className="border border-gray-300 p-2 w-16">No.</th>
- <th className="border border-gray-300 p-2">Position</th>
- <th className="border border-gray-300 p-2">Name</th>
- <th className="border border-gray-300 p-2">Tel. No.</th>
- <th className="border border-gray-300 p-2">Email</th>
- </tr>
- </thead>
- <tbody>
- {contractorReps.map((rep) => (
- <tr key={rep.id} className="bg-yellow-50">
- <td className="border border-gray-300 p-2 text-center">
- <Checkbox
- checked={rep.isActive}
- onCheckedChange={(checked) => updateContractorRep(rep.id, 'isActive', checked as boolean)}
- disabled={isDisabled || !isEnabled}
- />
- </td>
- <td className="border border-gray-300 p-2 text-center">{rep.id}</td>
- <td className="border border-gray-300 p-2">
- <Input
- value={rep.position}
- onChange={(e) => updateContractorRep(rep.id, 'position', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={rep.name}
- onChange={(e) => updateContractorRep(rep.id, 'name', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={rep.telNo}
- onChange={(e) => updateContractorRep(rep.id, 'telNo', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={rep.email}
- onChange={(e) => updateContractorRep(rep.id, 'email', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- />
- </td>
- </tr>
- ))}
- </tbody>
- </table>
- </div>
- </div>
-
- {/* Table 2: The Supplier's Representatives */}
- <div className="space-y-4">
- <div className="flex items-center justify-between">
- <h3 className="text-lg font-medium">Table 2: The Supplier &apos;s Representatives</h3>
- <div className="flex gap-2">
- <Button
- type="button"
- variant="outline"
- size="sm"
- onClick={addSupplierRow}
- disabled={isDisabled || !isEnabled}
- >
- <Plus className="w-4 h-4 mr-1" />
- 행 추가
- </Button>
- <Button
- type="button"
- variant="outline"
- size="sm"
- onClick={removeSupplierRow}
- disabled={isDisabled || !isEnabled}
- >
- <Trash2 className="w-4 h-4 mr-1" />
- 행 삭제
- </Button>
- </div>
- </div>
-
- <div className="overflow-x-auto">
- <table className={`w-full border-collapse border border-gray-300 ${!isEnabled ? 'opacity-50' : ''}`}>
- <thead>
- <tr className="bg-yellow-100">
- <th className="border border-gray-300 p-2 w-12"></th>
- <th className="border border-gray-300 p-2 w-16">No.</th>
- <th className="border border-gray-300 p-2">Position</th>
- <th className="border border-gray-300 p-2">Name</th>
- <th className="border border-gray-300 p-2">Tel. No.</th>
- <th className="border border-gray-300 p-2">Email</th>
- </tr>
- </thead>
- <tbody>
- {supplierReps.map((rep) => (
- <tr key={rep.id} className="bg-yellow-50">
- <td className="border border-gray-300 p-2 text-center">
- <Checkbox
- checked={rep.isActive}
- onCheckedChange={(checked) => updateSupplierRep(rep.id, 'isActive', checked as boolean)}
- disabled={isDisabled || !isEnabled}
- />
- </td>
- <td className="border border-gray-300 p-2 text-center">{rep.id}</td>
- <td className="border border-gray-300 p-2">
- <Input
- value={rep.position}
- onChange={(e) => updateSupplierRep(rep.id, 'position', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={rep.name}
- onChange={(e) => updateSupplierRep(rep.id, 'name', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={rep.telNo}
- onChange={(e) => updateSupplierRep(rep.id, 'telNo', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={rep.email}
- onChange={(e) => updateSupplierRep(rep.id, 'email', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- />
- </td>
- </tr>
- ))}
- </tbody>
- </table>
- </div>
- </div>
-
- {/* 저장 버튼 */}
- <div className="flex justify-end pt-4 border-t">
- <Button
- onClick={handleSaveCommunicationChannel}
- disabled={isLoading || isDisabled || !isEnabled}
- className="flex items-center gap-2"
- >
- {isLoading ? (
- <LoaderIcon className="w-4 h-4 animate-spin" />
- ) : (
- <Save className="w-4 h-4" />
- )}
- 커뮤니케이션 채널 저장
- </Button>
- </div>
- </div>
- </AccordionContent>
- </AccordionItem>
- </Accordion>
- </div>
- )
-}
diff --git a/lib/general-contracts_old/detail/general-contract-detail.tsx b/lib/general-contracts_old/detail/general-contract-detail.tsx
deleted file mode 100644
index 8e7a7aff..00000000
--- a/lib/general-contracts_old/detail/general-contract-detail.tsx
+++ /dev/null
@@ -1,186 +0,0 @@
-'use client'
-
-import { useState, useEffect } from 'react'
-import { useParams } from 'next/navigation'
-import Link from 'next/link'
-import { getContractById, getSubcontractChecklist } from '../service'
-import { GeneralContractInfoHeader } from './general-contract-info-header'
-import { Alert, AlertDescription } from '@/components/ui/alert'
-import { Button } from '@/components/ui/button'
-import { AlertCircle, ArrowLeft } from 'lucide-react'
-import { Skeleton } from '@/components/ui/skeleton'
-import { ContractItemsTable } from './general-contract-items-table'
-import { SubcontractChecklist } from './general-contract-subcontract-checklist'
-import { ContractBasicInfo } from './general-contract-basic-info'
-import { CommunicationChannel } from './general-contract-communication-channel'
-import { Location } from './general-contract-location'
-import { FieldServiceRate } from './general-contract-field-service-rate'
-import { OffsetDetails } from './general-contract-offset-details'
-import { ContractApprovalRequestDialog } from './general-contract-approval-request-dialog'
-
-export default function ContractDetailPage() {
- const params = useParams()
- const contractId = params?.id ? parseInt(params.id as string) : null
-
- const [contract, setContract] = useState<Record<string, unknown> | null>(null)
- const [loading, setLoading] = useState(true)
- const [error, setError] = useState<string | null>(null)
- const [showApprovalDialog, setShowApprovalDialog] = useState(false)
- const [subcontractChecklistData, setSubcontractChecklistData] = useState<any>(null)
-
- useEffect(() => {
- const fetchContract = async () => {
- try {
- setLoading(true)
- setError(null)
-
- // 계약 기본 정보 로드
- const contractData = await getContractById(contractId!)
- setContract(contractData)
-
- // 하도급법 체크리스트 데이터 로드
- try {
- const checklistData = await getSubcontractChecklist(contractId!)
- if (checklistData.success && checklistData.data) {
- setSubcontractChecklistData(checklistData.data)
- }
- } catch (checklistError) {
- console.log('하도급법 체크리스트 데이터 로드 실패:', checklistError)
- // 체크리스트 로드 실패는 전체 로드를 실패시키지 않음
- }
-
- } catch (err) {
- console.error('Error fetching contract:', err)
- setError('계약 정보를 불러오는 중 오류가 발생했습니다.')
- } finally {
- setLoading(false)
- }
- }
-
- if (contractId && !isNaN(contractId)) {
- fetchContract()
- } else {
- setError('유효하지 않은 계약 ID입니다.')
- setLoading(false)
- }
- }, [contractId])
-
- if (loading) {
- return (
- <div className="container mx-auto py-6 space-y-6">
- <Skeleton className="h-8 w-64" />
- <div className="grid gap-6">
- <div className="grid grid-cols-2 gap-4">
- <Skeleton className="h-10 w-full" />
- <Skeleton className="h-10 w-full" />
- </div>
- <div className="grid grid-cols-3 gap-4">
- <Skeleton className="h-10 w-full" />
- <Skeleton className="h-10 w-full" />
- <Skeleton className="h-10 w-full" />
- </div>
- <Skeleton className="h-32 w-full" />
- </div>
- </div>
- )
- }
-
- if (error) {
- return (
- <div className="container mx-auto py-6">
- <Alert variant="destructive">
- <AlertCircle className="h-4 w-4" />
- <AlertDescription>
- {error}
- </AlertDescription>
- </Alert>
- </div>
- )
- }
-
- return (
- <div className="container mx-auto py-6 space-y-6">
-
-
- <div className="flex items-center justify-between">
- <div>
- <h1 className="text-3xl font-bold tracking-tight">계약 상세</h1>
- <p className="text-muted-foreground">
- 계약번호: {contract?.contractNumber as string} (Rev.{contract?.revision as number})
- </p>
- </div>
- <div className="flex gap-2">
- {/* 계약승인요청 버튼 */}
- <Button
- onClick={() => setShowApprovalDialog(true)}
- className="bg-blue-600 hover:bg-blue-700"
- >
- 계약승인요청
- </Button>
- {/* 계약목록으로 돌아가기 버튼 */}
- <Button asChild variant="outline" size="sm">
- <Link href="/evcp/general-contracts">
- <ArrowLeft className="h-4 w-4 mr-2" />
- 계약목록으로 돌아가기
- </Link>
- </Button>
- </div>
- </div>
- {/* 계약 정보 헤더 */}
- {contract && <GeneralContractInfoHeader contract={contract} />}
-
- {/* 계약 상세 폼 */}
- {contract && (
- <div className="space-y-6">
- {/* ContractBasicInfo */}
- <ContractBasicInfo contractId={contract.id as number} />
- {/* 품목정보 */}
- {/* {!(contract?.contractScope === '단가' || contract?.contractScope === '물량(실적)') && (
- <div className="mb-4">
- <p className="text-sm text-gray-600 mb-2">
- <strong>품목정보 입력 안내:</strong>
- <br />
- 단가/물량 확정 계약의 경우 수량 및 총 계약금액은 별도로 관리됩니다.
- </p>
- </div>
- )} */}
- <ContractItemsTable
- contractId={contract.id as number}
- items={[]}
- onItemsChange={() => {}}
- onTotalAmountChange={() => {}}
- availableBudget={0}
- readOnly={contract?.contractScope === '단가' || contract?.contractScope === '물량(실적)'}
- />
- {/* 하도급법 자율점검 체크리스트 */}
- <SubcontractChecklist
- contractId={contract.id as number}
- onDataChange={(data) => setSubcontractChecklistData(data)}
- readOnly={false}
- initialData={subcontractChecklistData}
- />
- {/* Communication Channel */}
- <CommunicationChannel contractId={Number(contract.id)} />
-
- {/* Location */}
- <Location contractId={Number(contract.id)} />
-
- {/* Field Service Rate */}
- <FieldServiceRate contractId={Number(contract.id)} />
-
- {/* Offset Details */}
- <OffsetDetails contractId={Number(contract.id)} />
- </div>
- )}
-
- {/* 계약승인요청 다이얼로그 */}
- {contract && (
- <ContractApprovalRequestDialog
- contract={contract}
- open={showApprovalDialog}
- onOpenChange={setShowApprovalDialog}
- />
- )}
- </div>
- )
-}
diff --git a/lib/general-contracts_old/detail/general-contract-documents.tsx b/lib/general-contracts_old/detail/general-contract-documents.tsx
deleted file mode 100644
index b0f20e7f..00000000
--- a/lib/general-contracts_old/detail/general-contract-documents.tsx
+++ /dev/null
@@ -1,383 +0,0 @@
-'use client'
-
-import React, { useState, useEffect } from 'react'
-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 { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
-import { Badge } from '@/components/ui/badge'
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
-import {
- Download,
- Trash2,
- FileText,
- LoaderIcon,
- Paperclip,
- MessageSquare
-} from 'lucide-react'
-import { toast } from 'sonner'
-import { useTransition } from 'react'
-import {
- uploadContractAttachment,
- getContractAttachments,
- getContractAttachmentForDownload,
- deleteContractAttachment
-} from '../service'
-import { downloadFile } from '@/lib/file-download'
-
-interface ContractDocument {
- id: number
- contractId: number
- documentName: string
- fileName: string
- filePath: string
- documentType?: string
- shiComment?: string | null
- vendorComment?: string | null
- uploadedAt: Date
- uploadedById: number
-}
-
-interface ContractDocumentsProps {
- contractId: number
- userId: string
- readOnly?: boolean
-}
-
-export function ContractDocuments({ contractId, userId, readOnly = false }: ContractDocumentsProps) {
- const [documents, setDocuments] = useState<ContractDocument[]>([])
- const [isLoading, setIsLoading] = useState(false)
- const [isPending, startTransition] = useTransition()
- const [editingComment, setEditingComment] = useState<{ id: number; type: 'shi' | 'vendor' } | null>(null)
- const [commentText, setCommentText] = useState('')
- const [selectedDocumentType, setSelectedDocumentType] = useState('')
-
- const loadDocuments = React.useCallback(async () => {
- setIsLoading(true)
- try {
- const documentList = await getContractAttachments(contractId)
- setDocuments(documentList as ContractDocument[])
- } catch (error) {
- console.error('Error loading documents:', error)
- toast.error('문서 목록을 불러오는 중 오류가 발생했습니다.')
- } finally {
- setIsLoading(false)
- }
- }, [contractId])
-
- useEffect(() => {
- loadDocuments()
- }, [loadDocuments])
-
- const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
- const file = event.target.files?.[0]
- if (!file) return
-
- if (!selectedDocumentType) {
- toast.error('문서 유형을 선택해주세요.')
- return
- }
-
- startTransition(async () => {
- try {
- // 본 계약문서 타입인 경우 기존 문서 확인
- if (selectedDocumentType === 'main') {
- const existingMainDoc = documents.find(doc => doc.documentType === 'main')
- if (existingMainDoc) {
- toast.info('기존 계약문서가 새롭게 업로드한 문서로 대체됩니다.')
- // 기존 본 계약문서 삭제
- await deleteContractAttachment(existingMainDoc.id, contractId)
- }
- }
-
- await uploadContractAttachment(contractId, file, userId, selectedDocumentType)
- toast.success('문서가 업로드되었습니다.')
- loadDocuments()
- // 파일 입력 초기화
- event.target.value = ''
- } catch (error) {
- console.error('Error uploading document:', error)
- toast.error('문서 업로드 중 오류가 발생했습니다.')
- }
- })
- }
-
- const handleDownload = async (document: ContractDocument) => {
- try {
- const fileData = await getContractAttachmentForDownload(document.id, contractId)
- downloadFile(fileData.attachment?.filePath || '', fileData.attachment?.fileName || '', {
- showToast: true
- })
- } catch (error) {
- console.error('Error downloading document:', error)
- toast.error('문서 다운로드 중 오류가 발생했습니다.')
- }
- }
-
- const handleDelete = async (documentId: number) => {
-
- startTransition(async () => {
- try {
- await deleteContractAttachment(documentId, contractId)
- toast.success('문서가 삭제되었습니다.')
- loadDocuments()
- } catch (error) {
- console.error('Error deleting document:', error)
- toast.error('문서 삭제 중 오류가 발생했습니다.')
- }
- })
- }
-
- const handleEditComment = (documentId: number, type: 'shi' | 'vendor', currentComment?: string) => {
- setEditingComment({ id: documentId, type })
- setCommentText(currentComment || '')
- }
-
- const handleSaveComment = async () => {
- if (!editingComment) return
-
- try {
- // TODO: API 호출로 댓글 저장
- toast.success('댓글이 저장되었습니다.')
- setEditingComment(null)
- setCommentText('')
- loadDocuments()
- } catch (error) {
- console.error('Error saving comment:', error)
- toast.error('댓글 저장 중 오류가 발생했습니다.')
- }
- }
-
- const getDocumentTypeLabel = (documentName: string) => {
- switch (documentName) {
- case 'specification': return '사양'
- case 'pricing': return '단가종류'
- case 'other': return '기타'
- default: return documentName
- }
- }
-
- const getDocumentTypeColor = (documentName: string) => {
- switch (documentName) {
- case 'specification': return 'bg-blue-100 text-blue-800'
- case 'pricing': return 'bg-green-100 text-green-800'
- case 'other': return 'bg-gray-100 text-gray-800'
- default: return 'bg-gray-100 text-gray-800'
- }
- }
-
- const groupedDocuments = documents.reduce((acc, doc) => {
- const type = doc.documentName
- if (!acc[type]) {
- acc[type] = []
- }
- acc[type].push(doc)
- return acc
- }, {} as Record<string, ContractDocument[]>)
-
- const documentTypes = [
- { value: 'specification', label: '사양' },
- { value: 'pricing', label: '단가종류' },
- { value: 'other', label: '기타' }
- ]
-
- return (
- <Card>
- <CardHeader>
- <CardTitle className="flex items-center gap-2">
- <Paperclip className="h-5 w-5" />
- 계약 첨부문서
- </CardTitle>
- </CardHeader>
- <CardContent className="space-y-6">
- {/* 파일 업로드 */}
- {!readOnly && (
- <div className="space-y-4">
- <div className="flex items-center gap-4">
- <Select value={selectedDocumentType} onValueChange={setSelectedDocumentType}>
- <SelectTrigger className="w-40">
- <SelectValue placeholder="문서 유형" />
- </SelectTrigger>
- <SelectContent>
- {documentTypes.map((type) => (
- <SelectItem key={type.value} value={type.value}>
- {type.label}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- <Input
- type="file"
- onChange={handleFileUpload}
- disabled={isPending}
- className="flex-1"
- />
- </div>
- </div>
- )}
-
- {/* 문서 목록 */}
- {isLoading ? (
- <div className="flex items-center justify-center py-8">
- <LoaderIcon className="h-6 w-6 animate-spin" />
- <span className="ml-2">문서를 불러오는 중...</span>
- </div>
- ) : documents.length === 0 ? (
- <div className="text-center py-8 text-muted-foreground">
- <FileText className="h-12 w-12 mx-auto mb-4 opacity-50" />
- <p>업로드된 문서가 없습니다.</p>
- </div>
- ) : (
- <div className="space-y-6">
- {Object.entries(groupedDocuments).map(([type, docs]) => (
- <div key={type} className="space-y-3">
- <div className="flex items-center gap-2">
- <Badge className={getDocumentTypeColor(type)}>
- {getDocumentTypeLabel(type)}
- </Badge>
- <span className="text-sm text-muted-foreground">
- {docs.length}개 문서
- </span>
- </div>
-
- <div className="space-y-3">
- {docs.map((doc) => (
- <div key={doc.id} className="border rounded-lg p-4 space-y-3">
- <div className="flex items-center justify-between">
- <div className="flex items-center gap-3">
- <FileText className="h-5 w-5 text-muted-foreground" />
- <div>
- <p className="font-medium">{doc.fileName}</p>
- <p className="text-sm text-muted-foreground">
- 업로드: {new Date(doc.uploadedAt).toLocaleDateString()}
- </p>
- </div>
- </div>
-
- {!readOnly && (
- <div className="flex items-center gap-2">
- <Button
- variant="outline"
- size="sm"
- onClick={() => handleDownload(doc)}
- >
- <Download className="h-4 w-4" />
- </Button>
- <Button
- variant="outline"
- size="sm"
- onClick={() => handleDelete(doc.id)}
- className="text-red-600 hover:text-red-700"
- >
- <Trash2 className="h-4 w-4" />
- </Button>
- </div>
- )}
- </div>
-
- {/* 댓글 섹션 */}
- <div className="grid grid-cols-2 gap-4">
- {/* SHI 댓글 */}
- <div className="space-y-2">
- <div className="flex items-center justify-between">
- <Label className="text-sm font-medium">SHI 댓글</Label>
- {!readOnly && (
- <Button
- variant="ghost"
- size="sm"
- onClick={() => handleEditComment(doc.id, 'shi', doc.shiComment || '')}
- >
- <MessageSquare className="h-4 w-4" />
- </Button>
- )}
- </div>
- {editingComment?.id === doc.id && editingComment.type === 'shi' ? (
- <div className="space-y-2">
- <Textarea
- value={commentText}
- onChange={(e) => setCommentText(e.target.value)}
- placeholder="SHI 댓글을 입력하세요"
- rows={3}
- />
- <div className="flex gap-2">
- <Button size="sm" onClick={handleSaveComment}>
- 저장
- </Button>
- <Button
- size="sm"
- variant="outline"
- onClick={() => setEditingComment(null)}
- >
- 취소
- </Button>
- </div>
- </div>
- ) : (
- <div className="min-h-[60px] p-3 bg-gray-50 rounded border">
- {doc.shiComment ? (
- <p className="text-sm">{doc.shiComment}</p>
- ) : (
- <p className="text-sm text-muted-foreground">댓글이 없습니다.</p>
- )}
- </div>
- )}
- </div>
-
- {/* Vendor 댓글 */}
- <div className="space-y-2">
- <div className="flex items-center justify-between">
- <Label className="text-sm font-medium">Vendor 댓글</Label>
- {!readOnly && (
- <Button
- variant="ghost"
- size="sm"
- onClick={() => handleEditComment(doc.id, 'vendor', doc.vendorComment || '')}
- >
- <MessageSquare className="h-4 w-4" />
- </Button>
- )}
- </div>
- {editingComment?.id === doc.id && editingComment.type === 'vendor' ? (
- <div className="space-y-2">
- <Textarea
- value={commentText}
- onChange={(e) => setCommentText(e.target.value)}
- placeholder="Vendor 댓글을 입력하세요"
- rows={3}
- />
- <div className="flex gap-2">
- <Button size="sm" onClick={handleSaveComment}>
- 저장
- </Button>
- <Button
- size="sm"
- variant="outline"
- onClick={() => setEditingComment(null)}
- >
- 취소
- </Button>
- </div>
- </div>
- ) : (
- <div className="min-h-[60px] p-3 bg-gray-50 rounded border">
- {doc.vendorComment ? (
- <p className="text-sm">{doc.vendorComment}</p>
- ) : (
- <p className="text-sm text-muted-foreground">댓글이 없습니다.</p>
- )}
- </div>
- )}
- </div>
- </div>
- </div>
- ))}
- </div>
- </div>
- ))}
- </div>
- )}
- </CardContent>
- </Card>
- )
-}
diff --git a/lib/general-contracts_old/detail/general-contract-field-service-rate.tsx b/lib/general-contracts_old/detail/general-contract-field-service-rate.tsx
deleted file mode 100644
index a8158307..00000000
--- a/lib/general-contracts_old/detail/general-contract-field-service-rate.tsx
+++ /dev/null
@@ -1,288 +0,0 @@
-'use client'
-
-import React, { useState, useEffect } from 'react'
-import { useSession } from 'next-auth/react'
-import { Input } from '@/components/ui/input'
-import { Button } from '@/components/ui/button'
-import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
-import { Checkbox } from '@/components/ui/checkbox'
-import { Plus, Trash2, Save, LoaderIcon, DollarSign } from 'lucide-react'
-import { getFieldServiceRate, updateFieldServiceRate } from '../service'
-import { toast } from 'sonner'
-
-interface FieldServiceRateProps {
- contractId: number
- contractType?: string
-}
-
-interface FieldServiceRateItem {
- id: string
- levelOfSpecialist: string
- description: string
- rateCurrency: string
- onshoreRate: string
- offshoreRate: string
- rateUnit: string
- remark: string
-}
-
-export function FieldServiceRate({ contractId }: FieldServiceRateProps) {
- const session = useSession()
- const [isLoading, setIsLoading] = useState(false)
- const [isEnabled, setIsEnabled] = useState(true)
-
- // 특정 계약종류를 제외한 일반계약은 Default로 표시
- const isDisabled = false
-
- const [fieldServiceRates, setFieldServiceRates] = useState<FieldServiceRateItem[]>([])
-
- // 초기 데이터 로드
- useEffect(() => {
- const loadFieldServiceRate = async () => {
- try {
- const data = await getFieldServiceRate(contractId)
- if (data && data.enabled !== undefined) {
- setIsEnabled(data.enabled)
- setFieldServiceRates(data.fieldServiceRates || [])
- } else {
- }
- } catch (error) {
- console.error('Field Service Rate 데이터 로드 실패:', error)
- toast.error('Field Service Rate 데이터를 불러오는데 실패했습니다.')
- }
- }
-
- loadFieldServiceRate()
- }, [contractId])
-
- const addFieldServiceRateRow = () => {
- const newRow: FieldServiceRateItem = {
- id: Date.now().toString(),
- levelOfSpecialist: '',
- description: '',
- rateCurrency: 'USD',
- onshoreRate: '',
- offshoreRate: '',
- rateUnit: 'day',
- remark: ''
- }
- setFieldServiceRates([...fieldServiceRates, newRow])
- }
-
- const removeFieldServiceRateRow = (id: string) => {
- setFieldServiceRates(fieldServiceRates.filter(item => item.id !== id))
- }
-
- const updateFieldServiceRateData = (id: string, field: keyof FieldServiceRateItem, value: string) => {
- setFieldServiceRates(prev =>
- prev.map(item =>
- item.id === id ? { ...item, [field]: value } : item
- )
- )
- }
-
- const handleSaveFieldServiceRate = async () => {
- if (!session.data?.user?.id) {
- toast.error('로그인이 필요합니다.')
- return
- }
-
- setIsLoading(true)
- try {
- const fieldServiceRateData = {
- enabled: isEnabled,
- fieldServiceRates: fieldServiceRates
- }
-
- await updateFieldServiceRate(contractId, fieldServiceRateData, Number(session.data.user.id))
- toast.success('Field Service Rate가 성공적으로 저장되었습니다.')
- } catch (error) {
- console.error('Field Service Rate 저장 실패:', error)
- toast.error('Field Service Rate 저장에 실패했습니다.')
- } finally {
- setIsLoading(false)
- }
- }
-
- return (
- <div className="w-full">
- <Accordion type="single" collapsible className="w-full">
- <AccordionItem value="field-service-rate">
- <AccordionTrigger className="hover:no-underline">
- <div className="flex items-center gap-3 w-full">
- <DollarSign className="w-5 h-5" />
- <span className="font-medium">Field Service Rate</span>
- </div>
- </AccordionTrigger>
- <AccordionContent>
- <div className="space-y-6">
- {/* 체크박스 */}
- <div className="flex items-center gap-2">
- <Checkbox
- checked={isEnabled}
- disabled={isDisabled}
- onCheckedChange={(checked) => {
- if (!isDisabled) {
- setIsEnabled(checked as boolean)
- }
- }}
- />
- <span className="text-sm font-medium">Field Service Rate 활성화</span>
- </div>
-
- {/* Field Service Rate 테이블 */}
- <div className="space-y-4">
- <div className="flex items-center justify-between">
- <h3 className="text-lg font-medium">Field Service Rate</h3>
- <div className="flex gap-2">
- <Button
- type="button"
- variant="outline"
- size="sm"
- onClick={addFieldServiceRateRow}
- disabled={isDisabled || !isEnabled}
- className="flex items-center gap-2"
- >
- <Plus className="w-4 h-4" />
- 행 추가
- </Button>
- <Button
- type="button"
- variant="outline"
- size="sm"
- onClick={() => {
- if (confirm('선택된 행들을 삭제하시겠습니까?')) {
- // 선택된 행들 삭제 로직 (필요시 구현)
- }
- }}
- disabled={isDisabled || !isEnabled}
- className="flex items-center gap-2"
- >
- <Trash2 className="w-4 h-4" />
- 행 삭제
- </Button>
- </div>
- </div>
-
- <div className="overflow-x-auto">
- <table className={`w-full border-collapse border border-gray-300 ${!isEnabled ? 'opacity-50' : ''}`}>
- <thead>
- <tr className="bg-yellow-100">
- <th className="border border-gray-300 p-2 w-16">No.</th>
- <th className="border border-gray-300 p-2 w-40">Level of Specialist</th>
- <th className="border border-gray-300 p-2">Description</th>
- <th className="border border-gray-300 p-2 w-32">Rate Currency</th>
- <th className="border border-gray-300 p-2 w-32">Onshore Rate</th>
- <th className="border border-gray-300 p-2 w-32">Offshore Rate</th>
- <th className="border border-gray-300 p-2 w-24">Rate Unit</th>
- <th className="border border-gray-300 p-2 w-32">Remark</th>
- <th className="border border-gray-300 p-2 w-20">Action</th>
- </tr>
- </thead>
- <tbody>
- {fieldServiceRates.map((item, index) => (
- <tr key={item.id} className="bg-yellow-50">
- <td className="border border-gray-300 p-2 text-center">{index + 1}</td>
- <td className="border border-gray-300 p-2">
- <Input
- value={item.levelOfSpecialist}
- onChange={(e) => updateFieldServiceRateData(item.id, 'levelOfSpecialist', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={item.description}
- onChange={(e) => updateFieldServiceRateData(item.id, 'description', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={item.rateCurrency}
- onChange={(e) => updateFieldServiceRateData(item.id, 'rateCurrency', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={item.onshoreRate}
- onChange={(e) => updateFieldServiceRateData(item.id, 'onshoreRate', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={item.offshoreRate}
- onChange={(e) => updateFieldServiceRateData(item.id, 'offshoreRate', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={item.rateUnit}
- onChange={(e) => updateFieldServiceRateData(item.id, 'rateUnit', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={item.remark}
- onChange={(e) => updateFieldServiceRateData(item.id, 'remark', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- />
- </td>
- <td className="border border-gray-300 p-2 text-center">
- <Button
- type="button"
- variant="ghost"
- size="sm"
- onClick={() => removeFieldServiceRateRow(item.id)}
- disabled={isDisabled || !isEnabled}
- className="h-8 w-8 p-0 text-red-600 hover:text-red-700"
- >
- <Trash2 className="w-4 h-4" />
- </Button>
- </td>
- </tr>
- ))}
- </tbody>
- </table>
- </div>
-
- {/* Note 정보 */}
- <div className="space-y-2 text-sm text-gray-600">
- <p><strong>Note #1:</strong> Air fare, travelling costs and hours, Visa, training, medical test and any additional applications are included.</p>
- <p><strong>Note #2:</strong> Accommodation, meal and local transportation are included.</p>
- </div>
- </div>
-
- {/* 저장 버튼 */}
- <div className="flex justify-end pt-4 border-t">
- <Button
- onClick={handleSaveFieldServiceRate}
- disabled={isLoading || isDisabled || !isEnabled}
- className="flex items-center gap-2"
- >
- {isLoading ? (
- <LoaderIcon className="w-4 h-4 animate-spin" />
- ) : (
- <Save className="w-4 h-4" />
- )}
- Field Service Rate 저장
- </Button>
- </div>
- </div>
- </AccordionContent>
- </AccordionItem>
- </Accordion>
- </div>
- )
-}
diff --git a/lib/general-contracts_old/detail/general-contract-info-header.tsx b/lib/general-contracts_old/detail/general-contract-info-header.tsx
deleted file mode 100644
index 9be9840d..00000000
--- a/lib/general-contracts_old/detail/general-contract-info-header.tsx
+++ /dev/null
@@ -1,211 +0,0 @@
-import { Building2, Package, DollarSign, Calendar, FileText } from 'lucide-react'
-import { formatDate } from '@/lib/utils'
-
-interface GeneralContractInfoHeaderProps {
- contract: {
- id: number
- contractNumber: string
- revision: number
- status: string
- category: string
- type: string
- name: string
- vendorName?: string
- vendorCode?: string
- startDate: string
- endDate: string
- validityEndDate: string
- contractAmount?: string
- currency?: string
- registeredAt: string
- signedAt?: string
- linkedRfqOrItb?: string
- linkedBidNumber?: string
- linkedPoNumber?: string
- }
-}
-
-const statusLabels = {
- 'Draft': '임시저장',
- 'Request to Review': '조건검토요청',
- 'Confirm to Review': '조건검토완료',
- 'Contract Accept Request': '계약승인요청',
- 'Complete the Contract': '계약체결',
- 'Reject to Accept Contract': '계약승인거절',
- 'Contract Delete': '계약폐기',
- 'PCR Request': 'PCR요청',
- 'VO Request': 'VO요청',
- 'PCR Accept': 'PCR승인',
- 'PCR Reject': 'PCR거절'
-}
-
-const categoryLabels = {
- '단가계약': '단가계약',
- '일반계약': '일반계약',
- '매각계약': '매각계약'
-}
-
-const typeLabels = {
- 'UP': '자재단가계약',
- 'LE': '임대차계약',
- 'IL': '개별운송계약',
- 'AL': '연간운송계약',
- 'OS': '외주용역계약',
- 'OW': '도급계약',
- 'IS': '검사계약',
- 'LO': 'LOI',
- 'FA': 'FA',
- 'SC': '납품합의계약',
- 'OF': '클레임상계계약',
- 'AW': '사전작업합의',
- 'AD': '사전납품합의',
- 'AM': '설계계약',
- 'SC_SELL': '폐기물매각계약'
-}
-
-export function GeneralContractInfoHeader({ contract }: GeneralContractInfoHeaderProps) {
- return (
- <div className="bg-white border rounded-lg p-6 mb-6 shadow-sm">
- {/* 3개 섹션을 Grid로 배치 - 각 섹션이 동일한 width로 꽉 채움 */}
- <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
- {/* 왼쪽 섹션: 계약 기본 정보 */}
- <div className="w-full space-y-4">
- {/* 계약번호 */}
- <div className="mb-4">
- <div className="flex items-center gap-2 text-sm text-gray-500 mb-2">
- <FileText className="w-4 h-4" />
- <span>계약번호 (Rev.)</span>
- </div>
- <div className="font-mono font-medium text-gray-900">
- {contract.contractNumber} (Rev.{contract.revision})
- </div>
- </div>
-
- {/* 계약명 */}
- <div className="mb-4">
- <div className="flex items-center gap-2 text-sm text-gray-500 mb-2">
- <Package className="w-4 h-4" />
- <span>계약명</span>
- </div>
- <div className="font-medium text-gray-900">{contract.name}</div>
- </div>
-
- {/* 협력업체 */}
- <div className="mb-4">
- <div className="flex items-center gap-2 text-sm text-gray-500 mb-2">
- <Building2 className="w-4 h-4" />
- <span>협력업체</span>
- </div>
- <div className="font-medium text-gray-900">
- {contract.vendorName || '협력업체 미선택'}
- {contract.vendorCode && (
- <span className="text-sm text-gray-500 ml-2">({contract.vendorCode})</span>
- )}
- </div>
- </div>
-
-
- {/* 계약금액 */}
- {contract.contractAmount && (
- <div className="mb-4">
- <div className="flex items-center gap-2 text-sm text-gray-500 mb-2">
- <DollarSign className="w-4 h-4" />
- <span>계약금액</span>
- </div>
- <div className="font-semibold text-gray-900">
- {new Intl.NumberFormat('ko-KR', {
- style: 'currency',
- currency: contract.currency || 'KRW',
- }).format(Number(contract.contractAmount))}
- </div>
- </div>
- )}
- </div>
-
- {/* 가운데 섹션: 계약 분류 정보 */}
- <div className="w-full border-l border-gray-100 pl-6">
- <div className="space-y-3">
- <div className="flex flex-col gap-1">
- <span className="text-gray-500 text-sm">계약상태</span>
- <span className="font-medium">{statusLabels[contract.status as keyof typeof statusLabels] || contract.status}</span>
- </div>
-
- <div className="flex flex-col gap-1">
- <span className="text-gray-500 text-sm">계약구분</span>
- <span className="font-medium">{categoryLabels[contract.category as keyof typeof categoryLabels] || contract.category}</span>
- </div>
-
- <div className="flex flex-col gap-1">
- <span className="text-gray-500 text-sm">계약종류</span>
- <span className="font-medium">{typeLabels[contract.type as keyof typeof typeLabels] || contract.type}</span>
- </div>
-
- <div className="flex flex-col gap-1">
- <span className="text-gray-500 text-sm">통화</span>
- <span className="font-mono font-medium">{contract.currency || 'KRW'}</span>
- </div>
- </div>
- </div>
-
- {/* 오른쪽 섹션: 일정 및 연계 정보 */}
- <div className="w-full border-l border-gray-100 pl-6">
- <div className="flex items-center gap-2 mb-3 text-sm text-gray-500">
- <Calendar className="w-4 h-4" />
- <span>일정 및 연계 정보</span>
- </div>
- <div className="space-y-3">
- <div>
- <span className="text-gray-500 text-sm">계약기간</span>
- <div className="font-medium">
- {formatDate(contract.startDate, 'KR')} ~ {formatDate(contract.endDate, 'KR')}
- </div>
- </div>
-
- <div>
- <span className="text-gray-500 text-sm">계약 유효기간</span>
- <div className="font-medium">{formatDate(contract.validityEndDate, 'KR')}</div>
- </div>
-
- {contract.signedAt && (
- <div>
- <span className="text-gray-500 text-sm">계약체결일</span>
- <div className="font-medium">{formatDate(contract.signedAt, 'KR')}</div>
- </div>
- )}
-
- {contract.registeredAt && (
- <div>
- <span className="text-gray-500 text-sm">등록일</span>
- <div className="font-medium">{formatDate(contract.registeredAt, 'KR')}</div>
- </div>
- )}
-
- {(contract.linkedRfqOrItb || contract.linkedBidNumber || contract.linkedPoNumber) && (
- <div className="space-y-2">
- <span className="text-gray-500 text-sm font-medium">연계 정보</span>
- {contract.linkedRfqOrItb && (
- <div>
- <span className="text-gray-500 text-xs">연계 견적/입찰번호</span>
- <div className="font-medium text-sm">{contract.linkedRfqOrItb}</div>
- </div>
- )}
- {contract.linkedBidNumber && (
- <div>
- <span className="text-gray-500 text-xs">연계 BID번호</span>
- <div className="font-medium text-sm">{contract.linkedBidNumber}</div>
- </div>
- )}
- {contract.linkedPoNumber && (
- <div>
- <span className="text-gray-500 text-xs">연계 PO번호</span>
- <div className="font-medium text-sm">{contract.linkedPoNumber}</div>
- </div>
- )}
- </div>
- )}
- </div>
- </div>
- </div>
- </div>
- )
-}
diff --git a/lib/general-contracts_old/detail/general-contract-items-table.tsx b/lib/general-contracts_old/detail/general-contract-items-table.tsx
deleted file mode 100644
index 1b9a1a06..00000000
--- a/lib/general-contracts_old/detail/general-contract-items-table.tsx
+++ /dev/null
@@ -1,602 +0,0 @@
-'use client'
-
-import * as React from 'react'
-import { Card, CardContent, CardHeader } from '@/components/ui/card'
-import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
-import { Input } from '@/components/ui/input'
-import { Label } from '@/components/ui/label'
-import { Button } from '@/components/ui/button'
-import { Checkbox } from '@/components/ui/checkbox'
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from '@/components/ui/table'
-import {
- Package,
- Plus,
- Trash2,
-} from 'lucide-react'
-import { toast } from 'sonner'
-import { updateContractItems, getContractItems } from '../service'
-import { Save, LoaderIcon } from 'lucide-react'
-
-interface ContractItem {
- id?: number
- itemCode: string
- itemInfo: string
- specification: string
- quantity: number
- quantityUnit: string
- totalWeight: number
- weightUnit: string
- contractDeliveryDate: string
- contractUnitPrice: number
- contractAmount: number
- contractCurrency: string
- isSelected?: boolean
- [key: string]: unknown
-}
-
-interface ContractItemsTableProps {
- contractId: number
- items: ContractItem[]
- onItemsChange: (items: ContractItem[]) => void
- onTotalAmountChange: (total: number) => void
- availableBudget?: number
- readOnly?: boolean
-}
-
-// 통화 목록
-const CURRENCIES = ["USD", "EUR", "KRW", "JPY", "CNY"];
-
-// 수량 단위 목록
-const QUANTITY_UNITS = ["KG", "TON", "EA", "M", "M2", "M3", "L", "ML", "G", "SET", "PCS"];
-
-// 중량 단위 목록
-const WEIGHT_UNITS = ["KG", "TON", "G", "LB", "OZ"];
-
-export function ContractItemsTable({
- contractId,
- items,
- onItemsChange,
- onTotalAmountChange,
- availableBudget = 0,
- readOnly = false
-}: ContractItemsTableProps) {
- const [localItems, setLocalItems] = React.useState<ContractItem[]>(items)
- const [isSaving, setIsSaving] = React.useState(false)
- const [isLoading, setIsLoading] = React.useState(false)
- const [isEnabled, setIsEnabled] = React.useState(true)
-
- // 초기 데이터 로드
- React.useEffect(() => {
- const loadItems = async () => {
- try {
- setIsLoading(true)
- const fetchedItems = await getContractItems(contractId)
- const formattedItems = fetchedItems.map(item => ({
- id: item.id,
- itemCode: item.itemCode || '',
- itemInfo: item.itemInfo || '',
- specification: item.specification || '',
- quantity: Number(item.quantity) || 0,
- quantityUnit: item.quantityUnit || 'EA',
- totalWeight: Number(item.totalWeight) || 0,
- weightUnit: item.weightUnit || 'KG',
- contractDeliveryDate: item.contractDeliveryDate || '',
- contractUnitPrice: Number(item.contractUnitPrice) || 0,
- contractAmount: Number(item.contractAmount) || 0,
- contractCurrency: item.contractCurrency || 'KRW',
- isSelected: false
- })) as ContractItem[]
- setLocalItems(formattedItems as ContractItem[])
- onItemsChange(formattedItems as ContractItem[])
- } catch (error) {
- console.error('Error loading contract items:', error)
- // 기본 빈 배열로 설정
- setLocalItems([])
- onItemsChange([])
- } finally {
- setIsLoading(false)
- }
- }
-
- loadItems()
- }, [contractId, onItemsChange])
-
- // 로컬 상태와 부모 상태 동기화 (초기 로드 후에는 부모 상태 우선)
- React.useEffect(() => {
- if (items.length > 0) {
- setLocalItems(items)
- }
- }, [items])
-
- const handleSaveItems = async () => {
- try {
- setIsSaving(true)
-
- // validation 체크
- const errors: string[] = []
- for (let index = 0; index < localItems.length; index++) {
- const item = localItems[index]
- if (!item.itemCode) errors.push(`${index + 1}번째 품목의 품목코드`)
- if (!item.itemInfo) errors.push(`${index + 1}번째 품목의 Item 정보`)
- if (!item.quantity || item.quantity <= 0) errors.push(`${index + 1}번째 품목의 수량`)
- if (!item.contractUnitPrice || item.contractUnitPrice <= 0) errors.push(`${index + 1}번째 품목의 단가`)
- if (!item.contractDeliveryDate) errors.push(`${index + 1}번째 품목의 납기일`)
- }
-
- if (errors.length > 0) {
- toast.error(`다음 항목을 입력해주세요: ${errors.join(', ')}`)
- return
- }
-
- await updateContractItems(contractId, localItems as any)
- toast.success('품목정보가 저장되었습니다.')
- } catch (error) {
- console.error('Error saving contract items:', error)
- toast.error('품목정보 저장 중 오류가 발생했습니다.')
- } finally {
- setIsSaving(false)
- }
- }
-
- // 총 금액 계산
- const totalAmount = localItems.reduce((sum, item) => sum + item.contractAmount, 0)
- const totalQuantity = localItems.reduce((sum, item) => sum + item.quantity, 0)
- const totalUnitPrice = localItems.reduce((sum, item) => sum + item.contractUnitPrice, 0)
- const amountDifference = availableBudget - totalAmount
- const budgetRatio = availableBudget > 0 ? (totalAmount / availableBudget) * 100 : 0
-
- // 부모 컴포넌트에 총 금액 전달
- React.useEffect(() => {
- onTotalAmountChange(totalAmount)
- }, [totalAmount, onTotalAmountChange])
-
- // 아이템 업데이트
- const updateItem = (index: number, field: keyof ContractItem, value: string | number | boolean | undefined) => {
- const updatedItems = [...localItems]
- updatedItems[index] = { ...updatedItems[index], [field]: value }
-
- // 단가나 수량이 변경되면 금액 자동 계산
- if (field === 'contractUnitPrice' || field === 'quantity') {
- const item = updatedItems[index]
- updatedItems[index].contractAmount = item.contractUnitPrice * item.quantity
- }
-
- setLocalItems(updatedItems)
- onItemsChange(updatedItems)
- }
-
- // 행 추가
- const addRow = () => {
- const newItem: ContractItem = {
- itemCode: '',
- itemInfo: '',
- specification: '',
- quantity: 0,
- quantityUnit: 'EA', // 기본 수량 단위
- totalWeight: 0,
- weightUnit: 'KG', // 기본 중량 단위
- contractDeliveryDate: '',
- contractUnitPrice: 0,
- contractAmount: 0,
- contractCurrency: 'KRW', // 기본 통화
- isSelected: false
- }
- const updatedItems = [...localItems, newItem]
- setLocalItems(updatedItems)
- onItemsChange(updatedItems)
- }
-
- // 선택된 행 삭제
- const deleteSelectedRows = () => {
- const selectedIndices = localItems
- .map((item, index) => item.isSelected ? index : -1)
- .filter(index => index !== -1)
-
- if (selectedIndices.length === 0) {
- toast.error("삭제할 행을 선택해주세요.")
- return
- }
-
- const updatedItems = localItems.filter((_, index) => !selectedIndices.includes(index))
- setLocalItems(updatedItems)
- onItemsChange(updatedItems)
- toast.success(`${selectedIndices.length}개 행이 삭제되었습니다.`)
- }
-
- // 전체 선택/해제
- const toggleSelectAll = (checked: boolean) => {
- const updatedItems = localItems.map(item => ({ ...item, isSelected: checked }))
- setLocalItems(updatedItems)
- onItemsChange(updatedItems)
- }
-
-
- // 통화 포맷팅
- const formatCurrency = (amount: number, currency: string = 'KRW') => {
- return new Intl.NumberFormat('ko-KR', {
- style: 'currency',
- currency: currency,
- }).format(amount)
- }
-
- const allSelected = localItems.length > 0 && localItems.every(item => item.isSelected)
- const someSelected = localItems.some(item => item.isSelected)
-
- if (isLoading) {
- return (
- <Accordion type="single" collapsible className="w-full">
- <AccordionItem value="items">
- <AccordionTrigger className="hover:no-underline">
- <div className="flex items-center gap-2">
- <Package className="w-5 h-5" />
- <span>품목 정보</span>
- <span className="text-sm text-gray-500">(로딩 중...)</span>
- </div>
- </AccordionTrigger>
- <AccordionContent>
- <div className="flex items-center justify-center py-8">
- <LoaderIcon className="w-6 h-6 animate-spin mr-2" />
- <span>품목 정보를 불러오는 중...</span>
- </div>
- </AccordionContent>
- </AccordionItem>
- </Accordion>
- )
- }
-
- return (
- <Accordion type="single" collapsible className="w-full">
- <AccordionItem value="items">
- <AccordionTrigger className="hover:no-underline">
- <div className="flex items-center gap-3 w-full">
- <Package className="w-5 h-5" />
- <span className="font-medium">품목 정보</span>
- <span className="text-sm text-gray-500">({localItems.length}개 품목)</span>
- </div>
- </AccordionTrigger>
- <AccordionContent>
- <Card>
- <CardHeader>
- {/* 체크박스 */}
- <div className="flex items-center gap-2 mb-4">
- <Checkbox
- checked={isEnabled}
- onCheckedChange={(checked) => setIsEnabled(checked as boolean)}
- disabled={readOnly}
- />
- <span className="text-sm font-medium">품목 정보 활성화</span>
- </div>
-
- <div className="flex items-center justify-between">
- <div className="flex items-center gap-2">
- <span className="text-sm text-gray-600">총 금액: {formatCurrency(totalAmount, localItems[0]?.contractCurrency || 'KRW')}</span>
- <span className="text-sm text-gray-600">총 수량: {totalQuantity.toLocaleString()}</span>
- </div>
- {!readOnly && (
- <div className="flex items-center gap-2">
- <Button
- variant="outline"
- size="sm"
- onClick={addRow}
- disabled={!isEnabled}
- className="flex items-center gap-2"
- >
- <Plus className="w-4 h-4" />
- 행 추가
- </Button>
- <Button
- variant="outline"
- size="sm"
- onClick={deleteSelectedRows}
- disabled={!isEnabled}
- className="flex items-center gap-2 text-red-600 hover:text-red-700"
- >
- <Trash2 className="w-4 h-4" />
- 행 삭제
- </Button>
- <Button
- onClick={handleSaveItems}
- disabled={isSaving || !isEnabled}
- className="flex items-center gap-2"
- >
- {isSaving ? (
- <LoaderIcon className="w-4 h-4 animate-spin" />
- ) : (
- <Save className="w-4 h-4" />
- )}
- 품목정보 저장
- </Button>
- </div>
- )}
- </div>
-
- {/* 요약 정보 */}
- <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-4">
- <div className="space-y-1">
- <Label className="text-sm font-medium">총 계약금액</Label>
- <div className="text-lg font-bold text-primary">
- {formatCurrency(totalAmount, localItems[0]?.contractCurrency || 'KRW')}
- </div>
- </div>
- <div className="space-y-1">
- <Label className="text-sm font-medium">가용예산</Label>
- <div className="text-lg font-bold">
- {formatCurrency(availableBudget, localItems[0]?.contractCurrency || 'KRW')}
- </div>
- </div>
- <div className="space-y-1">
- <Label className="text-sm font-medium">가용예산 比 (금액차)</Label>
- <div className={`text-lg font-bold ${amountDifference >= 0 ? 'text-green-600' : 'text-red-600'}`}>
- {formatCurrency(amountDifference, localItems[0]?.contractCurrency || 'KRW')}
- </div>
- </div>
- <div className="space-y-1">
- <Label className="text-sm font-medium">가용예산 比 (비율)</Label>
- <div className={`text-lg font-bold ${budgetRatio <= 100 ? 'text-green-600' : 'text-red-600'}`}>
- {budgetRatio.toFixed(1)}%
- </div>
- </div>
- </div>
- </CardHeader>
-
- <CardContent>
- <div className="overflow-x-auto">
- <Table>
- <TableHeader>
- <TableRow className="border-b-2">
- <TableHead className="w-12 px-2">
- {!readOnly && (
- <Checkbox
- checked={allSelected}
- ref={(el) => {
- if (el) (el as HTMLInputElement & { indeterminate?: boolean }).indeterminate = someSelected && !allSelected
- }}
- onCheckedChange={toggleSelectAll}
- disabled={!isEnabled}
- />
- )}
- </TableHead>
- <TableHead className="px-3 py-3 font-semibold">품목코드 (PKG No.)</TableHead>
- <TableHead className="px-3 py-3 font-semibold">Item 정보 (자재그룹 / 자재코드)</TableHead>
- <TableHead className="px-3 py-3 font-semibold">규격</TableHead>
- <TableHead className="px-3 py-3 font-semibold text-right">수량</TableHead>
- <TableHead className="px-3 py-3 font-semibold">수량단위</TableHead>
- <TableHead className="px-3 py-3 font-semibold text-right">총 중량</TableHead>
- <TableHead className="px-3 py-3 font-semibold">중량단위</TableHead>
- <TableHead className="px-3 py-3 font-semibold">계약납기일</TableHead>
- <TableHead className="px-3 py-3 font-semibold text-right">계약단가</TableHead>
- <TableHead className="px-3 py-3 font-semibold text-right">계약금액</TableHead>
- <TableHead className="px-3 py-3 font-semibold">계약통화</TableHead>
- </TableRow>
- </TableHeader>
- <TableBody>
- {localItems.map((item, index) => (
- <TableRow key={index} className="hover:bg-muted/30 transition-colors">
- <TableCell className="px-2">
- {!readOnly && (
- <Checkbox
- checked={item.isSelected || false}
- onCheckedChange={(checked) =>
- updateItem(index, 'isSelected', checked)
- }
- disabled={!isEnabled}
- />
- )}
- </TableCell>
- <TableCell className="px-3 py-3">
- {readOnly ? (
- <span className="text-sm">{item.itemCode || '-'}</span>
- ) : (
- <Input
- value={item.itemCode}
- onChange={(e) => updateItem(index, 'itemCode', e.target.value)}
- placeholder="품목코드"
- className="h-8 text-sm"
- disabled={!isEnabled}
- />
- )}
- </TableCell>
- <TableCell className="px-3 py-3">
- {readOnly ? (
- <span className="text-sm">{item.itemInfo || '-'}</span>
- ) : (
- <Input
- value={item.itemInfo}
- onChange={(e) => updateItem(index, 'itemInfo', e.target.value)}
- placeholder="Item 정보"
- className="h-8 text-sm"
- disabled={!isEnabled}
- />
- )}
- </TableCell>
- <TableCell className="px-3 py-3">
- {readOnly ? (
- <span className="text-sm">{item.specification || '-'}</span>
- ) : (
- <Input
- value={item.specification}
- onChange={(e) => updateItem(index, 'specification', e.target.value)}
- placeholder="규격"
- className="h-8 text-sm"
- disabled={!isEnabled}
- />
- )}
- </TableCell>
- <TableCell className="px-3 py-3">
- {readOnly ? (
- <span className="text-sm text-right">{item.quantity.toLocaleString()}</span>
- ) : (
- <Input
- type="number"
- value={item.quantity}
- onChange={(e) => updateItem(index, 'quantity', parseFloat(e.target.value) || 0)}
- className="h-8 text-sm text-right"
- placeholder="0"
- disabled={!isEnabled}
- />
- )}
- </TableCell>
- <TableCell className="px-3 py-3">
- {readOnly ? (
- <span className="text-sm">{item.quantityUnit || '-'}</span>
- ) : (
- <Select
- value={item.quantityUnit}
- onValueChange={(value) => updateItem(index, 'quantityUnit', value)}
- disabled={!isEnabled}
- >
- <SelectTrigger className="h-8 text-sm w-20">
- <SelectValue />
- </SelectTrigger>
- <SelectContent>
- {QUANTITY_UNITS.map((unit) => (
- <SelectItem key={unit} value={unit}>
- {unit}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- )}
- </TableCell>
- <TableCell className="px-3 py-3">
- {readOnly ? (
- <span className="text-sm text-right">{item.totalWeight.toLocaleString()}</span>
- ) : (
- <Input
- type="number"
- value={item.totalWeight}
- onChange={(e) => updateItem(index, 'totalWeight', parseFloat(e.target.value) || 0)}
- className="h-8 text-sm text-right"
- placeholder="0"
- disabled={!isEnabled}
- />
- )}
- </TableCell>
- <TableCell className="px-3 py-3">
- {readOnly ? (
- <span className="text-sm">{item.weightUnit || '-'}</span>
- ) : (
- <Select
- value={item.weightUnit}
- onValueChange={(value) => updateItem(index, 'weightUnit', value)}
- disabled={!isEnabled}
- >
- <SelectTrigger className="h-8 text-sm w-20">
- <SelectValue />
- </SelectTrigger>
- <SelectContent>
- {WEIGHT_UNITS.map((unit) => (
- <SelectItem key={unit} value={unit}>
- {unit}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- )}
- </TableCell>
- <TableCell className="px-3 py-3">
- {readOnly ? (
- <span className="text-sm">{item.contractDeliveryDate || '-'}</span>
- ) : (
- <Input
- type="date"
- value={item.contractDeliveryDate}
- onChange={(e) => updateItem(index, 'contractDeliveryDate', e.target.value)}
- className="h-8 text-sm"
- disabled={!isEnabled}
- />
- )}
- </TableCell>
- <TableCell className="px-3 py-3">
- {readOnly ? (
- <span className="text-sm text-right">{item.contractUnitPrice.toLocaleString()}</span>
- ) : (
- <Input
- type="number"
- value={item.contractUnitPrice}
- onChange={(e) => updateItem(index, 'contractUnitPrice', parseFloat(e.target.value) || 0)}
- className="h-8 text-sm text-right"
- placeholder="0"
- disabled={!isEnabled}
- />
- )}
- </TableCell>
- <TableCell className="px-3 py-3">
- <div className="font-semibold text-primary text-right text-sm">
- {formatCurrency(item.contractAmount)}
- </div>
- </TableCell>
- <TableCell className="px-3 py-3">
- {readOnly ? (
- <span className="text-sm">{item.contractCurrency || '-'}</span>
- ) : (
- <Select
- value={item.contractCurrency}
- onValueChange={(value) => updateItem(index, 'contractCurrency', value)}
- disabled={!isEnabled}
- >
- <SelectTrigger className="h-8 text-sm w-20">
- <SelectValue />
- </SelectTrigger>
- <SelectContent>
- {CURRENCIES.map((currency) => (
- <SelectItem key={currency} value={currency}>
- {currency}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- )}
- </TableCell>
- </TableRow>
- ))}
- </TableBody>
- </Table>
- </div>
-
- {/* 합계 정보 */}
- {localItems.length > 0 && (
- <div className="mt-6 flex justify-end">
- <Card className="w-80 bg-gradient-to-r from-primary/5 to-primary/10 border-primary/20">
- <CardContent className="p-6">
- <div className="space-y-4">
- <div className="flex items-center justify-between">
- <span className="text-sm font-medium text-muted-foreground">총 수량</span>
- <span className="text-lg font-semibold">
- {totalQuantity.toLocaleString()} {localItems[0]?.quantityUnit || 'KG'}
- </span>
- </div>
- <div className="flex items-center justify-between">
- <span className="text-sm font-medium text-muted-foreground">총 단가</span>
- <span className="text-lg font-semibold">
- {formatCurrency(totalUnitPrice, localItems[0]?.contractCurrency || 'KRW')}
- </span>
- </div>
- <div className="border-t pt-4">
- <div className="flex items-center justify-between">
- <span className="text-xl font-bold text-primary">합계 금액</span>
- <span className="text-2xl font-bold text-primary">
- {formatCurrency(totalAmount, localItems[0]?.contractCurrency || 'KRW')}
- </span>
- </div>
- </div>
- </div>
- </CardContent>
- </Card>
- </div>
- )}
- </CardContent>
- </Card>
- </AccordionContent>
- </AccordionItem>
- </Accordion>
- )
-}
diff --git a/lib/general-contracts_old/detail/general-contract-location.tsx b/lib/general-contracts_old/detail/general-contract-location.tsx
deleted file mode 100644
index 5b388895..00000000
--- a/lib/general-contracts_old/detail/general-contract-location.tsx
+++ /dev/null
@@ -1,480 +0,0 @@
-'use client'
-
-import React, { useState, useEffect } from 'react'
-import { useSession } from 'next-auth/react'
-import { Input } from '@/components/ui/input'
-import { Button } from '@/components/ui/button'
-import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
-import { Checkbox } from '@/components/ui/checkbox'
-import { Save, LoaderIcon, MapPin } from 'lucide-react'
-import { updateLocation, getLocation } from '../service'
-import { toast } from 'sonner'
-
-interface LocationProps {
- contractType?: string
- contractId: number
-}
-
-interface LocationData {
- country: {
- projectManager: string
- engineering: string
- procurement: string
- fabrication: string
- assembly: string
- test: string
- shippingExw: string
- shippingFob: string
- remark: string
- }
- location: {
- projectManager: string
- engineering: string
- procurement: string
- fabrication: string
- assembly: string
- test: string
- shippingExw: string
- shippingFob: string
- remark: string
- }
- subContractor: {
- projectManager: string
- engineering: string
- procurement: string
- fabrication: string
- assembly: string
- test: string
- shippingExw: string
- shippingFob: string
- remark: string
- }
-}
-
-export function Location({ contractId }: LocationProps) {
- const session = useSession()
- const [isLoading, setIsLoading] = useState(false)
- const [isEnabled, setIsEnabled] = useState(true)
- const [locationData, setLocationData] = useState<LocationData>({
- country: {
- projectManager: '',
- engineering: '',
- procurement: '',
- fabrication: '',
- assembly: '',
- test: '',
- shippingExw: '',
- shippingFob: '',
- remark: ''
- },
- location: {
- projectManager: '',
- engineering: '',
- procurement: '',
- fabrication: '',
- assembly: '',
- test: '',
- shippingExw: '',
- shippingFob: '',
- remark: ''
- },
- subContractor: {
- projectManager: '',
- engineering: '',
- procurement: '',
- fabrication: '',
- assembly: '',
- test: '',
- shippingExw: '',
- shippingFob: '',
- remark: ''
- }
- })
-
- // 특정 계약종류를 제외한 일반계약은 Default로 표시
- const isDisabled = false // 일단 모든 계약종류에서 활성화
-
- // 초기 데이터 로드
- useEffect(() => {
- const loadLocationData = async () => {
- try {
- const data = await getLocation(contractId)
- if (data && data.locations) {
- setLocationData(data.locations)
- setIsEnabled(data.enabled || true)
- } else {
- // 기본 데이터는 이미 useState에서 설정됨
- }
- } catch (error) {
- console.error('Error loading location data:', error)
- // 기본 데이터는 이미 useState에서 설정됨
- }
- }
-
- loadLocationData()
- }, [contractId])
-
- const updateLocationData = (rowType: keyof LocationData, field: keyof LocationData['country'], value: string) => {
- setLocationData(prev => ({
- ...prev,
- [rowType]: {
- ...prev[rowType],
- [field]: value
- }
- }))
- }
-
- const handleSaveLocation = async () => {
- const userId = session.data?.user?.id ? Number(session.data.user.id) : null
-
- if (!userId) {
- toast.error('사용자 정보를 찾을 수 없습니다.')
- return
- }
-
- try {
- setIsLoading(true)
-
- const locationDataToSave = {
- enabled: isEnabled,
- locations: locationData
- }
-
- await updateLocation(contractId, locationDataToSave, userId)
- toast.success('Location 정보가 저장되었습니다.')
- } catch (error) {
- console.error('Error saving location:', error)
- toast.error('Location 정보 저장에 실패했습니다.')
- } finally {
- setIsLoading(false)
- }
- }
-
- return (
- <div className="w-full">
- <Accordion type="single" collapsible className="w-full">
- <AccordionItem value="location">
- <AccordionTrigger className="hover:no-underline">
- <div className="flex items-center gap-3 w-full">
- <MapPin className="w-5 h-5" />
- <span className="font-medium">Location</span>
- </div>
- </AccordionTrigger>
- <AccordionContent>
- <div className="space-y-6">
- {/* 체크박스 */}
- <div className="flex items-center gap-2">
- <Checkbox
- checked={isEnabled}
- disabled={isDisabled}
- onCheckedChange={(checked) => {
- if (!isDisabled) {
- setIsEnabled(checked as boolean)
- }
- }}
- />
- <span className="text-sm font-medium">Location 활성화</span>
- </div>
-
- {/* Location 테이블 */}
- <div className="space-y-4">
- <h3 className="text-lg font-medium">Location</h3>
-
- <div className="overflow-x-auto">
- <table className={`w-full border-collapse border border-gray-300 ${!isEnabled ? 'opacity-50' : ''}`}>
- <thead>
- <tr className="bg-yellow-100">
- <th className="border border-gray-300 p-2">Activity</th>
- <th className="border border-gray-300 p-2">Project Manager</th>
- <th className="border border-gray-300 p-2">Engineering</th>
- <th className="border border-gray-300 p-2">Procurement</th>
- <th className="border border-gray-300 p-2">Fabrication</th>
- <th className="border border-gray-300 p-2">Assembly</th>
- <th className="border border-gray-300 p-2">Test (FAT)</th>
- <th className="border border-gray-300 p-2">Shipping (EXW)</th>
- <th className="border border-gray-300 p-2">Shipping (FOB)</th>
- <th className="border border-gray-300 p-2">Remark</th>
- </tr>
- </thead>
- <tbody>
- {/* Country Row */}
- <tr className="bg-yellow-50">
- <td className="border border-gray-300 p-2 font-medium">Country</td>
- <td className="border border-gray-300 p-2">
- <Input
- value={locationData?.country?.projectManager || ''}
- onChange={(e) => updateLocationData('country', 'projectManager', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- placeholder="국가"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={locationData?.country?.engineering || ''}
- onChange={(e) => updateLocationData('country', 'engineering', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- placeholder="국가"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={locationData?.country.procurement}
- onChange={(e) => updateLocationData('country', 'procurement', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- placeholder="국가"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={locationData?.country.fabrication}
- onChange={(e) => updateLocationData('country', 'fabrication', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- placeholder="국가"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={locationData?.country.assembly}
- onChange={(e) => updateLocationData('country', 'assembly', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- placeholder="국가"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={locationData?.country.test}
- onChange={(e) => updateLocationData('country', 'test', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- placeholder="국가"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={locationData?.country.shippingExw}
- onChange={(e) => updateLocationData('country', 'shippingExw', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- placeholder="국가"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={locationData?.country.shippingFob}
- onChange={(e) => updateLocationData('country', 'shippingFob', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- placeholder="국가"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={locationData?.country.remark}
- onChange={(e) => updateLocationData('country', 'remark', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- />
- </td>
- </tr>
-
- {/* Location (City) Row */}
- <tr className="bg-yellow-50">
- <td className="border border-gray-300 p-2 font-medium">Location (City)</td>
- <td className="border border-gray-300 p-2">
- <Input
- value={locationData?.location.projectManager}
- onChange={(e) => updateLocationData('location', 'projectManager', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- placeholder="국가 내 지역"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={locationData?.location.engineering}
- onChange={(e) => updateLocationData('location', 'engineering', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- placeholder="국가 내 지역"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={locationData?.location.procurement}
- onChange={(e) => updateLocationData('location', 'procurement', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- placeholder="국가 내 지역"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={locationData?.location.fabrication}
- onChange={(e) => updateLocationData('location', 'fabrication', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- placeholder="국가 내 지역"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={locationData?.location.assembly}
- onChange={(e) => updateLocationData('location', 'assembly', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- placeholder="국가 내 지역"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={locationData?.location.test}
- onChange={(e) => updateLocationData('location', 'test', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- placeholder="국가 내 지역"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={locationData?.location.shippingExw}
- onChange={(e) => updateLocationData('location', 'shippingExw', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- placeholder="국가 내 지역"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={locationData?.location.shippingFob}
- onChange={(e) => updateLocationData('location', 'shippingFob', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- placeholder="국가 내 지역"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={locationData?.location.remark}
- onChange={(e) => updateLocationData('location', 'remark', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- />
- </td>
- </tr>
-
- {/* Sub-Contractor Row */}
- <tr className="bg-yellow-50">
- <td className="border border-gray-300 p-2 font-medium">Sub-Contractor<br />(where applicable)</td>
- <td className="border border-gray-300 p-2">
- <Input
- value={locationData?.subContractor?.projectManager || ''}
- onChange={(e) => updateLocationData('subContractor', 'projectManager', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- placeholder="국가 및 지역"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={locationData?.subContractor?.engineering || ''}
- onChange={(e) => updateLocationData('subContractor', 'engineering', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- placeholder="국가 및 지역"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={locationData?.subContractor?.procurement || ''}
- onChange={(e) => updateLocationData('subContractor', 'procurement', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- placeholder="국가 및 지역"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={locationData?.subContractor?.fabrication || ''}
- onChange={(e) => updateLocationData('subContractor', 'fabrication', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- placeholder="국가 및 지역"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={locationData?.subContractor?.assembly || ''}
- onChange={(e) => updateLocationData('subContractor', 'assembly', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- placeholder="국가 및 지역"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={locationData?.subContractor?.test || ''}
- onChange={(e) => updateLocationData('subContractor', 'test', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- placeholder="국가 및 지역"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={locationData?.subContractor?.shippingExw || ''}
- onChange={(e) => updateLocationData('subContractor', 'shippingExw', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- placeholder="국가 및 지역"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={locationData?.subContractor?.shippingFob || ''}
- onChange={(e) => updateLocationData('subContractor', 'shippingFob', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- placeholder="국가 및 지역"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={locationData?.subContractor?.remark || ''}
- onChange={(e) => updateLocationData('subContractor', 'remark', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- />
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- </div>
-
- {/* 저장 버튼 */}
- <div className="flex justify-end pt-4 border-t">
- <Button
- onClick={handleSaveLocation}
- disabled={isLoading || isDisabled || !isEnabled}
- className="flex items-center gap-2"
- >
- {isLoading ? (
- <LoaderIcon className="w-4 h-4 animate-spin" />
- ) : (
- <Save className="w-4 h-4" />
- )}
- Location 저장
- </Button>
- </div>
- </div>
- </AccordionContent>
- </AccordionItem>
- </Accordion>
- </div>
- )
-} \ No newline at end of file
diff --git a/lib/general-contracts_old/detail/general-contract-offset-details.tsx b/lib/general-contracts_old/detail/general-contract-offset-details.tsx
deleted file mode 100644
index af4f2ef2..00000000
--- a/lib/general-contracts_old/detail/general-contract-offset-details.tsx
+++ /dev/null
@@ -1,314 +0,0 @@
-'use client'
-
-import React, { useState, useEffect } from 'react'
-import { useSession } from 'next-auth/react'
-import { Input } from '@/components/ui/input'
-import { Button } from '@/components/ui/button'
-import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
-import { Checkbox } from '@/components/ui/checkbox'
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
-import { Plus, Trash2, Save, LoaderIcon, RotateCcw } from 'lucide-react'
-import { getOffsetDetails, updateOffsetDetails } from '../service'
-import { toast } from 'sonner'
-
-interface OffsetDetailsProps {
- contractId: number
- contractType?: string
-}
-
-interface OffsetDetailItem {
- id: string
- project: string
- poNumber: string
- poItemDescription: string
- offsetReason: string
- contractCurrency: string
- contractAmount: string
- offsetDate: string
- remark: string
-}
-
-export function OffsetDetails({ contractId }: OffsetDetailsProps) {
- const session = useSession()
- const [isLoading, setIsLoading] = useState(false)
- const [isEnabled, setIsEnabled] = useState(true)
-
- // 특정 계약종류를 제외한 일반계약은 Default로 표시
- const isDisabled = false
-
- const [offsetDetails, setOffsetDetails] = useState<OffsetDetailItem[]>([])
-
- // 회입/상계사유 옵션
- const offsetReasonOptions = [
- '판매자 사양불만족',
- '납기지연',
- '품질불량',
- '계약조건변경',
- '기타'
- ]
-
- // 초기 데이터 로드
- useEffect(() => {
- const loadOffsetDetails = async () => {
- try {
- const data = await getOffsetDetails(contractId)
- if (data && data.enabled !== undefined) {
- setIsEnabled(data.enabled)
- setOffsetDetails(data.offsetDetails || [])
- } else {
- }
- } catch (error) {
- console.error('회입/상계내역 데이터 로드 실패:', error)
- toast.error('회입/상계내역 데이터를 불러오는데 실패했습니다.')
- }
- }
-
- loadOffsetDetails()
- }, [contractId])
-
- const addOffsetDetailRow = () => {
- const newRow: OffsetDetailItem = {
- id: Date.now().toString(),
- project: '',
- poNumber: '',
- poItemDescription: '',
- offsetReason: '',
- contractCurrency: 'KRW',
- contractAmount: '',
- offsetDate: '',
- remark: ''
- }
- setOffsetDetails([...offsetDetails, newRow])
- }
-
- const removeOffsetDetailRow = (id: string) => {
- setOffsetDetails(offsetDetails.filter(item => item.id !== id))
- }
-
- const updateOffsetDetailData = (id: string, field: keyof OffsetDetailItem, value: string) => {
- setOffsetDetails(prev =>
- prev.map(item =>
- item.id === id ? { ...item, [field]: value } : item
- )
- )
- }
-
- const handleSaveOffsetDetails = async () => {
- if (!session.data?.user?.id) {
- toast.error('로그인이 필요합니다.')
- return
- }
-
- setIsLoading(true)
- try {
- const offsetDetailsData = {
- enabled: isEnabled,
- offsetDetails: offsetDetails
- }
-
- await updateOffsetDetails(contractId, offsetDetailsData, Number(session.data.user.id))
- toast.success('회입/상계내역이 성공적으로 저장되었습니다.')
- } catch (error) {
- console.error('회입/상계내역 저장 실패:', error)
- toast.error('회입/상계내역 저장에 실패했습니다.')
- } finally {
- setIsLoading(false)
- }
- }
-
- return (
- <div className="w-full">
- <Accordion type="single" collapsible className="w-full">
- <AccordionItem value="offset-details">
- <AccordionTrigger className="hover:no-underline">
- <div className="flex items-center gap-3 w-full">
- <RotateCcw className="w-5 h-5" />
- <span className="font-medium">회입/상계내역</span>
- </div>
- </AccordionTrigger>
- <AccordionContent>
- <div className="space-y-6">
- {/* 체크박스 */}
- <div className="flex items-center gap-2">
- <Checkbox
- checked={isEnabled}
- disabled={isDisabled}
- onCheckedChange={(checked) => {
- if (!isDisabled) {
- setIsEnabled(checked as boolean)
- }
- }}
- />
- <span className="text-sm font-medium">회입/상계내역 활성화</span>
- </div>
-
- {/* 회입/상계내역 테이블 */}
- <div className="space-y-4">
- <div className="flex items-center justify-between">
- <h3 className="text-lg font-medium">회입/상계내역</h3>
- <div className="flex gap-2">
- <Button
- type="button"
- variant="outline"
- size="sm"
- onClick={addOffsetDetailRow}
- disabled={isDisabled || !isEnabled}
- className="flex items-center gap-2"
- >
- <Plus className="w-4 h-4" />
- 행 추가
- </Button>
- <Button
- type="button"
- variant="outline"
- size="sm"
- onClick={() => {
- if (confirm('선택된 행들을 삭제하시겠습니까?')) {
- // 선택된 행들 삭제 로직 (필요시 구현)
- }
- }}
- disabled={isDisabled || !isEnabled}
- className="flex items-center gap-2"
- >
- <Trash2 className="w-4 h-4" />
- 행 삭제
- </Button>
- </div>
- </div>
-
- <div className="overflow-x-auto">
- <table className={`w-full border-collapse border border-gray-300 ${!isEnabled ? 'opacity-50' : ''}`}>
- <thead>
- <tr className="bg-yellow-100">
- <th className="border border-gray-300 p-2 w-16">No.</th>
- <th className="border border-gray-300 p-2 w-32">프로젝트</th>
- <th className="border border-gray-300 p-2 w-40">발주번호</th>
- <th className="border border-gray-300 p-2">발주품목(Description)</th>
- <th className="border border-gray-300 p-2 w-40">회입/상계사유</th>
- <th className="border border-gray-300 p-2 w-32">계약통화</th>
- <th className="border border-gray-300 p-2 w-32">계약금액</th>
- <th className="border border-gray-300 p-2 w-32">회입/상계일</th>
- <th className="border border-gray-300 p-2">비고</th>
- <th className="border border-gray-300 p-2 w-20">Action</th>
- </tr>
- </thead>
- <tbody>
- {offsetDetails.map((item, index) => (
- <tr key={item.id} className="bg-yellow-50">
- <td className="border border-gray-300 p-2 text-center">{index + 1}</td>
- <td className="border border-gray-300 p-2">
- <Input
- value={item.project}
- onChange={(e) => updateOffsetDetailData(item.id, 'project', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={item.poNumber}
- onChange={(e) => updateOffsetDetailData(item.id, 'poNumber', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={item.poItemDescription}
- onChange={(e) => updateOffsetDetailData(item.id, 'poItemDescription', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Select
- value={item.offsetReason}
- onValueChange={(value) => updateOffsetDetailData(item.id, 'offsetReason', value)}
- disabled={isDisabled || !isEnabled}
- >
- <SelectTrigger className="border-0 bg-transparent p-0 h-auto">
- <SelectValue placeholder="선택하세요" />
- </SelectTrigger>
- <SelectContent>
- {offsetReasonOptions.map((option) => (
- <SelectItem key={option} value={option}>
- {option}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={item.contractCurrency}
- onChange={(e) => updateOffsetDetailData(item.id, 'contractCurrency', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={item.contractAmount}
- onChange={(e) => updateOffsetDetailData(item.id, 'contractAmount', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- type="date"
- value={item.offsetDate}
- onChange={(e) => updateOffsetDetailData(item.id, 'offsetDate', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- />
- </td>
- <td className="border border-gray-300 p-2">
- <Input
- value={item.remark}
- onChange={(e) => updateOffsetDetailData(item.id, 'remark', e.target.value)}
- disabled={isDisabled || !isEnabled}
- className="border-0 bg-transparent p-0 h-auto"
- />
- </td>
- <td className="border border-gray-300 p-2 text-center">
- <Button
- type="button"
- variant="ghost"
- size="sm"
- onClick={() => removeOffsetDetailRow(item.id)}
- disabled={isDisabled || !isEnabled}
- className="h-8 w-8 p-0 text-red-600 hover:text-red-700"
- >
- <Trash2 className="w-4 h-4" />
- </Button>
- </td>
- </tr>
- ))}
- </tbody>
- </table>
- </div>
- </div>
-
- {/* 저장 버튼 */}
- <div className="flex justify-end pt-4 border-t">
- <Button
- onClick={handleSaveOffsetDetails}
- disabled={isLoading || isDisabled || !isEnabled}
- className="flex items-center gap-2"
- >
- {isLoading ? (
- <LoaderIcon className="w-4 h-4 animate-spin" />
- ) : (
- <Save className="w-4 h-4" />
- )}
- 회입/상계내역 저장
- </Button>
- </div>
- </div>
- </AccordionContent>
- </AccordionItem>
- </Accordion>
- </div>
- )
-}
diff --git a/lib/general-contracts_old/detail/general-contract-subcontract-checklist.tsx b/lib/general-contracts_old/detail/general-contract-subcontract-checklist.tsx
deleted file mode 100644
index ce7c8baf..00000000
--- a/lib/general-contracts_old/detail/general-contract-subcontract-checklist.tsx
+++ /dev/null
@@ -1,610 +0,0 @@
-'use client'
-
-import React, { useState } from 'react'
-import { Card, CardContent } from '@/components/ui/card'
-import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
-import { Checkbox } from '@/components/ui/checkbox'
-import { Label } from '@/components/ui/label'
-import { Badge } from '@/components/ui/badge'
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
-import { Textarea } from '@/components/ui/textarea'
-import { Alert, AlertDescription } from '@/components/ui/alert'
-import { Button } from '@/components/ui/button'
-import { updateSubcontractChecklist } from '../service'
-import { toast } from 'sonner'
-import { AlertTriangle, CheckCircle, XCircle, HelpCircle, Save } from 'lucide-react'
-
-interface SubcontractChecklistData {
- // 1. 계약서면발급
- contractDocumentIssuance: {
- workOrderBeforeStart: boolean
- entrustmentDetails: boolean
- deliveryDetails: boolean
- inspectionMethod: boolean
- subcontractPayment: boolean
- materialProvision: boolean
- priceAdjustment: boolean
- }
- // 2. 부당하도급대금결정행위
- unfairSubcontractPricing: {
- priceReductionWithBasis: boolean
- noNegotiationAfterLowestBid: boolean
- noDeceptionInPricing: boolean
- noUniformPriceReduction: boolean
- noDiscriminatoryTreatment: boolean
- }
- // 점검결과
- inspectionResult: 'compliant' | 'violation' | 'suspected_violation'
- // 귀책부서 (위반/위반의심 시 필수)
- responsibleDepartment?: string
- // 원인 (위반/위반의심 시 필수)
- cause?: string
- causeOther?: string
- // 대책 (위반/위반의심 시 필수)
- countermeasure?: string
- countermeasureOther?: string
-}
-
-interface SubcontractChecklistProps {
- contractId: number
- onDataChange: (data: SubcontractChecklistData) => void
- readOnly?: boolean
- initialData?: SubcontractChecklistData
-}
-
-export function SubcontractChecklist({ contractId, onDataChange, readOnly = false, initialData }: SubcontractChecklistProps) {
- // 기본 데이터 구조
- const defaultData: SubcontractChecklistData = {
- contractDocumentIssuance: {
- workOrderBeforeStart: false,
- entrustmentDetails: false,
- deliveryDetails: false,
- inspectionMethod: false,
- subcontractPayment: false,
- materialProvision: false,
- priceAdjustment: false,
- },
- unfairSubcontractPricing: {
- priceReductionWithBasis: false,
- noNegotiationAfterLowestBid: false,
- noDeceptionInPricing: false,
- noUniformPriceReduction: false,
- noDiscriminatoryTreatment: false,
- },
- inspectionResult: 'compliant',
- }
-
- // initialData와 기본값을 깊이 병합
- const mergedInitialData = React.useMemo(() => {
- if (!initialData) return defaultData
-
- return {
- contractDocumentIssuance: {
- ...defaultData.contractDocumentIssuance,
- ...(initialData.contractDocumentIssuance || {}),
- },
- unfairSubcontractPricing: {
- ...defaultData.unfairSubcontractPricing,
- ...(initialData.unfairSubcontractPricing || {}),
- },
- inspectionResult: initialData.inspectionResult || defaultData.inspectionResult,
- responsibleDepartment: initialData.responsibleDepartment,
- cause: initialData.cause,
- causeOther: initialData.causeOther,
- countermeasure: initialData.countermeasure,
- countermeasureOther: initialData.countermeasureOther,
- }
- }, [initialData])
-
- const [isEnabled, setIsEnabled] = useState(true)
- const [data, setData] = useState<SubcontractChecklistData>(mergedInitialData)
-
- // 점검결과 자동 계산 함수
- const calculateInspectionResult = (
- contractDocumentIssuance: SubcontractChecklistData['contractDocumentIssuance'],
- unfairSubcontractPricing: SubcontractChecklistData['unfairSubcontractPricing']
- ): 'compliant' | 'violation' | 'suspected_violation' => {
- // 1. 계약서면발급의 모든 항목이 체크되어야 함
- const allContractItemsChecked = Object.values(contractDocumentIssuance).every(checked => checked)
-
- // 2. 부당하도급대금결정행위에서 'X' 항목 체크 확인
- const hasUnfairPricingViolation = Object.values(unfairSubcontractPricing).some(checked => !checked)
-
- if (!allContractItemsChecked) {
- return 'violation'
- } else if (hasUnfairPricingViolation) {
- return 'suspected_violation'
- }
-
- return 'compliant'
- }
-
- const handleContractDocumentChange = (field: keyof SubcontractChecklistData['contractDocumentIssuance'], checked: boolean) => {
- setData(prev => {
- const newContractDocumentIssuance = {
- ...prev.contractDocumentIssuance,
- [field]: checked
- }
- const newInspectionResult = calculateInspectionResult(newContractDocumentIssuance, prev.unfairSubcontractPricing)
-
- return {
- ...prev,
- contractDocumentIssuance: newContractDocumentIssuance,
- inspectionResult: newInspectionResult
- }
- })
- }
-
- const handleUnfairPricingChange = (field: keyof SubcontractChecklistData['unfairSubcontractPricing'], checked: boolean) => {
- setData(prev => {
- const newUnfairSubcontractPricing = {
- ...prev.unfairSubcontractPricing,
- [field]: checked
- }
- const newInspectionResult = calculateInspectionResult(prev.contractDocumentIssuance, newUnfairSubcontractPricing)
-
- return {
- ...prev,
- unfairSubcontractPricing: newUnfairSubcontractPricing,
- inspectionResult: newInspectionResult
- }
- })
- }
-
- const handleFieldChange = (field: keyof SubcontractChecklistData, value: string) => {
- setData(prev => ({ ...prev, [field]: value }))
- }
-
- // 데이터 변경 시 부모 컴포넌트에 전달 (저장 시에만)
- const handleSave = async () => {
- try {
- // validation 체크
- const errors = []
-
- // 위반 또는 위반의심인 경우 필수 필드 체크
- if (data.inspectionResult === 'violation' || data.inspectionResult === 'suspected_violation') {
- if (!data.responsibleDepartment) errors.push('귀책부서')
- if (!data.cause) errors.push('원인')
- if (!data.countermeasure) errors.push('대책')
-
- // 기타 선택 시 추가 입력 필드 체크
- if (data.cause === '기타' && !data.causeOther) errors.push('원인 기타 입력')
- if (data.countermeasure === '기타' && !data.countermeasureOther) errors.push('대책 기타 입력')
- }
-
- if (errors.length > 0) {
- toast.error(`다음 항목을 입력해주세요: ${errors.join(', ')}`)
- return
- }
-
- await updateSubcontractChecklist(contractId, data)
- onDataChange(data)
- toast.success('하도급법 체크리스트가 저장되었습니다.')
- } catch (error) {
- console.error('Error saving subcontract checklist:', error)
- toast.error('하도급법 체크리스트 저장 중 오류가 발생했습니다.')
- }
- }
-
- const getInspectionResultInfo = () => {
- switch (data.inspectionResult) {
- case 'compliant':
- return {
- icon: <CheckCircle className="h-5 w-5 text-green-600" />,
- label: '준수',
- color: 'bg-green-100 text-green-800',
- description: '1. 계약서면발급의 모든 항목에 체크, 2. 부당하도급에서 X항목에 체크한 상태'
- }
- case 'violation':
- return {
- icon: <XCircle className="h-5 w-5 text-red-600" />,
- label: '위반',
- color: 'bg-red-100 text-red-800',
- description: '1. 계약서면발급의 모든 항목 중 1개 이상 미체크 한 경우'
- }
- case 'suspected_violation':
- return {
- icon: <AlertTriangle className="h-5 w-5 text-yellow-600" />,
- label: '위반의심',
- color: 'bg-yellow-100 text-yellow-800',
- description: '2. 부당하도급에서 O항목에 체크한 경우'
- }
- default:
- // 기본값으로 준수 상태 반환
- return {
- icon: <CheckCircle className="h-5 w-5 text-green-600" />,
- label: '준수',
- color: 'bg-green-100 text-green-800',
- description: '점검 결과가 유효하지 않습니다.'
- }
- }
- }
-
- const resultInfo = getInspectionResultInfo()
- const isViolationOrSuspected = data.inspectionResult === 'violation' || data.inspectionResult === 'suspected_violation'
-
- const causeOptions = [
- { value: '서면미교부_현업부서 하도급법 이해 부족', label: '서면미교부_현업부서 하도급법 이해 부족' },
- { value: '서면미교부_기존계약 만료前 계약연장에 대한 사전조치 소홀', label: '서면미교부_기존계약 만료前 계약연장에 대한 사전조치 소홀' },
- { value: '서면미교부_긴급작업時 先작업합의서 체결 절차 未인지', label: '서면미교부_긴급작업時 先작업합의서 체결 절차 未인지' },
- { value: '부당가격인하_예산부족 等 원가절감 필요성 대두', label: '부당가격인하_예산부족 等 원가절감 필요성 대두' },
- { value: '부당가격인하_하도급법 이해부족 및 금액 협의과정에 대한 근거 미흡', label: '부당가격인하_하도급법 이해부족 및 금액 협의과정에 대한 근거 미흡' },
- { value: '기타', label: '기타' }
- ]
-
- const countermeasureOptions = [
- { value: '서면미교부_준법지원을 통한 현업부서 계몽활동 실시', label: '서면미교부_준법지원을 통한 현업부서 계몽활동 실시' },
- { value: '서면미교부_계약만료일정 별도 관리 및 사전점검', label: '서면미교부_계약만료일정 별도 관리 및 사전점검' },
- { value: '서면미교부_작업착수前 先작업합의서 체결토록 현업에 가이드', label: '서면미교부_작업착수前 先작업합의서 체결토록 현업에 가이드' },
- { value: '부당가격인하_최종 협의된 견적서 접수/보관 必', label: '부당가격인하_최종 협의된 견적서 접수/보관 必' },
- { value: '부당가격인하_합의서 체결시 \'자율적 의사결정\' 等 문구 삽입', label: '부당가격인하_합의서 체결시 \'자율적 의사결정\' 等 문구 삽입' },
- { value: '부당가격인하_수의계약時 금액 협의과정에 대한 근거 확보 (회의록, 메일, 당초/변경 견적서 等)', label: '부당가격인하_수의계약時 금액 협의과정에 대한 근거 확보 (회의록, 메일, 당초/변경 견적서 等)' },
- { value: '기타', label: '기타' }
- ]
-
- return (
- <Accordion type="single" collapsible className="w-full">
- <AccordionItem value="checklist">
- <AccordionTrigger className="hover:no-underline">
- <div className="flex items-center gap-3 w-full">
- <HelpCircle className="h-5 w-5" />
- <span className="font-medium">하도급법 자율점검 체크리스트</span>
- <Badge className={resultInfo.color}>
- {resultInfo.label}
- </Badge>
- </div>
- </AccordionTrigger>
- <AccordionContent>
- <Card>
- <CardContent className="space-y-6 pt-6">
- {/* 체크박스 */}
- <div className="flex items-center gap-2">
- <Checkbox
- checked={isEnabled}
- onCheckedChange={(checked) => setIsEnabled(checked as boolean)}
- disabled={readOnly}
- />
- <span className="text-sm font-medium">하도급법 자율점검 체크리스트 활성화</span>
- </div>
-
- {/* 점검결과 표시 */}
- <div className="flex items-center gap-4 p-4 bg-gray-50 rounded-lg">
- <div className="flex items-center gap-2">
- {resultInfo.icon}
- <Badge className={resultInfo.color}>
- {resultInfo.label}
- </Badge>
- </div>
- <div className="text-sm text-gray-600">
- {resultInfo.description}
- </div>
- </div>
-
- <Accordion type="multiple" defaultValue={["contract-document", "unfair-pricing"]} className="w-full">
- {/* 1. 계약서면발급 */}
- <AccordionItem value="contract-document">
- <AccordionTrigger className="text-lg font-semibold">
- 1. 계약서면발급
- </AccordionTrigger>
- <AccordionContent>
- <div className="space-y-4 p-4">
- <Alert>
- <AlertDescription>
- 본 계약에 해당하는 항목을 아래 안내사항에 따라 &apos;O&apos;인 경우 체크하세요.
- </AlertDescription>
- </Alert>
-
- <div className="space-y-4">
- <div className="flex items-start space-x-3">
- <Checkbox
- id="workOrderBeforeStart"
- checked={data.contractDocumentIssuance.workOrderBeforeStart}
- onCheckedChange={(checked) => handleContractDocumentChange('workOrderBeforeStart', checked as boolean)}
- disabled={!isEnabled || readOnly}
- />
- <div className="space-y-1">
- <Label htmlFor="workOrderBeforeStart" className="text-sm font-medium">
- (1) 작업 착수前 계약 서면을 발급하지 못하는 경우 작업지시서(선작업합의서)를 발급했는가?
- </Label>
- <p className="text-xs text-gray-500">
- ※ 단가, 물량 등을 정하지 못하는 경우 정하는 기일을 기재
- </p>
- </div>
- </div>
-
- <div className="flex items-start space-x-3">
- <Checkbox
- id="entrustmentDetails"
- checked={data.contractDocumentIssuance.entrustmentDetails}
- onCheckedChange={(checked) => handleContractDocumentChange('entrustmentDetails', checked as boolean)}
- disabled={!isEnabled || readOnly}
- />
- <div className="space-y-1">
- <Label htmlFor="entrustmentDetails" className="text-sm font-medium">
- (2) 위탁일자와 위탁내용(품명, 수량 등)을 명기하였는가?
- </Label>
- </div>
- </div>
-
- <div className="flex items-start space-x-3">
- <Checkbox
- id="deliveryDetails"
- checked={data.contractDocumentIssuance.deliveryDetails}
- onCheckedChange={(checked) => handleContractDocumentChange('deliveryDetails', checked as boolean)}
- disabled={!isEnabled || readOnly}
- />
- <div className="space-y-1">
- <Label htmlFor="deliveryDetails" className="text-sm font-medium">
- (3) 납품, 인도 또는 제공하는 시기 및 장소(납기 및 납품장소)를 명기하였는가?
- </Label>
- <p className="text-xs text-gray-500">
- 예: 삼성의 검사완료(승인) 후 목적물 인도 등
- </p>
- </div>
- </div>
-
- <div className="flex items-start space-x-3">
- <Checkbox
- id="inspectionMethod"
- checked={data.contractDocumentIssuance.inspectionMethod}
- onCheckedChange={(checked) => handleContractDocumentChange('inspectionMethod', checked as boolean)}
- disabled={!isEnabled || readOnly}
- />
- <div className="space-y-1">
- <Label htmlFor="inspectionMethod" className="text-sm font-medium">
- (4) 검사의 방법 및 시기를 명기하였는가?
- </Label>
- <p className="text-xs text-gray-500">
- 예: 작업완료 후 삼성담당자 입회하에 검사를 실시하고 10일 이내 검사결과 통보
- </p>
- </div>
- </div>
-
- <div className="flex items-start space-x-3">
- <Checkbox
- id="subcontractPayment"
- checked={data.contractDocumentIssuance.subcontractPayment}
- onCheckedChange={(checked) => handleContractDocumentChange('subcontractPayment', checked as boolean)}
- disabled={!isEnabled || readOnly}
- />
- <div className="space-y-1">
- <Label htmlFor="subcontractPayment" className="text-sm font-medium">
- (5) 하도급대금과 그 지급방법(현금, 어음 등) 및 지급기일을 명기하였는가?
- </Label>
- </div>
- </div>
-
- <div className="flex items-start space-x-3">
- <Checkbox
- id="materialProvision"
- checked={data.contractDocumentIssuance.materialProvision}
- onCheckedChange={(checked) => handleContractDocumentChange('materialProvision', checked as boolean)}
- disabled={!isEnabled || readOnly}
- />
- <div className="space-y-1">
- <Label htmlFor="materialProvision" className="text-sm font-medium">
- (6) 원재료 등 제공 시 품명/수량/제공일/대가/대가 지급방법 및 기일을 명기하였는가?
- </Label>
- <p className="text-xs text-gray-500">
- 해당사항 없을 시에도 기재로 간주
- </p>
- </div>
- </div>
-
- <div className="flex items-start space-x-3">
- <Checkbox
- id="priceAdjustment"
- checked={data.contractDocumentIssuance.priceAdjustment}
- onCheckedChange={(checked) => handleContractDocumentChange('priceAdjustment', checked as boolean)}
- disabled={!isEnabled || readOnly}
- />
- <div className="space-y-1">
- <Label htmlFor="priceAdjustment" className="text-sm font-medium">
- (7) 원재료 등 가격변동에 따른 대금 조정 요건/방법/절차를 명기하였는가?
- </Label>
- </div>
- </div>
- </div>
- </div>
- </AccordionContent>
- </AccordionItem>
-
- {/* 2. 부당하도급대금결정행위 */}
- <AccordionItem value="unfair-pricing">
- <AccordionTrigger className="text-lg font-semibold">
- 2. 부당하도급대금결정행위
- </AccordionTrigger>
- <AccordionContent>
- <div className="space-y-4 p-4">
- <Alert>
- <AlertDescription>
- 본 계약에 해당하는 항목을 아래 안내사항에 따라 &apos;O&apos;인 경우 체크하세요.
- <br />
- <strong>※ &apos;X&apos; 항목에 다음 안내사항이 자동 표기됩니다:</strong>
- </AlertDescription>
- </Alert>
-
- <div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 space-y-2">
- <h4 className="font-medium text-yellow-800">안내사항:</h4>
- <ul className="text-sm text-yellow-700 space-y-1">
- <li>• 단가 인하時 객관/타당한 근거에 의해 산출하고 협력사와 합의</li>
- <li>• 최저가 경쟁입찰 후 입찰자와 대금인하 협상 불가</li>
- <li>• 협력사에 발주량 등 거래조건에 착오를 일으키게 하거나 타 사업자 견적 또는 거짓 견적을 보여주는 등 기만하여 대금을 결정할 수 없음</li>
- <li>• 정당한 이유 없이 일률적 비율로 단가 인하 불가</li>
- <li>• 정당한 이유 없이 특정 사업자를 차별 취급 하도록 대금 결정 불가</li>
- </ul>
- </div>
-
- <div className="space-y-4">
- <div className="flex items-start space-x-3">
- <Checkbox
- id="priceReductionWithBasis"
- checked={data.unfairSubcontractPricing.priceReductionWithBasis}
- onCheckedChange={(checked) => handleUnfairPricingChange('priceReductionWithBasis', checked as boolean)}
- disabled={!isEnabled || readOnly}
- />
- <Label htmlFor="priceReductionWithBasis" className="text-sm font-medium">
- 단가 인하時 객관/타당한 근거에 의해 산출하고 협력사와 합의
- </Label>
- </div>
-
- <div className="flex items-start space-x-3">
- <Checkbox
- id="noNegotiationAfterLowestBid"
- checked={data.unfairSubcontractPricing.noNegotiationAfterLowestBid}
- onCheckedChange={(checked) => handleUnfairPricingChange('noNegotiationAfterLowestBid', checked as boolean)}
- disabled={!isEnabled || readOnly}
- />
- <Label htmlFor="noNegotiationAfterLowestBid" className="text-sm font-medium">
- 최저가 경쟁입찰 후 입찰자와 대금인하 협상 불가
- </Label>
- </div>
-
- <div className="flex items-start space-x-3">
- <Checkbox
- id="noDeceptionInPricing"
- checked={data.unfairSubcontractPricing.noDeceptionInPricing}
- onCheckedChange={(checked) => handleUnfairPricingChange('noDeceptionInPricing', checked as boolean)}
- disabled={!isEnabled || readOnly}
- />
- <Label htmlFor="noDeceptionInPricing" className="text-sm font-medium">
- 협력사에 발주량 등 거래조건에 착오를 일으키게 하거나 타 사업자 견적 또는 거짓 견적을 보여주는 등 기만하여 대금을 결정할 수 없음
- </Label>
- </div>
-
- <div className="flex items-start space-x-3">
- <Checkbox
- id="noUniformPriceReduction"
- checked={data.unfairSubcontractPricing.noUniformPriceReduction}
- onCheckedChange={(checked) => handleUnfairPricingChange('noUniformPriceReduction', checked as boolean)}
- disabled={!isEnabled || readOnly}
- />
- <Label htmlFor="noUniformPriceReduction" className="text-sm font-medium">
- 정당한 이유 없이 일률적 비율로 단가 인하 불가
- </Label>
- </div>
-
- <div className="flex items-start space-x-3">
- <Checkbox
- id="noDiscriminatoryTreatment"
- checked={data.unfairSubcontractPricing.noDiscriminatoryTreatment}
- onCheckedChange={(checked) => handleUnfairPricingChange('noDiscriminatoryTreatment', checked as boolean)}
- disabled={!isEnabled || readOnly}
- />
- <Label htmlFor="noDiscriminatoryTreatment" className="text-sm font-medium">
- 정당한 이유 없이 특정 사업자를 차별 취급 하도록 대금 결정 불가
- </Label>
- </div>
- </div>
- </div>
- </AccordionContent>
- </AccordionItem>
-
- {/* 위반/위반의심 시 추가 정보 */}
- {isViolationOrSuspected && (
- <AccordionItem value="violation-details">
- <AccordionTrigger className="text-lg font-semibold">
- 위반/위반의심 상세 정보
- </AccordionTrigger>
- <AccordionContent>
- <div className="space-y-4 p-4">
- <Alert>
- <AlertTriangle className="h-4 w-4" />
- <AlertDescription>
- 점검결과가 위반 또는 위반의심인 경우 아래 정보를 필수로 입력해주세요.
- </AlertDescription>
- </Alert>
-
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
- <div className="space-y-2">
- <Label htmlFor="responsibleDepartment">귀책부서 *</Label>
- <Textarea
- id="responsibleDepartment"
- value={data.responsibleDepartment || ''}
- onChange={(e) => handleFieldChange('responsibleDepartment', e.target.value)}
- placeholder="귀책부서를 입력하세요"
- rows={2}
- disabled={!isEnabled || readOnly}
- />
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="cause">원인 *</Label>
- <Select
- value={data.cause || ''}
- onValueChange={(value) => handleFieldChange('cause', value)}
- disabled={!isEnabled || readOnly}
- >
- <SelectTrigger disabled={!isEnabled || readOnly}>
- <SelectValue placeholder="원인을 선택하세요" />
- </SelectTrigger>
- <SelectContent>
- {causeOptions.map((option) => (
- <SelectItem key={option.value} value={option.value}>
- {option.label}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- {data.cause === '기타' && (
- <Textarea
- value={data.causeOther || ''}
- onChange={(e) => handleFieldChange('causeOther', e.target.value)}
- placeholder="기타 원인을 입력하세요"
- rows={2}
- disabled={!isEnabled || readOnly}
- />
- )}
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="countermeasure">대책 *</Label>
- <Select
- value={data.countermeasure || ''}
- onValueChange={(value) => handleFieldChange('countermeasure', value)}
- disabled={!isEnabled || readOnly}
- >
- <SelectTrigger disabled={!isEnabled || readOnly}>
- <SelectValue placeholder="대책을 선택하세요" />
- </SelectTrigger>
- <SelectContent>
- {countermeasureOptions.map((option) => (
- <SelectItem key={option.value} value={option.value}>
- {option.label}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- {data.countermeasure === '기타' && (
- <Textarea
- value={data.countermeasureOther || ''}
- onChange={(e) => handleFieldChange('countermeasureOther', e.target.value)}
- placeholder="기타 대책을 입력하세요"
- rows={2}
- disabled={!isEnabled || readOnly}
- />
- )}
- </div>
- </div>
- </div>
- </AccordionContent>
- </AccordionItem>
- )}
- </Accordion>
-
- {/* 저장 버튼 */}
- {!readOnly && (
- <div className="flex justify-end pt-4 border-t">
- <Button onClick={handleSave} className="flex items-center gap-2">
- <Save className="h-4 w-4" />
- 체크리스트 저장
- </Button>
- </div>
- )}
- </CardContent>
- </Card>
- </AccordionContent>
- </AccordionItem>
- </Accordion>
- )
-}
diff --git a/lib/general-contracts_old/main/create-general-contract-dialog.tsx b/lib/general-contracts_old/main/create-general-contract-dialog.tsx
deleted file mode 100644
index 2c3fc8bc..00000000
--- a/lib/general-contracts_old/main/create-general-contract-dialog.tsx
+++ /dev/null
@@ -1,413 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { useRouter } from "next/navigation"
-import { Plus } from "lucide-react"
-import { toast } from "sonner"
-import { Button } from "@/components/ui/button"
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog"
-import { Input } from "@/components/ui/input"
-import { Label } from "@/components/ui/label"
-import { Textarea } from "@/components/ui/textarea"
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
-import { Calendar } from "@/components/ui/calendar"
-import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
-import { CalendarIcon } from "lucide-react"
-import { format } from "date-fns"
-import { ko } from "date-fns/locale"
-import { cn } from "@/lib/utils"
-import { createContract, getVendors, getProjects } from "@/lib/general-contracts/service"
-import {
- GENERAL_CONTRACT_CATEGORIES,
- GENERAL_CONTRACT_TYPES,
- GENERAL_EXECUTION_METHODS
-} from "@/lib/general-contracts/types"
-import { useSession } from "next-auth/react"
-
-interface CreateContractForm {
- contractNumber: string
- name: string
- category: string
- type: string
- executionMethod: string
- vendorId: number | null
- projectId: number | null
- startDate: Date | undefined
- endDate: Date | undefined
- validityEndDate: Date | undefined
- notes: string
-}
-
-export function CreateGeneralContractDialog() {
- const router = useRouter()
- const { data: session } = useSession()
- const [open, setOpen] = React.useState(false)
- const [isLoading, setIsLoading] = React.useState(false)
- const [vendors, setVendors] = React.useState<Array<{ id: number; vendorName: string; vendorCode: string | null }>>([])
- const [projects, setProjects] = React.useState<Array<{ id: number; code: string; name: string; type: string }>>([])
-
- const [form, setForm] = React.useState<CreateContractForm>({
- contractNumber: '',
- name: '',
- category: '',
- type: '',
- executionMethod: '',
- vendorId: null,
- projectId: null,
- startDate: undefined,
- endDate: undefined,
- validityEndDate: undefined,
- notes: '',
- })
-
- // 업체 목록 조회
- React.useEffect(() => {
- const fetchVendors = async () => {
- try {
- const vendorList = await getVendors()
- setVendors(vendorList)
- } catch (error) {
- console.error('Error fetching vendors:', error)
- }
- }
- fetchVendors()
- }, [])
-
- // 프로젝트 목록 조회
- React.useEffect(() => {
- const fetchProjects = async () => {
- try {
- const projectList = await getProjects()
- console.log(projectList)
- setProjects(projectList)
- } catch (error) {
- console.error('Error fetching projects:', error)
- }
- }
- fetchProjects()
- }, [])
-
- const handleSubmit = async () => {
- // 필수 필드 검증
- if (!form.name || !form.category || !form.type || !form.executionMethod ||
- !form.vendorId || !form.startDate || !form.endDate) {
- toast.error("필수 항목을 모두 입력해주세요.")
- return
- }
-
- if (!form.validityEndDate) {
- setForm(prev => ({ ...prev, validityEndDate: form.endDate }))
- }
-
- try {
- setIsLoading(true)
-
- const contractData = {
- contractNumber: '',
- name: form.name,
- category: form.category,
- type: form.type,
- executionMethod: form.executionMethod,
- projectId: form.projectId,
- contractSourceType: 'manual',
- vendorId: form.vendorId!,
- startDate: form.startDate!.toISOString().split('T')[0],
- endDate: form.endDate!.toISOString().split('T')[0],
- validityEndDate: (form.validityEndDate || form.endDate!).toISOString().split('T')[0],
- status: 'Draft',
- registeredById: session?.user?.id || 1,
- lastUpdatedById: session?.user?.id || 1,
- notes: form.notes,
- }
-
- await createContract(contractData)
-
- toast.success("새 계약이 생성되었습니다.")
- setOpen(false)
- resetForm()
-
- // 상세 페이지로 이동
- router.refresh()
- } catch (error) {
- console.error('Error creating contract:', error)
- toast.error("계약 생성 중 오류가 발생했습니다.")
- } finally {
- setIsLoading(false)
- }
- }
-
- const resetForm = () => {
- setForm({
- contractNumber: '',
- name: '',
- category: '',
- type: '',
- executionMethod: '',
- vendorId: null,
- projectId: null,
- startDate: undefined,
- endDate: undefined,
- validityEndDate: undefined,
- notes: '',
- })
- }
-
- return (
- <Dialog open={open} onOpenChange={(newOpen) => {
- setOpen(newOpen)
- if (!newOpen) resetForm()
- }}>
- <DialogTrigger asChild>
- <Button size="sm">
- <Plus className="mr-2 h-4 w-4" />
- 신규등록
- </Button>
- </DialogTrigger>
- <DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
- <DialogHeader>
- <DialogTitle>새 계약 등록</DialogTitle>
- <DialogDescription>
- 새로운 계약의 기본 정보를 입력하세요.
- </DialogDescription>
- </DialogHeader>
-
- <div className="grid gap-4 py-4">
- <div className="grid grid-cols-1 gap-4">
- <div className="grid gap-2">
- <Label htmlFor="name">계약명 *</Label>
- <Input
- id="name"
- value={form.name}
- onChange={(e) => setForm(prev => ({ ...prev, name: e.target.value }))}
- placeholder="계약명을 입력하세요"
- />
- </div>
- </div>
-
- <div className="grid grid-cols-3 gap-4">
- <div className="grid gap-2">
- <Label htmlFor="category">계약구분 *</Label>
- <Select value={form.category} onValueChange={(value) => setForm(prev => ({ ...prev, category: value }))}>
- <SelectTrigger>
- <SelectValue placeholder="계약구분 선택" />
- </SelectTrigger>
- <SelectContent>
- {GENERAL_CONTRACT_CATEGORIES.map((category) => {
- const categoryLabels = {
- 'unit_price': '단가계약',
- 'general': '일반계약',
- 'sale': '매각계약'
- }
- return (
- <SelectItem key={category} value={category}>
- {category} - {categoryLabels[category as keyof typeof categoryLabels]}
- </SelectItem>
- )
- })}
- </SelectContent>
- </Select>
- </div>
-
- <div className="grid gap-2">
- <Label htmlFor="type">계약종류 *</Label>
- <Select value={form.type} onValueChange={(value) => setForm(prev => ({ ...prev, type: value }))}>
- <SelectTrigger>
- <SelectValue placeholder="계약종류 선택" />
- </SelectTrigger>
- <SelectContent>
- {GENERAL_CONTRACT_TYPES.map((type) => {
- const typeLabels = {
- 'UP': '자재단가계약',
- 'LE': '임대차계약',
- 'IL': '개별운송계약',
- 'AL': '연간운송계약',
- 'OS': '외주용역계약',
- 'OW': '도급계약',
- 'IS': '검사계약',
- 'LO': 'LOI',
- 'FA': 'FA',
- 'SC': '납품합의계약',
- 'OF': '클레임상계계약',
- 'AW': '사전작업합의',
- 'AD': '사전납품합의',
- 'AM': '설계계약',
- 'SC_SELL': '폐기물매각계약'
- }
- return (
- <SelectItem key={type} value={type}>
- {type} - {typeLabels[type as keyof typeof typeLabels]}
- </SelectItem>
- )
- })}
- </SelectContent>
- </Select>
- </div>
-
- <div className="grid gap-2">
- <Label htmlFor="executionMethod">체결방식 *</Label>
- <Select value={form.executionMethod} onValueChange={(value) => setForm(prev => ({ ...prev, executionMethod: value }))}>
- <SelectTrigger>
- <SelectValue placeholder="체결방식 선택" />
- </SelectTrigger>
- <SelectContent>
- {GENERAL_EXECUTION_METHODS.map((method) => (
- <SelectItem key={method} value={method}>
- {method}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- </div>
- </div>
-
- <div className="grid gap-2">
- <Label htmlFor="project">프로젝트</Label>
- <Select value={form.projectId?.toString()} onValueChange={(value) => setForm(prev => ({ ...prev, projectId: parseInt(value) }))}>
- <SelectTrigger>
- <SelectValue placeholder="프로젝트 선택 (선택사항)" />
- </SelectTrigger>
- <SelectContent>
- {projects.map((project) => (
- <SelectItem key={project.id} value={project.id.toString()}>
- {project.name} ({project.code})
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- </div>
-
- <div className="grid gap-2">
- <Label htmlFor="vendor">협력업체 *</Label>
- <Select value={form.vendorId?.toString()} onValueChange={(value) => setForm(prev => ({ ...prev, vendorId: parseInt(value) }))}>
- <SelectTrigger>
- <SelectValue placeholder="협력업체 선택" />
- </SelectTrigger>
- <SelectContent>
- {vendors.map((vendor) => (
- <SelectItem key={vendor.id} value={vendor.id.toString()}>
- {vendor.vendorName} ({vendor.vendorCode})
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- </div>
-
- <div className="grid grid-cols-3 gap-4">
- <div className="grid gap-2">
- <Label>계약시작일 *</Label>
- <Popover>
- <PopoverTrigger asChild>
- <Button
- variant="outline"
- className={cn(
- "justify-start text-left font-normal",
- !form.startDate && "text-muted-foreground"
- )}
- >
- <CalendarIcon className="mr-2 h-4 w-4" />
- {form.startDate ? format(form.startDate, "yyyy-MM-dd", { locale: ko }) : "날짜 선택"}
- </Button>
- </PopoverTrigger>
- <PopoverContent className="w-auto p-0">
- <Calendar
- mode="single"
- selected={form.startDate}
- onSelect={(date) => setForm(prev => ({ ...prev, startDate: date }))}
- initialFocus
- />
- </PopoverContent>
- </Popover>
- </div>
-
- <div className="grid gap-2">
- <Label>계약종료일 *</Label>
- <Popover>
- <PopoverTrigger asChild>
- <Button
- variant="outline"
- className={cn(
- "justify-start text-left font-normal",
- !form.endDate && "text-muted-foreground"
- )}
- >
- <CalendarIcon className="mr-2 h-4 w-4" />
- {form.endDate ? format(form.endDate, "yyyy-MM-dd", { locale: ko }) : "날짜 선택"}
- </Button>
- </PopoverTrigger>
- <PopoverContent className="w-auto p-0">
- <Calendar
- mode="single"
- selected={form.endDate}
- onSelect={(date) => setForm(prev => ({ ...prev, endDate: date }))}
- initialFocus
- />
- </PopoverContent>
- </Popover>
- </div>
-
- <div className="grid gap-2">
- <Label>유효기간종료일</Label>
- <Popover>
- <PopoverTrigger asChild>
- <Button
- variant="outline"
- className={cn(
- "justify-start text-left font-normal",
- !form.validityEndDate && "text-muted-foreground"
- )}
- >
- <CalendarIcon className="mr-2 h-4 w-4" />
- {form.validityEndDate ? format(form.validityEndDate, "yyyy-MM-dd", { locale: ko }) : "날짜 선택"}
- </Button>
- </PopoverTrigger>
- <PopoverContent className="w-auto p-0">
- <Calendar
- mode="single"
- selected={form.validityEndDate}
- onSelect={(date) => setForm(prev => ({ ...prev, validityEndDate: date }))}
- initialFocus
- />
- </PopoverContent>
- </Popover>
- </div>
- </div>
- <div className="grid gap-2">
- <Label htmlFor="notes">비고</Label>
- <Textarea
- id="notes"
- value={form.notes}
- onChange={(e) => setForm(prev => ({ ...prev, notes: e.target.value }))}
- placeholder="비고사항을 입력하세요"
- rows={3}
- />
- </div>
- </div>
-
- <DialogFooter>
- <Button
- type="button"
- variant="outline"
- onClick={() => setOpen(false)}
- >
- 취소
- </Button>
- <Button
- type="button"
- onClick={handleSubmit}
- disabled={isLoading}
- >
- {isLoading ? '생성 중...' : '생성'}
- </Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- )
-}
diff --git a/lib/general-contracts_old/main/general-contract-update-sheet.tsx b/lib/general-contracts_old/main/general-contract-update-sheet.tsx
deleted file mode 100644
index 54f4ae4e..00000000
--- a/lib/general-contracts_old/main/general-contract-update-sheet.tsx
+++ /dev/null
@@ -1,401 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { useForm } from "react-hook-form"
-import { zodResolver } from "@hookform/resolvers/zod"
-import { z } from "zod"
-import { toast } from "sonner"
-import { Button } from "@/components/ui/button"
-import {
- Sheet,
- SheetContent,
- SheetDescription,
- SheetFooter,
- SheetHeader,
- SheetTitle,
-} from "@/components/ui/sheet"
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form"
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select"
-import { Input } from "@/components/ui/input"
-import { Textarea } from "@/components/ui/textarea"
-import {
- GENERAL_CONTRACT_CATEGORIES,
- GENERAL_CONTRACT_TYPES,
- GENERAL_EXECUTION_METHODS,
-} from "@/lib/general-contracts/types"
-import { updateContract } from "../service"
-import { GeneralContractListItem } from "./general-contracts-table-columns"
-import { useSession } from "next-auth/react"
-const updateContractSchema = z.object({
- category: z.string().min(1, "계약구분을 선택해주세요"),
- type: z.string().min(1, "계약종류를 선택해주세요"),
- executionMethod: z.string().min(1, "체결방식을 선택해주세요"),
- name: z.string().min(1, "계약명을 입력해주세요"),
- startDate: z.string().min(1, "계약시작일을 선택해주세요"),
- endDate: z.string().min(1, "계약종료일을 선택해주세요"),
- validityEndDate: z.string().min(1, "유효기간종료일을 선택해주세요"),
- contractScope: z.string().min(1, "계약확정범위를 선택해주세요"),
- notes: z.string().optional(),
- linkedRfqOrItb: z.string().optional(),
- linkedPoNumber: z.string().optional(),
- linkedBidNumber: z.string().optional(),
-})
-
-type UpdateContractFormData = z.infer<typeof updateContractSchema>
-
-interface GeneralContractUpdateSheetProps {
- contract: GeneralContractListItem | null
- open: boolean
- onOpenChange: (open: boolean) => void
- onSuccess?: () => void
-}
-
-export function GeneralContractUpdateSheet({
- contract,
- open,
- onOpenChange,
- onSuccess,
-}: GeneralContractUpdateSheetProps) {
- const [isSubmitting, setIsSubmitting] = React.useState(false)
- const session = useSession()
- const userId = session.data?.user?.id ? Number(session.data.user.id) : null
- const form = useForm<UpdateContractFormData>({
- resolver: zodResolver(updateContractSchema),
- defaultValues: {
- category: "",
- type: "",
- executionMethod: "",
- name: "",
- startDate: "",
- endDate: "",
- validityEndDate: "",
- contractScope: "",
- notes: "",
- linkedRfqOrItb: "",
- linkedPoNumber: "",
- linkedBidNumber: "",
- },
- })
-
- // 계약확정범위에 따른 품목정보 필드 비활성화 여부
- const watchedContractScope = form.watch("contractScope")
- const isItemsDisabled = watchedContractScope === '단가' || watchedContractScope === '물량(실적)'
-
- // 계약 데이터가 변경될 때 폼 초기화
- React.useEffect(() => {
- if (contract) {
- console.log("Loading contract data:", contract)
- const formData = {
- category: contract.category || "",
- type: contract.type || "",
- executionMethod: contract.executionMethod || "",
- name: contract.name || "",
- startDate: contract.startDate || "",
- endDate: contract.endDate || "",
- validityEndDate: contract.validityEndDate || "",
- contractScope: contract.contractScope || "",
- notes: contract.notes || "",
- linkedRfqOrItb: contract.linkedRfqOrItb || "",
- linkedPoNumber: contract.linkedPoNumber || "",
- linkedBidNumber: contract.linkedBidNumber || "",
- }
- console.log("Form data to reset:", formData)
- form.reset(formData)
- }
- }, [contract, form])
-
- const onSubmit = async (data: UpdateContractFormData) => {
- if (!contract) return
-
- try {
- setIsSubmitting(true)
-
- await updateContract(contract.id, {
- category: data.category,
- type: data.type,
- executionMethod: data.executionMethod,
- name: data.name,
- startDate: data.startDate,
- endDate: data.endDate,
- validityEndDate: data.validityEndDate,
- contractScope: data.contractScope,
- notes: data.notes,
- linkedRfqOrItb: data.linkedRfqOrItb,
- linkedPoNumber: data.linkedPoNumber,
- linkedBidNumber: data.linkedBidNumber,
- vendorId: contract.vendorId,
- lastUpdatedById: userId,
- })
-
- toast.success("계약 정보가 성공적으로 수정되었습니다.")
- onOpenChange(false)
- onSuccess?.()
- } catch (error) {
- console.error("Error updating contract:", error)
- toast.error("계약 정보 수정 중 오류가 발생했습니다.")
- } finally {
- setIsSubmitting(false)
- }
- }
-
- return (
- <Sheet open={open} onOpenChange={onOpenChange}>
- <SheetContent className="w-[800px] sm:max-w-[800px] flex flex-col" style={{width: 800, maxWidth: 800, height: '100vh'}}>
- <SheetHeader className="flex-shrink-0">
- <SheetTitle>계약 정보 수정</SheetTitle>
- <SheetDescription>
- 계약의 기본 정보를 수정합니다. 변경사항은 즉시 저장됩니다.
- </SheetDescription>
- </SheetHeader>
-
- <div className="flex-1 overflow-y-auto min-h-0">
- <Form {...form}>
- <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6 h-full">
- <div className="grid gap-4 py-4">
- {/* 계약구분 */}
- <FormField
- control={form.control}
- name="category"
- render={({ field }) => (
- <FormItem>
- <FormLabel>계약구분 *</FormLabel>
- <Select onValueChange={field.onChange} value={field.value}>
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="계약구분을 선택하세요" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- {GENERAL_CONTRACT_CATEGORIES.map((category) => {
- const categoryLabels = {
- 'unit_price': '단가계약',
- 'general': '일반계약',
- 'sale': '매각계약'
- }
- return (
- <SelectItem key={category} value={category}>
- {category} - {categoryLabels[category as keyof typeof categoryLabels]}
- </SelectItem>
- )})}
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 계약종류 */}
- <FormField
- control={form.control}
- name="type"
- render={({ field }) => (
- <FormItem>
- <FormLabel>계약종류 *</FormLabel>
- <Select onValueChange={field.onChange} value={field.value}>
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="계약종류를 선택하세요" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- {GENERAL_CONTRACT_TYPES.map((type) => {
- const typeLabels = {
- 'UP': '자재단가계약',
- 'LE': '임대차계약',
- 'IL': '개별운송계약',
- 'AL': '연간운송계약',
- 'OS': '외주용역계약',
- 'OW': '도급계약',
- 'IS': '검사계약',
- 'LO': 'LOI',
- 'FA': 'FA',
- 'SC': '납품합의계약',
- 'OF': '클레임상계계약',
- 'AW': '사전작업합의',
- 'AD': '사전납품합의',
- 'AM': '설계계약',
- 'SC_SELL': '폐기물매각계약'
- }
- return (
- <SelectItem key={type} value={type}>
- {type} - {typeLabels[type as keyof typeof typeLabels]}
- </SelectItem>
- )})}
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 체결방식 */}
- <FormField
- control={form.control}
- name="executionMethod"
- render={({ field }) => (
- <FormItem>
- <FormLabel>체결방식 *</FormLabel>
- <Select onValueChange={field.onChange} value={field.value}>
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="체결방식을 선택하세요" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- {GENERAL_EXECUTION_METHODS.map((method) => {
- const methodLabels = {
- '전자계약': '전자계약',
- '오프라인계약': '오프라인계약'
- }
- return (
- <SelectItem key={method} value={method}>
- {method} - {methodLabels[method as keyof typeof methodLabels]}
- </SelectItem>
- )})}
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 계약명 */}
- <FormField
- control={form.control}
- name="name"
- render={({ field }) => (
- <FormItem>
- <FormLabel>계약명 *</FormLabel>
- <FormControl>
- <Input placeholder="계약명을 입력하세요" {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 계약시작일 */}
- <FormField
- control={form.control}
- name="startDate"
- render={({ field }) => (
- <FormItem>
- <FormLabel>계약시작일 *</FormLabel>
- <FormControl>
- <Input type="date" {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 계약종료일 */}
- <FormField
- control={form.control}
- name="endDate"
- render={({ field }) => (
- <FormItem>
- <FormLabel>계약종료일 *</FormLabel>
- <FormControl>
- <Input type="date" {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 유효기간종료일 */}
- <FormField
- control={form.control}
- name="validityEndDate"
- render={({ field }) => (
- <FormItem>
- <FormLabel>유효기간종료일 *</FormLabel>
- <FormControl>
- <Input type="date" {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 계약확정범위 */}
- <FormField
- control={form.control}
- name="contractScope"
- render={({ field }) => (
- <FormItem>
- <FormLabel>계약확정범위 *</FormLabel>
- <Select onValueChange={field.onChange} value={field.value}>
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="계약확정범위를 선택하세요" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- <SelectItem value="단가">단가</SelectItem>
- <SelectItem value="금액">금액</SelectItem>
- <SelectItem value="물량(실적)">물량(실적)</SelectItem>
- </SelectContent>
- </Select>
- <FormMessage />
- <p className="text-sm text-muted-foreground">
- 해당 계약으로 확정되는 범위를 선택하세요.
- </p>
- </FormItem>
- )}
- />
-
- {/* 비고 */}
- <FormField
- control={form.control}
- name="notes"
- render={({ field }) => (
- <FormItem>
- <FormLabel>비고</FormLabel>
- <FormControl>
- <Textarea
- placeholder="비고를 입력하세요"
- className="min-h-[100px]"
- {...field}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- </div>
-
- <SheetFooter className="flex-shrink-0 mt-6">
- <Button
- type="button"
- variant="outline"
- onClick={() => onOpenChange(false)}
- disabled={isSubmitting}
- >
- 취소
- </Button>
- <Button type="submit" disabled={isSubmitting}>
- {isSubmitting ? "수정 중..." : "수정"}
- </Button>
- </SheetFooter>
- </form>
- </Form>
- </div>
- </SheetContent>
- </Sheet>
- )
-}
diff --git a/lib/general-contracts_old/main/general-contracts-table-columns.tsx b/lib/general-contracts_old/main/general-contracts-table-columns.tsx
deleted file mode 100644
index a08d8b81..00000000
--- a/lib/general-contracts_old/main/general-contracts-table-columns.tsx
+++ /dev/null
@@ -1,571 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { type ColumnDef } from "@tanstack/react-table"
-import { Checkbox } from "@/components/ui/checkbox"
-import { Badge } from "@/components/ui/badge"
-import { Button } from "@/components/ui/button"
-import {
- Eye, Edit, MoreHorizontal
-} from "lucide-react"
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu"
-import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
-import { DataTableRowAction } from "@/types/table"
-import { formatDate } from "@/lib/utils"
-
-// 일반계약 리스트 아이템 타입 정의
-export interface GeneralContractListItem {
- id: number
- contractNumber: string
- revision: number
- status: string
- category: string
- type: string
- executionMethod: string
- name: string
- contractSourceType?: string
- startDate: string
- endDate: string
- validityEndDate?: string
- contractScope?: string
- specificationType?: string
- specificationManualText?: string
- contractAmount?: number | string | null
- totalAmount?: number | string | null
- currency?: string
- registeredAt: string
- signedAt?: string
- linkedPoNumber?: string
- linkedRfqOrItb?: string
- linkedBidNumber?: string
- lastUpdatedAt: string
- notes?: string
- vendorId?: number
- vendorName?: string
- vendorCode?: string
- projectId?: number
- projectName?: string
- projectCode?: string
- managerName?: string
- lastUpdatedByName?: string
-}
-
-interface GetColumnsProps {
- setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<GeneralContractListItem> | null>>
-}
-
-// 상태별 배지 색상
-const getStatusBadgeVariant = (status: string) => {
- switch (status) {
- case 'Draft':
- return 'outline'
- case 'Request to Review':
- case 'Confirm to Review':
- return 'secondary'
- case 'Contract Accept Request':
- return 'default'
- case 'Complete the Contract':
- return 'default'
- case 'Reject to Accept Contract':
- case 'Contract Delete':
- return 'destructive'
- default:
- return 'outline'
- }
-}
-
-// 상태 텍스트 변환
-const getStatusText = (status: string) => {
- switch (status) {
- case 'Draft':
- return '임시저장'
- case 'Request to Review':
- return '조건검토요청'
- case 'Confirm to Review':
- return '조건검토완료'
- case 'Contract Accept Request':
- return '계약승인요청'
- case 'Complete the Contract':
- return '계약체결'
- case 'Reject to Accept Contract':
- return '계약승인거절'
- case 'Contract Delete':
- return '계약폐기'
- case 'PCR Request':
- return 'PCR요청'
- case 'VO Request':
- return 'VO요청'
- case 'PCR Accept':
- return 'PCR승인'
- case 'PCR Reject':
- return 'PCR거절'
- default:
- return status
- }
-}
-
-// 계약구분 텍스트 변환
-const getCategoryText = (category: string) => {
- switch (category) {
- case 'unit_price':
- return '단가계약'
- case 'general':
- return '일반계약'
- case 'sale':
- return '매각계약'
- default:
- return category
- }
-}
-
-// 계약종류 텍스트 변환
-const getTypeText = (type: string) => {
- switch (type) {
- case 'UP':
- return '자재단가계약'
- case 'LE':
- return '임대차계약'
- case 'IL':
- return '개별운송계약'
- case 'AL':
- return '연간운송계약'
- case 'OS':
- return '외주용역계약'
- case 'OW':
- return '도급계약'
- case 'IS':
- return '검사계약'
- case 'LO':
- return 'LOI'
- case 'FA':
- return 'FA'
- case 'SC':
- return '납품합의계약'
- case 'OF':
- return '클레임상계계약'
- case 'AW':
- return '사전작업합의'
- case 'AD':
- return '사전납품합의'
- case 'AM':
- return '설계계약'
- case 'SC_SELL':
- return '폐기물매각계약'
- default:
- return type
- }
-}
-
-// 체결방식 텍스트 변환
-const getExecutionMethodText = (method: string) => {
- switch (method) {
- case '전자계약':
- return '전자계약'
- case '오프라인계약':
- return '오프라인계약'
- default:
- return method
- }
-}
-
-// 업체선정방법 텍스트 변환
-const getcontractSourceTypeText = (method?: string) => {
- if (!method) return '-'
- switch (method) {
- case 'estimate':
- return '견적'
- case 'bid':
- return '입찰'
- case 'manual':
- return '자체생성'
- default:
- return method
- }
-}
-
-// 금액 포맷팅
-const formatCurrency = (amount: string | number | null | undefined, currency = 'KRW') => {
- if (!amount && amount !== 0) return '-'
-
- const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount
- if (isNaN(numAmount)) return '-'
-
- // 통화 코드가 null이거나 유효하지 않은 경우 기본값 사용
- const safeCurrency = currency && typeof currency === 'string' ? currency : 'USD'
-
- return new Intl.NumberFormat('ko-KR', {
- style: 'currency',
- currency: safeCurrency,
- minimumFractionDigits: 0,
- maximumFractionDigits: 0,
- }).format(numAmount)
-}
-
-export function getGeneralContractsColumns({ setRowAction }: GetColumnsProps): ColumnDef<GeneralContractListItem>[] {
- return [
- // ═══════════════════════════════════════════════════════════════
- // 선택 및 기본 정보
- // ═══════════════════════════════════════════════════════════════
- {
- id: "select",
- header: ({ table }) => (
- <Checkbox
- checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate")}
- onCheckedChange={(v) => table.toggleAllPageRowsSelected(!!v)}
- aria-label="select all"
- className="translate-y-0.5"
- />
- ),
- cell: ({ row }) => (
- <Checkbox
- checked={row.getIsSelected()}
- onCheckedChange={(v) => row.toggleSelected(!!v)}
- aria-label="select row"
- className="translate-y-0.5"
- />
- ),
- size: 40,
- enableSorting: false,
- enableHiding: false,
- },
-
- // ░░░ 계약번호 ░░░
- {
- accessorKey: "contractNumber",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="계약번호 (Rev.)" />,
- cell: ({ row }) => (
- <div className="font-mono text-sm">
- {row.original.contractNumber}
- {row.original.revision > 0 && (
- <span className="ml-1 text-xs text-muted-foreground">
- Rev.{row.original.revision}
- </span>
- )}
- </div>
- ),
- size: 150,
- meta: { excelHeader: "계약번호 (Rev.)" },
- },
-
- // ░░░ 계약상태 ░░░
- {
- accessorKey: "status",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="계약상태" />,
- cell: ({ row }) => (
- <Badge variant={getStatusBadgeVariant(row.original.status)}>
- {getStatusText(row.original.status)}
- </Badge>
- ),
- size: 120,
- meta: { excelHeader: "계약상태" },
- },
-
- // ░░░ 계약명 ░░░
- {
- accessorKey: "name",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="계약명" />,
- cell: ({ row }) => (
- <div className="truncate max-w-[200px]" title={row.original.name}>
- <Button
- variant="link"
- className="p-0 h-auto text-left justify-start"
- onClick={() => setRowAction({ row, type: "view" })}
- >
- {row.original.name}
- </Button>
- </div>
- ),
- size: 200,
- meta: { excelHeader: "계약명" },
- },
-
- // ═══════════════════════════════════════════════════════════════
- // 계약 정보
- // ═══════════════════════════════════════════════════════════════
- {
- header: "계약 정보",
- columns: [
- {
- accessorKey: "category",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="계약구분" />,
- cell: ({ row }) => (
- <Badge variant="outline">
- {getCategoryText(row.original.category)}
- </Badge>
- ),
- size: 100,
- meta: { excelHeader: "계약구분" },
- },
-
- {
- accessorKey: "type",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="계약종류" />,
- cell: ({ row }) => (
- <Badge variant="secondary">
- {getTypeText(row.original.type)}
- </Badge>
- ),
- size: 120,
- meta: { excelHeader: "계약종류" },
- },
-
- {
- accessorKey: "executionMethod",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="체결방식" />,
- cell: ({ row }) => (
- <Badge variant="outline">
- {getExecutionMethodText(row.original.executionMethod)}
- </Badge>
- ),
- size: 100,
- meta: { excelHeader: "체결방식" },
- },
-
- {
- accessorKey: "contractSourceType",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="업체선정방법" />,
- cell: ({ row }) => (
- <Badge variant="outline">
- {getcontractSourceTypeText(row.original.contractSourceType)}
- </Badge>
- ),
- size: 200,
- meta: { excelHeader: "업체선정방법" },
- },
- ]
- },
-
- // ═══════════════════════════════════════════════════════════════
- // 협력업체 정보
- // ═══════════════════════════════════════════════════════════════
- {
- header: "협력업체",
- columns: [
- {
- accessorKey: "vendorName",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="협력업체명" />,
- cell: ({ row }) => (
- <div className="flex flex-col">
- <span className="font-medium">{row.original.vendorName || '-'}</span>
- <span className="text-xs text-muted-foreground">
- {row.original.vendorCode ? row.original.vendorCode : "-"}
- </span>
- </div>
- ),
- size: 150,
- meta: { excelHeader: "협력업체명" },
- },
-
- {
- accessorKey: "projectName",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="프로젝트명" />,
- cell: ({ row }) => (
- <div className="flex flex-col">
- <span className="font-medium">{row.original.projectName || '-'}</span>
- <span className="text-xs text-muted-foreground">
- {row.original.projectCode ? row.original.projectCode : "-"}
- </span>
- </div>
- ),
- size: 150,
- meta: { excelHeader: "프로젝트명" },
- },
-
- ]
- },
-
- // ═══════════════════════════════════════════════════════════════
- // 기간 정보
- // ═══════════════════════════════════════════════════════════════
- {
- header: "계약기간",
- columns: [
- {
- id: "contractPeriod",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="계약기간" />,
- cell: ({ row }) => {
- const startDate = row.original.startDate
- const endDate = row.original.endDate
-
- if (!startDate || !endDate) return <span className="text-muted-foreground">-</span>
-
- const now = new Date()
- const isActive = now >= new Date(startDate) && now <= new Date(endDate)
- const isExpired = now > new Date(endDate)
-
- return (
- <div className="text-xs">
- <div className={`${isActive ? 'text-green-600 font-medium' : isExpired ? 'text-red-600' : 'text-gray-600'}`}>
- {formatDate(startDate, "KR")} ~ {formatDate(endDate, "KR")}
- </div>
- {isActive && (
- <Badge variant="default" className="text-xs mt-1">진행중</Badge>
- )}
- {isExpired && (
- <Badge variant="destructive" className="text-xs mt-1">만료</Badge>
- )}
- </div>
- )
- },
- size: 200,
- meta: { excelHeader: "계약기간" },
- },
- ]
- },
-
- // ═══════════════════════════════════════════════════════════════
- // 금액 정보
- // ═══════════════════════════════════════════════════════════════
- {
- header: "금액 정보",
- columns: [
- {
- accessorKey: "currency",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="통화" />,
- cell: ({ row }) => (
- <span className="font-mono text-sm">{row.original.currency || 'KRW'}</span>
- ),
- size: 60,
- meta: { excelHeader: "통화" },
- },
-
- {
- accessorKey: "contractAmount",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="계약금액" />,
- cell: ({ row }) => (
- <span className="text-sm font-medium">
- {formatCurrency(row.original.contractAmount, row.original.currency)}
- </span>
- ),
- size: 200,
- meta: { excelHeader: "계약금액" },
- },
- ]
- },
-
- // ═══════════════════════════════════════════════════════════════
- // 담당자 및 관리 정보
- // ═══════════════════════════════════════════════════════════════
- {
- header: "관리 정보",
- columns: [
- {
- accessorKey: "managerName",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="계약담당자" />,
- cell: ({ row }) => (
- <div className="truncate max-w-[100px]" title={row.original.managerName || ''}>
- {row.original.managerName || '-'}
- </div>
- ),
- size: 100,
- meta: { excelHeader: "계약담당자" },
- },
-
- {
- accessorKey: "registeredAt",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="계약등록일" />,
- cell: ({ row }) => (
- <span className="text-sm">{formatDate(row.original.registeredAt, "KR")}</span>
- ),
- size: 100,
- meta: { excelHeader: "계약등록일" },
- },
-
- {
- accessorKey: "signedAt",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="계약체결일" />,
- cell: ({ row }) => (
- <span className="text-sm">
- {row.original.signedAt ? formatDate(row.original.signedAt, "KR") : '-'}
- </span>
- ),
- size: 100,
- meta: { excelHeader: "계약체결일" },
- },
-
- {
- accessorKey: "linkedPoNumber",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="연계 PO번호" />,
- cell: ({ row }) => (
- <span className="font-mono text-sm">{row.original.linkedPoNumber || '-'}</span>
- ),
- size: 140,
- meta: { excelHeader: "연계 PO번호" },
- },
-
- {
- accessorKey: "lastUpdatedAt",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="최종수정일" />,
- cell: ({ row }) => (
- <span className="text-sm">{formatDate(row.original.lastUpdatedAt, "KR")}</span>
- ),
- size: 100,
- meta: { excelHeader: "최종수정일" },
- },
-
- {
- accessorKey: "lastUpdatedByName",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="최종수정자" />,
- cell: ({ row }) => (
- <span className="text-sm">{row.original.lastUpdatedByName || '-'}</span>
- ),
- size: 100,
- meta: { excelHeader: "최종수정자" },
- },
- ]
- },
-
- // ░░░ 비고 ░░░
- {
- accessorKey: "notes",
- header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="비고" />,
- cell: ({ row }) => (
- <div className="truncate max-w-[150px]" title={row.original.notes || ''}>
- {row.original.notes || '-'}
- </div>
- ),
- size: 150,
- meta: { excelHeader: "비고" },
- },
-
- // ═══════════════════════════════════════════════════════════════
- // 액션
- // ═══════════════════════════════════════════════════════════════
- {
- id: "actions",
- header: "액션",
- cell: ({ row }) => (
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button variant="ghost" className="h-8 w-8 p-0">
- <span className="sr-only">메뉴 열기</span>
- <MoreHorizontal className="h-4 w-4" />
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent align="end">
- {row.original.status !== 'Contract Delete' && (
- <>
- <DropdownMenuItem onClick={() => setRowAction({ row, type: "view" })}>
- <Eye className="mr-2 h-4 w-4" />
- 상세보기
- </DropdownMenuItem>
- <DropdownMenuItem onClick={() => setRowAction({ row, type: "update" })}>
- <Edit className="mr-2 h-4 w-4" />
- 수정
- </DropdownMenuItem>
- </>
- )}
- </DropdownMenuContent>
- </DropdownMenu>
- ),
- size: 50,
- enableSorting: false,
- enableHiding: false,
- },
- ]
-}
diff --git a/lib/general-contracts_old/main/general-contracts-table-toolbar-actions.tsx b/lib/general-contracts_old/main/general-contracts-table-toolbar-actions.tsx
deleted file mode 100644
index f16b759a..00000000
--- a/lib/general-contracts_old/main/general-contracts-table-toolbar-actions.tsx
+++ /dev/null
@@ -1,124 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { type Table } from "@tanstack/react-table"
-import {
- Download, FileSpreadsheet,
- Trash2,
-} from "lucide-react"
-import { deleteContract } from "../service"
-import { toast } from "sonner"
-import { exportTableToExcel } from "@/lib/export"
-import { Button } from "@/components/ui/button"
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu"
-import { GeneralContractListItem } from "./general-contracts-table-columns"
-import { CreateGeneralContractDialog } from "./create-general-contract-dialog"
-
-interface GeneralContractsTableToolbarActionsProps {
- table: Table<GeneralContractListItem>
-}
-
-export function GeneralContractsTableToolbarActions({ table }: GeneralContractsTableToolbarActionsProps) {
- const [isExporting, setIsExporting] = React.useState(false)
-
- // 선택된 계약들
- const selectedContracts = React.useMemo(() => {
- return table
- .getFilteredSelectedRowModel()
- .rows
- .map(row => row.original)
- }, [table.getFilteredSelectedRowModel().rows])
-
- const handleExport = async () => {
- try {
- setIsExporting(true)
- await exportTableToExcel(table, {
- filename: "general-contracts",
- excludeColumns: ["select", "actions"],
- })
- toast.success("계약 목록이 성공적으로 내보내졌습니다.")
- } catch (error) {
- toast.error("내보내기 중 오류가 발생했습니다.")
- } finally {
- setIsExporting(false)
- }
- }
-
-
- const handleDelete = async () => {
- if (selectedContracts.length === 0) {
- toast.error("계약폐기할 계약을 선택해주세요.")
- return
- }
-
- // // 계약폐기 확인
- // const confirmed = window.confirm(
- // `선택한 ${selectedContracts.length}개 계약을 폐기하시겠습니까?\n계약폐기 후에는 복구할 수 없습니다.`
- // )
-
- // if (!confirmed) return
-
- try {
- // 선택된 모든 계약을 폐기 처리
- const deletePromises = selectedContracts.map(contract =>
- deleteContract(contract.id)
- )
-
- await Promise.all(deletePromises)
-
- toast.success(`${selectedContracts.length}개 계약이 폐기되었습니다.`)
-
- // 테이블 새로고침
- } catch (error) {
- console.error('Error deleting contracts:', error)
- toast.error("계약폐기 중 오류가 발생했습니다.")
- }
- }
-
- return (
- <div className="flex items-center gap-2">
- {/* 신규 등록 */}
- <CreateGeneralContractDialog />
-
- {/* 계약폐기 */}
- <Button
- variant="outline"
- size="sm"
- onClick={handleDelete}
- disabled={selectedContracts.length === 0}
- className="text-red-600 hover:text-red-700 hover:bg-red-50"
- >
- <Trash2 className="mr-2 h-4 w-4" />
- 계약폐기
- </Button>
-
- {/* Export */}
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button
- variant="outline"
- size="sm"
- className="gap-2"
- disabled={isExporting}
- >
- <Download className="size-4" aria-hidden="true" />
- <span className="hidden sm:inline">
- {isExporting ? "내보내는 중..." : "Export"}
- </span>
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent align="end">
- <DropdownMenuItem onClick={handleExport} disabled={isExporting}>
- <FileSpreadsheet className="mr-2 size-4" />
- <span>계약 목록 내보내기</span>
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
- </div>
- )
-}
diff --git a/lib/general-contracts_old/main/general-contracts-table.tsx b/lib/general-contracts_old/main/general-contracts-table.tsx
deleted file mode 100644
index e4c96ee3..00000000
--- a/lib/general-contracts_old/main/general-contracts-table.tsx
+++ /dev/null
@@ -1,217 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { useRouter } from "next/navigation"
-import type {
- DataTableAdvancedFilterField,
- DataTableFilterField,
- DataTableRowAction,
-} from "@/types/table"
-
-import { useDataTable } from "@/hooks/use-data-table"
-import { DataTable } from "@/components/data-table/data-table"
-import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"
-import { getGeneralContractsColumns, GeneralContractListItem } from "./general-contracts-table-columns"
-import { getGeneralContracts, getGeneralContractStatusCounts } from "@/lib/general-contracts/service"
-import { GeneralContractsTableToolbarActions } from "./general-contracts-table-toolbar-actions"
-import { GeneralContractUpdateSheet } from "./general-contract-update-sheet"
-import {
- GENERAL_EXECUTION_METHODS
-} from "@/lib/general-contracts/types"
-
-// 상태 라벨 매핑
-const contractStatusLabels = {
- 'Draft': '임시저장',
- 'Request to Review': '조건검토요청',
- 'Confirm to Review': '조건검토완료',
- 'Contract Accept Request': '계약승인요청',
- 'Complete the Contract': '계약체결',
- 'Reject to Accept Contract': '계약승인거절',
- 'Contract Delete': '계약폐기',
- 'PCR Request': 'PCR요청',
- 'VO Request': 'VO요청',
- 'PCR Accept': 'PCR승인',
- 'PCR Reject': 'PCR거절'
-}
-
-// 계약구분 라벨 매핑
-const contractCategoryLabels = {
- '단가계약': '단가계약',
- '일반계약': '일반계약',
- '매각계약': '매각계약'
-}
-
-// 계약종류 라벨 매핑
-const contractTypeLabels = {
- 'UP': '자재단가계약',
- 'LE': '임대차계약',
- 'IL': '개별운송계약',
- 'AL': '연간운송계약',
- 'OS': '외주용역계약',
- 'OW': '도급계약',
- 'IS': '검사계약',
- 'LO': 'LOI',
- 'FA': 'FA',
- 'SC': '납품합의계약',
- 'OF': '클레임상계계약',
- 'AW': '사전작업합의',
- 'AD': '사전납품합의',
- 'AM': '설계계약',
- 'SC_SELL': '폐기물매각계약'
-}
-
-interface GeneralContractsTableProps {
- promises: Promise<
- [
- Awaited<ReturnType<typeof getGeneralContracts>>,
- Awaited<ReturnType<typeof getGeneralContractStatusCounts>>
- ]
- >
-}
-
-export function GeneralContractsTable({ promises }: GeneralContractsTableProps) {
- const [{ data, pageCount }, statusCounts] = React.use(promises)
- const [isCompact, setIsCompact] = React.useState<boolean>(false)
- const [rowAction, setRowAction] = React.useState<DataTableRowAction<GeneralContractListItem> | null>(null)
- const [updateSheetOpen, setUpdateSheetOpen] = React.useState(false)
- const [selectedContract, setSelectedContract] = React.useState<GeneralContractListItem | null>(null)
-
- console.log(data, "data")
-
- const router = useRouter()
-
- const columns = React.useMemo(
- () => getGeneralContractsColumns({ setRowAction }),
- [setRowAction]
- )
-
- // rowAction 변경 감지하여 해당 액션 처리
- React.useEffect(() => {
- if (rowAction) {
- setSelectedContract(rowAction.row.original)
-
- switch (rowAction.type) {
- case "view":
- // 상세 페이지로 이동
- router.push(`/evcp/general-contracts/${rowAction.row.original.id}`)
- break
- case "update":
- // 수정 시트 열기
- setSelectedContract(rowAction.row.original)
- setUpdateSheetOpen(true)
- break
- default:
- break
- }
- }
- }, [rowAction, router])
-
- const filterFields: DataTableFilterField<GeneralContractListItem>[] = []
-
- const advancedFilterFields: DataTableAdvancedFilterField<GeneralContractListItem>[] = [
- { id: "name", label: "계약명", type: "text" },
- { id: "contractNumber", label: "계약번호", type: "text" },
- { id: "vendorName", label: "협력업체명", type: "text" },
- { id: "managerName", label: "계약담당자", type: "text" },
- {
- id: "status",
- label: "계약상태",
- type: "multi-select",
- options: Object.entries(contractStatusLabels).map(([value, label]) => ({
- label,
- value,
- count: statusCounts[value] || 0,
- })),
- },
- {
- id: "category",
- label: "계약구분",
- type: "select",
- options: Object.entries(contractCategoryLabels).map(([value, label]) => ({
- label,
- value,
- })),
- },
- {
- id: "type",
- label: "계약종류",
- type: "select",
- options: Object.entries(contractTypeLabels).map(([value, label]) => ({
- label,
- value,
- })),
- },
- {
- id: "executionMethod",
- label: "체결방식",
- type: "select",
- options: GENERAL_EXECUTION_METHODS.map(value => ({
- label: value,
- value: value,
- })),
- },
- {
- id: "contractSourceType",
- label: "업체선정방법",
- type: "select",
- options: [
- { label: "estimate", value: "견적" },
- { label: "bid", value: "입찰" },
- { label: "manual", value: "자체생성" },
- ],
- },
- { id: "registeredAt", label: "계약등록일", type: "date" },
- { id: "signedAt", label: "계약체결일", type: "date" },
- { id: "lastUpdatedAt", label: "최종수정일", type: "date" },
- ]
-
- const { table } = useDataTable({
- data,
- columns,
- pageCount,
- filterFields,
- enablePinning: true,
- enableAdvancedFilter: true,
- initialState: {
- sorting: [{ id: "registeredAt", desc: true }],
- columnPinning: { right: ["actions"] },
- },
- getRowId: (originalRow) => String(originalRow.id),
- shallow: false,
- clearOnDefault: true,
- })
-
- const handleCompactChange = React.useCallback((compact: boolean) => {
- setIsCompact(compact)
- }, [])
-
- return (
- <>
- <DataTable
- table={table}
- compact={isCompact}
- >
- <DataTableAdvancedToolbar
- table={table}
- filterFields={advancedFilterFields}
- shallow={false}
- enableCompactToggle={true}
- compactStorageKey="generalContractsTableCompact"
- onCompactChange={handleCompactChange}
- >
- <GeneralContractsTableToolbarActions table={table} />
- </DataTableAdvancedToolbar>
- </DataTable>
-
- <GeneralContractUpdateSheet
- contract={selectedContract}
- open={updateSheetOpen}
- onOpenChange={setUpdateSheetOpen}
- onSuccess={() => {
- // 테이블 새로고침 또는 상태 업데이트
- window.location.reload()
- }}
- />
- </>
- )
-}
diff --git a/lib/general-contracts_old/service.ts b/lib/general-contracts_old/service.ts
deleted file mode 100644
index 2422706a..00000000
--- a/lib/general-contracts_old/service.ts
+++ /dev/null
@@ -1,1933 +0,0 @@
-'use server'
-
-import { revalidatePath } from 'next/cache'
-import { eq, and, or, desc, asc, count, ilike, SQL, gte, lte, lt, like, sql } from 'drizzle-orm'
-import db from '@/db/db'
-import path from 'path'
-import { promises as fs } from 'fs'
-import { generalContracts, generalContractItems, generalContractAttachments } from '@/db/schema/generalContract'
-import { contracts, contractItems, contractEnvelopes, contractSigners } from '@/db/schema/contract'
-import { basicContract, basicContractTemplates } from '@/db/schema/basicContractDocumnet'
-import { vendors } from '@/db/schema/vendors'
-import { users } from '@/db/schema/users'
-import { projects } from '@/db/schema/projects'
-import { items } from '@/db/schema/items'
-import { filterColumns } from '@/lib/filter-columns'
-import { saveDRMFile } from '@/lib/file-stroage'
-import { decryptWithServerAction } from '@/components/drm/drmUtils'
-import { saveBuffer } from '@/lib/file-stroage'
-import { v4 as uuidv4 } from 'uuid'
-import { GetGeneralContractsSchema } from './validation'
-import { sendEmail } from '../mail/sendEmail'
-
-export async function getGeneralContracts(input: GetGeneralContractsSchema) {
- try {
- const offset = (input.page - 1) * input.perPage
-
- console.log(input.filters)
- console.log(input.sort)
-
- // ✅ 1) 고급 필터 조건
- let advancedWhere: SQL<unknown> | undefined = undefined
- if (input.filters && input.filters.length > 0) {
- advancedWhere = filterColumns({
- table: generalContracts,
- filters: input.filters as any,
- joinOperator: input.joinOperator || 'and',
- })
- }
-
- // ✅ 2) 기본 필터 조건들
- const basicConditions: SQL<unknown>[] = []
-
- if (input.contractNumber) {
- basicConditions.push(ilike(generalContracts.contractNumber, `%${input.contractNumber}%`))
- }
-
- if (input.name) {
- basicConditions.push(ilike(generalContracts.name, `%${input.name}%`))
- }
-
- if (input.status && input.status.length > 0) {
- basicConditions.push(
- or(...input.status.map(status => eq(generalContracts.status, status)))!
- )
- }
-
- if (input.category && input.category.length > 0) {
- basicConditions.push(
- or(...input.category.map(category => eq(generalContracts.category, category)))!
- )
- }
-
- if (input.type && input.type.length > 0) {
- basicConditions.push(
- or(...input.type.map(type => eq(generalContracts.type, type)))!
- )
- }
-
- if (input.executionMethod && input.executionMethod.length > 0) {
- basicConditions.push(
- or(...input.executionMethod.map(method => eq(generalContracts.executionMethod, method)))!
- )
- }
-
- if (input.contractSourceType && input.contractSourceType.length > 0) {
- basicConditions.push(
- or(...input.contractSourceType.map(method => eq(generalContracts.contractSourceType, method)))!
- )
- }
-
- if (input.vendorId && input.vendorId > 0) {
- basicConditions.push(eq(generalContracts.vendorId, input.vendorId))
- }
-
- if (input.managerName) {
- basicConditions.push(ilike(users.name, `%${input.managerName}%`))
- }
-
- // 날짜 필터들
- if (input.registeredAtFrom) {
- basicConditions.push(gte(generalContracts.registeredAt, new Date(input.registeredAtFrom)))
- }
- if (input.registeredAtTo) {
- basicConditions.push(lte(generalContracts.registeredAt, new Date(input.registeredAtTo)))
- }
-
- if (input.signedAtFrom) {
- basicConditions.push(gte(generalContracts.signedAt, new Date(input.signedAtFrom)))
- }
- if (input.signedAtTo) {
- basicConditions.push(lte(generalContracts.signedAt, new Date(input.signedAtTo)))
- }
-
- if (input.startDateFrom) {
- basicConditions.push(gte(generalContracts.startDate, new Date(input.startDateFrom)))
- }
- if (input.startDateTo) {
- basicConditions.push(lte(generalContracts.startDate, new Date(input.startDateTo)))
- }
-
- if (input.endDateFrom) {
- basicConditions.push(gte(generalContracts.endDate, new Date(input.endDateFrom)))
- }
- if (input.endDateTo) {
- basicConditions.push(lte(generalContracts.endDate, new Date(input.endDateTo)))
- }
-
- // 금액 필터들
- if (input.contractAmountMin) {
- basicConditions.push(gte(generalContracts.contractAmount, parseFloat(input.contractAmountMin)))
- }
- if (input.contractAmountMax) {
- basicConditions.push(lte(generalContracts.contractAmount, parseFloat(input.contractAmountMax)))
- }
-
- const basicWhere = basicConditions.length > 0 ? and(...basicConditions) : undefined
-
- // ✅ 3) 글로벌 검색 조건
- let globalWhere: SQL<unknown> | undefined = undefined
- if (input.search) {
- const s = `%${input.search}%`
- const searchConditions = [
- ilike(generalContracts.contractNumber, s),
- ilike(generalContracts.name, s),
- ilike(generalContracts.notes, s),
- ilike(vendors.vendorName, s),
- ilike(users.name, s),
- ilike(generalContracts.linkedPoNumber, s),
- ilike(generalContracts.linkedRfqOrItb, s),
- ilike(generalContracts.linkedBidNumber, s),
- ]
- globalWhere = or(...searchConditions)
- }
-
- // ✅ 4) 최종 WHERE 조건
- const whereConditions: SQL<unknown>[] = []
- if (advancedWhere) whereConditions.push(advancedWhere)
- if (basicWhere) whereConditions.push(basicWhere)
- if (globalWhere) whereConditions.push(globalWhere)
-
- const finalWhere = whereConditions.length > 0 ? and(...whereConditions) : undefined
-
- // ✅ 5) 전체 개수 조회
- const totalResult = await db
- .select({ count: count() })
- .from(generalContracts)
- .leftJoin(vendors, eq(generalContracts.vendorId, vendors.id))
- .leftJoin(users, eq(generalContracts.registeredById, users.id))
- .where(finalWhere)
-
- const total = totalResult[0]?.count || 0
-
- if (total === 0) {
- return { data: [], pageCount: 0, total: 0 }
- }
-
- console.log("Total contracts:", total)
-
- // ✅ 6) 정렬 및 페이징
- const orderByColumns: any[] = []
-
- for (const sort of input.sort) {
- const column = sort.id
-
- // generalContracts 테이블의 컬럼들
- if (column in generalContracts) {
- const contractColumn = generalContracts[column as keyof typeof generalContracts]
- orderByColumns.push(sort.desc ? desc(contractColumn) : asc(contractColumn))
- }
- // vendors 테이블의 컬럼들
- else if (column === 'vendorName' || column === 'vendorCode') {
- const vendorColumn = vendors[column as keyof typeof vendors]
- orderByColumns.push(sort.desc ? desc(vendorColumn) : asc(vendorColumn))
- }
- // users 테이블의 컬럼들
- else if (column === 'managerName' || column === 'lastUpdatedByName') {
- const userColumn = users.name
- orderByColumns.push(sort.desc ? desc(userColumn) : asc(userColumn))
- }
- }
-
- if (orderByColumns.length === 0) {
- orderByColumns.push(desc(generalContracts.registeredAt))
- }
-
- // ✅ 7) 메인 쿼리
- const data = await db
- .select({
- id: generalContracts.id,
- contractNumber: generalContracts.contractNumber,
- revision: generalContracts.revision,
- status: generalContracts.status,
- category: generalContracts.category,
- type: generalContracts.type,
- executionMethod: generalContracts.executionMethod,
- name: generalContracts.name,
- contractSourceType: generalContracts.contractSourceType,
- startDate: generalContracts.startDate,
- endDate: generalContracts.endDate,
- validityEndDate: generalContracts.validityEndDate,
- contractScope: generalContracts.contractScope,
- specificationType: generalContracts.specificationType,
- specificationManualText: generalContracts.specificationManualText,
- contractAmount: generalContracts.contractAmount,
- totalAmount: generalContracts.totalAmount,
- currency: generalContracts.currency,
- registeredAt: generalContracts.registeredAt,
- signedAt: generalContracts.signedAt,
- linkedRfqOrItb: generalContracts.linkedRfqOrItb,
- linkedPoNumber: generalContracts.linkedPoNumber,
- linkedBidNumber: generalContracts.linkedBidNumber,
- lastUpdatedAt: generalContracts.lastUpdatedAt,
- notes: generalContracts.notes,
- // Vendor info
- vendorId: generalContracts.vendorId,
- vendorName: vendors.vendorName,
- vendorCode: vendors.vendorCode,
- // Project info
- projectId: generalContracts.projectId,
- projectName: projects.name,
- projectCode: projects.code,
- // User info
- managerName: users.name,
- lastUpdatedByName: users.name,
- })
- .from(generalContracts)
- .leftJoin(vendors, eq(generalContracts.vendorId, vendors.id))
- .leftJoin(users, eq(generalContracts.registeredById, users.id))
- .leftJoin(projects, eq(generalContracts.projectId, projects.id))
- .where(finalWhere)
- .orderBy(...orderByColumns)
- .limit(input.perPage)
- .offset(offset)
-
- const pageCount = Math.ceil(total / input.perPage)
-
- return { data, pageCount, total }
-
- } catch (err) {
- console.error("Error in getGeneralContracts:", err)
- return { data: [], pageCount: 0, total: 0 }
- }
-}
-
-export async function getContractById(id: number) {
- try {
- // ID 유효성 검사
- if (!id || isNaN(id) || id <= 0) {
- throw new Error('Invalid contract ID')
- }
-
- const contract = await db
- .select()
- .from(generalContracts)
- .where(eq(generalContracts.id, id))
- .limit(1)
-
- if (!contract.length) {
- throw new Error('Contract not found')
- }
-
- // Get contract items
- const items = await db
- .select()
- .from(generalContractItems)
- .where(eq(generalContractItems.contractId, id))
-
- // Get contract attachments
- const attachments = await db
- .select()
- .from(generalContractAttachments)
- .where(eq(generalContractAttachments.contractId, id))
-
- // Get vendor info
- const vendor = await db
- .select()
- .from(vendors)
- .where(eq(vendors.id, contract[0].vendorId))
- .limit(1)
-
- // Get project info
- const project = contract[0].projectId ? await db
- .select()
- .from(projects)
- .where(eq(projects.id, contract[0].projectId))
- .limit(1) : null
-
- // Get manager info
- const manager = await db
- .select()
- .from(users)
- .where(eq(users.id, contract[0].registeredById))
- .limit(1)
-
- return {
- ...contract[0],
- contractItems: items,
- attachments,
- vendor: vendor[0] || null,
- vendorCode: vendor[0]?.vendorCode || null,
- vendorName: vendor[0]?.vendorName || null,
- project: project ? project[0] : null,
- projectName: project ? project[0].name : null,
- projectCode: project ? project[0].code : null,
- manager: manager[0] || null
- }
- } catch (error) {
- console.error('Error fetching contract by ID:', error)
- throw new Error('Failed to fetch contract')
- }
-}
-
-export async function getContractBasicInfo(id: number) {
- try {
- const [contract] = await db
- .select()
- .from(generalContracts)
- .where(eq(generalContracts.id, id))
- .limit(1)
-
- if (!contract) {
- return null
- }
-
- // JSON 필드를 문자열에서 객체로 변환하여 클라이언트에서 사용하기 쉽게 만듭니다.
- // Drizzle ORM이 JSONB 타입을 처리하지만, 명확성을 위해 명시적으로 파싱하는 것이 좋습니다.
- const parsedContract = {
- ...contract,
- warrantyPeriod: contract.warrantyPeriod as any,
- paymentBeforeDelivery: contract.paymentBeforeDelivery as any,
- paymentAfterDelivery: contract.paymentAfterDelivery as any,
- contractEstablishmentConditions: contract.contractEstablishmentConditions as any,
- mandatoryDocuments: contract.mandatoryDocuments as any,
- contractTerminationConditions: contract.contractTerminationConditions as any,
- }
-
- // 품목정보 총합 계산 로직 (기존 코드와 동일)
- const contractItems = await db
- .select()
- .from(generalContractItems)
- .where(eq(generalContractItems.contractId, id))
-
- let calculatedContractAmount = null
- if (contractItems && contractItems.length > 0) {
- calculatedContractAmount = contractItems.reduce((sum, item) => {
- const amount = parseFloat(item.contractAmount || '0')
- return sum + amount
- }, 0)
- }
-
- return {
- ...parsedContract,
- contractAmount: calculatedContractAmount,
- }
-
- } catch (error) {
- console.error('Error getting contract basic info:', error)
- throw new Error('Failed to fetch contract basic info')
- }
-}
-
-export async function createContract(data: Record<string, unknown>) {
- try {
- // 계약번호 자동 생성
- // TODO: 구매 발주담당자 코드 필요 - 파라미터 추가
- const rawUserId = data.registeredById
- const userId = (rawUserId && !isNaN(Number(rawUserId))) ? String(rawUserId) : undefined
- const contractNumber = await generateContractNumber(
- userId,
- data.type as string
- )
-
- const [newContract] = await db
- .insert(generalContracts)
- .values({
- contractNumber: contractNumber,
- revision: 0,
- // contractSourceType: data.contractSourceType || 'manual',
- status: data.status || 'Draft',
- category: data.category as string,
- type: data.type as string,
- executionMethod: data.executionMethod as string,
- name: data.name as string,
- vendorId: data.vendorId as number,
- projectId: data.projectId as number,
- startDate: data.startDate as string,
- endDate: data.endDate as string,
- validityEndDate: data.validityEndDate as string,
- linkedRfqOrItb: data.linkedRfqOrItb as string,
- linkedPoNumber: data.linkedPoNumber as string,
- linkedBidNumber: data.linkedBidNumber as string,
- contractScope: data.contractScope as string,
- warrantyPeriod: data.warrantyPeriod || {},
- specificationType: data.specificationType as string,
- specificationManualText: data.specificationManualText as string,
- unitPriceType: data.unitPriceType as string,
- contractAmount: data.contractAmount as number,
- currency: data.currency as string,
- paymentBeforeDelivery: data.paymentBeforeDelivery || {},
- paymentDelivery: data.paymentDelivery as string,
- paymentAfterDelivery: data.paymentAfterDelivery || {},
- paymentTerm: data.paymentTerm as string,
- taxType: data.taxType as string,
- liquidatedDamages: data.liquidatedDamages as number,
- liquidatedDamagesPercent: data.liquidatedDamagesPercent as number,
- deliveryType: data.deliveryType as string,
- deliveryTerm: data.deliveryTerm as string,
- shippingLocation: data.shippingLocation as string,
- dischargeLocation: data.dischargeLocation as string,
- contractDeliveryDate: data.contractDeliveryDate as string,
- contractEstablishmentConditions: data.contractEstablishmentConditions || {},
- interlockingSystem: data.interlockingSystem as string,
- mandatoryDocuments: data.mandatoryDocuments || {},
- contractTerminationConditions: data.contractTerminationConditions || {},
- terms: data.terms || {},
- complianceChecklist: data.complianceChecklist || {},
- communicationChannels: data.communicationChannels || {},
- locations: data.locations || {},
- fieldServiceRates: data.fieldServiceRates || {},
- offsetDetails: data.offsetDetails || {},
- totalAmount: data.totalAmount as number,
- availableBudget: data.availableBudget as number,
- registeredById: data.registeredById as number,
- lastUpdatedById: data.lastUpdatedById as number,
- notes: data.notes as string,
- })
- .returning()
- console.log(newContract,"newContract")
-
-
- revalidatePath('/general-contracts')
- return newContract
- } catch (error) {
- console.error('Error creating contract:', error)
- throw new Error('Failed to create contract')
- }
-}
-
-export async function updateContractBasicInfo(id: number, data: Record<string, unknown>, userId: number) {
- try {
- // 업데이트할 데이터 정리
- // 클라이언트에서 전송된 formData를 그대로 사용합니다.
- const {
- specificationType,
- specificationManualText,
- unitPriceType,
- warrantyPeriod,
- currency,
- linkedPoNumber,
- linkedBidNumber,
- notes,
- paymentBeforeDelivery,
- paymentDelivery,
- paymentAfterDelivery,
- paymentTerm,
- taxType,
- liquidatedDamages,
- liquidatedDamagesPercent,
- deliveryType,
- deliveryTerm,
- shippingLocation,
- dischargeLocation,
- contractDeliveryDate,
- contractEstablishmentConditions,
- interlockingSystem,
- mandatoryDocuments,
- contractTerminationConditions,
- } = data
-
- // 계약금액 자동 집계 로직
- const contractItems = await db
- .select()
- .from(generalContractItems)
- .where(eq(generalContractItems.contractId, id))
-
- let calculatedContractAmount: number | null = null
- if (contractItems && contractItems.length > 0) {
- calculatedContractAmount = contractItems.reduce((sum, item) => {
- const amount = parseFloat(item.contractAmount || '0')
- return sum + amount
- }, 0)
- }
-
- // 데이터 타입 변환 및 검증
- const convertToNumberOrNull = (value: unknown): number | null => {
- if (value === null || value === undefined || value === '' || value === 'false') {
- return null
- }
- const num = typeof value === 'string' ? parseFloat(value) : Number(value)
- return isNaN(num) ? null : num
- }
-
- // 날짜 필드에서 빈 문자열을 null로 변환
- const convertEmptyStringToNull = (value: unknown): string | null => {
- return (value === '' || value === undefined) ? null : value as string
- }
-
- // 업데이트할 데이터 객체 생성
- const updateData: Record<string, unknown> = {
- specificationType,
- specificationManualText,
- unitPriceType,
- warrantyPeriod, // JSON 필드
- currency,
- linkedPoNumber,
- linkedBidNumber,
- notes,
- paymentBeforeDelivery, // JSON 필드
- paymentDelivery: convertToNumberOrNull(paymentDelivery),
- paymentAfterDelivery, // JSON 필드
- paymentTerm,
- taxType,
- liquidatedDamages: convertToNumberOrNull(liquidatedDamages),
- liquidatedDamagesPercent: convertToNumberOrNull(liquidatedDamagesPercent),
- deliveryType,
- deliveryTerm,
- shippingLocation,
- dischargeLocation,
- contractDeliveryDate: convertEmptyStringToNull(contractDeliveryDate),
- contractEstablishmentConditions, // JSON 필드
- interlockingSystem,
- mandatoryDocuments, // JSON 필드
- contractTerminationConditions, // JSON 필드
- contractAmount: calculatedContractAmount || 0,
- lastUpdatedAt: new Date(),
- lastUpdatedById: userId,
- }
-
- // DB에 업데이트 실행
- const [updatedContract] = await db
- .update(generalContracts)
- .set(updateData)
- .where(eq(generalContracts.id, id))
- .returning()
-
- revalidatePath('/general-contracts')
- revalidatePath(`/general-contracts/detail/${id}`)
- return updatedContract
- } catch (error) {
- console.error('Error updating contract basic info:', error)
- throw new Error('Failed to update contract basic info')
- }
-}
-
-// 품목정보 조회
-export async function getContractItems(contractId: number) {
- try {
- const items = await db
- .select()
- .from(generalContractItems)
- .where(eq(generalContractItems.contractId, contractId))
- .orderBy(asc(generalContractItems.id))
-
- return items
- } catch (error) {
- console.error('Error getting contract items:', error)
- throw new Error('Failed to get contract items')
- }
-}
-
-// 품목정보 생성
-export async function createContractItem(contractId: number, itemData: Record<string, unknown>) {
- try {
- const [newItem] = await db
- .insert(generalContractItems)
- .values({
- contractId,
- itemCode: itemData.itemCode as string,
- itemInfo: itemData.itemInfo as string,
- specification: itemData.specification as string,
- quantity: itemData.quantity as number,
- quantityUnit: itemData.quantityUnit as string,
- contractDeliveryDate: itemData.contractDeliveryDate as string,
- contractUnitPrice: itemData.contractUnitPrice as number,
- contractAmount: itemData.contractAmount as number,
- contractCurrency: itemData.contractCurrency as string,
- })
- .returning()
-
- // 계약금액 자동 업데이트
- await updateContractAmount(contractId)
-
- revalidatePath('/general-contracts')
- return newItem
- } catch (error) {
- console.error('Error creating contract item:', error)
- throw new Error('Failed to create contract item')
- }
-}
-
-// 품목정보 업데이트
-export async function updateContractItem(itemId: number, itemData: Record<string, unknown>) {
- try {
- const [updatedItem] = await db
- .update(generalContractItems)
- .set({
- itemCode: itemData.itemCode as string,
- itemInfo: itemData.itemInfo as string,
- specification: itemData.specification as string,
- quantity: itemData.quantity as number,
- quantityUnit: itemData.quantityUnit as string,
- contractDeliveryDate: itemData.contractDeliveryDate as string,
- contractUnitPrice: itemData.contractUnitPrice as number,
- contractAmount: itemData.contractAmount as number,
- contractCurrency: itemData.contractCurrency as string,
- updatedAt: new Date()
- })
- .where(eq(generalContractItems.id, itemId))
- .returning()
-
- // 계약금액 자동 업데이트
- await updateContractAmount(updatedItem.contractId)
-
- revalidatePath('/general-contracts')
- return updatedItem
- } catch (error) {
- console.error('Error updating contract item:', error)
- throw new Error('Failed to update contract item')
- }
-}
-
-// 품목정보 삭제
-export async function deleteContractItem(itemId: number) {
- try {
- // 삭제 전 계약 ID 조회
- const [item] = await db
- .select({ contractId: generalContractItems.contractId })
- .from(generalContractItems)
- .where(eq(generalContractItems.id, itemId))
- .limit(1)
-
- if (!item) {
- throw new Error('Contract item not found')
- }
-
- await db
- .delete(generalContractItems)
- .where(eq(generalContractItems.id, itemId))
-
- // 계약금액 자동 업데이트
- await updateContractAmount(item.contractId)
-
- revalidatePath('/general-contracts')
- return { success: true }
- } catch (error) {
- console.error('Error deleting contract item:', error)
- throw new Error('Failed to delete contract item')
- }
-}
-
-// 품목정보 일괄 업데이트 (기존 함수 개선)
-export async function updateContractItems(contractId: number, items: Record<string, unknown>[]) {
- try {
- // 기존 품목 삭제
- await db
- .delete(generalContractItems)
- .where(eq(generalContractItems.contractId, contractId))
-
- // 새 품목 추가
- if (items && items.length > 0) {
- await db
- .insert(generalContractItems)
- .values(
- items.map((item: Record<string, unknown>) => ({
- contractId,
- itemCode: item.itemCode as string,
- itemInfo: item.itemInfo as string,
- specification: item.specification as string,
- quantity: item.quantity as number,
- quantityUnit: item.quantityUnit as string,
- totalWeight: item.totalWeight as number,
- weightUnit: item.weightUnit as string,
- contractDeliveryDate: item.contractDeliveryDate as string,
- contractUnitPrice: item.contractUnitPrice as number,
- contractAmount: item.contractAmount as number,
- contractCurrency: item.contractCurrency as string,
- }))
- )
- }
-
- // 계약금액 자동 업데이트
- await updateContractAmount(contractId)
-
- revalidatePath('/general-contracts')
- return { success: true }
- } catch (error) {
- console.error('Error updating contract items:', error)
- throw new Error('Failed to update contract items')
- }
-}
-
-// 계약금액 자동 업데이트 헬퍼 함수
-async function updateContractAmount(contractId: number) {
- try {
- const items = await db
- .select({ contractAmount: generalContractItems.contractAmount })
- .from(generalContractItems)
- .where(eq(generalContractItems.contractId, contractId))
-
- let calculatedContractAmount: number | null = null
- if (items && items.length > 0) {
- calculatedContractAmount = items.reduce((sum, item) => {
- const amount = parseFloat(String(item.contractAmount || '0'))
- return sum + amount
- }, 0)
- }
-
- // 계약 테이블의 contractAmount 업데이트
- await db
- .update(generalContracts)
- .set({
- contractAmount: calculatedContractAmount || 0,
- lastUpdatedAt: new Date()
- })
- .where(eq(generalContracts.id, contractId))
- } catch (error) {
- console.error('Error updating contract amount:', error)
- throw new Error('Failed to update contract amount')
- }
-}
-
-export async function updateSubcontractChecklist(contractId: number, checklistData: Record<string, unknown>) {
- try {
- await db
- .update(generalContracts)
- .set({
- complianceChecklist: checklistData,
- lastUpdatedAt: new Date()
- })
- .where(eq(generalContracts.id, contractId))
-
- revalidatePath('/general-contracts')
- return { success: true }
- } catch (error) {
- console.error('Error updating subcontract checklist:', error)
- throw new Error('Failed to update subcontract checklist')
- }
-}
-
-export async function getSubcontractChecklist(contractId: number) {
- try {
- const result = await db
- .select()
- .from(generalContracts)
- .where(eq(generalContracts.id, contractId))
- .limit(1)
-
- if (result.length === 0) {
- return { success: false, error: '계약을 찾을 수 없습니다.' }
- }
-
- const contract = result[0]
- const checklistData = contract.complianceChecklist as any
-
- return {
- success: true,
- enabled: !!checklistData,
- data: checklistData || {}
- }
- } catch (error) {
- console.error('Error getting subcontract checklist:', error)
- return { success: false, error: '하도급 체크리스트 조회에 실패했습니다.' }
- }
-}
-
-export async function getBasicInfo(contractId: number) {
- try {
- const result = await db
- .select()
- .from(generalContracts)
- .where(eq(generalContracts.id, contractId))
- .limit(1)
-
- if (result.length === 0) {
- return { success: false, error: '계약을 찾을 수 없습니다.' }
- }
-
- const contract = result[0]
- return {
- success: true,
- enabled: true, // basic-info는 항상 활성화
- data: {
- // 기본 정보
- contractNumber: contract.contractNumber,
- contractName: contract.name,
- vendorId: contract.vendorId,
- vendorName: contract.vendorName,
- projectName: contract.projectName,
- contractType: contract.type,
- contractStatus: contract.status,
- startDate: contract.startDate,
- endDate: contract.endDate,
- contractAmount: contract.contractAmount,
- currency: contract.currency,
- description: contract.description,
- specificationType: contract.specificationType,
- specificationManualText: contract.specificationManualText,
- unitPriceType: contract.unitPriceType,
- warrantyPeriod: contract.warrantyPeriod,
- linkedPoNumber: contract.linkedPoNumber,
- linkedBidNumber: contract.linkedBidNumber,
- notes: contract.notes,
-
- // 지급/인도 조건
- paymentBeforeDelivery: contract.paymentBeforeDelivery,
- paymentDelivery: contract.paymentDelivery,
- paymentAfterDelivery: contract.paymentAfterDelivery,
- paymentTerm: contract.paymentTerm,
- taxType: contract.taxType,
- liquidatedDamages: contract.liquidatedDamages,
- liquidatedDamagesPercent: contract.liquidatedDamagesPercent,
- deliveryType: contract.deliveryType,
- deliveryTerm: contract.deliveryTerm,
- shippingLocation: contract.shippingLocation,
- dischargeLocation: contract.dischargeLocation,
- contractDeliveryDate: contract.contractDeliveryDate,
-
- // 추가 조건
- contractEstablishmentConditions: contract.contractEstablishmentConditions,
- interlockingSystem: contract.interlockingSystem,
- mandatoryDocuments: contract.mandatoryDocuments,
- contractTerminationConditions: contract.contractTerminationConditions
- }
- }
- } catch (error) {
- console.error('Error getting basic info:', error)
- return { success: false, error: '기본 정보 조회에 실패했습니다.' }
- }
-}
-
-
-export async function getCommunicationChannel(contractId: number) {
- try {
- const [contract] = await db
- .select({
- communicationChannels: generalContracts.communicationChannels
- })
- .from(generalContracts)
- .where(eq(generalContracts.id, contractId))
- .limit(1)
-
- if (!contract) {
- return null
- }
-
- return contract.communicationChannels as any
- } catch (error) {
- console.error('Error getting communication channel:', error)
- throw new Error('Failed to get communication channel')
- }
-}
-
-export async function updateCommunicationChannel(contractId: number, communicationData: Record<string, unknown>, userId: number) {
- try {
- await db
- .update(generalContracts)
- .set({
- communicationChannels: communicationData,
- lastUpdatedAt: new Date(),
- lastUpdatedById: userId
- })
- .where(eq(generalContracts.id, contractId))
-
- revalidatePath('/general-contracts')
- return { success: true }
- } catch (error) {
- console.error('Error updating communication channel:', error)
- throw new Error('Failed to update communication channel')
- }
-}
-
-export async function updateLocation(contractId: number, locationData: Record<string, unknown>, userId: number) {
- try {
- await db
- .update(generalContracts)
- .set({
- locations: locationData,
- lastUpdatedAt: new Date(),
- lastUpdatedById: userId
- })
- .where(eq(generalContracts.id, contractId))
-
- revalidatePath('/general-contracts')
- return { success: true }
- } catch (error) {
- console.error('Error updating location:', error)
- throw new Error('Failed to update location')
- }
-}
-
-export async function getLocation(contractId: number) {
- try {
- const [contract] = await db
- .select({
- locations: generalContracts.locations
- })
- .from(generalContracts)
- .where(eq(generalContracts.id, contractId))
- .limit(1)
-
- if (!contract) {
- return null
- }
-
- return contract.locations as any
- } catch (error) {
- console.error('Error getting location:', error)
- throw new Error('Failed to get location')
- }
-}
-
-export async function updateContract(id: number, data: Record<string, unknown>) {
- try {
- // 숫자 필드에서 빈 문자열을 null로 변환
- const cleanedData = { ...data }
- const numericFields = [
- 'vendorId',
- 'projectId',
- 'warrantyPeriodValue',
- 'warrantyPeriodMax',
- 'contractAmount',
- 'totalAmount',
- 'availableBudget',
- 'liquidatedDamages',
- 'liquidatedDamagesPercent',
- 'lastUpdatedById'
- ]
-
- // 모든 필드에서 빈 문자열, undefined, 빈 객체 등을 정리
- Object.keys(cleanedData).forEach(key => {
- const value = cleanedData[key]
-
- // 빈 문자열을 null로 변환
- if (value === '') {
- cleanedData[key] = null
- }
-
- // 빈 객체를 null로 변환
- if (value && typeof value === 'object' && Object.keys(value).length === 0) {
- cleanedData[key] = null
- }
- })
-
- // 숫자 필드들 추가 정리 (vendorId는 NOT NULL이므로 null로 설정하지 않음)
- numericFields.forEach(field => {
- if (field === 'vendorId') {
- // vendorId는 필수 필드이므로 null로 설정하지 않음
- if (cleanedData[field] === '' || cleanedData[field] === undefined || cleanedData[field] === 0) {
- // 유효하지 않은 값이면 에러 발생
- throw new Error('Vendor ID is required and cannot be null')
- }
- } else {
- // 다른 숫자 필드들은 빈 값이면 null로 설정
- if (cleanedData[field] === '' || cleanedData[field] === undefined || cleanedData[field] === 0) {
- cleanedData[field] = null
- }
- }
- })
-
- const [updatedContract] = await db
- .update(generalContracts)
- .set({
- ...cleanedData,
- lastUpdatedAt: new Date(),
- revision: (cleanedData.revision as number) ? (cleanedData.revision as number) + 1 : 0,
- })
- .where(eq(generalContracts.id, id))
- .returning()
-
- // Update contract items if provided
- if (data.contractItems && Array.isArray(data.contractItems)) {
- // Delete existing items
- await db
- .delete(generalContractItems)
- .where(eq(generalContractItems.contractId, id))
-
- // Insert new items
- if (data.contractItems.length > 0) {
- await db
- .insert(generalContractItems)
- .values(
- data.contractItems.map((item: any) => ({
- project: item.project,
- itemCode: item.itemCode,
- itemInfo: item.itemInfo,
- specification: item.specification,
- quantity: item.quantity,
- quantityUnit: item.quantityUnit,
- contractDeliveryDate: item.contractDeliveryDate,
- contractUnitPrice: item.contractUnitPrice,
- contractAmount: item.contractAmount,
- contractCurrency: item.contractCurrency,
- contractId: id,
- }))
- )
- }
- }
-
- // Update attachments if provided
- if (data.attachments && Array.isArray(data.attachments)) {
- // Delete existing attachments
- await db
- .delete(generalContractAttachments)
- .where(eq(generalContractAttachments.contractId, id))
-
- // Insert new attachments
- if (data.attachments.length > 0) {
- await db
- .insert(generalContractAttachments)
- .values(
- data.attachments.map((attachment: any) => ({
- ...attachment,
- contractId: id,
- }))
- )
- }
- }
-
- revalidatePath('/general-contracts')
- revalidatePath(`/general-contracts/detail/${id}`)
- return updatedContract
- } catch (error) {
- console.error('Error updating contract:', error)
- throw new Error('Failed to update contract')
- }
-}
-
-export async function deleteContract(id: number) {
- try {
- // 현재 계약 정보 조회
- await db
- .select({ revision: generalContracts.revision })
- .from(generalContracts)
- .where(eq(generalContracts.id, id))
- .limit(1)
-
- // 계약폐기: status를 'Contract Delete'로 변경
- const [updatedContract] = await db
- .update(generalContracts)
- .set({
- status: 'Contract Delete',
- lastUpdatedAt: new Date(),
- // revision: (currentContract[0]?.revision || 0) + 1 // 계약 파기 시 리비전 증가? 확인 필요
- })
- .where(eq(generalContracts.id, id))
- .returning()
-
- revalidatePath('/general-contracts')
- return { success: true, contract: updatedContract }
- } catch (error) {
- console.error('Error deleting contract:', error)
- throw new Error('Failed to delete contract')
- }
-}
-
-// 상태별 개수 집계
-export async function getGeneralContractStatusCounts() {
- try {
- const counts = await db
- .select({
- status: generalContracts.status,
- count: count(),
- })
- .from(generalContracts)
- .groupBy(generalContracts.status)
-
- return counts.reduce((acc, { status, count }) => {
- acc[status] = count
- return acc
- }, {} as Record<string, number>)
- } catch (error) {
- console.error('Failed to get contract status counts:', error)
- return {}
- }
-}
-
-// 계약구분별 개수 집계
-export async function getGeneralContractCategoryCounts() {
- try {
- const counts = await db
- .select({
- category: generalContracts.category,
- count: count(),
- })
- .from(generalContracts)
- .groupBy(generalContracts.category)
-
- return counts.reduce((acc, { category, count }) => {
- acc[category] = count
- return acc
- }, {} as Record<string, number>)
- } catch (error) {
- console.error('Failed to get contract category counts:', error)
- return {}
- }
-}
-
-export async function getVendors() {
- try {
- const vendorList = await db
- .select({
- id: vendors.id,
- vendorName: vendors.vendorName,
- vendorCode: vendors.vendorCode,
- })
- .from(vendors)
- .orderBy(asc(vendors.vendorName))
-
- return vendorList
- } catch (error) {
- console.error('Error fetching vendors:', error)
- throw new Error('Failed to fetch vendors')
- }
-}
-
-// 첨부파일 업로드
-export async function uploadContractAttachment(contractId: number, file: File, userId: string, documentName: string = '사양 및 공급범위') {
- try {
- // userId를 숫자로 변환
- const userIdNumber = parseInt(userId)
- if (isNaN(userIdNumber)) {
- throw new Error('Invalid user ID')
- }
-
- const saveResult = await saveDRMFile(
- file,
- decryptWithServerAction,
- `general-contracts/${contractId}/attachments`,
- userId,
- )
-
- if (saveResult.success && saveResult.publicPath) {
- // generalContractAttachments 테이블에 저장
- const [attachment] = await db.insert(generalContractAttachments).values({
- contractId,
- documentName,
- fileName: saveResult.fileName || file.name,
- filePath: saveResult.publicPath,
- uploadedById: userIdNumber,
- uploadedAt: new Date(),
- }).returning()
-
- return {
- success: true,
- message: '파일이 성공적으로 업로드되었습니다.',
- attachment
- }
- } else {
- return {
- success: false,
- error: saveResult.error || '파일 저장에 실패했습니다.'
- }
- }
- } catch (error) {
- console.error('Failed to upload contract attachment:', error)
- return {
- success: false,
- error: '파일 업로드에 실패했습니다.'
- }
- }
-}
-
-// 첨부파일 목록 조회
-export async function getContractAttachments(contractId: number) {
- try {
- const attachments = await db
- .select()
- .from(generalContractAttachments)
- .where(eq(generalContractAttachments.contractId, contractId))
- .orderBy(desc(generalContractAttachments.uploadedAt))
-
- return attachments
- } catch (error) {
- console.error('Failed to get contract attachments:', error)
- return []
- }
-}
-
-// 첨부파일 다운로드
-export async function getContractAttachmentForDownload(attachmentId: number, contractId: number) {
- try {
- const attachments = await db
- .select()
- .from(generalContractAttachments)
- .where(and(
- eq(generalContractAttachments.id, attachmentId),
- eq(generalContractAttachments.contractId, contractId)
- ))
- .limit(1)
-
- if (attachments.length === 0) {
- return {
- success: false,
- error: '첨부파일을 찾을 수 없습니다.'
- }
- }
-
- return {
- success: true,
- attachment: attachments[0]
- }
- } catch (error) {
- console.error('Failed to get contract attachment for download:', error)
- return {
- success: false,
- error: '첨부파일 다운로드 준비에 실패했습니다.'
- }
- }
-}
-
-// 첨부파일 삭제
-export async function deleteContractAttachment(attachmentId: number, contractId: number) {
- try {
- const attachments = await db
- .select()
- .from(generalContractAttachments)
- .where(and(
- eq(generalContractAttachments.id, attachmentId),
- eq(generalContractAttachments.contractId, contractId)
- ))
- .limit(1)
-
- if (attachments.length === 0) {
- return {
- success: false,
- error: '첨부파일을 찾을 수 없습니다.'
- }
- }
-
- // 데이터베이스에서 삭제
- await db
- .delete(generalContractAttachments)
- .where(eq(generalContractAttachments.id, attachmentId))
-
- return {
- success: true,
- message: '첨부파일이 삭제되었습니다.'
- }
- } catch (error) {
- console.error('Failed to delete contract attachment:', error)
- return {
- success: false,
- error: '첨부파일 삭제에 실패했습니다.'
- }
- }
-}
-
-// 계약승인요청용 파일 업로드 (DRM 사용)
-export async function uploadContractApprovalFile(contractId: number, file: File, userId: string) {
- try {
- const userIdNumber = parseInt(userId)
- if (isNaN(userIdNumber)) {
- throw new Error('Invalid user ID')
- }
-
- const saveResult = await saveDRMFile(
- file,
- decryptWithServerAction,
- `general-contracts/${contractId}/approval-documents`,
- userId,
- )
-
- if (saveResult.success && saveResult.publicPath) {
- return {
- success: true,
- message: '파일이 성공적으로 업로드되었습니다.',
- filePath: saveResult.publicPath,
- fileName: saveResult.fileName || file.name
- }
- } else {
- return {
- success: false,
- error: saveResult.error || '파일 저장에 실패했습니다.'
- }
- }
- } catch (error) {
- console.error('Failed to upload contract approval file:', error)
- return {
- success: false,
- error: '파일 업로드에 실패했습니다.'
- }
- }
-}
-
-
-
-// 계약승인요청 전송
-export async function sendContractApprovalRequest(
- contractSummary: any,
- pdfBuffer: Uint8Array,
- documentType: string,
- userId: string,
- generatedBasicContracts?: Array<{ key: string; buffer: number[]; fileName: string }>
-) {
- try {
- // contracts 테이블에 새 계약 생성 (generalContracts에서 contracts로 복사)
- const contractData = await mapContractSummaryToDb(contractSummary)
-
- const [newContract] = await db.insert(contracts).values({
- ...contractData,
- contractNo: contractData.contractNo || `GC-${Date.now()}`, // contractNumber 대신 contractNo 사용
- }).returning()
-
- const contractId = newContract.id
-
- // const items: {
- // id: number;
- // createdAt: Date;
- // updatedAt: Date;
- // contractId: number;
- // itemCode: string | null;
- // quantity: string | null;
- // contractAmount: string | null;
- // contractCurrency: string | null;
- // contractDeliveryDate: string | null;
- // specification: string | null;
- // itemInfo: string | null;
- // quantityUnit: string | null;
- // totalWeight: string | null;
- // weightUnit: string | null;
- // contractUnitPrice: string | null;
- // }[]
-
- // contractItems 테이블에 품목 정보 저장 (general-contract-items가 있을 때만)
- if (contractSummary.items && contractSummary.items.length > 0) {
- const projectNo = contractSummary.basicInfo?.projectCode || contractSummary.basicInfo?.projectId?.toString() || 'NULL'
-
- for (const item of contractSummary.items) {
- let itemId: number
-
- // 1. items 테이블에서 itemCode로 기존 아이템 검색
- if (item.itemCode) {
- // const existingItem = await db
- // .select({ id: items.id })
- // .from(items)
- // .where(and(
- // eq(items.itemCode, item.itemCode),
- // eq(items.ProjectNo, projectNo)
- // ))
- // .limit(1)
- const existingItem = await db
- .select({ id: items.id })
- .from(items)
- .where(
- eq(items.itemCode, item.itemCode)
- )
- .limit(1)
-
- if (existingItem.length > 0) {
- // 기존 아이템이 있으면 해당 ID 사용
- itemId = existingItem[0].id
- } else {
- // 기존 아이템이 없으면 새로 생성
- const newItem = await db.insert(items).values({
- ProjectNo: projectNo,
- itemCode: item.itemCode,
- itemName: item.itemInfo || item.description || item.itemCode,
- packageCode: item.itemCode,
- description: item.specification || item.description || '',
- unitOfMeasure: item.quantityUnit || 'EA',
- createdAt: new Date(),
- updatedAt: new Date(),
- }).returning({ id: items.id })
-
- itemId = newItem[0].id
- }
-
-
- // 2. contractItems에 저장
- await db.insert(contractItems).values({
- contractId,
- itemId: itemId,
- description: item.itemInfo || item.description || '',
- quantity: Math.floor(Number(item.quantity) || 1), // 정수로 변환
- unitPrice: item.contractUnitPrice || item.unitPrice || 0,
- taxRate: item.taxRate || 0,
- taxAmount: item.taxAmount || 0,
- totalLineAmount: item.contractAmount || item.totalLineAmount || 0,
- remark: item.remark || '',
- })
- }else{
- //아이템코드가 없으니 pass
- continue
- }
- }
- }
-
- // PDF 버퍼를 saveBuffer 함수로 저장
- const fileId = uuidv4()
- const fileName = `${fileId}.pdf`
-
- // PDF 버퍼를 Buffer로 변환
- let bufferData: Buffer
- if (Buffer.isBuffer(pdfBuffer)) {
- bufferData = pdfBuffer
- } else if (pdfBuffer instanceof ArrayBuffer) {
- bufferData = Buffer.from(pdfBuffer)
- } else if (pdfBuffer instanceof Uint8Array) {
- bufferData = Buffer.from(pdfBuffer)
- } else {
- bufferData = Buffer.from(pdfBuffer as any)
- }
-
- // saveBuffer 함수를 사용해서 파일 저장
- const saveResult = await saveBuffer({
- buffer: bufferData,
- fileName: fileName,
- directory: "generalContracts",
- originalName: `contract_${contractId}_${documentType}_${fileId}.pdf`,
- userId: userId
- })
-
- if (!saveResult.success) {
- throw new Error(saveResult.error || 'PDF 파일 저장에 실패했습니다.')
- }
-
- const finalFileName = saveResult.fileName || fileName
- const finalFilePath = saveResult.publicPath
- ? saveResult.publicPath.replace('/api/files/', '')
- : `/generalContracts/${fileName}`
-
- // contractEnvelopes 테이블에 서명할 PDF 파일 정보 저장
- const [newEnvelope] = await db.insert(contractEnvelopes).values({
- contractId: contractId,
- envelopeId: `envelope_${contractId}_${Date.now()}`,
- documentId: `document_${contractId}_${Date.now()}`,
- envelopeStatus: 'PENDING',
- fileName: finalFileName,
- filePath: finalFilePath,
- }).returning()
-
- // contractSigners 테이블에 벤더 서명자 정보 저장
- const vendorEmail = contractSummary.basicInfo?.vendorEmail || 'vendor@example.com'
- const vendorName = contractSummary.basicInfo?.vendorName || '벤더'
-
- await db.insert(contractSigners).values({
- envelopeId: newEnvelope.id,
- signerType: 'VENDOR',
- signerEmail: vendorEmail,
- signerName: vendorName,
- signerPosition: '대표자',
- signerStatus: 'PENDING',
- })
-
- // generalContractAttachments에 contractId 업데이트 (일반계약의 첨부파일들을 PO 계약과 연결)
- const generalContractId = contractSummary.basicInfo?.id || contractSummary.id
- if (generalContractId) {
- await db.update(generalContractAttachments)
- .set({ poContractId: contractId })
- .where(eq(generalContractAttachments.contractId, generalContractId))
- }
-
- // 기본계약 처리 (클라이언트에서 생성된 PDF 사용 또는 자동 생성)
- await processGeneratedBasicContracts(contractSummary, contractId, userId, generatedBasicContracts)
-
- try {
- sendEmail({
- to: contractSummary.basicInfo.vendorEmail,
- subject: `계약승인요청`,
- template: "contract-approval-request",
- context: {
- contractId: contractId,
- loginUrl: `${process.env.NEXT_PUBLIC_URL}/partners/po`,
- language: "ko",
- },
- })
- // 계약 상태 업데이트
- await db.update(generalContracts)
- .set({
- status: 'Contract Accept Request',
- lastUpdatedAt: new Date()
- })
- .where(eq(generalContracts.id, generalContractId))
-
- } catch (error) {
- console.error('계약승인요청 전송 오류:', error)
-
- }
-
-
- revalidatePath('/evcp/general-contracts')
- revalidatePath('/evcp/general-contracts/detail')
- revalidatePath('/evcp/general-contracts/detail/contract-approval-request-dialog')
-
- return {
- success: true,
- message: '계약승인요청이 성공적으로 전송되었습니다.',
- pdfPath: saveResult.publicPath
- }
-
- } catch (error: any) {
- console.error('계약승인요청 전송 오류:', error)
-
- // 중복 계약 번호 오류 처리
- if (error.message && error.message.includes('duplicate key value violates unique constraint')) {
- return {
- success: false,
- error: '이미 존재하는 계약번호입니다. 다른 계약번호를 사용해주세요.'
- }
- }
-
- // 다른 데이터베이스 오류 처리
- if (error.code === '23505') { // PostgreSQL unique constraint violation
- return {
- success: false,
- error: '중복된 데이터가 존재합니다. 입력값을 확인해주세요.'
- }
- }
-
- return {
- success: false,
- error: `계약승인요청 전송 중 오류가 발생했습니다: ${error.message}`
- }
- }
-}
-
-// 클라이언트에서 생성된 기본계약 처리 (RFQ-Last 방식)
-async function processGeneratedBasicContracts(
- contractSummary: any,
- contractId: number,
- userId: string,
- generatedBasicContracts: Array<{ key: string; buffer: number[]; fileName: string }>
-): Promise<void> {
- try {
- const userIdNumber = parseInt(userId)
- if (isNaN(userIdNumber)) {
- throw new Error('Invalid user ID')
- }
-
- console.log(`${generatedBasicContracts.length}개의 클라이언트 생성 기본계약을 처리합니다.`)
-
- // 기본계약 디렉토리 생성 (RFQ-Last 방식)
- const nasPath = process.env.NAS_PATH || "/evcp_nas"
- const isProduction = process.env.NODE_ENV === "production"
- const baseDir = isProduction ? nasPath : path.join(process.cwd(), "public")
- const contractsDir = path.join(baseDir, "basicContracts")
- await fs.mkdir(contractsDir, { recursive: true })
-
- for (const contractData of generatedBasicContracts) {
- try {
- console.log(contractSummary.basicInfo?.vendorId || 'unknown', contractData.buffer.length)
-
- // PDF 버퍼를 Buffer로 변환 및 파일 저장
- const pdfBuffer = Buffer.from(contractData.buffer)
- const fileName = contractData.fileName
- const filePath = path.join(contractsDir, fileName)
-
- await fs.writeFile(filePath, pdfBuffer)
-
- // key에서 템플릿 정보 추출 (vendorId_type_templateName 형식)
- const keyParts = contractData.key.split('_')
- const vendorId = parseInt(keyParts[0])
- const contractType = keyParts[1]
- const templateName = keyParts.slice(2).join('_')
-
- // 템플릿 조회
- const template = await getTemplateByName(templateName)
-
- console.log("템플릿", templateName, template)
-
- if (template) {
- // 웹 접근 경로 설정 (RFQ-Last 방식)
- let filePublicPath: string
- if (isProduction) {
- filePublicPath = `/api/files/basicContracts/${fileName}`
- } else {
- filePublicPath = `/basicContracts/${fileName}`
- }
-
- // basicContract 테이블에 저장
- const deadline = new Date()
- deadline.setDate(deadline.getDate() + 10) // 10일 후 마감
-
- await db.insert(basicContract).values({
- templateId: template.id,
- vendorId: vendorId,
- requestedBy: userIdNumber,
- generalContractId: contractSummary.basicInfo?.id || contractSummary.id,
- fileName: fileName,
- filePath: filePublicPath,
- deadline: deadline.toISOString().split('T')[0], // YYYY-MM-DD 형식으로
- status: 'PENDING'
- })
-
- console.log(`클라이언트 생성 기본계약 저장 완료:${contractData.fileName}`)
- } else {
- console.error(`템플릿을 찾을 수 없음: ${templateName}`)
- }
-
- } catch (error) {
- console.error(`기본계약 처리 실패 (${contractData.fileName}):`, error)
- // 개별 계약서 처리 실패는 전체 프로세스를 중단하지 않음
- }
- }
-
- } catch (error) {
- console.error('클라이언트 생성 기본계약 처리 중 오류:', error)
- // 기본계약 생성 실패는 계약 승인 요청 전체를 실패시키지 않음
- }
-}
-
-// 템플릿명으로 템플릿 조회 (RFQ-Last 방식)
-async function getTemplateByName(templateName: string) {
- const [template] = await db
- .select()
- .from(basicContractTemplates)
- .where(
- and(
- ilike(basicContractTemplates.templateName, `%${templateName}%`),
- eq(basicContractTemplates.status, "ACTIVE")
- )
- )
- .limit(1)
-
- return template
-}
-
-async function mapContractSummaryToDb(contractSummary: any) {
- const basicInfo = contractSummary.basicInfo || {}
-
- // 계약번호 생성
- const contractNumber = await generateContractNumber(
- basicInfo.userId,
- basicInfo.contractType || basicInfo.type || 'UP'
- )
-
- return {
- // 기본 정보
- projectId: basicInfo.projectId || null, // 기본값 설정
- vendorId: basicInfo.vendorId,
- contractNo: contractNumber,
- contractName: basicInfo.contractName || '계약승인요청',
- status: 'PENDING_APPROVAL',
-
- // 계약 기간
- startDate: basicInfo.startDate || new Date().toISOString().split('T')[0],
- endDate: basicInfo.endDate || new Date().toISOString().split('T')[0],
-
- // 지급/인도 조건
- paymentTerms: basicInfo.paymentTerm || '',
- deliveryTerms: basicInfo.deliveryTerm || '',
- deliveryDate: basicInfo.contractDeliveryDate || basicInfo.deliveryDate || new Date().toISOString().split('T')[0],
- shippmentPlace: basicInfo.shippingLocation || basicInfo.shippmentPlace || '',
- deliveryLocation: basicInfo.dischargeLocation || basicInfo.deliveryLocation || '',
-
- // 금액 정보
- budgetAmount: Number(basicInfo.totalAmount || basicInfo.contractAmount || 0),
- budgetCurrency: basicInfo.currency || basicInfo.contractCurrency || 'USD',
- totalAmountKrw: Number(basicInfo.totalAmount || basicInfo.contractAmount || 0),
- currency: basicInfo.currency || basicInfo.contractCurrency || 'USD',
- totalAmount: Number(basicInfo.totalAmount || basicInfo.contractAmount || 0),
-
- // // SAP ECC 관련 필드들
- // poVersion: basicInfo.revision || 1,
- // purchaseDocType: basicInfo.type || 'UP',
- // purchaseOrg: basicInfo.purchaseOrg || '',
- // purchaseGroup: basicInfo.purchaseGroup || '',
- // exchangeRate: Number(basicInfo.exchangeRate || 1),
-
- // // 계약/보증 관련
- // contractGuaranteeCode: basicInfo.contractGuaranteeCode || '',
- // defectGuaranteeCode: basicInfo.defectGuaranteeCode || '',
- // guaranteePeriodCode: basicInfo.guaranteePeriodCode || '',
- // advancePaymentYn: basicInfo.advancePaymentYn || 'N',
-
- // // 전자계약/승인 관련
- // electronicContractYn: basicInfo.electronicContractYn || 'Y',
- // electronicApprovalDate: basicInfo.electronicApprovalDate || null,
- // electronicApprovalTime: basicInfo.electronicApprovalTime || '',
- // ownerApprovalYn: basicInfo.ownerApprovalYn || 'N',
-
- // // 기타
- // plannedInOutFlag: basicInfo.plannedInOutFlag || 'I',
- // settlementStandard: basicInfo.settlementStandard || 'A',
- // weightSettlementFlag: basicInfo.weightSettlementFlag || 'N',
-
- // 연동제 관련
- priceIndexYn: basicInfo.priceIndexYn || 'N',
- writtenContractNo: basicInfo.contractNumber || '',
- contractVersion: basicInfo.revision || 1,
-
- // // 부분 납품/결제
- // partialShippingAllowed: basicInfo.partialShippingAllowed || false,
- // partialPaymentAllowed: basicInfo.partialPaymentAllowed || false,
-
- // 메모
- remarks: basicInfo.notes || basicInfo.remarks || '',
-
- // 버전 관리
- version: basicInfo.revision || 1,
-
- // 타임스탬프 (contracts 테이블 스키마에 맞게)
- createdAt: new Date(),
- updatedAt: new Date()
- }
-}
-
-// Field Service Rate 관련 서버 액션들
-export async function getFieldServiceRate(contractId: number) {
- try {
- const result = await db
- .select({ fieldServiceRates: generalContracts.fieldServiceRates })
- .from(generalContracts)
- .where(eq(generalContracts.id, contractId))
- .limit(1)
-
- if (result.length === 0) {
- return null
- }
-
- return result[0].fieldServiceRates as Record<string, unknown> || null
- } catch (error) {
- console.error('Failed to get field service rate:', error)
- throw new Error('Field Service Rate 데이터를 불러오는데 실패했습니다.')
- }
-}
-
-export async function updateFieldServiceRate(
- contractId: number,
- fieldServiceRateData: Record<string, unknown>,
- userId: number
-) {
- try {
- await db
- .update(generalContracts)
- .set({
- fieldServiceRates: fieldServiceRateData,
- lastUpdatedAt: new Date(),
- lastUpdatedById: userId
- })
- .where(eq(generalContracts.id, contractId))
-
- revalidatePath('/evcp/general-contracts')
- return { success: true }
- } catch (error) {
- console.error('Failed to update field service rate:', error)
- throw new Error('Field Service Rate 업데이트에 실패했습니다.')
- }
-}
-
-// Offset Details 관련 서버 액션들
-export async function getOffsetDetails(contractId: number) {
- try {
- const result = await db
- .select({ offsetDetails: generalContracts.offsetDetails })
- .from(generalContracts)
- .where(eq(generalContracts.id, contractId))
- .limit(1)
-
- if (result.length === 0) {
- return null
- }
-
- return result[0].offsetDetails as Record<string, unknown> || null
- } catch (error) {
- console.error('Failed to get offset details:', error)
- throw new Error('회입/상계내역 데이터를 불러오는데 실패했습니다.')
- }
-}
-
-export async function updateOffsetDetails(
- contractId: number,
- offsetDetailsData: Record<string, unknown>,
- userId: number
-) {
- try {
- await db
- .update(generalContracts)
- .set({
- offsetDetails: offsetDetailsData,
- lastUpdatedAt: new Date(),
- lastUpdatedById: userId
- })
- .where(eq(generalContracts.id, contractId))
-
- revalidatePath('/evcp/general-contracts')
- return { success: true }
- } catch (error) {
- console.error('Failed to update offset details:', error)
- throw new Error('회입/상계내역 업데이트에 실패했습니다.')
- }
-}
-
-// 계약번호 생성 함수
-export async function generateContractNumber(
- userId?: string,
- contractType: string
-): Promise<string> {
- try {
- // 계약종류 매핑 (2자리) - GENERAL_CONTRACT_TYPES 상수 사용
- const contractTypeMap: Record<string, string> = {
- 'UP': 'UP', // 자재단가계약
- 'LE': 'LE', // 임대차계약
- 'IL': 'IL', // 개별운송계약
- 'AL': 'AL', // 연간운송계약
- 'OS': 'OS', // 외주용역계약
- 'OW': 'OW', // 도급계약
- 'IS': 'IS', // 검사계약
- 'LO': 'LO', // LOI (의향서)
- 'FA': 'FA', // FA (Frame Agreement)
- 'SC': 'SC', // 납품합의계약 (Supply Contract)
- 'OF': 'OF', // 클레임상계계약 (Offset Agreement)
- 'AW': 'AW', // 사전작업합의 (Advanced Work)
- 'AD': 'AD', // 사전납품합의 (Advanced Delivery)
- 'AM': 'AM', // 설계계약
- 'SC_SELL': 'SC' // 폐기물매각계약 (Scrap) - 납품합의계약과 동일한 코드 사용
- }
-
- const typeCode = contractTypeMap[contractType] || 'XX' // 기본값
- // user 테이블의 user.userCode가 있으면 발주담당자 코드로 사용
- // userId가 주어졌을 때 user.userCode를 조회, 없으면 '000' 사용
- let purchaseManagerCode = '000';
- if (userId) {
- const user = await db
- .select({ userCode: users.userCode })
- .from(users)
- .where(eq(users.id, parseInt(userId || '0')))
- .limit(1);
- if (user[0]?.userCode && user[0].userCode.length >= 3) {
- purchaseManagerCode = user[0].userCode.substring(0, 3).toUpperCase();
- }
- }
- let managerCode: string
- if (purchaseManagerCode && purchaseManagerCode.length >= 3) {
- // 발주담당자 코드가 있으면 3자리 사용
- managerCode = purchaseManagerCode.substring(0, 3).toUpperCase()
- } else {
- // 발주담당자 코드가 없으면 일련번호로 대체 (001부터 시작)
- const currentYear = new Date().getFullYear()
- const prefix = `C${typeCode}${currentYear.toString().slice(-2)}`
-
- // 해당 패턴으로 시작하는 계약번호 중 가장 큰 일련번호 찾기
- const existingContracts = await db
- .select({ contractNumber: generalContracts.contractNumber })
- .from(generalContracts)
- .where(like(generalContracts.contractNumber, `${prefix}%`))
- .orderBy(desc(generalContracts.contractNumber))
- .limit(1)
-
- let sequenceNumber = 1
- if (existingContracts.length > 0) {
- const lastContractNumber = existingContracts[0].contractNumber
- const lastSequenceStr = lastContractNumber.slice(-3)
-
- // contractNumber에서 숫자만 추출하여 sequence 찾기
- const numericParts = lastContractNumber.match(/\d+/g)
- if (numericParts && numericParts.length > 0) {
- // 마지막 숫자 부분을 시퀀스로 사용 (일반적으로 마지막 3자리)
- const potentialSequence = numericParts[numericParts.length - 1]
- const lastSequence = parseInt(potentialSequence)
-
- if (!isNaN(lastSequence)) {
- sequenceNumber = lastSequence + 1
- }
- }
- // 숫자를 찾지 못했거나 파싱 실패 시 sequenceNumber = 1 유지
- }
-
- // 일련번호를 3자리로 포맷팅
- managerCode = sequenceNumber.toString().padStart(3, '0')
- }
-
- // 일련번호 생성 (3자리)
- const currentYear = new Date().getFullYear()
- const prefix = `C${managerCode}${typeCode}${currentYear.toString().slice(-2)}`
-
- // 해당 패턴으로 시작하는 계약번호 중 가장 큰 일련번호 찾기
- const existingContracts = await db
- .select({ contractNumber: generalContracts.contractNumber })
- .from(generalContracts)
- .where(like(generalContracts.contractNumber, `${prefix}%`))
- .orderBy(desc(generalContracts.contractNumber))
- .limit(1)
-
- let sequenceNumber = 1
- if (existingContracts.length > 0) {
- const lastContractNumber = existingContracts[0].contractNumber
-
- // contractNumber에서 숫자만 추출하여 sequence 찾기
- const numericParts = lastContractNumber.match(/\d+/g)
- if (numericParts && numericParts.length > 0) {
- // 마지막 숫자 부분을 시퀀스로 사용
- const potentialSequence = numericParts[numericParts.length - 1]
- const lastSequence = parseInt(potentialSequence)
-
- if (!isNaN(lastSequence)) {
- sequenceNumber = lastSequence + 1
- }
- }
- // 숫자를 찾지 못했거나 파싱 실패 시 sequenceNumber = 1 유지
- }
-
- // 최종 계약번호 생성: C + 발주담당자코드(3자리) + 계약종류(2자리) + 연도(2자리) + 일련번호(3자리)
- const finalSequence = sequenceNumber.toString().padStart(3, '0')
- const contractNumber = `C${managerCode}${typeCode}${currentYear.toString().slice(-2)}${finalSequence}`
-
- return contractNumber
-
- } catch (error) {
- console.error('계약번호 생성 오류:', error)
- throw new Error('계약번호 생성에 실패했습니다.')
- }
-}
-
-// 프로젝트 목록 조회
-export async function getProjects() {
- try {
- const projectList = await db
- .select({
- id: projects.id,
- code: projects.code,
- name: projects.name,
- type: projects.type,
- })
- .from(projects)
- .orderBy(asc(projects.name))
-
- return projectList
- } catch (error) {
- console.error('Error fetching projects:', error)
- throw new Error('Failed to fetch projects')
- }
-}
diff --git a/lib/general-contracts_old/types.ts b/lib/general-contracts_old/types.ts
deleted file mode 100644
index 2b6731b6..00000000
--- a/lib/general-contracts_old/types.ts
+++ /dev/null
@@ -1,125 +0,0 @@
-// 일반계약 관련 타입 정의
-
-// 1. 계약구분
-export const GENERAL_CONTRACT_CATEGORIES = [
- 'unit_price', // 단가계약
- 'general', // 일반계약
- 'sale' // 매각계약
-] as const;
-
-export type GeneralContractCategory = typeof GENERAL_CONTRACT_CATEGORIES[number];
-
-// 2. 계약종류
-export const GENERAL_CONTRACT_TYPES = [
- 'UP', // 자재단가계약
- 'LE', // 임대차계약
- 'IL', // 개별운송계약
- 'AL', // 연간운송계약
- 'OS', // 외주용역계약
- 'OW', // 도급계약
- 'IS', // 검사계약
- 'LO', // LOI (의향서)
- 'FA', // FA (Frame Agreement)
- 'SC', // 납품합의계약 (Supply Contract)
- 'OF', // 클레임상계계약 (Offset Agreement)
- 'AW', // 사전작업합의 (Advanced Work)
- 'AD', // 사전납품합의 (Advanced Delivery)
- 'AM', // 설계계약
- 'SC_SELL' // 폐기물매각계약 (Scrap) - 납품합의계약과 코드 중복으로 별도 명명
-] as const;
-
-export type GeneralContractType = typeof GENERAL_CONTRACT_TYPES[number];
-
-// 3. 계약상태
-export const GENERAL_CONTRACT_STATUSES = [
- 'Draft', // 임시 저장
- 'Request to Review', // 조건검토요청
- 'Confirm to Review', // 조건검토완료
- 'Contract Accept Request', // 계약승인요청
- 'Complete the Contract', // 계약체결(승인)
- 'Reject to Accept Contract', // 계약승인거절
- 'Contract Delete', // 계약폐기
- 'PCR Request', // PCR요청
- 'VO Request', // VO 요청
- 'PCR Accept', // PCR승인
- 'PCR Reject' // PCR거절
-] as const;
-
-export type GeneralContractStatus = typeof GENERAL_CONTRACT_STATUSES[number];
-
-// 4. 체결방식
-export const GENERAL_EXECUTION_METHODS = [
- '전자계약',
- '오프라인계약'
-] as const;
-
-export type GeneralExecutionMethod = typeof GENERAL_EXECUTION_METHODS[number];
-
-// 6. 계약확정범위
-export const GENERAL_CONTRACT_SCOPES = [
- '단가',
- '금액',
- '물량',
- '기타'
-] as const;
-
-export type GeneralContractScope = typeof GENERAL_CONTRACT_SCOPES[number];
-
-// 7. 납기종류
-export const GENERAL_DELIVERY_TYPES = [
- '단일납기',
- '분할납기',
- '구간납기'
-] as const;
-
-export type GeneralDeliveryType = typeof GENERAL_DELIVERY_TYPES[number];
-
-// 8. 연동제 적용 여부
-export const GENERAL_LINKAGE_TYPES = [
- 'Y',
- 'N'
-] as const;
-
-export type GeneralLinkageType = typeof GENERAL_LINKAGE_TYPES[number];
-
-// 9. 하도급법 점검결과
-export const GENERAL_COMPLIANCE_RESULTS = [
- '준수',
- '위반',
- '위반의심'
-] as const;
-
-export type GeneralComplianceResult = typeof GENERAL_COMPLIANCE_RESULTS[number];
-
-// 타입 가드 함수들
-export const isGeneralContractCategory = (value: string): value is GeneralContractCategory => {
- return GENERAL_CONTRACT_CATEGORIES.includes(value as GeneralContractCategory);
-};
-
-export const isGeneralContractType = (value: string): value is GeneralContractType => {
- return GENERAL_CONTRACT_TYPES.includes(value as GeneralContractType);
-};
-
-export const isGeneralContractStatus = (value: string): value is GeneralContractStatus => {
- return GENERAL_CONTRACT_STATUSES.includes(value as GeneralContractStatus);
-};
-
-export const isGeneralExecutionMethod = (value: string): value is GeneralExecutionMethod => {
- return GENERAL_EXECUTION_METHODS.includes(value as GeneralExecutionMethod);
-};
-
-export const isGeneralContractScope = (value: string): value is GeneralContractScope => {
- return GENERAL_CONTRACT_SCOPES.includes(value as GeneralContractScope);
-};
-
-export const isGeneralDeliveryType = (value: string): value is GeneralDeliveryType => {
- return GENERAL_DELIVERY_TYPES.includes(value as GeneralDeliveryType);
-};
-
-export const isGeneralLinkageType = (value: string): value is GeneralLinkageType => {
- return GENERAL_LINKAGE_TYPES.includes(value as GeneralLinkageType);
-};
-
-export const isGeneralComplianceResult = (value: string): value is GeneralComplianceResult => {
- return GENERAL_COMPLIANCE_RESULTS.includes(value as GeneralComplianceResult);
-};
diff --git a/lib/general-contracts_old/validation.ts b/lib/general-contracts_old/validation.ts
deleted file mode 100644
index 5aa516e7..00000000
--- a/lib/general-contracts_old/validation.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import { generalContracts } from "@/db/schema/generalContract"
-import {
- createSearchParamsCache,
- parseAsArrayOf,
- parseAsInteger,
- parseAsString,
- parseAsStringEnum,
-} from "nuqs/server"
-import * as z from "zod"
-
-import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers"
-
-export const searchParamsCache = createSearchParamsCache({
- flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault([]),
- page: parseAsInteger.withDefault(1),
- perPage: parseAsInteger.withDefault(10),
- sort: getSortingStateParser<typeof generalContracts.$inferSelect>().withDefault([
- { id: "registeredAt", desc: true },
- ]),
-
- // 기본 필터
- contractNumber: parseAsString.withDefault(""),
- name: parseAsString.withDefault(""),
- status: parseAsArrayOf(z.enum(generalContracts.status.enumValues)).withDefault([]),
- category: parseAsArrayOf(z.enum(generalContracts.category.enumValues)).withDefault([]),
- type: parseAsArrayOf(z.enum(generalContracts.type.enumValues)).withDefault([]),
- executionMethod: parseAsArrayOf(z.enum(generalContracts.executionMethod.enumValues)).withDefault([]),
- contractSourceType: parseAsArrayOf(z.enum(generalContracts.contractSourceType.enumValues)).withDefault([]),
- vendorId: parseAsInteger.withDefault(0),
- managerName: parseAsString.withDefault(""),
-
- // 날짜 필터
- registeredAtFrom: parseAsString.withDefault(""),
- registeredAtTo: parseAsString.withDefault(""),
- signedAtFrom: parseAsString.withDefault(""),
- signedAtTo: parseAsString.withDefault(""),
- startDateFrom: parseAsString.withDefault(""),
- startDateTo: parseAsString.withDefault(""),
- endDateFrom: parseAsString.withDefault(""),
- endDateTo: parseAsString.withDefault(""),
-
- // 금액 필터
- contractAmountMin: parseAsString.withDefault(""),
- contractAmountMax: parseAsString.withDefault(""),
-
- // 고급 필터
- filters: getFiltersStateParser().withDefault([]),
- joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"),
- search: parseAsString.withDefault(""),
-})
-
-export type GetGeneralContractsSchema = Awaited<ReturnType<typeof searchParamsCache.parse>>
-
-export const createGeneralContractSchema = z.object({
- contractNumber: z.string().optional(),
- name: z.string().min(1, "계약명을 입력해주세요"),
- category: z.string().min(1, "계약구분을 선택해주세요"),
- type: z.string().min(1, "계약종류를 선택해주세요"),
- executionMethod: z.string().min(1, "체결방식을 선택해주세요"),
- vendorId: z.number().min(1, "협력업체를 선택해주세요"),
- startDate: z.string().min(1, "계약시작일을 선택해주세요"),
- endDate: z.string().min(1, "계약종료일을 선택해주세요"),
- validityEndDate: z.string().optional(),
- contractScope: z.string().optional(),
- specificationType: z.string().optional(),
- specificationManualText: z.string().optional(),
- contractAmount: z.number().optional(),
- currency: z.string().default("KRW"),
- notes: z.string().optional(),
- linkedRfqOrItb: z.string().optional(),
- linkedPoNumber: z.string().optional(),
- linkedBidNumber: z.string().optional(),
- registeredById: z.number().min(1, "등록자 ID가 필요합니다"),
- lastUpdatedById: z.number().min(1, "수정자 ID가 필요합니다"),
-})
-
-export const updateGeneralContractSchema = createGeneralContractSchema.partial().extend({
- id: z.number().min(1, "계약 ID가 필요합니다"),
-})
-
-export type CreateGeneralContractInput = z.infer<typeof createGeneralContractSchema>
-export type UpdateGeneralContractInput = z.infer<typeof updateGeneralContractSchema>
diff --git a/lib/mail/templates/risks-notification.hbs b/lib/mail/templates/risks-notification.hbs
index 24a37e00..8dd53e83 100644
--- a/lib/mail/templates/risks-notification.hbs
+++ b/lib/mail/templates/risks-notification.hbs
@@ -62,7 +62,7 @@
</tr>
<tr>
<td bgcolor="#f2f2f2" colspan="2" align="center" style="border: 1px solid #cccccc; color: #000000; font-family: Noto Sans KR, sans-serif; font-size: 14px;">
- <b>코드</b>
+ <b>벤더코드</b>
</td>
<td colspan="2" align="center" style="border: 1px solid #cccccc; color: #000000; font-family: Noto Sans KR, sans-serif; font-size: 14px;">
{{vendorCode}}
diff --git a/lib/pq/service.ts b/lib/pq/service.ts
index 15d9aebc..fd71888e 100644
--- a/lib/pq/service.ts
+++ b/lib/pq/service.ts
@@ -3777,7 +3777,28 @@ export async function createPQListAction(input: CreatePqListInput) {
if (existingGeneralPQ.length > 0) {
return {
success: false,
- error: "General PQ 목록은 하나만 생성할 수 있습니다"
+ error: "General PQ 목록은 하나만 생성할 수 있습니다. 먼저 활성화된 General PQ를 비활성화한 후 생성해주세요."
+ };
+ }
+ }
+
+ // Non-Inspection PQ인 경우 중복 체크
+ if (validated.type === "NON_INSPECTION") {
+ const existingNonInspectionPQ = await db
+ .select()
+ .from(pqLists)
+ .where(
+ and(
+ eq(pqLists.type, "NON_INSPECTION"),
+ eq(pqLists.isDeleted, false)
+ )
+ )
+ .limit(1);
+
+ if (existingNonInspectionPQ.length > 0) {
+ return {
+ success: false,
+ error: "미실사 PQ 목록은 하나만 생성할 수 있습니다. 먼저 활성화된 미실사 PQ를 비활성화한 후 생성해주세요."
};
}
}
@@ -3829,6 +3850,20 @@ export async function createPQListAction(input: CreatePqListInput) {
})
.returning();
+ // NON_INSPECTION 타입인 경우 기존 활성화된 NON_INSPECTION 리스트들을 비활성화
+ if (validated.type === "NON_INSPECTION") {
+ await tx
+ .update(pqLists)
+ .set({ isDeleted: true, updatedAt: now, updatedBy: userId })
+ .where(
+ and(
+ eq(pqLists.type, "NON_INSPECTION"),
+ eq(pqLists.isDeleted, false),
+ ne(pqLists.id, newPqList.id) // 새로 생성한 리스트 제외
+ )
+ );
+ }
+
// 프로젝트 PQ인 경우 General PQ 항목들을 자동으로 복사
let copiedCriteriaCount = 0;
if (validated.type === "PROJECT") {
@@ -4417,6 +4452,58 @@ export async function getPQListInfo(pqListId: number) {
)();
}
+// 활성화된 미실사 PQ 리스트 조회 (하나만 존재한다고 가정)
+export async function getNonInspectionPQLists() {
+ unstable_noStore();
+
+ try {
+ const pqList = await db
+ .select({
+ id: pqLists.id,
+ name: pqLists.name,
+ type: pqLists.type,
+ projectId: pqLists.projectId,
+ validTo: pqLists.validTo,
+ isDeleted: pqLists.isDeleted,
+ createdAt: pqLists.createdAt,
+ updatedAt: pqLists.updatedAt,
+ })
+ .from(pqLists)
+ .where(and(
+ eq(pqLists.type, "NON_INSPECTION"),
+ eq(pqLists.isDeleted, false)
+ ))
+ .limit(1)
+ .then(rows => rows[0]);
+
+ if (!pqList) {
+ return {
+ success: false,
+ error: "활성화된 미실사 PQ 목록을 찾을 수 없습니다"
+ };
+ }
+
+ // 현재 시간과 비교하여 상태 결정
+ const now = new Date();
+ const isValid = !pqList.validTo || pqList.validTo > now;
+ const status = isValid ? "ACTIVE" : "INACTIVE";
+
+ return {
+ success: true,
+ data: {
+ ...pqList,
+ status
+ }
+ };
+ } catch (error) {
+ console.error("Error in getNonInspectionPQLists:", error);
+ return {
+ success: false,
+ error: "미실사 PQ 목록 정보를 가져오는 중 오류가 발생했습니다"
+ };
+ }
+}
+
export async function getPQsByListId(pqListId: number, input: GetPQSchema) {
return unstable_cache(
async () => {
diff --git a/lib/pq/table/copy-pq-list-dialog.tsx b/lib/pq/table/copy-pq-list-dialog.tsx
index 51b7eed1..a92407e4 100644
--- a/lib/pq/table/copy-pq-list-dialog.tsx
+++ b/lib/pq/table/copy-pq-list-dialog.tsx
@@ -132,7 +132,7 @@ export function CopyPqDialog({
</FormControl>
<SelectContent>
{pqLists
- .filter(pqList => pqList.type !== "GENERAL") // 일반 PQ 제외
+ .filter(pqList => pqList.type !== "GENERAL" && pqList.type !== "NON_INSPECTION") // 일반 PQ 제외 미실사 PQ 제외
.map((pqList) => (
<SelectItem key={pqList.id} value={pqList.id.toString()}>
<div className="flex items-center gap-2">
diff --git a/lib/rfq-last/attachment/vendor-response-table.tsx b/lib/rfq-last/attachment/vendor-response-table.tsx
index 22f813b3..b09487d6 100644
--- a/lib/rfq-last/attachment/vendor-response-table.tsx
+++ b/lib/rfq-last/attachment/vendor-response-table.tsx
@@ -590,7 +590,7 @@ export function VendorResponseTable({
) : (
<>
<CheckCircle2 className="h-3 w-3 mr-1" />
- {selectedVendor} 문서 확정
+ {selectedVendor} 설계전송 문서 확정
</>
)}
</Button>
diff --git a/lib/rfq-last/quotation-compare-view.tsx b/lib/rfq-last/quotation-compare-view.tsx
index 3bb27b55..7a4fd751 100644
--- a/lib/rfq-last/quotation-compare-view.tsx
+++ b/lib/rfq-last/quotation-compare-view.tsx
@@ -70,7 +70,6 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) {
const [selectedResponse, setSelectedResponse] = React.useState<VendorResponseVersion | null>(null);
const [selectedVendorName, setSelectedVendorName] = React.useState<string>("");
const [selectedContractType, setSelectedContractType] = React.useState<"PO" | "CONTRACT" | "BIDDING" | "">("");
- const [selectionReason, setSelectionReason] = React.useState("");
const [cancelReason, setCancelReason] = React.useState("");
const [isSubmitting, setIsSubmitting] = React.useState(false);
const router = useRouter()
@@ -323,11 +322,6 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) {
return;
}
- if (!selectionReason.trim()) {
- toast.error("선정 사유를 입력해주세요.");
- return;
- }
-
setIsSubmitting(true);
try {
const vendor = data.vendors.find(v => v.vendorId === parseInt(selectedVendorId));
@@ -348,8 +342,8 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) {
vendorCode: vendor.vendorCode,
totalAmount: latestResponse.totalAmount,
currency: latestResponse.currency,
- selectionReason: selectionReason,
- priceRank: latestResponse.rank || 0,
+ selectionReason: "",
+ priceRank: 0,
hasConditionDifferences: latestResponse.conditionDifferences.hasDifferences,
criticalDifferences: latestResponse.conditionDifferences.criticalDifferences,
});
@@ -357,7 +351,6 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) {
if (result.success) {
toast.success("업체가 성공적으로 선정되었습니다.");
setShowSelectionDialog(false);
- setSelectionReason("");
router.refresh()
} else {
throw new Error(result.error || "업체 선정 중 오류가 발생했습니다.");
@@ -410,24 +403,25 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) {
<Button
variant="destructive"
onClick={() => setShowCancelDialog(true)}
+ disabled={selectedContractType !== ""}
className="gap-2"
>
<X className="h-4 w-4" />
선정 취소
</Button>
)}
- <Button
+ {/* <Button
variant="outline"
onClick={() => {
setSelectedVendorId("");
setShowSelectionDialog(true);
}}
- disabled={isSelectionApproved}
+ disabled={isSelectionApproved || selectedContractType !== ""}
className="gap-2"
>
<RefreshCw className="h-4 w-4" />
재선정
- </Button>
+ </Button> */}
</>
) : (
<Button
@@ -479,7 +473,6 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) {
<p className="font-semibold">선정 업체: {selectedVendor.vendorName} ({selectedVendor.vendorCode})</p>
<p>선정 금액: {formatAmount(selectedVendor.totalAmount, selectedVendor.currency)}</p>
<p>선정일: {selectedVendor.selectionDate ? format(new Date(selectedVendor.selectionDate), "yyyy년 MM월 dd일", { locale: ko }) : "-"}</p>
- <p>선정 사유: {selectedVendor.selectionReason || "-"}</p>
{selectedVendor.contractNo && (
<>
<div className="border-t pt-1 mt-2">
@@ -508,6 +501,7 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) {
setSelectedContractType("PO");
setShowContractDialog(true);
}}
+ disabled={data.rfqInfo.rfqCode?.startsWith("I")}
className="gap-1 bg-green-600 hover:bg-green-700 text-xs"
>
<FileText className="h-3 w-3" />
@@ -929,7 +923,7 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) {
{/* 납기일 */}
<tr>
- <td className="p-3 font-medium">납기일</td>
+ <td className="p-3 font-medium">PR납기 요청일</td>
{data.vendors.map((vendor) => {
const latestResponse = vendor.responses[0];
return (
@@ -1245,36 +1239,17 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) {
)}
</span>
</div>
- <div className="flex justify-between">
- <span className="text-sm font-medium">가격 순위</span>
- <span className="text-sm">
- #{latestResponse?.rank || 0}
- </span>
- </div>
{latestResponse?.conditionDifferences.hasDifferences && (
<Alert className="mt-2">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
- 제시 조건과 차이가 있습니다. 선정 사유를 명확히 기재해주세요.
+ 제시 조건과 차이가 있습니다.
</AlertDescription>
</Alert>
)}
</div>
</div>
- <div className="space-y-2">
- <label htmlFor="selection-reason" className="text-sm font-medium">
- 선정 사유 *
- </label>
- <textarea
- id="selection-reason"
- className="w-full min-h-[100px] p-3 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
- placeholder="업체 선정 사유를 입력해주세요..."
- value={selectionReason}
- onChange={(e) => setSelectionReason(e.target.value)}
- required
- />
- </div>
</div>
);
})()}
@@ -1284,7 +1259,6 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) {
variant="outline"
onClick={() => {
setShowSelectionDialog(false);
- setSelectionReason("");
}}
disabled={isSubmitting}
>
@@ -1292,7 +1266,7 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) {
</Button>
<Button
onClick={handleVendorSelection}
- disabled={!selectionReason || isSubmitting}
+ disabled={isSubmitting}
>
{isSubmitting ? "처리 중..." : hasSelection ? "재선정 확정" : "선정 확정"}
</Button>
diff --git a/lib/rfq-last/table/rfq-table-columns.tsx b/lib/rfq-last/table/rfq-table-columns.tsx
index e8a5ba94..6976e1c5 100644
--- a/lib/rfq-last/table/rfq-table-columns.tsx
+++ b/lib/rfq-last/table/rfq-table-columns.tsx
@@ -34,7 +34,7 @@ const getStatusBadgeVariant = (status: string) => {
case "RFQ 생성": return "outline";
case "구매담당지정": return "secondary";
case "견적요청문서 확정": return "default";
- case "Short List 확정": return "default";
+ case "TBE 요청": return "default";
case "TBE 완료": return "default";
case "RFQ 발송": return "default";
case "견적접수": return "default";
diff --git a/lib/rfq-last/table/rfq-table.tsx b/lib/rfq-last/table/rfq-table.tsx
index 46bb4670..80f1422e 100644
--- a/lib/rfq-last/table/rfq-table.tsx
+++ b/lib/rfq-last/table/rfq-table.tsx
@@ -258,7 +258,7 @@ export function RfqTable({
{ label: "RFQ 생성", value: "RFQ 생성" },
{ label: "구매담당지정", value: "구매담당지정" },
{ label: "견적요청문서 확정", value: "견적요청문서 확정" },
- { label: "Short List 확정", value: "Short List 확정" },
+ { label: "TBE 요청", value: "TBE 요청" },
{ label: "TBE 완료", value: "TBE 완료" },
{ label: "RFQ 발송", value: "RFQ 발송" },
{ label: "견적접수", value: "견적접수" },
diff --git a/lib/rfq-last/validations.ts b/lib/rfq-last/validations.ts
index 6a5816d4..6b39d52d 100644
--- a/lib/rfq-last/validations.ts
+++ b/lib/rfq-last/validations.ts
@@ -17,7 +17,7 @@ import { RfqLastAttachments } from "@/db/schema";
{ value: "RFQ 생성", label: "RFQ 생성" },
{ value: "구매담당지정", label: "구매담당지정" },
{ value: "견적요청문서 확정", label: "견적요청문서 확정" },
- { value: "Short List 확정", label: "Short List 확정" },
+ { value: "TBE 요청", label: "TBE 요청" },
{ value: "TBE 완료", label: "TBE 완료" },
{ value: "RFQ 발송", label: "RFQ 발송" },
{ value: "견적접수", label: "견적접수" },
diff --git a/lib/rfq-last/vendor/batch-update-conditions-dialog.tsx b/lib/rfq-last/vendor/batch-update-conditions-dialog.tsx
index c258293b..6112aed4 100644
--- a/lib/rfq-last/vendor/batch-update-conditions-dialog.tsx
+++ b/lib/rfq-last/vendor/batch-update-conditions-dialog.tsx
@@ -1096,36 +1096,18 @@ export function BatchUpdateConditionsDialog({
checked={fieldsToUpdate.materialPrice}
onCheckedChange={(checked) => {
setFieldsToUpdate({ ...fieldsToUpdate, materialPrice: !!checked });
- if (checked) {
- form.setValue("materialPriceRelatedYn", true);
- }
}}
/>
- <FormField
- control={form.control}
- name="materialPriceRelatedYn"
- render={({ field }) => (
- <FormItem className="flex-1 flex items-center justify-between">
- <div className="space-y-0.5">
- <FormLabel className={cn(
- !fieldsToUpdate.materialPrice && "text-muted-foreground"
- )}>
- 연동제 적용 요건문의
- </FormLabel>
- <div className="text-sm text-muted-foreground">
- 원자재 가격 연동 여부
- </div>
- </div>
- <FormControl>
- <Switch
- checked={field.value}
- onCheckedChange={field.onChange}
- disabled={!fieldsToUpdate.materialPrice}
- />
- </FormControl>
- </FormItem>
- )}
- />
+ <div className="space-y-0.5 flex-1">
+ <FormLabel className={cn(
+ !fieldsToUpdate.materialPrice && "text-muted-foreground"
+ )}>
+ 연동제 적용 요건문의
+ </FormLabel>
+ <div className="text-sm text-muted-foreground">
+ 원자재 가격 연동 여부
+ </div>
+ </div>
</div>
{/* Spare Part */}
@@ -1140,33 +1122,18 @@ export function BatchUpdateConditionsDialog({
}
}}
/>
- <FormField
- control={form.control}
- name="sparepartYn"
- render={({ field }) => (
- <FormItem className="flex-1 flex items-center justify-between">
- <div className="space-y-0.5">
- <FormLabel className={cn(
- !fieldsToUpdate.sparepart && "text-muted-foreground"
- )}>
- Spare Part
- </FormLabel>
- <div className="text-sm text-muted-foreground">
- 예비 부품 요구사항
- </div>
- </div>
- <FormControl>
- <Switch
- checked={field.value}
- onCheckedChange={field.onChange}
- disabled={!fieldsToUpdate.sparepart}
- />
- </FormControl>
- </FormItem>
- )}
- />
+ <div className="space-y-0.5 flex-1">
+ <FormLabel className={cn(
+ !fieldsToUpdate.sparepart && "text-muted-foreground"
+ )}>
+ Spare Part
+ </FormLabel>
+ <div className="text-sm text-muted-foreground">
+ 예비 부품 요구사항
+ </div>
+ </div>
</div>
- {form.watch("sparepartYn") && fieldsToUpdate.sparepart && (
+ {fieldsToUpdate.sparepart && (
<FormField
control={form.control}
name="sparepartDescription"
@@ -1198,33 +1165,18 @@ export function BatchUpdateConditionsDialog({
}
}}
/>
- <FormField
- control={form.control}
- name="firstYn"
- render={({ field }) => (
- <FormItem className="flex-1 flex items-center justify-between">
- <div className="space-y-0.5">
- <FormLabel className={cn(
- !fieldsToUpdate.first && "text-muted-foreground"
- )}>
- 초도품 관리
- </FormLabel>
- <div className="text-sm text-muted-foreground">
- 초도품 관리 요구사항
- </div>
- </div>
- <FormControl>
- <Switch
- checked={field.value}
- onCheckedChange={field.onChange}
- disabled={!fieldsToUpdate.first}
- />
- </FormControl>
- </FormItem>
- )}
- />
+ <div className="space-y-0.5 flex-1">
+ <FormLabel className={cn(
+ !fieldsToUpdate.first && "text-muted-foreground"
+ )}>
+ 초도품 관리
+ </FormLabel>
+ <div className="text-sm text-muted-foreground">
+ 초도품 관리 요구사항
+ </div>
+ </div>
</div>
- {form.watch("firstYn") && fieldsToUpdate.first && (
+ {fieldsToUpdate.first && (
<FormField
control={form.control}
name="firstDescription"
diff --git a/lib/rfq-last/vendor/rfq-vendor-table.tsx b/lib/rfq-last/vendor/rfq-vendor-table.tsx
index 20dc5409..efc17171 100644
--- a/lib/rfq-last/vendor/rfq-vendor-table.tsx
+++ b/lib/rfq-last/vendor/rfq-vendor-table.tsx
@@ -333,23 +333,44 @@ export function RfqVendorTable({
console.log(mergedData, "mergedData")
console.log(rfqId, "rfqId")
- // Short List 확정 핸들러
+ // TBE 요청 핸들러
const handleShortListConfirm = React.useCallback(async () => {
try {
setIsUpdatingShortList(true);
- // response가 있는 벤더들만 필터링
- const vendorsWithResponse = selectedRows.filter(vendor =>
- vendor.response && vendor.response.vendor&& vendor.response.isDocumentConfirmed
- );
+ // response가 있는 벤더들 필터링
+ const vendorsWithResponse = selectedRows.filter(vendor =>
+ vendor.response && vendor.response.vendor
+ );
- if (vendorsWithResponse.length === 0) {
- toast.warning("응답이 있는 벤더를 선택해주세요.");
- return;
- }
+ if (vendorsWithResponse.length === 0) {
+ toast.warning("응답이 있는 벤더를 선택해주세요.");
+ return;
+ }
+
+ // 문서확정된 벤더들 필터링
+ const vendorsWithConfirmedDocs = vendorsWithResponse.filter(vendor =>
+ vendor.response.isDocumentConfirmed
+ );
+
+ // 문서확정되지 않은 벤더가 있는 경우 경고 메시지 표시
+ const vendorsWithoutConfirmedDocs = vendorsWithResponse.filter(vendor =>
+ !vendor.response.isDocumentConfirmed
+ );
+
+ if (vendorsWithoutConfirmedDocs.length > 0) {
+ toast.warning("벤더회신문서를 확인하시고 설계전송 문서 확정해주세요");
+ return;
+ }
+
+ // 문서확정된 벤더만 TBE 요청 처리
+ if (vendorsWithConfirmedDocs.length === 0) {
+ toast.warning("문서가 확정된 벤더가 없습니다.");
+ return;
+ }
- const vendorIds = vendorsWithResponse
+ const vendorIds = vendorsWithConfirmedDocs
.map(vendor => vendor.vendorId)
.filter(id => id != null);
@@ -361,8 +382,8 @@ export function RfqVendorTable({
router.refresh();
}
} catch (error) {
- console.error("Short List 확정 실패:", error);
- toast.error("Short List 확정에 실패했습니다.");
+ console.error("TBE 요청 실패:", error);
+ toast.error("TBE 요청에 실패했습니다.");
} finally {
setIsUpdatingShortList(false);
}
@@ -1478,7 +1499,7 @@ export function RfqVendorTable({
},
{
id: "responseDetail",
- header: "회신상세",
+ header: "제출여부",
cell: ({ row }) => {
const hasResponse = !!row.original.response?.submission?.submittedAt;
@@ -1820,7 +1841,10 @@ export function RfqVendorTable({
// 참여 의사가 있는 선택된 벤더 수 계산
const participatingCount = selectedRows.length;
const shortListCount = selectedRows.filter(v => v.shortList).length;
- const vendorsWithResponseCount = selectedRows.filter(v => v.response && v.response.vendor && v.response.isDocumentConfirmed).length;
+ // TBE 요청 버튼용: 응답이 있는 벤더 수 (문서확정 여부와 무관)
+ const vendorsWithResponseCount = selectedRows.filter(v => v.response && v.response.vendor).length;
+ // 문서확정된 벤더 수
+ const vendorsWithConfirmedDocsCount = selectedRows.filter(v => v.response && v.response.vendor && v.response.isDocumentConfirmed).length;
// 견적서가 있는 선택된 벤더 수 계산 (취소되지 않은 벤더만)
const quotationCount = nonCancelledRows.filter(row =>
@@ -1897,7 +1921,7 @@ export function RfqVendorTable({
</Button>
)}
- {/* Short List 확정 버튼 */}
+ {/* TBE 요청 버튼 */}
{!rfqCode?.startsWith("F") &&
<Button
variant="outline"
@@ -1914,7 +1938,7 @@ export function RfqVendorTable({
) : (
<>
<CheckSquare className="h-4 w-4 mr-2" />
- Short List 확정
+ TBE 요청
{vendorsWithResponseCount > 0 && ` (${vendorsWithResponseCount})`}
</>
)}
diff --git a/lib/rfq-last/vendor/send-rfq-dialog.tsx b/lib/rfq-last/vendor/send-rfq-dialog.tsx
index bf90bc6e..b5dcad5b 100644
--- a/lib/rfq-last/vendor/send-rfq-dialog.tsx
+++ b/lib/rfq-last/vendor/send-rfq-dialog.tsx
@@ -1059,7 +1059,7 @@ export function SendRfqDialog({
<th className="text-left p-2 text-xs font-medium">No.</th>
<th className="text-left p-2 text-xs font-medium">업체명</th>
<th className="text-left p-2 text-xs font-medium">기본계약</th>
- <th className="text-left p-2 text-xs font-medium">
+ {/* <th className="text-left p-2 text-xs font-medium">
<div className="flex items-center gap-2">
<span>계약서 재발송</span>
{vendorsWithRecipients.some(v => v.sendVersion && v.sendVersion > 0) && (
@@ -1091,7 +1091,7 @@ export function SendRfqDialog({
</TooltipProvider>
)}
</div>
- </th>
+ </th> */}
<th className="text-left p-2 text-xs font-medium">주 수신자</th>
<th className="text-left p-2 text-xs font-medium">CC</th>
<th className="text-left p-2 text-xs font-medium">작업</th>
@@ -1222,7 +1222,7 @@ export function SendRfqDialog({
<span className="text-xs text-muted-foreground">없음</span>
)}
</td>
- <td className="p-2">
+ {/* <td className="p-2">
{isResend && contracts.length > 0 ? (
<div className="flex items-center justify-center">
<TooltipProvider>
@@ -1259,7 +1259,7 @@ export function SendRfqDialog({
{isResend ? "계약서 없음" : "-"}
</span>
)}
- </td>
+ </td> */}
<td className="p-2">
<Select
value={vendor.selectedMainEmail}
diff --git a/lib/site-visit/client-site-visit-wrapper.tsx b/lib/site-visit/client-site-visit-wrapper.tsx
index aa466771..e2664ac3 100644
--- a/lib/site-visit/client-site-visit-wrapper.tsx
+++ b/lib/site-visit/client-site-visit-wrapper.tsx
@@ -3,7 +3,7 @@
import * as React from "react"
import { format } from "date-fns"
import { ko } from "date-fns/locale"
-import { Building2, Calendar, Users, MessageSquare, Ellipsis, Eye, Edit, Download, Paperclip } from "lucide-react"
+import { Building2, Calendar, Users, MessageSquare, Ellipsis, Eye, Edit, Download, Paperclip, X } from "lucide-react"
import { toast } from "sonner"
import { downloadFile } from "@/lib/file-download"
@@ -167,11 +167,11 @@ export function ClientSiteVisitWrapper({
case "REQUESTED":
return "요청됨"
case "SENT":
- return "발송됨"
+ return "요청접수"
case "COMPLETED":
return "완료"
case "VENDOR_SUBMITTED":
- return "협력업체 제출"
+ return "제출완료"
default:
return status
}
@@ -226,10 +226,10 @@ export function ClientSiteVisitWrapper({
<div className="text-2xl font-bold">{siteVisitRequests.length}</div>
</CardContent>
</Card>
-
+
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
- <CardTitle className="text-sm font-medium">발송됨</CardTitle>
+ <CardTitle className="text-sm font-medium">요청 접수</CardTitle>
<MessageSquare className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
@@ -238,10 +238,10 @@ export function ClientSiteVisitWrapper({
</div>
</CardContent>
</Card>
-
+
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
- <CardTitle className="text-sm font-medium">완료</CardTitle>
+ <CardTitle className="text-sm font-medium">제출 완료</CardTitle>
<Calendar className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
@@ -250,7 +250,7 @@ export function ClientSiteVisitWrapper({
</div>
</CardContent>
</Card>
-
+
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">대기중</CardTitle>
diff --git a/lib/site-visit/vendor-info-sheet.tsx b/lib/site-visit/vendor-info-sheet.tsx
index f72766fe..2a20e212 100644
--- a/lib/site-visit/vendor-info-sheet.tsx
+++ b/lib/site-visit/vendor-info-sheet.tsx
@@ -30,21 +30,21 @@ import { Upload, X, FileText } from "lucide-react"
// 협력업체 정보 입력 스키마
const vendorInfoSchema = z.object({
- // 공장 정보
- factoryName: z.string().min(1, "공장명을 입력해주세요."),
- factoryLocation: z.string().min(1, "공장위치를 입력해주세요."),
- factoryAddress: z.string().min(1, "공장주소를 입력해주세요."),
+ // 실사 장소 정보
+ factoryName: z.string().min(1, "실사 장소명을 입력해주세요."),
+ factoryLocation: z.string().min(1, "실사 지역을 입력해주세요."),
+ factoryAddress: z.string().min(1, "실사 주소를 입력해주세요."),
- // 공장 PIC 정보
- factoryPicName: z.string().min(1, "공장 담당자 이름을 입력해주세요."),
- factoryPicPhone: z.string().min(1, "공장 담당자 전화번호를 입력해주세요."),
+ // 실사 참석자 정보
+ factoryPicName: z.string().min(1, "실사 참석자 이름을 입력해주세요."),
+ factoryPicPhone: z.string().min(1, "실사 참석자 전화번호를 입력해주세요."),
factoryPicEmail: z.string().email("올바른 이메일 주소를 입력해주세요."),
- // 공장 가는 법
- factoryDirections: z.string().min(1, "공장 가는 법을 입력해주세요."),
-
- // 공장 출입절차
- accessProcedure: z.string().min(1, "공장 출입절차를 입력해주세요."),
+ // 실사 장소 이동 방법
+ factoryDirections: z.string().min(1, "실사 장소 이동 방법을 입력해주세요."),
+
+ // 실사 장소 출입절차
+ accessProcedure: z.string().min(1, "실사 장소 출입절차를 입력해주세요."),
// 첨부파일
hasAttachments: z.boolean().default(false),
@@ -176,9 +176,9 @@ export function VendorInfoSheet({
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
- {/* 공장 정보 */}
+ {/* 실사 장소 정보 */}
<div className="space-y-4">
- <h3 className="text-lg font-semibold border-b pb-2">공장 정보</h3>
+ <h3 className="text-lg font-semibold border-b pb-2">실사 장소 정보</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
@@ -186,9 +186,9 @@ export function VendorInfoSheet({
name="factoryName"
render={({ field }) => (
<FormItem>
- <FormLabel>공장명 *</FormLabel>
+ <FormLabel>실사 장소명 *</FormLabel>
<FormControl>
- <Input placeholder="공장명을 입력하세요" {...field} disabled={isPending} />
+ <Input placeholder="실사 장소명을 입력하세요 (예: 00공장)" {...field} disabled={isPending} />
</FormControl>
<FormMessage />
</FormItem>
@@ -200,7 +200,7 @@ export function VendorInfoSheet({
name="factoryLocation"
render={({ field }) => (
<FormItem>
- <FormLabel>공장위치 *</FormLabel>
+ <FormLabel>실사 지역 *</FormLabel>
<FormControl>
<Input placeholder="국가 또는 지역 (예: Finland, 부산)" {...field} disabled={isPending} />
</FormControl>
@@ -215,11 +215,11 @@ export function VendorInfoSheet({
name="factoryAddress"
render={({ field }) => (
<FormItem>
- <FormLabel>공장주소 *</FormLabel>
+ <FormLabel>실사 주소 *</FormLabel>
<FormControl>
- <Textarea
- placeholder="상세 주소를 입력하세요"
- {...field}
+ <Textarea
+ placeholder="상세 주소를 입력하세요"
+ {...field}
disabled={isPending}
className="min-h-[80px]"
/>
@@ -232,9 +232,9 @@ export function VendorInfoSheet({
</div>
</div>
- {/* 공장 PIC 정보 */}
+ {/* 실사 참석자 정보 */}
<div className="space-y-4">
- <h3 className="text-lg font-semibold border-b pb-2">공장 담당자 정보</h3>
+ <h3 className="text-lg font-semibold border-b pb-2">실사 참석자 정보</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<FormField
@@ -281,9 +281,9 @@ export function VendorInfoSheet({
</div>
</div>
- {/* 공장 정보 상세 */}
+ {/* 실사 장소 상세 */}
<div className="space-y-4">
- <h3 className="text-lg font-semibold border-b pb-2">공장 정보 상세</h3>
+ <h3 className="text-lg font-semibold border-b pb-2">실사 장소 상세</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<FormField
@@ -291,11 +291,11 @@ export function VendorInfoSheet({
name="factoryDirections"
render={({ field }) => (
<FormItem>
- <FormLabel>공장 가는 법 *</FormLabel>
+ <FormLabel>실사 장소 이동 방법 *</FormLabel>
<FormControl>
- <Textarea
- placeholder="공항에서 공장까지 가는 방법, 대중교통 정보 등을 상세히 입력하세요"
- {...field}
+ <Textarea
+ placeholder="공항에서 실사 장소까지 가는 방법, 대중교통 정보 등을 상세히 입력하세요"
+ {...field}
disabled={isPending}
className="min-h-[120px]"
/>
@@ -310,11 +310,11 @@ export function VendorInfoSheet({
name="accessProcedure"
render={({ field }) => (
<FormItem>
- <FormLabel>공장 출입절차 *</FormLabel>
+ <FormLabel>실사 장소 출입절차 *</FormLabel>
<FormControl>
- <Textarea
- placeholder="신분증 제출, 출입증 교환, 준비물 등 출입 절차를 상세히 입력하세요"
- {...field}
+ <Textarea
+ placeholder="신분증 제출, 출입증 교환, 준비물 등 출입 절차를 상세히 입력하세요"
+ {...field}
disabled={isPending}
className="min-h-[120px]"
/>
diff --git a/lib/site-visit/vendor-info-view-dialog.tsx b/lib/site-visit/vendor-info-view-dialog.tsx
index fb2b0dfe..ded5de72 100644
--- a/lib/site-visit/vendor-info-view-dialog.tsx
+++ b/lib/site-visit/vendor-info-view-dialog.tsx
@@ -92,28 +92,28 @@ function VendorDetailView({
return (
<div className="space-y-6">
- {/* 협력업체 공장 정보 */}
+ {/* 협력업체 실사 장소 정보 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Building2 className="h-5 w-5" />
- 협력업체 공장 정보
+ 협력업체 실사 장소 정보
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4">
<div>
- <h4 className="font-semibold mb-2">공장 기본 정보</h4>
+ <h4 className="font-semibold mb-2">실사 장소 기본 정보</h4>
<div className="space-y-2 text-sm">
- <div><span className="font-medium">공장명:</span> {vendorInfo.factoryName}</div>
- <div><span className="font-medium">공장위치:</span> {vendorInfo.factoryLocation}</div>
- <div><span className="font-medium">공장주소:</span> {vendorInfo.factoryAddress}</div>
+ <div><span className="font-medium">실사 장소명:</span> {vendorInfo.factoryName}</div>
+ <div><span className="font-medium">실사 지역:</span> {vendorInfo.factoryLocation}</div>
+ <div><span className="font-medium">실사 주소:</span> {vendorInfo.factoryAddress}</div>
</div>
</div>
<div>
- <h4 className="font-semibold mb-2">공장 담당자 정보</h4>
+ <h4 className="font-semibold mb-2">실사 참석자 정보</h4>
<div className="space-y-2 text-sm">
<div className="flex items-center gap-2">
<User className="h-4 w-4" />
@@ -134,16 +134,16 @@ function VendorDetailView({
<div className="space-y-4">
{vendorInfo.factoryDirections && (
<div>
- <h4 className="font-semibold mb-2">공장 가는 법</h4>
+ <h4 className="font-semibold mb-2">실사 장소 이동 방법</h4>
<div className="bg-muted p-3 rounded-md">
<p className="text-sm whitespace-pre-wrap">{vendorInfo.factoryDirections}</p>
</div>
</div>
)}
-
+
{vendorInfo.accessProcedure && (
<div>
- <h4 className="font-semibold mb-2">공장 출입절차</h4>
+ <h4 className="font-semibold mb-2">실사 장소 출입절차</h4>
<div className="bg-muted p-3 rounded-md">
<p className="text-sm whitespace-pre-wrap">{vendorInfo.accessProcedure}</p>
</div>
@@ -411,7 +411,7 @@ export function VendorInfoViewDialog({
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-sm text-muted-foreground">
<div>
- <span className="font-medium">공장명:</span>{" "}
+ <span className="font-medium">실사 장소명:</span>{" "}
{request.vendorInfo?.factoryName || "미입력"}
</div>
<div>
diff --git a/lib/vendors/rfq-history-table/rfq-history-table.tsx b/lib/vendors/rfq-history-table/rfq-history-table.tsx
index 11a4bf9d..c618cbc9 100644
--- a/lib/vendors/rfq-history-table/rfq-history-table.tsx
+++ b/lib/vendors/rfq-history-table/rfq-history-table.tsx
@@ -98,7 +98,7 @@ export function VendorRfqHistoryTable({ promises, lng, vendorId }: RfqHistoryTab
label: "견적상태",
options: [
{ label: "ITB 발송", value: "ITB 발송" },
- { label: "Short List 확정", value: "Short List 확정" },
+ { label: "TBE 요청", value: "TBE 요청" },
{ label: "최종업체선정", value: "최종업체선정" },
{ label: "견적접수", value: "견적접수" },
{ label: "견적평가중", value: "견적평가중" },
@@ -143,7 +143,7 @@ export function VendorRfqHistoryTable({ promises, lng, vendorId }: RfqHistoryTab
type: "multi-select",
options: [
{ label: "ITB 발송", value: "ITB 발송" },
- { label: "Short List 확정", value: "Short List 확정" },
+ { label: "TBE 요청", value: "TBE 요청" },
{ label: "최종업체선정", value: "최종업체선정" },
{ label: "견적접수", value: "견적접수" },
{ label: "견적평가중", value: "견적평가중" },
diff --git a/lib/vendors/table/request-pq-dialog.tsx b/lib/vendors/table/request-pq-dialog.tsx
index 07057dbe..83eab201 100644
--- a/lib/vendors/table/request-pq-dialog.tsx
+++ b/lib/vendors/table/request-pq-dialog.tsx
@@ -40,7 +40,7 @@ import { Badge } from "@/components/ui/badge"
import { Progress } from "@/components/ui/progress"
import { Vendor } from "@/db/schema/vendors"
import { requestBasicContractInfo, requestPQVendors, sendBasicContractEmail } from "../service"
-import { getProjectsWithPQList } from "@/lib/pq/service"
+import { getProjectsWithPQList, getNonInspectionPQLists } from "@/lib/pq/service"
import type { Project } from "@/lib/pq/service"
import { useSession } from "next-auth/react"
import { DatePicker } from "@/components/ui/date-picker"
@@ -84,12 +84,16 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro
const [agreements, setAgreements] = React.useState<Record<string, boolean>>({})
const [extraNote, setExtraNote] = React.useState<string>("")
const [pqItems, setPqItems] = React.useState<MaterialSearchItem[]>([])
-
+
// PQ 품목 선택 관련 상태는 MaterialGroupSelectorDialogMulti에서 관리됨
const [isLoadingProjects, setIsLoadingProjects] = React.useState(false)
const [basicContractTemplates, setBasicContractTemplates] = React.useState<BasicContractTemplate[]>([])
const [selectedTemplateIds, setSelectedTemplateIds] = React.useState<number[]>([])
const [isLoadingTemplates, setIsLoadingTemplates] = React.useState(false)
+
+ // 미실사 PQ 관련 상태
+ const [activeNonInspectionPQList, setActiveNonInspectionPQList] = React.useState<any>(null)
+ const [isLoadingNonInspectionPQ, setIsLoadingNonInspectionPQ] = React.useState(false)
// 비밀유지 계약서 첨부파일 관련 상태
const [ndaAttachments, setNdaAttachments] = React.useState<File[]>([])
@@ -110,6 +114,19 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro
setIsLoadingProjects(true)
getProjectsWithPQList().then(setProjects).catch(() => toast.error("프로젝트 로딩 실패"))
.finally(() => setIsLoadingProjects(false))
+ } else if (type === "NON_INSPECTION") {
+ setIsLoadingNonInspectionPQ(true)
+ // 활성화된 미실사 PQ 리스트 조회
+ getNonInspectionPQLists().then(result => {
+ if (result.success) {
+ setActiveNonInspectionPQList(result.data)
+ } else {
+ setActiveNonInspectionPQList(null)
+ }
+ }).catch(() => {
+ toast.error("미실사 PQ 리스트 로딩 실패")
+ setActiveNonInspectionPQList(null)
+ }).finally(() => setIsLoadingNonInspectionPQ(false))
}
}, [type])
@@ -221,6 +238,8 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro
setShowProgress(false)
setPqHistory({})
setIsLoadingHistory(false)
+ setActiveNonInspectionPQList(null)
+ setIsLoadingNonInspectionPQ(false)
}
}, [props.open])
@@ -261,6 +280,7 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro
const onApprove = () => {
if (!type) return toast.error("PQ 유형을 선택하세요.")
if (type === "PROJECT" && !selectedProjectId) return toast.error("프로젝트를 선택하세요.")
+ if (type === "NON_INSPECTION" && !activeNonInspectionPQList) return toast.error("활성화된 미실사 PQ 리스트가 없습니다.")
if (!dueDate) return toast.error("마감일을 선택하세요.")
if (pqItems.length === 0) return toast.error("PQ 대상 품목을 선택하세요.")
if (!session?.user?.id) return toast.error("인증 실패")
@@ -639,7 +659,7 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro
})
if (emailResult.success) {
- toast.success(`${templateIds.length}개 템플릿에 대한 기본계약서가 생성되었고, ${vendorIds.length}개 협력업체에 이메일이 발송되었습니다`)
+ toast.success(`${vendorIds.length}개 협력업체에 이메일이 발송되었습니다`)
} else {
toast.warning(`계약서는 생성되었으나 일부 이메일 발송 실패: ${emailResult.error}`)
}
@@ -793,6 +813,31 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro
</div>
)}
+ {type === "NON_INSPECTION" && (
+ <div className="space-y-2">
+ <Label>활성화된 미실사 PQ 리스트</Label>
+ {isLoadingNonInspectionPQ ? (
+ <div className="text-sm text-muted-foreground">로딩 중...</div>
+ ) : activeNonInspectionPQList ? (
+ <div className="p-3 border rounded-md bg-muted/30">
+ <div className="flex items-center gap-2">
+ <Badge variant="secondary">미실사 PQ</Badge>
+ <span className="font-medium">{activeNonInspectionPQList.name}</span>
+ </div>
+ <div className="text-sm text-muted-foreground mt-1">
+ 활성화된 미실사 PQ 리스트를 기준으로 요청합니다.
+ </div>
+ </div>
+ ) : (
+ <div className="p-3 border rounded-md bg-destructive/10 border-destructive/20">
+ <div className="text-sm text-destructive">
+ 활성화된 미실사 PQ 리스트가 없습니다. 먼저 PQ 관리에서 미실사 PQ 리스트를 생성하고 활성화해주세요.
+ </div>
+ </div>
+ )}
+ </div>
+ )}
+
{/* 마감일 입력 */}
<div className="space-y-2">
<Label htmlFor="dueDate">PQ 제출 마감일</Label>
@@ -875,7 +920,7 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro
)}
</div>
)}
- {selectedTemplateIds.length > 0 && (
+ {/* {selectedTemplateIds.length > 0 && (
<div className="text-xs text-muted-foreground">
{selectedTemplateIds.length}개 템플릿이 선택되었습니다.
{vendors.length > 0 && vendors.every(v => v.country !== 'KR') &&
@@ -883,7 +928,7 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro
{vendors.length > 0 && vendors.every(v => v.country === 'KR') &&
" (내자 벤더 - 자동 선택됨)"}
</div>
- )}
+ )} */}
</div>
{/* 비밀유지 계약서 첨부파일 */}
@@ -1013,9 +1058,9 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro
<DialogClose asChild>
<Button variant="outline" disabled={isApprovePending}>취소</Button>
</DialogClose>
- <Button
- onClick={onApprove}
- disabled={isApprovePending || !type || (type === "PROJECT" && !selectedProjectId)}
+ <Button
+ onClick={onApprove}
+ disabled={isApprovePending || !type || (type === "PROJECT" && !selectedProjectId) || (type === "NON_INSPECTION" && !activeNonInspectionPQList)}
>
{isApprovePending && <Loader className="mr-2 size-4 animate-spin" />}
요청하기
@@ -1067,9 +1112,9 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro
<DrawerClose asChild>
<Button variant="outline" disabled={isApprovePending} className="flex-1">취소</Button>
</DrawerClose>
- <Button
- onClick={onApprove}
- disabled={isApprovePending || !type || (type === "PROJECT" && !selectedProjectId)}
+ <Button
+ onClick={onApprove}
+ disabled={isApprovePending || !type || (type === "PROJECT" && !selectedProjectId) || (type === "NON_INSPECTION" && !activeNonInspectionPQList)}
className="flex-1"
>
{isApprovePending && <Loader className="mr-2 size-4 animate-spin" />}