diff options
| author | joonhoekim <26rote@gmail.com> | 2025-11-29 22:58:24 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-11-29 22:58:24 +0900 |
| commit | 12af09245b38da8cc3fdb851ebb03bc0de45c8be (patch) | |
| tree | 9cf4b0db3c32f23a243b63b11665392d0c0e966d | |
| parent | eea317cb775587d002e7a97d62220e5c8f37066d (diff) | |
(김준회) 미사용 스키마 코드 제거, 미사용 페이지 제거
34 files changed, 41 insertions, 4237 deletions
diff --git a/app/[lng]/evcp/(evcp)/(not-used)/b-rfq/[id]/final/page.tsx b/app/[lng]/evcp/(evcp)/(not-used)/b-rfq/[id]/final/page.tsx deleted file mode 100644 index d50ec03d..00000000 --- a/app/[lng]/evcp/(evcp)/(not-used)/b-rfq/[id]/final/page.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getFinalRfqDetail } from "@/lib/b-rfq/service" -import { searchParamsFinalRfqDetailCache } from "@/lib/b-rfq/validations" -import { FinalRfqDetailTable } from "@/lib/b-rfq/final/final-rfq-detail-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise<SearchParams> -} - -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 = searchParamsFinalRfqDetailCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = getFinalRfqDetail({ - ...search, - filters: validFilters, - }, idAsNumber) - - // 4) 렌더링 - return ( - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium"> - Fianl RFQ List - </h3> - <p className="text-sm text-muted-foreground"> - 업체에게 최종 RFQ를 송부하는 화면입니다. - </p> - </div> - <Separator /> - <div> - <FinalRfqDetailTable promises={promises} rfqId={idAsNumber}/> - </div> - </div> - ) -}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/(not-used)/b-rfq/[id]/initial/page.tsx b/app/[lng]/evcp/(evcp)/(not-used)/b-rfq/[id]/initial/page.tsx deleted file mode 100644 index 1af65fbc..00000000 --- a/app/[lng]/evcp/(evcp)/(not-used)/b-rfq/[id]/initial/page.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { InitialRfqDetailTable } from "@/lib/b-rfq/initial/initial-rfq-detail-table" -import { getInitialRfqDetail } from "@/lib/b-rfq/service" -import { searchParamsInitialRfqDetailCache } from "@/lib/b-rfq/validations" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise<SearchParams> -} - -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 = searchParamsInitialRfqDetailCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = getInitialRfqDetail({ - ...search, - filters: validFilters, - }, idAsNumber) - - // 4) 렌더링 - return ( - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium"> - Initial RFQ List - </h3> - <p className="text-sm text-muted-foreground"> - 설계로부터 받은 RFQ 문서와 구매 RFQ 문서 및 사전 계약자료를 Vendor에 발송하기 위한 RFQ 생성 및 관리하는 화면입니다. - </p> - </div> - <Separator /> - <div> - <InitialRfqDetailTable promises={promises} rfqId={idAsNumber}/> - </div> - </div> - ) -}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/(not-used)/b-rfq/[id]/layout.tsx b/app/[lng]/evcp/(evcp)/(not-used)/b-rfq/[id]/layout.tsx deleted file mode 100644 index d6836437..00000000 --- a/app/[lng]/evcp/(evcp)/(not-used)/b-rfq/[id]/layout.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { Metadata } from "next" -import Link from "next/link" -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" -import { formatDate } from "@/lib/utils" -import { Button } from "@/components/ui/button" -import { ArrowLeft } from "lucide-react" -import { RfqDashboardView } from "@/db/schema" -import { findBRfqById } from "@/lib/b-rfq/service" - -export const metadata: Metadata = { - title: "견적 RFQ 상세", -} - -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: RfqDashboardView | null = await findBRfqById(idAsNumber) - - // 3) 사이드바 메뉴 - const sidebarNavItems = [ - { - title: "견적/입찰 문서관리", - href: `/${lng}/evcp/b-rfq/${id}`, - }, - { - title: "Initial RFQ 발송", - href: `/${lng}/evcp/b-rfq/${id}/initial`, - }, - { - title: "Final RFQ 발송", - href: `/${lng}/evcp/b-rfq/${id}/final`, - }, - - ] - - 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="flex items-center justify-end mb-4"> - <Link href={`/${lng}/evcp/b-rfq`} passHref> - <Button variant="ghost" className="flex items-center text-primary hover:text-primary/80 transition-colors p-0 h-auto"> - <ArrowLeft className="mr-1 h-4 w-4" /> - <span>RFQ 목록으로 돌아가기</span> - </Button> - </Link> - </div> - <div className="space-y-0.5"> - {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} - <h2 className="text-2xl font-bold tracking-tight"> - {rfq - ? `${rfq.rfqCode ?? ""} | ${rfq.packageNo ?? ""} | ${rfq.packageName ?? ""}` - : "Loading RFQ..."} - </h2> - - <p className="text-muted-foreground"> - PR발행 전 RFQ를 생성하여 관리하는 화면입니다. - </p> - <h3>Due Date:{rfq && rfq?.dueDate && <strong>{formatDate(rfq?.dueDate, "KR")}</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="lg:w-64 flex-shrink-0"> - <SidebarNav items={sidebarNavItems} /> - </aside> - <div className="lg:w-[calc(100%-16rem)] overflow-auto">{children}</div> - </div> - </div> - </section> - </div> - </> - ) -}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/(not-used)/b-rfq/[id]/page.tsx b/app/[lng]/evcp/(evcp)/(not-used)/b-rfq/[id]/page.tsx deleted file mode 100644 index 26dc45fb..00000000 --- a/app/[lng]/evcp/(evcp)/(not-used)/b-rfq/[id]/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 { searchParamsRfqAttachmentsCache } from "@/lib/b-rfq/validations" -import { getRfqAttachments } from "@/lib/b-rfq/service" -import { RfqAttachmentsTable } from "@/lib/b-rfq/attachment/attachment-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise<SearchParams> -} - -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 = searchParamsRfqAttachmentsCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = getRfqAttachments({ - ...search, - filters: validFilters, - }, idAsNumber) - - // 4) 렌더링 - return ( - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium"> - 견적 RFQ 문서관리 - </h3> - <p className="text-sm text-muted-foreground"> - 설계로부터 받은 RFQ 문서와 구매 RFQ 문서를 관리하고 Vendor 회신을 점검/관리하는 화면입니다. - </p> - </div> - <Separator /> - <div> - <RfqAttachmentsTable promises={promises} rfqId={idAsNumber} /> - </div> - </div> - ) -}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/(not-used)/b-rfq/page.tsx b/app/[lng]/evcp/(evcp)/(not-used)/b-rfq/page.tsx deleted file mode 100644 index 6dc0fb44..00000000 --- a/app/[lng]/evcp/(evcp)/(not-used)/b-rfq/page.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import * as React from "react" -import { Metadata } from "next" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { searchParamsRFQDashboardCache } from "@/lib/b-rfq/validations" -import { getRFQDashboard } from "@/lib/b-rfq/service" -import { RFQDashboardTable } from "@/lib/b-rfq/summary-table/summary-rfq-table" -import { InformationButton } from "@/components/information/information-button" - -export const metadata: Metadata = { - title: "견적 RFQ", - description: "", -} - -interface PQReviewPageProps { - searchParams: Promise<SearchParams> -} - -export default async function PQReviewPage(props: PQReviewPageProps) { - const searchParams = await props.searchParams - const search = searchParamsRFQDashboardCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - // 기본 필터 처리 (통일된 이름 사용) - let basicFilters = [] - if (search.basicFilters && search.basicFilters.length > 0) { - basicFilters = search.basicFilters - console.log("Using search.basicFilters:", basicFilters); - } else { - console.log("No basic filters found"); - } - - // 모든 필터를 합쳐서 처리 - const allFilters = [...validFilters, ...basicFilters] - - // 조인 연산자도 통일된 이름 사용 - const joinOperator = search.basicJoinOperator || search.joinOperator || 'and'; - - // Promise.all로 감싸서 전달 - const promises = Promise.all([ - getRFQDashboard({ - ...search, - filters: allFilters, - joinOperator, - }) - ]) - - console.log(search, "견적") - - return ( - <Shell className="gap-4"> - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <div className="flex items-center gap-2"> - <h2 className="text-2xl font-bold tracking-tight"> - 견적 RFQ - </h2> - <InformationButton pagePath="evcp/b-rfq" /> - </div> - </div> - </div> - </div> - - {/* Items처럼 직접 테이블 렌더링 */} - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={8} - searchableColumnCount={2} - filterableColumnCount={3} - cellWidths={["10rem", "15rem", "12rem", "12rem", "8rem", "8rem", "10rem", "8rem"]} - shrinkZero - /> - } - > - <RFQDashboardTable promises={promises} /> - </React.Suspense> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/(not-used)/bqcbe/page.tsx b/app/[lng]/evcp/(evcp)/(not-used)/bqcbe/page.tsx deleted file mode 100644 index 6f97efd3..00000000 --- a/app/[lng]/evcp/(evcp)/(not-used)/bqcbe/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getAllCBE } from "@/lib/rfqs/service" -import { searchParamsCBECache } from "@/lib/rfqs/validations" - -import { AllCbeTable } from "@/lib/cbe/table/cbe-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<SearchParams> - rfqType: RfqType -} - -export default async function RfqCBEPage(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 = searchParamsCBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getAllCBE({ - ...search, - filters: validFilters, - rfqType - } - ) - ]) - - // 4) 렌더링 - 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"> - CBE 보내기 - </h2> - {/* <p className="text-muted-foreground"> - 초대된 협력업체에게 CBE를 보낼 수 있습니다. <br/>체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 CBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. - </p> */} - </div> - </div> - </div> - - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <AllCbeTable promises={promises}/> - </React.Suspense> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/(not-used)/bqtbe/page.tsx b/app/[lng]/evcp/(evcp)/(not-used)/bqtbe/page.tsx deleted file mode 100644 index a1fee1b6..00000000 --- a/app/[lng]/evcp/(evcp)/(not-used)/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<SearchParams> - 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 ( - <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"> - TBE 보내기 - </h2> - {/* <p className="text-muted-foreground"> - 초대된 협력업체에게 TBE를 보낼 수 있습니다. <br/>체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. - </p> */} - </div> - </div> - </div> - - <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/(evcp)/(not-used)/budgetary-rfq/[id]/cbe/page.tsx b/app/[lng]/evcp/(evcp)/(not-used)/budgetary-rfq/[id]/cbe/page.tsx deleted file mode 100644 index 956facd3..00000000 --- a/app/[lng]/evcp/(evcp)/(not-used)/budgetary-rfq/[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<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/>체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 CBE 내용이 메일로 전달되고 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)/(not-used)/budgetary-rfq/[id]/layout.tsx b/app/[lng]/evcp/(evcp)/(not-used)/budgetary-rfq/[id]/layout.tsx deleted file mode 100644 index 2b80e64f..00000000 --- a/app/[lng]/evcp/(evcp)/(not-used)/budgetary-rfq/[id]/layout.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { Metadata } from "next" -import Link from "next/link" -import { ArrowLeft } from "lucide-react" - -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" -import { RfqViewWithItems } from "@/db/schema/rfq" -import { findRfqById } from "@/lib/rfqs/service" -import { formatDate } from "@/lib/utils" -import { Button } from "@/components/ui/button" - -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: RfqViewWithItems | 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="flex items-center justify-end mb-4"> - <Link href={`/${lng}/evcp/budgetary-rfq`} passHref> - <Button variant="ghost" className="flex items-center text-primary hover:text-primary/80 transition-colors p-0 h-auto"> - <ArrowLeft className="mr-1 h-4 w-4" /> - <span>Budgetary RFQ 목록으로 돌아가기</span> - </Button> - </Link> - </div> - <div className="space-y-0.5"> - {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} - <h2 className="text-2xl font-bold tracking-tight"> - {rfq - ? `${rfq.projectCode ?? ""} ${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 && rfq?.dueDate && <strong>{formatDate(rfq?.dueDate, "KR")}</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="lg:w-64 flex-shrink-0"> - <SidebarNav items={sidebarNavItems} /> - </aside> - <div className="lg:w-[calc(100%-16rem)] overflow-auto">{children}</div> - </div> - </div> - </section> - </div> - </> - ) -}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/(not-used)/budgetary-rfq/[id]/page.tsx b/app/[lng]/evcp/(evcp)/(not-used)/budgetary-rfq/[id]/page.tsx deleted file mode 100644 index dd9df563..00000000 --- a/app/[lng]/evcp/(evcp)/(not-used)/budgetary-rfq/[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<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)/(not-used)/budgetary-rfq/[id]/tbe/page.tsx b/app/[lng]/evcp/(evcp)/(not-used)/budgetary-rfq/[id]/tbe/page.tsx deleted file mode 100644 index ec894e1c..00000000 --- a/app/[lng]/evcp/(evcp)/(not-used)/budgetary-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<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/(evcp)/(not-used)/budgetary-rfq/page.tsx b/app/[lng]/evcp/(evcp)/(not-used)/budgetary-rfq/page.tsx deleted file mode 100644 index 290b9f27..00000000 --- a/app/[lng]/evcp/(evcp)/(not-used)/budgetary-rfq/page.tsx +++ /dev/null @@ -1,89 +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 { InformationButton } from "@/components/information/information-button" -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> - <div className="flex items-center gap-2"> - <h2 className="text-2xl font-bold tracking-tight"> - {title} - </h2> - <InformationButton pagePath="evcp/budgetary-rfq" /> - </div> - {/* <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)/(not-used)/budgetary/[id]/cbe/page.tsx b/app/[lng]/evcp/(evcp)/(not-used)/budgetary/[id]/cbe/page.tsx deleted file mode 100644 index 956facd3..00000000 --- a/app/[lng]/evcp/(evcp)/(not-used)/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<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/>체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 CBE 내용이 메일로 전달되고 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)/(not-used)/budgetary/[id]/layout.tsx b/app/[lng]/evcp/(evcp)/(not-used)/budgetary/[id]/layout.tsx deleted file mode 100644 index d58d8363..00000000 --- a/app/[lng]/evcp/(evcp)/(not-used)/budgetary/[id]/layout.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { Metadata } from "next" -import Link from "next/link" -import { ArrowLeft } from "lucide-react" -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" -import { RfqViewWithItems } from "@/db/schema/rfq" -import { findRfqById } from "@/lib/rfqs/service" -import { formatDate } from "@/lib/utils" -import { Button } from "@/components/ui/button" - -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: RfqViewWithItems | 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"> - {/* RFQ 목록으로 돌아가는 링크 추가 */} - <div className="flex items-center justify-end mb-4"> - <Link href={`/${lng}/evcp/budgetary`} passHref> - <Button variant="ghost" className="flex items-center text-primary hover:text-primary/80 transition-colors p-0 h-auto"> - <ArrowLeft className="mr-1 h-4 w-4" /> - <span>Budgetary Quote 목록으로 돌아가기</span> - </Button> - </Link> - </div> - - <div className="space-y-0.5"> - {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} - <h2 className="text-2xl font-bold tracking-tight"> - {rfq - ? `${rfq.projectCode ?? ""} ${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 && rfq?.dueDate && <strong>{formatDate(rfq?.dueDate, "KR")}</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="lg:w-64 flex-shrink-0"> - <SidebarNav items={sidebarNavItems} /> - </aside> - <div className="lg:w-[calc(100%-16rem)] overflow-auto">{children}</div> - </div> - </div> - </section> - </div> - </> - ) -}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/(not-used)/budgetary/[id]/page.tsx b/app/[lng]/evcp/(evcp)/(not-used)/budgetary/[id]/page.tsx deleted file mode 100644 index dd9df563..00000000 --- a/app/[lng]/evcp/(evcp)/(not-used)/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<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)/(not-used)/budgetary/[id]/tbe/page.tsx b/app/[lng]/evcp/(evcp)/(not-used)/budgetary/[id]/tbe/page.tsx deleted file mode 100644 index ec894e1c..00000000 --- a/app/[lng]/evcp/(evcp)/(not-used)/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<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/(evcp)/(not-used)/budgetary/page.tsx b/app/[lng]/evcp/(evcp)/(not-used)/budgetary/page.tsx deleted file mode 100644 index 15b4cdd4..00000000 --- a/app/[lng]/evcp/(evcp)/(not-used)/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<SearchParams>; - 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 ( - <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)/(not-used)/materials/page.tsx b/app/[lng]/evcp/(evcp)/(not-used)/materials/page.tsx deleted file mode 100644 index 00983a3f..00000000 --- a/app/[lng]/evcp/(evcp)/(not-used)/materials/page.tsx +++ /dev/null @@ -1,76 +0,0 @@ -/** - * 자재마스터 테이블 - * MDG 자재마스터를 그대로 보여줄 것임 - * 수정/추가 기능은 불필요 - */ - -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { getMaterials } from "@/lib/material/services" -import { MaterialTable } from "@/lib/material/table/material-table" -import { InformationButton } from "@/components/information/information-button" -import { searchParamsCache } from "@/lib/material/validations" - -interface MaterialPageProps { - searchParams: Promise<SearchParams> -} - -export default async function MaterialPage(props: MaterialPageProps) { - const searchParams = await props.searchParams - - // searchParamsCache를 사용해서 파라미터 파싱 - const search = searchParamsCache.parse(searchParams) - - // pageSize 기반으로 모드 자동 결정 - const isInfiniteMode = search.perPage >= 1_000_000 - - // 페이지네이션 모드일 때만 서버에서 데이터 가져오기 - // 무한 스크롤 모드에서는 클라이언트에서 SWR로 데이터 로드 - const promises = isInfiniteMode - ? undefined - : Promise.all([ - getMaterials(search as any), // 타입 캐스팅으로 임시 해결 - ]) - - 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> - <div className="flex items-center gap-2"> - <h2 className="text-2xl font-bold tracking-tight"> - 자재마스터 - </h2> - <InformationButton pagePath="evcp/material" /> - </div> - <p className="text-muted-foreground"> - MDG로부터 수신된 자재마스터 정보입니다. - </p> - </div> - </div> - </div> - - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - {/* 추가 컴포넌트가 필요한 경우 여기에 */} - </React.Suspense> - - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={5} - searchableColumnCount={1} - filterableColumnCount={3} - cellWidths={["10rem", "15rem", "20rem", "12rem", "12rem", "12rem"]} - shrinkZero - /> - } - > - <MaterialTable promises={promises} /> - </React.Suspense> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/(not-used)/po-rfq/page.tsx b/app/[lng]/evcp/(evcp)/(not-used)/po-rfq/page.tsx deleted file mode 100644 index 27e48384..00000000 --- a/app/[lng]/evcp/(evcp)/(not-used)/po-rfq/page.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { getPORfqs } from "@/lib/procurement-rfqs/services" -import { searchParamsCache } from "@/lib/procurement-rfqs/validations" -import { getValidFilters } from "@/lib/data-table" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { RFQListTable } from "@/lib/procurement-rfqs/table/rfq-table" -import { type SearchParams } from "@/types/table" -import * as React from "react" -import { InformationButton } from "@/components/information/information-button" -interface RfqPageProps { - searchParams: Promise<SearchParams> -} - -export default async function RfqPage(props: RfqPageProps) { - // searchParams를 await하여 resolve - const searchParams = await props.searchParams - - // 파라미터 파싱 - const search = searchParamsCache.parse(searchParams); - const validFilters = getValidFilters(search.filters); - - // RFQ 서버는 기본필터와 고급필터를 분리해서 받으므로 그대로 전달 - const promises = Promise.all([ - getPORfqs({ - ...search, // 모든 파라미터 전달 (page, perPage, sort, basicFilters, filters 등) - filters: validFilters, // 고급 필터를 명시적으로 오버라이드 (파싱된 버전) - }) - ]) - - return ( - <Shell variant="fullscreen" className="h-full"> {/* fullscreen variant 사용 */} - {/* 고정 헤더 영역 */} - <div className="flex-shrink-0"> - <div className="flex items-center justify-between"> - <div> - <div className="flex items-center gap-2"> - <h2 className="text-2xl font-bold tracking-tight"> - RFQ - </h2> - <InformationButton pagePath="evcp/po-rfq" /> - </div> - </div> - </div> - </div> - - {/* 테이블 영역 - 남은 공간 모두 차지 */} - <div className="flex-1 min-h-0"> - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={8} - searchableColumnCount={2} - filterableColumnCount={3} - cellWidths={["10rem", "15rem", "12rem", "12rem", "8rem", "8rem", "10rem", "8rem"]} - shrinkZero - /> - } - > - <RFQListTable promises={promises} className="h-full" /> - </React.Suspense> - </div> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/(not-used)/poa/page.tsx b/app/[lng]/evcp/(evcp)/(not-used)/poa/page.tsx deleted file mode 100644 index 0ced4957..00000000 --- a/app/[lng]/evcp/(evcp)/(not-used)/poa/page.tsx +++ /dev/null @@ -1,64 +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" -import { InformationButton } from "@/components/information/information-button" -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -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 ( - <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> - <div className="flex items-center gap-2"> - <h2 className="text-2xl font-bold tracking-tight"> - 변경 PO 확인 및 전자서명 - </h2> - <InformationButton pagePath="evcp/poa" /> - </div> - {/* <p className="text-muted-foreground"> - 발행된 PO의 변경 내역을 확인하고 관리할 수 있습니다. - </p> */} - </div> - </div> - </div> - - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - </React.Suspense> - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <ChangeOrderListsTable promises={promises} /> - </React.Suspense> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/(not-used)/project-gtc/page.tsx b/app/[lng]/evcp/(evcp)/(not-used)/project-gtc/page.tsx deleted file mode 100644 index ac9ce03c..00000000 --- a/app/[lng]/evcp/(evcp)/(not-used)/project-gtc/page.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { getProjectGtcList } from "@/lib/project-gtc/service" -import { projectGtcSearchParamsSchema } from "@/lib/project-gtc/validations" -import { ProjectGtcTable } from "@/lib/project-gtc/table/project-gtc-table" -import { InformationButton } from "@/components/information/information-button" -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = projectGtcSearchParamsSchema.parse(searchParams) - - const promises = Promise.all([ - getProjectGtcList({ - page: search.page, - perPage: search.perPage, - search: search.search, - sort: search.sort, - }), - ]) - - 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> - <div className="flex items-center gap-2"> - <h2 className="text-2xl font-bold tracking-tight"> - Project GTC 관리 - </h2> - <InformationButton pagePath="evcp/project-gtc" /> - </div> - {/* <p className="text-muted-foreground"> - 프로젝트별 GTC(General Terms and Conditions) 파일을 관리할 수 있습니다. - 각 프로젝트마다 하나의 GTC 파일을 업로드할 수 있으며, 파일 업로드 시 기존 파일은 자동으로 교체됩니다. - </p> */} - </div> - </div> - </div> - - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - {/* 추가 기능이 필요하면 여기에 추가 */} - </React.Suspense> - - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={8} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["3rem", "3rem", "12rem", "20rem", "10rem", "20rem", "15rem", "12rem", "3rem"]} - shrinkZero - /> - } - > - <ProjectGtcTable promises={promises} /> - </React.Suspense> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/(not-used)/project-vendors/page.tsx b/app/[lng]/evcp/(evcp)/(not-used)/project-vendors/page.tsx deleted file mode 100644 index 07caddbb..00000000 --- a/app/[lng]/evcp/(evcp)/(not-used)/project-vendors/page.tsx +++ /dev/null @@ -1,77 +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 { ProjectAVLTable } from "@/lib/project-avl/table/proejctAVL-table" -import { getProjecTAVL } from "@/lib/project-avl/service" -import { searchProjectAVLParamsCache } from "@/lib/project-avl/validations" -import { InformationButton } from "@/components/information/information-button" - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchProjectAVLParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getProjecTAVL({ - ...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> - <div className="flex items-center gap-2"> - <h2 className="text-2xl font-bold tracking-tight"> - 프로젝트 AVL 리스트 - </h2> - <InformationButton pagePath="evcp/project-vendors" /> - </div> - {/* <p className="text-muted-foreground"> - 프로젝트 PQ를 통과한 벤더의 리스트를 보여줍니다.{" "} - <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 - /> - } - > - <ProjectAVLTable promises={promises} /> - </React.Suspense> - </Shell> - ) -} diff --git a/app/[lng]/evcp/(evcp)/(not-used)/rfq/[id]/cbe/page.tsx b/app/[lng]/evcp/(evcp)/(not-used)/rfq/[id]/cbe/page.tsx deleted file mode 100644 index fb288a98..00000000 --- a/app/[lng]/evcp/(evcp)/(not-used)/rfq/[id]/cbe/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 { searchParamsCBECache } from "@/lib/rfqs/validations" -import { getCBE } from "@/lib/rfqs/service" -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 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 = 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 />"발행하기" 버튼을 통해 CBE를 전송하면 CBE 내용이 메일로 전달되고 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)/(not-used)/rfq/[id]/layout.tsx b/app/[lng]/evcp/(evcp)/(not-used)/rfq/[id]/layout.tsx deleted file mode 100644 index 92817b4b..00000000 --- a/app/[lng]/evcp/(evcp)/(not-used)/rfq/[id]/layout.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { Metadata } from "next" -import Link from "next/link" -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" -import { RfqViewWithItems } from "@/db/schema/rfq" -import { findRfqById } from "@/lib/rfqs/service" -import { formatDate } from "@/lib/utils" -import { Button } from "@/components/ui/button" -import { ArrowLeft } from "lucide-react" - -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: RfqViewWithItems | 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 ( - <> - <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="flex items-center justify-end mb-4"> - <Link href={`/${lng}/evcp/rfq`} passHref> - <Button variant="ghost" className="flex items-center text-primary hover:text-primary/80 transition-colors p-0 h-auto"> - <ArrowLeft className="mr-1 h-4 w-4" /> - <span>RFQ 목록으로 돌아가기</span> - </Button> - </Link> - </div> - <div className="space-y-0.5"> - {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} - <h2 className="text-2xl font-bold tracking-tight"> - {rfq - ? `${rfq.projectCode ?? ""} ${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 && rfq?.dueDate && <strong>{formatDate(rfq?.dueDate, "KR")}</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="lg:w-64 flex-shrink-0"> - <SidebarNav items={sidebarNavItems} /> - </aside> - <div className="lg:w-[calc(100%-16rem)] overflow-auto">{children}</div> - </div> - </div> - </section> - </div> - </> - ) -}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/(not-used)/rfq/[id]/page.tsx b/app/[lng]/evcp/(evcp)/(not-used)/rfq/[id]/page.tsx deleted file mode 100644 index 1a9f4b18..00000000 --- a/app/[lng]/evcp/(evcp)/(not-used)/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<SearchParams> -} - -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 ( - <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}/> - </div> - </div> - ) -}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/(not-used)/rfq/[id]/tbe/page.tsx b/app/[lng]/evcp/(evcp)/(not-used)/rfq/[id]/tbe/page.tsx deleted file mode 100644 index 76eea302..00000000 --- a/app/[lng]/evcp/(evcp)/(not-used)/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<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를 전송하면 첨부파일과 함께 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/(evcp)/(not-used)/rfq/page.tsx b/app/[lng]/evcp/(evcp)/(not-used)/rfq/page.tsx deleted file mode 100644 index 26f49cfb..00000000 --- a/app/[lng]/evcp/(evcp)/(not-used)/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<SearchParams>; - 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 ( - <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} - </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)/(not-used)/tasks/page.tsx b/app/[lng]/evcp/(evcp)/(not-used)/tasks/page.tsx deleted file mode 100644 index 91b946fb..00000000 --- a/app/[lng]/evcp/(evcp)/(not-used)/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<SearchParams> -} - -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 ( - <Shell className="gap-2"> - <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 - /> - } - > - <TasksTable promises={promises} /> - </React.Suspense> - </Shell> - ) -} diff --git a/app/[lng]/evcp/(evcp)/(not-used)/tbe/page.tsx b/app/[lng]/evcp/(evcp)/(not-used)/tbe/page.tsx deleted file mode 100644 index 211cf376..00000000 --- a/app/[lng]/evcp/(evcp)/(not-used)/tbe/page.tsx +++ /dev/null @@ -1,113 +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" -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"> - TBE 관리 - </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/db/schema/bRfq.ts b/db/schema/bRfq.ts deleted file mode 100644 index 6eef6ee3..00000000 --- a/db/schema/bRfq.ts +++ /dev/null @@ -1,1409 +0,0 @@ -import { pgTable, pgView, serial, varchar, text, timestamp, boolean, integer, numeric, date, uniqueIndex, foreignKey } from "drizzle-orm/pg-core"; -import { eq, sql, and, relations } from "drizzle-orm"; -import { projects } from "./projects"; -import { users } from "./users"; -import { vendors } from "./vendors"; -import { incoterms, paymentTerms } from "./procurementRFQ"; - -export const bRfqs = pgTable( - "b_rfqs", - { - id: serial("id").primaryKey(), - - // RFQ 고유 코드 - rfqCode: varchar("rfq_code", { length: 50 }).unique(), // ex) "RFQ-2025-001" - - // 프로젝트 참조 - projectId: integer("project_id") - .references(() => projects.id, { onDelete: "set null" }), - - description: varchar("description", { length: 255 }), - - remark: text("remark"), - - dueDate: date("due_date", { mode: "date" }) - .$type<Date>() - .notNull(), - - status: varchar("status", { length: 30 }) - .$type<"DRAFT" | "Doc. Received" | "PIC Assigned" | "Doc. Confirmed" | "Init. RFQ Sent" | "Init. RFQ Answered" | "TBE started" | "TBE finished" | "Final RFQ Sent" | "Quotation Received" | "Vendor Selected">() - .default("DRAFT") - .notNull(), - - picCode: varchar("pic_code", { length: 50 }), - picName: varchar("pic_name", { length: 50 }), - EngPicName: varchar("eng_pic_name", { length: 50 }), - - projectCompany: varchar("project_company", { length: 255 }), - projectFlag: varchar("project_flag", { length: 255 }), - projectSite: varchar("project_site", { length: 255 }), - - packageNo: varchar("package_no", { length: 50 }), - packageName: varchar("package_name", { length: 255 }), - - // 생성자 - createdBy: integer("created_by") - .notNull() - .references(() => users.id, { onDelete: "set null" }), - - updatedBy: integer("updated_by") - .notNull() - .references(() => users.id, { onDelete: "set null" }), - - createdAt: timestamp("created_at").defaultNow().notNull(), - updatedAt: timestamp("updated_at").defaultNow().notNull(), - } -); - -export const initialRfq = pgTable("initial_rfq", { - id: serial("id").primaryKey(), - rfqId: integer("rfq_id") - .notNull() - .references(() => bRfqs.id), - - initialRfqStatus: varchar("initial_rfq_status", { length: 30 }) - .$type<"DRAFT" | "Init. RFQ Sent" | "S/L Decline" | "Init. RFQ Answered">() - .default("DRAFT") - .notNull(), - - vendorId: integer("vendor_id") - .notNull() - .references(() => vendors.id), - - dueDate: date("due_date", { mode: "date" }) - .$type<Date>() - .notNull(), - - validDate: date("valid_date", { mode: "date" }) - .$type<Date>(), - - incotermsCode: varchar("incoterms_code", { length: 20 }) - .references(() => incoterms.code, { onDelete: "set null" }), - - gtc: varchar("gtc", { length: 255 }), - gtcValidDate: varchar("gtc_valid_date", { length: 255 }), - - classification: varchar("classification", { length: 255 }), - sparepart: varchar("sparepart", { length: 255 }), - - shortList: boolean('short_list').notNull().default(false), - returnYn: boolean('return_yn').notNull().default(false), - - cpRequestYn: boolean('cp_request_yn').notNull().default(false), - - prjectGtcYn: boolean('prject_gtc_yn').notNull().default(false), - - returnRevision: integer("return_revision") - .notNull().default(0), - - rfqRevision: integer("rfq_revision") - .notNull().default(0), - - createdAt: timestamp("created_at").defaultNow().notNull(), - updatedAt: timestamp("updated_at").defaultNow().notNull(), -}); - -export const finalRfq = pgTable("final_rfq", { - id: serial("id").primaryKey(), - rfqId: integer("rfq_id") - .notNull() - .references(() => bRfqs.id), - - finalRfqStatus: varchar("final_rfq_status", { length: 30 }) - .$type<"DRAFT" | "Final RFQ Sent" | "Final RFQ Answered">() - .default("DRAFT") - .notNull(), - - vendorId: integer("vendor_id") - .notNull() - .references(() => vendors.id), - - dueDate: date("due_date", { mode: "date" }) - .$type<Date>() - .notNull(), - - validDate: date("valid_date", { mode: "date" }) - .$type<Date>(), - - incotermsCode: varchar("incoterms_code", { length: 20 }) - .references(() => incoterms.code, { onDelete: "set null" }), - - gtc: varchar("gtc", { length: 255 }), - gtcValidDate: varchar("gtc_valid_date", { length: 255 }), - - classification: varchar("classification", { length: 255 }), - sparepart: varchar("sparepart", { length: 255 }), - - shortList: boolean('short_list').notNull().default(false), - returnYn: boolean('return_yn').notNull().default(false), - - cpRequestYn: boolean('cp_request_yn').notNull().default(false), - - prjectGtcYn: boolean('prject_gtc_yn').notNull().default(true), - - returnRevision: integer("return_revision") - .notNull().default(0), - - currency: varchar("currency", { length: 10 }).default("KRW"), - - paymentTermsCode: varchar("payment_terms_code", { length: 50 }) - .references(() => paymentTerms.code, { onDelete: "set null" }), - - taxCode: varchar("tax_code", { length: 255 }).default("VV"), - deliveryDate: date("delivery_date", { mode: "date" }) - .$type<Date>() - .notNull(), - - placeOfShipping: varchar("place_of_shipping", { length: 255 }), - placeOfDestination: varchar("place_of_destination", { length: 255 }), - - firsttimeYn: boolean('firsttime_yn').notNull().default(true), - materialPriceRelatedYn: boolean("material_price_related_yn").default(false), - remark: text("remark"), - - vendorRemark: text("vendor_remark"), - - createdAt: timestamp("created_at").defaultNow().notNull(), - updatedAt: timestamp("updated_at").defaultNow().notNull(), -}); - -export const bRfqsAttachments = pgTable( - "b_rfq_attachments", - { - id: serial("id").primaryKey(), - attachmentType: varchar("attachment_type", { length: 50 }).notNull(), - serialNo: varchar("serial_no", { length: 50 }).notNull(), - rfqId: integer("rfq_id") - .notNull() - .references(() => bRfqs.id), - - // 현재 리비전 정보 (빠른 접근용) - currentRevision: varchar("current_revision", { length: 10 }).notNull().default("Rev.0"), - latestRevisionId: integer("latest_revision_id"), // self-reference to bRfqAttachmentRevisions - - // 메타 정보 - description: varchar("description", { length: 500 }), - createdBy: integer("created_by") - .references(() => users.id, { onDelete: "set null" }) - .notNull(), - createdAt: timestamp("created_at").defaultNow().notNull(), - updatedAt: timestamp("updated_at").defaultNow().notNull(), - } -) - -// 리비전 테이블 (모든 파일 버전 관리) -export const bRfqAttachmentRevisions = pgTable( - "b_rfq_attachment_revisions", - { - id: serial("id").primaryKey(), - attachmentId: integer("attachment_id") - .notNull() - .references(() => bRfqsAttachments.id, { onDelete: "cascade" }), - - // 리비전 정보 - revisionNo: varchar("revision_no", { length: 10 }).notNull(), // "Rev.0", "Rev.1", "Rev.2" - revisionComment: text("revision_comment"), - isLatest: boolean("is_latest").notNull().default(true), - - // 파일 정보 - fileName: varchar("file_name", { length: 255 }).notNull(), - originalFileName: varchar("original_file_name", { length: 255 }).notNull(), - filePath: varchar("file_path", { length: 512 }).notNull(), - fileSize: integer("file_size"), - fileType: varchar("file_type", { length: 100 }), - - // 리비전 생성 정보 - createdBy: integer("created_by") - .references(() => users.id, { onDelete: "set null" }) - .notNull(), - createdAt: timestamp("created_at").defaultNow().notNull(), - - - }, - (t) => ({ - // 첨부파일당 하나의 최신 리비전만 허용 - latestRevisionIdx: uniqueIndex('latest_revision_idx') - .on(t.attachmentId, t.isLatest) - .where(eq(t.isLatest, true)), - - // 첨부파일 + 리비전 번호 유니크 - attachmentRevisionIdx: uniqueIndex('attachment_revision_idx') - .on(t.attachmentId, t.revisionNo), - }) -) - -// 첨부파일 + 최신 리비전 뷰 -export const attachmentsWithLatestRevisionView = pgView("attachments_with_latest_revision", { - // 메인 첨부파일 정보 - attachmentId: integer("attachment_id"), - attachmentType: varchar("attachment_type", { length: 50 }), - serialNo: varchar("serial_no", { length: 50 }), - rfqId: integer("rfq_id"), - description: varchar("description", { length: 500 }), - currentRevision: varchar("current_revision", { length: 10 }), - - // 최신 리비전 파일 정보 - revisionId: integer("revision_id"), - fileName: varchar("file_name", { length: 255 }), - originalFileName: varchar("original_file_name", { length: 255 }), - filePath: varchar("file_path", { length: 512 }), - fileSize: integer("file_size"), - fileType: varchar("file_type", { length: 100 }), - revisionComment: text("revision_comment"), - - // 생성/수정 정보 - createdBy: integer("created_by"), - createdByName: varchar("created_by_name", { length: 255 }), - createdAt: timestamp("created_at"), - updatedAt: timestamp("updated_at"), -}).as(sql` - SELECT - a.id as attachment_id, - a.attachment_type, - a.serial_no, - a.rfq_id, - a.description, - a.current_revision, - - r.id as revision_id, - r.file_name, - r.original_file_name, - r.file_path, - r.file_size, - r.file_type, - r.revision_comment, - - a.created_by, - u.name as created_by_name, - a.created_at, - a.updated_at - FROM b_rfq_attachments a - LEFT JOIN b_rfq_attachment_revisions r ON a.latest_revision_id = r.id - LEFT JOIN users u ON a.created_by = u.id - `) - -// 2. 벤더별 첨부파일 응답 현황 관리 -export const vendorAttachmentResponses = pgTable( - "vendor_attachment_responses", - { - id: serial("id").primaryKey(), - attachmentId: integer("attachment_id") - .notNull() - .references(() => bRfqsAttachments.id, { onDelete: "cascade" }), - - vendorId: integer("vendor_id") - .notNull() - .references(() => vendors.id, { onDelete: "cascade" }), - - rfqType: varchar("rfq_type", { length: 20 }) - .$type<"INITIAL" | "FINAL">() - .notNull(), // initial_rfq 또는 final_rfq 구분 - - rfqRecordId: integer("rfq_record_id").notNull(), // initialRfq.id 또는 finalRfq.id - - responseStatus: varchar("response_status", { length: 30 }) - .$type<"NOT_RESPONDED" | "RESPONDED" | "REVISION_REQUESTED" | "WAIVED">() - .default("NOT_RESPONDED") - .notNull(), - - currentRevision: varchar("current_revision", { length: 10 }).default("Rev.0"), - respondedRevision: varchar("responded_revision", { length: 10 }), - - responseComment: text("response_comment"), - vendorComment: text("vendor_comment"), - - revisionRequestComment: text("revision_request_comment"), - - - // 응답 관련 날짜 - requestedAt: timestamp("requested_at").notNull(), - respondedAt: timestamp("responded_at"), - revisionRequestedAt: timestamp("revision_requested_at"), - - createdAt: timestamp("created_at").defaultNow().notNull(), - updatedAt: timestamp("updated_at").defaultNow().notNull(), - - createdBy: integer("created_by") - .references(() => users.id, { onDelete: "set null" }) - , - - updatedBy: integer("updated_by") - .references(() => users.id, { onDelete: "set null" }) - , - }, (t) => ({ - // attachmentId + vendorId + rfqType 유니크 - vendorResponseIdx: uniqueIndex('vendor_response_idx').on( - t.attachmentId.asc(), - t.vendorId.asc(), - t.rfqType.asc(), - ), - })); - -// 3. 벤더 응답 첨부파일 -export const vendorResponseAttachmentsB = pgTable( - "vendor_response_attachments_b", - { - id: serial("id").primaryKey(), - vendorResponseId: integer("vendor_response_id") - .notNull() - .references(() => vendorAttachmentResponses.id, { onDelete: "cascade" }), - - fileName: varchar("file_name", { length: 255 }).notNull(), - originalFileName: varchar("original_file_name", { length: 255 }).notNull(), - filePath: varchar("file_path", { length: 512 }).notNull(), - fileSize: integer("file_size"), - fileType: varchar("file_type", { length: 100 }), - - description: varchar("description", { length: 500 }), - - uploadedBy: integer("uploaded_by") - .references(() => users.id, { onDelete: "set null" }), - uploadedAt: timestamp("uploaded_at").defaultNow().notNull(), - }, -); - -// 4. 응답 히스토리 추적 (선택사항) -export const vendorResponseHistory = pgTable( - "vendor_response_history", - { - id: serial("id").primaryKey(), - vendorResponseId: integer("vendor_response_id") - .notNull() - .references(() => vendorAttachmentResponses.id, { onDelete: "cascade" }), - - action: varchar("action", { length: 50 }) - .$type<"REQUESTED" | "RESPONDED" | "REVISION_REQUESTED" | "REVISED" | "WAIVED">() - .notNull(), - - previousStatus: varchar("previous_status", { length: 30 }), - newStatus: varchar("new_status", { length: 30 }), - - comment: text("comment"), - - actionBy: integer("action_by") - .references(() => users.id, { onDelete: "set null" }), - actionAt: timestamp("action_at").defaultNow().notNull(), - }, -); - -// === 유용한 뷰들 === - -// 1. bRfqs 기본 마스터 뷰 (프로젝트 정보 포함) -export const bRfqsMasterView = pgView("b_rfqs_master", { - rfqId: integer("rfq_id"), - rfqCode: varchar("rfq_code", { length: 50 }), - description: varchar("description", { length: 255 }), - status: varchar("status", { length: 30 }), - dueDate: date("due_date"), - picCode: varchar("pic_code", { length: 50 }), - picName: varchar("pic_name", { length: 50 }), - EngPicName: varchar("eng_pic_name", { length: 50 }), - packageNo: varchar("package_no", { length: 50 }), - packageName: varchar("package_name", { length: 255 }), - projectId: integer("project_id"), - projectCode: varchar("project_code", { length: 50 }), - projectName: text("project_name"), - projectType: varchar("project_type", { length: 20 }), - projectCompany: varchar("project_company", { length: 255 }), - projectFlag: varchar("project_flag", { length: 255 }), - projectSite: varchar("project_site", { length: 255 }), - totalAttachments: integer("total_attachments"), - createdAt: timestamp("created_at"), - updatedAt: timestamp("updated_at"), -}).as(sql` - SELECT - br.id as rfq_id, - br.rfq_code, - br.description, - br.status, - br.due_date, - br.pic_code, - br.pic_name, - br.eng_pic_name, - br.package_no, - br.package_name, - br.project_id, - p.code as project_code, - p.name as project_name, - p.type as project_type, - br.project_company, - br.project_flag, - br.project_site, - COALESCE(att_count.total_attachments, 0) as total_attachments, - br.created_at, - br.updated_at - FROM b_rfqs br - LEFT JOIN projects p ON br.project_id = p.id - LEFT JOIN ( - SELECT rfq_id, COUNT(*) as total_attachments - FROM b_rfq_attachments - GROUP BY rfq_id - ) att_count ON br.id = att_count.rfq_id -`); - -// 2. Initial RFQ 상세 뷰 (벤더, 인코텀즈 정보 포함) -export const initialRfqDetailView = pgView("initial_rfq_detail", { - rfqId: integer("rfq_id"), - rfqCode: varchar("rfq_code", { length: 50 }), - rfqStatus: varchar("rfq_status", { length: 30 }), - initialRfqId: integer("initial_rfq_id"), - initialRfqStatus: varchar("initial_rfq_status", { length: 30 }), - vendorId: integer("vendor_id"), - vendorCode: varchar("vendor_code", { length: 50 }), - vendorName: varchar("vendor_name", { length: 255 }), - vendorCategory: varchar("vendor_category", { length: 255 }), - vendorCountry: varchar("vendor_country", { length: 100 }), - vendorBusinessSize: varchar("vendor_business_size", { length: 50 }), - dueDate: date("due_date"), - validDate: date("valid_date"), - incotermsCode: varchar("incoterms_code", { length: 20 }), - incotermsDescription: varchar("incoterms_description", { length: 255 }), - shortList: boolean("short_list"), - returnYn: boolean("return_yn"), - cpRequestYn: boolean("cp_request_yn"), - prjectGtcYn: boolean("prject_gtc_yn"), - returnRevision: integer("return_revision"), - rfqRevision: integer("rfq_revision"), - gtc: varchar("gtc", { length: 255 }), - gtcValidDate: varchar("gtc_valid_date", { length: 255 }), - classification: varchar("classification", { length: 255 }), - sparepart: varchar("sparepart", { length: 255 }), - createdAt: timestamp("created_at"), - updatedAt: timestamp("updated_at"), -}).as(sql` - SELECT - br.id as rfq_id, - br.rfq_code, - br.status as rfq_status, - ir.id as initial_rfq_id, - ir.initial_rfq_status, - ir.vendor_id, - v.vendor_code, - v.vendor_name, - v.country as vendor_country, - v.business_size as vendor_business_size, - v.vendor_category as vendor_category, - ir.due_date, - ir.valid_date, - ir.incoterms_code, - inc.description as incoterms_description, - ir.short_list, - ir.return_yn, - ir.cp_request_yn, - ir.prject_gtc_yn, - ir.return_revision, - ir.rfq_revision, - ir.gtc, - ir.gtc_valid_date, - ir.classification, - ir.sparepart, - ir.created_at, - ir.updated_at - FROM b_rfqs br - JOIN initial_rfq ir ON br.id = ir.rfq_id - LEFT JOIN vendors_with_types v ON ir.vendor_id = v.id - LEFT JOIN incoterms inc ON ir.incoterms_code = inc.code -`); - -// 3. Final RFQ 상세 뷰 (벤더, 인코텀즈, 결제조건 정보 포함) -export const finalRfqDetailView = pgView("final_rfq_detail", { - rfqId: integer("rfq_id"), - rfqCode: varchar("rfq_code", { length: 50 }), - rfqStatus: varchar("rfq_status", { length: 30 }), - finalRfqId: integer("final_rfq_id"), - finalRfqStatus: varchar("final_rfq_status", { length: 30 }), - vendorId: integer("vendor_id"), - vendorCode: varchar("vendor_code", { length: 50 }), - vendorName: varchar("vendor_name", { length: 255 }), - vendorCountry: varchar("vendor_country", { length: 100 }), - vendorBusinessSize: varchar("vendor_business_size", { length: 50 }), - dueDate: date("due_date"), - validDate: date("valid_date"), - deliveryDate: date("delivery_date"), - incotermsCode: varchar("incoterms_code", { length: 20 }), - incotermsDescription: varchar("incoterms_description", { length: 255 }), - paymentTermsCode: varchar("payment_terms_code", { length: 50 }), - paymentTermsDescription: varchar("payment_terms_description", { length: 255 }), - currency: varchar("currency", { length: 10 }), - taxCode: varchar("tax_code", { length: 255 }), - placeOfShipping: varchar("place_of_shipping", { length: 255 }), - placeOfDestination: varchar("place_of_destination", { length: 255 }), - shortList: boolean("short_list"), - returnYn: boolean("return_yn"), - cpRequestYn: boolean("cp_request_yn"), - prjectGtcYn: boolean("prject_gtc_yn"), - firsttimeYn: boolean("firsttime_yn"), - materialPriceRelatedYn: boolean("material_price_related_yn"), - returnRevision: integer("return_revision"), - gtc: varchar("gtc", { length: 255 }), - gtcValidDate: varchar("gtc_valid_date", { length: 255 }), - classification: varchar("classification", { length: 255 }), - sparepart: varchar("sparepart", { length: 255 }), - remark: text("remark"), - vendorRemark: text("vendor_remark"), - createdAt: timestamp("created_at"), - updatedAt: timestamp("updated_at"), -}).as(sql` - SELECT - br.id as rfq_id, - br.rfq_code, - br.status as rfq_status, - fr.id as final_rfq_id, - fr.final_rfq_status, - fr.vendor_id, - v.vendor_code, - v.vendor_name, - v.country as vendor_country, - v.business_size as vendor_business_size, - fr.due_date, - fr.valid_date, - fr.delivery_date, - fr.incoterms_code, - inc.description as incoterms_description, - fr.payment_terms_code, - pt.description as payment_terms_description, - fr.currency, - fr.tax_code, - fr.place_of_shipping, - fr.place_of_destination, - fr.short_list, - fr.return_yn, - fr.cp_request_yn, - fr.prject_gtc_yn, - fr.firsttime_yn, - fr.material_price_related_yn, - fr.return_revision, - fr.gtc, - fr.gtc_valid_date, - fr.classification, - fr.sparepart, - fr.remark, - fr.vendor_remark, - fr.created_at, - fr.updated_at - FROM b_rfqs br - JOIN final_rfq fr ON br.id = fr.rfq_id - LEFT JOIN vendors v ON fr.vendor_id = v.id - LEFT JOIN incoterms inc ON fr.incoterms_code = inc.code - LEFT JOIN payment_terms pt ON fr.payment_terms_code = pt.code -`); - -// 4. 벤더 응답 현황 요약 뷰 -export const vendorResponseSummaryView2 = pgView("vendor_response_summary", { - rfqId: integer("rfq_id"), - rfqCode: varchar("rfq_code", { length: 50 }), - rfqStatus: varchar("rfq_status", { length: 30 }), - vendorId: integer("vendor_id"), - vendorCode: varchar("vendor_code", { length: 50 }), - vendorName: varchar("vendor_name", { length: 255 }), - vendorCountry: varchar("vendor_country", { length: 100 }), - vendorBusinessSize: varchar("vendor_business_size", { length: 50 }), - rfqType: varchar("rfq_type", { length: 20 }), - totalAttachments: integer("total_attachments"), - respondedCount: integer("responded_count"), - pendingCount: integer("pending_count"), - waivedCount: integer("waived_count"), - revisionRequestedCount: integer("revision_requested_count"), - responseRate: numeric("response_rate", { precision: 5, scale: 2 }), - completionRate: numeric("completion_rate", { precision: 5, scale: 2 }), -}).as(sql` - SELECT - br.id as rfq_id, - br.rfq_code, - br.status as rfq_status, - v.id as vendor_id, - v.vendor_code, - v.vendor_name, - v.country as vendor_country, - v.business_size as vendor_business_size, - var.rfq_type, - COUNT(var.id) as total_attachments, - COUNT(CASE WHEN var.response_status = 'RESPONDED' THEN 1 END) as responded_count, - COUNT(CASE WHEN var.response_status = 'NOT_RESPONDED' THEN 1 END) as pending_count, - COUNT(CASE WHEN var.response_status = 'WAIVED' THEN 1 END) as waived_count, - COUNT(CASE WHEN var.response_status = 'REVISION_REQUESTED' THEN 1 END) as revision_requested_count, - ROUND( - (COUNT(CASE WHEN var.response_status = 'RESPONDED' THEN 1 END) * 100.0 / - NULLIF(COUNT(CASE WHEN var.response_status != 'WAIVED' THEN 1 END), 0)), - 2 - ) as response_rate, - ROUND( - ((COUNT(CASE WHEN var.response_status = 'RESPONDED' THEN 1 END) + - COUNT(CASE WHEN var.response_status = 'WAIVED' THEN 1 END)) * 100.0 / COUNT(var.id)), - 2 - ) as completion_rate - FROM b_rfqs br - JOIN b_rfq_attachments bra ON br.id = bra.rfq_id - JOIN vendor_attachment_responses var ON bra.id = var.attachment_id - JOIN vendors v ON var.vendor_id = v.id - GROUP BY br.id, br.rfq_code, br.status, v.id, v.vendor_code, v.vendor_name, v.country, v.business_size, var.rfq_type -`); - -// 5. RFQ 전체 진행 현황 대시보드 뷰 -export const rfqDashboardView = pgView("rfq_dashboard", { - rfqId: integer("rfq_id"), - rfqCode: varchar("rfq_code", { length: 50 }), - description: varchar("description", { length: 255 }), - status: varchar("status", { length: 30 }), - dueDate: date("due_date"), - projectCode: varchar("project_code", { length: 50 }), - projectName: text("project_name"), - packageNo: varchar("package_no", { length: 50 }), - packageName: varchar("package_name", { length: 255 }), - picCode: varchar("pic_code", { length: 50 }), - picName: varchar("pic_name", { length: 50 }), - engPicName: varchar("eng_pic_name", { length: 50 }), - projectCompany: varchar("project_company", { length: 255 }), - projectFlag: varchar("project_flag", { length: 255 }), - projectSite: varchar("project_site", { length: 255 }), - totalAttachments: integer("total_attachments"), - initialVendorCount: integer("initial_vendor_count"), - finalVendorCount: integer("final_vendor_count"), - initialResponseRate: numeric("initial_response_rate", { precision: 5, scale: 2 }), - finalResponseRate: numeric("final_response_rate", { precision: 5, scale: 2 }), - overallProgress: numeric("overall_progress", { precision: 5, scale: 2 }), - daysToDeadline: integer("days_to_deadline"), - createdAt: timestamp("created_at"), - updatedAt: timestamp("updated_at"), - remark: text("remark"), - updatedByName: varchar("updated_by_name", { length: 255 }), - updatedByEmail: varchar("updated_by_email", { length: 255 }), -}).as(sql` - -- ② SELECT 절 확장 ------------------------------------------- - SELECT - br.id AS rfq_id, - br.rfq_code, - br.description, - br.status, - br.due_date, - p.code AS project_code, - p.name AS project_name, - br.package_no, - br.package_name, - br.pic_code, - br.pic_name, - br.eng_pic_name, - br.project_company, - br.project_flag, - br.project_site, - br.remark, - - -- 첨부/벤더 요약 ----------------------- - COALESCE(att_count.total_attachments, 0) AS total_attachments, - COALESCE(init_summary.vendor_count, 0) AS initial_vendor_count, - COALESCE(final_summary.vendor_count, 0) AS final_vendor_count, - COALESCE(init_summary.avg_response_rate, 0) AS initial_response_rate, - COALESCE(final_summary.avg_response_rate, 0) AS final_response_rate, - - -- 진행률·마감까지 일수 -------------- - CASE - WHEN br.status = 'DRAFT' THEN 0 - WHEN br.status = 'Doc. Received' THEN 10 - WHEN br.status = 'PIC Assigned' THEN 20 - WHEN br.status = 'Doc. Confirmed' THEN 30 - WHEN br.status = 'Init. RFQ Sent' THEN 40 - WHEN br.status = 'Init. RFQ Answered' THEN 50 - WHEN br.status = 'TBE started' THEN 60 - WHEN br.status = 'TBE finished' THEN 70 - WHEN br.status = 'Final RFQ Sent' THEN 80 - WHEN br.status = 'Quotation Received' THEN 90 - WHEN br.status = 'Vendor Selected' THEN 100 - ELSE 0 - END AS overall_progress, - (br.due_date - CURRENT_DATE) AS days_to_deadline, - - br.created_at, - br.updated_at, - - -- 💡 추가되는 컬럼 ------------------- - upd.name AS updated_by_name, - upd.email AS updated_by_email - FROM b_rfqs br - LEFT JOIN projects p ON br.project_id = p.id - - -- ③ 사용자 정보 조인 -------------------- - LEFT JOIN users upd ON br.updated_by = upd.id - - -- (나머지 이미 있던 JOIN 들은 그대로) ----- - LEFT JOIN ( - SELECT rfq_id, COUNT(*) AS total_attachments - FROM b_rfq_attachments - GROUP BY rfq_id - ) att_count ON br.id = att_count.rfq_id - - LEFT JOIN ( - SELECT - rfq_id, - COUNT(DISTINCT vendor_id) AS vendor_count, - AVG(response_rate) AS avg_response_rate - FROM vendor_response_summary - WHERE rfq_type = 'INITIAL' - GROUP BY rfq_id - ) init_summary ON br.id = init_summary.rfq_id - - LEFT JOIN ( - SELECT - rfq_id, - COUNT(DISTINCT vendor_id) AS vendor_count, - AVG(response_rate) AS avg_response_rate - FROM vendor_response_summary - WHERE rfq_type = 'FINAL' - GROUP BY rfq_id - ) final_summary ON br.id = final_summary.rfq_id - `); - -// 사용 예시 타입 정의 -export type VendorAttachmentResponse = typeof vendorAttachmentResponses.$inferSelect; -export type NewVendorAttachmentResponse = typeof vendorAttachmentResponses.$inferInsert; -export type AttachmentRevision = typeof bRfqAttachmentRevisions.$inferSelect; -export type ResponseAttachment = typeof vendorResponseAttachmentsB.$inferSelect; - -export type InitialRfqDetailView = typeof initialRfqDetailView.$inferSelect; -export type FinalRfqDetailView = typeof finalRfqDetailView.$inferSelect; -export type RfqDashboardView = typeof rfqDashboardView.$inferSelect; - - -export const bRfqsRelations = relations(bRfqs, ({ one, many }) => ({ - // 단일 관계 - project: one(projects, { - fields: [bRfqs.projectId], - references: [projects.id], - }), - createdByUser: one(users, { - fields: [bRfqs.createdBy], - references: [users.id], - relationName: "bRfqCreatedBy" - }), - updatedByUser: one(users, { - fields: [bRfqs.updatedBy], - references: [users.id], - relationName: "bRfqUpdatedBy" - }), - - // 다중 관계 - attachments: many(bRfqsAttachments), - initialRfqs: many(initialRfq), - finalRfqs: many(finalRfq), - })); - - // bRfqsAttachments 관계 정의 - export const bRfqsAttachmentsRelations = relations(bRfqsAttachments, ({ one, many }) => ({ - // 단일 관계 - rfq: one(bRfqs, { - fields: [bRfqsAttachments.rfqId], - references: [bRfqs.id], - }), - createdByUser: one(users, { - fields: [bRfqsAttachments.createdBy], - references: [users.id], - }), - latestRevision: one(bRfqAttachmentRevisions, { - fields: [bRfqsAttachments.latestRevisionId], - references: [bRfqAttachmentRevisions.id], - relationName: "attachmentLatestRevision" - }), - - // 다중 관계 - revisions: many(bRfqAttachmentRevisions), - vendorResponses: many(vendorAttachmentResponses), - })); - - // bRfqAttachmentRevisions 관계 정의 - export const bRfqAttachmentRevisionsRelations = relations(bRfqAttachmentRevisions, ({ one }) => ({ - attachment: one(bRfqsAttachments, { - fields: [bRfqAttachmentRevisions.attachmentId], - references: [bRfqsAttachments.id], - }), - createdByUser: one(users, { - fields: [bRfqAttachmentRevisions.createdBy], - references: [users.id], - }), - })); - - // vendorAttachmentResponses 관계 정의 - export const vendorAttachmentResponsesRelations = relations(vendorAttachmentResponses, ({ one, many }) => ({ - // 단일 관계 - attachment: one(bRfqsAttachments, { - fields: [vendorAttachmentResponses.attachmentId], - references: [bRfqsAttachments.id], - }), - vendor: one(vendors, { - fields: [vendorAttachmentResponses.vendorId], - references: [vendors.id], - }), - - // 다중 관계 - responseAttachments: many(vendorResponseAttachmentsB), - history: many(vendorResponseHistory), - })); - - // vendorResponseAttachmentsB 관계 정의 - export const vendorResponseAttachmentsBRelations = relations(vendorResponseAttachmentsB, ({ one }) => ({ - vendorResponse: one(vendorAttachmentResponses, { - fields: [vendorResponseAttachmentsB.vendorResponseId], - references: [vendorAttachmentResponses.id], - }), - uploadedByUser: one(users, { - fields: [vendorResponseAttachmentsB.uploadedBy], - references: [users.id], - }), - })); - - // vendorResponseHistory 관계 정의 - export const vendorResponseHistoryRelations_old = relations(vendorResponseHistory, ({ one }) => ({ - vendorResponse: one(vendorAttachmentResponses, { - fields: [vendorResponseHistory.vendorResponseId], - references: [vendorAttachmentResponses.id], - }), - actionByUser: one(users, { - fields: [vendorResponseHistory.actionBy], - references: [users.id], - }), - })); - - // initialRfq 관계 정의 - export const initialRfqRelations = relations(initialRfq, ({ one }) => ({ - rfq: one(bRfqs, { - fields: [initialRfq.rfqId], - references: [bRfqs.id], - }), - vendor: one(vendors, { - fields: [initialRfq.vendorId], - references: [vendors.id], - }), - })); - - // finalRfq 관계 정의 - export const finalRfqRelations = relations(finalRfq, ({ one }) => ({ - rfq: one(bRfqs, { - fields: [finalRfq.rfqId], - references: [bRfqs.id], - }), - vendor: one(vendors, { - fields: [finalRfq.vendorId], - references: [vendors.id], - }), - })); - - - -// 업데이트된 vendorResponseAttachmentsEnhanced 뷰 -export const vendorResponseAttachmentsEnhanced = pgView("vendor_response_attachments_enhanced", { - // 벤더 응답 파일 기본 정보 - responseAttachmentId: integer("response_attachment_id"), - vendorResponseId: integer("vendor_response_id"), - fileName: varchar("file_name", { length: 255 }), - originalFileName: varchar("original_file_name", { length: 255 }), - filePath: varchar("file_path", { length: 512 }), - fileSize: integer("file_size"), - fileType: varchar("file_type", { length: 100 }), - description: varchar("description", { length: 500 }), - uploadedAt: timestamp("uploaded_at"), - - // 응답 기본 정보 - attachmentId: integer("attachment_id"), - vendorId: integer("vendor_id"), - rfqType: varchar("rfq_type", { length: 20 }), - rfqRecordId: integer("rfq_record_id"), - responseStatus: varchar("response_status", { length: 30 }), - currentRevision: varchar("current_revision", { length: 10 }), - respondedRevision: varchar("responded_revision", { length: 10 }), - - // 코멘트 관련 필드들 (새로 추가된 필드 포함) - responseComment: text("response_comment"), - vendorComment: text("vendor_comment"), - revisionRequestComment: text("revision_request_comment"), // 새로 추가 - - // 날짜 관련 필드들 (새로 추가된 필드 포함) - requestedAt: timestamp("requested_at"), - respondedAt: timestamp("responded_at"), - revisionRequestedAt: timestamp("revision_requested_at"), // 새로 추가 - - // 첨부파일 정보 - attachmentType: varchar("attachment_type", { length: 50 }), - serialNo: varchar("serial_no", { length: 50 }), - rfqId: integer("rfq_id"), - - // 벤더 정보 - vendorCode: varchar("vendor_code", { length: 50 }), - vendorName: varchar("vendor_name", { length: 255 }), - vendorCountry: varchar("vendor_country", { length: 100 }), - - // 발주처 현재 리비전 정보 - latestClientRevisionId: integer("latest_client_revision_id"), - latestClientRevisionNo: varchar("latest_client_revision_no", { length: 10 }), - latestClientFileName: varchar("latest_client_file_name", { length: 255 }), - - // 리비전 비교 정보 - isVersionMatched: boolean("is_version_matched"), - versionLag: integer("version_lag"), // 몇 버전 뒤처져 있는지 - needsUpdate: boolean("needs_update"), - - // 응답 파일 순서 (같은 응답에 대한 여러 파일이 있을 경우) - fileSequence: integer("file_sequence"), - isLatestResponseFile: boolean("is_latest_response_file"), - -}).as(sql` - SELECT - vra.id as response_attachment_id, - vra.vendor_response_id, - vra.file_name, - vra.original_file_name, - vra.file_path, - vra.file_size, - vra.file_type, - vra.description, - vra.uploaded_at, - - -- 응답 기본 정보 - var.attachment_id, - var.vendor_id, - var.rfq_type, - var.rfq_record_id, - var.response_status, - var.current_revision, - var.responded_revision, - - -- 코멘트 (새로 추가된 필드 포함) - var.response_comment, - var.vendor_comment, - var.revision_request_comment, - - -- 날짜 (새로 추가된 필드 포함) - var.requested_at, - var.responded_at, - var.revision_requested_at, - - -- 첨부파일 정보 - ba.attachment_type, - ba.serial_no, - ba.rfq_id, - - -- 벤더 정보 - v.vendor_code, - v.vendor_name, - v.country as vendor_country, - - -- 발주처 현재 리비전 정보 - latest_rev.id as latest_client_revision_id, - latest_rev.revision_no as latest_client_revision_no, - latest_rev.original_file_name as latest_client_file_name, - - -- 리비전 비교 - CASE - WHEN var.responded_revision = ba.current_revision THEN true - ELSE false - END as is_version_matched, - - -- 버전 차이 계산 (Rev.0, Rev.1 형태 가정) - CASE - WHEN var.responded_revision IS NULL THEN NULL - WHEN ba.current_revision IS NULL THEN NULL - ELSE CAST(SUBSTRING(ba.current_revision FROM '[0-9]+') AS INTEGER) - - CAST(SUBSTRING(var.responded_revision FROM '[0-9]+') AS INTEGER) - END as version_lag, - - CASE - WHEN var.response_status = 'RESPONDED' - AND var.responded_revision != ba.current_revision THEN true - ELSE false - END as needs_update, - - -- 파일 순서 - ROW_NUMBER() OVER ( - PARTITION BY var.id - ORDER BY vra.uploaded_at DESC - ) as file_sequence, - - -- 최신 응답 파일 여부 - CASE - WHEN ROW_NUMBER() OVER ( - PARTITION BY var.id - ORDER BY vra.uploaded_at DESC - ) = 1 THEN true - ELSE false - END as is_latest_response_file - - FROM vendor_response_attachments_b vra - JOIN vendor_attachment_responses var ON vra.vendor_response_id = var.id - JOIN b_rfq_attachments ba ON var.attachment_id = ba.id - LEFT JOIN vendors v ON var.vendor_id = v.id - LEFT JOIN b_rfq_attachment_revisions latest_rev ON ba.latest_revision_id = latest_rev.id -`); - -// 2. 첨부파일별 리비전 히스토리 전체 뷰 -export const attachmentRevisionHistoryView = pgView("attachment_revision_history", { - rfqId: integer("rfq_id"), - rfqCode: varchar("rfq_code", { length: 50 }), - attachmentId: integer("attachment_id"), - attachmentType: varchar("attachment_type", { length: 50 }), - serialNo: varchar("serial_no", { length: 50 }), - - // 발주처 리비전 정보 - clientRevisionId: integer("client_revision_id"), - clientRevisionNo: varchar("client_revision_no", { length: 10 }), - clientFileName: varchar("client_file_name", { length: 255 }), - clientFilePath: varchar("client_file_path", { length: 512 }), - clientFileSize: integer("client_file_size"), - clientRevisionComment: text("client_revision_comment"), - clientRevisionCreatedAt: timestamp("client_revision_created_at"), - isLatestClientRevision: boolean("is_latest_client_revision"), - - // 이 리비전에 대한 벤더 응답 통계 - totalVendorResponses: integer("total_vendor_responses"), - respondedVendors: integer("responded_vendors"), - pendingVendors: integer("pending_vendors"), - totalResponseFiles: integer("total_response_files"), - -}).as(sql` - SELECT - br.id as rfq_id, - br.rfq_code, - ba.id as attachment_id, - ba.attachment_type, - ba.serial_no, - - -- 발주처 리비전 정보 - rev.id as client_revision_id, - rev.revision_no as client_revision_no, - rev.original_file_name as client_file_name, - rev.file_size as client_file_size, - rev.file_path as client_file_path, - rev.revision_comment as client_revision_comment, - rev.created_at as client_revision_created_at, - rev.is_latest as is_latest_client_revision, - - -- 벤더 응답 통계 - COALESCE(response_stats.total_responses, 0) as total_vendor_responses, - COALESCE(response_stats.responded_count, 0) as responded_vendors, - COALESCE(response_stats.pending_count, 0) as pending_vendors, - COALESCE(response_stats.total_files, 0) as total_response_files - - FROM b_rfqs br - JOIN b_rfq_attachments ba ON br.id = ba.rfq_id - JOIN b_rfq_attachment_revisions rev ON ba.id = rev.attachment_id - LEFT JOIN ( - SELECT - var.attachment_id, - COUNT(*) as total_responses, - COUNT(CASE WHEN var.response_status = 'RESPONDED' THEN 1 END) as responded_count, - COUNT(CASE WHEN var.response_status = 'NOT_RESPONDED' THEN 1 END) as pending_count, - COUNT(vra.id) as total_files - FROM vendor_attachment_responses var - LEFT JOIN vendor_response_attachments_b vra ON var.id = vra.vendor_response_id - GROUP BY var.attachment_id - ) response_stats ON ba.id = response_stats.attachment_id - - ORDER BY ba.id, rev.created_at DESC -`); - -// 3. 벤더별 응답 현황 상세 뷰 (리비전 정보 포함) -// 업데이트된 vendorResponseDetailView -export const vendorResponseDetailView = pgView("vendor_response_detail", { - // 기본 식별 정보 - responseId: integer("response_id"), - rfqId: integer("rfq_id"), - rfqCode: varchar("rfq_code", { length: 50 }), - rfqType: varchar("rfq_type", { length: 20 }), - rfqRecordId: integer("rfq_record_id"), - - // 첨부파일 정보 - attachmentId: integer("attachment_id"), - attachmentType: varchar("attachment_type", { length: 50 }), - serialNo: varchar("serial_no", { length: 50 }), - attachmentDescription: varchar("attachment_description", { length: 500 }), - - // 벤더 정보 - vendorId: integer("vendor_id"), - vendorCode: varchar("vendor_code", { length: 50 }), - vendorName: varchar("vendor_name", { length: 255 }), - vendorCountry: varchar("vendor_country", { length: 100 }), - - // 응답 상태 정보 - responseStatus: varchar("response_status", { length: 30 }), - currentRevision: varchar("current_revision", { length: 10 }), - respondedRevision: varchar("responded_revision", { length: 10 }), - - // 코멘트 관련 필드들 (새로 추가된 필드 포함) - responseComment: text("response_comment"), - vendorComment: text("vendor_comment"), - revisionRequestComment: text("revision_request_comment"), // 새로 추가 - - // 날짜 관련 필드들 (새로 추가된 필드 포함) - requestedAt: timestamp("requested_at"), - respondedAt: timestamp("responded_at"), - revisionRequestedAt: timestamp("revision_requested_at"), // 새로 추가 - - // 발주처 최신 리비전 정보 - latestClientRevisionNo: varchar("latest_client_revision_no", { length: 10 }), - latestClientFileName: varchar("latest_client_file_name", { length: 255 }), - latestClientFileSize: integer("latest_client_file_size"), - latestClientRevisionComment: text("latest_client_revision_comment"), - - // 리비전 분석 - isVersionMatched: boolean("is_version_matched"), - versionLag: integer("version_lag"), - needsUpdate: boolean("needs_update"), - hasMultipleRevisions: boolean("has_multiple_revisions"), - - // 응답 파일 통계 - totalResponseFiles: integer("total_response_files"), - latestResponseFileName: varchar("latest_response_file_name", { length: 255 }), - latestResponseFileSize: integer("latest_response_file_size"), - latestResponseUploadedAt: timestamp("latest_response_uploaded_at"), - - // 효과적인 상태 (UI 표시용) - effectiveStatus: varchar("effective_status", { length: 50 }), - -}).as(sql` - SELECT - var.id as response_id, - ba.rfq_id, - br.rfq_code, - var.rfq_type, - var.rfq_record_id, - - -- 첨부파일 정보 - ba.id as attachment_id, - ba.attachment_type, - ba.serial_no, - ba.description as attachment_description, - - -- 벤더 정보 - v.id as vendor_id, - v.vendor_code, - v.vendor_name, - v.country as vendor_country, - - -- 응답 상태 - var.response_status, - var.current_revision, - var.responded_revision, - - -- 코멘트 (새로 추가된 필드 포함) - var.response_comment, - var.vendor_comment, - var.revision_request_comment, - - -- 날짜 (새로 추가된 필드 포함) - var.requested_at, - var.responded_at, - var.revision_requested_at, - - -- 발주처 최신 리비전 - latest_rev.revision_no as latest_client_revision_no, - latest_rev.original_file_name as latest_client_file_name, - latest_rev.file_size as latest_client_file_size, - latest_rev.revision_comment as latest_client_revision_comment, - - -- 리비전 분석 - CASE - WHEN var.responded_revision = ba.current_revision THEN true - ELSE false - END as is_version_matched, - - CASE - WHEN var.responded_revision IS NULL OR ba.current_revision IS NULL THEN NULL - ELSE CAST(SUBSTRING(ba.current_revision FROM '[0-9]+') AS INTEGER) - - CAST(SUBSTRING(var.responded_revision FROM '[0-9]+') AS INTEGER) - END as version_lag, - - CASE - WHEN var.response_status = 'RESPONDED' - AND var.responded_revision != ba.current_revision THEN true - ELSE false - END as needs_update, - - CASE - WHEN revision_count.total_revisions > 1 THEN true - ELSE false - END as has_multiple_revisions, - - -- 응답 파일 정보 - COALESCE(file_stats.total_files, 0) as total_response_files, - file_stats.latest_file_name as latest_response_file_name, - file_stats.latest_file_size as latest_response_file_size, - file_stats.latest_uploaded_at as latest_response_uploaded_at, - - -- 효과적인 상태 - CASE - WHEN var.response_status = 'NOT_RESPONDED' THEN 'NOT_RESPONDED' - WHEN var.response_status = 'WAIVED' THEN 'WAIVED' - WHEN var.response_status = 'REVISION_REQUESTED' THEN 'REVISION_REQUESTED' - WHEN var.response_status = 'RESPONDED' AND var.responded_revision = ba.current_revision THEN 'UP_TO_DATE' - WHEN var.response_status = 'RESPONDED' AND var.responded_revision != ba.current_revision THEN 'VERSION_MISMATCH' - ELSE var.response_status - END as effective_status - - FROM vendor_attachment_responses var - JOIN b_rfq_attachments ba ON var.attachment_id = ba.id - JOIN b_rfqs br ON ba.rfq_id = br.id - LEFT JOIN vendors v ON var.vendor_id = v.id - LEFT JOIN b_rfq_attachment_revisions latest_rev ON ba.latest_revision_id = latest_rev.id - LEFT JOIN ( - SELECT - attachment_id, - COUNT(*) as total_revisions - FROM b_rfq_attachment_revisions - GROUP BY attachment_id - ) revision_count ON ba.id = revision_count.attachment_id - LEFT JOIN ( - SELECT - vendor_response_id, - COUNT(*) as total_files, - MAX(original_file_name) as latest_file_name, - MAX(file_size) as latest_file_size, - MAX(uploaded_at) as latest_uploaded_at - FROM vendor_response_attachments_b - GROUP BY vendor_response_id - ) file_stats ON var.id = file_stats.vendor_response_id -`); - -// 4. RFQ 진행 현황 요약 뷰 (리비전 정보 포함) -export const rfqProgressSummaryView = pgView("rfq_progress_summary", { - rfqId: integer("rfq_id"), - rfqCode: varchar("rfq_code", { length: 50 }), - rfqStatus: varchar("rfq_status", { length: 30 }), - dueDate: date("due_date"), - daysToDeadline: integer("days_to_deadline"), - - // 첨부파일 통계 - totalAttachments: integer("total_attachments"), - attachmentsWithMultipleRevisions: integer("attachments_with_multiple_revisions"), - totalClientRevisions: integer("total_client_revisions"), - - // 응답 통계 (INITIAL) - initialVendorCount: integer("initial_vendor_count"), - initialTotalResponses: integer("initial_total_responses"), - initialRespondedCount: integer("initial_responded_count"), - initialUpToDateCount: integer("initial_up_to_date_count"), - initialVersionMismatchCount: integer("initial_version_mismatch_count"), - initialResponseRate: numeric("initial_response_rate", { precision: 5, scale: 2 }), - initialVersionMatchRate: numeric("initial_version_match_rate", { precision: 5, scale: 2 }), - - // 응답 통계 (FINAL) - finalVendorCount: integer("final_vendor_count"), - finalTotalResponses: integer("final_total_responses"), - finalRespondedCount: integer("final_responded_count"), - finalUpToDateCount: integer("final_up_to_date_count"), - finalVersionMismatchCount: integer("final_version_mismatch_count"), - finalResponseRate: numeric("final_response_rate", { precision: 5, scale: 2 }), - finalVersionMatchRate: numeric("final_version_match_rate", { precision: 5, scale: 2 }), - - // 전체 파일 통계 - totalResponseFiles: integer("total_response_files"), - -}).as(sql` - SELECT - br.id as rfq_id, - br.rfq_code, - br.status as rfq_status, - br.due_date, - (br.due_date - CURRENT_DATE) as days_to_deadline, - - -- 첨부파일 통계 - attachment_stats.total_attachments, - attachment_stats.attachments_with_multiple_revisions, - attachment_stats.total_client_revisions, - - -- Initial RFQ 통계 - COALESCE(initial_stats.vendor_count, 0) as initial_vendor_count, - COALESCE(initial_stats.total_responses, 0) as initial_total_responses, - COALESCE(initial_stats.responded_count, 0) as initial_responded_count, - COALESCE(initial_stats.up_to_date_count, 0) as initial_up_to_date_count, - COALESCE(initial_stats.version_mismatch_count, 0) as initial_version_mismatch_count, - COALESCE(initial_stats.response_rate, 0) as initial_response_rate, - COALESCE(initial_stats.version_match_rate, 0) as initial_version_match_rate, - - -- Final RFQ 통계 - COALESCE(final_stats.vendor_count, 0) as final_vendor_count, - COALESCE(final_stats.total_responses, 0) as final_total_responses, - COALESCE(final_stats.responded_count, 0) as final_responded_count, - COALESCE(final_stats.up_to_date_count, 0) as final_up_to_date_count, - COALESCE(final_stats.version_mismatch_count, 0) as final_version_mismatch_count, - COALESCE(final_stats.response_rate, 0) as final_response_rate, - COALESCE(final_stats.version_match_rate, 0) as final_version_match_rate, - - COALESCE(file_stats.total_files, 0) as total_response_files - - FROM b_rfqs br - LEFT JOIN ( - SELECT - ba.rfq_id, - COUNT(*) as total_attachments, - COUNT(CASE WHEN rev_count.total_revisions > 1 THEN 1 END) as attachments_with_multiple_revisions, - SUM(rev_count.total_revisions) as total_client_revisions - FROM b_rfq_attachments ba - LEFT JOIN ( - SELECT - attachment_id, - COUNT(*) as total_revisions - FROM b_rfq_attachment_revisions - GROUP BY attachment_id - ) rev_count ON ba.id = rev_count.attachment_id - GROUP BY ba.rfq_id - ) attachment_stats ON br.id = attachment_stats.rfq_id - - LEFT JOIN ( - SELECT - br.id as rfq_id, - COUNT(DISTINCT var.vendor_id) as vendor_count, - COUNT(*) as total_responses, - COUNT(CASE WHEN var.response_status = 'RESPONDED' THEN 1 END) as responded_count, - COUNT(CASE WHEN vrd.effective_status = 'UP_TO_DATE' THEN 1 END) as up_to_date_count, - COUNT(CASE WHEN vrd.effective_status = 'VERSION_MISMATCH' THEN 1 END) as version_mismatch_count, - ROUND( - COUNT(CASE WHEN var.response_status = 'RESPONDED' THEN 1 END) * 100.0 / - NULLIF(COUNT(*), 0), 2 - ) as response_rate, - ROUND( - COUNT(CASE WHEN vrd.effective_status = 'UP_TO_DATE' THEN 1 END) * 100.0 / - NULLIF(COUNT(CASE WHEN var.response_status = 'RESPONDED' THEN 1 END), 0), 2 - ) as version_match_rate - FROM b_rfqs br - JOIN vendor_response_detail vrd ON br.id = vrd.rfq_id - JOIN vendor_attachment_responses var ON vrd.response_id = var.id - WHERE var.rfq_type = 'INITIAL' - GROUP BY br.id - ) initial_stats ON br.id = initial_stats.rfq_id - - LEFT JOIN ( - SELECT - br.id as rfq_id, - COUNT(DISTINCT var.vendor_id) as vendor_count, - COUNT(*) as total_responses, - COUNT(CASE WHEN var.response_status = 'RESPONDED' THEN 1 END) as responded_count, - COUNT(CASE WHEN vrd.effective_status = 'UP_TO_DATE' THEN 1 END) as up_to_date_count, - COUNT(CASE WHEN vrd.effective_status = 'VERSION_MISMATCH' THEN 1 END) as version_mismatch_count, - ROUND( - COUNT(CASE WHEN var.response_status = 'RESPONDED' THEN 1 END) * 100.0 / - NULLIF(COUNT(*), 0), 2 - ) as response_rate, - ROUND( - COUNT(CASE WHEN vrd.effective_status = 'UP_TO_DATE' THEN 1 END) * 100.0 / - NULLIF(COUNT(CASE WHEN var.response_status = 'RESPONDED' THEN 1 END), 0), 2 - ) as version_match_rate - FROM b_rfqs br - JOIN vendor_response_detail vrd ON br.id = vrd.rfq_id - JOIN vendor_attachment_responses var ON vrd.response_id = var.id - WHERE var.rfq_type = 'FINAL' - GROUP BY br.id - ) final_stats ON br.id = final_stats.rfq_id - - LEFT JOIN ( - SELECT - br.id as rfq_id, - COUNT(vra.id) as total_files - FROM b_rfqs br - JOIN b_rfq_attachments ba ON br.id = ba.rfq_id - JOIN vendor_attachment_responses var ON ba.id = var.attachment_id - LEFT JOIN vendor_response_attachments_b vra ON var.id = vra.vendor_response_id - GROUP BY br.id - ) file_stats ON br.id = file_stats.rfq_id -`); - -// 타입 정의 -export type VendorResponseAttachmentEnhanced = typeof vendorResponseAttachmentsEnhanced.$inferSelect; -export type AttachmentRevisionHistory = typeof attachmentRevisionHistoryView.$inferSelect; -export type VendorResponseDetail = typeof vendorResponseDetailView.$inferSelect; -export type RfqProgressSummary = typeof rfqProgressSummaryView.$inferSelect;
\ No newline at end of file diff --git a/db/schema/index.ts b/db/schema/index.ts index cd54e032..7d433f7c 100644 --- a/db/schema/index.ts +++ b/db/schema/index.ts @@ -1,24 +1,29 @@ +export * from './vendors'; export * from './companies'; export * from './contract'; export * from './items'; export * from './pq'; export * from './projects'; -export * from './rfq'; +// 미사용 테이블 주석 처리 +// export * from './rfq'; + export * from './users'; export * from './vendorData'; export * from './vendorDocu'; -export * from './vendors'; export * from './tasks'; export * from './logs'; export * from './basicContractDocumnet'; + +// 미사용 테이블 주석 처리 export * from './procurementRFQ'; export * from './agreementComments'; export * from './setting'; export * from './techSales'; export * from './ocr'; // 명시적 import/export로 vendorResponseSummaryView 이름 충돌 방지 -export * from './bRfq'; +// 미사용 스키마 제거 +// export * from './bRfq'; export * from './techVendors'; export * from './evaluation'; export * from './evaluationTarget'; diff --git a/db/schema/procurementRFQ.ts b/db/schema/procurementRFQ.ts index fe60bb0e..2756f934 100644 --- a/db/schema/procurementRFQ.ts +++ b/db/schema/procurementRFQ.ts @@ -1,65 +1,4 @@ -import { foreignKey, pgTable, pgView, serial, varchar, text, timestamp, boolean, integer, numeric, date, alias, check } from "drizzle-orm/pg-core"; -import { eq, sql, relations } from "drizzle-orm"; -import { projects } from "./projects"; -import { users } from "./users"; -import { vendors } from "./vendors"; - -export const procurementRfqs = pgTable( - "procurement_rfqs", - { - id: serial("id").primaryKey(), - - // RFQ 고유 코드 - rfqCode: varchar("rfq_code", { length: 50 }).unique(), // ex) "RFQ-2025-001" - - // 프로젝트: ECC RFQ는 프로젝트 테이블과 1:N 관계를 가져야 함 - // WHY?: 여러 프로젝트 혹은 여러 시리즈의 동일 품목을 PR로 묶어 올리기 때문 - projectId: varchar("project_id", { length: 1000 }), - - // SS, II, null 값을 가질 수 있음. - // SS = 시리즈 통합, II = 품목 통합, 공란 = 통합 없음 - series: varchar("series", { length: 50 }), - - // 자재코드, 자재명: ECC RFQ는 자재코드, 자재명을 가지지 않음 - // WHY?: 여러 프로젝트 혹은 여러 시리즈의 동일 품목을 PR로 묶어 올리기 때문 - // 아래 컬럼은 대표 자재코드, 대표 자재명으로 사용 - itemCode: varchar("item_code", { length: 100 }), - itemName: varchar("item_name", { length: 255 }), - - dueDate: date("due_date", { mode: "date" }) - .$type<Date>(), // 인터페이스한 값은 dueDate가 없으므로 notNull 제약조건 제거 - - rfqSendDate: date("rfq_send_date", { mode: "date" }) - .$type<Date | null>(), // notNull() 제약조건 제거, null 허용 (ECC에서 수신 후 보내지 않은 RFQ) - - status: varchar("status", { length: 30 }) - .$type<"RFQ Created" | "RFQ Vendor Assignned" | "RFQ Sent" | "Quotation Analysis" | "PO Transfer" | "PO Create">() - .default("RFQ Created") - .notNull(), - - rfqSealedYn: boolean("rfq_sealed_yn").default(false), - picCode: varchar("pic_code", { length: 50 }), // 구매그룹에 대응시킴 (담당자 코드로 3자리) - - remark: text("remark"), - // 생성자 - - sentBy: integer("sent_by") - .references(() => users.id, { onDelete: "set null" }), - - - createdBy: integer("created_by") - .notNull() - .references(() => users.id, { onDelete: "set null" }), - - updatedBy: integer("updated_by") - .notNull() - .references(() => users.id, { onDelete: "set null" }), - - createdAt: timestamp("created_at").defaultNow().notNull(), - updatedAt: timestamp("updated_at").defaultNow().notNull(), - }, - -); +import { pgTable, varchar, timestamp, boolean } from "drizzle-orm/pg-core"; /** * 지불조건, 인코텀즈, 선적/하역(출발지, 도착지) 테이블은 Non-SAP에서 동기화 (Oracle DB to PostgreSQL) @@ -91,712 +30,4 @@ export const placeOfShipping = pgTable("place_of_shipping", { createdAt: timestamp("created_at").defaultNow().notNull(), }); -export const procurementRfqDetails = pgTable( - "procurement_rfq_details", - { - id: serial("id").primaryKey(), - procurementRfqsId: integer("procurement_rfqs_id") - .references(() => procurementRfqs.id, { onDelete: "set null" }), - vendorsId: integer("vendors_id") - .references(() => vendors.id, { onDelete: "set null" }), - currency: varchar("currency", { length: 10 }).default("USD"), - - // 정규화된 paymentTerms 참조 - paymentTermsCode: varchar("payment_terms_code", { length: 50 }) - .references(() => paymentTerms.code, { onDelete: "set null" }), - // paymentTerms 필드는 제거 (코드로 조회) - - // 정규화된 incoterms 참조 - incotermsCode: varchar("incoterms_code", { length: 20 }) - .references(() => incoterms.code, { onDelete: "set null" }), - incotermsDetail: varchar("incoterms_detail", { length: 255 }), - - deliveryDate: date("delivery_date", { mode: "date" }) - .$type<Date>() - .notNull(), - taxCode: varchar("tax_code", { length: 255 }).default("VV"), - placeOfShipping: varchar("place_of_shipping", { length: 255 }), - placeOfDestination: varchar("place_of_destination", { length: 255 }), - remark: text("remark"), - cancelReason: text("cancel_reason"), - updatedBy: integer("updated_by") - .notNull() - .references(() => users.id, { onDelete: "set null" }), - updatedAt: timestamp("updated_at").defaultNow().notNull(), - materialPriceRelatedYn: boolean("material_price_related_yn").default(false), - } -); - -export const prItems = pgTable( - "pr_items", - { - id: serial("id").primaryKey(), - procurementRfqsId: integer("procurement_rfqs_id") - .references(() => procurementRfqs.id, { onDelete: "set null" }), - - rfqItem: varchar("rfq_item", { length: 50 }), // 단위 - prItem: varchar("pr_item", { length: 50 }), // 단위 - prNo: varchar("pr_no", { length: 50 }), // 단위 - - // itemId: integer("item_id") - // .references(() => items.id, { onDelete: "set null" }), - - materialCode: varchar("material_code", { length: 255 }), - materialCategory: varchar("material_category", { length: 255 }), - acc: varchar("acc", { length: 255 }), - materialDescription: varchar("material_description", { length: 255 }), - size: varchar("size", { length: 255 }), - deliveryDate: date("delivery_date", { mode: "date" }) - .$type<Date>(), - quantity: numeric("quantity", { precision: 12, scale: 2 }) - .$type<number>() - .default(1), - uom: varchar("uom", { length: 50 }), // 단위 - grossWeight: numeric("gross_weight", { precision: 12, scale: 2 }) - .$type<number>() - .default(1), - gwUom: varchar("gw_uom", { length: 50 }), // 단위 - - specNo: varchar("spec_no", { length: 255 }), - specUrl: varchar("spec_url", { length: 255 }), - trackingNo: varchar("tracking_no", { length: 255 }), - - majorYn: boolean("major_yn").default(false), - - projectDef: varchar("project_def", { length: 255 }), - projectSc: varchar("project_sc", { length: 255 }), - projectKl: varchar("project_kl", { length: 255 }), - projectLc: varchar("project_lc", { length: 255 }), - projectDl: varchar("project_dl", { length: 255 }), - - remark: text("remark"), - - }, -); - - -//view -export const procurementRfqsView = pgView("procurement_rfqs_view").as((qb) => { - const createdByUser = alias(users, "created_by_user"); - const updatedByUser = alias(users, "updated_by_user"); - const sentByUser = alias(users, "sent_by_user"); - - return qb - .select({ - // Basic RFQ identification - id: sql<number>`${procurementRfqs.id}`.as("id"), - rfqCode: sql<string>`${procurementRfqs.rfqCode}`.as("rfq_code"), - series: sql<string | null>`${procurementRfqs.series}`.as("series"), - rfqSealedYn: sql<string | null>`${procurementRfqs.rfqSealedYn}`.as("rfq_sealed_yn"), - - // Project information - projectCode: sql<string | null>`${projects.code}`.as("project_code"), - projectName: sql<string | null>`${projects.name}`.as("project_name"), - - // Item information - itemCode: sql<string | null>`${procurementRfqs.itemCode}`.as("item_code"), - itemName: sql<string | null>`${procurementRfqs.itemName}`.as("item_name"), - - // Status and dates - status: sql<string>`${procurementRfqs.status}`.as("status"), - picCode: sql<string | null>`${procurementRfqs.picCode}`.as("pic_code"), - rfqSendDate: sql<Date | null>`${procurementRfqs.rfqSendDate}`.as("rfq_send_date"), - dueDate: sql<Date | null>`${procurementRfqs.dueDate}`.as("due_date"), - - // 가장 빠른 견적서 제출 날짜 추가 - earliestQuotationSubmittedAt: sql<Date | null>`( - SELECT MIN(submitted_at) - FROM procurement_vendor_quotations - WHERE rfq_id = ${procurementRfqs.id} - AND submitted_at IS NOT NULL - )`.as("earliest_quotation_submitted_at"), - - // Audit information - createdByUserName: sql<string | null>`${createdByUser.name}`.as("created_by_user_name"), - sentByUserName: sql<string | null>`${sentByUser.name}`.as("sent_by_user_name"), - updatedAt: sql<Date>`${procurementRfqs.updatedAt}`.as("updated_at"), - updatedByUserName: sql<string | null>`${updatedByUser.name}`.as("updated_by_user_name"), - remark: sql<string | null>`${procurementRfqs.remark}`.as("remark"), - - // Related item information - majorItemMaterialCode: sql<string | null>`( - SELECT material_code - FROM pr_items - WHERE procurement_rfqs_id = ${procurementRfqs.id} - AND major_yn = true - LIMIT 1 - )`.as("major_item_material_code"), - - poNo: sql<string | null>`( - SELECT pr_no - FROM pr_items - WHERE procurement_rfqs_id = ${procurementRfqs.id} - AND major_yn = true - LIMIT 1 - )`.as("po_no"), - - prItemsCount: sql<number>`( - SELECT COUNT(*) - FROM pr_items - WHERE procurement_rfqs_id = ${procurementRfqs.id} - )`.as("pr_items_count") - }) - .from(procurementRfqs) - .leftJoin(projects, eq(procurementRfqs.projectId, projects.id)) - // .leftJoin(items, eq(procurementRfqs.itemId, items.id)) - .leftJoin(createdByUser, eq(procurementRfqs.createdBy, createdByUser.id)) - .leftJoin(updatedByUser, eq(procurementRfqs.updatedBy, updatedByUser.id)) - .leftJoin(sentByUser, eq(procurementRfqs.sentBy, sentByUser.id)); -}); - -// 수정된 pr_items_view -export const prItemsView = pgView("pr_items_view").as((qb) => { - return qb - .select({ - id: prItems.id, - procurementRfqsId: prItems.procurementRfqsId, - rfqItem: prItems.rfqItem, - prItem: prItems.prItem, - prNo: prItems.prNo, - // itemId: prItems.itemId, - materialCode: prItems.materialCode, - materialCategory: prItems.materialCategory, - acc: prItems.acc, - materialDescription: prItems.materialDescription, - size: prItems.size, - deliveryDate: prItems.deliveryDate, - quantity: prItems.quantity, - uom: prItems.uom, - grossWeight: prItems.grossWeight, // 필드명 수정 - gwUom: prItems.gwUom, - specNo: prItems.specNo, - specUrl: prItems.specUrl, - trackingNo: prItems.trackingNo, - majorYn: prItems.majorYn, - projectDef: prItems.projectDef, - projectSc: prItems.projectSc, - projectKl: prItems.projectKl, - projectLc: prItems.projectLc, - projectDl: prItems.projectDl, - remark: prItems.remark, - rfqCode: procurementRfqs.rfqCode, - itemCode: procurementRfqs.itemCode, - itemName: procurementRfqs.itemName - }) - .from(prItems) - .leftJoin(procurementRfqs, eq(prItems.procurementRfqsId, procurementRfqs.id)) -}); - -export const procurementRfqDetailsView = pgView("procurement_rfq_details_view").as((qb) => { - // 기존 별칭 정의 유지 - const rfqDetailsTable = alias(procurementRfqDetails, "rfq_details"); - const rfqsTable = alias(procurementRfqs, "rfqs"); - const projectsTable = alias(projects, "projects"); - const vendorsTable = alias(vendors, "vendors"); - const paymentTermsTable = alias(paymentTerms, "payment_terms"); - const incotermsTable = alias(incoterms, "incoterms"); - const updatedByUser = alias(users, "updated_by_user"); - - return qb - .select({ - // procurementRfqDetails 필드 - detailId: sql<number>`${rfqDetailsTable.id}`.as("detail_id"), - rfqId: sql<number>`${rfqsTable.id}`.as("rfq_id"), - rfqCode: sql<string>`${rfqsTable.rfqCode}`.as("rfq_code"), - - // 프로젝트 관련 필드 - projectCode: sql<string | null>`${projectsTable.code}`.as("project_code"), - projectName: sql<string | null>`${projectsTable.name}`.as("project_name"), - - // 아이템 관련 필드 - itemCode: sql<string | null>`${rfqsTable.itemCode}`.as("item_code"), - itemName: sql<string | null>`${rfqsTable.itemName}`.as("item_name"), - - // 벤더 관련 필드 - vendorName: sql<string | null>`${vendorsTable.vendorName}`.as("vendor_name"), - vendorCode: sql<string | null>`${vendorsTable.vendorCode}`.as("vendor_code"), - vendorId: sql<string | null>`${vendorsTable.id}`.as("vendor_id"), - vendorCountry: sql<string | null>`${vendorsTable.country}`.as("vendor_country"), - - // RFQ 상세 정보 필드 - currency: sql<string | null>`${rfqDetailsTable.currency}`.as("currency"), - paymentTermsCode: sql<string | null>`${paymentTermsTable.code}`.as("payment_terms_code"), - paymentTermsDescription: sql<string | null>`${paymentTermsTable.description}`.as("payment_terms_description"), - incotermsCode: sql<string | null>`${incotermsTable.code}`.as("incoterms_code"), - incotermsDescription: sql<string | null>`${incotermsTable.description}`.as("incoterms_description"), - incotermsDetail: sql<string | null>`${rfqDetailsTable.incotermsDetail}`.as("incoterms_detail"), - deliveryDate: sql<Date | null>`${rfqDetailsTable.deliveryDate}`.as("delivery_date"), - taxCode: sql<string | null>`${rfqDetailsTable.taxCode}`.as("tax_code"), - placeOfShipping: sql<string | null>`${rfqDetailsTable.placeOfShipping}`.as("place_of_shipping"), - placeOfDestination: sql<string | null>`${rfqDetailsTable.placeOfDestination}`.as("place_of_destination"), - materialPriceRelatedYn: sql<boolean | null>`${rfqDetailsTable.materialPriceRelatedYn}`.as("material_price_related_yn"), - - // 업데이트 관련 필드 - updatedByUserName: sql<string | null>`${updatedByUser.name}`.as("updated_by_user_name"), - updatedAt: sql<Date | null>`${rfqDetailsTable.updatedAt}`.as("updated_at"), - - // 추가적으로 pr_items 개수도 카운트하는 필드 추가 - prItemsCount: sql<number>`( - SELECT COUNT(*) - FROM pr_items - WHERE procurement_rfqs_id = ${rfqsTable.id} - )`.as("pr_items_count"), - - // 메이저 아이템 개수도 카운트 - majorItemsCount: sql<number>`( - SELECT COUNT(*) - FROM pr_items - WHERE procurement_rfqs_id = ${rfqsTable.id} - AND major_yn = true - )`.as("major_items_count"), - - // 새로운 필드 추가: 코멘트 수 카운트 - commentCount: sql<number>`( - SELECT COUNT(*) - FROM procurement_rfq_comments - WHERE rfq_id = ${rfqsTable.id} AND vendor_id = ${rfqDetailsTable.vendorsId} - )`.as("comment_count"), - - // 새로운 필드 추가: 최근 코멘트 날짜 - lastCommentDate: sql<Date | null>`( - SELECT created_at - FROM procurement_rfq_comments - WHERE rfq_id = ${rfqsTable.id} AND vendor_id = ${rfqDetailsTable.vendorsId} - ORDER BY created_at DESC LIMIT 1 - )`.as("last_comment_date"), - - // 새로운 필드 추가: 벤더가 마지막으로 코멘트한 날짜 - lastVendorCommentDate: sql<Date | null>`( - SELECT created_at - FROM procurement_rfq_comments - WHERE rfq_id = ${rfqsTable.id} AND vendor_id = ${rfqDetailsTable.vendorsId} AND is_vendor_comment = true - ORDER BY created_at DESC LIMIT 1 - )`.as("last_vendor_comment_date"), - - // 새로운 필드 추가: 첨부파일 수 카운트 - attachmentCount: sql<number>`( - SELECT COUNT(*) - FROM procurement_rfq_attachments - WHERE rfq_id = ${rfqsTable.id} AND vendor_id = ${rfqDetailsTable.vendorsId} - )`.as("attachment_count"), - - // 새로운 필드 추가: 견적서 제출 여부 - hasQuotation: sql<boolean>`( - SELECT COUNT(*) > 0 - FROM procurement_vendor_quotations - WHERE rfq_id = ${rfqsTable.id} AND vendor_id = ${rfqDetailsTable.vendorsId} - )`.as("has_quotation"), - - // 새로운 필드 추가: 견적서 상태 - quotationStatus: sql<string | null>`( - SELECT status - FROM procurement_vendor_quotations - WHERE rfq_id = ${rfqsTable.id} AND vendor_id = ${rfqDetailsTable.vendorsId} - ORDER BY created_at DESC LIMIT 1 - )`.as("quotation_status"), - - // 새로운 필드 추가: 견적서 총액 - quotationTotalPrice: sql<number | null>`( - SELECT total_price - FROM procurement_vendor_quotations - WHERE rfq_id = ${rfqsTable.id} AND vendor_id = ${rfqDetailsTable.vendorsId} - ORDER BY created_at DESC LIMIT 1 - )`.as("quotation_total_price"), - - // 최신 견적서 버전 - quotationVersion: sql<number | null>`( - SELECT quotation_version - FROM procurement_vendor_quotations - WHERE rfq_id = ${rfqsTable.id} AND vendor_id = ${rfqDetailsTable.vendorsId} - ORDER BY quotation_version DESC LIMIT 1 - )`.as("quotation_version"), - - // 총 견적서 버전 수 - quotationVersionCount: sql<number>`( - SELECT COUNT(DISTINCT quotation_version) - FROM procurement_vendor_quotations - WHERE rfq_id = ${rfqsTable.id} AND vendor_id = ${rfqDetailsTable.vendorsId} - )`.as("quotation_version_count"), - - // 마지막 견적서 생성 날짜 - lastQuotationDate: sql<Date | null>`( - SELECT created_at - FROM procurement_vendor_quotations - WHERE rfq_id = ${rfqsTable.id} AND vendor_id = ${rfqDetailsTable.vendorsId} - ORDER BY quotation_version DESC LIMIT 1 - )`.as("last_quotation_date"), - }) - .from(rfqDetailsTable) - // 기존 조인 유지 - .leftJoin(rfqsTable, eq(rfqDetailsTable.procurementRfqsId, rfqsTable.id)) - .leftJoin(projectsTable, eq(rfqsTable.projectId, projectsTable.id)) - // .leftJoin(itemsTable, eq(rfqsTable.itemId, itemsTable.id)) - .leftJoin(vendorsTable, eq(rfqDetailsTable.vendorsId, vendorsTable.id)) - .leftJoin(paymentTermsTable, eq(rfqDetailsTable.paymentTermsCode, paymentTermsTable.code)) - .leftJoin(incotermsTable, eq(rfqDetailsTable.incotermsCode, incotermsTable.code)) - .leftJoin(updatedByUser, eq(rfqDetailsTable.updatedBy, updatedByUser.id)); -}); - -export const procurementAttachments = pgTable( - "procurement_attachments", - { - id: serial("id").primaryKey(), - attachmentType: varchar("attachment_type", { length: 50 }).notNull(), // 'RFQ_COMMON', 'VENDOR_SPECIFIC' - procurementRfqsId: integer("procurement_rfqs_id") - .references(() => procurementRfqs.id, { onDelete: "cascade" }), - procurementRfqDetailsId: integer("procurement_rfq_details_id") - .references(() => procurementRfqDetails.id, { onDelete: "cascade" }), - fileName: varchar("file_name", { length: 255 }).notNull(), - originalFileName: varchar("original_file_name", { length: 255 }).notNull(), - filePath: varchar("file_path", { length: 512 }).notNull(), - fileSize: integer("file_size"), - fileType: varchar("file_type", { length: 100 }), - description: varchar("description", { length: 500 }), - createdBy: integer("created_by") - .references(() => users.id, { onDelete: "set null" }) - .notNull(), - createdAt: timestamp("created_at").defaultNow().notNull(), - }, - (table) => ({ - attachmentTypeCheck: check( - "attachment_type_check", - sql`${table.procurementRfqsId} IS NOT NULL OR ${table.procurementRfqDetailsId} IS NOT NULL` - ) - }) -); - -export type ProcurementRfqsView = typeof procurementRfqsView.$inferSelect; -export type PrItemsView = typeof prItemsView.$inferSelect; -export type ProcurementRfqDetailsView = typeof procurementRfqDetailsView.$inferSelect; - - -//vendor response -export const procurementVendorQuotations = pgTable( - "procurement_vendor_quotations", - { - id: serial("id").primaryKey(), - rfqId: integer("rfq_id") - .notNull() - .references(() => procurementRfqs.id, { onDelete: "cascade" }), - vendorId: integer("vendor_id") - .notNull() - .references(() => vendors.id, { onDelete: "set null" }), - - // 견적 요약 정보 - quotationCode: varchar("quotation_code", { length: 50 }), - quotationVersion: integer("quotation_version").default(1), - totalItemsCount: integer("total_items_count").default(0), - subTotal: numeric("sub_total").default("0"), - taxTotal: numeric("tax_total").default("0"), - discountTotal: numeric("discount_total").default("0"), - totalPrice: numeric("total_price").default("0"), - currency: varchar("currency", { length: 10 }).default("USD"), - - // 견적 유효성 및 배송 정보 - validUntil: date("valid_until", { mode: "date" }).$type<Date>(), - estimatedDeliveryDate: date("estimated_delivery_date", { mode: "date" }).$type<Date>(), - - // 지불 조건 등 상세 정보 - paymentTermsCode: varchar("payment_terms_code", { length: 50 }) - .references(() => paymentTerms.code, { onDelete: "set null" }), - incotermsCode: varchar("incoterms_code", { length: 20 }) - .references(() => incoterms.code, { onDelete: "set null" }), - incotermsDetail: varchar("incoterms_detail", { length: 255 }), - - // 상태 관리 - status: varchar("status", { length: 30 }) - .$type<"Draft" | "Submitted" | "Revised" | "Rejected" | "Accepted">() - .default("Draft") - .notNull(), - - // 기타 정보 - remark: text("remark"), - rejectionReason: text("rejection_reason"), - submittedAt: timestamp("submitted_at"), - acceptedAt: timestamp("accepted_at"), - - // 감사 필드 - createdBy: integer("created_by"), - updatedBy: integer("updated_by"), - createdAt: timestamp("created_at").defaultNow().notNull(), - updatedAt: timestamp("updated_at").defaultNow().notNull(), - } -); - -export const procurementRfqComments = pgTable( - "procurement_rfq_comments", - { - id: serial("id").primaryKey(), - rfqId: integer("rfq_id") - .notNull() - .references(() => procurementRfqs.id, { onDelete: "cascade" }), - vendorId: integer("vendor_id") - .references(() => vendors.id, { onDelete: "set null" }), - userId: integer("user_id") - .references(() => users.id, { onDelete: "set null" }), - content: text("content").notNull(), - isVendorComment: boolean("is_vendor_comment").default(false), - isRead: boolean("is_read").default(false), // 읽음 상태 추가 - parentCommentId: integer("parent_comment_id"), - createdAt: timestamp("created_at").defaultNow().notNull(), - updatedAt: timestamp("updated_at").defaultNow().notNull(), - }, - // 자기참조 FK 정의 - (table) => { - return { - parentFk: foreignKey({ - columns: [table.parentCommentId], - foreignColumns: [table.id], - }).onDelete("set null"), - }; - } -); - -export const procurementRfqAttachments = pgTable( - "procurement_rfq_attachments", - { - id: serial("id").primaryKey(), - rfqId: integer("rfq_id") - .notNull() - .references(() => procurementRfqs.id, { onDelete: "cascade" }), - commentId: integer("comment_id") - .references(() => procurementRfqComments.id, { onDelete: "cascade" }), - quotationId: integer("quotation_id") - .references(() => procurementVendorQuotations.id, { onDelete: "cascade" }), - fileName: varchar("file_name", { length: 255 }).notNull(), - fileSize: integer("file_size").notNull(), - fileType: varchar("file_type", { length: 100 }), - filePath: varchar("file_path", { length: 500 }).notNull(), - isVendorUpload: boolean("is_vendor_upload").default(false), - uploadedBy: integer("uploaded_by") - .references(() => users.id, { onDelete: "set null" }), - vendorId: integer("vendor_id") - .references(() => vendors.id, { onDelete: "set null" }), - uploadedAt: timestamp("uploaded_at").defaultNow().notNull(), - } -); - -export const procurementQuotationItems = pgTable( - "procurement_quotation_items", - { - id: serial("id").primaryKey(), - quotationId: integer("quotation_id") - .notNull() - .references(() => procurementVendorQuotations.id, { onDelete: "cascade" }), - - // PR 아이템과의 연결 추가 - prItemId: integer("pr_item_id") - .notNull() - .references(() => prItems.id, { onDelete: "cascade" }), - - // 원본 PR 아이템 정보 참조 (읽기 전용) - materialCode: varchar("material_code", { length: 50 }), - materialDescription: varchar("material_description", { length: 255 }), - quantity: numeric("quantity").notNull(), - uom: varchar("uom", { length: 20 }), - - // 벤더가 입력하는 정보 - unitPrice: numeric("unit_price").notNull(), - totalPrice: numeric("total_price").notNull(), - currency: varchar("currency", { length: 10 }).default("USD"), - vendorMaterialCode: varchar("vendor_material_code", { length: 50 }), - vendorMaterialDescription: varchar("vendor_material_description", { length: 255 }), - - // 배송 관련 정보 - deliveryDate: date("delivery_date", { mode: "date" }).$type<Date>(), - leadTimeInDays: integer("lead_time_in_days"), - - // 세금 및 기타 정보 - taxRate: numeric("tax_rate"), - taxAmount: numeric("tax_amount"), - discountRate: numeric("discount_rate"), - discountAmount: numeric("discount_amount"), - - // 기타 정보 - remark: text("remark"), - isAlternative: boolean("is_alternative").default(false), - isRecommended: boolean("is_recommended").default(false), - createdAt: timestamp("created_at").defaultNow().notNull(), - updatedAt: timestamp("updated_at").defaultNow().notNull(), - } -); - - -// procurementRfqComments 테이블의 관계 정의 - 타입 명시 -export const procurementRfqCommentsRelations = relations( - procurementRfqComments, - ({ one, many }) => ({ - user: one(users, { - fields: [procurementRfqComments.userId], - references: [users.id], - }), - vendor: one(vendors, { - fields: [procurementRfqComments.vendorId], - references: [vendors.id], - }), - rfq: one(procurementRfqs, { - fields: [procurementRfqComments.rfqId], - references: [procurementRfqs.id], - }), - parentComment: one(procurementRfqComments, { - fields: [procurementRfqComments.parentCommentId], - references: [procurementRfqComments.id], - relationName: "commentHierarchy", - }), - childComments: many(procurementRfqComments, { relationName: "commentHierarchy" }), - attachments: many(procurementRfqAttachments, { relationName: "commentAttachments" }), - }) -); - -export const procurementRfqAttachmentsRelations = relations( - procurementRfqAttachments, - ({ one }) => ({ - comment: one(procurementRfqComments, { - fields: [procurementRfqAttachments.commentId], - references: [procurementRfqComments.id], - relationName: "commentAttachments", - }), - rfq: one(procurementRfqs, { - fields: [procurementRfqAttachments.rfqId], - references: [procurementRfqs.id], - }), - uploader: one(users, { - fields: [procurementRfqAttachments.uploadedBy], - references: [users.id], - }), - vendor: one(vendors, { - fields: [procurementRfqAttachments.vendorId], - references: [vendors.id], - }), - }) -); - - -export const procurementRfqsRelations = relations( - procurementRfqs, - ({ one, many }) => ({ - project: one(projects, { - fields: [procurementRfqs.projectId], - references: [projects.id], - }), - // item: one(items, { - // fields: [procurementRfqs.itemId], - // references: [items.id], - // }), - createdByUser: one(users, { - fields: [procurementRfqs.createdBy], - references: [users.id], - relationName: "rfqCreator", - }), - updatedByUser: one(users, { - fields: [procurementRfqs.updatedBy], - references: [users.id], - relationName: "rfqUpdater", - }), - rfqDetails: many(procurementRfqDetails), - prItems: many(prItems, { relationName: "rfqPrItems" }), - quotations: many(procurementVendorQuotations), - - }) -); - -export const prItemsRelations = relations( - prItems, - ({ one }) => ({ - rfq: one(procurementRfqs, { - fields: [prItems.procurementRfqsId], - references: [procurementRfqs.id], - relationName: "rfqPrItems" - }), - }) -); - -// procurementRfqDetails 테이블의 관계 정의 -export const procurementRfqDetailsRelations = relations( - procurementRfqDetails, - ({ one }) => ({ - rfq: one(procurementRfqs, { - fields: [procurementRfqDetails.procurementRfqsId], - references: [procurementRfqs.id], - }), - vendor: one(vendors, { - fields: [procurementRfqDetails.vendorsId], - references: [vendors.id], - }), - paymentTerms: one(paymentTerms, { - fields: [procurementRfqDetails.paymentTermsCode], - references: [paymentTerms.code], - }), - incoterms: one(incoterms, { - fields: [procurementRfqDetails.incotermsCode], - references: [incoterms.code], - }), - updatedByUser: one(users, { - fields: [procurementRfqDetails.updatedBy], - references: [users.id], - }), - }) -); - -export const vendorsRelations = relations( - vendors, - ({ many }) => ({ - users: many(users, { - relationName: "vendorUsers" - }), - quotations: many(procurementVendorQuotations), - }) -); - -export const usersRelations = relations( - users, - ({ one }) => ({ - vendor: one(vendors, { - fields: [users.companyId], - references: [vendors.id], - relationName: "vendorUsers" - }), - }) -); - -export const procurementVendorQuotationsRelations = relations( - procurementVendorQuotations, - ({ one, many }) => ({ - rfq: one(procurementRfqs, { - fields: [procurementVendorQuotations.rfqId], - references: [procurementRfqs.id], - // relationName 제거 - }), - vendor: one(vendors, { - fields: [procurementVendorQuotations.vendorId], - references: [vendors.id], - // relationName 제거 - }), - items: many(procurementQuotationItems), - paymentTerms: one(paymentTerms, { - fields: [procurementVendorQuotations.paymentTermsCode], - references: [paymentTerms.code], - }), - incoterms: one(incoterms, { - fields: [procurementVendorQuotations.incotermsCode], - references: [incoterms.code], - }), - }) -); - -export const procurementQuotationItemsRelations = relations( - procurementQuotationItems, - ({ one }) => ({ - quotation: one(procurementVendorQuotations, { - fields: [procurementQuotationItems.quotationId], - references: [procurementVendorQuotations.id], - }), - prItem: one(prItems, { - fields: [procurementQuotationItems.prItemId], - references: [prItems.id], - }), - }) -); - -export type ProcurementVendorQuotations = typeof procurementVendorQuotations.$inferSelect; export type Incoterm = typeof incoterms.$inferSelect; diff --git a/db/schema/rfqLastTBE.ts b/db/schema/rfqLastTBE.ts index e690ce4b..20cefba3 100644 --- a/db/schema/rfqLastTBE.ts +++ b/db/schema/rfqLastTBE.ts @@ -1,6 +1,6 @@ import { pgTable, pgView, serial, varchar, text, timestamp, boolean, integer, numeric, date, alias, jsonb, uniqueIndex, index } from "drizzle-orm/pg-core"; import { eq, sql, relations } from "drizzle-orm"; -import { rfqsLast, rfqLastDetails, rfqLastAttachments, rfqLastAttachmentRevisions } from "./rfqLast"; +import { rfqsLast, rfqLastDetails, rfqLastAttachments, rfqLastAttachmentRevisions, rfqPrItems } from "./rfqLast"; import { users } from "./users"; import { vendors } from "./vendors"; import { rfqLastVendorAttachments } from "./rfqVendor"; @@ -393,23 +393,23 @@ export const tbeSessionSummaryView = pgView("tbe_session_summary_view").as((qb) // 문서 검토 통계 totalDocuments: sql<number>`( SELECT COUNT(*) - FROM rfq_last_tbe_document_reviews - WHERE tbe_session_id = ${tbeSession.id} + FROM ${rfqLastTbeDocumentReviews} + WHERE ${rfqLastTbeDocumentReviews.tbeSessionId} = ${tbeSession.id} )`.as("total_documents"), reviewedDocuments: sql<number>`( SELECT COUNT(*) - FROM rfq_last_tbe_document_reviews - WHERE tbe_session_id = ${tbeSession.id} - AND review_status IN ('검토완료', '승인') + FROM ${rfqLastTbeDocumentReviews} + WHERE ${rfqLastTbeDocumentReviews.tbeSessionId} = ${tbeSession.id} + AND ${rfqLastTbeDocumentReviews.reviewStatus} IN ('검토완료', '승인') )`.as("reviewed_documents"), // PDFTron 코멘트 통계 totalComments: sql<number>`( - SELECT COUNT(*) - FROM rfq_last_tbe_pdftron_comments pc - JOIN rfq_last_tbe_document_reviews dr ON pc.document_review_id = dr.id - WHERE dr.tbe_session_id = ${tbeSession.id} + SELECT COALESCE(SUM((${rfqLastTbePdftronComments.commentSummary}->>'total')::int), 0) + FROM ${rfqLastTbePdftronComments} + JOIN ${rfqLastTbeDocumentReviews} ON ${rfqLastTbePdftronComments.documentReviewId} = ${rfqLastTbeDocumentReviews.id} + WHERE ${rfqLastTbeDocumentReviews.tbeSessionId} = ${tbeSession.id} )`.as("total_comments"), @@ -537,54 +537,53 @@ export const tbeLastView = pgView("tbe_last_view").as((qb) => { // PR 아이템 수 prItemsCount: sql<number>`( SELECT COUNT(*) - FROM rfq_pr_items - WHERE rfqs_last_id = ${rfqsLast.id} + FROM ${rfqPrItems} + WHERE ${rfqPrItems.rfqsLastId} = ${rfqsLast.id} )`.as("pr_items_count"), majorItemsCount: sql<number>`( SELECT COUNT(*) - FROM rfq_pr_items - WHERE rfqs_last_id = ${rfqsLast.id} - AND major_yn = true + FROM ${rfqPrItems} + WHERE ${rfqPrItems.rfqsLastId} = ${rfqsLast.id} + AND ${rfqPrItems.majorYn} = true )`.as("major_items_count"), // 구매자 문서 수 (설계 문서) buyerDocumentsCount: sql<number>`( SELECT COUNT(*) - FROM rfq_last_tbe_document_reviews - WHERE tbe_session_id = ${rfqLastTbeSessions.id} - AND document_source = 'buyer' + FROM ${rfqLastTbeDocumentReviews} + WHERE ${rfqLastTbeDocumentReviews.tbeSessionId} = ${rfqLastTbeSessions.id} + AND ${rfqLastTbeDocumentReviews.documentSource} = 'buyer' )`.as("buyer_documents_count"), // 벤더 문서 수 vendorDocumentsCount: sql<number>`( SELECT COUNT(*) - FROM rfq_last_tbe_vendor_documents - WHERE tbe_session_id = ${rfqLastTbeSessions.id} + FROM ${rfqLastTbeVendorDocuments} + WHERE ${rfqLastTbeVendorDocuments.tbeSessionId} = ${rfqLastTbeSessions.id} )`.as("vendor_documents_count"), // 검토 완료 문서 수 reviewedDocumentsCount: sql<number>`( SELECT COUNT(*) - FROM rfq_last_tbe_document_reviews - WHERE tbe_session_id = ${rfqLastTbeSessions.id} - AND review_status IN ('검토완료', '승인') + FROM ${rfqLastTbeDocumentReviews} + WHERE ${rfqLastTbeDocumentReviews.tbeSessionId} = ${rfqLastTbeSessions.id} + AND ${rfqLastTbeDocumentReviews.reviewStatus} IN ('검토완료', '승인') )`.as("reviewed_documents_count"), // PDFTron 코멘트 수 totalCommentsCount: sql<number>`( - SELECT COUNT(*) - FROM rfq_last_tbe_pdftron_comments pc - JOIN rfq_last_tbe_document_reviews dr ON pc.document_review_id = dr.id - WHERE dr.tbe_session_id = ${rfqLastTbeSessions.id} + SELECT COALESCE(SUM((${rfqLastTbePdftronComments.commentSummary}->>'total')::int), 0) + FROM ${rfqLastTbePdftronComments} + JOIN ${rfqLastTbeDocumentReviews} ON ${rfqLastTbePdftronComments.documentReviewId} = ${rfqLastTbeDocumentReviews.id} + WHERE ${rfqLastTbeDocumentReviews.tbeSessionId} = ${rfqLastTbeSessions.id} )`.as("total_comments_count"), unresolvedCommentsCount: sql<number>`( - SELECT COUNT(*) - FROM rfq_last_tbe_pdftron_comments pc - JOIN rfq_last_tbe_document_reviews dr ON pc.document_review_id = dr.id - WHERE dr.tbe_session_id = ${rfqLastTbeSessions.id} - AND pc.status = 'open' + SELECT COALESCE(SUM((${rfqLastTbePdftronComments.commentSummary}->>'open')::int), 0) + FROM ${rfqLastTbePdftronComments} + JOIN ${rfqLastTbeDocumentReviews} ON ${rfqLastTbePdftronComments.documentReviewId} = ${rfqLastTbeDocumentReviews.id} + WHERE ${rfqLastTbeDocumentReviews.tbeSessionId} = ${rfqLastTbeSessions.id} )`.as("unresolved_comments_count"), // 타임스탬프 diff --git a/db/schema/vendors.ts b/db/schema/vendors.ts index d587d441..c194cf61 100644 --- a/db/schema/vendors.ts +++ b/db/schema/vendors.ts @@ -114,8 +114,7 @@ export const vendorPossibleItems = pgTable("vendor_possible_items", { vendorId: integer("vendor_id").notNull().references(() => vendors.id), // itemId: integer("item_id"), // 별도 item 테이블 연동시 itemCode: varchar("item_code", { length: 100 }) - .notNull() - .references(() => items.itemCode, { onDelete: "cascade" }), + .notNull(), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }); |
