From ba8cd44a0ed2c613a5f2cee06bfc9bd0f61f21c7 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 7 Nov 2025 08:39:04 +0000 Subject: (최겸) 입찰/견적 수정사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../evcp/(evcp)/(procurement)/bid-failure/page.tsx | 39 + .../evcp/(evcp)/(procurement)/bid-receive/page.tsx | 39 + .../(evcp)/(procurement)/bid-selection/page.tsx | 39 + .../(evcp)/(procurement)/bid/[id]/bidding-tabs.tsx | 77 + .../(procurement)/bid/[id]/companies/page.tsx | 75 + .../(evcp)/(procurement)/bid/[id]/detail/page.tsx | 51 - .../(evcp)/(procurement)/bid/[id]/info/page.tsx | 75 + .../(evcp)/(procurement)/bid/[id]/items/page.tsx | 76 + .../evcp/(evcp)/(procurement)/bid/[id]/layout.tsx | 92 - .../evcp/(evcp)/(procurement)/bid/[id]/page.tsx | 12 - .../(procurement)/bid/[id]/pre-quote/page.tsx | 56 - .../(procurement)/bid/[id]/schedule/page.tsx | 73 + app/[lng]/evcp/(evcp)/(procurement)/bid/page.tsx | 78 +- .../(evcp)/(procurement)/bidding-notice/page.tsx | 28 +- app/[lng]/partners/(partners)/bid/[id]/page.tsx | 2 + .../(partners)/bid/[id]/pre-quote/page.tsx | 97 - app/[lng]/partners/(partners)/bid/page.tsx | 7 +- .../general-contract-review/[contractId]/page.tsx | 62 + .../[contractId]/vendor-contract-review-client.tsx | 423 ++ .../(partners)/general-contract-review/page.tsx | 48 + .../vendor-general-contract-review-table.tsx | 142 + components/bidding/bidding-conditions-edit.tsx | 469 --- components/bidding/bidding-info-header.tsx | 217 +- components/bidding/bidding-round-actions.tsx | 201 + .../bidding/create/bidding-create-dialog.tsx | 1281 ++++++ .../bidding/manage/bidding-basic-info-editor.tsx | 1407 +++++++ .../bidding/manage/bidding-companies-editor.tsx | 803 ++++ .../manage/bidding-detail-vendor-create-dialog.tsx | 437 ++ components/bidding/manage/bidding-items-editor.tsx | 1143 ++++++ .../bidding/manage/bidding-schedule-editor.tsx | 661 +++ .../bidding/manage/create-pre-quote-rfq-dialog.tsx | 742 ++++ components/bidding/price-adjustment-dialog.tsx | 6 +- .../selectors/cost-center/cost-center-selector.tsx | 335 ++ .../selectors/cost-center/cost-center-service.ts | 89 + .../cost-center/cost-center-single-selector.tsx | 378 ++ components/common/selectors/cost-center/index.ts | 12 + .../selectors/gl-account/gl-account-selector.tsx | 311 ++ .../selectors/gl-account/gl-account-service.ts | 79 + .../gl-account/gl-account-single-selector.tsx | 358 ++ components/common/selectors/gl-account/index.ts | 12 + components/common/selectors/wbs-code/index.ts | 12 + .../selectors/wbs-code/wbs-code-selector.tsx | 323 ++ .../common/selectors/wbs-code/wbs-code-service.ts | 92 + .../wbs-code/wbs-code-single-selector.tsx | 365 ++ db/schema/bidding.ts | 958 +++-- db/schema/generalContract.ts | 18 +- i18n/locales/en/menu.json | 7 +- i18n/locales/ko/menu.json | 6 +- lib/bidding/actions.ts | 1065 ++--- lib/bidding/bidding-notice-editor.tsx | 99 +- lib/bidding/bidding-notice-template-manager.tsx | 63 + lib/bidding/detail/bidding-actions.ts | 227 ++ lib/bidding/detail/service.ts | 160 +- .../detail/table/bidding-detail-content.tsx | 201 +- .../detail/table/bidding-detail-vendor-columns.tsx | 23 +- .../table/bidding-detail-vendor-create-dialog.tsx | 328 -- .../bidding-detail-vendor-toolbar-actions.tsx | 8 +- .../detail/table/bidding-invitation-dialog.tsx | 718 +++- lib/bidding/failure/biddings-failure-columns.tsx | 320 ++ lib/bidding/failure/biddings-failure-table.tsx | 223 ++ lib/bidding/list/bidding-detail-dialogs.tsx | 554 +-- lib/bidding/list/bidding-pr-documents-dialog.tsx | 405 ++ lib/bidding/list/biddings-table-columns.tsx | 649 ++- .../list/biddings-table-toolbar-actions.tsx | 64 +- lib/bidding/list/biddings-table.tsx | 30 +- lib/bidding/list/create-bidding-dialog.tsx | 4230 ++++++++++---------- lib/bidding/pre-quote/service.ts | 3145 ++++++++------- .../table/bidding-pre-quote-attachments-dialog.tsx | 224 -- .../pre-quote/table/bidding-pre-quote-content.tsx | 51 - .../table/bidding-pre-quote-invitation-dialog.tsx | 770 ---- .../bidding-pre-quote-item-details-dialog.tsx | 125 - .../table/bidding-pre-quote-selection-dialog.tsx | 157 - .../table/bidding-pre-quote-vendor-columns.tsx | 398 -- .../bidding-pre-quote-vendor-create-dialog.tsx | 311 -- .../table/bidding-pre-quote-vendor-edit-dialog.tsx | 200 - .../table/bidding-pre-quote-vendor-table.tsx | 257 -- .../bidding-pre-quote-vendor-toolbar-actions.tsx | 130 - lib/bidding/receive/biddings-receive-columns.tsx | 360 ++ lib/bidding/receive/biddings-receive-table.tsx | 211 + .../selection/biddings-selection-columns.tsx | 289 ++ lib/bidding/selection/biddings-selection-table.tsx | 218 + lib/bidding/service.ts | 2953 ++++++++++++-- lib/bidding/validation.ts | 170 +- .../vendor/components/pr-items-pricing-table.tsx | 27 +- lib/bidding/vendor/partners-bidding-detail.tsx | 904 ++++- .../vendor/partners-bidding-list-columns.tsx | 104 +- lib/bidding/vendor/partners-bidding-pre-quote.tsx | 1413 ------- .../general-contract-approval-request-dialog.tsx | 141 +- .../detail/general-contract-basic-info.tsx | 488 ++- .../general-contract-communication-channel.tsx | 362 -- .../detail/general-contract-detail.tsx | 81 +- .../detail/general-contract-documents.tsx | 11 +- .../detail/general-contract-field-service-rate.tsx | 288 -- .../detail/general-contract-info-header.tsx | 5 +- .../detail/general-contract-items-table.tsx | 292 +- .../detail/general-contract-location.tsx | 480 --- .../detail/general-contract-offset-details.tsx | 314 -- .../detail/general-contract-review-comments.tsx | 194 + .../general-contract-review-request-dialog.tsx | 891 +++++ .../detail/general-contract-storage-info.tsx | 249 ++ .../general-contract-subcontract-checklist.tsx | 47 +- .../detail/general-contract-yard-entry-info.tsx | 232 ++ .../main/create-general-contract-dialog.tsx | 156 +- .../main/general-contract-update-sheet.tsx | 53 +- .../main/general-contracts-table-columns.tsx | 34 +- .../main/general-contracts-table.tsx | 5 +- lib/general-contracts/service.ts | 1102 ++++- lib/general-contracts/types.ts | 8 +- .../general-contract-approval-request-dialog.tsx | 1312 ++++++ .../detail/general-contract-basic-info.tsx | 1250 ++++++ .../general-contract-communication-channel.tsx | 362 ++ .../detail/general-contract-detail.tsx | 186 + .../detail/general-contract-documents.tsx | 383 ++ .../detail/general-contract-field-service-rate.tsx | 288 ++ .../detail/general-contract-info-header.tsx | 211 + .../detail/general-contract-items-table.tsx | 602 +++ .../detail/general-contract-location.tsx | 480 +++ .../detail/general-contract-offset-details.tsx | 314 ++ .../general-contract-subcontract-checklist.tsx | 610 +++ .../main/create-general-contract-dialog.tsx | 413 ++ .../main/general-contract-update-sheet.tsx | 401 ++ .../main/general-contracts-table-columns.tsx | 571 +++ .../general-contracts-table-toolbar-actions.tsx | 124 + .../main/general-contracts-table.tsx | 217 + lib/general-contracts_old/service.ts | 1933 +++++++++ lib/general-contracts_old/types.ts | 125 + lib/general-contracts_old/validation.ts | 82 + 127 files changed, 35955 insertions(+), 13439 deletions(-) create mode 100644 app/[lng]/evcp/(evcp)/(procurement)/bid-failure/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/(procurement)/bid-receive/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/(procurement)/bid-selection/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/bidding-tabs.tsx create mode 100644 app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/companies/page.tsx delete mode 100644 app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/detail/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/info/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/items/page.tsx delete mode 100644 app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/layout.tsx delete mode 100644 app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/page.tsx delete mode 100644 app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/pre-quote/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/schedule/page.tsx delete mode 100644 app/[lng]/partners/(partners)/bid/[id]/pre-quote/page.tsx create mode 100644 app/[lng]/partners/(partners)/general-contract-review/[contractId]/page.tsx create mode 100644 app/[lng]/partners/(partners)/general-contract-review/[contractId]/vendor-contract-review-client.tsx create mode 100644 app/[lng]/partners/(partners)/general-contract-review/page.tsx create mode 100644 app/[lng]/partners/(partners)/general-contract-review/vendor-general-contract-review-table.tsx delete mode 100644 components/bidding/bidding-conditions-edit.tsx create mode 100644 components/bidding/bidding-round-actions.tsx create mode 100644 components/bidding/create/bidding-create-dialog.tsx create mode 100644 components/bidding/manage/bidding-basic-info-editor.tsx create mode 100644 components/bidding/manage/bidding-companies-editor.tsx create mode 100644 components/bidding/manage/bidding-detail-vendor-create-dialog.tsx create mode 100644 components/bidding/manage/bidding-items-editor.tsx create mode 100644 components/bidding/manage/bidding-schedule-editor.tsx create mode 100644 components/bidding/manage/create-pre-quote-rfq-dialog.tsx create mode 100644 components/common/selectors/cost-center/cost-center-selector.tsx create mode 100644 components/common/selectors/cost-center/cost-center-service.ts create mode 100644 components/common/selectors/cost-center/cost-center-single-selector.tsx create mode 100644 components/common/selectors/cost-center/index.ts create mode 100644 components/common/selectors/gl-account/gl-account-selector.tsx create mode 100644 components/common/selectors/gl-account/gl-account-service.ts create mode 100644 components/common/selectors/gl-account/gl-account-single-selector.tsx create mode 100644 components/common/selectors/gl-account/index.ts create mode 100644 components/common/selectors/wbs-code/index.ts create mode 100644 components/common/selectors/wbs-code/wbs-code-selector.tsx create mode 100644 components/common/selectors/wbs-code/wbs-code-service.ts create mode 100644 components/common/selectors/wbs-code/wbs-code-single-selector.tsx create mode 100644 lib/bidding/bidding-notice-template-manager.tsx create mode 100644 lib/bidding/detail/bidding-actions.ts delete mode 100644 lib/bidding/detail/table/bidding-detail-vendor-create-dialog.tsx create mode 100644 lib/bidding/failure/biddings-failure-columns.tsx create mode 100644 lib/bidding/failure/biddings-failure-table.tsx create mode 100644 lib/bidding/list/bidding-pr-documents-dialog.tsx delete mode 100644 lib/bidding/pre-quote/table/bidding-pre-quote-attachments-dialog.tsx delete mode 100644 lib/bidding/pre-quote/table/bidding-pre-quote-content.tsx delete mode 100644 lib/bidding/pre-quote/table/bidding-pre-quote-invitation-dialog.tsx delete mode 100644 lib/bidding/pre-quote/table/bidding-pre-quote-item-details-dialog.tsx delete mode 100644 lib/bidding/pre-quote/table/bidding-pre-quote-selection-dialog.tsx delete mode 100644 lib/bidding/pre-quote/table/bidding-pre-quote-vendor-columns.tsx delete mode 100644 lib/bidding/pre-quote/table/bidding-pre-quote-vendor-create-dialog.tsx delete mode 100644 lib/bidding/pre-quote/table/bidding-pre-quote-vendor-edit-dialog.tsx delete mode 100644 lib/bidding/pre-quote/table/bidding-pre-quote-vendor-table.tsx delete mode 100644 lib/bidding/pre-quote/table/bidding-pre-quote-vendor-toolbar-actions.tsx create mode 100644 lib/bidding/receive/biddings-receive-columns.tsx create mode 100644 lib/bidding/receive/biddings-receive-table.tsx create mode 100644 lib/bidding/selection/biddings-selection-columns.tsx create mode 100644 lib/bidding/selection/biddings-selection-table.tsx delete mode 100644 lib/bidding/vendor/partners-bidding-pre-quote.tsx delete mode 100644 lib/general-contracts/detail/general-contract-communication-channel.tsx delete mode 100644 lib/general-contracts/detail/general-contract-field-service-rate.tsx delete mode 100644 lib/general-contracts/detail/general-contract-location.tsx delete mode 100644 lib/general-contracts/detail/general-contract-offset-details.tsx create mode 100644 lib/general-contracts/detail/general-contract-review-comments.tsx create mode 100644 lib/general-contracts/detail/general-contract-review-request-dialog.tsx create mode 100644 lib/general-contracts/detail/general-contract-storage-info.tsx create mode 100644 lib/general-contracts/detail/general-contract-yard-entry-info.tsx create mode 100644 lib/general-contracts_old/detail/general-contract-approval-request-dialog.tsx create mode 100644 lib/general-contracts_old/detail/general-contract-basic-info.tsx create mode 100644 lib/general-contracts_old/detail/general-contract-communication-channel.tsx create mode 100644 lib/general-contracts_old/detail/general-contract-detail.tsx create mode 100644 lib/general-contracts_old/detail/general-contract-documents.tsx create mode 100644 lib/general-contracts_old/detail/general-contract-field-service-rate.tsx create mode 100644 lib/general-contracts_old/detail/general-contract-info-header.tsx create mode 100644 lib/general-contracts_old/detail/general-contract-items-table.tsx create mode 100644 lib/general-contracts_old/detail/general-contract-location.tsx create mode 100644 lib/general-contracts_old/detail/general-contract-offset-details.tsx create mode 100644 lib/general-contracts_old/detail/general-contract-subcontract-checklist.tsx create mode 100644 lib/general-contracts_old/main/create-general-contract-dialog.tsx create mode 100644 lib/general-contracts_old/main/general-contract-update-sheet.tsx create mode 100644 lib/general-contracts_old/main/general-contracts-table-columns.tsx create mode 100644 lib/general-contracts_old/main/general-contracts-table-toolbar-actions.tsx create mode 100644 lib/general-contracts_old/main/general-contracts-table.tsx create mode 100644 lib/general-contracts_old/service.ts create mode 100644 lib/general-contracts_old/types.ts create mode 100644 lib/general-contracts_old/validation.ts diff --git a/app/[lng]/evcp/(evcp)/(procurement)/bid-failure/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/bid-failure/page.tsx new file mode 100644 index 00000000..f460f570 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/(procurement)/bid-failure/page.tsx @@ -0,0 +1,39 @@ +import { Metadata } from 'next' +import { getBiddingsForFailure } from '@/lib/bidding/service' +import { GetBiddingsSchema, searchParamsCache } from '@/lib/bidding/validation' +import { BiddingsFailureTable } from '@/lib/bidding/failure/biddings-failure-table' + +export const metadata: Metadata = { + title: '유찰입찰', + description: '유찰된 입찰 내역을 확인하고 재입찰을 진행할 수 있습니다.', +} + +interface BiddingFailurePageProps { + searchParams: Promise> +} + +export default async function BiddingFailurePage({ + searchParams, +}: BiddingFailurePageProps) { + // URL 파라미터 검증 + const searchParamsResolved = await searchParams + const search = searchParamsCache.parse(searchParamsResolved) + + // 데이터 조회 + const biddingsPromise = getBiddingsForFailure(search) + + return ( +
+
+
+

유찰입찰

+

+ 유찰된 입찰 내역을 확인하고 재입찰을 진행할 수 있습니다. +

+
+
+ + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/(procurement)/bid-receive/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/bid-receive/page.tsx new file mode 100644 index 00000000..0d725bbf --- /dev/null +++ b/app/[lng]/evcp/(evcp)/(procurement)/bid-receive/page.tsx @@ -0,0 +1,39 @@ +import { Metadata } from 'next' +import { getBiddingsForReceive } from '@/lib/bidding/service' +import { GetBiddingsSchema, searchParamsCache } from '@/lib/bidding/validation' +import { BiddingsReceiveTable } from '@/lib/bidding/receive/biddings-receive-table' + +export const metadata: Metadata = { + title: '입찰서접수및마감', + description: '입찰서 접수 및 마감 현황을 확인하고 개찰을 진행할 수 있습니다.', +} + +interface BiddingReceivePageProps { + searchParams: Promise> +} + +export default async function BiddingReceivePage({ + searchParams, +}: BiddingReceivePageProps) { + // URL 파라미터 검증 + const searchParamsResolved = await searchParams + const search = searchParamsCache.parse(searchParamsResolved) + + // 데이터 조회 + const biddingsPromise = getBiddingsForReceive(search) + + return ( +
+
+
+

입찰서접수및마감

+

+ 입찰서 접수 현황을 확인하고 개찰을 진행할 수 있습니다. +

+
+
+ + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/(procurement)/bid-selection/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/bid-selection/page.tsx new file mode 100644 index 00000000..40b714de --- /dev/null +++ b/app/[lng]/evcp/(evcp)/(procurement)/bid-selection/page.tsx @@ -0,0 +1,39 @@ +import { Metadata } from 'next' +import { getBiddingsForSelection } from '@/lib/bidding/service' +import { GetBiddingsSchema, searchParamsCache } from '@/lib/bidding/validation' +import { BiddingsSelectionTable } from '@/lib/bidding/selection/biddings-selection-table' + +export const metadata: Metadata = { + title: '입찰선정', + description: '개찰 이후 입찰가를 확인하고 낙찰업체를 선정할 수 있습니다.', +} + +interface BiddingSelectionPageProps { + searchParams: Promise> +} + +export default async function BiddingSelectionPage({ + searchParams, +}: BiddingSelectionPageProps) { + // URL 파라미터 검증 + const searchParamsResolved = await searchParams + const search = searchParamsCache.parse(searchParamsResolved) + + // 데이터 조회 + const biddingsPromise = getBiddingsForSelection(search) + + return ( +
+
+
+

입찰선정

+

+ 개찰 이후 입찰가를 확인하고 낙찰업체를 선정할 수 있습니다. +

+
+
+ + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/bidding-tabs.tsx b/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/bidding-tabs.tsx new file mode 100644 index 00000000..0321d273 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/bidding-tabs.tsx @@ -0,0 +1,77 @@ +"use client" + +import * as React from "react" +import { usePathname, useRouter } from "next/navigation" +import { Button } from "@/components/ui/button" +import { cn } from "@/lib/utils" + +interface BiddingTabsProps { + id: string +} + +export function BiddingTabs({ id }: BiddingTabsProps) { + const pathname = usePathname() + const router = useRouter() + + const tabs = React.useMemo(() => [ + { + key: "info", + label: "입찰 기본 정보", + href: `/evcp/bid/${id}/info`, + }, + { + key: "companies", + label: "입찰 업체", + href: `/evcp/bid/${id}/companies`, + }, + { + key: "items", + label: "입찰 품목", + href: `/evcp/bid/${id}/items`, + }, + { + key: "schedule", + label: "입찰 계획", + href: `/evcp/bid/${id}/schedule`, + }, + ], [id]) + + // 현재 활성 탭 결정 + const activeTab = React.useMemo(() => { + if (!pathname) return "info" + + // pathname에서 lng 부분 제거 (예: /en/evcp/bid/10 -> /evcp/bid/10) + const normalizedPath = pathname.replace(/^\/[^/]+/, '') || pathname + + // 기본 페이지는 info로 처리 + if (normalizedPath === `/evcp/bid/${id}` || normalizedPath.endsWith(`/bid/${id}`)) { + return "info" + } + + const matchedTab = tabs.find(tab => normalizedPath.includes(`/${tab.key}`)) + return matchedTab?.key || "info" + }, [pathname, id, tabs]) + + return ( +
+ {tabs.map((tab) => { + const isActive = activeTab === tab.key + return ( + + ) + })} +
+ ) +} + diff --git a/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/companies/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/companies/page.tsx new file mode 100644 index 00000000..f1699665 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/companies/page.tsx @@ -0,0 +1,75 @@ +import { notFound } from 'next/navigation' +import { getBiddingById } from "@/lib/bidding/service" +import { Bidding } from "@/db/schema/bidding" +import { Button } from "@/components/ui/button" +import { ArrowLeft } from "lucide-react" +import Link from "next/link" +import { BiddingCompaniesEditor } from "@/components/bidding/manage/bidding-companies-editor" +import { BiddingTabs } from "../bidding-tabs" + +// 메타데이터 생성 +export async function generateMetadata({ params }: { params: Promise<{ lng: string; id: string }> }) { + const { lng, id } = await params + const parsedId = parseInt(id) + if (isNaN(parsedId)) return { title: '입찰 업체 및 담당자 관리' } + + try { + const bidding = await getBiddingById(parsedId) + return { + title: bidding ? `${bidding.title} - 입찰 업체 및 담당자 관리` : '입찰 업체 및 담당자 관리', + } + } catch { + return { title: '입찰 업체 및 담당자 관리' } + } +} + +interface PageProps { + params: Promise<{ lng: string; id: string }> +} + +export default async function BiddingCompaniesPage({ params }: PageProps) { + const { lng, id } = await params + const parsedId = parseInt(id) + + if (isNaN(parsedId)) { + notFound() + } + + const bidding: Bidding | null = await getBiddingById(parsedId) + + if (!bidding) { + notFound() + } + + return ( +
+ {/* 헤더 */} +
+
+
+

+ 입찰 업체 및 담당자 관리 +

+

+ 입찰 No. {bidding.biddingNumber ?? ""} - {bidding.title} +

+
+
+ + + +
+ + {/* 탭 네비게이션 */} +
+ +
+ + {/* 입찰 업체 및 담당자 에디터 */} + +
+ ) +} diff --git a/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/detail/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/detail/page.tsx deleted file mode 100644 index 4dc36e20..00000000 --- a/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/detail/page.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { Suspense } from 'react' -import { notFound } from 'next/navigation' -import { getBiddingDetailData } from '@/lib/bidding/detail/service' -import { BiddingDetailContent } from '@/lib/bidding/detail/table/bidding-detail-content' - -// 메타데이터 생성 -export async function generateMetadata({ params }: { params: Promise<{ id: string }> }) { - const { id } = await params - const parsedId = parseInt(id) - if (isNaN(parsedId)) return { title: '입찰 관리상세' } - - try { - const detailData = await getBiddingDetailData(parsedId) - return { - title: detailData.bidding ? `${detailData.bidding.title} - 입찰 관리상세` : '입찰 관리상세', - } - } catch { - return { title: '입찰 관리상세' } - } -} - -interface PageProps { - params: Promise<{ id: string }> -} - -export default async function Page({ params }: PageProps) { - const { id } = await params - const parsedId = parseInt(id) - - if (isNaN(parsedId)) { - notFound() - } - - // 통합 데이터 로딩 함수 사용 - const detailData = await getBiddingDetailData(parsedId) - - if (!detailData.bidding) { - notFound() - } - - return ( - 로딩 중...}> - - - ) -} diff --git a/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/info/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/info/page.tsx new file mode 100644 index 00000000..7281d206 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/info/page.tsx @@ -0,0 +1,75 @@ +import { notFound } from 'next/navigation' +import { getBiddingById } from "@/lib/bidding/service" +import { Bidding } from "@/db/schema/bidding" +import { Button } from "@/components/ui/button" +import { ArrowLeft } from "lucide-react" +import Link from "next/link" +import { BiddingBasicInfoEditor } from "@/components/bidding/manage/bidding-basic-info-editor" +import { BiddingTabs } from "../bidding-tabs" + +// 메타데이터 생성 +export async function generateMetadata({ params }: { params: Promise<{ lng: string; id: string }> }) { + const { id } = await params + const parsedId = parseInt(id) + if (isNaN(parsedId)) return { title: '입찰 기본 정보 관리' } + + try { + const bidding = await getBiddingById(parsedId) + return { + title: bidding ? `${bidding.title} - 입찰 기본 정보 관리` : '입찰 기본 정보 관리', + } + } catch { + return { title: '입찰 기본 정보 관리' } + } +} + +interface PageProps { + params: Promise<{ lng: string; id: string }> +} + +export default async function BiddingBasicInfoPage({ params }: PageProps) { + const { lng, id } = await params + const parsedId = parseInt(id) + + if (isNaN(parsedId)) { + notFound() + } + + const bidding: Bidding | null = await getBiddingById(parsedId) + + if (!bidding) { + notFound() + } + + return ( +
+ {/* 헤더 */} +
+
+
+

+ 입찰 기본 정보 관리 +

+

+ 입찰 No. {bidding.biddingNumber ?? ""} - {bidding.title} +

+
+
+ + + +
+ + {/* 탭 네비게이션 */} +
+ +
+ + {/* 입찰 기본 정보 에디터 */} + +
+ ) +} diff --git a/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/items/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/items/page.tsx new file mode 100644 index 00000000..5b686a1c --- /dev/null +++ b/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/items/page.tsx @@ -0,0 +1,76 @@ +import { notFound } from 'next/navigation' +import { getBiddingById } from "@/lib/bidding/service" +import { Bidding } from "@/db/schema/bidding" +import { Button } from "@/components/ui/button" +import { ArrowLeft } from "lucide-react" +import Link from "next/link" +import { BiddingItemsEditor } from "@/components/bidding/manage/bidding-items-editor" +import { BiddingTabs } from "../bidding-tabs" + +// 메타데이터 생성 +export async function generateMetadata({ params }: { params: Promise<{ lng: string; id: string }> }) { + const { lng, id } = await params + const parsedId = parseInt(id) + if (isNaN(parsedId)) return { title: '입찰 품목 관리' } + + try { + const bidding = await getBiddingById(parsedId) + return { + title: bidding ? `${bidding.title} - 입찰 품목 관리` : '입찰 품목 관리', + } + } catch { + return { title: '입찰 품목 관리' } + } +} + +interface PageProps { + params: Promise<{ lng: string; id: string }> +} + +export default async function BiddingItemsPage({ params }: PageProps) { + const { lng, id } = await params + const parsedId = parseInt(id) + + if (isNaN(parsedId)) { + notFound() + } + + const bidding: Bidding | null = await getBiddingById(parsedId) + + if (!bidding) { + notFound() + } + + return ( +
+ {/* 헤더 */} +
+
+
+

+ 입찰 품목 관리 +

+

+ 입찰 No. {bidding.biddingNumber ?? ""} - {bidding.title} +

+
+ +
+ + + +
+ + {/* 탭 네비게이션 */} +
+ +
+ + {/* 입찰 품목 에디터 */} + +
+ ) +} diff --git a/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/layout.tsx b/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/layout.tsx deleted file mode 100644 index 80e7f8d2..00000000 --- a/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/layout.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { Metadata } from "next" - -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" -import { getBiddingById, getBiddingConditions } from "@/lib/bidding/service" -import { Bidding } from "@/db/schema/bidding" -import { Button } from "@/components/ui/button" -import { ArrowLeft } from "lucide-react" -import Link from "next/link" -import { BiddingInfoHeader } from "@/components/bidding/bidding-info-header" -import { BiddingConditionsEdit } from "@/components/bidding/bidding-conditions-edit" -export const metadata: Metadata = { - title: "Bidding Detail", -} - -export default async function SettingsLayout({ - children, - params, -}: { - children: React.ReactNode - params: { lng: string , id: string} -}) { - - // 1) URL 파라미터에서 id 추출, Number로 변환 - const resolvedParams = await params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - // 2) DB에서 해당 입찰 정보 조회 - const bidding: Bidding | null = await getBiddingById(idAsNumber) - const biddingConditions = await getBiddingConditions(idAsNumber) - - // 3) 사이드바 메뉴 - const sidebarNavItems = [ - { - title: "입찰 사전견적", - href: `/${lng}/evcp/bid/${id}/pre-quote`, - }, - { - title: "입찰 관리상세", - href: `/${lng}/evcp/bid/${id}/detail`, - }, - ] - - return ( - <> -
-
-
- {/* RFQ 목록으로 돌아가는 링크 추가 */} -
-
- {/* 4) 입찰 정보가 있으면 번호 + 제목 + "상세 정보" 표기 */} -

- {bidding - ? `입찰 No. ${bidding.biddingNumber ?? ""} - ${bidding.title}` - : "Loading Bidding..."} -

-
- - - -
- - {/* 입찰 정보 헤더 */} - - - {/* 입찰 조건 */} - {bidding && ( - - )} - - -
- -
{children}
-
-
-
-
- - ) -} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/page.tsx deleted file mode 100644 index ca0788a5..00000000 --- a/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { redirect } from 'next/navigation' - -interface PageProps { - params: Promise<{ lng: string; id: string }> -} - -export default async function Page({ params }: PageProps) { - const { lng, id } = await params - - // 기본적으로 입찰 사전견적 페이지로 리다이렉트 - redirect(`/${lng}/evcp/bid/${id}/pre-quote`) -} diff --git a/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/pre-quote/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/pre-quote/page.tsx deleted file mode 100644 index d978974b..00000000 --- a/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/pre-quote/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Suspense } from 'react' -import { notFound } from 'next/navigation' -import { getBiddingDetailData } from '@/lib/bidding/detail/service' -import { getBiddingCompanies } from '@/lib/bidding/pre-quote/service' -import { BiddingPreQuoteContent } from '@/lib/bidding/pre-quote/table/bidding-pre-quote-content' - -// 메타데이터 생성 -export async function generateMetadata({ params }: { params: Promise<{ id: string }> }) { - const { id } = await params - const parsedId = parseInt(id) - if (isNaN(parsedId)) return { title: '입찰 사전견적' } - - try { - const detailData = await getBiddingDetailData(parsedId) - return { - title: detailData.bidding ? `${detailData.bidding.title} - 입찰 사전견적` : '입찰 사전견적', - } - } catch { - return { title: '입찰 사전견적' } - } -} - -interface PageProps { - params: Promise<{ id: string }> -} - -export default async function Page({ params }: PageProps) { - const { id } = await params - const parsedId = parseInt(id) - - if (isNaN(parsedId)) { - notFound() - } - - // 통합 데이터 로딩 함수 사용 - const detailData = await getBiddingDetailData(parsedId) - - if (!detailData.bidding) { - notFound() - } - - // 사전견적용 입찰 업체들 조회 - const biddingCompaniesResult = await getBiddingCompanies(parsedId) - const biddingCompanies = biddingCompaniesResult?.success ? biddingCompaniesResult.data || [] : [] - - return ( - 로딩 중...}> - - - ) -} diff --git a/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/schedule/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/schedule/page.tsx new file mode 100644 index 00000000..a79bef88 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/(procurement)/bid/[id]/schedule/page.tsx @@ -0,0 +1,73 @@ +import { notFound } from 'next/navigation' +import { getBiddingById } from "@/lib/bidding/service" +import { Bidding } from "@/db/schema/bidding" +import { Button } from "@/components/ui/button" +import { ArrowLeft } from "lucide-react" +import Link from "next/link" +import { BiddingScheduleEditor } from "@/components/bidding/manage/bidding-schedule-editor" +import { BiddingTabs } from "../bidding-tabs" + +// 메타데이터 생성 +export async function generateMetadata({ params }: { params: Promise<{ lng: string; id: string }> }) { + const { lng, id } = await params + const parsedId = parseInt(id) + if (isNaN(parsedId)) return { title: '입찰 일정 관리' } + + try { + const bidding = await getBiddingById(parsedId) + return { + title: bidding ? `${bidding.title} - 입찰 일정 관리` : '입찰 일정 관리', + } + } catch { + return { title: '입찰 일정 관리' } + } +} + +interface PageProps { + params: Promise<{ lng: string; id: string }> +} + +export default async function BiddingSchedulePage({ params }: PageProps) { + const { lng, id } = await params + const parsedId = parseInt(id) + + if (isNaN(parsedId)) { + notFound() + } + + const bidding: Bidding | null = await getBiddingById(parsedId) + + if (!bidding) { + notFound() + } + + return ( +
+ {/* 헤더 */} +
+
+

+ 입찰 일정 관리 +

+

+ 입찰 No. {bidding.biddingNumber ?? ""} - {bidding.title} +

+
+ + + +
+ + {/* 탭 네비게이션 */} +
+ +
+ + {/* 입찰 일정 에디터 */} + +
+ ) +} diff --git a/app/[lng]/evcp/(evcp)/(procurement)/bid/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/bid/page.tsx index aa9f33b5..973593d8 100644 --- a/app/[lng]/evcp/(evcp)/(procurement)/bid/page.tsx +++ b/app/[lng]/evcp/(evcp)/(procurement)/bid/page.tsx @@ -4,14 +4,9 @@ import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" import { getBiddings, getBiddingStatusCounts, - getBiddingTypeCounts, - getBiddingManagerCounts, - getBiddingMonthlyStats, - getUserCodeByEmail, } from "@/lib/bidding/service" import { searchParamsCache } from "@/lib/bidding/validation" import { BiddingsPageHeader } from "@/lib/bidding/list/biddings-page-header" -import { BiddingsStatsCards } from "@/lib/bidding/list/biddings-stats-cards" import { BiddingsTable } from "@/lib/bidding/list/biddings-table" import { getValidFilters } from "@/lib/data-table" import { type SearchParams } from "@/types/table" @@ -33,30 +28,13 @@ export default async function BiddingsPage(props: IndexPageProps) { const validFilters = getValidFilters(search.filters) - // ✅ 입찰 데이터를 먼저 가져옴 - const biddingsResult = await getBiddings({ - ...search, - filters: validFilters, - }) - - // ✅ 입찰 데이터에 managerCode 추가 - const biddingsDataWithManagerCode = await Promise.all( - biddingsResult.data.map(async (item) => { - let managerCode: string | null = null - if (item.managerEmail) { - managerCode = await getUserCodeByEmail(item.managerEmail) - } - return { ...item, managerCode: managerCode || null } - }) - ) - // ✅ 모든 데이터를 병렬로 로드 const promises = Promise.all([ - Promise.resolve({ ...biddingsResult, data: biddingsDataWithManagerCode }), + getBiddings({ + ...search, + filters: validFilters, + }), getBiddingStatusCounts(), - getBiddingTypeCounts(), - getBiddingManagerCounts(), - getBiddingMonthlyStats(), ]) return ( @@ -66,13 +44,6 @@ export default async function BiddingsPage(props: IndexPageProps) { {/* ═══════════════════════════════════════════════════════════════ */} - {/* ═══════════════════════════════════════════════════════════════ */} - {/* 통계 카드들 */} - {/* ═══════════════════════════════════════════════════════════════ */} - }> - - - {/* ═══════════════════════════════════════════════════════════════ */} {/* 메인 테이블 */} {/* ═══════════════════════════════════════════════════════════════ */} @@ -92,44 +63,3 @@ export default async function BiddingsPage(props: IndexPageProps) { ) } - -// ═══════════════════════════════════════════════════════════════ -// 통계 카드 래퍼 컴포넌트 -// ═══════════════════════════════════════════════════════════════ -async function BiddingsStatsCardsWrapper({ - promises -}: { - promises: Promise<[ - Awaited>, - Awaited>, - Awaited>, - Awaited>, - Awaited>, - ]> -}) { - const [biddingsResult, statusCounts, typeCounts, managerCounts, monthlyStats] = await promises - - return ( - - ) -} - -// 통계 카드 스켈레톤 -function BiddingsStatsCardsSkeleton() { - return ( -
- {Array.from({ length: 4 }).map((_, i) => ( -
-
-
-
- ))} -
- ) -} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/(procurement)/bidding-notice/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/bidding-notice/page.tsx index 003db012..09ce13e7 100644 --- a/app/[lng]/evcp/(evcp)/(procurement)/bidding-notice/page.tsx +++ b/app/[lng]/evcp/(evcp)/(procurement)/bidding-notice/page.tsx @@ -1,38 +1,24 @@ -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' -import { BiddingNoticeEditor } from '@/lib/bidding/bidding-notice-editor' -import { getBiddingNoticeTemplate } from '@/lib/bidding/service' +import { BiddingNoticeTemplateManager } from '@/lib/bidding/bidding-notice-template-manager' +import { getBiddingNoticeTemplates } from '@/lib/bidding/service' // template 받을 때, 비동기 함수 호출 후 await 하므로 static-pre-render 과정에서 dynamic-server-error 발생. // 따라서, dynamic 속성을 force-dynamic 으로 설정하여 동적 렌더링 처리 -// getBiddingNoticeTemplate 함수에 대한 Promise를 넘기는 식으로 수정하게 되면 force-dynamic 선언을 제거해도 됨. export const dynamic = 'force-dynamic' export default async function BiddingNoticePage() { - const template = await getBiddingNoticeTemplate() + const templates = await getBiddingNoticeTemplates() return (
-

입찰공고문 관리

+

입찰공고문 템플릿 관리

- 표준 입찰공고문 템플릿을 작성하고 관리할 수 있습니다. + 입찰공고문 템플릿을 타입별로 작성하고 관리할 수 있습니다. + 각 타입별 템플릿은 입찰 생성 시 기본 양식으로 사용됩니다.

- - - 표준 입찰공고문 템플릿 - - 이 템플릿은 실제 입찰 공고 작성 시 기본 양식으로 사용됩니다. - 필요한 표준 정보와 서식을 미리 작성해두세요. - - - - - - +
) } \ No newline at end of file diff --git a/app/[lng]/partners/(partners)/bid/[id]/page.tsx b/app/[lng]/partners/(partners)/bid/[id]/page.tsx index b8c7ea59..b564b48f 100644 --- a/app/[lng]/partners/(partners)/bid/[id]/page.tsx +++ b/app/[lng]/partners/(partners)/bid/[id]/page.tsx @@ -38,6 +38,8 @@ export default async function PartnersBidDetailPage(props: PartnersBidDetailPage
) } + console.log('biddingId:', biddingId) + console.log('companyId:', companyId) return (
diff --git a/app/[lng]/partners/(partners)/bid/[id]/pre-quote/page.tsx b/app/[lng]/partners/(partners)/bid/[id]/pre-quote/page.tsx deleted file mode 100644 index 6364f7f8..00000000 --- a/app/[lng]/partners/(partners)/bid/[id]/pre-quote/page.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { PartnersBiddingPreQuote } from '@/lib/bidding/vendor/partners-bidding-pre-quote' -import { Suspense } from 'react' -import { Skeleton } from '@/components/ui/skeleton' - -import { getServerSession } from 'next-auth' -import { authOptions } from "@/app/api/auth/[...nextauth]/route" - -interface PartnersPreQuotePageProps { - params: Promise<{ - id: string - }> -} - -export default async function PartnersPreQuotePage(props: PartnersPreQuotePageProps) { - const resolvedParams = await props.params - const biddingId = parseInt(resolvedParams.id) - - if (isNaN(biddingId)) { - return ( -
-
-

유효하지 않은 입찰 ID입니다.

-
-
- ) - } - - // 세션에서 companyId 가져오기 - const session = await getServerSession(authOptions) - const companyId = session?.user?.companyId - - if (!companyId) { - return ( -
-
-

회사 정보가 없습니다. 다시 로그인 해주세요.

-
-
- ) - } - - return ( -
- }> - - -
- ) -} - -function PreQuoteSkeleton() { - return ( -
- {/* 헤더 스켈레톤 */} -
-
- - -
-
- - {/* 입찰 공고 스켈레톤 */} -
- -
- {Array.from({ length: 6 }).map((_, i) => ( - - ))} -
-
- - {/* 현재 설정된 조건 스켈레톤 */} -
- -
- {Array.from({ length: 8 }).map((_, i) => ( - - ))} -
-
- - {/* 사전견적 폼 스켈레톤 */} -
- -
- {Array.from({ length: 10 }).map((_, i) => ( - - ))} - -
-
-
- ) -} diff --git a/app/[lng]/partners/(partners)/bid/page.tsx b/app/[lng]/partners/(partners)/bid/page.tsx index 05081c3a..a09dec72 100644 --- a/app/[lng]/partners/(partners)/bid/page.tsx +++ b/app/[lng]/partners/(partners)/bid/page.tsx @@ -5,7 +5,7 @@ import { authOptions } from "@/app/api/auth/[...nextauth]/route" import { getBiddingListForPartners } from '@/lib/bidding/detail/service' import { Shell } from '@/components/shell' import { DataTableSkeleton } from '@/components/data-table/data-table-skeleton' -import { InformationButton } from '@/components/information/information-button' + export default async function PartnersBidPage() { // 세션에서 companyId 가져오기 const session = await getServerSession(authOptions) @@ -31,10 +31,7 @@ export default async function PartnersBidPage() {
-
-

입찰 참여

- -
+

입찰 참여

참여 가능한 입찰 목록을 확인하고 응찰하실 수 있습니다.

diff --git a/app/[lng]/partners/(partners)/general-contract-review/[contractId]/page.tsx b/app/[lng]/partners/(partners)/general-contract-review/[contractId]/page.tsx new file mode 100644 index 00000000..82ff41c3 --- /dev/null +++ b/app/[lng]/partners/(partners)/general-contract-review/[contractId]/page.tsx @@ -0,0 +1,62 @@ +import * as React from "react" +import { getServerSession } from "next-auth/next" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" +import { redirect } from "next/navigation" +import { Shell } from "@/components/shell" +import { getContractForVendorReview } from "@/lib/general-contracts/service" +import { VendorContractReviewClient, type VendorContractReviewClientProps } from "./vendor-contract-review-client" + +interface VendorContractReviewPageProps { + params: Promise<{ contractId: string }> +} + +export default async function VendorContractReviewPage(props: VendorContractReviewPageProps) { + const resolvedParams = await props.params + const contractId = parseInt(resolvedParams.contractId) + + if (isNaN(contractId)) { + redirect('/partners') + } + + // 세션에서 벤더 정보 가져오기 + const session = await getServerSession(authOptions) + if (!session?.user?.companyId) { + return ( +
+ 정상적인 벤더에 소속된 계정이 아닙니다. +
+ ) + } + + const vendorId = session.user.companyId + + try { + // 협력업체용 계약 정보 조회 + const contract = await getContractForVendorReview(contractId, vendorId) + + return ( + + + + ) + } catch (error) { + console.error('계약 정보 조회 오류:', error) + return ( +
+
+

계약 정보를 불러올 수 없습니다.

+

+ {error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.'} +

+
+
+ ) + } +} + diff --git a/app/[lng]/partners/(partners)/general-contract-review/[contractId]/vendor-contract-review-client.tsx b/app/[lng]/partners/(partners)/general-contract-review/[contractId]/vendor-contract-review-client.tsx new file mode 100644 index 00000000..ee5815d6 --- /dev/null +++ b/app/[lng]/partners/(partners)/general-contract-review/[contractId]/vendor-contract-review-client.tsx @@ -0,0 +1,423 @@ +'use client' + +import React, { useState, useEffect, useRef } from 'react' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Label } from '@/components/ui/label' +import { Textarea } from '@/components/ui/textarea' +import { Badge } from '@/components/ui/badge' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { + FileText, + Send, + Save, + LoaderIcon, + AlertCircle, + Eye, + ArrowLeft, + MessageSquare +} from 'lucide-react' +import { toast } from 'sonner' +import { useRouter } from 'next/navigation' +import { + getContractForVendorReview, + vendorReplyToContractReview, + saveVendorCommentDraft +} from '@/lib/general-contracts/service' +import type { WebViewerInstance } from '@pdftron/webviewer' + +interface VendorContractReviewClientProps { + contract: { + id: number + contractNumber: string + revision: number + name: string | null + status: string + type: string | null + category: string | null + vendorId: number + contractAmount: number | null + currency: string | null + startDate: string | null + endDate: string | null + specificationType: string | null + specificationManualText: string | null + contractScope: string | null + notes: string | null + contractItems: Array> + attachments: Array<{ + id: number + contractId: number + documentName: string + fileName: string + filePath: string + vendorComment: string | null + shiComment: string | null + uploadedAt: Date + uploadedById: number + }> + vendor: { + id: number + vendorCode: string | null + vendorName: string | null + } | null + } + vendorId: number +} + +export function VendorContractReviewClient({ + contract: initialContract, + vendorId +}: VendorContractReviewClientProps) { + const router = useRouter() + const [contract, setContract] = useState(initialContract) + const [vendorComment, setVendorComment] = useState('') + const [isSaving, setIsSaving] = useState(false) + const [isSubmitting, setIsSubmitting] = useState(false) + + // PDFTron Viewer 관련 상태 + const viewerRef = useRef(null) + const instanceRef = useRef(null) + const [viewerLoading, setViewerLoading] = useState(false) + const [viewerInitialized, setViewerInitialized] = useState(false) + + // 계약 첨부파일에서 기존 Vendor Comment 로드 + useEffect(() => { + if (contract?.attachments && contract.attachments.length > 0) { + const firstAttachment = contract.attachments[0] + if (firstAttachment.vendorComment) { + setVendorComment(firstAttachment.vendorComment) + } + } + }, [contract]) + + // PDFTron Viewer 초기화 + useEffect(() => { + if (!viewerRef.current || viewerInitialized) return + + const initializeViewer = async () => { + try { + setViewerLoading(true) + const { default: WebViewer } = await import('@pdftron/webviewer') + + if (!viewerRef.current) return + + const instance = await WebViewer( + { + path: '/pdftronWeb', + licenseKey: process.env.NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY || '', + fullAPI: true, + enableFilePicker: false, + enableMeasurement: false, + enableRedaction: false, + enableAnnotations: false, + enablePrint: false, + enableDownload: false, + }, + viewerRef.current + ) + + instanceRef.current = instance + setViewerInitialized(true) + setViewerLoading(false) + + // 계약서 초안 PDF가 있으면 로드 + if (contract?.attachments && contract.attachments.length > 0) { + const pdfAttachment = contract.attachments.find( + (att) => att.filePath && att.filePath.endsWith('.pdf') + ) + if (pdfAttachment && pdfAttachment.filePath) { + // 파일 경로를 완전한 URL로 변환 + const fileUrl = pdfAttachment.filePath.startsWith('http') + ? pdfAttachment.filePath + : `${process.env.NEXT_PUBLIC_URL}${pdfAttachment.filePath}` + + instance.UI.loadDocument(fileUrl) + } + } + } catch (error) { + console.error('PDFTron Viewer 초기화 오류:', error) + setViewerLoading(false) + toast.error('문서 뷰어를 초기화할 수 없습니다.') + } + } + + initializeViewer() + + return () => { + if (instanceRef.current) { + try { + instanceRef.current.UI.dispose() + } catch (error) { + console.error('Viewer 정리 오류:', error) + } + } + } + }, [contract, viewerInitialized]) + + // 임시 저장 + const handleSaveDraft = async () => { + if (!vendorComment.trim()) { + toast.error('의견을 입력해주세요.') + return + } + + setIsSaving(true) + try { + await saveVendorCommentDraft(contract.id, vendorComment, vendorId) + toast.success('의견이 임시 저장되었습니다.') + } catch (error) { + console.error('임시 저장 오류:', error) + const errorMessage = error instanceof Error ? error.message : '임시 저장에 실패했습니다.' + toast.error(errorMessage) + } finally { + setIsSaving(false) + } + } + + // 의견 회신 + const handleSubmitReply = async () => { + if (!vendorComment.trim()) { + toast.error('의견을 입력해주세요.') + return + } + + setIsSubmitting(true) + try { + await vendorReplyToContractReview(contract.id, vendorComment, vendorId) + toast.success('의견이 성공적으로 회신되었습니다.') + + // 계약 정보 다시 로드 + const updatedContract = await getContractForVendorReview(contract.id, vendorId) + setContract({ + ...updatedContract, + name: updatedContract.name || '', + } as VendorContractReviewClientProps['contract']) + + // 상태 변경 후 메시지 표시 + setTimeout(() => { + router.push('/partners/dashboard') + }, 2000) + } catch (error) { + console.error('의견 회신 오류:', error) + const errorMessage = error instanceof Error ? error.message : '의견 회신에 실패했습니다.' + toast.error(errorMessage) + } finally { + setIsSubmitting(false) + } + } + + const getStatusLabel = (status: string) => { + const statusLabels: Record = { + 'Draft': '임시저장', + 'Request to Review': '조건검토요청', + 'Vendor Replied Review': '협력업체 회신', + 'SHI Confirmed Review': '당사 검토 확정', + 'Contract Accept Request': '계약승인요청', + 'Complete the Contract': '계약체결', + } + return statusLabels[status] || status + } + + const getStatusColor = (status: string) => { + const statusColors: Record = { + 'Request to Review': 'bg-yellow-100 text-yellow-800', + 'Vendor Replied Review': 'bg-blue-100 text-blue-800', + 'SHI Confirmed Review': 'bg-green-100 text-green-800', + } + return statusColors[status] || 'bg-gray-100 text-gray-800' + } + + return ( +
+ {/* 헤더 */} +
+
+
+ +
+

일반계약 조건검토

+

+ 계약번호: {contract.contractNumber} (Rev.{contract.revision}) +

+
+
+ + {getStatusLabel(contract.status)} + +
+
+ + {/* 상태 안내 */} + {contract.status === 'Request to Review' && ( + + + + 계약 조건 검토를 요청받았습니다. 계약서 초안을 확인하고 의견을 입력해주세요. + + + )} + + {/* 계약 정보 카드 */} + + + + + 계약 정보 + + + +
+
+ +

{contract.name || '-'}

+
+
+ +

+ {contract.contractAmount?.toLocaleString() || '0'} {contract.currency || 'KRW'} +

+
+
+ +

+ {contract.startDate ? new Date(contract.startDate).toLocaleDateString() : '-'} ~{' '} + {contract.endDate ? new Date(contract.endDate).toLocaleDateString() : '-'} +

+
+
+ +

{contract.contractScope || '-'}

+
+
+
+
+ + {/* 계약서 초안 뷰어 */} + + + + + 계약서 초안 + + + +
+ {viewerLoading && ( +
+ + 문서를 불러오는 중... +
+ )} +
+
+ + + + {/* Vendor Comment 입력 */} + {contract.status === 'Request to Review' && ( + + + + + 검토 의견 입력 + + + +
+ +