From ee77f36b1ceece1236d45fba102c3ea410acebc1 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 11 Sep 2025 11:20:42 +0000 Subject: (최겸) 구매 계약 메인 및 상세 기능 개발(템플릿 연동 및 계약 전달 개발 필요) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../evcp/(evcp)/general-contracts/[id]/page.tsx | 70 ++ app/[lng]/evcp/(evcp)/general-contracts/page.tsx | 113 ++ config/menuConfig.ts | 2 +- db/schema/generalContract.ts | 209 ++++ db/schema/index.ts | 1 + .../detail/general-contract-basic-info.tsx | 1104 ++++++++++++++++++ .../general-contract-communication-channel.tsx | 362 ++++++ .../detail/general-contract-detail.tsx | 152 +++ .../detail/general-contract-documents.tsx | 372 ++++++ .../detail/general-contract-field-service-rate.tsx | 288 +++++ .../detail/general-contract-info-header.tsx | 211 ++++ .../detail/general-contract-items-table.tsx | 549 +++++++++ .../detail/general-contract-location.tsx | 480 ++++++++ .../detail/general-contract-offset-details.tsx | 314 +++++ .../detail/subcontract-checklist.tsx | 577 +++++++++ .../main/create-general-contract-dialog.tsx | 479 ++++++++ .../main/general-contract-update-sheet.tsx | 420 +++++++ .../main/general-contracts-table-columns.tsx | 547 +++++++++ .../general-contracts-table-toolbar-actions.tsx | 124 ++ .../main/general-contracts-table.tsx | 220 ++++ lib/general-contracts/service.ts | 1226 ++++++++++++++++++++ lib/general-contracts/types.ts | 138 +++ lib/general-contracts/validation.ts | 83 ++ 23 files changed, 8040 insertions(+), 1 deletion(-) create mode 100644 app/[lng]/evcp/(evcp)/general-contracts/[id]/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/general-contracts/page.tsx create mode 100644 db/schema/generalContract.ts create mode 100644 lib/general-contracts/detail/general-contract-basic-info.tsx create mode 100644 lib/general-contracts/detail/general-contract-communication-channel.tsx create mode 100644 lib/general-contracts/detail/general-contract-detail.tsx create mode 100644 lib/general-contracts/detail/general-contract-documents.tsx create mode 100644 lib/general-contracts/detail/general-contract-field-service-rate.tsx create mode 100644 lib/general-contracts/detail/general-contract-info-header.tsx create mode 100644 lib/general-contracts/detail/general-contract-items-table.tsx create mode 100644 lib/general-contracts/detail/general-contract-location.tsx create mode 100644 lib/general-contracts/detail/general-contract-offset-details.tsx create mode 100644 lib/general-contracts/detail/subcontract-checklist.tsx create mode 100644 lib/general-contracts/main/create-general-contract-dialog.tsx create mode 100644 lib/general-contracts/main/general-contract-update-sheet.tsx create mode 100644 lib/general-contracts/main/general-contracts-table-columns.tsx create mode 100644 lib/general-contracts/main/general-contracts-table-toolbar-actions.tsx create mode 100644 lib/general-contracts/main/general-contracts-table.tsx create mode 100644 lib/general-contracts/service.ts create mode 100644 lib/general-contracts/types.ts create mode 100644 lib/general-contracts/validation.ts diff --git a/app/[lng]/evcp/(evcp)/general-contracts/[id]/page.tsx b/app/[lng]/evcp/(evcp)/general-contracts/[id]/page.tsx new file mode 100644 index 00000000..632f7145 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/general-contracts/[id]/page.tsx @@ -0,0 +1,70 @@ +import { redirect, notFound } from 'next/navigation' +import { Shell } from "@/components/shell" +import { getContractById } from "@/lib/general-contracts/service" +import ContractDetailPage from "@/lib/general-contracts/detail/general-contract-detail" + +interface PageProps { + params: Promise<{ lng: string; id: string }> +} + +export async function generateMetadata({ params }: PageProps) { + const { id } = await params + + // ID 유효성 검사 + if (!id || isNaN(parseInt(id))) { + return { + title: "계약 상세", + description: "일반계약 상세 정보", + } + } + + try { + const contractId = parseInt(id) + const contract = await getContractById(contractId) + + if (!contract) { + return { + title: "계약을 찾을 수 없습니다", + description: "요청하신 계약을 찾을 수 없습니다.", + } + } + + return { + title: `계약 상세 - ${contract.name}`, + description: `계약번호: ${contract.contractNumber} (Rev.${contract.revision})`, + } + } catch { + return { + title: "계약 상세", + description: "일반계약 상세 정보", + } + } +} + +export default async function Page({ params }: PageProps) { + const { lng, id } = await params + + // ID 유효성 검사 + const contractId = parseInt(id) + if (isNaN(contractId) || contractId <= 0) { + notFound() + } + + try { + // 계약 정보 사전 로드 + const contract = await getContractById(contractId) + + if (!contract) { + notFound() + } + + return ( + + + + ) + } catch (error) { + console.error("Error loading contract:", error) + notFound() + } +} diff --git a/app/[lng]/evcp/(evcp)/general-contracts/page.tsx b/app/[lng]/evcp/(evcp)/general-contracts/page.tsx new file mode 100644 index 00000000..47677bb3 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/general-contracts/page.tsx @@ -0,0 +1,113 @@ +import { Suspense } from "react" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { + getGeneralContracts, + getGeneralContractStatusCounts, + getGeneralContractCategoryCounts, + getVendors +} from "@/lib/general-contracts/service" +import { GeneralContractsTable } from "@/lib/general-contracts/main/general-contracts-table" +import { getValidFilters } from "@/lib/data-table" +import { type SearchParams } from "@/types/table" +import { InformationButton } from "@/components/information/information-button" + +export const metadata = { + title: "일반계약 관리", + description: "일반계약을 생성하고 관리할 수 있습니다.", +} + +interface IndexPageProps { + searchParams: Promise +} + +// searchParams 파싱을 위한 기본 파서 함수 +function parseSearchParams(searchParams: SearchParams) { + const page = Number(searchParams.page) || 1 + const perPage = Number(searchParams.per_page) || 10 + const sort = searchParams.sort + ? Array.isArray(searchParams.sort) + ? searchParams.sort.map((s: string) => { + const [id, desc] = s.split('.') + return { id, desc: desc === 'desc' } + }) + : [{ id: searchParams.sort.split('.')[0], desc: searchParams.sort.split('.')[1] === 'desc' }] + : [{ id: "registeredAt", desc: true }] + + return { + page, + perPage, + sort, + filters: [], + contractNumber: searchParams.contractNumber as string, + name: searchParams.name as string, + status: searchParams.status as string, + category: searchParams.category as string, + type: searchParams.type as string, + vendorId: searchParams.vendorId ? Number(searchParams.vendorId) : undefined, + createdAtFrom: searchParams.createdAtFrom as string, + createdAtTo: searchParams.createdAtTo as string, + signedAtFrom: searchParams.signedAtFrom as string, + signedAtTo: searchParams.signedAtTo as string, + search: searchParams.search as string, + } +} + +export default async function GeneralContractsPage(props: IndexPageProps) { + // ✅ searchParams 파싱 + const searchParams = await props.searchParams + const search = parseSearchParams(searchParams) + + const validFilters = getValidFilters(search.filters) + + // ✅ 모든 데이터를 병렬로 로드 + const promises = Promise.all([ + getGeneralContracts({ + ...search, + filters: validFilters, + }), + getGeneralContractStatusCounts(), + getGeneralContractCategoryCounts(), + getVendors(), + ]) + + return ( + + {/* ═══════════════════════════════════════════════════════════════ */} + {/* 페이지 헤더 */} + {/* ═══════════════════════════════════════════════════════════════ */} +
+
+
+
+

+ 일반계약 관리 +

+ +
+

+ 일반계약을 생성하고 관리할 수 있습니다. 계약 상세정보, 품목정보, 납품확인서 등을 관리할 수 있습니다. +

+
+
+
+ + {/* ═══════════════════════════════════════════════════════════════ */} + {/* 메인 테이블 */} + {/* ═══════════════════════════════════════════════════════════════ */} + + } + > + + +
+ ) +} diff --git a/config/menuConfig.ts b/config/menuConfig.ts index c5f5075a..389bd97c 100644 --- a/config/menuConfig.ts +++ b/config/menuConfig.ts @@ -331,7 +331,7 @@ export const mainNav: MenuSection[] = [ }, { titleKey: "menu.procurement.general_contract", - href: "/evcp/contract", + href: "/evcp/general-contracts", descriptionKey: "menu.procurement.general_contract_desc", groupKey: "groups.order_management" }, diff --git a/db/schema/generalContract.ts b/db/schema/generalContract.ts new file mode 100644 index 00000000..bb671494 --- /dev/null +++ b/db/schema/generalContract.ts @@ -0,0 +1,209 @@ +import { pgTable, serial, varchar, integer, date, timestamp, decimal, text, jsonb, boolean } from 'drizzle-orm/pg-core'; +import { relations } from 'drizzle-orm'; +import { users } from './users'; // users 테이블이 존재한다고 가정 +import { vendors } from './vendors'; // vendors 테이블이 존재한다고 가정 + +export const generalContractTemplates = pgTable('general_contract_templates', { + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), + contractTemplateType: varchar('contract_template_type', { length: 2 }).notNull(), + contractTemplateName: text('contract_template_name').notNull(), + revision: integer('revision').notNull().default(1), + status: varchar('status', { length: 20 }).notNull().default('ACTIVE'), + fileName: varchar("file_name", { length: 255 }), + filePath: varchar("file_path", { length: 1024 }), + legalReviewRequired: boolean('legal_review_required').notNull().default(false), + createdAt: timestamp('created_at').defaultNow(), + createdBy: integer('created_by').references(() => users.id), + updatedAt: timestamp('updated_at').defaultNow(), + updatedBy: integer('updated_by').references(() => users.id), + disposedAt: timestamp('disposed_at'), + restoredAt: timestamp('restored_at'), +}); + + +export const generalContracts = pgTable('general_contracts', { + // ═══════════════════════════════════════════════════════════════ + // 기본 식별 정보 + // ═══════════════════════════════════════════════════════════════ + id: serial('id').primaryKey(), // 계약 고유 ID + contractNumber: varchar('contract_number', { length: 255 }).notNull().unique(), // 계약번호 (자동 채번) + revision: integer('revision').notNull().default(0), // 계약 개정 번호 + + // ═══════════════════════════════════════════════════════════════ + // 계약 분류 및 상태 + // ═══════════════════════════════════════════════════════════════ + status: varchar('status', { length: 50 }).notNull(), // 계약 상태 (Draft, Complete the Contract, Contract Delete 등) + category: varchar('category', { length: 50 }).notNull(), // 계약구분 (단가계약, 일반계약, 매각계약) + type: varchar('type', { length: 50 }).notNull(), // 계약종류 (UP, LE, IL, AL 등) + executionMethod: varchar('execution_method', { length: 50 }).notNull(), // 체결방식 (단가계약, 일반계약 등) + name: varchar('name', { length: 255 }).notNull(), // 계약명 + selectionMethod: varchar('selection_method', { length: 50 }), // 업체선정방법 + + // ═══════════════════════════════════════════════════════════════ + // 협력업체 및 계약 기간 + // ═══════════════════════════════════════════════════════════════ + vendorId: integer('vendor_id').notNull().references(() => vendors.id), // 협력업체 ID + startDate: date('start_date').notNull(), // 계약 시작일 + endDate: date('end_date').notNull(), // 계약 종료일 + validityEndDate: date('validity_end_date').notNull(), // 계약 유효기간 종료일 + + // ═══════════════════════════════════════════════════════════════ + // 연계 정보 + // ═══════════════════════════════════════════════════════════════ + linkedRfqOrItb: varchar('linked_rfq_or_itb', { length: 255 }), // 연계 RFQ/ITB 번호 + linkedPoNumber: varchar('linked_po_number', { length: 255 }), // 연계 PO 번호 + linkedBidNumber: varchar('linked_bid_number', { length: 255 }), // 연계 BID 번호 + + // ═══════════════════════════════════════════════════════════════ + // 계약 범위 및 사양 + // ═══════════════════════════════════════════════════════════════ + contractScope: varchar('contract_scope', { length: 50 }), // 계약 범위 + warrantyPeriod: jsonb('warranty_period').default({}), // 품질/하자 보증기간 + specificationType: varchar('specification_type', { length: 50 }), // 사양 유형 + specificationManualText: text('specification_manual_text'), // 사양 매뉴얼 텍스트 + + // ═══════════════════════════════════════════════════════════════ + // 금액 정보 + // ═══════════════════════════════════════════════════════════════ + unitPriceType: varchar('unit_price_type', { length: 50 }), // 단가 유형 + contractAmount: decimal('contract_amount', { precision: 15, scale: 2 }), // 계약 금액 + currency: varchar('currency', { length: 10 }), // 통화 + totalAmount: decimal('total_amount', { precision: 15, scale: 2 }), // 총 금액 + availableBudget: decimal('available_budget', { precision: 15, scale: 2 }), // 가용 예산 + + // ═══════════════════════════════════════════════════════════════ + // 지급조건 (Payment Condition) + // ═══════════════════════════════════════════════════════════════ + paymentBeforeDelivery: jsonb('payment_before_delivery').default({}), // 납품 전 지급조건 + paymentDelivery: varchar('payment_delivery', { length: 50 }), // 납품 지급조건 + paymentAfterDelivery: jsonb('payment_after_delivery').default({}), // 납품 외 지급조건 + contractCurrency: varchar('contract_currency', { length: 10 }), // 계약통화 + paymentTerm: varchar('payment_term', { length: 50 }), // 지불조건 (L003 등) + taxType: varchar('tax_type', { length: 50 }), // 세금 (VV 등) + liquidatedDamages: decimal('liquidated_damages', { precision: 15, scale: 2 }), // 지체상금 + liquidatedDamagesPercent: decimal('liquidated_damages_percent', { precision: 5, scale: 2 }), // 지체상금 비율 + claimAmount: jsonb('claim_amount').default({}), // 클레임금액 + + // ═══════════════════════════════════════════════════════════════ + // 인도조건 (Delivery Condition) + // ═══════════════════════════════════════════════════════════════ + deliveryType: varchar('delivery_type', { length: 50 }), // 납기종류 (단일납기, 분할납기, 구간납기) + deliveryTerm: varchar('delivery_term', { length: 50 }), // 인도조건 (FOB 등) + shippingLocation: varchar('shipping_location', { length: 100 }), // 선적지 + dischargeLocation: varchar('discharge_location', { length: 100 }), // 하역지 + contractDeliveryDate: date('contract_delivery_date'), // 계약납기일 + + // ═══════════════════════════════════════════════════════════════ + // 추가조건 (Additional Condition) + // ═══════════════════════════════════════════════════════════════ + contractEstablishmentConditions: jsonb('contract_establishment_conditions').default({}), // 계약성립조건 + interlockingSystem: varchar('interlocking_system', { length: 10 }), // 연동제적용 (Y/N) + mandatoryDocuments: jsonb('mandatory_documents').default({}), // 필수문서동의 + contractTerminationConditions: jsonb('contract_termination_conditions').default({}), // 계약해지조건 + + // ═══════════════════════════════════════════════════════════════ + // 기타 계약 조건 및 약관 (JSON 형태) + // ═══════════════════════════════════════════════════════════════ + terms: jsonb('terms').default({}), // 계약 조건 + complianceChecklist: jsonb('compliance_checklist').default({}), // 컴플라이언스 체크리스트 + communicationChannels: jsonb('communication_channels').default({}), // 커뮤니케이션 채널 + locations: jsonb('locations').default({}), // 위치 정보 + fieldServiceRates: jsonb('field_service_rates').default({}), // 현장 서비스 요금 + offsetDetails: jsonb('offset_details').default({}), // 오프셋 세부사항 + + // ═══════════════════════════════════════════════════════════════ + // 시스템 관리 정보 + // ═══════════════════════════════════════════════════════════════ + registeredById: integer('registered_by_id').notNull().references(() => users.id), // 등록자 ID + registeredAt: timestamp('registered_at').notNull().defaultNow(), // 등록일시 + signedAt: timestamp('signed_at'), // 계약 체결일시 + lastUpdatedById: integer('last_updated_by_id').notNull().references(() => users.id), // 최종 수정자 ID + lastUpdatedAt: timestamp('last_updated_at').notNull().defaultNow(), // 최종 수정일시 + notes: text('notes'), // 비고 +}); + +export const generalContractItems = pgTable('general_contract_items', { + // ═══════════════════════════════════════════════════════════════ + // 기본 식별 정보 + // ═══════════════════════════════════════════════════════════════ + id: serial('id').primaryKey(), // 품목 고유 ID + contractId: integer('contract_id').notNull().references(() => generalContracts.id), // 계약 ID (외래키) + + // ═══════════════════════════════════════════════════════════════ + // 품목 기본 정보 + // ═══════════════════════════════════════════════════════════════ + project: varchar('project', { length: 255 }), // 프로젝트 명 + itemCode: varchar('item_code', { length: 100 }), // 품목코드 (PKG No.) + itemInfo: varchar('item_info', { length: 500 }), // Item 정보 (자재그룹 / 자재코드) + specification: varchar('specification', { length: 500 }), // 규격 + + // ═══════════════════════════════════════════════════════════════ + // 수량 및 단가 정보 + // ═══════════════════════════════════════════════════════════════ + quantity: decimal('quantity', { precision: 15, scale: 3 }), // 수량 + quantityUnit: varchar('quantity_unit', { length: 50 }), // 수량단위 + contractDeliveryDate: date('contract_delivery_date'), // 계약납기일 + contractUnitPrice: decimal('contract_unit_price', { precision: 15, scale: 2 }), // 계약단가 + contractAmount: decimal('contract_amount', { precision: 15, scale: 2 }), // 계약금액 + contractCurrency: varchar('contract_currency', { length: 10 }), // 계약통화 + + // ═══════════════════════════════════════════════════════════════ + // 시스템 관리 정보 + // ═══════════════════════════════════════════════════════════════ + createdAt: timestamp('created_at').notNull().defaultNow(), // 생성일시 + updatedAt: timestamp('updated_at').notNull().defaultNow(), // 수정일시 +}); + +export const generalContractAttachments = pgTable('general_contract_attachments', { + id: serial('id').primaryKey(), + contractId: integer('contract_id').notNull().references(() => generalContracts.id), + documentName: varchar('document_name', { length: 255 }).notNull(), // '사양 및 공급범위', '단가파일', '계약서 서명본' 등 + fileName: varchar('file_name', { length: 255 }).notNull(), // 실제 파일명 + filePath: varchar('file_path', { length: 512 }).notNull(), // 파일 저장 경로 (S3 URL 등) + shiComment: text('shi_comment'), + vendorComment: text('vendor_comment'), + legalReview: boolean('legal_review').notNull().default(false), // 법무 검토 여부 + uploadedAt: timestamp('uploaded_at').notNull().defaultNow(), + uploadedById: integer('uploaded_by_id').notNull().references(() => users.id), +}); + +export const generalContractAttachmentsRelations = relations(generalContractAttachments, ({ one }) => ({ + contract: one(generalContracts, { + fields: [generalContractAttachments.contractId], + references: [generalContracts.id], + }), +})); +export const generalContractItemsRelations = relations(generalContractItems, ({ one }) => ({ + contract: one(generalContracts, { + fields: [generalContractItems.contractId], + references: [generalContracts.id], + }), +})); +export const generalContractsRelations = relations(generalContracts, ({ one, many }) => ({ + manager: one(users, { + fields: [generalContracts.registeredById], + references: [users.id], + relationName: 'contract_manager', + }), + lastUpdatedBy: one(users, { + fields: [generalContracts.lastUpdatedById], + references: [users.id], + relationName: 'contract_last_updated_by', + }), + vendor: one(vendors, { + fields: [generalContracts.vendorId], + references: [vendors.id], + }), + items: many(generalContractItems), + attachments: many(generalContractAttachments), + })); + + // TypeScript 타입 정의 +export type GeneralContractTemplate = typeof generalContractTemplates.$inferSelect; +export type NewGeneralContractTemplate = typeof generalContractTemplates.$inferInsert; +export type GeneralContract = typeof generalContracts.$inferSelect; +export type NewGeneralContract = typeof generalContracts.$inferInsert; +export type GeneralContractItem = typeof generalContractItems.$inferSelect; +export type NewGeneralContractItem = typeof generalContractItems.$inferInsert; +export type GeneralContractAttachment = typeof generalContractAttachments.$inferSelect; +export type NewGeneralContractAttachment = typeof generalContractAttachments.$inferInsert; \ No newline at end of file diff --git a/db/schema/index.ts b/db/schema/index.ts index bcabac1b..1c2d5998 100644 --- a/db/schema/index.ts +++ b/db/schema/index.ts @@ -38,6 +38,7 @@ export * from './vendorRegistrations'; export * from './compliance'; export * from './rfqLast'; export * from './rfqVendor'; +export * from './generalContract'; // 부서별 도메인 할당 관리 export * from './departmentDomainAssignments'; diff --git a/lib/general-contracts/detail/general-contract-basic-info.tsx b/lib/general-contracts/detail/general-contract-basic-info.tsx new file mode 100644 index 00000000..fd8983f6 --- /dev/null +++ b/lib/general-contracts/detail/general-contract-basic-info.tsx @@ -0,0 +1,1104 @@ +'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' + +interface ContractBasicInfoProps { + contractId: number +} + +export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) { + const session = useSession() + const [isLoading, setIsLoading] = useState(false) + const [contract, setContract] = useState(null) + const userId = session.data?.user?.id ? Number(session.data.user.id) : null + + // 독립적인 상태 관리 + const [paymentDeliveryPercent, setPaymentDeliveryPercent] = useState('') + + 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, + currency: 'KRW', + linkedPoNumber: '', + linkedBidNumber: '', + notes: '', + // 개별 JSON 필드들 (스키마에 맞게) + paymentBeforeDelivery: {} as any, + paymentDelivery: '', // varchar 타입 + paymentAfterDelivery: {} as any, + paymentTerm: '', + taxType: '', + liquidatedDamages: false, + 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>({}) + + // 계약 데이터 로드 + 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 as number | 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: contractData?.liquidatedDamages || false, + 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]) + 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 ( + + + 계약 기본 정보 + + + + + 기본 정보 + 지급/인도 조건 + 추가 조건 + 계약첨부문서 + + + {/* 기본 정보 탭 */} + + + {/* 보증기간 및 단가유형 */} + + 보증기간 및 단가유형 + + + {/* 3그리드: 보증기간, 사양, 단가 */} +
+ {/* 보증기간 */} +
+ +
+
+ setFormData(prev => ({ + ...prev, + warrantyPeriod: { + ...prev.warrantyPeriod, + 납품후: { + ...prev.warrantyPeriod.납품후, + enabled: e.target.checked + } + } + }))} + className="rounded" + /> + +
+ {formData.warrantyPeriod.납품후?.enabled && ( +
+ setFormData(prev => ({ + ...prev, + warrantyPeriod: { + ...prev.warrantyPeriod, + 납품후: { + ...prev.warrantyPeriod.납품후, + period: parseInt(e.target.value) || 0 + } + } + }))} + className="w-20 h-8 text-sm" + /> + 개월, 최대 + setFormData(prev => ({ + ...prev, + warrantyPeriod: { + ...prev.warrantyPeriod, + 납품후: { + ...prev.warrantyPeriod.납품후, + maxPeriod: parseInt(e.target.value) || 0 + } + } + }))} + className="w-20 h-8 text-sm" + /> + 개월 +
+ )} + +
+ setFormData(prev => ({ + ...prev, + warrantyPeriod: { + ...prev.warrantyPeriod, + 인도후: { + ...prev.warrantyPeriod.인도후, + enabled: e.target.checked + } + } + }))} + className="rounded" + /> + +
+ {formData.warrantyPeriod.인도후?.enabled && ( +
+ setFormData(prev => ({ + ...prev, + warrantyPeriod: { + ...prev.warrantyPeriod, + 인도후: { + ...prev.warrantyPeriod.인도후, + period: parseInt(e.target.value) || 0 + } + } + }))} + className="w-20 h-8 text-sm" + /> + 개월, 최대 + setFormData(prev => ({ + ...prev, + warrantyPeriod: { + ...prev.warrantyPeriod, + 인도후: { + ...prev.warrantyPeriod.인도후, + maxPeriod: parseInt(e.target.value) || 0 + } + } + }))} + className="w-20 h-8 text-sm" + /> + 개월 +
+ )} + +
+ setFormData(prev => ({ + ...prev, + warrantyPeriod: { + ...prev.warrantyPeriod, + 작업후: { + ...prev.warrantyPeriod.작업후, + enabled: e.target.checked + } + } + }))} + className="rounded" + /> + +
+ {formData.warrantyPeriod.작업후?.enabled && ( +
+ setFormData(prev => ({ + ...prev, + warrantyPeriod: { + ...prev.warrantyPeriod, + 작업후: { + ...prev.warrantyPeriod.작업후, + period: parseInt(e.target.value) || 0 + } + } + }))} + className="w-20 h-8 text-sm" + /> + 개월, 최대 + setFormData(prev => ({ + ...prev, + warrantyPeriod: { + ...prev.warrantyPeriod, + 작업후: { + ...prev.warrantyPeriod.작업후, + maxPeriod: parseInt(e.target.value) || 0 + } + } + }))} + className="w-20 h-8 text-sm" + /> + 개월 +
+ )} + +
+ setFormData(prev => ({ + ...prev, + warrantyPeriod: { + ...prev.warrantyPeriod, + 기타: { + ...prev.warrantyPeriod.기타, + enabled: e.target.checked + } + } + }))} + className="rounded" + /> + +
+
+
+ {/* 사양 */} +
+ + + {errors.specificationType && ( +

사양은 필수값입니다.

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