From 9ceed79cf32c896f8a998399bf1b296506b2cd4a Mon Sep 17 00:00:00 2001 From: dujinkim Date: Tue, 8 Apr 2025 03:08:19 +0000 Subject: 로그인 및 미들웨어 처리. 구조 변경 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/[lng]/evcp/(evcp)/bqtbe/page.tsx | 72 +++++++++++++ .../evcp/(evcp)/budgetary-rfq/[id]/cbe/page.tsx | 56 ++++++++++ .../evcp/(evcp)/budgetary-rfq/[id]/layout.tsx | 80 +++++++++++++++ app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/page.tsx | 57 +++++++++++ .../evcp/(evcp)/budgetary-rfq/[id]/tbe/page.tsx | 55 ++++++++++ app/[lng]/evcp/(evcp)/budgetary-rfq/page.tsx | 86 ++++++++++++++++ app/[lng]/evcp/(evcp)/budgetary/[id]/cbe/page.tsx | 56 ++++++++++ app/[lng]/evcp/(evcp)/budgetary/[id]/layout.tsx | 80 +++++++++++++++ app/[lng]/evcp/(evcp)/budgetary/[id]/page.tsx | 57 +++++++++++ app/[lng]/evcp/(evcp)/budgetary/[id]/tbe/page.tsx | 55 ++++++++++ app/[lng]/evcp/(evcp)/budgetary/page.tsx | 86 ++++++++++++++++ app/[lng]/evcp/(evcp)/equip-class/page.tsx | 75 ++++++++++++++ app/[lng]/evcp/(evcp)/faq/manage/actions.ts | 48 +++++++++ app/[lng]/evcp/(evcp)/faq/manage/page.tsx | 38 +++++++ app/[lng]/evcp/(evcp)/faq/page.tsx | 62 +++++++++++ app/[lng]/evcp/(evcp)/form-list/page.tsx | 75 ++++++++++++++ app/[lng]/evcp/(evcp)/items/page.tsx | 74 ++++++++++++++ app/[lng]/evcp/(evcp)/layout.tsx | 17 ++++ app/[lng]/evcp/(evcp)/po/page.tsx | 65 ++++++++++++ app/[lng]/evcp/(evcp)/poa/page.tsx | 61 +++++++++++ app/[lng]/evcp/(evcp)/pq-criteria/[id]/page.tsx | 81 +++++++++++++++ app/[lng]/evcp/(evcp)/pq-criteria/page.tsx | 70 +++++++++++++ app/[lng]/evcp/(evcp)/pq/[vendorId]/page.tsx | 107 +++++++++++++++++++ app/[lng]/evcp/(evcp)/pq/page.tsx | 71 +++++++++++++ app/[lng]/evcp/(evcp)/projects/page.tsx | 75 ++++++++++++++ app/[lng]/evcp/(evcp)/report/page.tsx | 8 ++ app/[lng]/evcp/(evcp)/rfq/[id]/cbe/page.tsx | 53 ++++++++++ app/[lng]/evcp/(evcp)/rfq/[id]/layout.tsx | 80 +++++++++++++++ app/[lng]/evcp/(evcp)/rfq/[id]/page.tsx | 55 ++++++++++ app/[lng]/evcp/(evcp)/rfq/[id]/tbe/page.tsx | 55 ++++++++++ app/[lng]/evcp/(evcp)/rfq/page.tsx | 80 +++++++++++++++ app/[lng]/evcp/(evcp)/settings/layout.tsx | 68 +++++++++++++ app/[lng]/evcp/(evcp)/settings/page.tsx | 18 ++++ .../evcp/(evcp)/settings/preferences/page.tsx | 17 ++++ app/[lng]/evcp/(evcp)/system/admin-users/page.tsx | 60 +++++++++++ app/[lng]/evcp/(evcp)/system/layout.tsx | 75 ++++++++++++++ app/[lng]/evcp/(evcp)/system/page.tsx | 56 ++++++++++ app/[lng]/evcp/(evcp)/system/permissions/page.tsx | 17 ++++ app/[lng]/evcp/(evcp)/system/roles/page.tsx | 68 +++++++++++++ app/[lng]/evcp/(evcp)/tag-numbering/page.tsx | 74 ++++++++++++++ app/[lng]/evcp/(evcp)/tasks/page.tsx | 63 ++++++++++++ app/[lng]/evcp/(evcp)/tbe/page.tsx | 113 +++++++++++++++++++++ app/[lng]/evcp/(evcp)/vendor-candidates/page.tsx | 66 ++++++++++++ .../evcp/(evcp)/vendor-investigation/page.tsx | 65 ++++++++++++ .../evcp/(evcp)/vendors/[id]/info/items/page.tsx | 56 ++++++++++ app/[lng]/evcp/(evcp)/vendors/[id]/info/layout.tsx | 79 ++++++++++++++ app/[lng]/evcp/(evcp)/vendors/[id]/info/page.tsx | 56 ++++++++++ .../(evcp)/vendors/[id]/info/rfq-history/page.tsx | 55 ++++++++++ app/[lng]/evcp/(evcp)/vendors/page.tsx | 78 ++++++++++++++ app/[lng]/evcp/bqtbe/page.tsx | 72 ------------- app/[lng]/evcp/budgetary/[id]/cbe/page.tsx | 56 ---------- app/[lng]/evcp/budgetary/[id]/layout.tsx | 80 --------------- app/[lng]/evcp/budgetary/[id]/page.tsx | 57 ----------- app/[lng]/evcp/budgetary/[id]/tbe/page.tsx | 55 ---------- app/[lng]/evcp/budgetary/page.tsx | 86 ---------------- app/[lng]/evcp/equip-class/page.tsx | 75 -------------- app/[lng]/evcp/faq/manage/actions.ts | 48 --------- app/[lng]/evcp/faq/manage/page.tsx | 38 ------- app/[lng]/evcp/faq/page.tsx | 62 ----------- app/[lng]/evcp/form-list/page.tsx | 75 -------------- app/[lng]/evcp/items/page.tsx | 74 -------------- app/[lng]/evcp/layout.tsx | 17 ---- app/[lng]/evcp/page.tsx | 27 +++-- app/[lng]/evcp/po/page.tsx | 65 ------------ app/[lng]/evcp/poa/page.tsx | 61 ----------- app/[lng]/evcp/pq-criteria/[id]/page.tsx | 81 --------------- app/[lng]/evcp/pq-criteria/page.tsx | 70 ------------- app/[lng]/evcp/pq/[vendorId]/page.tsx | 109 -------------------- app/[lng]/evcp/pq/page.tsx | 71 ------------- app/[lng]/evcp/report/page.tsx | 8 -- app/[lng]/evcp/rfq/[id]/cbe/page.tsx | 53 ---------- app/[lng]/evcp/rfq/[id]/layout.tsx | 80 --------------- app/[lng]/evcp/rfq/[id]/page.tsx | 55 ---------- app/[lng]/evcp/rfq/[id]/tbe/page.tsx | 55 ---------- app/[lng]/evcp/rfq/page.tsx | 80 --------------- app/[lng]/evcp/settings/layout.tsx | 68 ------------- app/[lng]/evcp/settings/page.tsx | 18 ---- app/[lng]/evcp/settings/preferences/page.tsx | 17 ---- app/[lng]/evcp/system/admin-users/page.tsx | 60 ----------- app/[lng]/evcp/system/layout.tsx | 75 -------------- app/[lng]/evcp/system/page.tsx | 56 ---------- app/[lng]/evcp/system/permissions/page.tsx | 17 ---- app/[lng]/evcp/system/roles/page.tsx | 68 ------------- app/[lng]/evcp/tag-numbering/page.tsx | 74 -------------- app/[lng]/evcp/tasks/page.tsx | 63 ------------ app/[lng]/evcp/vendor-candidates/page.tsx | 66 ------------ app/[lng]/evcp/vendor-investigation/page.tsx | 65 ------------ app/[lng]/evcp/vendors/[id]/info/items/page.tsx | 56 ---------- app/[lng]/evcp/vendors/[id]/info/layout.tsx | 79 -------------- app/[lng]/evcp/vendors/[id]/info/page.tsx | 56 ---------- .../evcp/vendors/[id]/info/rfq-history/page.tsx | 55 ---------- app/[lng]/evcp/vendors/page.tsx | 78 -------------- app/[lng]/partners/pq/page.tsx | 91 +++++------------ app/api/auth/[...nextauth]/route.ts | 42 +++++++- app/api/cron/forms/route.ts | 21 ++++ app/api/cron/object-classes/route.ts | 21 ++++ app/api/cron/projects/route.ts | 21 ++++ app/api/cron/tag-types/route.ts | 20 ++++ 98 files changed, 3242 insertions(+), 2629 deletions(-) create mode 100644 app/[lng]/evcp/(evcp)/bqtbe/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/cbe/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/layout.tsx create mode 100644 app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/tbe/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/budgetary-rfq/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/budgetary/[id]/cbe/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/budgetary/[id]/layout.tsx create mode 100644 app/[lng]/evcp/(evcp)/budgetary/[id]/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/budgetary/[id]/tbe/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/budgetary/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/equip-class/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/faq/manage/actions.ts create mode 100644 app/[lng]/evcp/(evcp)/faq/manage/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/faq/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/form-list/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/items/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/layout.tsx create mode 100644 app/[lng]/evcp/(evcp)/po/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/poa/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/pq-criteria/[id]/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/pq-criteria/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/pq/[vendorId]/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/pq/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/projects/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/report/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/rfq/[id]/cbe/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/rfq/[id]/layout.tsx create mode 100644 app/[lng]/evcp/(evcp)/rfq/[id]/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/rfq/[id]/tbe/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/rfq/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/settings/layout.tsx create mode 100644 app/[lng]/evcp/(evcp)/settings/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/settings/preferences/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/system/admin-users/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/system/layout.tsx create mode 100644 app/[lng]/evcp/(evcp)/system/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/system/permissions/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/system/roles/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/tag-numbering/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/tasks/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/tbe/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/vendor-candidates/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/vendor-investigation/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/vendors/[id]/info/items/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/vendors/[id]/info/layout.tsx create mode 100644 app/[lng]/evcp/(evcp)/vendors/[id]/info/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/vendors/[id]/info/rfq-history/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/vendors/page.tsx delete mode 100644 app/[lng]/evcp/bqtbe/page.tsx delete mode 100644 app/[lng]/evcp/budgetary/[id]/cbe/page.tsx delete mode 100644 app/[lng]/evcp/budgetary/[id]/layout.tsx delete mode 100644 app/[lng]/evcp/budgetary/[id]/page.tsx delete mode 100644 app/[lng]/evcp/budgetary/[id]/tbe/page.tsx delete mode 100644 app/[lng]/evcp/budgetary/page.tsx delete mode 100644 app/[lng]/evcp/equip-class/page.tsx delete mode 100644 app/[lng]/evcp/faq/manage/actions.ts delete mode 100644 app/[lng]/evcp/faq/manage/page.tsx delete mode 100644 app/[lng]/evcp/faq/page.tsx delete mode 100644 app/[lng]/evcp/form-list/page.tsx delete mode 100644 app/[lng]/evcp/items/page.tsx delete mode 100644 app/[lng]/evcp/layout.tsx delete mode 100644 app/[lng]/evcp/po/page.tsx delete mode 100644 app/[lng]/evcp/poa/page.tsx delete mode 100644 app/[lng]/evcp/pq-criteria/[id]/page.tsx delete mode 100644 app/[lng]/evcp/pq-criteria/page.tsx delete mode 100644 app/[lng]/evcp/pq/[vendorId]/page.tsx delete mode 100644 app/[lng]/evcp/pq/page.tsx delete mode 100644 app/[lng]/evcp/report/page.tsx delete mode 100644 app/[lng]/evcp/rfq/[id]/cbe/page.tsx delete mode 100644 app/[lng]/evcp/rfq/[id]/layout.tsx delete mode 100644 app/[lng]/evcp/rfq/[id]/page.tsx delete mode 100644 app/[lng]/evcp/rfq/[id]/tbe/page.tsx delete mode 100644 app/[lng]/evcp/rfq/page.tsx delete mode 100644 app/[lng]/evcp/settings/layout.tsx delete mode 100644 app/[lng]/evcp/settings/page.tsx delete mode 100644 app/[lng]/evcp/settings/preferences/page.tsx delete mode 100644 app/[lng]/evcp/system/admin-users/page.tsx delete mode 100644 app/[lng]/evcp/system/layout.tsx delete mode 100644 app/[lng]/evcp/system/page.tsx delete mode 100644 app/[lng]/evcp/system/permissions/page.tsx delete mode 100644 app/[lng]/evcp/system/roles/page.tsx delete mode 100644 app/[lng]/evcp/tag-numbering/page.tsx delete mode 100644 app/[lng]/evcp/tasks/page.tsx delete mode 100644 app/[lng]/evcp/vendor-candidates/page.tsx delete mode 100644 app/[lng]/evcp/vendor-investigation/page.tsx delete mode 100644 app/[lng]/evcp/vendors/[id]/info/items/page.tsx delete mode 100644 app/[lng]/evcp/vendors/[id]/info/layout.tsx delete mode 100644 app/[lng]/evcp/vendors/[id]/info/page.tsx delete mode 100644 app/[lng]/evcp/vendors/[id]/info/rfq-history/page.tsx delete mode 100644 app/[lng]/evcp/vendors/page.tsx create mode 100644 app/api/cron/forms/route.ts create mode 100644 app/api/cron/object-classes/route.ts create mode 100644 app/api/cron/projects/route.ts create mode 100644 app/api/cron/tag-types/route.ts (limited to 'app') diff --git a/app/[lng]/evcp/(evcp)/bqtbe/page.tsx b/app/[lng]/evcp/(evcp)/bqtbe/page.tsx new file mode 100644 index 00000000..655bd30a --- /dev/null +++ b/app/[lng]/evcp/(evcp)/bqtbe/page.tsx @@ -0,0 +1,72 @@ +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getAllTBE } from "@/lib/rfqs/service" +import { searchParamsTBECache } from "@/lib/rfqs/validations" +import { AllTbeTable } from "@/lib/tbe/table/tbe-table" +import { RfqType } from "@/lib/rfqs/validations" +import * as React from "react" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + } + searchParams: Promise + rfqType: RfqType +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + + const rfqType = props.rfqType ?? RfqType.BUDGETARY // rfqType이 없으면 BUDGETARY로 설정 + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getAllTBE({ + ...search, + filters: validFilters, + rfqType + } + ) + ]) + + // 4) 렌더링 + return ( + +
+
+
+

+ Technical Bid Evaluation +

+

+ 초대된 벤더에게 TBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다. +

+
+
+
+ + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/cbe/page.tsx b/app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/cbe/page.tsx new file mode 100644 index 00000000..9a4ae7eb --- /dev/null +++ b/app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/cbe/page.tsx @@ -0,0 +1,56 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getCBE, getTBE } from "@/lib/rfqs/service" +import { searchParamsCBECache, } from "@/lib/rfqs/validations" +import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" +import { CbeTable } from "@/lib/rfqs/cbe-table/cbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsCBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getCBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Commercial Bid Evaluation +

+

+ 초대된 벤더에게 CBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/layout.tsx b/app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/layout.tsx new file mode 100644 index 00000000..39f045e5 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/layout.tsx @@ -0,0 +1,80 @@ +import { Metadata } from "next" + +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" +import { findVendorById } from "@/lib/vendors/service" // 가정: 여기에 findVendorById가 있다고 가정 +import { Rfq, RfqWithItems } from "@/db/schema/rfq" +import { findRfqById } from "@/lib/rfqs/service" +import { formatDate } from "@/lib/utils" + +export const metadata: Metadata = { + title: "Vendor Detail", +} + +export default async function RfqLayout({ + 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 rfq: RfqWithItems | null = await findRfqById(idAsNumber) + + // 3) 사이드바 메뉴 + const sidebarNavItems = [ + { + title: "Matched Vendors", + href: `/${lng}/evcp/budgetary/${id}`, + }, + { + title: "TBE", + href: `/${lng}/evcp/budgetary/${id}/tbe`, + }, + { + title: "CBE", + href: `/${lng}/evcp/budgetary/${id}/cbe`, + }, + + ] + + return ( + <> +
+
+
+
+ {/* 4) 벤더 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} +

+ {rfq + ? `${rfq.rfqCode ?? ""} 관리` + : "Loading RFQ..."} +

+ +

+ {rfq + ? `${rfq.description ?? ""} ${rfq.lines.map(line => line.itemCode).join(", ")}` + : ""} +

+

Due Date:{ rfq && {formatDate(rfq?.dueDate)}}

+
+ +
+ +
{children}
+
+
+
+
+ + ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/page.tsx b/app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/page.tsx new file mode 100644 index 00000000..f6160574 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/page.tsx @@ -0,0 +1,57 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getMatchedVendors } from "@/lib/rfqs/service" +import { searchParamsMatchedVCache } from "@/lib/rfqs/validations" +import { MatchedVendorsTable } from "@/lib/rfqs/vendor-table/vendors-table" +import { RfqType } from "@/lib/rfqs/validations" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise + rfqType: RfqType +} + +export default async function RfqPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + const rfqType = props.rfqType ?? RfqType.BUDGETARY // rfqType이 없으면 BUDGETARY로 설정 + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + const searchParams = await props.searchParams + const search = searchParamsMatchedVCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getMatchedVendors({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Vendors +

+

+ 등록된 벤더 중에서 이 RFQ 아이템에 매칭되는 업체를 보여줍니다.
"발행하기" 버튼을 통해 RFQ를 전송하면 첨부파일과 함께 RFQ 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/tbe/page.tsx b/app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/tbe/page.tsx new file mode 100644 index 00000000..a6259696 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/tbe/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getTBE } from "@/lib/rfqs/service" +import { searchParamsTBECache } from "@/lib/rfqs/validations" +import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Technical Bid Evaluation +

+

+ 초대된 벤더에게 TBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/budgetary-rfq/page.tsx b/app/[lng]/evcp/(evcp)/budgetary-rfq/page.tsx new file mode 100644 index 00000000..dc2a4a2b --- /dev/null +++ b/app/[lng]/evcp/(evcp)/budgetary-rfq/page.tsx @@ -0,0 +1,86 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { searchParamsCache } from "@/lib/rfqs/validations" +import { getRfqs, getRfqStatusCounts } from "@/lib/rfqs/service" +import { RfqsTable } from "@/lib/rfqs/table/rfqs-table" +import { getAllItems } from "@/lib/items/service" +import { RfqType } from "@/lib/rfqs/validations" +import { Ellipsis } from "lucide-react" + +interface RfqPageProps { + searchParams: Promise; + rfqType: RfqType; + title: string; + description: string; +} + +export default async function RfqPage({ + searchParams, + rfqType = RfqType.PURCHASE_BUDGETARY, + title = "Budgetary Quote", + description = "Budgetary Quote를 등록하여 요청 및 응답을 관리할 수 있습니다." +}: RfqPageProps) { + const search = searchParamsCache.parse(await searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getRfqs({ + ...search, + filters: validFilters, + rfqType // 전달받은 rfqType 사용 + }), + getRfqStatusCounts(rfqType), // rfqType 전달 + getAllItems() + ]) + + return ( + +
+
+
+

+ {title} +

+

+ {description} + 기본적인 정보와 RFQ를 위한 아이템 등록 및 첨부를 한 후, + + + 버튼 + 을 클릭하면 "Proceed"를 통해 상세화면으로 이동하여 진행할 수 있습니다. +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/budgetary/[id]/cbe/page.tsx b/app/[lng]/evcp/(evcp)/budgetary/[id]/cbe/page.tsx new file mode 100644 index 00000000..9a4ae7eb --- /dev/null +++ b/app/[lng]/evcp/(evcp)/budgetary/[id]/cbe/page.tsx @@ -0,0 +1,56 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getCBE, getTBE } from "@/lib/rfqs/service" +import { searchParamsCBECache, } from "@/lib/rfqs/validations" +import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" +import { CbeTable } from "@/lib/rfqs/cbe-table/cbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsCBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getCBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Commercial Bid Evaluation +

+

+ 초대된 벤더에게 CBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/budgetary/[id]/layout.tsx b/app/[lng]/evcp/(evcp)/budgetary/[id]/layout.tsx new file mode 100644 index 00000000..39f045e5 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/budgetary/[id]/layout.tsx @@ -0,0 +1,80 @@ +import { Metadata } from "next" + +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" +import { findVendorById } from "@/lib/vendors/service" // 가정: 여기에 findVendorById가 있다고 가정 +import { Rfq, RfqWithItems } from "@/db/schema/rfq" +import { findRfqById } from "@/lib/rfqs/service" +import { formatDate } from "@/lib/utils" + +export const metadata: Metadata = { + title: "Vendor Detail", +} + +export default async function RfqLayout({ + 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 rfq: RfqWithItems | null = await findRfqById(idAsNumber) + + // 3) 사이드바 메뉴 + const sidebarNavItems = [ + { + title: "Matched Vendors", + href: `/${lng}/evcp/budgetary/${id}`, + }, + { + title: "TBE", + href: `/${lng}/evcp/budgetary/${id}/tbe`, + }, + { + title: "CBE", + href: `/${lng}/evcp/budgetary/${id}/cbe`, + }, + + ] + + return ( + <> +
+
+
+
+ {/* 4) 벤더 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} +

+ {rfq + ? `${rfq.rfqCode ?? ""} 관리` + : "Loading RFQ..."} +

+ +

+ {rfq + ? `${rfq.description ?? ""} ${rfq.lines.map(line => line.itemCode).join(", ")}` + : ""} +

+

Due Date:{ rfq && {formatDate(rfq?.dueDate)}}

+
+ +
+ +
{children}
+
+
+
+
+ + ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/budgetary/[id]/page.tsx b/app/[lng]/evcp/(evcp)/budgetary/[id]/page.tsx new file mode 100644 index 00000000..f6160574 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/budgetary/[id]/page.tsx @@ -0,0 +1,57 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getMatchedVendors } from "@/lib/rfqs/service" +import { searchParamsMatchedVCache } from "@/lib/rfqs/validations" +import { MatchedVendorsTable } from "@/lib/rfqs/vendor-table/vendors-table" +import { RfqType } from "@/lib/rfqs/validations" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise + rfqType: RfqType +} + +export default async function RfqPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + const rfqType = props.rfqType ?? RfqType.BUDGETARY // rfqType이 없으면 BUDGETARY로 설정 + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + const searchParams = await props.searchParams + const search = searchParamsMatchedVCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getMatchedVendors({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Vendors +

+

+ 등록된 벤더 중에서 이 RFQ 아이템에 매칭되는 업체를 보여줍니다.
"발행하기" 버튼을 통해 RFQ를 전송하면 첨부파일과 함께 RFQ 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/budgetary/[id]/tbe/page.tsx b/app/[lng]/evcp/(evcp)/budgetary/[id]/tbe/page.tsx new file mode 100644 index 00000000..a6259696 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/budgetary/[id]/tbe/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getTBE } from "@/lib/rfqs/service" +import { searchParamsTBECache } from "@/lib/rfqs/validations" +import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Technical Bid Evaluation +

+

+ 초대된 벤더에게 TBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/budgetary/page.tsx b/app/[lng]/evcp/(evcp)/budgetary/page.tsx new file mode 100644 index 00000000..04550353 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/budgetary/page.tsx @@ -0,0 +1,86 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { searchParamsCache } from "@/lib/rfqs/validations" +import { getRfqs, getRfqStatusCounts } from "@/lib/rfqs/service" +import { RfqsTable } from "@/lib/rfqs/table/rfqs-table" +import { getAllItems } from "@/lib/items/service" +import { RfqType } from "@/lib/rfqs/validations" +import { Ellipsis } from "lucide-react" + +interface RfqPageProps { + searchParams: Promise; + rfqType: RfqType; + title: string; + description: string; +} + +export default async function RfqPage({ + searchParams, + rfqType = RfqType.BUDGETARY, + title = "Budgetary Quote", + description = "Budgetary Quote를 등록하여 요청 및 응답을 관리할 수 있습니다." +}: RfqPageProps) { + const search = searchParamsCache.parse(await searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getRfqs({ + ...search, + filters: validFilters, + rfqType // 전달받은 rfqType 사용 + }), + getRfqStatusCounts(rfqType), // rfqType 전달 + getAllItems() + ]) + + return ( + +
+
+
+

+ {title} +

+

+ {description} + 기본적인 정보와 RFQ를 위한 아이템 등록 및 첨부를 한 후, + + + 버튼 + 을 클릭하면 "Proceed"를 통해 상세화면으로 이동하여 진행할 수 있습니다. +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/equip-class/page.tsx b/app/[lng]/evcp/(evcp)/equip-class/page.tsx new file mode 100644 index 00000000..375eb69e --- /dev/null +++ b/app/[lng]/evcp/(evcp)/equip-class/page.tsx @@ -0,0 +1,75 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsCache } from "@/lib/equip-class/validation" +import { FormListsTable } from "@/lib/form-list/table/formLists-table" +import { getTagClassists } from "@/lib/equip-class/service" +import { EquipClassTable } from "@/lib/equip-class/table/equipClass-table" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTagClassists({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ Object Class List from S-EDP +

+

+ Object Class List를 확인할 수 있습니다.{" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/evcp/(evcp)/faq/manage/actions.ts b/app/[lng]/evcp/(evcp)/faq/manage/actions.ts new file mode 100644 index 00000000..bc443a8a --- /dev/null +++ b/app/[lng]/evcp/(evcp)/faq/manage/actions.ts @@ -0,0 +1,48 @@ +'use server'; + +import { promises as fs } from 'fs'; +import path from 'path'; +import { FaqCategory } from '@/components/faq/FaqCard'; +import { fallbackLng } from '@/i18n/settings'; + +const FAQ_CONFIG_PATH = path.join(process.cwd(), 'config', 'faqDataConfig.ts'); + +export async function updateFaqData(lng: string, newData: FaqCategory[]) { + try { + const fileContent = await fs.readFile(FAQ_CONFIG_PATH, 'utf-8'); + const dataMatch = fileContent.match(/export const faqCategories[^=]*=\s*(\{[\s\S]*\});/); + if (!dataMatch) { + throw new Error('FAQ 데이터 형식이 올바르지 않습니다.'); + } + + const allData = eval(`(${dataMatch[1]})`); + const updatedData = { + ...allData, + [lng]: newData + }; + + const newFileContent = `import { FaqCategory } from "@/components/faq/FaqCard";\n\ninterface LocalizedFaqCategories {\n [lng: string]: FaqCategory[];\n}\n\nexport const faqCategories: LocalizedFaqCategories = ${JSON.stringify(updatedData, null, 4)};`; + await fs.writeFile(FAQ_CONFIG_PATH, newFileContent, 'utf-8'); + + return { success: true }; + } catch (error) { + console.error('FAQ 데이터 업데이트 중 오류 발생:', error); + return { success: false, error: '데이터 업데이트 중 오류가 발생했습니다.' }; + } +} + +export async function getFaqData(lng: string): Promise<{ data: FaqCategory[] }> { + try { + const fileContent = await fs.readFile(FAQ_CONFIG_PATH, 'utf-8'); + const dataMatch = fileContent.match(/export const faqCategories[^=]*=\s*(\{[\s\S]*\});/); + if (!dataMatch) { + throw new Error('FAQ 데이터 형식이 올바르지 않습니다.'); + } + + const allData = eval(`(${dataMatch[1]})`); + return { data: allData[lng] || allData[fallbackLng] || [] }; + } catch (error) { + console.error('FAQ 데이터 읽기 중 오류 발생:', error); + return { data: [] }; + } +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/faq/manage/page.tsx b/app/[lng]/evcp/(evcp)/faq/manage/page.tsx new file mode 100644 index 00000000..011bbfa4 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/faq/manage/page.tsx @@ -0,0 +1,38 @@ +import { FaqManager } from '@/components/faq/FaqManager'; +import { getFaqData, updateFaqData } from './actions'; +import { revalidatePath } from 'next/cache'; +import { FaqCategory } from '@/components/faq/FaqCard'; + +interface Props { + params: { + lng: string; + } +} + +export default async function FaqManagePage(props: Props) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const { data } = await getFaqData(lng); + + async function handleSave(newData: FaqCategory[]) { + 'use server'; + await updateFaqData(lng, newData); + revalidatePath(`/${lng}/evcp/faq`); + } + + return ( +
+
+
+
+

FAQ Management

+

+ Manage FAQ categories and items for {lng.toUpperCase()} language. +

+
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/faq/page.tsx b/app/[lng]/evcp/(evcp)/faq/page.tsx new file mode 100644 index 00000000..9b62b7e4 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/faq/page.tsx @@ -0,0 +1,62 @@ +import { Separator } from "@/components/ui/separator" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { faqCategories } from "@/config/faqDataConfig" +import { FaqCard } from "@/components/faq/FaqCard" +import { Button } from "@/components/ui/button" +import { Settings } from "lucide-react" +import Link from "next/link" +import { fallbackLng } from "@/i18n/settings" + +interface Props { + params: { + lng: string; + } +} + +export default async function FaqPage(props: Props) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const localizedFaqCategories = faqCategories[lng] || faqCategories[fallbackLng]; + + return ( +
+
+
+
+
+

Frequently Asked Questions

+

+ Find answers to common questions about using the EVCP system. +

+
+ + + +
+ + + + + {localizedFaqCategories.map((category) => ( + + {category.label} + + ))} + + + {localizedFaqCategories.map((category) => ( + + {category.items.map((item, index) => ( + + ))} + + ))} + +
+
+
+ ) +} diff --git a/app/[lng]/evcp/(evcp)/form-list/page.tsx b/app/[lng]/evcp/(evcp)/form-list/page.tsx new file mode 100644 index 00000000..f96917d6 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/form-list/page.tsx @@ -0,0 +1,75 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsCache } from "@/lib/form-list/validation" +import { ItemsTable } from "@/lib/items/table/items-table" +import { getFormLists } from "@/lib/form-list/service" +import { FormListsTable } from "@/lib/form-list/table/formLists-table" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getFormLists({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ Form List from S-EDP +

+

+ 벤더 데이터 입력을 위한 Form 리스트입니다.{" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/evcp/(evcp)/items/page.tsx b/app/[lng]/evcp/(evcp)/items/page.tsx new file mode 100644 index 00000000..144689ff --- /dev/null +++ b/app/[lng]/evcp/(evcp)/items/page.tsx @@ -0,0 +1,74 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsCache } from "@/lib/items/validations" +import { getItems } from "@/lib/items/service" +import { ItemsTable } from "@/lib/items/table/items-table" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getItems({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ Package Items +

+

+ Item을 등록하고 관리할 수 있습니다.{" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/evcp/(evcp)/layout.tsx b/app/[lng]/evcp/(evcp)/layout.tsx new file mode 100644 index 00000000..9dc39f7b --- /dev/null +++ b/app/[lng]/evcp/(evcp)/layout.tsx @@ -0,0 +1,17 @@ +import { ReactNode } from 'react'; +import { Header } from '@/components/layout/Header'; +import { SiteFooter } from '@/components/layout/Footer'; + +export default function EvcpLayout({ children }: { children: ReactNode }) { + return ( +
+
+
+
+ {children} +
+
+ +
+ ); +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/po/page.tsx b/app/[lng]/evcp/(evcp)/po/page.tsx new file mode 100644 index 00000000..fa528df0 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/po/page.tsx @@ -0,0 +1,65 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getPOs } from "@/lib/po/service" +import { searchParamsCache } from "@/lib/po/validations" +import { PoListsTable } from "@/lib/po/table/po-table" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getPOs({ + ...search, + filters: validFilters, + }), + ]) + + return ( + + +
+
+
+

+ PO 확인 및 전자서명 +

+

+ 기간계 시스템으로부터 PO를 확인하고 벤더에게 전자서명을 요청할 수 있습니다. 요쳥된 전자서명의 이력 또한 확인할 수 있습니다. + +

+
+
+
+ + + }> + + + } + > + + +
+ ) +} diff --git a/app/[lng]/evcp/(evcp)/poa/page.tsx b/app/[lng]/evcp/(evcp)/poa/page.tsx new file mode 100644 index 00000000..dec5e05b --- /dev/null +++ b/app/[lng]/evcp/(evcp)/poa/page.tsx @@ -0,0 +1,61 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getChangeOrders } from "@/lib/poa/service" +import { searchParamsCache } from "@/lib/poa/validations" +import { ChangeOrderListsTable } from "@/lib/poa/table/poa-table" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getChangeOrders({ + ...search, + filters: validFilters, + }), + ]) + + return ( + +
+
+
+

+ 변경 PO 확인 및 전자서명 +

+

+ 발행된 PO의 변경 내역을 확인하고 관리할 수 있습니다. +

+
+
+
+ + }> + + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/pq-criteria/[id]/page.tsx b/app/[lng]/evcp/(evcp)/pq-criteria/[id]/page.tsx new file mode 100644 index 00000000..f040a0ca --- /dev/null +++ b/app/[lng]/evcp/(evcp)/pq-criteria/[id]/page.tsx @@ -0,0 +1,81 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsCache } from "@/lib/pq/validations" +import { getPQs } from "@/lib/pq/service" +import { PqsTable } from "@/lib/pq/table/pq-table" +import { ProjectSelectorWrapper } from "@/components/pq/project-select-wrapper" +import { notFound } from "next/navigation" + +interface ProjectPageProps { + params: { id: string } + searchParams: Promise +} + +export default async function ProjectPage(props: ProjectPageProps) { + const resolvedParams = await props.params + const id = resolvedParams.id + + const projectId = parseInt(id, 10) + + // 유효하지 않은 projectId 확인 + if (isNaN(projectId)) { + notFound() + } + + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + // filters가 없는 경우를 처리 + const validFilters = getValidFilters(search.filters) + + // 프로젝트별 PQ 데이터 가져오기 + const promises = Promise.all([ + getPQs({ + ...search, + filters: validFilters, + }, projectId, false) + ]) + + return ( + +
+
+

+ Pre-Qualification Check Sheet +

+

+ 벤더 등록을 위한, 벤더가 제출할 PQ 항목을: 프로젝트별로 관리할 수 있습니다. +

+
+ +
+ + }> + {/* */} + + + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/pq-criteria/page.tsx b/app/[lng]/evcp/(evcp)/pq-criteria/page.tsx new file mode 100644 index 00000000..778baa93 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/pq-criteria/page.tsx @@ -0,0 +1,70 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsCache } from "@/lib/pq/validations" +import { getPQs } from "@/lib/pq/service" +import { PqsTable } from "@/lib/pq/table/pq-table" +import { ProjectSelectorWrapper } from "@/components/pq/project-select-wrapper" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + // filters가 없는 경우를 처리 + + const validFilters = getValidFilters(search.filters) + + // onlyGeneral: true로 설정하여 일반 PQ 항목만 가져옴 + const promises = Promise.all([ + getPQs({ + ...search, + filters: validFilters, + }, null, true) + ]) + + return ( + +
+
+

+ Pre-Qualification Check Sheet +

+

+ 벤더 등록을 위한, 벤더가 제출할 PQ 항목을 관리할 수 있습니다. +

+
+ +
+ + }> + {/* */} + + + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/pq/[vendorId]/page.tsx b/app/[lng]/evcp/(evcp)/pq/[vendorId]/page.tsx new file mode 100644 index 00000000..4c2555a3 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/pq/[vendorId]/page.tsx @@ -0,0 +1,107 @@ +import * as React from "react" +import { Shell } from "@/components/shell" +import { type SearchParams } from "@/types/table" +import { getPQDataByVendorId, getVendorPQsList, loadGeneralPQData, loadProjectPQData } from "@/lib/pq/service" +import { Vendor } from "@/db/schema/vendors" +import { findVendorById } from "@/lib/vendors/service" +import VendorPQAdminReview from "@/components/pq/pq-review-detail" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Badge } from "@/components/ui/badge" + +interface IndexPageProps { + params: { + vendorId: string + } + searchParams: Promise +} + +export default async function PQReviewPage(props: IndexPageProps) { + const resolvedParams = await props.params + const vendorId = Number(resolvedParams.vendorId) + + // Fetch the vendor data + const vendor: Vendor | null = await findVendorById(vendorId) + if (!vendor) return
Vendor not found
+ + // Get list of all PQs (general + project-specific) for this vendor + const pqsList = await getVendorPQsList(vendorId) + + // Determine default active PQ to display + // If query param projectId exists, use that, otherwise use general PQ if available + const searchParams = await props.searchParams + const activeProjectId = searchParams.projectId ? Number(searchParams.projectId) : undefined + + // If no projectId query param, default to general PQ or first project PQ + const defaultTabId = activeProjectId ? + `project-${activeProjectId}` : + (pqsList.hasGeneralPq ? 'general' : `project-${pqsList.projectPQs[0]?.projectId}`) + + // Fetch PQ data for the active tab + let pqData; + if (activeProjectId) { + // Get project-specific PQ data + pqData = await getPQDataByVendorId(vendorId, activeProjectId) + } else { + // Get general PQ data + pqData = await getPQDataByVendorId(vendorId) + } + + return ( + + {pqsList.hasGeneralPq || pqsList.projectPQs.length > 0 ? ( + +
+

+ {vendor.vendorName} PQ Review +

+ + + {pqsList.hasGeneralPq && ( + + General PQ Standard + + )} + + {pqsList.projectPQs.map((project) => ( + + {project.projectName} {project.status} + + ))} + +
+ + {/* Tab content for General PQ */} + {pqsList.hasGeneralPq && ( + + + + )} + + {/* Tab content for each Project PQ */} + {pqsList.projectPQs.map((project) => ( + + loadProjectPQData(vendorId, project.projectId)} pqType="project" + /> + + ))} +
+ ) : ( +
+

No PQ submissions found for this vendor

+
+ )} +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/pq/page.tsx b/app/[lng]/evcp/(evcp)/pq/page.tsx new file mode 100644 index 00000000..46b22b12 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/pq/page.tsx @@ -0,0 +1,71 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { getVendorsInPQ } from "@/lib/pq/service" +import { searchParamsCache } from "@/lib/vendors/validations" +import { VendorsPQReviewTable } from "@/lib/pq/pq-review-table/vendors-table" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getVendorsInPQ({ + ...search, + filters: validFilters, + }), + ]) + + return ( + + +
+
+
+

+ Pre-Qualification Review +

+

+ 벤더가 제출한 PQ를 확인하고 수정 요청 등을 할 수 있으며 PQ 종료 후에는 통과 여부를 결정할 수 있습니다. + +

+
+
+
+ + + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/evcp/(evcp)/projects/page.tsx b/app/[lng]/evcp/(evcp)/projects/page.tsx new file mode 100644 index 00000000..0320f259 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/projects/page.tsx @@ -0,0 +1,75 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { ItemsTable } from "@/lib/items/table/items-table" +import { getProjectLists } from "@/lib/projects/service" +import { ProjectsTable } from "@/lib/projects/table/projects-table" +import { searchParamsProjectsCache } from "@/lib/projects/validation" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsProjectsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getProjectLists({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ Project List from S-EDP +

+

+ S-EDP로부터 수신하는 프로젝트 리스트입니다. 향후 MDG로 전환됩니다.{" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/evcp/(evcp)/report/page.tsx b/app/[lng]/evcp/(evcp)/report/page.tsx new file mode 100644 index 00000000..a1e9f8be --- /dev/null +++ b/app/[lng]/evcp/(evcp)/report/page.tsx @@ -0,0 +1,8 @@ + +export default function Pages() { + return ( + <> + test + + ) + } \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/rfq/[id]/cbe/page.tsx b/app/[lng]/evcp/(evcp)/rfq/[id]/cbe/page.tsx new file mode 100644 index 00000000..bc32641f --- /dev/null +++ b/app/[lng]/evcp/(evcp)/rfq/[id]/cbe/page.tsx @@ -0,0 +1,53 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsTBECache } from "@/lib/rfqs/validations" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqCBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // const promises = Promise.all([ + // getCBE({ + // ...search, + // filters: validFilters, + // }, + // idAsNumber) + // ]) + + // 4) 렌더링 + return ( +
+
+

+ Technical Bid Evaluation +

+

+ 초대된 벤더에게 CBE를 보낼 수 있습니다.
"발행하기" 버튼을 통해 CBE를 전송하면 CBE 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/rfq/[id]/layout.tsx b/app/[lng]/evcp/(evcp)/rfq/[id]/layout.tsx new file mode 100644 index 00000000..2aac90eb --- /dev/null +++ b/app/[lng]/evcp/(evcp)/rfq/[id]/layout.tsx @@ -0,0 +1,80 @@ +import { Metadata } from "next" + +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" +import { findVendorById } from "@/lib/vendors/service" // 가정: 여기에 findVendorById가 있다고 가정 +import { Rfq, RfqWithItems } from "@/db/schema/rfq" +import { findRfqById } from "@/lib/rfqs/service" +import { formatDate } from "@/lib/utils" + +export const metadata: Metadata = { + title: "Vendor Detail", +} + +export default async function RfqLayout({ + 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 rfq: RfqWithItems | null = await findRfqById(idAsNumber) + + // 3) 사이드바 메뉴 + const sidebarNavItems = [ + { + title: "Matched Vendors", + href: `/${lng}/evcp/rfq/${id}`, + }, + { + title: "TBE", + href: `/${lng}/evcp/rfq/${id}/tbe`, + }, + { + title: "CBE", + href: `/${lng}/evcp/rfq/${id}/cbe`, + }, + + ] + + return ( + <> +
+
+
+
+ {/* 4) 벤더 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} +

+ {rfq + ? `${rfq.rfqCode ?? ""} 관리` + : "Loading RFQ..."} +

+ +

+ {rfq + ? `${rfq.description ?? ""} ${rfq.lines.map(line => line.itemCode).join(", ")}` + : ""} +

+

Due Date:{ rfq && {formatDate(rfq?.dueDate)}}

+
+ +
+ +
{children}
+
+
+
+
+ + ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/rfq/[id]/page.tsx b/app/[lng]/evcp/(evcp)/rfq/[id]/page.tsx new file mode 100644 index 00000000..026ca5ac --- /dev/null +++ b/app/[lng]/evcp/(evcp)/rfq/[id]/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getMatchedVendors } from "@/lib/rfqs/service" +import { searchParamsMatchedVCache } from "@/lib/rfqs/validations" +import { MatchedVendorsTable } from "@/lib/rfqs/vendor-table/vendors-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsMatchedVCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getMatchedVendors({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Vendors +

+

+ 등록된 벤더 중에서 이 RFQ 아이템에 매칭되는 업체를 보여줍니다.
"발행하기" 버튼을 통해 RFQ를 전송하면 첨부파일과 함께 RFQ 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/rfq/[id]/tbe/page.tsx b/app/[lng]/evcp/(evcp)/rfq/[id]/tbe/page.tsx new file mode 100644 index 00000000..15c5d93c --- /dev/null +++ b/app/[lng]/evcp/(evcp)/rfq/[id]/tbe/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getTBE } from "@/lib/rfqs/service" +import { searchParamsTBECache } from "@/lib/rfqs/validations" +import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTBE({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ Technical Bid Evaluation +

+

+ 초대된 벤더에게 TBE를 보낼 수 있습니다.
"발행하기" 버튼을 통해 TBE를 전송하면 첨부파일과 함께 TBE 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/rfq/page.tsx b/app/[lng]/evcp/(evcp)/rfq/page.tsx new file mode 100644 index 00000000..3417b0bf --- /dev/null +++ b/app/[lng]/evcp/(evcp)/rfq/page.tsx @@ -0,0 +1,80 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { searchParamsCache } from "@/lib/rfqs/validations" +import { getRfqs, getRfqStatusCounts } from "@/lib/rfqs/service" +import { RfqsTable } from "@/lib/rfqs/table/rfqs-table" +import { getAllItems } from "@/lib/items/service" +import { RfqType } from "@/lib/rfqs/validations" + +interface RfqPageProps { + searchParams: Promise; + rfqType: RfqType; + title: string; + description: string; +} + +export default async function RfqPage({ + searchParams, + rfqType = RfqType.PURCHASE, + title = "RFQ", + description = "RFQ를 등록하고 관리할 수 있습니다." +}: RfqPageProps) { + const search = searchParamsCache.parse(await searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getRfqs({ + ...search, + filters: validFilters, + rfqType // 전달받은 rfqType 사용 + }), + getRfqStatusCounts(rfqType), // rfqType 전달 + getAllItems() + ]) + + return ( + +
+
+
+

+ {title} +

+

+ {description} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/settings/layout.tsx b/app/[lng]/evcp/(evcp)/settings/layout.tsx new file mode 100644 index 00000000..6f373567 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/settings/layout.tsx @@ -0,0 +1,68 @@ +import { Metadata } from "next" + +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" + +export const metadata: Metadata = { + title: "Settings", + // description: "Advanced form example using react-hook-form and Zod.", +} + + +interface SettingsLayoutProps { + children: React.ReactNode + params: { lng: string } +} + +export default async function SettingsLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string } +}) { + const resolvedParams = await params + const lng = resolvedParams.lng + + + const sidebarNavItems = [ + + { + title: "Account", + href: `/${lng}/evcp/settings`, + }, + { + title: "Preferences", + href: `/${lng}/evcp/settings/preferences`, + } + + + ] + + + return ( + <> +
+
+
+
+

Settings

+

+ Manage your account settings and preferences. +

+
+ +
+ +
{children}
+
+
+
+
+ + + + ) +} diff --git a/app/[lng]/evcp/(evcp)/settings/page.tsx b/app/[lng]/evcp/(evcp)/settings/page.tsx new file mode 100644 index 00000000..a6eaac90 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/settings/page.tsx @@ -0,0 +1,18 @@ +import { Separator } from "@/components/ui/separator" +import { AccountForm } from "@/components/settings/account-form" + +export default function SettingsAccountPage() { + return ( +
+
+

Account

+

+ Update your account settings. Set your preferred language and + timezone. +

+
+ + +
+ ) +} diff --git a/app/[lng]/evcp/(evcp)/settings/preferences/page.tsx b/app/[lng]/evcp/(evcp)/settings/preferences/page.tsx new file mode 100644 index 00000000..e2a88021 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/settings/preferences/page.tsx @@ -0,0 +1,17 @@ +import { Separator } from "@/components/ui/separator" +import { AppearanceForm } from "@/components/settings/appearance-form" + +export default function SettingsAppearancePage() { + return ( +
+
+

Preference

+

+ Customize the preference of the app. +

+
+ + +
+ ) +} diff --git a/app/[lng]/evcp/(evcp)/system/admin-users/page.tsx b/app/[lng]/evcp/(evcp)/system/admin-users/page.tsx new file mode 100644 index 00000000..11a9e9fb --- /dev/null +++ b/app/[lng]/evcp/(evcp)/system/admin-users/page.tsx @@ -0,0 +1,60 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { DateRangePicker } from "@/components/date-range-picker" +import { Separator } from "@/components/ui/separator" + +import { searchParamsCache } from "@/lib/admin-users/validations" +import { getAllCompanies, getAllRoles, getUserCountGroupByCompany, getUserCountGroupByRole, getUsers } from "@/lib/admin-users/service" +import { AdmUserTable } from "@/lib/admin-users/table/ausers-table" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function UserTable(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getUsers({ + ...search, + filters: validFilters, + }), + getUserCountGroupByCompany(), + getUserCountGroupByRole(), + getAllCompanies(), + getAllRoles() + ]) + + return ( + + } + > +
+
+

Vendor Admin User Management

+

+ 협력업체의 유저 전체를 조회하고 어드민 유저를 생성할 수 있는 페이지입니다. 이곳에서 초기 유저를 생성시킬 수 있습니다.
생성 후에는 생성된 사용자의 이메일로 생성 통보 이메일이 발송되며 사용자는 이메일과 OTP로 로그인이 가능합니다. +

+
+ + +
+
+ + ) +} diff --git a/app/[lng]/evcp/(evcp)/system/layout.tsx b/app/[lng]/evcp/(evcp)/system/layout.tsx new file mode 100644 index 00000000..62f3e845 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/system/layout.tsx @@ -0,0 +1,75 @@ +import { Metadata } from "next" + +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" + +export const metadata: Metadata = { + title: "System Setting", + // description: "Advanced form example using react-hook-form and Zod.", +} + + +interface SettingsLayoutProps { + children: React.ReactNode + params: { lng: string } +} + +export default async function SettingsLayout({ + children, + params, +}: { + children: React.ReactNode + params: { lng: string } +}) { + const resolvedParams = await params + const lng = resolvedParams.lng + + + const sidebarNavItems = [ + + { + title: "SHI Users", + href: `/${lng}/evcp/system`, + }, + { + title: "Roles", + href: `/${lng}/evcp/system/roles`, + }, + { + title: "Permissions", + href: `/${lng}/evcp/system/permissions`, + }, + { + title: "Vendor Users", + href: `/${lng}/evcp/system/admin-users`, + }, + + ] + + + return ( + <> +
+
+
+
+

시스템 설정

+

+ 사용자, 롤, 접근 권한을 관리하세요. +

+
+ +
+ +
{children}
+
+
+
+
+ + + + ) +} diff --git a/app/[lng]/evcp/(evcp)/system/page.tsx b/app/[lng]/evcp/(evcp)/system/page.tsx new file mode 100644 index 00000000..fe0a262c --- /dev/null +++ b/app/[lng]/evcp/(evcp)/system/page.tsx @@ -0,0 +1,56 @@ +import { Separator } from "@/components/ui/separator" +import { type SearchParams } from "@/types/table" +import * as React from "react" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsCache } from "@/lib/admin-users/validations" +import { getAllRoles, getUsersEVCP } from "@/lib/users/service" +import { getUserCountGroupByRole } from "@/lib/admin-users/service" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { UserTable } from "@/lib/users/table/users-table" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function SystemUserPage(props: IndexPageProps) { + + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getUsersEVCP({ + ...search, + filters: validFilters, + }), + getUserCountGroupByRole(), + getAllRoles() + ]) + + return ( + + } + > +
+
+

SHI Users

+

+ 시스템 전체 사용자들을 조회하고 관리할 수 있는 페이지입니다. 사용자에게 롤을 할당하는 것으로 메뉴별 권한을 관리할 수 있습니다. +

+
+ + +
+
+ + ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/system/permissions/page.tsx b/app/[lng]/evcp/(evcp)/system/permissions/page.tsx new file mode 100644 index 00000000..6aa2b693 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/system/permissions/page.tsx @@ -0,0 +1,17 @@ +import PermissionsTree from "@/components/system/permissionsTree" +import { Separator } from "@/components/ui/separator" + +export default function PermissionsPage() { + return ( +
+
+

Permissions

+

+ Set permissions to the menu by Role +

+
+ + +
+ ) +} diff --git a/app/[lng]/evcp/(evcp)/system/roles/page.tsx b/app/[lng]/evcp/(evcp)/system/roles/page.tsx new file mode 100644 index 00000000..fe074600 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/system/roles/page.tsx @@ -0,0 +1,68 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Separator } from "@/components/ui/separator" + +import { searchParamsCache } from "@/lib/roles/validations" +import { searchParamsCache as searchParamsCache2 } from "@/lib/admin-users/validations" +import { RolesTable } from "@/lib/roles/table/roles-table" +import { getRolesWithCount } from "@/lib/roles/services" +import { getUsersAll } from "@/lib/users/service" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function UserTable(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + const search2 = searchParamsCache2.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getRolesWithCount({ + ...search, + filters: validFilters, + }), + + + ]) + + + const promises2 = Promise.all([ + getUsersAll({ + ...search2, + filters: validFilters, + }, "evcp"), + ]) + + + return ( + + } + > +
+
+

Role Management

+

+ 역할을 생성하고 역할에 유저를 할당할 수 있는 페이지입니다. 역할에 메뉴의 접근 권한 역시 할당할 수 있습니다. +

+
+ + +
+
+ + ) +} diff --git a/app/[lng]/evcp/(evcp)/tag-numbering/page.tsx b/app/[lng]/evcp/(evcp)/tag-numbering/page.tsx new file mode 100644 index 00000000..9d5b903a --- /dev/null +++ b/app/[lng]/evcp/(evcp)/tag-numbering/page.tsx @@ -0,0 +1,74 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsCache } from "@/lib/tag-numbering/validation" +import { getTagNumbering } from "@/lib/tag-numbering/service" +import { TagNumberingTable } from "@/lib/tag-numbering/table/tagNumbering-table" + + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTagNumbering({ + ...search, + filters: validFilters, + }), + + ]) + + return ( + +
+
+
+

+ Tag Numbering from S-EDP +

+

+ 태그 넘버링을 위한 룰셋을 S-EDP로부터 가져오고 확인할 수 있습니다{" "} + {/* + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} +

+
+
+
+ + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/evcp/(evcp)/tasks/page.tsx b/app/[lng]/evcp/(evcp)/tasks/page.tsx new file mode 100644 index 00000000..f14cc757 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/tasks/page.tsx @@ -0,0 +1,63 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { DateRangePicker } from "@/components/date-range-picker" +import { Shell } from "@/components/shell" + +import { FeatureFlagsProvider } from "@/lib/tasks/table/feature-flags-provider" +import { TasksTable } from "@/lib/tasks/table/tasks-table" +import { + getTaskPriorityCounts, + getTasks, + getTaskStatusCounts, +} from "@/lib/tasks/service" +import { searchParamsCache } from "@/lib/tasks/validations" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTasks({ + ...search, + filters: validFilters, + }), + getTaskStatusCounts(), + getTaskPriorityCounts(), + ]) + + return ( + + }> + {/* */} + + + } + > + + + + ) +} diff --git a/app/[lng]/evcp/(evcp)/tbe/page.tsx b/app/[lng]/evcp/(evcp)/tbe/page.tsx new file mode 100644 index 00000000..2461ed42 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/tbe/page.tsx @@ -0,0 +1,113 @@ +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getAllTBE } from "@/lib/rfqs/service" +import { searchParamsTBECache } from "@/lib/rfqs/validations" +import { AllTbeTable } from "@/lib/tbe/table/tbe-table" +import { RfqType } from "@/lib/rfqs/validations" +import * as React from "react" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" + +interface IndexPageProps { + params: { + lng: string + } + searchParams: Promise +} + +// 타입별 페이지 설명 구성 (Budgetary 제외) +const typeConfig: Record = { + "purchase": { + title: "Purchase RFQ Technical Bid Evaluation", + description: "실제 구매 발주 전 가격 요청을 위한 TBE입니다.", + rfqType: RfqType.PURCHASE + }, + "purchase-budgetary": { + title: "Purchase Budgetary RFQ Technical Bid Evaluation", + description: "프로젝트 수주 후, 공식 입찰 전 예산 책정을 위한 TBE입니다.", + rfqType: RfqType.PURCHASE_BUDGETARY + } +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + + // URL 쿼리 파라미터에서 타입 추출 + const searchParams = await props.searchParams + // 기본값으로 'purchase' 사용 + const typeParam = searchParams?.type as string || 'purchase' + + // 유효한 타입인지 확인하고 기본값 설정 + const validType = Object.keys(typeConfig).includes(typeParam) ? typeParam : 'purchase' + const rfqType = typeConfig[validType].rfqType + + // SearchParams 파싱 (Zod) + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // 현재 선택된 타입의 데이터 로드 + const promises = Promise.all([ + getAllTBE({ + ...search, + filters: validFilters, + rfqType + }) + ]) + + // 페이지 경로 생성 함수 - 단순화 + const getTabUrl = (type: string) => { + return `/${lng}/evcp/tbe?type=${type}`; + } + + return ( + +
+
+
+

+ Technical Bid Evaluation +

+

+ 초대된 벤더에게 TBE를 보낼 수 있습니다.
+ 체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다. +

+
+
+
+ + {/* 타입 선택 탭 (Budgetary 제외) */} + + + + Purchase + + + Purchase Budgetary + + + +
+

+ {typeConfig[validType].description} +

+
+
+ + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/vendor-candidates/page.tsx b/app/[lng]/evcp/(evcp)/vendor-candidates/page.tsx new file mode 100644 index 00000000..668c0dc6 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/vendor-candidates/page.tsx @@ -0,0 +1,66 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { getVendorCandidateCounts, getVendorCandidates } from "@/lib/vendor-candidates/service" +import { searchParamsCandidateCache } from "@/lib/vendor-candidates/validations" +import { VendorCandidateTable } from "@/lib/vendor-candidates/table/candidates-table" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCandidateCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getVendorCandidates({ + ...search, + filters: validFilters, + }), + getVendorCandidateCounts() + ]) + + return ( + + +
+
+
+

+ Vendor Candidates Management +

+

+ 수집한 벤더 후보를 등록하고 초대 메일을 송부할 수 있습니다. + +

+
+
+
+ + + }> + + + } + > + + +
+ ) +} diff --git a/app/[lng]/evcp/(evcp)/vendor-investigation/page.tsx b/app/[lng]/evcp/(evcp)/vendor-investigation/page.tsx new file mode 100644 index 00000000..c59de869 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/vendor-investigation/page.tsx @@ -0,0 +1,65 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +import { VendorsInvestigationTable } from "@/lib/vendor-investigation/table/investigation-table" +import { getVendorsInvestigation } from "@/lib/vendor-investigation/service" +import { searchParamsInvestigationCache } from "@/lib/vendor-investigation/validations" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsInvestigationCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getVendorsInvestigation({ + ...search, + filters: validFilters, + }), + ]) + + return ( + + +
+
+
+

+ Vendor Investigation Management +

+

+ 요청된 Vendor 실사에 대한 스케줄 정보를 관리하고 결과를 입력할 수 있습니다. + +

+
+
+
+ + + }> + + + } + > + + +
+ ) +} diff --git a/app/[lng]/evcp/(evcp)/vendors/[id]/info/items/page.tsx b/app/[lng]/evcp/(evcp)/vendors/[id]/info/items/page.tsx new file mode 100644 index 00000000..e9ff17b4 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/vendors/[id]/info/items/page.tsx @@ -0,0 +1,56 @@ +import { Separator } from "@/components/ui/separator" +import { getVendorItems } from "@/lib/vendors/service" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsItemCache } from "@/lib/vendors/validations" +import { VendorItemsTable } from "@/lib/vendors/items-table/item-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function SettingsAccountPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsItemCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + + + const promises = Promise.all([ + getVendorItems({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + // 4) 렌더링 + return ( +
+
+

+ Possible Items +

+

+ 딜리버리가 가능한 아이템 리스트를 확인할 수 있습니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/vendors/[id]/info/layout.tsx b/app/[lng]/evcp/(evcp)/vendors/[id]/info/layout.tsx new file mode 100644 index 00000000..39e0bac0 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/vendors/[id]/info/layout.tsx @@ -0,0 +1,79 @@ +import { Metadata } from "next" + +import { Separator } from "@/components/ui/separator" +import { SidebarNav } from "@/components/layout/sidebar-nav" +import { findVendorById } from "@/lib/vendors/service" // 가정: 여기에 findVendorById가 있다고 가정 +import { Vendor } from "@/db/schema/vendors" + +export const metadata: Metadata = { + title: "Vendor 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 vendor: Vendor | null = await findVendorById(idAsNumber) + + // 3) 사이드바 메뉴 + const sidebarNavItems = [ + { + title: "Contacts", + href: `/${lng}/evcp/vendors/${id}/info`, + }, + { + title: "Items", + href: `/${lng}/evcp/vendors/${id}/info/items`, + }, + { + title: "RFQ History", + href: `/${lng}/evcp/vendors/${id}/info/rfq-history`, + }, + { + title: "Bidding History", + href: `/${lng}/evcp/vendors/${id}/info/bid-history`, + }, + { + title: "Contract History", + href: `/${lng}/evcp/vendors/${id}/info/contract-history`, + }, + ] + + return ( + <> +
+
+
+
+ {/* 4) 벤더 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} +

+ {vendor + ? `${vendor.vendorCode ?? ""} - ${vendor.vendorName} 상세 정보` + : "Loading Vendor..."} +

+

벤더 관련 상세사항을 확인하세요.

+
+ +
+ +
{children}
+
+
+
+
+ + ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/vendors/[id]/info/page.tsx b/app/[lng]/evcp/(evcp)/vendors/[id]/info/page.tsx new file mode 100644 index 00000000..6279e924 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/vendors/[id]/info/page.tsx @@ -0,0 +1,56 @@ +import { Separator } from "@/components/ui/separator" +import { getVendorContacts } from "@/lib/vendors/service" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsContactCache } from "@/lib/vendors/validations" +import { VendorContactsTable } from "@/lib/vendors/contacts-table/contact-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function SettingsAccountPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsContactCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + + + const promises = Promise.all([ + getVendorContacts({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + // 4) 렌더링 + return ( +
+
+

+ Contacts +

+

+ 업무별 담당자 정보를 확인하세요. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/vendors/[id]/info/rfq-history/page.tsx b/app/[lng]/evcp/(evcp)/vendors/[id]/info/rfq-history/page.tsx new file mode 100644 index 00000000..1d2f618c --- /dev/null +++ b/app/[lng]/evcp/(evcp)/vendors/[id]/info/rfq-history/page.tsx @@ -0,0 +1,55 @@ +import { Separator } from "@/components/ui/separator" +import { getRfqHistory } from "@/lib/vendors/service" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { searchParamsRfqHistoryCache } from "@/lib/vendors/validations" +import { VendorRfqHistoryTable } from "@/lib/vendors/rfq-history-table/rfq-history-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise +} + +export default async function RfqHistoryPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + const id = resolvedParams.id + + const idAsNumber = Number(id) + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsRfqHistoryCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getRfqHistory({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + // 4) 렌더링 + return ( +
+
+

+ RFQ History +

+

+ 벤더의 RFQ 참여 이력을 확인할 수 있습니다. +

+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/vendors/page.tsx b/app/[lng]/evcp/(evcp)/vendors/page.tsx new file mode 100644 index 00000000..e3cc7fdc --- /dev/null +++ b/app/[lng]/evcp/(evcp)/vendors/page.tsx @@ -0,0 +1,78 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + + +import { searchParamsCache } from "@/lib/vendors/validations" +import { getVendors, getVendorStatusCounts } from "@/lib/vendors/service" +import { VendorsTable } from "@/lib/vendors/table/vendors-table" +import { Ellipsis } from "lucide-react" + +interface IndexPageProps { + searchParams: Promise +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getVendors({ + ...search, + filters: validFilters, + }), + getVendorStatusCounts(), + ]) + + return ( + + +
+
+
+

+ Vendor Information +

+

+ 벤더에 대한 요약 정보를 확인하고{" "} + + + 버튼 + + 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다.
벤더의 상태에 따라 가입을 승인해주거나 PQ 요청을 할 수 있고 검토가 완료된 벤더를 기간계 시스템에 전송하여 벤더 코드를 따올 수 있습니다. +

+
+
+
+ + + }> + {/* */} + + + } + > + + +
+ ) +} diff --git a/app/[lng]/evcp/bqtbe/page.tsx b/app/[lng]/evcp/bqtbe/page.tsx deleted file mode 100644 index 655bd30a..00000000 --- a/app/[lng]/evcp/bqtbe/page.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getAllTBE } from "@/lib/rfqs/service" -import { searchParamsTBECache } from "@/lib/rfqs/validations" -import { AllTbeTable } from "@/lib/tbe/table/tbe-table" -import { RfqType } from "@/lib/rfqs/validations" -import * as React from "react" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - } - searchParams: Promise - rfqType: RfqType -} - -export default async function RfqTBEPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - - const rfqType = props.rfqType ?? RfqType.BUDGETARY // rfqType이 없으면 BUDGETARY로 설정 - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsTBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getAllTBE({ - ...search, - filters: validFilters, - rfqType - } - ) - ]) - - // 4) 렌더링 - return ( - -
-
-
-

- Technical Bid Evaluation -

-

- 초대된 벤더에게 TBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다. -

-
-
-
- - - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/evcp/budgetary/[id]/cbe/page.tsx b/app/[lng]/evcp/budgetary/[id]/cbe/page.tsx deleted file mode 100644 index 9a4ae7eb..00000000 --- a/app/[lng]/evcp/budgetary/[id]/cbe/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getCBE, getTBE } from "@/lib/rfqs/service" -import { searchParamsCBECache, } from "@/lib/rfqs/validations" -import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" -import { CbeTable } from "@/lib/rfqs/cbe-table/cbe-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise -} - -export default async function RfqTBEPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsCBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getCBE({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - // 4) 렌더링 - return ( -
-
-

- Commercial Bid Evaluation -

-

- 초대된 벤더에게 CBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다. -

-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/evcp/budgetary/[id]/layout.tsx b/app/[lng]/evcp/budgetary/[id]/layout.tsx deleted file mode 100644 index 39f045e5..00000000 --- a/app/[lng]/evcp/budgetary/[id]/layout.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Metadata } from "next" - -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" -import { findVendorById } from "@/lib/vendors/service" // 가정: 여기에 findVendorById가 있다고 가정 -import { Rfq, RfqWithItems } from "@/db/schema/rfq" -import { findRfqById } from "@/lib/rfqs/service" -import { formatDate } from "@/lib/utils" - -export const metadata: Metadata = { - title: "Vendor Detail", -} - -export default async function RfqLayout({ - 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 rfq: RfqWithItems | null = await findRfqById(idAsNumber) - - // 3) 사이드바 메뉴 - const sidebarNavItems = [ - { - title: "Matched Vendors", - href: `/${lng}/evcp/budgetary/${id}`, - }, - { - title: "TBE", - href: `/${lng}/evcp/budgetary/${id}/tbe`, - }, - { - title: "CBE", - href: `/${lng}/evcp/budgetary/${id}/cbe`, - }, - - ] - - return ( - <> -
-
-
-
- {/* 4) 벤더 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} -

- {rfq - ? `${rfq.rfqCode ?? ""} 관리` - : "Loading RFQ..."} -

- -

- {rfq - ? `${rfq.description ?? ""} ${rfq.lines.map(line => line.itemCode).join(", ")}` - : ""} -

-

Due Date:{ rfq && {formatDate(rfq?.dueDate)}}

-
- -
- -
{children}
-
-
-
-
- - ) -} \ No newline at end of file diff --git a/app/[lng]/evcp/budgetary/[id]/page.tsx b/app/[lng]/evcp/budgetary/[id]/page.tsx deleted file mode 100644 index f6160574..00000000 --- a/app/[lng]/evcp/budgetary/[id]/page.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getMatchedVendors } from "@/lib/rfqs/service" -import { searchParamsMatchedVCache } from "@/lib/rfqs/validations" -import { MatchedVendorsTable } from "@/lib/rfqs/vendor-table/vendors-table" -import { RfqType } from "@/lib/rfqs/validations" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise - rfqType: RfqType -} - -export default async function RfqPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - const rfqType = props.rfqType ?? RfqType.BUDGETARY // rfqType이 없으면 BUDGETARY로 설정 - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - const searchParams = await props.searchParams - const search = searchParamsMatchedVCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getMatchedVendors({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - // 4) 렌더링 - return ( -
-
-

- Vendors -

-

- 등록된 벤더 중에서 이 RFQ 아이템에 매칭되는 업체를 보여줍니다.
"발행하기" 버튼을 통해 RFQ를 전송하면 첨부파일과 함께 RFQ 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다. -

-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/evcp/budgetary/[id]/tbe/page.tsx b/app/[lng]/evcp/budgetary/[id]/tbe/page.tsx deleted file mode 100644 index a6259696..00000000 --- a/app/[lng]/evcp/budgetary/[id]/tbe/page.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getTBE } from "@/lib/rfqs/service" -import { searchParamsTBECache } from "@/lib/rfqs/validations" -import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise -} - -export default async function RfqTBEPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsTBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getTBE({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - // 4) 렌더링 - return ( -
-
-

- Technical Bid Evaluation -

-

- 초대된 벤더에게 TBE를 보낼 수 있습니다.
체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다. -

-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/evcp/budgetary/page.tsx b/app/[lng]/evcp/budgetary/page.tsx deleted file mode 100644 index 04550353..00000000 --- a/app/[lng]/evcp/budgetary/page.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" - -import { searchParamsCache } from "@/lib/rfqs/validations" -import { getRfqs, getRfqStatusCounts } from "@/lib/rfqs/service" -import { RfqsTable } from "@/lib/rfqs/table/rfqs-table" -import { getAllItems } from "@/lib/items/service" -import { RfqType } from "@/lib/rfqs/validations" -import { Ellipsis } from "lucide-react" - -interface RfqPageProps { - searchParams: Promise; - rfqType: RfqType; - title: string; - description: string; -} - -export default async function RfqPage({ - searchParams, - rfqType = RfqType.BUDGETARY, - title = "Budgetary Quote", - description = "Budgetary Quote를 등록하여 요청 및 응답을 관리할 수 있습니다." -}: RfqPageProps) { - const search = searchParamsCache.parse(await searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getRfqs({ - ...search, - filters: validFilters, - rfqType // 전달받은 rfqType 사용 - }), - getRfqStatusCounts(rfqType), // rfqType 전달 - getAllItems() - ]) - - return ( - -
-
-
-

- {title} -

-

- {description} - 기본적인 정보와 RFQ를 위한 아이템 등록 및 첨부를 한 후, - - - 버튼 - 을 클릭하면 "Proceed"를 통해 상세화면으로 이동하여 진행할 수 있습니다. -

-
-
-
- - }> - {/* */} - - - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/evcp/equip-class/page.tsx b/app/[lng]/evcp/equip-class/page.tsx deleted file mode 100644 index 375eb69e..00000000 --- a/app/[lng]/evcp/equip-class/page.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { searchParamsCache } from "@/lib/equip-class/validation" -import { FormListsTable } from "@/lib/form-list/table/formLists-table" -import { getTagClassists } from "@/lib/equip-class/service" -import { EquipClassTable } from "@/lib/equip-class/table/equipClass-table" - - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getTagClassists({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - -
-
-
-

- Object Class List from S-EDP -

-

- Object Class List를 확인할 수 있습니다.{" "} - {/* - - 버튼 - - 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} -

-
-
-
- - }> - {/* */} - - - } - > - - -
- ) -} diff --git a/app/[lng]/evcp/faq/manage/actions.ts b/app/[lng]/evcp/faq/manage/actions.ts deleted file mode 100644 index bc443a8a..00000000 --- a/app/[lng]/evcp/faq/manage/actions.ts +++ /dev/null @@ -1,48 +0,0 @@ -'use server'; - -import { promises as fs } from 'fs'; -import path from 'path'; -import { FaqCategory } from '@/components/faq/FaqCard'; -import { fallbackLng } from '@/i18n/settings'; - -const FAQ_CONFIG_PATH = path.join(process.cwd(), 'config', 'faqDataConfig.ts'); - -export async function updateFaqData(lng: string, newData: FaqCategory[]) { - try { - const fileContent = await fs.readFile(FAQ_CONFIG_PATH, 'utf-8'); - const dataMatch = fileContent.match(/export const faqCategories[^=]*=\s*(\{[\s\S]*\});/); - if (!dataMatch) { - throw new Error('FAQ 데이터 형식이 올바르지 않습니다.'); - } - - const allData = eval(`(${dataMatch[1]})`); - const updatedData = { - ...allData, - [lng]: newData - }; - - const newFileContent = `import { FaqCategory } from "@/components/faq/FaqCard";\n\ninterface LocalizedFaqCategories {\n [lng: string]: FaqCategory[];\n}\n\nexport const faqCategories: LocalizedFaqCategories = ${JSON.stringify(updatedData, null, 4)};`; - await fs.writeFile(FAQ_CONFIG_PATH, newFileContent, 'utf-8'); - - return { success: true }; - } catch (error) { - console.error('FAQ 데이터 업데이트 중 오류 발생:', error); - return { success: false, error: '데이터 업데이트 중 오류가 발생했습니다.' }; - } -} - -export async function getFaqData(lng: string): Promise<{ data: FaqCategory[] }> { - try { - const fileContent = await fs.readFile(FAQ_CONFIG_PATH, 'utf-8'); - const dataMatch = fileContent.match(/export const faqCategories[^=]*=\s*(\{[\s\S]*\});/); - if (!dataMatch) { - throw new Error('FAQ 데이터 형식이 올바르지 않습니다.'); - } - - const allData = eval(`(${dataMatch[1]})`); - return { data: allData[lng] || allData[fallbackLng] || [] }; - } catch (error) { - console.error('FAQ 데이터 읽기 중 오류 발생:', error); - return { data: [] }; - } -} \ No newline at end of file diff --git a/app/[lng]/evcp/faq/manage/page.tsx b/app/[lng]/evcp/faq/manage/page.tsx deleted file mode 100644 index 011bbfa4..00000000 --- a/app/[lng]/evcp/faq/manage/page.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { FaqManager } from '@/components/faq/FaqManager'; -import { getFaqData, updateFaqData } from './actions'; -import { revalidatePath } from 'next/cache'; -import { FaqCategory } from '@/components/faq/FaqCard'; - -interface Props { - params: { - lng: string; - } -} - -export default async function FaqManagePage(props: Props) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const { data } = await getFaqData(lng); - - async function handleSave(newData: FaqCategory[]) { - 'use server'; - await updateFaqData(lng, newData); - revalidatePath(`/${lng}/evcp/faq`); - } - - return ( -
-
-
-
-

FAQ Management

-

- Manage FAQ categories and items for {lng.toUpperCase()} language. -

-
- -
-
-
- ); -} \ No newline at end of file diff --git a/app/[lng]/evcp/faq/page.tsx b/app/[lng]/evcp/faq/page.tsx deleted file mode 100644 index 9b62b7e4..00000000 --- a/app/[lng]/evcp/faq/page.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { faqCategories } from "@/config/faqDataConfig" -import { FaqCard } from "@/components/faq/FaqCard" -import { Button } from "@/components/ui/button" -import { Settings } from "lucide-react" -import Link from "next/link" -import { fallbackLng } from "@/i18n/settings" - -interface Props { - params: { - lng: string; - } -} - -export default async function FaqPage(props: Props) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const localizedFaqCategories = faqCategories[lng] || faqCategories[fallbackLng]; - - return ( -
-
-
-
-
-

Frequently Asked Questions

-

- Find answers to common questions about using the EVCP system. -

-
- - - -
- - - - - {localizedFaqCategories.map((category) => ( - - {category.label} - - ))} - - - {localizedFaqCategories.map((category) => ( - - {category.items.map((item, index) => ( - - ))} - - ))} - -
-
-
- ) -} diff --git a/app/[lng]/evcp/form-list/page.tsx b/app/[lng]/evcp/form-list/page.tsx deleted file mode 100644 index f96917d6..00000000 --- a/app/[lng]/evcp/form-list/page.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { searchParamsCache } from "@/lib/form-list/validation" -import { ItemsTable } from "@/lib/items/table/items-table" -import { getFormLists } from "@/lib/form-list/service" -import { FormListsTable } from "@/lib/form-list/table/formLists-table" - - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getFormLists({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - -
-
-
-

- Form List from S-EDP -

-

- 벤더 데이터 입력을 위한 Form 리스트입니다.{" "} - {/* - - 버튼 - - 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} -

-
-
-
- - }> - {/* */} - - - } - > - - -
- ) -} diff --git a/app/[lng]/evcp/items/page.tsx b/app/[lng]/evcp/items/page.tsx deleted file mode 100644 index 144689ff..00000000 --- a/app/[lng]/evcp/items/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { searchParamsCache } from "@/lib/items/validations" -import { getItems } from "@/lib/items/service" -import { ItemsTable } from "@/lib/items/table/items-table" - - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getItems({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - -
-
-
-

- Package Items -

-

- Item을 등록하고 관리할 수 있습니다.{" "} - {/* - - 버튼 - - 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} -

-
-
-
- - }> - {/* */} - - - } - > - - -
- ) -} diff --git a/app/[lng]/evcp/layout.tsx b/app/[lng]/evcp/layout.tsx deleted file mode 100644 index 9dc39f7b..00000000 --- a/app/[lng]/evcp/layout.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { ReactNode } from 'react'; -import { Header } from '@/components/layout/Header'; -import { SiteFooter } from '@/components/layout/Footer'; - -export default function EvcpLayout({ children }: { children: ReactNode }) { - return ( -
-
-
-
- {children} -
-
- -
- ); -} \ No newline at end of file diff --git a/app/[lng]/evcp/page.tsx b/app/[lng]/evcp/page.tsx index a1e9f8be..f9662cb7 100644 --- a/app/[lng]/evcp/page.tsx +++ b/app/[lng]/evcp/page.tsx @@ -1,8 +1,21 @@ +import { Metadata } from "next" +import { Suspense } from "react" +import { LoginFormSkeleton } from "@/components/login/login-form-skeleton" +import { LoginFormSHI } from "@/components/login/login-form-shi" -export default function Pages() { - return ( - <> - test - - ) - } \ No newline at end of file +export const metadata: Metadata = { + title: "eVCP Portal", + description: "", +} + +export default function AuthenticationPage() { + + + return ( + <> + }> + + + + ) +} diff --git a/app/[lng]/evcp/po/page.tsx b/app/[lng]/evcp/po/page.tsx deleted file mode 100644 index fa528df0..00000000 --- a/app/[lng]/evcp/po/page.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { getPOs } from "@/lib/po/service" -import { searchParamsCache } from "@/lib/po/validations" -import { PoListsTable } from "@/lib/po/table/po-table" - - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getPOs({ - ...search, - filters: validFilters, - }), - ]) - - return ( - - -
-
-
-

- PO 확인 및 전자서명 -

-

- 기간계 시스템으로부터 PO를 확인하고 벤더에게 전자서명을 요청할 수 있습니다. 요쳥된 전자서명의 이력 또한 확인할 수 있습니다. - -

-
-
-
- - - }> - - - } - > - - -
- ) -} diff --git a/app/[lng]/evcp/poa/page.tsx b/app/[lng]/evcp/poa/page.tsx deleted file mode 100644 index dec5e05b..00000000 --- a/app/[lng]/evcp/poa/page.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { getChangeOrders } from "@/lib/poa/service" -import { searchParamsCache } from "@/lib/poa/validations" -import { ChangeOrderListsTable } from "@/lib/poa/table/poa-table" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getChangeOrders({ - ...search, - filters: validFilters, - }), - ]) - - return ( - -
-
-
-

- 변경 PO 확인 및 전자서명 -

-

- 발행된 PO의 변경 내역을 확인하고 관리할 수 있습니다. -

-
-
-
- - }> - - - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/evcp/pq-criteria/[id]/page.tsx b/app/[lng]/evcp/pq-criteria/[id]/page.tsx deleted file mode 100644 index f040a0ca..00000000 --- a/app/[lng]/evcp/pq-criteria/[id]/page.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { searchParamsCache } from "@/lib/pq/validations" -import { getPQs } from "@/lib/pq/service" -import { PqsTable } from "@/lib/pq/table/pq-table" -import { ProjectSelectorWrapper } from "@/components/pq/project-select-wrapper" -import { notFound } from "next/navigation" - -interface ProjectPageProps { - params: { id: string } - searchParams: Promise -} - -export default async function ProjectPage(props: ProjectPageProps) { - const resolvedParams = await props.params - const id = resolvedParams.id - - const projectId = parseInt(id, 10) - - // 유효하지 않은 projectId 확인 - if (isNaN(projectId)) { - notFound() - } - - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - // filters가 없는 경우를 처리 - const validFilters = getValidFilters(search.filters) - - // 프로젝트별 PQ 데이터 가져오기 - const promises = Promise.all([ - getPQs({ - ...search, - filters: validFilters, - }, projectId, false) - ]) - - return ( - -
-
-

- Pre-Qualification Check Sheet -

-

- 벤더 등록을 위한, 벤더가 제출할 PQ 항목을: 프로젝트별로 관리할 수 있습니다. -

-
- -
- - }> - {/* */} - - - - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/evcp/pq-criteria/page.tsx b/app/[lng]/evcp/pq-criteria/page.tsx deleted file mode 100644 index 778baa93..00000000 --- a/app/[lng]/evcp/pq-criteria/page.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { searchParamsCache } from "@/lib/pq/validations" -import { getPQs } from "@/lib/pq/service" -import { PqsTable } from "@/lib/pq/table/pq-table" -import { ProjectSelectorWrapper } from "@/components/pq/project-select-wrapper" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - // filters가 없는 경우를 처리 - - const validFilters = getValidFilters(search.filters) - - // onlyGeneral: true로 설정하여 일반 PQ 항목만 가져옴 - const promises = Promise.all([ - getPQs({ - ...search, - filters: validFilters, - }, null, true) - ]) - - return ( - -
-
-

- Pre-Qualification Check Sheet -

-

- 벤더 등록을 위한, 벤더가 제출할 PQ 항목을 관리할 수 있습니다. -

-
- -
- - }> - {/* */} - - - - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/evcp/pq/[vendorId]/page.tsx b/app/[lng]/evcp/pq/[vendorId]/page.tsx deleted file mode 100644 index 97c9a29a..00000000 --- a/app/[lng]/evcp/pq/[vendorId]/page.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import * as React from "react" -import { Shell } from "@/components/shell" -import { Skeleton } from "@/components/ui/skeleton" -import { type SearchParams } from "@/types/table" -import { getPQDataByVendorId, getVendorPQsList } from "@/lib/pq/service" -import { Vendor } from "@/db/schema/vendors" -import { findVendorById } from "@/lib/vendors/service" -import VendorPQAdminReview from "@/components/pq/pq-review-detail" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { Badge } from "@/components/ui/badge" - -interface IndexPageProps { - params: { - vendorId: string - } - searchParams: Promise -} - -export default async function PQReviewPage(props: IndexPageProps) { - const resolvedParams = await props.params - const vendorId = Number(resolvedParams.vendorId) - - // Fetch the vendor data - const vendor: Vendor | null = await findVendorById(vendorId) - if (!vendor) return
Vendor not found
- - // Get list of all PQs (general + project-specific) for this vendor - const pqsList = await getVendorPQsList(vendorId) - - // Determine default active PQ to display - // If query param projectId exists, use that, otherwise use general PQ if available - const searchParams = await props.searchParams - const activeProjectId = searchParams.projectId ? Number(searchParams.projectId) : undefined - - // If no projectId query param, default to general PQ or first project PQ - const defaultTabId = activeProjectId ? - `project-${activeProjectId}` : - (pqsList.hasGeneralPq ? 'general' : `project-${pqsList.projectPQs[0]?.projectId}`) - - // Fetch PQ data for the active tab - let pqData; - if (activeProjectId) { - // Get project-specific PQ data - pqData = await getPQDataByVendorId(vendorId, activeProjectId) - } else { - // Get general PQ data - pqData = await getPQDataByVendorId(vendorId) - } - - return ( - - {pqsList.hasGeneralPq || pqsList.projectPQs.length > 0 ? ( - -
-

- {vendor.vendorName} PQ Review -

- - - {pqsList.hasGeneralPq && ( - - General PQ Standard - - )} - - {pqsList.projectPQs.map((project) => ( - - {project.projectName} {project.status} - - ))} - -
- - {/* Tab content for General PQ */} - {pqsList.hasGeneralPq && ( - - getPQDataByVendorId(vendorId)} - pqType="general" - /> - - )} - - {/* Tab content for each Project PQ */} - {pqsList.projectPQs.map((project) => ( - - getPQDataByVendorId(vendorId, project.projectId)} - pqType="project" - /> - - ))} -
- ) : ( -
-

No PQ submissions found for this vendor

-
- )} -
- ) -} \ No newline at end of file diff --git a/app/[lng]/evcp/pq/page.tsx b/app/[lng]/evcp/pq/page.tsx deleted file mode 100644 index 46b22b12..00000000 --- a/app/[lng]/evcp/pq/page.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" - -import { getVendorsInPQ } from "@/lib/pq/service" -import { searchParamsCache } from "@/lib/vendors/validations" -import { VendorsPQReviewTable } from "@/lib/pq/pq-review-table/vendors-table" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getVendorsInPQ({ - ...search, - filters: validFilters, - }), - ]) - - return ( - - -
-
-
-

- Pre-Qualification Review -

-

- 벤더가 제출한 PQ를 확인하고 수정 요청 등을 할 수 있으며 PQ 종료 후에는 통과 여부를 결정할 수 있습니다. - -

-
-
-
- - - }> - {/* */} - - - } - > - - -
- ) -} diff --git a/app/[lng]/evcp/report/page.tsx b/app/[lng]/evcp/report/page.tsx deleted file mode 100644 index a1e9f8be..00000000 --- a/app/[lng]/evcp/report/page.tsx +++ /dev/null @@ -1,8 +0,0 @@ - -export default function Pages() { - return ( - <> - test - - ) - } \ No newline at end of file diff --git a/app/[lng]/evcp/rfq/[id]/cbe/page.tsx b/app/[lng]/evcp/rfq/[id]/cbe/page.tsx deleted file mode 100644 index bc32641f..00000000 --- a/app/[lng]/evcp/rfq/[id]/cbe/page.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { searchParamsTBECache } from "@/lib/rfqs/validations" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise -} - -export default async function RfqCBEPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsTBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - // const promises = Promise.all([ - // getCBE({ - // ...search, - // filters: validFilters, - // }, - // idAsNumber) - // ]) - - // 4) 렌더링 - return ( -
-
-

- Technical Bid Evaluation -

-

- 초대된 벤더에게 CBE를 보낼 수 있습니다.
"발행하기" 버튼을 통해 CBE를 전송하면 CBE 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다. -

-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/evcp/rfq/[id]/layout.tsx b/app/[lng]/evcp/rfq/[id]/layout.tsx deleted file mode 100644 index 2aac90eb..00000000 --- a/app/[lng]/evcp/rfq/[id]/layout.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Metadata } from "next" - -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" -import { findVendorById } from "@/lib/vendors/service" // 가정: 여기에 findVendorById가 있다고 가정 -import { Rfq, RfqWithItems } from "@/db/schema/rfq" -import { findRfqById } from "@/lib/rfqs/service" -import { formatDate } from "@/lib/utils" - -export const metadata: Metadata = { - title: "Vendor Detail", -} - -export default async function RfqLayout({ - 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 rfq: RfqWithItems | null = await findRfqById(idAsNumber) - - // 3) 사이드바 메뉴 - const sidebarNavItems = [ - { - title: "Matched Vendors", - href: `/${lng}/evcp/rfq/${id}`, - }, - { - title: "TBE", - href: `/${lng}/evcp/rfq/${id}/tbe`, - }, - { - title: "CBE", - href: `/${lng}/evcp/rfq/${id}/cbe`, - }, - - ] - - return ( - <> -
-
-
-
- {/* 4) 벤더 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} -

- {rfq - ? `${rfq.rfqCode ?? ""} 관리` - : "Loading RFQ..."} -

- -

- {rfq - ? `${rfq.description ?? ""} ${rfq.lines.map(line => line.itemCode).join(", ")}` - : ""} -

-

Due Date:{ rfq && {formatDate(rfq?.dueDate)}}

-
- -
- -
{children}
-
-
-
-
- - ) -} \ No newline at end of file diff --git a/app/[lng]/evcp/rfq/[id]/page.tsx b/app/[lng]/evcp/rfq/[id]/page.tsx deleted file mode 100644 index 026ca5ac..00000000 --- a/app/[lng]/evcp/rfq/[id]/page.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getMatchedVendors } from "@/lib/rfqs/service" -import { searchParamsMatchedVCache } from "@/lib/rfqs/validations" -import { MatchedVendorsTable } from "@/lib/rfqs/vendor-table/vendors-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise -} - -export default async function RfqPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsMatchedVCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getMatchedVendors({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - // 4) 렌더링 - return ( -
-
-

- Vendors -

-

- 등록된 벤더 중에서 이 RFQ 아이템에 매칭되는 업체를 보여줍니다.
"발행하기" 버튼을 통해 RFQ를 전송하면 첨부파일과 함께 RFQ 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다. -

-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/evcp/rfq/[id]/tbe/page.tsx b/app/[lng]/evcp/rfq/[id]/tbe/page.tsx deleted file mode 100644 index 15c5d93c..00000000 --- a/app/[lng]/evcp/rfq/[id]/tbe/page.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getTBE } from "@/lib/rfqs/service" -import { searchParamsTBECache } from "@/lib/rfqs/validations" -import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise -} - -export default async function RfqTBEPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsTBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getTBE({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - // 4) 렌더링 - return ( -
-
-

- Technical Bid Evaluation -

-

- 초대된 벤더에게 TBE를 보낼 수 있습니다.
"발행하기" 버튼을 통해 TBE를 전송하면 첨부파일과 함께 TBE 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다. -

-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/evcp/rfq/page.tsx b/app/[lng]/evcp/rfq/page.tsx deleted file mode 100644 index 3417b0bf..00000000 --- a/app/[lng]/evcp/rfq/page.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" - -import { searchParamsCache } from "@/lib/rfqs/validations" -import { getRfqs, getRfqStatusCounts } from "@/lib/rfqs/service" -import { RfqsTable } from "@/lib/rfqs/table/rfqs-table" -import { getAllItems } from "@/lib/items/service" -import { RfqType } from "@/lib/rfqs/validations" - -interface RfqPageProps { - searchParams: Promise; - rfqType: RfqType; - title: string; - description: string; -} - -export default async function RfqPage({ - searchParams, - rfqType = RfqType.PURCHASE, - title = "RFQ", - description = "RFQ를 등록하고 관리할 수 있습니다." -}: RfqPageProps) { - const search = searchParamsCache.parse(await searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getRfqs({ - ...search, - filters: validFilters, - rfqType // 전달받은 rfqType 사용 - }), - getRfqStatusCounts(rfqType), // rfqType 전달 - getAllItems() - ]) - - return ( - -
-
-
-

- {title} -

-

- {description} -

-
-
-
- - }> - {/* */} - - - } - > - - -
- ) -} \ No newline at end of file diff --git a/app/[lng]/evcp/settings/layout.tsx b/app/[lng]/evcp/settings/layout.tsx deleted file mode 100644 index 6f373567..00000000 --- a/app/[lng]/evcp/settings/layout.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { Metadata } from "next" - -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" - -export const metadata: Metadata = { - title: "Settings", - // description: "Advanced form example using react-hook-form and Zod.", -} - - -interface SettingsLayoutProps { - children: React.ReactNode - params: { lng: string } -} - -export default async function SettingsLayout({ - children, - params, -}: { - children: React.ReactNode - params: { lng: string } -}) { - const resolvedParams = await params - const lng = resolvedParams.lng - - - const sidebarNavItems = [ - - { - title: "Account", - href: `/${lng}/evcp/settings`, - }, - { - title: "Preferences", - href: `/${lng}/evcp/settings/preferences`, - } - - - ] - - - return ( - <> -
-
-
-
-

Settings

-

- Manage your account settings and preferences. -

-
- -
- -
{children}
-
-
-
-
- - - - ) -} diff --git a/app/[lng]/evcp/settings/page.tsx b/app/[lng]/evcp/settings/page.tsx deleted file mode 100644 index a6eaac90..00000000 --- a/app/[lng]/evcp/settings/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { AccountForm } from "@/components/settings/account-form" - -export default function SettingsAccountPage() { - return ( -
-
-

Account

-

- Update your account settings. Set your preferred language and - timezone. -

-
- - -
- ) -} diff --git a/app/[lng]/evcp/settings/preferences/page.tsx b/app/[lng]/evcp/settings/preferences/page.tsx deleted file mode 100644 index e2a88021..00000000 --- a/app/[lng]/evcp/settings/preferences/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { AppearanceForm } from "@/components/settings/appearance-form" - -export default function SettingsAppearancePage() { - return ( -
-
-

Preference

-

- Customize the preference of the app. -

-
- - -
- ) -} diff --git a/app/[lng]/evcp/system/admin-users/page.tsx b/app/[lng]/evcp/system/admin-users/page.tsx deleted file mode 100644 index 11a9e9fb..00000000 --- a/app/[lng]/evcp/system/admin-users/page.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { DateRangePicker } from "@/components/date-range-picker" -import { Separator } from "@/components/ui/separator" - -import { searchParamsCache } from "@/lib/admin-users/validations" -import { getAllCompanies, getAllRoles, getUserCountGroupByCompany, getUserCountGroupByRole, getUsers } from "@/lib/admin-users/service" -import { AdmUserTable } from "@/lib/admin-users/table/ausers-table" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function UserTable(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getUsers({ - ...search, - filters: validFilters, - }), - getUserCountGroupByCompany(), - getUserCountGroupByRole(), - getAllCompanies(), - getAllRoles() - ]) - - return ( - - } - > -
-
-

Vendor Admin User Management

-

- 협력업체의 유저 전체를 조회하고 어드민 유저를 생성할 수 있는 페이지입니다. 이곳에서 초기 유저를 생성시킬 수 있습니다.
생성 후에는 생성된 사용자의 이메일로 생성 통보 이메일이 발송되며 사용자는 이메일과 OTP로 로그인이 가능합니다. -

-
- - -
-
- - ) -} diff --git a/app/[lng]/evcp/system/layout.tsx b/app/[lng]/evcp/system/layout.tsx deleted file mode 100644 index 4885a028..00000000 --- a/app/[lng]/evcp/system/layout.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { Metadata } from "next" - -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" - -export const metadata: Metadata = { - title: "System Setting", - // description: "Advanced form example using react-hook-form and Zod.", -} - - -interface SettingsLayoutProps { - children: React.ReactNode - params: { lng: string } -} - -export default async function SettingsLayout({ - children, - params, -}: { - children: React.ReactNode - params: { lng: string } -}) { - const resolvedParams = await params - const lng = resolvedParams.lng - - - const sidebarNavItems = [ - - { - title: "Users", - href: `/${lng}/evcp/system`, - }, - { - title: "Roles", - href: `/${lng}/evcp/system/roles`, - }, - { - title: "Permissions", - href: `/${lng}/evcp/system/permissions`, - }, - { - title: "Vendor Users", - href: `/${lng}/evcp/system/admin-users`, - }, - - ] - - - return ( - <> -
-
-
-
-

시스템 설정

-

- 사용자, 롤, 접근 권한을 관리하세요. -

-
- -
- -
{children}
-
-
-
-
- - - - ) -} diff --git a/app/[lng]/evcp/system/page.tsx b/app/[lng]/evcp/system/page.tsx deleted file mode 100644 index 2d180028..00000000 --- a/app/[lng]/evcp/system/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import * as React from "react" -import { getValidFilters } from "@/lib/data-table" -import { searchParamsCache } from "@/lib/admin-users/validations" -import { getAllRoles, getUsersEVCP } from "@/lib/users/service" -import { getUserCountGroupByRole } from "@/lib/admin-users/service" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { UserTable } from "@/lib/users/table/users-table" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function SystemUserPage(props: IndexPageProps) { - - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getUsersEVCP({ - ...search, - filters: validFilters, - }), - getUserCountGroupByRole(), - getAllRoles() - ]) - - return ( - - } - > -
-
-

Users

-

- 시스템 전체 사용자들을 조회하고 관리할 수 있는 페이지입니다. 사용자에게 롤을 할당하는 것으로 메뉴별 권한을 관리할 수 있습니다. -

-
- - -
-
- - ) -} \ No newline at end of file diff --git a/app/[lng]/evcp/system/permissions/page.tsx b/app/[lng]/evcp/system/permissions/page.tsx deleted file mode 100644 index 6aa2b693..00000000 --- a/app/[lng]/evcp/system/permissions/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import PermissionsTree from "@/components/system/permissionsTree" -import { Separator } from "@/components/ui/separator" - -export default function PermissionsPage() { - return ( -
-
-

Permissions

-

- Set permissions to the menu by Role -

-
- - -
- ) -} diff --git a/app/[lng]/evcp/system/roles/page.tsx b/app/[lng]/evcp/system/roles/page.tsx deleted file mode 100644 index fe074600..00000000 --- a/app/[lng]/evcp/system/roles/page.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Separator } from "@/components/ui/separator" - -import { searchParamsCache } from "@/lib/roles/validations" -import { searchParamsCache as searchParamsCache2 } from "@/lib/admin-users/validations" -import { RolesTable } from "@/lib/roles/table/roles-table" -import { getRolesWithCount } from "@/lib/roles/services" -import { getUsersAll } from "@/lib/users/service" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function UserTable(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - const search2 = searchParamsCache2.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getRolesWithCount({ - ...search, - filters: validFilters, - }), - - - ]) - - - const promises2 = Promise.all([ - getUsersAll({ - ...search2, - filters: validFilters, - }, "evcp"), - ]) - - - return ( - - } - > -
-
-

Role Management

-

- 역할을 생성하고 역할에 유저를 할당할 수 있는 페이지입니다. 역할에 메뉴의 접근 권한 역시 할당할 수 있습니다. -

-
- - -
-
- - ) -} diff --git a/app/[lng]/evcp/tag-numbering/page.tsx b/app/[lng]/evcp/tag-numbering/page.tsx deleted file mode 100644 index 9d5b903a..00000000 --- a/app/[lng]/evcp/tag-numbering/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { searchParamsCache } from "@/lib/tag-numbering/validation" -import { getTagNumbering } from "@/lib/tag-numbering/service" -import { TagNumberingTable } from "@/lib/tag-numbering/table/tagNumbering-table" - - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getTagNumbering({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - -
-
-
-

- Tag Numbering from S-EDP -

-

- 태그 넘버링을 위한 룰셋을 S-EDP로부터 가져오고 확인할 수 있습니다{" "} - {/* - - 버튼 - - 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} -

-
-
-
- - }> - {/* */} - - - } - > - - -
- ) -} diff --git a/app/[lng]/evcp/tasks/page.tsx b/app/[lng]/evcp/tasks/page.tsx deleted file mode 100644 index f14cc757..00000000 --- a/app/[lng]/evcp/tasks/page.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { DateRangePicker } from "@/components/date-range-picker" -import { Shell } from "@/components/shell" - -import { FeatureFlagsProvider } from "@/lib/tasks/table/feature-flags-provider" -import { TasksTable } from "@/lib/tasks/table/tasks-table" -import { - getTaskPriorityCounts, - getTasks, - getTaskStatusCounts, -} from "@/lib/tasks/service" -import { searchParamsCache } from "@/lib/tasks/validations" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getTasks({ - ...search, - filters: validFilters, - }), - getTaskStatusCounts(), - getTaskPriorityCounts(), - ]) - - return ( - - }> - {/* */} - - - } - > - - - - ) -} diff --git a/app/[lng]/evcp/vendor-candidates/page.tsx b/app/[lng]/evcp/vendor-candidates/page.tsx deleted file mode 100644 index 668c0dc6..00000000 --- a/app/[lng]/evcp/vendor-candidates/page.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" - -import { getVendorCandidateCounts, getVendorCandidates } from "@/lib/vendor-candidates/service" -import { searchParamsCandidateCache } from "@/lib/vendor-candidates/validations" -import { VendorCandidateTable } from "@/lib/vendor-candidates/table/candidates-table" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCandidateCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getVendorCandidates({ - ...search, - filters: validFilters, - }), - getVendorCandidateCounts() - ]) - - return ( - - -
-
-
-

- Vendor Candidates Management -

-

- 수집한 벤더 후보를 등록하고 초대 메일을 송부할 수 있습니다. - -

-
-
-
- - - }> - - - } - > - - -
- ) -} diff --git a/app/[lng]/evcp/vendor-investigation/page.tsx b/app/[lng]/evcp/vendor-investigation/page.tsx deleted file mode 100644 index c59de869..00000000 --- a/app/[lng]/evcp/vendor-investigation/page.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" - -import { VendorsInvestigationTable } from "@/lib/vendor-investigation/table/investigation-table" -import { getVendorsInvestigation } from "@/lib/vendor-investigation/service" -import { searchParamsInvestigationCache } from "@/lib/vendor-investigation/validations" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsInvestigationCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getVendorsInvestigation({ - ...search, - filters: validFilters, - }), - ]) - - return ( - - -
-
-
-

- Vendor Investigation Management -

-

- 요청된 Vendor 실사에 대한 스케줄 정보를 관리하고 결과를 입력할 수 있습니다. - -

-
-
-
- - - }> - - - } - > - - -
- ) -} diff --git a/app/[lng]/evcp/vendors/[id]/info/items/page.tsx b/app/[lng]/evcp/vendors/[id]/info/items/page.tsx deleted file mode 100644 index e9ff17b4..00000000 --- a/app/[lng]/evcp/vendors/[id]/info/items/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { getVendorItems } from "@/lib/vendors/service" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { searchParamsItemCache } from "@/lib/vendors/validations" -import { VendorItemsTable } from "@/lib/vendors/items-table/item-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise -} - -export default async function SettingsAccountPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsItemCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - - - const promises = Promise.all([ - getVendorItems({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - // 4) 렌더링 - return ( -
-
-

- Possible Items -

-

- 딜리버리가 가능한 아이템 리스트를 확인할 수 있습니다. -

-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/evcp/vendors/[id]/info/layout.tsx b/app/[lng]/evcp/vendors/[id]/info/layout.tsx deleted file mode 100644 index 39e0bac0..00000000 --- a/app/[lng]/evcp/vendors/[id]/info/layout.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { Metadata } from "next" - -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" -import { findVendorById } from "@/lib/vendors/service" // 가정: 여기에 findVendorById가 있다고 가정 -import { Vendor } from "@/db/schema/vendors" - -export const metadata: Metadata = { - title: "Vendor 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 vendor: Vendor | null = await findVendorById(idAsNumber) - - // 3) 사이드바 메뉴 - const sidebarNavItems = [ - { - title: "Contacts", - href: `/${lng}/evcp/vendors/${id}/info`, - }, - { - title: "Items", - href: `/${lng}/evcp/vendors/${id}/info/items`, - }, - { - title: "RFQ History", - href: `/${lng}/evcp/vendors/${id}/info/rfq-history`, - }, - { - title: "Bidding History", - href: `/${lng}/evcp/vendors/${id}/info/bid-history`, - }, - { - title: "Contract History", - href: `/${lng}/evcp/vendors/${id}/info/contract-history`, - }, - ] - - return ( - <> -
-
-
-
- {/* 4) 벤더 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} -

- {vendor - ? `${vendor.vendorCode ?? ""} - ${vendor.vendorName} 상세 정보` - : "Loading Vendor..."} -

-

벤더 관련 상세사항을 확인하세요.

-
- -
- -
{children}
-
-
-
-
- - ) -} \ No newline at end of file diff --git a/app/[lng]/evcp/vendors/[id]/info/page.tsx b/app/[lng]/evcp/vendors/[id]/info/page.tsx deleted file mode 100644 index 6279e924..00000000 --- a/app/[lng]/evcp/vendors/[id]/info/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { getVendorContacts } from "@/lib/vendors/service" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { searchParamsContactCache } from "@/lib/vendors/validations" -import { VendorContactsTable } from "@/lib/vendors/contacts-table/contact-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise -} - -export default async function SettingsAccountPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsContactCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - - - const promises = Promise.all([ - getVendorContacts({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - // 4) 렌더링 - return ( -
-
-

- Contacts -

-

- 업무별 담당자 정보를 확인하세요. -

-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/evcp/vendors/[id]/info/rfq-history/page.tsx b/app/[lng]/evcp/vendors/[id]/info/rfq-history/page.tsx deleted file mode 100644 index 1d2f618c..00000000 --- a/app/[lng]/evcp/vendors/[id]/info/rfq-history/page.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { getRfqHistory } from "@/lib/vendors/service" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { searchParamsRfqHistoryCache } from "@/lib/vendors/validations" -import { VendorRfqHistoryTable } from "@/lib/vendors/rfq-history-table/rfq-history-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise -} - -export default async function RfqHistoryPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsRfqHistoryCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getRfqHistory({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - // 4) 렌더링 - return ( -
-
-

- RFQ History -

-

- 벤더의 RFQ 참여 이력을 확인할 수 있습니다. -

-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/app/[lng]/evcp/vendors/page.tsx b/app/[lng]/evcp/vendors/page.tsx deleted file mode 100644 index e3cc7fdc..00000000 --- a/app/[lng]/evcp/vendors/page.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" - - -import { searchParamsCache } from "@/lib/vendors/validations" -import { getVendors, getVendorStatusCounts } from "@/lib/vendors/service" -import { VendorsTable } from "@/lib/vendors/table/vendors-table" -import { Ellipsis } from "lucide-react" - -interface IndexPageProps { - searchParams: Promise -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getVendors({ - ...search, - filters: validFilters, - }), - getVendorStatusCounts(), - ]) - - return ( - - -
-
-
-

- Vendor Information -

-

- 벤더에 대한 요약 정보를 확인하고{" "} - - - 버튼 - - 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다.
벤더의 상태에 따라 가입을 승인해주거나 PQ 요청을 할 수 있고 검토가 완료된 벤더를 기간계 시스템에 전송하여 벤더 코드를 따올 수 있습니다. -

-
-
-
- - - }> - {/* */} - - - } - > - - -
- ) -} diff --git a/app/[lng]/partners/pq/page.tsx b/app/[lng]/partners/pq/page.tsx index 42c88b21..08faeebb 100644 --- a/app/[lng]/partners/pq/page.tsx +++ b/app/[lng]/partners/pq/page.tsx @@ -1,85 +1,44 @@ import { getServerSession } from "next-auth" import { authOptions } from "@/app/api/auth/[...nextauth]/route" -import * as React from "react" -import { Shell } from "@/components/shell" -import { Skeleton } from "@/components/ui/skeleton" import { getPQDataByVendorId, getPQProjectsByVendorId } from "@/lib/pq/service" -import { PQInputTabs } from "@/components/pq/pq-input-tabs" -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { ClientPQWrapper } from "@/components/pq/client-pq-input-wrapper" +import { unstable_noStore as noStore } from 'next/cache' +// 페이지가 기본적으로 동적임을 나타냄 +export const dynamic = "force-dynamic" export default async function PQInputPage({ - searchParams + searchParams, }: { searchParams: { projectId?: string } }) { + // Opt out of caching for this route + noStore() + // 세션 const session = await getServerSession(authOptions) // 예: 세션에서 vendorId 가져오기 // const vendorId = session?.user.companyId const vendorId = 17 // 임시 const idAsNumber = Number(vendorId) - - const projectId = searchParams.projectId ? parseInt(searchParams.projectId, 10) : undefined - - // 벤더에게 요청된 프로젝트 PQ 목록 가져오기 (탭 표시용) + + // 서버에서는 모든 데이터를 가져오고, 프로젝트 필터링은 클라이언트에서 진행 const projectPQs = await getPQProjectsByVendorId(idAsNumber) - - // PQ 데이터 조회 - const pqData = await getPQDataByVendorId(idAsNumber, projectId) - - // 현재 프로젝트 정보 (있다면) - const currentProject = projectId - ? projectPQs.find(p => p.projectId === projectId) - : null - + + // 두 가지 방법으로 수정할 수 있습니다: + + // 방법 1: 먼저 allPQData 데이터를 projectId 없이 가져오기 + const allPQData = await getPQDataByVendorId(idAsNumber, undefined) + + // 방법 2: rawProjectId를 클라이언트로 전달하고, 클라이언트가 필터링을 처리 + + // 클라이언트 컴포넌트로 데이터와 원시 searchParams 전달 return ( - - {/* 헤더 - 프로젝트 정보 포함 */} -
-

- Pre-Qualification Check Sheet - {currentProject && ( - - - {currentProject.projectCode} - - )} -

-

- PQ에 적절한 응답을 제출하시기 바랍니다. -

-
- - {/* 일반/프로젝트 PQ 선택 탭 */} - {projectPQs.length > 0 && ( -
- - - - 일반 PQ - - - {projectPQs.map(project => ( - - - {project.projectCode} - - - ))} - - -
- )} - - {/* PQ 입력 탭 */} - }> - - -
+ ) } \ No newline at end of file diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index 609a63d7..cd91774c 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -8,7 +8,7 @@ import { JWT } from "next-auth/jwt" import CredentialsProvider from 'next-auth/providers/credentials' -import { verifyOtp } from '@/lib/users/verifyOtp' +import { verifyExternalCredentials, verifyOtp } from '@/lib/users/verifyOtp' // 1) 모듈 보강 선언 declare module "next-auth" { @@ -61,7 +61,7 @@ export const authOptions: NextAuthOptions = { } return { - id: String(user.id ?? email ?? "dts"), + id: String(user.id ?? email ?? "dts"), email: user.email, imageUrl: user.imageUrl ?? null, name: user.name, // DB에서 가져온 실제 이름 @@ -69,6 +69,44 @@ export const authOptions: NextAuthOptions = { domain: user.domain, // DB에서 가져온 실제 이름 } }, + }), + // 새로 추가할 ID/비밀번호 provider + CredentialsProvider({ + id: 'credentials-password', + name: 'Username Password', + credentials: { + username: { label: "Username", type: "text" }, + password: { label: "Password", type: "password" } + }, + async authorize(credentials, req) { // req 매개변수 추가 + if (!credentials?.username || !credentials?.password) { + return null; + } + + try { + // 여기서 외부 서비스 API를 호출하여 사용자 인증 + const user = await verifyExternalCredentials( + credentials.username, + credentials.password + ); + + if (user) { + return { + id: String(user.id), // id를 string으로 변환 + name: user.name, + email: user.email, + // 첫 번째 provider와 동일한 필드 구조 유지 + imageUrl: user.imageUrl ?? null, + companyId: user.companyId, + domain: user.domain + }; + } + return null; + } catch (error) { + console.error("Authentication error:", error); + return null; + } + } }) ], // (3) session.strategy는 'jwt'가 되도록 선언 diff --git a/app/api/cron/forms/route.ts b/app/api/cron/forms/route.ts new file mode 100644 index 00000000..f58c146b --- /dev/null +++ b/app/api/cron/forms/route.ts @@ -0,0 +1,21 @@ +// src/app/api/cron/tag-form-mappings/route.ts +import { syncTagFormMappings } from '@/lib/sedp/sync-form'; +import { NextRequest } from 'next/server'; + +export async function GET(request: NextRequest) { + try { + console.log('태그 폼 매핑 동기화 API 호출됨:', new Date().toISOString()); + + // syncTagFormMappings 함수 호출 + const result = await syncTagFormMappings(); + + // 성공 시 결과와 함께 200 OK 반환 + return Response.json({ success: true, result }, { status: 200 }); + } catch (error: any) { + console.error('태그 폼 매핑 동기화 API 에러:', error); + + // 에러 시에는 message를 담아 500 반환 + const message = error.message || 'Something went wrong'; + return Response.json({ success: false, error: message }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/api/cron/object-classes/route.ts b/app/api/cron/object-classes/route.ts new file mode 100644 index 00000000..9a574b1b --- /dev/null +++ b/app/api/cron/object-classes/route.ts @@ -0,0 +1,21 @@ +// src/app/api/cron/object-classes/route.ts +import { syncObjectClasses } from '@/lib/sedp/sync-object-class'; +import { NextRequest } from 'next/server'; + +export async function GET(request: NextRequest) { + try { + console.log('오브젝트 클래스 동기화 API 호출됨:', new Date().toISOString()); + + // syncObjectClasses 함수 호출 + const result = await syncObjectClasses(); + + // 성공 시 결과와 함께 200 OK 반환 + return Response.json({ success: true, result }, { status: 200 }); + } catch (error: any) { + console.error('오브젝트 클래스 동기화 API 에러:', error); + + // 에러 시에는 message를 담아 500 반환 + const message = error.message || 'Something went wrong'; + return Response.json({ success: false, error: message }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/api/cron/projects/route.ts b/app/api/cron/projects/route.ts new file mode 100644 index 00000000..d8e6af51 --- /dev/null +++ b/app/api/cron/projects/route.ts @@ -0,0 +1,21 @@ +// src/app/api/cron/projects/route.ts +import { syncProjects } from '@/lib/sedp/sync-projects'; +import { NextRequest } from 'next/server'; + +export async function GET(request: NextRequest) { + try { + console.log('프로젝트 동기화 API 호출됨:', new Date().toISOString()); + + // syncProjects 함수 호출 + const result = await syncProjects(); + + // 성공 시 결과와 함께 200 OK 반환 + return Response.json({ success: true, result }, { status: 200 }); + } catch (error: any) { + console.error('프로젝트 동기화 API 에러:', error); + + // 에러 시에는 message를 담아 500 반환 + const message = error.message || 'Something went wrong'; + return Response.json({ success: false, error: message }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/api/cron/tag-types/route.ts b/app/api/cron/tag-types/route.ts new file mode 100644 index 00000000..35145984 --- /dev/null +++ b/app/api/cron/tag-types/route.ts @@ -0,0 +1,20 @@ +import { syncTagSubfields } from '@/lib/sedp/sync-tag-types'; +import { NextRequest } from 'next/server'; + +export async function GET(request: NextRequest) { + try { + console.log('태그 서브필드 동기화 API 호출됨:', new Date().toISOString()); + + // syncTagSubfields 함수 호출 + const result = await syncTagSubfields(); + + // 성공 시 결과와 함께 200 OK 반환 + return Response.json({ success: true, result }, { status: 200 }); + } catch (error: any) { + console.error('태그 서브필드 동기화 API 에러:', error); + + // 에러 시에는 message를 담아 500 반환 + const message = error.message || 'Something went wrong'; + return Response.json({ success: false, error: message }, { status: 500 }); + } +} \ No newline at end of file -- cgit v1.2.3