summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/[lng]/evcp/(evcp)/bqtbe/page.tsx (renamed from app/[lng]/evcp/bqtbe/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/cbe/page.tsx (renamed from app/[lng]/evcp/budgetary/[id]/cbe/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/layout.tsx (renamed from app/[lng]/evcp/budgetary/[id]/layout.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/page.tsx (renamed from app/[lng]/evcp/budgetary/[id]/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/tbe/page.tsx (renamed from app/[lng]/evcp/budgetary/[id]/tbe/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/budgetary-rfq/page.tsx86
-rw-r--r--app/[lng]/evcp/(evcp)/budgetary/[id]/cbe/page.tsx56
-rw-r--r--app/[lng]/evcp/(evcp)/budgetary/[id]/layout.tsx80
-rw-r--r--app/[lng]/evcp/(evcp)/budgetary/[id]/page.tsx57
-rw-r--r--app/[lng]/evcp/(evcp)/budgetary/[id]/tbe/page.tsx55
-rw-r--r--app/[lng]/evcp/(evcp)/budgetary/page.tsx (renamed from app/[lng]/evcp/budgetary/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/equip-class/page.tsx (renamed from app/[lng]/evcp/equip-class/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/faq/manage/actions.ts (renamed from app/[lng]/evcp/faq/manage/actions.ts)0
-rw-r--r--app/[lng]/evcp/(evcp)/faq/manage/page.tsx (renamed from app/[lng]/evcp/faq/manage/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/faq/page.tsx (renamed from app/[lng]/evcp/faq/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/form-list/page.tsx (renamed from app/[lng]/evcp/form-list/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/items/page.tsx (renamed from app/[lng]/evcp/items/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/layout.tsx (renamed from app/[lng]/evcp/layout.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/po/page.tsx (renamed from app/[lng]/evcp/po/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/poa/page.tsx (renamed from app/[lng]/evcp/poa/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/pq-criteria/[id]/page.tsx (renamed from app/[lng]/evcp/pq-criteria/[id]/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/pq-criteria/page.tsx (renamed from app/[lng]/evcp/pq-criteria/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/pq/[vendorId]/page.tsx (renamed from app/[lng]/evcp/pq/[vendorId]/page.tsx)8
-rw-r--r--app/[lng]/evcp/(evcp)/pq/page.tsx (renamed from app/[lng]/evcp/pq/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/projects/page.tsx75
-rw-r--r--app/[lng]/evcp/(evcp)/report/page.tsx (renamed from app/[lng]/evcp/report/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/rfq/[id]/cbe/page.tsx (renamed from app/[lng]/evcp/rfq/[id]/cbe/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/rfq/[id]/layout.tsx (renamed from app/[lng]/evcp/rfq/[id]/layout.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/rfq/[id]/page.tsx (renamed from app/[lng]/evcp/rfq/[id]/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/rfq/[id]/tbe/page.tsx (renamed from app/[lng]/evcp/rfq/[id]/tbe/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/rfq/page.tsx (renamed from app/[lng]/evcp/rfq/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/settings/layout.tsx (renamed from app/[lng]/evcp/settings/layout.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/settings/page.tsx (renamed from app/[lng]/evcp/settings/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/settings/preferences/page.tsx (renamed from app/[lng]/evcp/settings/preferences/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/system/admin-users/page.tsx (renamed from app/[lng]/evcp/system/admin-users/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/system/layout.tsx (renamed from app/[lng]/evcp/system/layout.tsx)2
-rw-r--r--app/[lng]/evcp/(evcp)/system/page.tsx (renamed from app/[lng]/evcp/system/page.tsx)2
-rw-r--r--app/[lng]/evcp/(evcp)/system/permissions/page.tsx (renamed from app/[lng]/evcp/system/permissions/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/system/roles/page.tsx (renamed from app/[lng]/evcp/system/roles/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/tag-numbering/page.tsx (renamed from app/[lng]/evcp/tag-numbering/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/tasks/page.tsx (renamed from app/[lng]/evcp/tasks/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/tbe/page.tsx113
-rw-r--r--app/[lng]/evcp/(evcp)/vendor-candidates/page.tsx (renamed from app/[lng]/evcp/vendor-candidates/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/vendor-investigation/page.tsx (renamed from app/[lng]/evcp/vendor-investigation/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/vendors/[id]/info/items/page.tsx (renamed from app/[lng]/evcp/vendors/[id]/info/items/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/vendors/[id]/info/layout.tsx (renamed from app/[lng]/evcp/vendors/[id]/info/layout.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/vendors/[id]/info/page.tsx (renamed from app/[lng]/evcp/vendors/[id]/info/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/vendors/[id]/info/rfq-history/page.tsx (renamed from app/[lng]/evcp/vendors/[id]/info/rfq-history/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/vendors/page.tsx (renamed from app/[lng]/evcp/vendors/page.tsx)0
-rw-r--r--app/[lng]/evcp/page.tsx27
-rw-r--r--app/[lng]/partners/pq/page.tsx91
-rw-r--r--app/api/auth/[...nextauth]/route.ts42
-rw-r--r--app/api/cron/forms/route.ts21
-rw-r--r--app/api/cron/object-classes/route.ts21
-rw-r--r--app/api/cron/projects/route.ts21
-rw-r--r--app/api/cron/tag-types/route.ts20
56 files changed, 695 insertions, 82 deletions
diff --git a/app/[lng]/evcp/bqtbe/page.tsx b/app/[lng]/evcp/(evcp)/bqtbe/page.tsx
index 655bd30a..655bd30a 100644
--- a/app/[lng]/evcp/bqtbe/page.tsx
+++ b/app/[lng]/evcp/(evcp)/bqtbe/page.tsx
diff --git a/app/[lng]/evcp/budgetary/[id]/cbe/page.tsx b/app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/cbe/page.tsx
index 9a4ae7eb..9a4ae7eb 100644
--- a/app/[lng]/evcp/budgetary/[id]/cbe/page.tsx
+++ b/app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/cbe/page.tsx
diff --git a/app/[lng]/evcp/budgetary/[id]/layout.tsx b/app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/layout.tsx
index 39f045e5..39f045e5 100644
--- a/app/[lng]/evcp/budgetary/[id]/layout.tsx
+++ b/app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/layout.tsx
diff --git a/app/[lng]/evcp/budgetary/[id]/page.tsx b/app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/page.tsx
index f6160574..f6160574 100644
--- a/app/[lng]/evcp/budgetary/[id]/page.tsx
+++ b/app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/page.tsx
diff --git a/app/[lng]/evcp/budgetary/[id]/tbe/page.tsx b/app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/tbe/page.tsx
index a6259696..a6259696 100644
--- a/app/[lng]/evcp/budgetary/[id]/tbe/page.tsx
+++ b/app/[lng]/evcp/(evcp)/budgetary-rfq/[id]/tbe/page.tsx
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<SearchParams>;
+ 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 (
+ <Shell className="gap-2">
+ <div className="flex items-center justify-between space-y-2">
+ <div className="flex items-center justify-between space-y-2">
+ <div>
+ <h2 className="text-2xl font-bold tracking-tight">
+ {title}
+ </h2>
+ <p className="text-muted-foreground">
+ {description}
+ 기본적인 정보와 RFQ를 위한 아이템 등록 및 첨부를 한 후,
+ <span className="inline-flex items-center whitespace-nowrap">
+ <Ellipsis className="size-3" />
+ <span className="ml-1">버튼</span>
+ </span> 을 클릭하면 "Proceed"를 통해 상세화면으로 이동하여 진행할 수 있습니다.
+ </p>
+ </div>
+ </div>
+ </div>
+
+ <React.Suspense fallback={<Skeleton className="h-7 w-52" />}>
+ {/* <DateRangePicker
+ triggerSize="sm"
+ triggerClassName="ml-auto w-56 sm:w-60"
+ align="end"
+ shallow={false}
+ /> */}
+ </React.Suspense>
+ <React.Suspense
+ fallback={
+ <DataTableSkeleton
+ columnCount={6}
+ searchableColumnCount={1}
+ filterableColumnCount={2}
+ cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]}
+ shrinkZero
+ />
+ }
+ >
+ <RfqsTable promises={promises} rfqType={rfqType} />
+ </React.Suspense>
+ </Shell>
+ )
+} \ 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<SearchParams>
+}
+
+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 (
+ <div className="space-y-6">
+ <div>
+ <h3 className="text-lg font-medium">
+ Commercial Bid Evaluation
+ </h3>
+ <p className="text-sm text-muted-foreground">
+ 초대된 벤더에게 CBE를 보낼 수 있습니다. <br/>체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다.
+ </p>
+ </div>
+ <Separator />
+ <div>
+ <CbeTable promises={promises} rfqId={idAsNumber}/>
+ </div>
+ </div>
+ )
+} \ 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 (
+ <>
+ <div className="container py-6">
+ <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow">
+ <div className="hidden space-y-6 p-10 pb-16 md:block">
+ <div className="space-y-0.5">
+ {/* 4) 벤더 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */}
+ <h2 className="text-2xl font-bold tracking-tight">
+ {rfq
+ ? `${rfq.rfqCode ?? ""} 관리`
+ : "Loading RFQ..."}
+ </h2>
+
+ <p className="text-muted-foreground">
+ {rfq
+ ? `${rfq.description ?? ""} ${rfq.lines.map(line => line.itemCode).join(", ")}`
+ : ""}
+ </p>
+ <h3>Due Date:{ rfq && <strong>{formatDate(rfq?.dueDate)}</strong>}</h3>
+ </div>
+ <Separator className="my-6" />
+ <div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0">
+ <aside className="-mx-4 lg:w-1/6">
+ <SidebarNav items={sidebarNavItems} />
+ </aside>
+ <div className="flex-1">{children}</div>
+ </div>
+ </div>
+ </section>
+ </div>
+ </>
+ )
+} \ 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<SearchParams>
+ 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 (
+ <div className="space-y-6">
+ <div>
+ <h3 className="text-lg font-medium">
+ Vendors
+ </h3>
+ <p className="text-sm text-muted-foreground">
+ 등록된 벤더 중에서 이 RFQ 아이템에 매칭되는 업체를 보여줍니다. <br/>"발행하기" 버튼을 통해 RFQ를 전송하면 첨부파일과 함께 RFQ 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다.
+ </p>
+ </div>
+ <Separator />
+ <div>
+ <MatchedVendorsTable promises={promises} rfqId={idAsNumber} rfqType={rfqType}/>
+ </div>
+ </div>
+ )
+} \ 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<SearchParams>
+}
+
+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 (
+ <div className="space-y-6">
+ <div>
+ <h3 className="text-lg font-medium">
+ Technical Bid Evaluation
+ </h3>
+ <p className="text-sm text-muted-foreground">
+ 초대된 벤더에게 TBE를 보낼 수 있습니다. <br/>체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다.
+ </p>
+ </div>
+ <Separator />
+ <div>
+ <TbeTable promises={promises} rfqId={idAsNumber}/>
+ </div>
+ </div>
+ )
+} \ No newline at end of file
diff --git a/app/[lng]/evcp/budgetary/page.tsx b/app/[lng]/evcp/(evcp)/budgetary/page.tsx
index 04550353..04550353 100644
--- a/app/[lng]/evcp/budgetary/page.tsx
+++ b/app/[lng]/evcp/(evcp)/budgetary/page.tsx
diff --git a/app/[lng]/evcp/equip-class/page.tsx b/app/[lng]/evcp/(evcp)/equip-class/page.tsx
index 375eb69e..375eb69e 100644
--- a/app/[lng]/evcp/equip-class/page.tsx
+++ b/app/[lng]/evcp/(evcp)/equip-class/page.tsx
diff --git a/app/[lng]/evcp/faq/manage/actions.ts b/app/[lng]/evcp/(evcp)/faq/manage/actions.ts
index bc443a8a..bc443a8a 100644
--- a/app/[lng]/evcp/faq/manage/actions.ts
+++ b/app/[lng]/evcp/(evcp)/faq/manage/actions.ts
diff --git a/app/[lng]/evcp/faq/manage/page.tsx b/app/[lng]/evcp/(evcp)/faq/manage/page.tsx
index 011bbfa4..011bbfa4 100644
--- a/app/[lng]/evcp/faq/manage/page.tsx
+++ b/app/[lng]/evcp/(evcp)/faq/manage/page.tsx
diff --git a/app/[lng]/evcp/faq/page.tsx b/app/[lng]/evcp/(evcp)/faq/page.tsx
index 9b62b7e4..9b62b7e4 100644
--- a/app/[lng]/evcp/faq/page.tsx
+++ b/app/[lng]/evcp/(evcp)/faq/page.tsx
diff --git a/app/[lng]/evcp/form-list/page.tsx b/app/[lng]/evcp/(evcp)/form-list/page.tsx
index f96917d6..f96917d6 100644
--- a/app/[lng]/evcp/form-list/page.tsx
+++ b/app/[lng]/evcp/(evcp)/form-list/page.tsx
diff --git a/app/[lng]/evcp/items/page.tsx b/app/[lng]/evcp/(evcp)/items/page.tsx
index 144689ff..144689ff 100644
--- a/app/[lng]/evcp/items/page.tsx
+++ b/app/[lng]/evcp/(evcp)/items/page.tsx
diff --git a/app/[lng]/evcp/layout.tsx b/app/[lng]/evcp/(evcp)/layout.tsx
index 9dc39f7b..9dc39f7b 100644
--- a/app/[lng]/evcp/layout.tsx
+++ b/app/[lng]/evcp/(evcp)/layout.tsx
diff --git a/app/[lng]/evcp/po/page.tsx b/app/[lng]/evcp/(evcp)/po/page.tsx
index fa528df0..fa528df0 100644
--- a/app/[lng]/evcp/po/page.tsx
+++ b/app/[lng]/evcp/(evcp)/po/page.tsx
diff --git a/app/[lng]/evcp/poa/page.tsx b/app/[lng]/evcp/(evcp)/poa/page.tsx
index dec5e05b..dec5e05b 100644
--- a/app/[lng]/evcp/poa/page.tsx
+++ b/app/[lng]/evcp/(evcp)/poa/page.tsx
diff --git a/app/[lng]/evcp/pq-criteria/[id]/page.tsx b/app/[lng]/evcp/(evcp)/pq-criteria/[id]/page.tsx
index f040a0ca..f040a0ca 100644
--- a/app/[lng]/evcp/pq-criteria/[id]/page.tsx
+++ b/app/[lng]/evcp/(evcp)/pq-criteria/[id]/page.tsx
diff --git a/app/[lng]/evcp/pq-criteria/page.tsx b/app/[lng]/evcp/(evcp)/pq-criteria/page.tsx
index 778baa93..778baa93 100644
--- a/app/[lng]/evcp/pq-criteria/page.tsx
+++ b/app/[lng]/evcp/(evcp)/pq-criteria/page.tsx
diff --git a/app/[lng]/evcp/pq/[vendorId]/page.tsx b/app/[lng]/evcp/(evcp)/pq/[vendorId]/page.tsx
index 97c9a29a..4c2555a3 100644
--- a/app/[lng]/evcp/pq/[vendorId]/page.tsx
+++ b/app/[lng]/evcp/(evcp)/pq/[vendorId]/page.tsx
@@ -1,8 +1,7 @@
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 { 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"
@@ -78,7 +77,7 @@ export default async function PQReviewPage(props: IndexPageProps) {
data={activeProjectId ? [] : pqData}
vendor={vendor}
projectId={undefined}
- loadData={() => getPQDataByVendorId(vendorId)}
+ loadData={loadGeneralPQData}
pqType="general"
/>
</TabsContent>
@@ -93,8 +92,7 @@ export default async function PQReviewPage(props: IndexPageProps) {
projectId={project.projectId}
projectName={project.projectName}
projectStatus={project.status}
- loadData={() => getPQDataByVendorId(vendorId, project.projectId)}
- pqType="project"
+ loadData={(vendorId, _projectId) => loadProjectPQData(vendorId, project.projectId)} pqType="project"
/>
</TabsContent>
))}
diff --git a/app/[lng]/evcp/pq/page.tsx b/app/[lng]/evcp/(evcp)/pq/page.tsx
index 46b22b12..46b22b12 100644
--- a/app/[lng]/evcp/pq/page.tsx
+++ b/app/[lng]/evcp/(evcp)/pq/page.tsx
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<SearchParams>
+}
+
+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 (
+ <Shell className="gap-2">
+ <div className="flex items-center justify-between space-y-2">
+ <div className="flex items-center justify-between space-y-2">
+ <div>
+ <h2 className="text-2xl font-bold tracking-tight">
+ Project List from S-EDP
+ </h2>
+ <p className="text-muted-foreground">
+ S-EDP로부터 수신하는 프로젝트 리스트입니다. 향후 MDG로 전환됩니다.{" "}
+ {/* <span className="inline-flex items-center whitespace-nowrap">
+ <Ellipsis className="size-3" />
+ <span className="ml-1">버튼</span>
+ </span>
+ 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */}
+ </p>
+ </div>
+ </div>
+ </div>
+
+ <React.Suspense fallback={<Skeleton className="h-7 w-52" />}>
+ {/* <DateRangePicker
+ triggerSize="sm"
+ triggerClassName="ml-auto w-56 sm:w-60"
+ align="end"
+ shallow={false}
+ /> */}
+ </React.Suspense>
+ <React.Suspense
+ fallback={
+ <DataTableSkeleton
+ columnCount={6}
+ searchableColumnCount={1}
+ filterableColumnCount={2}
+ cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]}
+ shrinkZero
+ />
+ }
+ >
+ <ProjectsTable promises={promises} />
+ </React.Suspense>
+ </Shell>
+ )
+}
diff --git a/app/[lng]/evcp/report/page.tsx b/app/[lng]/evcp/(evcp)/report/page.tsx
index a1e9f8be..a1e9f8be 100644
--- a/app/[lng]/evcp/report/page.tsx
+++ b/app/[lng]/evcp/(evcp)/report/page.tsx
diff --git a/app/[lng]/evcp/rfq/[id]/cbe/page.tsx b/app/[lng]/evcp/(evcp)/rfq/[id]/cbe/page.tsx
index bc32641f..bc32641f 100644
--- a/app/[lng]/evcp/rfq/[id]/cbe/page.tsx
+++ b/app/[lng]/evcp/(evcp)/rfq/[id]/cbe/page.tsx
diff --git a/app/[lng]/evcp/rfq/[id]/layout.tsx b/app/[lng]/evcp/(evcp)/rfq/[id]/layout.tsx
index 2aac90eb..2aac90eb 100644
--- a/app/[lng]/evcp/rfq/[id]/layout.tsx
+++ b/app/[lng]/evcp/(evcp)/rfq/[id]/layout.tsx
diff --git a/app/[lng]/evcp/rfq/[id]/page.tsx b/app/[lng]/evcp/(evcp)/rfq/[id]/page.tsx
index 026ca5ac..026ca5ac 100644
--- a/app/[lng]/evcp/rfq/[id]/page.tsx
+++ b/app/[lng]/evcp/(evcp)/rfq/[id]/page.tsx
diff --git a/app/[lng]/evcp/rfq/[id]/tbe/page.tsx b/app/[lng]/evcp/(evcp)/rfq/[id]/tbe/page.tsx
index 15c5d93c..15c5d93c 100644
--- a/app/[lng]/evcp/rfq/[id]/tbe/page.tsx
+++ b/app/[lng]/evcp/(evcp)/rfq/[id]/tbe/page.tsx
diff --git a/app/[lng]/evcp/rfq/page.tsx b/app/[lng]/evcp/(evcp)/rfq/page.tsx
index 3417b0bf..3417b0bf 100644
--- a/app/[lng]/evcp/rfq/page.tsx
+++ b/app/[lng]/evcp/(evcp)/rfq/page.tsx
diff --git a/app/[lng]/evcp/settings/layout.tsx b/app/[lng]/evcp/(evcp)/settings/layout.tsx
index 6f373567..6f373567 100644
--- a/app/[lng]/evcp/settings/layout.tsx
+++ b/app/[lng]/evcp/(evcp)/settings/layout.tsx
diff --git a/app/[lng]/evcp/settings/page.tsx b/app/[lng]/evcp/(evcp)/settings/page.tsx
index a6eaac90..a6eaac90 100644
--- a/app/[lng]/evcp/settings/page.tsx
+++ b/app/[lng]/evcp/(evcp)/settings/page.tsx
diff --git a/app/[lng]/evcp/settings/preferences/page.tsx b/app/[lng]/evcp/(evcp)/settings/preferences/page.tsx
index e2a88021..e2a88021 100644
--- a/app/[lng]/evcp/settings/preferences/page.tsx
+++ b/app/[lng]/evcp/(evcp)/settings/preferences/page.tsx
diff --git a/app/[lng]/evcp/system/admin-users/page.tsx b/app/[lng]/evcp/(evcp)/system/admin-users/page.tsx
index 11a9e9fb..11a9e9fb 100644
--- a/app/[lng]/evcp/system/admin-users/page.tsx
+++ b/app/[lng]/evcp/(evcp)/system/admin-users/page.tsx
diff --git a/app/[lng]/evcp/system/layout.tsx b/app/[lng]/evcp/(evcp)/system/layout.tsx
index 4885a028..62f3e845 100644
--- a/app/[lng]/evcp/system/layout.tsx
+++ b/app/[lng]/evcp/(evcp)/system/layout.tsx
@@ -28,7 +28,7 @@ export default async function SettingsLayout({
const sidebarNavItems = [
{
- title: "Users",
+ title: "SHI Users",
href: `/${lng}/evcp/system`,
},
{
diff --git a/app/[lng]/evcp/system/page.tsx b/app/[lng]/evcp/(evcp)/system/page.tsx
index 2d180028..fe0a262c 100644
--- a/app/[lng]/evcp/system/page.tsx
+++ b/app/[lng]/evcp/(evcp)/system/page.tsx
@@ -42,7 +42,7 @@ export default async function SystemUserPage(props: IndexPageProps) {
>
<div className="space-y-6">
<div>
- <h3 className="text-lg font-medium">Users</h3>
+ <h3 className="text-lg font-medium">SHI Users</h3>
<p className="text-sm text-muted-foreground">
시스템 전체 사용자들을 조회하고 관리할 수 있는 페이지입니다. 사용자에게 롤을 할당하는 것으로 메뉴별 권한을 관리할 수 있습니다.
</p>
diff --git a/app/[lng]/evcp/system/permissions/page.tsx b/app/[lng]/evcp/(evcp)/system/permissions/page.tsx
index 6aa2b693..6aa2b693 100644
--- a/app/[lng]/evcp/system/permissions/page.tsx
+++ b/app/[lng]/evcp/(evcp)/system/permissions/page.tsx
diff --git a/app/[lng]/evcp/system/roles/page.tsx b/app/[lng]/evcp/(evcp)/system/roles/page.tsx
index fe074600..fe074600 100644
--- a/app/[lng]/evcp/system/roles/page.tsx
+++ b/app/[lng]/evcp/(evcp)/system/roles/page.tsx
diff --git a/app/[lng]/evcp/tag-numbering/page.tsx b/app/[lng]/evcp/(evcp)/tag-numbering/page.tsx
index 9d5b903a..9d5b903a 100644
--- a/app/[lng]/evcp/tag-numbering/page.tsx
+++ b/app/[lng]/evcp/(evcp)/tag-numbering/page.tsx
diff --git a/app/[lng]/evcp/tasks/page.tsx b/app/[lng]/evcp/(evcp)/tasks/page.tsx
index f14cc757..f14cc757 100644
--- a/app/[lng]/evcp/tasks/page.tsx
+++ b/app/[lng]/evcp/(evcp)/tasks/page.tsx
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<SearchParams>
+}
+
+// 타입별 페이지 설명 구성 (Budgetary 제외)
+const typeConfig: Record<string, { title: string; description: string; rfqType: RfqType }> = {
+ "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 (
+ <Shell className="gap-2">
+ <div className="flex items-center justify-between space-y-2">
+ <div className="flex items-center justify-between space-y-2">
+ <div>
+ <h2 className="text-2xl font-bold tracking-tight">
+ Technical Bid Evaluation
+ </h2>
+ <p className="text-muted-foreground">
+ 초대된 벤더에게 TBE를 보낼 수 있습니다. <br/>
+ 체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 벤더가 입력할 수 있게 자동 생성됩니다.
+ </p>
+ </div>
+ </div>
+ </div>
+
+ {/* 타입 선택 탭 (Budgetary 제외) */}
+ <Tabs defaultValue={validType} value={validType} className="w-full">
+ <TabsList className="grid grid-cols-2 w-full max-w-md">
+ <TabsTrigger value="purchase" asChild>
+ <a href={getTabUrl('purchase')}>Purchase</a>
+ </TabsTrigger>
+ <TabsTrigger value="purchase-budgetary" asChild>
+ <a href={getTabUrl('purchase-budgetary')}>Purchase Budgetary</a>
+ </TabsTrigger>
+ </TabsList>
+
+ <div className="mt-2">
+ <p className="text-sm text-muted-foreground">
+ {typeConfig[validType].description}
+ </p>
+ </div>
+ </Tabs>
+
+ <React.Suspense
+ fallback={
+ <DataTableSkeleton
+ columnCount={6}
+ searchableColumnCount={1}
+ filterableColumnCount={2}
+ cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]}
+ shrinkZero
+ />
+ }
+ >
+ <AllTbeTable promises={promises}/>
+ </React.Suspense>
+ </Shell>
+ )
+} \ No newline at end of file
diff --git a/app/[lng]/evcp/vendor-candidates/page.tsx b/app/[lng]/evcp/(evcp)/vendor-candidates/page.tsx
index 668c0dc6..668c0dc6 100644
--- a/app/[lng]/evcp/vendor-candidates/page.tsx
+++ b/app/[lng]/evcp/(evcp)/vendor-candidates/page.tsx
diff --git a/app/[lng]/evcp/vendor-investigation/page.tsx b/app/[lng]/evcp/(evcp)/vendor-investigation/page.tsx
index c59de869..c59de869 100644
--- a/app/[lng]/evcp/vendor-investigation/page.tsx
+++ b/app/[lng]/evcp/(evcp)/vendor-investigation/page.tsx
diff --git a/app/[lng]/evcp/vendors/[id]/info/items/page.tsx b/app/[lng]/evcp/(evcp)/vendors/[id]/info/items/page.tsx
index e9ff17b4..e9ff17b4 100644
--- a/app/[lng]/evcp/vendors/[id]/info/items/page.tsx
+++ b/app/[lng]/evcp/(evcp)/vendors/[id]/info/items/page.tsx
diff --git a/app/[lng]/evcp/vendors/[id]/info/layout.tsx b/app/[lng]/evcp/(evcp)/vendors/[id]/info/layout.tsx
index 39e0bac0..39e0bac0 100644
--- a/app/[lng]/evcp/vendors/[id]/info/layout.tsx
+++ b/app/[lng]/evcp/(evcp)/vendors/[id]/info/layout.tsx
diff --git a/app/[lng]/evcp/vendors/[id]/info/page.tsx b/app/[lng]/evcp/(evcp)/vendors/[id]/info/page.tsx
index 6279e924..6279e924 100644
--- a/app/[lng]/evcp/vendors/[id]/info/page.tsx
+++ b/app/[lng]/evcp/(evcp)/vendors/[id]/info/page.tsx
diff --git a/app/[lng]/evcp/vendors/[id]/info/rfq-history/page.tsx b/app/[lng]/evcp/(evcp)/vendors/[id]/info/rfq-history/page.tsx
index 1d2f618c..1d2f618c 100644
--- a/app/[lng]/evcp/vendors/[id]/info/rfq-history/page.tsx
+++ b/app/[lng]/evcp/(evcp)/vendors/[id]/info/rfq-history/page.tsx
diff --git a/app/[lng]/evcp/vendors/page.tsx b/app/[lng]/evcp/(evcp)/vendors/page.tsx
index e3cc7fdc..e3cc7fdc 100644
--- a/app/[lng]/evcp/vendors/page.tsx
+++ b/app/[lng]/evcp/(evcp)/vendors/page.tsx
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 (
+ <>
+ <Suspense fallback={<LoginFormSkeleton/>}>
+ <LoginFormSHI />
+ </Suspense>
+ </>
+ )
+}
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 (
- <Shell className="gap-2">
- {/* 헤더 - 프로젝트 정보 포함 */}
- <div className="space-y-2">
- <h2 className="text-2xl font-bold tracking-tight">
- Pre-Qualification Check Sheet
- {currentProject && (
- <span className="ml-2 text-muted-foreground">
- - {currentProject.projectCode}
- </span>
- )}
- </h2>
- <p className="text-muted-foreground">
- PQ에 적절한 응답을 제출하시기 바랍니다.
- </p>
- </div>
-
- {/* 일반/프로젝트 PQ 선택 탭 */}
- {projectPQs.length > 0 && (
- <div className="border-b">
- <Tabs defaultValue={projectId ? `project-${projectId}` : "general"}>
- <TabsList>
- <TabsTrigger value="general" asChild>
- <a href="/partners/pq">일반 PQ</a>
- </TabsTrigger>
-
- {projectPQs.map(project => (
- <TabsTrigger key={project.projectId} value={`project-${project.projectId}`} asChild>
- <a href={`/partners/pq?projectId=${project.projectId}`}>
- {project.projectCode}
- </a>
- </TabsTrigger>
- ))}
- </TabsList>
- </Tabs>
- </div>
- )}
-
- {/* PQ 입력 탭 */}
- <React.Suspense fallback={<Skeleton className="h-7 w-52" />}>
- <PQInputTabs
- data={pqData}
- vendorId={idAsNumber}
- projectId={projectId}
- projectData={currentProject}
- />
- </React.Suspense>
- </Shell>
+ <ClientPQWrapper
+ allPQData={allPQData}
+ projectPQs={projectPQs}
+ vendorId={idAsNumber}
+ rawSearchParams={searchParams}
+ />
)
} \ 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