diff options
Diffstat (limited to 'app')
200 files changed, 260 insertions, 14575 deletions
diff --git a/app/[lng]/engineering/(engineering)/docu-list-rule/code-groups/page.tsx b/app/[lng]/engineering/(engineering)/docu-list-rule/code-groups/page.tsx deleted file mode 100644 index 5aebf15d..00000000 --- a/app/[lng]/engineering/(engineering)/docu-list-rule/code-groups/page.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import * as React from "react"; -import { type SearchParams } from "@/types/table"; -import { Shell } from "@/components/shell"; -import { Skeleton } from "@/components/ui/skeleton"; -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; -import { getCodeGroups } from "@/lib/docu-list-rule/code-groups/service"; -import { CodeGroupsTable } from "@/lib/docu-list-rule/code-groups/table/code-groups-table"; -import { searchParamsCodeGroupsCache } from "@/lib/docu-list-rule/code-groups/validation"; -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 = searchParamsCodeGroupsCache.parse(searchParams); - - const promises = Promise.all([ - getCodeGroups({ - ...search, - }), - ]); - - return ( - <Shell className="gap-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">Code Group 정의</h2> - <InformationButton pagePath="evcp/docu-list-rule/code-groups" /> - </div> - {/* <p className="text-muted-foreground"> - 문서 번호에 사용될 수 있는 다양한 코드 그룹의 정의를 관리하는 페이지입니다. - </p> */} - </div> - </div> - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}></React.Suspense> - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={7} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["8rem", "12rem", "10rem", "10rem", "12rem", "8rem", "12rem"]} - shrinkZero - /> - } - > - <CodeGroupsTable promises={promises} /> - </React.Suspense> - </Shell> - ); -}
\ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/docu-list-rule/combo-box-settings/page.tsx b/app/[lng]/engineering/(engineering)/docu-list-rule/combo-box-settings/page.tsx deleted file mode 100644 index cf0bf02e..00000000 --- a/app/[lng]/engineering/(engineering)/docu-list-rule/combo-box-settings/page.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import * as React from "react"; -import { Shell } from "@/components/shell"; -import { Skeleton } from "@/components/ui/skeleton"; -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; -import { getComboBoxCodeGroups } from "@/lib/docu-list-rule/combo-box-settings/service"; -import { ComboBoxSettingsTable } from "@/lib/docu-list-rule/combo-box-settings/table/combo-box-settings-table"; -import { InformationButton } from "@/components/information/information-button"; -import { searchParamsCodeGroupsCache } from "@/lib/docu-list-rule/code-groups/validation"; - -interface IndexPageProps { - searchParams: Promise<any>; -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams; - - const promises = Promise.all([ - getComboBoxCodeGroups( - searchParamsCodeGroupsCache.parse(searchParams) - ), - ]); - - return ( - <Shell className="gap-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">Combo Box 설정</h2> - <InformationButton pagePath="evcp/docu-list-rule/combo-box-settings" /> - </div> - {/* <p className="text-muted-foreground"> - Combo Box 옵션을 관리하는 페이지입니다. - 각 Code Group별로 Combo Box에 표시될 옵션들을 설정할 수 있습니다. - </p> */} - </div> - </div> - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}></React.Suspense> - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["8rem", "12rem", "10rem", "8rem", "12rem", "8rem"]} - shrinkZero - /> - } - > - <ComboBoxSettingsTable promises={promises} /> - </React.Suspense> - </Shell> - ); -}
\ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/docu-list-rule/document-class/page.tsx b/app/[lng]/engineering/(engineering)/docu-list-rule/document-class/page.tsx deleted file mode 100644 index 5c2c600e..00000000 --- a/app/[lng]/engineering/(engineering)/docu-list-rule/document-class/page.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import * as React from "react"; -import { Shell } from "@/components/shell"; -import { Skeleton } from "@/components/ui/skeleton"; -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; -import { getDocumentClassCodeGroups } from "@/lib/docu-list-rule/document-class/service"; -import { DocumentClassTable } from "@/lib/docu-list-rule/document-class/table/document-class-table"; -import { InformationButton } from "@/components/information/information-button"; -import { searchParamsDocumentClassCache } from "@/lib/docu-list-rule/document-class/validation"; - -interface IndexPageProps { - searchParams: Promise<any>; -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams; - - const promises = Promise.all([ - getDocumentClassCodeGroups( - searchParamsDocumentClassCache.parse(searchParams) - ), - ]); - - return ( - <Shell className="gap-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">Document Class 관리</h2> - <InformationButton pagePath="evcp/docu-list-rule/document-class" /> - </div> - {/* <p className="text-muted-foreground"> - Document Class를 관리합니다. - </p> */} - </div> - </div> - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}></React.Suspense> - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={4} - searchableColumnCount={1} - filterableColumnCount={1} - cellWidths={["10rem", "20rem", "10rem", "8rem"]} - shrinkZero - /> - } - > - <DocumentClassTable promises={promises} /> - </React.Suspense> - </Shell> - ); -}
\ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/docu-list-rule/layout.tsx b/app/[lng]/engineering/(engineering)/docu-list-rule/layout.tsx deleted file mode 100644 index 25023e4b..00000000 --- a/app/[lng]/engineering/(engineering)/docu-list-rule/layout.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { Metadata } from "next" - -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" - -export const metadata: Metadata = { - title: "Document Numbering Rule", -} - - - -export default async function DocumentNumberingLayout({ - children, - params, -}: { - children: React.ReactNode - params: { lng: string } -}) { - const resolvedParams = await params - const lng = resolvedParams.lng - - const sidebarNavItems = [ - { - title: "Document Class 관리", - href: `/${lng}/engineering/docu-list-rule/document-class`, - }, - { - title: "Code Group 정의", - href: `/${lng}/engineering/docu-list-rule/code-groups`, - }, - { - title: "Combo Box 설정", - href: `/${lng}/engineering/docu-list-rule/combo-box-settings`, - }, - { - title: "Number Type 관리", - href: `/${lng}/engineering/docu-list-rule/number-types`, - }, - { - title: "Number Type별 설정", - href: `/${lng}/engineering/docu-list-rule/number-type-configs`, - }, - ] - - return ( - <> - <div className="container py-6"> - <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow"> - <div className="hidden space-y-6 p-10 pb-16 md:block"> - <div className="space-y-0.5"> - <h2 className="text-2xl font-bold tracking-tight">Document Numbering Rule (해양)</h2> - <p className="text-muted-foreground"> - 벤더 제출 문서 리스트 작성 시에 사용되는 넘버링 - </p> - </div> - - <Separator className="my-6" /> - <div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0"> - <aside className="-mx-4 lg:w-1/5"> - <SidebarNav items={sidebarNavItems} /> - </aside> - <div className="flex-1 ">{children}</div> - </div> - </div> - </section> - </div> - </> - ) -}
\ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/docu-list-rule/number-type-configs/page.tsx b/app/[lng]/engineering/(engineering)/docu-list-rule/number-type-configs/page.tsx deleted file mode 100644 index 4195ba24..00000000 --- a/app/[lng]/engineering/(engineering)/docu-list-rule/number-type-configs/page.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import * as React from "react"; -import { Shell } from "@/components/shell"; -import { Skeleton } from "@/components/ui/skeleton"; -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; -import { NumberTypeConfigsTable } from "@/lib/docu-list-rule/number-type-configs/table/number-type-configs-table"; -import { getNumberTypes } from "@/lib/docu-list-rule/number-types/service"; -import { InformationButton } from "@/components/information/information-button"; - -interface IndexPageProps { - searchParams: Promise<any>; -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams; - - const promises = Promise.all([ - getNumberTypes({ - page: 1, - perPage: 1000, // 모든 Number Type을 가져오기 위해 큰 값 설정 - search: "", - sort: [{ id: "id", desc: false }], // DB 등록 순서대로 정렬 - filters: [], - joinOperator: "and", - flags: ["advancedTable"], - numberTypeId: "", - description: "", - isActive: "" - }), - ]); - - return ( - <Shell className="gap-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">Number Type별 설정</h2> - <InformationButton pagePath="evcp/docu-list-rule/number-type-configs" /> - </div> - {/* <p className="text-muted-foreground"> - 각 문서 번호 유형별로 어떤 코드 그룹들을 어떤 순서로 사용할지 설정하는 페이지입니다. - </p> */} - </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", "12rem", "12rem", "12rem"]} - shrinkZero - /> - } - > - <NumberTypeConfigsTable promises={promises} /> - </React.Suspense> - </Shell> - ); -}
\ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/docu-list-rule/number-types/page.tsx b/app/[lng]/engineering/(engineering)/docu-list-rule/number-types/page.tsx deleted file mode 100644 index 6fa010c7..00000000 --- a/app/[lng]/engineering/(engineering)/docu-list-rule/number-types/page.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import * as React from "react"; -import { Shell } from "@/components/shell"; -import { Skeleton } from "@/components/ui/skeleton"; -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; -import { NumberTypesTable } from "@/lib/docu-list-rule/number-types/table/number-types-table"; -import { getNumberTypes } from "@/lib/docu-list-rule/number-types/service"; -import { InformationButton } from "@/components/information/information-button"; -import { searchParamsNumberTypesCache } from "@/lib/docu-list-rule/number-types/validation"; - -interface IndexPageProps { - searchParams: Promise<any>; -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams; - - const promises = Promise.all([ - getNumberTypes( - searchParamsNumberTypesCache.parse(searchParams) - ), - ]); - - return ( - <Shell className="gap-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">Number Type 관리</h2> - <InformationButton pagePath="evcp/docu-list-rule/number-types" /> - </div> - {/* <p className="text-muted-foreground"> - 문서 번호 유형을 추가, 수정, 삭제할 수 있는 페이지입니다. - </p> */} - </div> - </div> - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}></React.Suspense> - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={4} - searchableColumnCount={1} - filterableColumnCount={0} - cellWidths={["10rem", "20rem", "10rem", "8rem"]} - shrinkZero - /> - } - > - <NumberTypesTable promises={promises} /> - </React.Suspense> - </Shell> - ); -}
\ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/docu-list-rule/page.tsx b/app/[lng]/engineering/(engineering)/docu-list-rule/page.tsx deleted file mode 100644 index b8d3559f..00000000 --- a/app/[lng]/engineering/(engineering)/docu-list-rule/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { redirect } from "next/navigation" - - -export default async function DocumentNumberingPage({ - params, -}: { - params: Promise<{ lng: string }> -}) { - const resolvedParams = await params; - // Code Group 페이지로 리다이렉트 - redirect(`/${resolvedParams.lng}/engineering/docu-list-rule/document-class`) -}
\ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/document-list-only/layout.tsx b/app/[lng]/engineering/(engineering)/document-list-only/layout.tsx deleted file mode 100644 index 17e78c0a..00000000 --- a/app/[lng]/engineering/(engineering)/document-list-only/layout.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Shell } from "@/components/shell" -import VendorDocumentListClientEvcp from "@/components/document-lists/vendor-doc-list-client-evcp" - -// Layout 컴포넌트는 서버 컴포넌트입니다 -export default async function EvcpDocuments({ - children, -}: { - children: React.ReactNode -}) { - return ( - <Shell className="gap-2"> - <VendorDocumentListClientEvcp> - {children} - </VendorDocumentListClientEvcp> - </Shell> - ) -} diff --git a/app/[lng]/engineering/(engineering)/document-list-only/page.tsx b/app/[lng]/engineering/(engineering)/document-list-only/page.tsx deleted file mode 100644 index 5b49a6ef..00000000 --- a/app/[lng]/engineering/(engineering)/document-list-only/page.tsx +++ /dev/null @@ -1,98 +0,0 @@ -// evcp/document-list-only/page.tsx - 전체 계약 대상 문서 목록 -import * as React from "react" -import { Suspense } from "react" -import { Skeleton } from "@/components/ui/skeleton" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { DocumentStagesTable } from "@/lib/vendor-document-list/plant/document-stages-table" -import { documentStageSearchParamsCache } from "@/lib/vendor-document-list/plant/document-stage-validations" -import { getDocumentStagesOnly } from "@/lib/vendor-document-list/plant/document-stages-service" - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -// 문서 테이블 래퍼 컴포넌트 (전체 계약용) -async function DocumentTableWrapper({ - searchParams -}: { - searchParams: SearchParams -}) { - const search = documentStageSearchParamsCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - // 필터 타입 변환 - const convertedFilters = validFilters.map(filter => ({ - id: (filter.id || filter.rowId) as string, - value: filter.value, - operator: (filter.operator === 'iLike' ? 'ilike' : - filter.operator === 'notILike' ? 'notin' : - filter.operator === 'isEmpty' ? 'eq' : - filter.operator === 'isNotEmpty' ? 'ne' : - filter.operator === 'isBetween' ? 'eq' : - filter.operator === 'isRelativeToToday' ? 'eq' : - filter.operator || 'eq') as 'eq' | 'in' | 'ne' | 'lt' | 'lte' | 'gt' | 'gte' | 'like' | 'ilike' | 'notin' - })) - - // evcp: 전체 계약 대상으로 문서 조회 - const documentsPromise = getDocumentStagesOnly({ - ...search, - filters: convertedFilters, - }, -1) // 세션에서 자동으로 도메인 감지 - - return ( - <DocumentStagesTable - promises={Promise.all([documentsPromise])} - contractId={-1} // 전체 계약을 의미 - projectType="plant" // 기본값으로 plant 사용 - /> - ) -} - -function TableLoadingSkeleton() { - return ( - <div className="space-y-4"> - <div className="flex items-center justify-between"> - <Skeleton className="h-6 w-32" /> - <div className="flex items-center gap-2"> - <Skeleton className="h-8 w-20" /> - <Skeleton className="h-8 w-24" /> - </div> - </div> - <div className="rounded-md border"> - <div className="p-4"> - <div className="space-y-3"> - {Array.from({ length: 5 }).map((_, i) => ( - <div key={i} className="flex items-center space-x-4"> - <Skeleton className="h-4 w-4" /> - <Skeleton className="h-4 w-24" /> - <Skeleton className="h-4 w-48" /> - <Skeleton className="h-4 w-20" /> - <Skeleton className="h-4 w-16" /> - <Skeleton className="h-4 w-12" /> - </div> - ))} - </div> - </div> - </div> - </div> - ) -} - -// 메인 페이지 컴포넌트 -export default async function DocumentStagesManagementPage({ - searchParams -}: IndexPageProps) { - const resolvedSearchParams = await searchParams - - return ( - <div className="mx-auto"> - {/* 문서 테이블 */} - <Suspense fallback={<TableLoadingSkeleton />}> - <DocumentTableWrapper - searchParams={resolvedSearchParams} - /> - </Suspense> - </div> - ) -}
\ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/document-list-ship/page.tsx b/app/[lng]/engineering/(engineering)/document-list-ship/page.tsx deleted file mode 100644 index e3915419..00000000 --- a/app/[lng]/engineering/(engineering)/document-list-ship/page.tsx +++ /dev/null @@ -1,144 +0,0 @@ -// page.tsx (간단한 Promise 생성과 로그인 처리) -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 { searchParamsShipDocuCache } from "@/lib/vendor-document-list/validations" -import { getServerSession } from "next-auth" -import { authOptions } from "@/app/api/auth/[...nextauth]/route" -import Link from "next/link" -import { Button } from "@/components/ui/button" -import { LogIn } from "lucide-react" -import { getUserVendorDocumentStats, getUserVendorDocumentStatsAll, getUserVendorDocuments, getUserVendorDocumentsAll } from "@/lib/vendor-document-list/enhanced-document-service" -import { UserVendorDocumentDisplay } from "@/components/ship-vendor-document/user-vendor-document-table-container" -import { InformationButton } from "@/components/information/information-button" -import { UserVendorALLDocumentDisplay } from "@/components/ship-vendor-document-all/user-vendor-document-table-container" -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsShipDocuCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - // Get session - const session = await getServerSession(authOptions) - - // Check if user is logged in - if (!session || !session.user) { - return ( - <Shell className="gap-6"> - <div className="flex items-center justify-between"> - <div> - <div className="flex items-center gap-2"> - <h2 className="text-2xl font-bold tracking-tight"> - 문서 관리 - </h2> - - </div> - {/* <p className="text-muted-foreground"> - 소속 회사의 모든 도서/도면을 확인하고 관리합니다. - </p> */} - </div> - </div> - - <div className="flex flex-col items-center justify-center py-12 text-center"> - <div className="rounded-lg border border-dashed p-10 shadow-sm"> - <h3 className="mb-2 text-xl font-semibold">로그인이 필요합니다</h3> - <p className="mb-6 text-muted-foreground"> - 문서를 확인하려면 먼저 로그인하세요. - </p> - <Button size="lg" asChild> - <Link href="/partners"> - <LogIn className="mr-2 h-4 w-4" /> - 로그인하기 - </Link> - </Button> - </div> - </div> - </Shell> - ) - } - - // User is logged in, get user ID - const requesterId = session.user.id ? Number(session.user.id) : null - - if (!requesterId) { - return ( - <Shell className="gap-6"> - <div className="flex items-center justify-between"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - Document Management - </h2> - </div> - </div> - <div className="flex flex-col items-center justify-center py-12 text-center"> - <div className="rounded-lg border border-dashed p-10 shadow-sm"> - <h3 className="mb-2 text-xl font-semibold">계정 오류</h3> - <p className="mb-6 text-muted-foreground"> - 사용자 정보가 올바르게 설정되지 않았습니다. 관리자에게 문의하세요. - </p> - </div> - </div> - </Shell> - ) - } - - // 검색 파라미터 정리 - const searchInput = { - ...search, - filters: validFilters, - } - - // Promise 생성 (모든 데이터를 페이지에서 처리) - const documentsPromise = getUserVendorDocumentsAll(requesterId, searchInput) - const statsPromise = getUserVendorDocumentStatsAll(requesterId) - - // Promise.all로 감싸서 전달 - const allPromises = Promise.all([documentsPromise, statsPromise]) - const statsResult = await documentsPromise - - - return ( - <Shell className="gap-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"> - 조선 Document Management - </h2> - <InformationButton pagePath="evcp/document-list-ship" /> - </div> - <p className="text-muted-foreground"> - - </p> - </div> - </div> - - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - {/* DateRangePicker can go here */} - </React.Suspense> - - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={8} - searchableColumnCount={1} - filterableColumnCount={3} - cellWidths={["10rem", "30rem", "15rem", "15rem", "15rem", "15rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <UserVendorALLDocumentDisplay - allPromises={allPromises} - /> - </React.Suspense> - </Shell> - ) -} - diff --git a/app/[lng]/engineering/(engineering)/faq/manage/actions.ts b/app/[lng]/engineering/(engineering)/faq/manage/actions.ts deleted file mode 100644 index bc443a8a..00000000 --- a/app/[lng]/engineering/(engineering)/faq/manage/actions.ts +++ /dev/null @@ -1,48 +0,0 @@ -'use server';
-
-import { promises as fs } from 'fs';
-import path from 'path';
-import { FaqCategory } from '@/components/faq/FaqCard';
-import { fallbackLng } from '@/i18n/settings';
-
-const FAQ_CONFIG_PATH = path.join(process.cwd(), 'config', 'faqDataConfig.ts');
-
-export async function updateFaqData(lng: string, newData: FaqCategory[]) {
- try {
- const fileContent = await fs.readFile(FAQ_CONFIG_PATH, 'utf-8');
- const dataMatch = fileContent.match(/export const faqCategories[^=]*=\s*(\{[\s\S]*\});/);
- if (!dataMatch) {
- throw new Error('FAQ 데이터 형식이 올바르지 않습니다.');
- }
-
- const allData = eval(`(${dataMatch[1]})`);
- const updatedData = {
- ...allData,
- [lng]: newData
- };
-
- const newFileContent = `import { FaqCategory } from "@/components/faq/FaqCard";\n\ninterface LocalizedFaqCategories {\n [lng: string]: FaqCategory[];\n}\n\nexport const faqCategories: LocalizedFaqCategories = ${JSON.stringify(updatedData, null, 4)};`;
- await fs.writeFile(FAQ_CONFIG_PATH, newFileContent, 'utf-8');
-
- return { success: true };
- } catch (error) {
- console.error('FAQ 데이터 업데이트 중 오류 발생:', error);
- return { success: false, error: '데이터 업데이트 중 오류가 발생했습니다.' };
- }
-}
-
-export async function getFaqData(lng: string): Promise<{ data: FaqCategory[] }> {
- try {
- const fileContent = await fs.readFile(FAQ_CONFIG_PATH, 'utf-8');
- const dataMatch = fileContent.match(/export const faqCategories[^=]*=\s*(\{[\s\S]*\});/);
- if (!dataMatch) {
- throw new Error('FAQ 데이터 형식이 올바르지 않습니다.');
- }
-
- const allData = eval(`(${dataMatch[1]})`);
- return { data: allData[lng] || allData[fallbackLng] || [] };
- } catch (error) {
- console.error('FAQ 데이터 읽기 중 오류 발생:', error);
- return { data: [] };
- }
-}
\ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/faq/manage/page.tsx b/app/[lng]/engineering/(engineering)/faq/manage/page.tsx deleted file mode 100644 index 011bbfa4..00000000 --- a/app/[lng]/engineering/(engineering)/faq/manage/page.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { FaqManager } from '@/components/faq/FaqManager';
-import { getFaqData, updateFaqData } from './actions';
-import { revalidatePath } from 'next/cache';
-import { FaqCategory } from '@/components/faq/FaqCard';
-
-interface Props {
- params: {
- lng: string;
- }
-}
-
-export default async function FaqManagePage(props: Props) {
- const resolvedParams = await props.params
- const lng = resolvedParams.lng
- const { data } = await getFaqData(lng);
-
- async function handleSave(newData: FaqCategory[]) {
- 'use server';
- await updateFaqData(lng, newData);
- revalidatePath(`/${lng}/evcp/faq`);
- }
-
- return (
- <div className="container py-6">
- <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow">
- <div className="space-y-6 p-10 pb-16">
- <div className="space-y-0.5">
- <h2 className="text-2xl font-bold tracking-tight">FAQ Management</h2>
- <p className="text-muted-foreground">
- Manage FAQ categories and items for {lng.toUpperCase()} language.
- </p>
- </div>
- <FaqManager initialData={data} onSave={handleSave} lng={lng} />
- </div>
- </section>
- </div>
- );
-}
\ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/faq/page.tsx b/app/[lng]/engineering/(engineering)/faq/page.tsx deleted file mode 100644 index 9b62b7e4..00000000 --- a/app/[lng]/engineering/(engineering)/faq/page.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Separator } from "@/components/ui/separator"
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
-import { faqCategories } from "@/config/faqDataConfig"
-import { FaqCard } from "@/components/faq/FaqCard"
-import { Button } from "@/components/ui/button"
-import { Settings } from "lucide-react"
-import Link from "next/link"
-import { fallbackLng } from "@/i18n/settings"
-
-interface Props {
- params: {
- lng: string;
- }
-}
-
-export default async function FaqPage(props: Props) {
- const resolvedParams = await props.params
- const lng = resolvedParams.lng
- const localizedFaqCategories = faqCategories[lng] || faqCategories[fallbackLng];
-
- return (
- <div className="container py-6">
- <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow">
- <div className="space-y-6 p-10 pb-16">
- <div className="flex justify-between items-center">
- <div className="space-y-0.5">
- <h2 className="text-2xl font-bold tracking-tight">Frequently Asked Questions</h2>
- <p className="text-muted-foreground">
- Find answers to common questions about using the EVCP system.
- </p>
- </div>
- <Link href={`/${lng}/evcp/faq/manage`}>
- <Button variant="outline">
- <Settings className="w-4 h-4 mr-2" />
- Manage FAQ
- </Button>
- </Link>
- </div>
- <Separator className="my-6" />
-
- <Tabs defaultValue={localizedFaqCategories[0]?.label} className="space-y-4">
- <TabsList>
- {localizedFaqCategories.map((category) => (
- <TabsTrigger key={category.label} value={category.label}>
- {category.label}
- </TabsTrigger>
- ))}
- </TabsList>
-
- {localizedFaqCategories.map((category) => (
- <TabsContent key={category.label} value={category.label} className="space-y-4">
- {category.items.map((item, index) => (
- <FaqCard key={index} item={item} />
- ))}
- </TabsContent>
- ))}
- </Tabs>
- </div>
- </section>
- </div>
- )
-}
diff --git a/app/[lng]/engineering/(engineering)/form-list/page.tsx b/app/[lng]/engineering/(engineering)/form-list/page.tsx deleted file mode 100644 index a2c6fbb9..00000000 --- a/app/[lng]/engineering/(engineering)/form-list/page.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { searchParamsCache } from "@/lib/form-list/validation" -import { ItemsTable } from "@/lib/items/table/items-table" -import { getFormLists } from "@/lib/form-list/service" -import { FormListsTable } from "@/lib/form-list/table/formLists-table" - - -interface IndexPageProps { - searchParams: Promise<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([ - getFormLists({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - 레지스터 목록 from S-EDP - </h2> - {/* <p className="text-muted-foreground"> - 협력업체 데이터 입력을 위한 레지스터 목록 리스트입니다.{" "} - <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 - /> - } - > - <FormListsTable promises={promises} /> - </React.Suspense> - </Shell> - ) -} diff --git a/app/[lng]/engineering/(engineering)/items/page.tsx b/app/[lng]/engineering/(engineering)/items/page.tsx deleted file mode 100644 index f8d9a5b1..00000000 --- a/app/[lng]/engineering/(engineering)/items/page.tsx +++ /dev/null @@ -1,68 +0,0 @@ -// app/items/page.tsx (업데이트) -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 { searchParamsCache } from "@/lib/items/validations" -import { getItems } from "@/lib/items/service" -import { ItemsTable } from "@/lib/items/table/items-table" -import { ViewModeToggle } from "@/components/data-table/view-mode-toggle" - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - // pageSize 기반으로 모드 자동 결정 - const isInfiniteMode = search.perPage >= 1_000_000 - - // 페이지네이션 모드일 때만 서버에서 데이터 가져오기 - // 무한 스크롤 모드에서는 클라이언트에서 SWR로 데이터 로드 - const promises = isInfiniteMode - ? undefined - : Promise.all([ - getItems(search), // searchParamsCache의 결과를 그대로 사용 - ]) - - 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"> - 패키지 넘버 - </h2> - {/* <p className="text-muted-foreground"> - S-EDP로부터 수신된 패키지 정보이며 PR 전 입찰, 견적에 사용되며 벤더 데이터, 문서와 연결됩니다. - </p> */} - </div> - </div> - - </div> - - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - {/* DateRangePicker 등 추가 컴포넌트 */} - </React.Suspense> - - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - {/* 통합된 ItemsTable 컴포넌트 사용 */} - <ItemsTable promises={promises} /> - </React.Suspense> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/layout.tsx b/app/[lng]/engineering/(engineering)/layout.tsx deleted file mode 100644 index 82b53307..00000000 --- a/app/[lng]/engineering/(engineering)/layout.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { ReactNode } from 'react'; -import { Header } from '@/components/layout/Header'; -import { SiteFooter } from '@/components/layout/Footer'; - -export default function EvcpLayout({ children }: { children: ReactNode }) { - return ( - <div className="relative flex min-h-svh flex-col bg-background"> - {/* <div className="relative flex min-h-svh flex-col bg-slate-100 "> */} - <Header /> - <main className="flex flex-1 flex-col"> - <div className='container-wrapper'> - {children} - </div> - </main> - <SiteFooter/> - </div> - ); -}
\ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/projects/page.tsx b/app/[lng]/engineering/(engineering)/projects/page.tsx deleted file mode 100644 index 199b175b..00000000 --- a/app/[lng]/engineering/(engineering)/projects/page.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { ItemsTable } from "@/lib/items/table/items-table" -import { getProjectLists } from "@/lib/projects/service" -import { ProjectsTable } from "@/lib/projects/table/projects-table" -import { searchParamsProjectsCache } from "@/lib/projects/validation" - - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsProjectsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getProjectLists({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - 프로젝트 리스트 from S-EDP - </h2> - {/* <p className="text-muted-foreground"> - S-EDP로부터 수신하는 프로젝트 리스트입니다. 향후 MDG로 전환됩니다.{" "} - <span className="inline-flex items-center whitespace-nowrap"> - <Ellipsis className="size-3" /> - <span className="ml-1">버튼</span> - </span> - 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. - </p> */} - </div> - </div> - </div> - - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - {/* <DateRangePicker - triggerSize="sm" - triggerClassName="ml-auto w-56 sm:w-60" - align="end" - shallow={false} - /> */} - </React.Suspense> - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <ProjectsTable promises={promises} /> - </React.Suspense> - </Shell> - ) -} diff --git a/app/[lng]/engineering/(engineering)/report/page.tsx b/app/[lng]/engineering/(engineering)/report/page.tsx deleted file mode 100644 index 64778ef1..00000000 --- a/app/[lng]/engineering/(engineering)/report/page.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import * as React from "react"; -import { Skeleton } from "@/components/ui/skeleton"; -import { Shell } from "@/components/shell"; -import { ErrorBoundary } from "@/components/error-boundary"; -import { getDashboardData } from "@/lib/dashboard/service"; -import { DashboardClient } from "@/lib/dashboard/dashboard-client"; - -// 데이터 fetch 시 비동기 함수 호출 후 await 하므로 static-pre-render 과정에서 dynamic-server-error 발생. -// 따라서, dynamic 속성을 force-dynamic 으로 설정하여 동적 렌더링 처리 -// getDashboardData 함수에 대한 Promise를 넘기는 식으로 수정하게 되면 force-dynamic 선언을 제거해도 됨. -export const dynamic = 'force-dynamic' - -export default async function IndexPage() { - // domain을 명시적으로 전달 - const domain = "engineering"; - - try { - // 서버에서 직접 데이터 fetch - const dashboardData = await getDashboardData(domain); - - return ( - <Shell className="gap-2"> - <DashboardClient initialData={dashboardData} /> - </Shell> - ); - } catch (error) { - console.error("Dashboard data fetch error:", error); - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-center py-12"> - <div className="text-center space-y-2"> - <p className="text-destructive">데이터를 불러오는데 실패했습니다.</p> - <p className="text-muted-foreground text-sm">{error instanceof Error ? error.message : "알 수 없는 오류가 발생했습니다."}</p> - </div> - </div> - </Shell> - ); - } -} - -function DashboardSkeleton() { - return ( - <div className="space-y-6"> - {/* 헤더 스켈레톤 */} - <div className="flex items-center justify-between"> - <div className="space-y-2"> - <Skeleton className="h-8 w-48" /> - <Skeleton className="h-4 w-72" /> - </div> - <Skeleton className="h-10 w-24" /> - </div> - - {/* 요약 카드 스켈레톤 */} - <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"> - {[...Array(4)].map((_, i) => ( - <div key={i} className="space-y-3 p-6 border rounded-lg"> - <div className="flex items-center justify-between"> - <Skeleton className="h-4 w-16" /> - <Skeleton className="h-4 w-4" /> - </div> - <Skeleton className="h-8 w-12" /> - <Skeleton className="h-3 w-20" /> - </div> - ))} - </div> - - {/* 차트 스켈레톤 */} - <div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> - {[...Array(2)].map((_, i) => ( - <div key={i} className="space-y-4 p-6 border rounded-lg"> - <div className="space-y-2"> - <Skeleton className="h-6 w-32" /> - <Skeleton className="h-4 w-48" /> - </div> - <Skeleton className="h-[300px] w-full" /> - </div> - ))} - </div> - - {/* 탭 스켈레톤 */} - <div className="space-y-4"> - <Skeleton className="h-10 w-64" /> - <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> - {[...Array(6)].map((_, i) => ( - <div key={i} className="space-y-4 p-6 border rounded-lg"> - <Skeleton className="h-6 w-32" /> - <div className="space-y-3"> - <div className="flex justify-between"> - <Skeleton className="h-4 w-16" /> - <Skeleton className="h-4 w-12" /> - </div> - <div className="flex gap-2"> - <Skeleton className="h-6 w-16" /> - <Skeleton className="h-6 w-16" /> - <Skeleton className="h-6 w-16" /> - </div> - <Skeleton className="h-2 w-full" /> - </div> - </div> - ))} - </div> - </div> - </div> - ); -} diff --git a/app/[lng]/engineering/(engineering)/tag-numbering/page.tsx b/app/[lng]/engineering/(engineering)/tag-numbering/page.tsx deleted file mode 100644 index 86ad2ec2..00000000 --- a/app/[lng]/engineering/(engineering)/tag-numbering/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { searchParamsCache } from "@/lib/tag-numbering/validation" -import { getTagNumbering } from "@/lib/tag-numbering/service" -import { TagNumberingTable } from "@/lib/tag-numbering/table/tagNumbering-table" - - -interface IndexPageProps { - searchParams: Promise<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([ - getTagNumbering({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - 태그 타입 목록 from S-EDP - </h2> - {/* <p className="text-muted-foreground"> - 태그 넘버링을 위한 룰셋을 S-EDP로부터 가져오고 확인할 수 있습니다{" "} - <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 - /> - } - > - <TagNumberingTable promises={promises} /> - </React.Suspense> - </Shell> - ) -} diff --git a/app/[lng]/engineering/(engineering)/tasks/page.tsx b/app/[lng]/engineering/(engineering)/tasks/page.tsx deleted file mode 100644 index 91b946fb..00000000 --- a/app/[lng]/engineering/(engineering)/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]/engineering/(engineering)/tbe/page.tsx b/app/[lng]/engineering/(engineering)/tbe/page.tsx deleted file mode 100644 index 211cf376..00000000 --- a/app/[lng]/engineering/(engineering)/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/app/[lng]/engineering/(engineering)/vendor-check-list/page.tsx b/app/[lng]/engineering/(engineering)/vendor-check-list/page.tsx deleted file mode 100644 index e6f9ce82..00000000 --- a/app/[lng]/engineering/(engineering)/vendor-check-list/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { getGenralEvaluationsSchema } from "@/lib/general-check-list/validation" -import { GeneralEvaluationsTable } from "@/lib/general-check-list/table/general-check-list-table" -import { getGeneralEvaluations } from "@/lib/general-check-list/service" - - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = getGenralEvaluationsSchema.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getGeneralEvaluations({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - 협력업체 평가자료 문항 관리 - </h2> - {/* <p className="text-muted-foreground"> - 협력업체 평가에 사용되는 정기평가 체크리스트를 관리{" "} - <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 - /> - } - > - <GeneralEvaluationsTable promises={promises} /> - </React.Suspense> - </Shell> - ) -} diff --git a/app/[lng]/engineering/(engineering)/vendor-data/form/[packageId]/[formId]/[projectId]/[contractId]/page.tsx b/app/[lng]/engineering/(engineering)/vendor-data/form/[packageId]/[formId]/[projectId]/[contractId]/page.tsx deleted file mode 100644 index f69aa525..00000000 --- a/app/[lng]/engineering/(engineering)/vendor-data/form/[packageId]/[formId]/[projectId]/[contractId]/page.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import DynamicTable from "@/components/form-data/form-data-table"; -import { findContractItemId, getFormData, getFormId } from "@/lib/forms/services"; - -interface IndexPageProps { - params: { - lng: string; - packageId: string; - formId: string; - projectId: string; - contractId: string; - - - }; - searchParams?: { - mode?: string; - }; -} - -export default async function FormPage({ params, searchParams }: IndexPageProps) { - // 1) 구조 분해 할당 - const resolvedParams = await params; - - // 2) searchParams도 await 필요 - const resolvedSearchParams = await searchParams; - - // 3) 구조 분해 할당 - const { lng, packageId, formId: formCode, projectId,contractId } = resolvedParams; - - // URL 쿼리 파라미터에서 mode 가져오기 (await 해서 사용) - const mode = resolvedSearchParams?.mode === "ENG" ? "ENG" : "IM"; // 기본값은 IM - - // 4) 변환 - let packageIdAsNumber = Number(packageId); - const contractIdAsNumber = Number(contractId); - - // packageId가 0이면 contractId와 formCode로 실제 contractItemId 찾기 - if (packageIdAsNumber === 0 && contractIdAsNumber > 0) { - console.log(`packageId가 0이므로 contractId ${contractIdAsNumber}와 formCode ${formCode}로 contractItemId 조회`); - - const foundContractItemId = await findContractItemId(contractIdAsNumber, formCode); - - if (foundContractItemId) { - console.log(`contractItemId ${foundContractItemId}를 찾았습니다. 이 값을 사용합니다.`); - packageIdAsNumber = foundContractItemId; - } else { - console.warn(`contractItemId를 찾을 수 없습니다. packageId는 계속 0으로 유지됩니다.`); - } - } - - // 5) DB 조회 - const { columns, data, editableFieldsMap } = await getFormData(formCode, packageIdAsNumber); - - - // 6) formId 및 report temp file 조회 - const { formId } = await getFormId(String(packageIdAsNumber), formCode); - - // 7) 예외 처리 - if (!columns) { - return ( - <p className="text-red-500">해당 폼의 메타 정보를 불러올 수 없습니다. ENG 모드의 경우에는 SHI 관리자에게 폼 생성 요청을 하시기 바랍니다.</p> - ); - } - - // 8) 렌더링 - return ( - <div className="space-y-6"> - <DynamicTable - contractItemId={packageIdAsNumber} - formCode={formCode} - formId={formId} - columnsJSON={columns} - dataJSON={data} - projectId={Number(projectId)} - editableFieldsMap={editableFieldsMap} // 새로 추가 - mode={mode} // 모드 전달 - /> - </div> - ); -}
\ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/vendor-data/layout.tsx b/app/[lng]/engineering/(engineering)/vendor-data/layout.tsx deleted file mode 100644 index 7d00359c..00000000 --- a/app/[lng]/engineering/(engineering)/vendor-data/layout.tsx +++ /dev/null @@ -1,67 +0,0 @@ -// app/vendor-data/layout.tsx -import * as React from "react" -import { cookies } from "next/headers" -import { Shell } from "@/components/shell" -import { getVendorProjectsAndContracts } from "@/lib/vendor-data/services" -import { VendorDataContainer } from "@/components/vendor-data/vendor-data-container" -import { InformationButton } from "@/components/information/information-button" -// Layout 컴포넌트는 서버 컴포넌트입니다 -export default async function VendorDataLayout({ - children, -}: { - children: React.ReactNode -}) { - // evcp: 전체 계약 대상으로 프로젝트 데이터 가져오기 - const projects = await getVendorProjectsAndContracts() - - // 레이아웃 설정 쿠키 가져오기 - // Next.js 15에서는 cookies()가 Promise를 반환하므로 await 사용 - const cookieStore = await cookies() - - // 이제 cookieStore.get() 메서드 사용 가능 - const layout = cookieStore.get("react-resizable-panels:layout:mail") - const collapsed = cookieStore.get("react-resizable-panels:collapsed") - - const defaultLayout = layout ? JSON.parse(layout.value) : undefined - const defaultCollapsed = collapsed ? JSON.parse(collapsed.value) : undefined - - 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="partners/vendor-data" /> - </div> - {/* <p className="text-muted-foreground"> - 각종 Data 입력할 수 있습니다 - </p> */} - </div> - </div> - </div> - - <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow"> - <div className="hidden flex-col md:flex"> - {projects.length === 0 ? ( - <div className="p-4 text-center text-sm text-muted-foreground"> - No projects found for this vendor. - </div> - ) : ( - <VendorDataContainer - projects={projects} - defaultLayout={defaultLayout} - defaultCollapsed={defaultCollapsed} - navCollapsedSize={4} - > - {/* 페이지별 콘텐츠가 여기에 들어갑니다 */} - {children} - </VendorDataContainer> - )} - </div> - </section> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/vendor-data/page.tsx b/app/[lng]/engineering/(engineering)/vendor-data/page.tsx deleted file mode 100644 index ddc21a2b..00000000 --- a/app/[lng]/engineering/(engineering)/vendor-data/page.tsx +++ /dev/null @@ -1,28 +0,0 @@ -// evcp/vendor-data/page.tsx - 전체 계약 대상 협력업체 데이터 -import * as React from "react" -import { Separator } from "@/components/ui/separator" - -export default async function IndexPage() { - return ( - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium">전체 계약 협력업체 데이터 대시보드</h3> - <p className="text-sm text-muted-foreground"> - 모든 계약의 협력업체 데이터를 확인하고 관리할 수 있습니다. - </p> - </div> - <Separator /> - <div className="grid gap-4"> - <div className="rounded-lg border p-4"> - <h4 className="text-sm font-medium">사용 방법</h4> - <p className="text-sm text-muted-foreground mt-1"> - 1. 왼쪽 사이드바에서 계약을 선택하세요.<br /> - 2. 선택한 계약의 패키지 항목을 클릭하세요.<br /> - 3. 패키지의 태그 정보를 확인하고 관리할 수 있습니다.<br /> - 4. 폼 항목을 클릭하여 칼럼 정보를 확인하고 관리할 수 있습니다. - </p> - </div> - </div> - </div> - ) -}
\ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/vendor-data/tag/[id]/page.tsx b/app/[lng]/engineering/(engineering)/vendor-data/tag/[id]/page.tsx deleted file mode 100644 index 7250732f..00000000 --- a/app/[lng]/engineering/(engineering)/vendor-data/tag/[id]/page.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { TagsTable } from "@/lib/tags/table/tag-table" -import { searchParamsCache } from "@/lib/tags/validations" -import { getTags } from "@/lib/tags/service" - -interface IndexPageProps { - params: { - id: string - } - searchParams: Promise<SearchParams> -} - -export default async function TagPage(props: IndexPageProps) { - const resolvedParams = await props.params - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getTags({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - // 4) 렌더링 - return ( - <div className="space-y-6"> - <div> - <TagsTable promises={promises} selectedPackageId={idAsNumber}/> - </div> - </div> - ) -}
\ No newline at end of file diff --git a/app/[lng]/engineering/(engineering)/vendor-investigation/page.tsx b/app/[lng]/engineering/(engineering)/vendor-investigation/page.tsx deleted file mode 100644 index af9f3e11..00000000 --- a/app/[lng]/engineering/(engineering)/vendor-investigation/page.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" - -import { VendorsInvestigationTable } from "@/lib/vendor-investigation/table/investigation-table" -import { getVendorsInvestigation } from "@/lib/vendor-investigation/service" -import { searchParamsInvestigationCache } from "@/lib/vendor-investigation/validations" - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsInvestigationCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getVendorsInvestigation({ - ...search, - filters: validFilters, - }), - ]) - - return ( - <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"> - 협력업체 실사 관리 - </h2> - {/* <p className="text-muted-foreground"> - 요청된 Vendor 실사에 대한 스케줄 정보를 관리하고 결과를 입력할 수 있습니다. - - </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 - /> - } - > - <VendorsInvestigationTable promises={promises}/> - </React.Suspense> - </Shell> - ) -} diff --git a/app/[lng]/engineering/page.tsx b/app/[lng]/engineering/page.tsx deleted file mode 100644 index f9662cb7..00000000 --- a/app/[lng]/engineering/page.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Metadata } from "next" -import { Suspense } from "react" -import { LoginFormSkeleton } from "@/components/login/login-form-skeleton" -import { LoginFormSHI } from "@/components/login/login-form-shi" - -export const metadata: Metadata = { - title: "eVCP Portal", - description: "", -} - -export default function AuthenticationPage() { - - - return ( - <> - <Suspense fallback={<LoginFormSkeleton/>}> - <LoginFormSHI /> - </Suspense> - </> - ) -} diff --git a/app/[lng]/evcp/(evcp)/(procurement)/p-items/page.tsx b/app/[lng]/evcp/(evcp)/(master-data)/p-items/page.tsx index e3810b5b..2b907a75 100644 --- a/app/[lng]/evcp/(evcp)/(procurement)/p-items/page.tsx +++ b/app/[lng]/evcp/(evcp)/(master-data)/p-items/page.tsx @@ -1,62 +1,62 @@ -import * as React from "react"
-import { type SearchParams } from "@/types/table"
-
-import { getValidFilters } from "@/lib/data-table"
-import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"
-import { Shell } from "@/components/shell"
-import { getProcurementItems } from "@/lib/procurement-items/service"
-import { ProcurementItemsTable } from "@/lib/procurement-items/table/procurement-items-table"
-import { searchParamsCache } from "@/lib/procurement-items/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 = searchParamsCache.parse(searchParams)
-
- const validFilters = getValidFilters(search.filters)
-
- const promises = Promise.all([
- getProcurementItems({
- ...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">
- 1회성 품목 관리
- </h2>
- <InformationButton pagePath="evcp/procurement-items" />
- </div>
- <p className="text-muted-foreground">
- 입찰에서 사용하는 1회성 품목을 등록하고 관리합니다.
- </p>
- </div>
- </div>
- </div>
-
- <React.Suspense
- fallback={
- <DataTableSkeleton
- columnCount={8}
- searchableColumnCount={1}
- filterableColumnCount={2}
- cellWidths={["10rem", "20rem", "8rem", "12rem", "6rem", "8rem", "10rem", "10rem"]}
- shrinkZero
- />
- }
- >
- <ProcurementItemsTable promises={promises} />
- </React.Suspense>
- </Shell>
- )
-}
+import * as React from "react" +import { type SearchParams } from "@/types/table" + +import { getValidFilters } from "@/lib/data-table" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getProcurementItems } from "@/lib/procurement-items/service" +import { ProcurementItemsTable } from "@/lib/procurement-items/table/procurement-items-table" +import { searchParamsCache } from "@/lib/procurement-items/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 = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getProcurementItems({ + ...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"> + 1회성 품목 관리 + </h2> + <InformationButton pagePath="evcp/procurement-items" /> + </div> + <p className="text-muted-foreground"> + 입찰에서 사용하는 1회성 품목을 등록하고 관리합니다. + </p> + </div> + </div> + </div> + + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={8} + searchableColumnCount={1} + filterableColumnCount={2} + cellWidths={["10rem", "20rem", "8rem", "12rem", "6rem", "8rem", "10rem", "10rem"]} + shrinkZero + /> + } + > + <ProcurementItemsTable promises={promises} /> + </React.Suspense> + </Shell> + ) +} 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/app/[lng]/evcp/(evcp)/(procurement)/legal-review/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/legal-review/page.tsx deleted file mode 100644 index 44150492..00000000 --- a/app/[lng]/evcp/(evcp)/(procurement)/legal-review/page.tsx +++ /dev/null @@ -1,87 +0,0 @@ -// app/(routes)/legal-works/page.tsx 수정 - -import * as React from "react"; -import { Metadata } from "next"; -import { type SearchParams } from "@/types/table"; -import { Shell } from "@/components/shell"; -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; -import { InformationButton } from "@/components/information/information-button"; -import { Badge } from "@/components/ui/badge"; // ✅ Badge 추가 -import { SearchParamsCacheLegalWorks } from "@/lib/legal-review/validations"; -import { getLegalWorks } from "@/lib/legal-review/service"; -import { LegalWorksTable } from "@/lib/legal-review/status/legal-table"; - -export const dynamic = "force-dynamic"; -export const revalidate = 0; - -export const metadata: Metadata = { - title: "법무검토 관리", - description: "법무 검토 요청 및 답변을 관리합니다.", -}; - -interface LegalWorksPageProps { - searchParams: Promise<SearchParams>; -} - -export default async function LegalWorksPage({ searchParams }: LegalWorksPageProps) { - const rawParams = await searchParams; - const parsedSearch = SearchParamsCacheLegalWorks.parse(rawParams); - - // ✅ EvaluationTargetsPage와 동일한 패턴으로 currentYear 추가 - const currentYear = new Date().getFullYear(); - - const promises = Promise.all([ - getLegalWorks(parsedSearch) - ]); - - return ( - <Shell className="gap-4"> - {/* Header - EvaluationTargetsPage와 동일한 패턴 */} - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center gap-2"> - <h2 className="text-2xl font-bold tracking-tight">법무검토 관리</h2> - <InformationButton pagePath="evcp/legal-review" /> - {/* ✅ EvaluationTargetsPage와 동일하게 Badge 추가 */} - <Badge variant="outline" className="text-sm"> - {currentYear}년 - </Badge> - </div> - </div> - - {/* Table */} - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={13} - searchableColumnCount={3} - filterableColumnCount={4} - cellWidths={[ - "3rem", // checkbox - "4rem", // No. - "5rem", // 구분 - "6rem", // 상태 - "8rem", // Vendor Code - "12rem", // Vendor Name - "4rem", // 긴급여부 - "7rem", // 답변요청일 - "7rem", // 의뢰일 - "7rem", // 답변예정일 - "7rem", // 법무완료일 - "8rem", // 검토요청자 - "8rem", // 법무답변자 - "4rem", // 첨부파일 - "8rem", // actions - ]} - shrinkZero - /> - } - > - {/* ✅ currentYear prop 추가 - EvaluationTargetsTable과 동일한 패턴 */} - <LegalWorksTable - promises={promises} - currentYear={currentYear} - /> - </React.Suspense> - </Shell> - ); -}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/layout.tsx b/app/[lng]/evcp/(evcp)/layout.tsx index 82b53307..7fe7f3e7 100644 --- a/app/[lng]/evcp/(evcp)/layout.tsx +++ b/app/[lng]/evcp/(evcp)/layout.tsx @@ -1,12 +1,40 @@ import { ReactNode } from 'react'; import { Header } from '@/components/layout/Header'; import { SiteFooter } from '@/components/layout/Footer'; +import { getServerSession } from "next-auth"; +import { authOptions } from "@/app/api/auth/[...nextauth]/route"; +import { verifyNonsapPermission } from "@/lib/nonsap/auth-service"; +import { PermissionChecker } from "@/components/common/permission-checker"; + +export default async function EvcpLayout({ children }: { children: ReactNode }) { + const session = await getServerSession(authOptions); + + let isAuthorized = true; + let authMessage = ""; + + // Only check permission if user is logged in + if (session?.user?.id) { + try { + const result = await verifyNonsapPermission( + parseInt(session.user.id), + ['SEARCH'] + ); + isAuthorized = result.authorized; + authMessage = result.message || ""; + } catch (error) { + console.error("Permission check failed:", error); + // Default to true in case of error to avoid blocking access due to system error + // but logic could be changed to false for strict security + isAuthorized = true; + authMessage = "Permission check error"; + } + } -export default function EvcpLayout({ children }: { children: ReactNode }) { return ( <div className="relative flex min-h-svh flex-col bg-background"> {/* <div className="relative flex min-h-svh flex-col bg-slate-100 "> */} <Header /> + <PermissionChecker authorized={isAuthorized} message={authMessage} /> <main className="flex flex-1 flex-col"> <div className='container-wrapper'> {children} @@ -15,4 +43,4 @@ export default function EvcpLayout({ children }: { children: ReactNode }) { <SiteFooter/> </div> ); -}
\ No newline at end of file +} diff --git a/app/[lng]/partners/(partners)/cbe/page.tsx b/app/[lng]/partners/(partners)/cbe/page.tsx deleted file mode 100644 index 4655cb60..00000000 --- a/app/[lng]/partners/(partners)/cbe/page.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getCBEbyVendorId, } from "@/lib/rfqs/service" -import { searchParamsCBECache } from "@/lib/rfqs/validations" -import { getServerSession } from "next-auth" -import { authOptions } from "@/app/api/auth/[...nextauth]/route" -import { TbeVendorTable } from "@/lib/vendor-rfq-response/vendor-tbe-table/tbe-table" -import * as React from "react" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { CbeVendorTable } from "@/lib/vendor-rfq-response/vendor-cbe-table/cbe-table" -import { InformationButton } from "@/components/information/information-button" -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise<SearchParams> -} - -export default async function CBEPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsCBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const session = await getServerSession(authOptions) - const vendorId = session?.user.companyId - // const vendorId = "17" - - const idAsNumber = Number(vendorId) - - const promises = Promise.all([ - getCBEbyVendorId({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - - 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"> - CBE 관리 - </h2> - <InformationButton pagePath="partners/cbe" /> - </div> - {/* <p className="text-sm text-muted-foreground"> - CBE에 응답하고 커뮤니케이션을 할 수 있습니다.{" "} - </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 - /> - } - > - <CbeVendorTable promises={promises} /> - </React.Suspense> - </Shell> - ) -} diff --git a/app/[lng]/partners/(partners)/rfq-answer/[vendorId]/[rfqRecordId]/page.tsx b/app/[lng]/partners/(partners)/rfq-answer/[vendorId]/[rfqRecordId]/page.tsx deleted file mode 100644 index 898dc41b..00000000 --- a/app/[lng]/partners/(partners)/rfq-answer/[vendorId]/[rfqRecordId]/page.tsx +++ /dev/null @@ -1,174 +0,0 @@ -// app/vendor/responses/[vendorId]/[rfqRecordId]/[rfqType]/page.tsx -import * as React from "react"; -import Link from "next/link"; -import { Metadata } from "next"; -import { getServerSession } from "next-auth/next"; -import { authOptions } from "@/app/api/auth/[...nextauth]/route"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; -import { Alert, AlertDescription } from "@/components/ui/alert"; -import { Progress } from "@/components/ui/progress"; -import { - ArrowLeft, - FileText, - AlertTriangle, - TrendingUp, - CheckCircle2, - RefreshCw, - GitBranch, - Clock, - FileCheck, - Calendar -} from "lucide-react"; -import { Shell } from "@/components/shell"; -import { formatDate } from "@/lib/utils"; -import { getRfqAttachmentResponsesWithRevisions } from "@/lib/b-rfq/service"; -import { FinalRfqResponseTable } from "@/lib/b-rfq/vendor-response/response-detail-table"; - -export const metadata: Metadata = { - title: "RFQ 응답 상세", - description: "RFQ 첨부파일별 응답 관리 - 고급 리비전 추적", -}; - -interface RfqResponseDetailPageProps { - params: Promise<{ - vendorId: string; - rfqRecordId: string; - }>; -} - -export default async function RfqResponseDetailPage(props: RfqResponseDetailPageProps) { - const params = await props.params; - const { vendorId, rfqRecordId, rfqType } = params; - - // 인증 확인 - const session = await getServerSession(authOptions); - - if (!session || !session.user) { - return ( - <Shell className="gap-6"> - <div className="text-center py-12"> - <p className="text-muted-foreground">로그인이 필요합니다.</p> - </div> - </Shell> - ); - } - - // 벤더 권한 확인 - if (session.user.domain !== "partners" || String(session.user.companyId) !== vendorId) { - return ( - <Shell className="gap-6"> - <div className="text-center py-12"> - <p className="text-muted-foreground">접근 권한이 없습니다.</p> - </div> - </Shell> - ); - } - - // 데이터 조회 (뷰 기반 고급 리비전 정보 포함) - const { data: responses, rfqInfo, vendorInfo, statistics, progressSummary } = - await getRfqAttachmentResponsesWithRevisions(vendorId, rfqRecordId); - - console.log("Enhanced RFQ Data:", { responses, statistics, progressSummary }); - - if (!rfqInfo) { - return ( - <Shell className="gap-6"> - <div className="text-center py-12"> - <p className="text-muted-foreground">RFQ 정보를 찾을 수 없습니다.</p> - </div> - </Shell> - ); - } - - const stats = statistics; - - return ( - <Shell className="gap-6"> - {/* 헤더 */} - <div className="flex items-center justify-between"> - <div className="flex items-center gap-4"> - <Button variant="ghost" size="sm" asChild> - <Link href="/partners/rfq-answer"> - <ArrowLeft className="h-4 w-4 mr-2" /> - 돌아가기 - </Link> - </Button> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - {rfqInfo.rfqCode} - RFQ 응답 관리 - </h2> - <p className="text-muted-foreground"> - 고급 리비전 추적 및 응답 상태 관리 - </p> - </div> - </div> - - {/* 마감일 표시 */} - {progressSummary?.daysToDeadline !== undefined && ( - <div className="text-right"> - <div className="flex items-center gap-2 text-sm text-muted-foreground"> - <Calendar className="h-4 w-4" /> - <span>마감까지</span> - </div> - <div className={`text-lg font-bold ${ - progressSummary.daysToDeadline < 0 - ? 'text-red-600' - : progressSummary.daysToDeadline <= 3 - ? 'text-orange-600' - : 'text-green-600' - }`}> - {progressSummary.daysToDeadline < 0 - ? `${Math.abs(progressSummary.daysToDeadline)}일 초과` - : `${progressSummary.daysToDeadline}일 남음` - } - </div> - </div> - )} - </div> - - {/* 중요 알림들 */} - <div className="space-y-3"> - {stats.versionMismatch > 0 && ( - <Alert className="border-blue-200 bg-blue-50"> - <RefreshCw className="h-4 w-4 text-blue-600" /> - <AlertDescription className="text-blue-800"> - <strong>{stats.versionMismatch}개 항목</strong>에서 발주처의 최신 리비전과 응답 리비전이 일치하지 않습니다. - 최신 버전으로 업데이트를 권장합니다. - </AlertDescription> - </Alert> - )} - - {progressSummary?.daysToDeadline !== undefined && progressSummary.daysToDeadline <= 3 && progressSummary.daysToDeadline >= 0 && ( - <Alert className="border-orange-200 bg-orange-50"> - <Clock className="h-4 w-4 text-orange-600" /> - <AlertDescription className="text-orange-800"> - 마감일이 <strong>{progressSummary.daysToDeadline}일</strong> 남았습니다. - 미응답 항목({stats.pending}개)의 신속한 처리가 필요합니다. - </AlertDescription> - </Alert> - )} - - {progressSummary?.attachmentsWithMultipleRevisions > 0 && ( - <Alert className="border-purple-200 bg-purple-50"> - <GitBranch className="h-4 w-4 text-purple-600" /> - <AlertDescription className="text-purple-800"> - <strong>{progressSummary.attachmentsWithMultipleRevisions}개 첨부파일</strong>에 - 다중 리비전이 있습니다. 히스토리를 확인하여 올바른 버전으로 응답해주세요. - </AlertDescription> - </Alert> - )} - </div> - <FinalRfqResponseTable - data={responses} - statistics={stats} - showHeader={true} - title="첨부파일별 응답 현황" - /> - - - - </Shell> - ); -}
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/rfq-answer/page.tsx b/app/[lng]/partners/(partners)/rfq-answer/page.tsx deleted file mode 100644 index 6eae491e..00000000 --- a/app/[lng]/partners/(partners)/rfq-answer/page.tsx +++ /dev/null @@ -1,213 +0,0 @@ -// app/vendor/responses/page.tsx -import * as React from "react"; -import Link from "next/link"; -import { Metadata } from "next"; -import { getServerSession } from "next-auth/next"; -import { authOptions } from "@/app/api/auth/[...nextauth]/route"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { LogIn, FileX, Clock, CheckCircle, AlertTriangle } from "lucide-react"; -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; -import { Shell } from "@/components/shell"; -import { getValidFilters } from "@/lib/data-table"; -import { type SearchParams } from "@/types/table"; -import { searchParamsVendorResponseCache } from "@/lib/b-rfq/validations"; -import { getVendorResponseProgress, getVendorResponseStatusCounts, getVendorRfqResponses } from "@/lib/b-rfq/service"; -import { VendorResponsesTable } from "@/lib/b-rfq/vendor-response/vendor-responses-table"; -import { InformationButton } from "@/components/information/information-button" -export const metadata: Metadata = { - title: "응답 관리", - description: "RFQ 첨부파일 응답 현황을 관리합니다", -}; - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsVendorResponseCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - // 인증 확인 - const session = await getServerSession(authOptions); - - // 로그인 확인 - if (!session || !session.user) { - return ( - <Shell className="gap-6"> - <div className="flex items-center justify-between"> - <div> - <div className="flex items-center gap-2"> - <h2 className="text-2xl font-bold tracking-tight"> - 응답 관리 - </h2> - <InformationButton pagePath="partners/rfq-answer" /> - </div> - {/* <p className="text-muted-foreground"> - RFQ 첨부파일 응답 현황을 확인하고 관리합니다. - </p> */} - </div> - </div> - - <div className="flex flex-col items-center justify-center py-12 text-center"> - <div className="rounded-lg border border-dashed p-10 shadow-sm"> - <h3 className="mb-2 text-xl font-semibold">로그인이 필요합니다</h3> - <p className="mb-6 text-muted-foreground"> - 응답 현황을 확인하려면 먼저 로그인하세요. - </p> - <Button size="lg" asChild> - <Link href="/partners?callbackUrl=/vendor/responses"> - <LogIn className="mr-2 h-4 w-4" /> - 로그인하기 - </Link> - </Button> - </div> - </div> - </Shell> - ); - } - - // 벤더 ID 확인 - const vendorId = session.user.companyId ? String(session.user.companyId) : "0"; - - // 벤더 권한 확인 - if (session.user.domain !== "partners") { - return ( - <Shell className="gap-6"> - <div className="flex items-center justify-between"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - 접근 권한 없음 - </h2> - </div> - </div> - <div className="flex flex-col items-center justify-center py-12 text-center"> - <div className="rounded-lg border border-dashed p-10 shadow-sm"> - <h3 className="mb-2 text-xl font-semibold">벤더 계정이 필요합니다</h3> - <p className="mb-6 text-muted-foreground"> - 벤더 계정으로 로그인해주세요. - </p> - </div> - </div> - </Shell> - ); - } - - // 데이터 가져오기 - const responsesPromise = getVendorRfqResponses({ - ...search, - filters: validFilters - }, vendorId); - - // 상태별 개수 및 진행률 가져오기 - const [statusCounts, progress] = await Promise.all([ - getVendorResponseStatusCounts(vendorId), - getVendorResponseProgress(vendorId) - ]); - - // 프로미스 배열 - const promises = Promise.all([responsesPromise]); - - return ( - <Shell className="gap-6"> - <div className="flex justify-between items-center"> - <div> - <h2 className="text-2xl font-bold tracking-tight">RFQ 응답 관리</h2> - <p className="text-muted-foreground"> - RFQ 첨부파일 응답 현황을 확인하고 관리합니다. - </p> - </div> - </div> - - {/* 상태별 통계 카드 */} - <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5"> - <Card> - <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> - <CardTitle className="text-sm font-medium">전체 요청</CardTitle> - <FileX className="h-4 w-4 text-muted-foreground" /> - </CardHeader> - <CardContent> - <div className="text-2xl font-bold">{progress.totalRequests}건</div> - <p className="text-xs text-muted-foreground"> - 총 응답 요청 수 - </p> - </CardContent> - </Card> - - <Card> - <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> - <CardTitle className="text-sm font-medium">미응답</CardTitle> - <Clock className="h-4 w-4 text-orange-600" /> - </CardHeader> - <CardContent> - <div className="text-2xl font-bold text-orange-600"> - {statusCounts.NOT_RESPONDED || 0}건 - </div> - <p className="text-xs text-muted-foreground"> - 응답 대기 중 - </p> - </CardContent> - </Card> - - <Card> - <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> - <CardTitle className="text-sm font-medium">응답완료</CardTitle> - <CheckCircle className="h-4 w-4 text-green-600" /> - </CardHeader> - <CardContent> - <div className="text-2xl font-bold text-green-600"> - {statusCounts.RESPONDED || 0}건 - </div> - <p className="text-xs text-muted-foreground"> - 응답률: {progress.responseRate}% - </p> - </CardContent> - </Card> - - <Card> - <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> - <CardTitle className="text-sm font-medium">수정요청</CardTitle> - <AlertTriangle className="h-4 w-4 text-yellow-600" /> - </CardHeader> - <CardContent> - <div className="text-2xl font-bold text-yellow-600"> - {statusCounts.REVISION_REQUESTED || 0}건 - </div> - <p className="text-xs text-muted-foreground"> - 재검토 필요 - </p> - </CardContent> - </Card> - - <Card> - <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> - <CardTitle className="text-sm font-medium">포기</CardTitle> - <FileX className="h-4 w-4 text-gray-600" /> - </CardHeader> - <CardContent> - <div className="text-2xl font-bold text-gray-600"> - {statusCounts.WAIVED || 0}건 - </div> - <p className="text-xs text-muted-foreground"> - 완료율: {progress.completionRate}% - </p> - </CardContent> - </Card> - </div> - - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={8} - searchableColumnCount={2} - filterableColumnCount={3} - cellWidths={["10rem", "12rem", "8rem", "10rem", "10rem", "8rem", "10rem", "8rem"]} - /> - } - > - <VendorResponsesTable promises={promises} /> - </React.Suspense> - </Shell> - ); -}
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/rfq-ship/[id]/page.tsx b/app/[lng]/partners/(partners)/rfq-ship/[id]/page.tsx deleted file mode 100644 index 5b52e4a4..00000000 --- a/app/[lng]/partners/(partners)/rfq-ship/[id]/page.tsx +++ /dev/null @@ -1,81 +0,0 @@ -// app/vendor/quotations/[id]/page.tsx - 견적 응답 페이지 -import { Metadata } from "next" -import { notFound } from "next/navigation" -import db from "@/db/db"; -import { eq } from "drizzle-orm" -import { procurementVendorQuotations } from "@/db/schema" -import { getServerSession } from "next-auth/next" -import { authOptions } from "@/app/api/auth/[...nextauth]/route" -import VendorQuotationEditor from "@/lib/procurement-rfqs/vendor-response/quotation-editor"; - - -interface PageProps { - params: Promise<{ - id: string - }> -} - -export async function generateMetadata(props: PageProps): Promise<Metadata> { - return { - title: "견적서 응답", - description: "RFQ에 대한 견적서 작성 및 제출", - } -} - -export default async function VendorQuotationPage(props: PageProps) { - const params = await props.params - const quotationId = parseInt(params.id) - - if (isNaN(quotationId)) { - notFound() - } - - // 인증 확인 - const session = await getServerSession(authOptions); - - if (!session?.user) { - return ( - <div className="flex h-full items-center justify-center"> - <div className="text-center"> - <h2 className="text-xl font-bold">로그인이 필요합니다</h2> - <p className="mt-2 text-muted-foreground">견적서 응답을 위해 로그인해주세요.</p> - </div> - </div> - ) - } - - // 견적서 정보 가져오기 - const quotation = await db.query.procurementVendorQuotations.findFirst({ - where: eq(procurementVendorQuotations.id, quotationId), - with: { - rfq: true, // 관계 설정 필요 - vendor: true, // 관계 설정 필요 - items: true, // 관계 설정 필요 - } - }) - - if (!quotation) { - notFound() - } - - // 벤더 권한 확인 (필요한 경우) - const isAuthorized = session.user.domain === "partners" && - session.user.companyId === quotation.vendorId - - if (!isAuthorized) { - return ( - <div className="flex h-full items-center justify-center"> - <div className="text-center"> - <h2 className="text-xl font-bold">접근 권한이 없습니다</h2> - <p className="mt-2 text-muted-foreground">이 견적서에 대한 권한이 없습니다.</p> - </div> - </div> - ) - } - - return ( - <div className="container py-8"> - <VendorQuotationEditor quotation={quotation} /> - </div> - ) -}
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/rfq-ship/page.tsx b/app/[lng]/partners/(partners)/rfq-ship/page.tsx deleted file mode 100644 index 332cca2d..00000000 --- a/app/[lng]/partners/(partners)/rfq-ship/page.tsx +++ /dev/null @@ -1,174 +0,0 @@ -// app/vendor/quotations/page.tsx -import * as React from "react"; -import Link from "next/link"; -import { Metadata } from "next"; -import { getServerSession } from "next-auth/next"; -import { authOptions } from "@/app/api/auth/[...nextauth]/route"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { LogIn } from "lucide-react"; -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; -import { Shell } from "@/components/shell"; -import { getValidFilters } from "@/lib/data-table"; -import { type SearchParams } from "@/types/table"; -import { searchParamsVendorRfqCache } from "@/lib/procurement-rfqs/validations"; -import { getQuotationStatusCounts, getVendorQuotations } from "@/lib/procurement-rfqs/services"; -import { VendorQuotationsTable } from "@/lib/procurement-rfqs/vendor-response/table/vendor-quotations-table"; -import { InformationButton } from "@/components/information/information-button" -export const metadata: Metadata = { - title: "견적 목록", - description: "진행 중인 견적서 목록", -}; - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsVendorRfqCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - // 인증 확인 - const session = await getServerSession(authOptions); - - // 로그인 확인 - if (!session || !session.user) { - return ( - <Shell className="gap-6"> - <div className="flex items-center justify-between"> - <div> - <div className="flex items-center gap-2"> - <h2 className="text-2xl font-bold tracking-tight"> - 견적 목록 - </h2> - <InformationButton pagePath="partners/rfq-ship" /> - </div> - {/* <p className="text-muted-foreground"> - 진행 중인 견적서 목록을 확인하고 관리합니다. - </p> */} - </div> - </div> - - <div className="flex flex-col items-center justify-center py-12 text-center"> - <div className="rounded-lg border border-dashed p-10 shadow-sm"> - <h3 className="mb-2 text-xl font-semibold">로그인이 필요합니다</h3> - <p className="mb-6 text-muted-foreground"> - 견적서를 확인하려면 먼저 로그인하세요. - </p> - <Button size="lg" asChild> - <Link href="/partners?callbackUrl=/vendor/quotations"> - <LogIn className="mr-2 h-4 w-4" /> - 로그인하기 - </Link> - </Button> - </div> - </div> - </Shell> - ); - } - - // 벤더 ID 확인 - const vendorId = session.user.companyId ? String(session.user.companyId) : "0"; - - // 벤더 권한 확인 - if (session.user.domain !== "partners") { - return ( - <Shell className="gap-6"> - <div className="flex items-center justify-between"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - 접근 권한 없음 - </h2> - </div> - </div> - <div className="flex flex-col items-center justify-center py-12 text-center"> - <div className="rounded-lg border border-dashed p-10 shadow-sm"> - <h3 className="mb-2 text-xl font-semibold">벤더 계정이 필요합니다</h3> - <p className="mb-6 text-muted-foreground"> - 벤더 계정으로 로그인해주세요. - </p> - </div> - </div> - </Shell> - ); - } - - // 데이터 가져오기 - const quotationsPromise = getVendorQuotations({ - ...search, - filters: validFilters - }, vendorId); - - // 상태별 개수 가져오기 - const statusCountsPromise = getQuotationStatusCounts(vendorId); - - // 모든 프로미스 병렬 실행 - const promises = Promise.all([quotationsPromise]); - const statusCounts = await statusCountsPromise; - - return ( - <Shell className="gap-6"> - <div className="flex justify-between items-center"> - <div> - <h2 className="text-2xl font-bold tracking-tight">견적 목록</h2> - <p className="text-muted-foreground"> - 진행 중인 견적서 목록을 확인하고 관리합니다. - </p> - </div> - </div> - - <div className="grid gap-4 md:grid-cols-4"> - <Card> - <CardHeader className="py-4"> - <CardTitle className="text-base">전체 견적</CardTitle> - </CardHeader> - <CardContent> - <div className="text-2xl font-bold"> - {Object.values(statusCounts).reduce((sum, count) => sum + count, 0)}건 - </div> - </CardContent> - </Card> - <Card> - <CardHeader className="py-4"> - <CardTitle className="text-base">작성 중</CardTitle> - </CardHeader> - <CardContent> - <div className="text-2xl font-bold">{statusCounts.Draft || 0}건</div> - </CardContent> - </Card> - <Card> - <CardHeader className="py-4"> - <CardTitle className="text-base">제출됨</CardTitle> - </CardHeader> - <CardContent> - <div className="text-2xl font-bold"> - {(statusCounts.Submitted || 0) + (statusCounts.Revised || 0)}건 - </div> - </CardContent> - </Card> - <Card> - <CardHeader className="py-4"> - <CardTitle className="text-base">승인됨</CardTitle> - </CardHeader> - <CardContent> - <div className="text-2xl font-bold">{statusCounts.Accepted || 0}건</div> - </CardContent> - </Card> - </div> - - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={7} - searchableColumnCount={2} - filterableColumnCount={3} - cellWidths={["10rem", "10rem", "8rem", "10rem", "10rem", "10rem", "8rem"]} - /> - } - > - <VendorQuotationsTable promises={promises} /> - </React.Suspense> - </Shell> - ); -}
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/rfq/page.tsx b/app/[lng]/partners/(partners)/rfq/page.tsx deleted file mode 100644 index 5cdb1dde..00000000 --- a/app/[lng]/partners/(partners)/rfq/page.tsx +++ /dev/null @@ -1,136 +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 { searchParamsRfqsForVendorsCache } from "@/lib/rfqs/validations" -import { RfqsVendorTable } from "@/lib/vendor-rfq-response/vendor-rfq-table/rfqs-table" -import { getServerSession } from "next-auth" -import { authOptions } from "@/app/api/auth/[...nextauth]/route" -import Link from "next/link" -import { Button } from "@/components/ui/button" -import { LogIn } from "lucide-react" -import { getRfqResponsesForVendor } from "@/lib/vendor-rfq-response/service" -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 = searchParamsRfqsForVendorsCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - // Get session - const session = await getServerSession(authOptions) - - // Check if user is logged in - if (!session || !session.user) { - // Return login required UI instead of redirecting - return ( - <Shell className="gap-6"> - <div className="flex items-center justify-between"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - RFQ - </h2> - {/* <p className="text-muted-foreground"> - RFQ를 응답하고 커뮤니케이션을 할 수 있습니다. - </p> */} - </div> - </div> - - <div className="flex flex-col items-center justify-center py-12 text-center"> - <div className="rounded-lg border border-dashed p-10 shadow-sm"> - <h3 className="mb-2 text-xl font-semibold">로그인이 필요합니다</h3> - <p className="mb-6 text-muted-foreground"> - RFQ를 확인하려면 먼저 로그인하세요. - </p> - <Button size="lg" asChild> - <Link href="/partners"> - <LogIn className="mr-2 h-4 w-4" /> - 로그인하기 - </Link> - </Button> - </div> - </div> - </Shell> - ) - } - - // User is logged in, proceed with vendor ID - const vendorId = session.user.companyId - - // Validate vendorId (should be a number) - const idAsNumber = Number(vendorId) - - if (isNaN(idAsNumber)) { - // Handle invalid vendor ID (this shouldn't happen if authentication is working properly) - return ( - <Shell className="gap-6"> - <div className="flex items-center justify-between"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - RFQ - </h2> - </div> - </div> - <div className="flex flex-col items-center justify-center py-12 text-center"> - <div className="rounded-lg border border-dashed p-10 shadow-sm"> - <h3 className="mb-2 text-xl font-semibold">계정 오류</h3> - <p className="mb-6 text-muted-foreground"> - 업체 정보가 올바르게 설정되지 않았습니다. 관리자에게 문의하세요. - </p> - </div> - </div> - </Shell> - ) - } - - // If we got here, we have a valid vendor ID - const promises = Promise.all([ - getRfqResponsesForVendor({ - ...search, - filters: validFilters, - }, idAsNumber) - ]) - - 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"> - RFQ - </h2> - <InformationButton pagePath="partners/rfq" /> - </div> - {/* <p className="text-muted-foreground"> - RFQ를 응답하고 커뮤니케이션을 할 수 있습니다. - </p> */} - </div> - </div> - </div> - - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - {/* DateRangePicker can go here */} - </React.Suspense> - - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <RfqsVendorTable promises={promises} /> - </React.Suspense> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/tbe/page.tsx b/app/[lng]/partners/(partners)/tbe/page.tsx deleted file mode 100644 index 38c24624..00000000 --- a/app/[lng]/partners/(partners)/tbe/page.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getTBEforVendor } from "@/lib/rfqs/service" -import { searchParamsTBECache } from "@/lib/rfqs/validations" -import { getServerSession } from "next-auth" -import { authOptions } from "@/app/api/auth/[...nextauth]/route" -import { TbeVendorTable } from "@/lib/vendor-rfq-response/vendor-tbe-table/tbe-table" -import * as React from "react" -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" -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 - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsTBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const session = await getServerSession(authOptions) - const vendorId = session?.user.companyId - // const vendorId = "17" - - const idAsNumber = Number(vendorId) - - const promises = Promise.all([ - getTBEforVendor({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - - 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"> - TBE 관리 - </h2> - <InformationButton pagePath="partners/tbe" /> - </div> - {/* <p className="text-sm text-muted-foreground"> - TBE에 응답하고 커뮤니케이션을 할 수 있습니다.{" "} - </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 - /> - } - > - <TbeVendorTable promises={promises} /> - </React.Suspense> - </Shell> - ) -} diff --git a/app/[lng]/procurement/(procurement)/b-rfq/[id]/final/page.tsx b/app/[lng]/procurement/(procurement)/b-rfq/[id]/final/page.tsx deleted file mode 100644 index e69de29b..00000000 --- a/app/[lng]/procurement/(procurement)/b-rfq/[id]/final/page.tsx +++ /dev/null diff --git a/app/[lng]/procurement/(procurement)/b-rfq/[id]/initial/page.tsx b/app/[lng]/procurement/(procurement)/b-rfq/[id]/initial/page.tsx deleted file mode 100644 index 1af65fbc..00000000 --- a/app/[lng]/procurement/(procurement)/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]/procurement/(procurement)/b-rfq/[id]/layout.tsx b/app/[lng]/procurement/(procurement)/b-rfq/[id]/layout.tsx deleted file mode 100644 index d6836437..00000000 --- a/app/[lng]/procurement/(procurement)/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]/procurement/(procurement)/b-rfq/[id]/page.tsx b/app/[lng]/procurement/(procurement)/b-rfq/[id]/page.tsx deleted file mode 100644 index 26dc45fb..00000000 --- a/app/[lng]/procurement/(procurement)/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]/procurement/(procurement)/b-rfq/page.tsx b/app/[lng]/procurement/(procurement)/b-rfq/page.tsx deleted file mode 100644 index a66d7b58..00000000 --- a/app/[lng]/procurement/(procurement)/b-rfq/page.tsx +++ /dev/null @@ -1,79 +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" - -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> - <h2 className="text-2xl font-bold tracking-tight"> - 견적 RFQ - </h2> - </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]/procurement/(procurement)/basic-contract-template/page.tsx b/app/[lng]/procurement/(procurement)/basic-contract-template/page.tsx deleted file mode 100644 index 26108323..00000000 --- a/app/[lng]/procurement/(procurement)/basic-contract-template/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { getBasicContractTemplates } from "@/lib/basic-contract/service" -import { searchParamsTemplatesCache } from "@/lib/basic-contract/validations" -import { BasicContractTemplateTable } from "@/lib/basic-contract/template/basic-contract-template" - - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsTemplatesCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getBasicContractTemplates({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - 기본 계약문서 관리 - </h2> - {/* <p className="text-muted-foreground"> - 기본계약서를 비롯하여 초기 서명이 필요한 문서를 등록하고 편집할 수 있습니다. 활성화된 템플릿이 서명 요청의 리스트에 나타나게 됩니다..{" "} - <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 - /> - } - > - <BasicContractTemplateTable promises={promises} /> - </React.Suspense> - </Shell> - ) -} diff --git a/app/[lng]/procurement/(procurement)/basic-contract/page.tsx b/app/[lng]/procurement/(procurement)/basic-contract/page.tsx deleted file mode 100644 index 19211d4e..00000000 --- a/app/[lng]/procurement/(procurement)/basic-contract/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { getBasicContracts } from "@/lib/basic-contract/service" -import { searchParamsCache } from "@/lib/basic-contract/validations" -import { BasicContractsTable } from "@/lib/basic-contract/status/basic-contract-table" - - -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([ - getBasicContracts({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - 기본계약서 서명 현황 - </h2> - {/* <p className="text-muted-foreground"> - 기본계약서를 비롯하여 초기 서명이 필요한 문서의 서명 현황을 확인할 수 있고 서명된 문서들을 다운로드할 수 있습니다. {" "} - <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 - /> - } - > - <BasicContractsTable promises={promises} /> - </React.Suspense> - </Shell> - ) -} diff --git a/app/[lng]/procurement/(procurement)/bqcbe/page.tsx b/app/[lng]/procurement/(procurement)/bqcbe/page.tsx deleted file mode 100644 index 831bb5a8..00000000 --- a/app/[lng]/procurement/(procurement)/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]/procurement/(procurement)/bqtbe/page.tsx b/app/[lng]/procurement/(procurement)/bqtbe/page.tsx deleted file mode 100644 index 3e56cfaa..00000000 --- a/app/[lng]/procurement/(procurement)/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]/procurement/(procurement)/budgetary-rfq/[id]/cbe/page.tsx b/app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/cbe/page.tsx deleted file mode 100644 index 956facd3..00000000 --- a/app/[lng]/procurement/(procurement)/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]/procurement/(procurement)/budgetary-rfq/[id]/layout.tsx b/app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/layout.tsx deleted file mode 100644 index 2b80e64f..00000000 --- a/app/[lng]/procurement/(procurement)/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]/procurement/(procurement)/budgetary-rfq/[id]/page.tsx b/app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/page.tsx deleted file mode 100644 index dd9df563..00000000 --- a/app/[lng]/procurement/(procurement)/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]/procurement/(procurement)/budgetary-rfq/[id]/tbe/page.tsx b/app/[lng]/procurement/(procurement)/budgetary-rfq/[id]/tbe/page.tsx deleted file mode 100644 index ec894e1c..00000000 --- a/app/[lng]/procurement/(procurement)/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]/procurement/(procurement)/budgetary-rfq/page.tsx b/app/[lng]/procurement/(procurement)/budgetary-rfq/page.tsx deleted file mode 100644 index f342bbff..00000000 --- a/app/[lng]/procurement/(procurement)/budgetary-rfq/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.PURCHASE_BUDGETARY, - title = "Budgetary Quote", - description = "Budgetary Quote를 등록하여 요청 및 응답을 관리할 수 있습니다." -}: RfqPageProps) { - const search = searchParamsCache.parse(await searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getRfqs({ - ...search, - filters: validFilters, - rfqType // 전달받은 rfqType 사용 - }), - getRfqStatusCounts(rfqType), // rfqType 전달 - getAllItems() - ]) - - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - {title} - </h2> - {/* <p className="text-muted-foreground"> - {description} - 기본적인 정보와 RFQ를 위한 아이템 등록 및 첨부를 한 후, - <span className="inline-flex items-center whitespace-nowrap"> - <Ellipsis className="size-3" /> - <span className="ml-1">버튼</span> - </span> 을 클릭하면 "Proceed"를 통해 상세화면으로 이동하여 진행할 수 있습니다. - </p> */} - </div> - </div> - </div> - - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - {/* <DateRangePicker - triggerSize="sm" - triggerClassName="ml-auto w-56 sm:w-60" - align="end" - shallow={false} - /> */} - </React.Suspense> - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <RfqsTable promises={promises} rfqType={rfqType} /> - </React.Suspense> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/budgetary/[id]/cbe/page.tsx b/app/[lng]/procurement/(procurement)/budgetary/[id]/cbe/page.tsx deleted file mode 100644 index 956facd3..00000000 --- a/app/[lng]/procurement/(procurement)/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]/procurement/(procurement)/budgetary/[id]/layout.tsx b/app/[lng]/procurement/(procurement)/budgetary/[id]/layout.tsx deleted file mode 100644 index d58d8363..00000000 --- a/app/[lng]/procurement/(procurement)/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]/procurement/(procurement)/budgetary/[id]/page.tsx b/app/[lng]/procurement/(procurement)/budgetary/[id]/page.tsx deleted file mode 100644 index dd9df563..00000000 --- a/app/[lng]/procurement/(procurement)/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]/procurement/(procurement)/budgetary/[id]/tbe/page.tsx b/app/[lng]/procurement/(procurement)/budgetary/[id]/tbe/page.tsx deleted file mode 100644 index ec894e1c..00000000 --- a/app/[lng]/procurement/(procurement)/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]/procurement/(procurement)/budgetary/page.tsx b/app/[lng]/procurement/(procurement)/budgetary/page.tsx deleted file mode 100644 index 15b4cdd4..00000000 --- a/app/[lng]/procurement/(procurement)/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]/procurement/(procurement)/dashboard/page.tsx b/app/[lng]/procurement/(procurement)/dashboard/page.tsx deleted file mode 100644 index 1d61dc16..00000000 --- a/app/[lng]/procurement/(procurement)/dashboard/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -// app/invalid-access/page.tsx - -export default function InvalidAccessPage() { - return ( - <main style={{ padding: '40px', textAlign: 'center' }}> - <h1>부적절한 접근입니다</h1> - <p> - 협력업체(Vendor)가 EVCP 화면에 접속하거나 <br /> - SHI 계정이 협력업체 화면에 접속하려고 시도하는 경우입니다. - </p> - <p> - <strong>접근 권한이 없으므로, 다른 화면으로 이동해 주세요.</strong> - </p> - </main> - ); - } -
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/equip-class/page.tsx b/app/[lng]/procurement/(procurement)/equip-class/page.tsx deleted file mode 100644 index 34fd32b6..00000000 --- a/app/[lng]/procurement/(procurement)/equip-class/page.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { searchParamsCache } from "@/lib/equip-class/validation" -import { FormListsTable } from "@/lib/form-list/table/formLists-table" -import { getTagClassists } from "@/lib/equip-class/service" -import { EquipClassTable } from "@/lib/equip-class/table/equipClass-table" - - -interface IndexPageProps { - searchParams: Promise<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([ - getTagClassists({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - 객체 클래스 목록 from S-EDP - </h2> - {/* <p className="text-muted-foreground"> - 객체 클래스 목록을 확인할 수 있습니다.{" "} - <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 - /> - } - > - <EquipClassTable promises={promises} /> - </React.Suspense> - </Shell> - ) -} diff --git a/app/[lng]/procurement/(procurement)/esg-check-list/page.tsx b/app/[lng]/procurement/(procurement)/esg-check-list/page.tsx deleted file mode 100644 index 8bccd3b7..00000000 --- a/app/[lng]/procurement/(procurement)/esg-check-list/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { getEsgEvaluations } from "@/lib/esg-check-list/service" -import { getEsgEvaluationsSchema } from "@/lib/esg-check-list/validation" -import { EsgEvaluationsTable } from "@/lib/esg-check-list/table/esg-table" - - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = getEsgEvaluationsSchema.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getEsgEvaluations({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - ESG 자가진단평가서 항목 관리 - </h2> - {/* <p className="text-muted-foreground"> - 협력업체 평가에 사용되는 ESG 자가진단표를 관리{" "} - <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 - /> - } - > - <EsgEvaluationsTable promises={promises} /> - </React.Suspense> - </Shell> - ) -} diff --git a/app/[lng]/procurement/(procurement)/evaluation-check-list/page.tsx b/app/[lng]/procurement/(procurement)/evaluation-check-list/page.tsx deleted file mode 100644 index 45da961b..00000000 --- a/app/[lng]/procurement/(procurement)/evaluation-check-list/page.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/* IMPORT */
-import { DataTableSkeleton } from '@/components/data-table/data-table-skeleton';
-import { getRegEvalCriteria } from '@/lib/evaluation-criteria/service';
-import { getValidFilters } from '@/lib/data-table';
-import RegEvalCriteriaTable from '@/lib/evaluation-criteria/table/reg-eval-criteria-table';
-import { searchParamsCache } from '@/lib/evaluation-criteria/validations';
-import { Shell } from '@/components/shell';
-import { Skeleton } from '@/components/ui/skeleton';
-import { Suspense } from 'react';
-import { type SearchParams } from '@/types/table';
-
-// ----------------------------------------------------------------------------------------------------
-
-/* TYPES */
-interface EvaluationCriteriaPageProps {
- searchParams: Promise<SearchParams>
-}
-
-// ----------------------------------------------------------------------------------------------------
-
-/* REGULAR EVALUATION CRITERIA PAGE */
-async function EvaluationCriteriaPage(props: EvaluationCriteriaPageProps) {
- const searchParams = await props.searchParams;
- const search = searchParamsCache.parse(searchParams);
- const validFilters = getValidFilters(search.filters);
- const promises = Promise.all([
- getRegEvalCriteria({
- ...search,
- filters: validFilters,
- }),
- ]);
-
- return (
- <Shell className="gap-2">
- <div className="flex items-center justify-between space-y-2">
- <div className="flex items-center justify-between space-y-2">
- <div>
- <h2 className="text-2xl font-bold tracking-tight">
- 협력업체 평가기준표 관리
- </h2>
- {/* <p className="text-muted-foreground">
- 협력업체 평가에 사용되는 평가기준표를 관리{" "}
- <span className="inline-flex items-center whitespace-nowrap">
- <Ellipsis className="size-3" />
- <span className="ml-1">버튼</span>
- </span>
- 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다.
- </p> */}
- </div>
- </div>
- </div>
-
- <Suspense fallback={<Skeleton className="h-7 w-52" />}>
- {/* <DateRangePicker
- triggerSize="sm"
- triggerClassName="ml-auto w-56 sm:w-60"
- align="end"
- shallow={false}
- /> */}
- </Suspense>
- <Suspense
- fallback={
- <DataTableSkeleton
- columnCount={11}
- searchableColumnCount={1}
- filterableColumnCount={2}
- cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]}
- shrinkZero
- />
- }
- >
- <RegEvalCriteriaTable promises={promises} />
- </Suspense>
- </Shell>
- )
-}
-
-// ----------------------------------------------------------------------------------------------------
-
-/* EXPORT */
-export default EvaluationCriteriaPage;
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/evaluation-input/[id]/page.tsx b/app/[lng]/procurement/(procurement)/evaluation-input/[id]/page.tsx deleted file mode 100644 index 3a403620..00000000 --- a/app/[lng]/procurement/(procurement)/evaluation-input/[id]/page.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { EvaluationPage } from "@/lib/evaluation-submit/evaluation-page" -import { Metadata } from "next" - -export const metadata: Metadata = { - title: "평가 작성", - description: "협력업체 평가를 작성합니다", -} - -interface PageProps { - params: { - id: string - } -} - -export default function Page({ params }: PageProps) { - return <EvaluationPage /> -} - -export async function generateStaticParams() { - // 동적 경로이므로 빈 배열 반환 - return [] -}
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/evaluation-input/page.tsx b/app/[lng]/procurement/(procurement)/evaluation-input/page.tsx deleted file mode 100644 index 00f1820f..00000000 --- a/app/[lng]/procurement/(procurement)/evaluation-input/page.tsx +++ /dev/null @@ -1,135 +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 { getServerSession } from "next-auth" -import { authOptions } from "@/app/api/auth/[...nextauth]/route" -import Link from "next/link" -import { Button } from "@/components/ui/button" -import { LogIn } from "lucide-react" -import { getSHIEvaluationSubmissions } from "@/lib/evaluation-submit/service" -import { getSHIEvaluationsSubmitSchema } from "@/lib/evaluation-submit/validation" -import { SHIEvaluationSubmissionsTable } from "@/lib/evaluation-submit/table/submit-table" - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = getSHIEvaluationsSubmitSchema.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - // Get session - const session = await getServerSession(authOptions) - - // Check if user is logged in - if (!session || !session.user) { - // Return login required UI instead of redirecting - return ( - <Shell className="gap-6"> - <div className="flex items-center justify-between"> - <div> - <div className="flex items-center gap-2"> - <h2 className="text-2xl font-bold tracking-tight"> - 정기평가 - </h2> - </div> - {/* <p className="text-muted-foreground"> - 요청된 정기평가를 입력하고 제출할 수 있습니다. - </p> */} - </div> - </div> - - <div className="flex flex-col items-center justify-center py-12 text-center"> - <div className="rounded-lg border border-dashed p-10 shadow-sm"> - <h3 className="mb-2 text-xl font-semibold">로그인이 필요합니다</h3> - <p className="mb-6 text-muted-foreground"> - 정기평가를 확인하려면 먼저 로그인하세요. - </p> - <Button size="lg" asChild> - <Link href="/partners"> - <LogIn className="mr-2 h-4 w-4" /> - 로그인하기 - </Link> - </Button> - </div> - </div> - </Shell> - ) - } - - const userId = session.user.id - - // Validate vendorId (should be a number) - const idAsNumber = Number(userId) - - - if (isNaN(idAsNumber)) { - // Handle invalid vendor ID (this shouldn't happen if authentication is working properly) - return ( - <Shell className="gap-6"> - <div className="flex items-center justify-between"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - 정기평가 - </h2> - </div> - </div> - <div className="flex flex-col items-center justify-center py-12 text-center"> - <div className="rounded-lg border border-dashed p-10 shadow-sm"> - <h3 className="mb-2 text-xl font-semibold">계정 오류</h3> - <p className="mb-6 text-muted-foreground"> - 관리자에게 문의하세요. - </p> - </div> - </div> - </Shell> - ) - } - - // If we got here, we have a valid vendor ID - const promises = Promise.all([ - getSHIEvaluationSubmissions({ - ...search, - filters: validFilters, - }, idAsNumber) - ]) - - 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"> - 정기평가 - </h2> - {/* <p className="text-muted-foreground"> - 요청된 정기평가를 입력하고 제출할 수 있습니다. - </p> */} - </div> - </div> - </div> - - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - {/* DateRangePicker can go here */} - </React.Suspense> - - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <SHIEvaluationSubmissionsTable promises={promises} /> - </React.Suspense> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/evaluation-target-list/page.tsx b/app/[lng]/procurement/(procurement)/evaluation-target-list/page.tsx deleted file mode 100644 index a0523eea..00000000 --- a/app/[lng]/procurement/(procurement)/evaluation-target-list/page.tsx +++ /dev/null @@ -1,118 +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 { HelpCircle } from "lucide-react" -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover" -import { Button } from "@/components/ui/button" -import { Badge } from "@/components/ui/badge" - -import { getDefaultEvaluationYear, searchParamsEvaluationTargetsCache } from "@/lib/evaluation-target-list/validation" -import { getEvaluationTargets } from "@/lib/evaluation-target-list/service" -import { EvaluationTargetsTable } from "@/lib/evaluation-target-list/table/evaluation-target-table" -import { InformationButton } from "@/components/information/information-button" -export const metadata: Metadata = { - title: "협력업체 평가 대상 관리", - description: "협력업체 평가 대상을 확정하고 담당자를 지정합니다.", -} - -interface EvaluationTargetsPageProps { - searchParams: Promise<SearchParams> -} - - - -export default async function EvaluationTargetsPage(props: EvaluationTargetsPageProps) { - const searchParams = await props.searchParams - const search = searchParamsEvaluationTargetsCache.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'; - - // 현재 평가년도 (필터에서 가져오거나 기본값 사용) - const currentEvaluationYear = search.evaluationYear || getDefaultEvaluationYear() - - // Promise.all로 감싸서 전달 - const promises = Promise.all([ - getEvaluationTargets({ - ...search, - filters: allFilters, - joinOperator, - }) - ]) - - 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 className="flex items-center gap-2"> - <div className="flex items-center gap-2"> - <h2 className="text-2xl font-bold tracking-tight"> - 협력업체 평가 대상 관리 - </h2> - <InformationButton pagePath="evcp/evaluation-target-list" /> - </div> - <Badge variant="outline" className="text-sm"> - {currentEvaluationYear}년도 - </Badge> - - </div> - </div> - </div> - - {/* 메인 테이블 (통계는 테이블 내부로 이동) */} - <React.Suspense - key={JSON.stringify(searchParams)} // URL 파라미터가 변경될 때마다 강제 리렌더링 - fallback={ - <DataTableSkeleton - columnCount={12} - searchableColumnCount={2} - filterableColumnCount={6} - cellWidths={[ - "3rem", // checkbox - "5rem", // 평가년도 - "4rem", // 구분 - "8rem", // 벤더코드 - "12rem", // 벤더명 - "4rem", // 내외자 - "6rem", // 자재구분 - "5rem", // 상태 - "5rem", // 의견일치 - "8rem", // 담당자현황 - "10rem", // 관리자의견 - "8rem" // actions - ]} - shrinkZero - /> - } - > - {currentEvaluationYear && - <EvaluationTargetsTable - promises={promises} - evaluationYear={currentEvaluationYear} - /> -} - </React.Suspense> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/evaluation/page.tsx b/app/[lng]/procurement/(procurement)/evaluation/page.tsx deleted file mode 100644 index 2d8cbed7..00000000 --- a/app/[lng]/procurement/(procurement)/evaluation/page.tsx +++ /dev/null @@ -1,181 +0,0 @@ -// ================================================================ -// 4. PERIODIC EVALUATIONS PAGE -// ================================================================ - -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 { HelpCircle } from "lucide-react" -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover" -import { Button } from "@/components/ui/button" -import { Badge } from "@/components/ui/badge" -import { PeriodicEvaluationsTable } from "@/lib/evaluation/table/evaluation-table" -import { getPeriodicEvaluations } from "@/lib/evaluation/service" -import { searchParamsEvaluationsCache } from "@/lib/evaluation/validation" - -export const metadata: Metadata = { - title: "협력업체 정기평가", - description: "협력업체 정기평가 진행 현황을 관리합니다.", -} - -interface PeriodicEvaluationsPageProps { - searchParams: Promise<SearchParams> -} - -// 프로세스 안내 팝오버 컴포넌트 -function ProcessGuidePopover() { - return ( - <Popover> - <PopoverTrigger asChild> - <Button variant="ghost" size="icon" className="h-6 w-6"> - <HelpCircle className="h-4 w-4 text-muted-foreground" /> - </Button> - </PopoverTrigger> - <PopoverContent className="w-96" align="start"> - <div className="space-y-3"> - <div className="space-y-1"> - <h4 className="font-medium">정기평가 프로세스</h4> - {/* <p className="text-sm text-muted-foreground"> - 확정된 평가 대상 업체들에 대한 정기평가 절차입니다. - </p> */} - </div> - <div className="space-y-3 text-sm"> - <div className="flex gap-3"> - <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600"> - 1 - </div> - <div> - <p className="font-medium">평가 대상 확정</p> - <p className="text-muted-foreground">평가 대상으로 확정된 업체들의 정기평가가 자동 생성됩니다.</p> - </div> - </div> - <div className="flex gap-3"> - <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600"> - 2 - </div> - <div> - <p className="font-medium">업체 자료 제출</p> - <p className="text-muted-foreground">각 업체는 평가에 필요한 자료를 제출 마감일까지 제출해야 합니다.</p> - </div> - </div> - <div className="flex gap-3"> - <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600"> - 3 - </div> - <div> - <p className="font-medium">평가자 검토</p> - <p className="text-muted-foreground">지정된 평가자들이 평가표를 기반으로 점수를 매기고 검토합니다.</p> - </div> - </div> - <div className="flex gap-3"> - <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600"> - 4 - </div> - <div> - <p className="font-medium">최종 확정</p> - <p className="text-muted-foreground">모든 평가가 완료되면 최종 점수와 등급이 확정됩니다.</p> - </div> - </div> - </div> - </div> - </PopoverContent> - </Popover> - ) -} - -// TODO: 이 함수들은 실제 서비스 파일에서 구현해야 함 -function getDefaultEvaluationYear() { - return new Date().getFullYear() -} - - - -export default async function PeriodicEvaluationsPage(props: PeriodicEvaluationsPageProps) { - const searchParams = await props.searchParams - const search = searchParamsEvaluationsCache.parse(searchParams) - const validFilters = getValidFilters(search.filters || []) - - // 기본 필터 처리 - let basicFilters = [] - if (search.basicFilters && search.basicFilters.length > 0) { - basicFilters = search.basicFilters - } - - // 모든 필터를 합쳐서 처리 - const allFilters = [...validFilters, ...basicFilters] - - // 조인 연산자 - const joinOperator = search.basicJoinOperator || search.joinOperator || 'and'; - - // 현재 평가년도 - const currentEvaluationYear = search.evaluationYear || getDefaultEvaluationYear() - - // Promise.all로 감싸서 전달 - const promises = Promise.all([ - getPeriodicEvaluations({ - ...search, - filters: allFilters, - joinOperator, - }) - ]) - - 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 className="flex items-center gap-2"> - <h2 className="text-2xl font-bold tracking-tight"> - 협력업체 정기평가 - </h2> - <Badge variant="outline" className="text-sm"> - {currentEvaluationYear}년도 - </Badge> - </div> - </div> - </div> - - {/* 메인 테이블 */} - <React.Suspense - key={JSON.stringify(searchParams)} - fallback={ - <DataTableSkeleton - columnCount={15} - searchableColumnCount={2} - filterableColumnCount={8} - cellWidths={[ - "3rem", // checkbox - "5rem", // 평가년도 - "5rem", // 평가기간 - "4rem", // 구분 - "8rem", // 벤더코드 - "12rem", // 벤더명 - "4rem", // 내외자 - "6rem", // 자재구분 - "5rem", // 문서제출 - "4rem", // 제출일 - "4rem", // 마감일 - "4rem", // 총점 - "4rem", // 등급 - "5rem", // 진행상태 - "8rem" // actions - ]} - shrinkZero - /> - } - > - <PeriodicEvaluationsTable - promises={promises} - evaluationYear={currentEvaluationYear} - /> - </React.Suspense> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/faq/manage/actions.ts b/app/[lng]/procurement/(procurement)/faq/manage/actions.ts deleted file mode 100644 index bc443a8a..00000000 --- a/app/[lng]/procurement/(procurement)/faq/manage/actions.ts +++ /dev/null @@ -1,48 +0,0 @@ -'use server';
-
-import { promises as fs } from 'fs';
-import path from 'path';
-import { FaqCategory } from '@/components/faq/FaqCard';
-import { fallbackLng } from '@/i18n/settings';
-
-const FAQ_CONFIG_PATH = path.join(process.cwd(), 'config', 'faqDataConfig.ts');
-
-export async function updateFaqData(lng: string, newData: FaqCategory[]) {
- try {
- const fileContent = await fs.readFile(FAQ_CONFIG_PATH, 'utf-8');
- const dataMatch = fileContent.match(/export const faqCategories[^=]*=\s*(\{[\s\S]*\});/);
- if (!dataMatch) {
- throw new Error('FAQ 데이터 형식이 올바르지 않습니다.');
- }
-
- const allData = eval(`(${dataMatch[1]})`);
- const updatedData = {
- ...allData,
- [lng]: newData
- };
-
- const newFileContent = `import { FaqCategory } from "@/components/faq/FaqCard";\n\ninterface LocalizedFaqCategories {\n [lng: string]: FaqCategory[];\n}\n\nexport const faqCategories: LocalizedFaqCategories = ${JSON.stringify(updatedData, null, 4)};`;
- await fs.writeFile(FAQ_CONFIG_PATH, newFileContent, 'utf-8');
-
- return { success: true };
- } catch (error) {
- console.error('FAQ 데이터 업데이트 중 오류 발생:', error);
- return { success: false, error: '데이터 업데이트 중 오류가 발생했습니다.' };
- }
-}
-
-export async function getFaqData(lng: string): Promise<{ data: FaqCategory[] }> {
- try {
- const fileContent = await fs.readFile(FAQ_CONFIG_PATH, 'utf-8');
- const dataMatch = fileContent.match(/export const faqCategories[^=]*=\s*(\{[\s\S]*\});/);
- if (!dataMatch) {
- throw new Error('FAQ 데이터 형식이 올바르지 않습니다.');
- }
-
- const allData = eval(`(${dataMatch[1]})`);
- return { data: allData[lng] || allData[fallbackLng] || [] };
- } catch (error) {
- console.error('FAQ 데이터 읽기 중 오류 발생:', error);
- return { data: [] };
- }
-}
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/faq/manage/page.tsx b/app/[lng]/procurement/(procurement)/faq/manage/page.tsx deleted file mode 100644 index 011bbfa4..00000000 --- a/app/[lng]/procurement/(procurement)/faq/manage/page.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { FaqManager } from '@/components/faq/FaqManager';
-import { getFaqData, updateFaqData } from './actions';
-import { revalidatePath } from 'next/cache';
-import { FaqCategory } from '@/components/faq/FaqCard';
-
-interface Props {
- params: {
- lng: string;
- }
-}
-
-export default async function FaqManagePage(props: Props) {
- const resolvedParams = await props.params
- const lng = resolvedParams.lng
- const { data } = await getFaqData(lng);
-
- async function handleSave(newData: FaqCategory[]) {
- 'use server';
- await updateFaqData(lng, newData);
- revalidatePath(`/${lng}/evcp/faq`);
- }
-
- return (
- <div className="container py-6">
- <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow">
- <div className="space-y-6 p-10 pb-16">
- <div className="space-y-0.5">
- <h2 className="text-2xl font-bold tracking-tight">FAQ Management</h2>
- <p className="text-muted-foreground">
- Manage FAQ categories and items for {lng.toUpperCase()} language.
- </p>
- </div>
- <FaqManager initialData={data} onSave={handleSave} lng={lng} />
- </div>
- </section>
- </div>
- );
-}
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/faq/page.tsx b/app/[lng]/procurement/(procurement)/faq/page.tsx deleted file mode 100644 index 00956591..00000000 --- a/app/[lng]/procurement/(procurement)/faq/page.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Separator } from "@/components/ui/separator"
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
-import { faqCategories } from "@/config/faqDataConfig"
-import { FaqCard } from "@/components/faq/FaqCard"
-import { Button } from "@/components/ui/button"
-import { Settings } from "lucide-react"
-import Link from "next/link"
-import { fallbackLng } from "@/i18n/settings"
-
-interface Props {
- params: {
- lng: string;
- }
-}
-
-export default async function FaqPage(props: Props) {
- const resolvedParams = await props.params
- const lng = resolvedParams.lng
- const localizedFaqCategories = faqCategories[lng] || faqCategories[fallbackLng];
-
- return (
- <div className="container py-6">
- <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow">
- <div className="space-y-6 p-10 pb-16">
- <div className="flex justify-between items-center">
- <div className="space-y-0.5">
- <h2 className="text-2xl font-bold tracking-tight">FAQ</h2>
- {/* <p className="text-muted-foreground">
- Find answers to common questions about using the EVCP system.
- </p> */}
- </div>
- <Link href={`/${lng}/evcp/faq/manage`}>
- <Button variant="outline">
- <Settings className="w-4 h-4 mr-2" />
- FAQ 관리
- </Button>
- </Link>
- </div>
- <Separator className="my-6" />
-
- <Tabs defaultValue={localizedFaqCategories[0]?.label} className="space-y-4">
- <TabsList>
- {localizedFaqCategories.map((category) => (
- <TabsTrigger key={category.label} value={category.label}>
- {category.label}
- </TabsTrigger>
- ))}
- </TabsList>
-
- {localizedFaqCategories.map((category) => (
- <TabsContent key={category.label} value={category.label} className="space-y-4">
- {category.items.map((item, index) => (
- <FaqCard key={index} item={item} />
- ))}
- </TabsContent>
- ))}
- </Tabs>
- </div>
- </section>
- </div>
- )
-}
diff --git a/app/[lng]/procurement/(procurement)/incoterms/page.tsx b/app/[lng]/procurement/(procurement)/incoterms/page.tsx deleted file mode 100644 index 804bc5af..00000000 --- a/app/[lng]/procurement/(procurement)/incoterms/page.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import * as React from "react"; -import { type SearchParams } from "@/types/table"; -import { getValidFilters } from "@/lib/data-table"; -import { Shell } from "@/components/shell"; -import { Skeleton } from "@/components/ui/skeleton"; -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; -import { SearchParamsCache } from "@/lib/incoterms/validations"; -import { getIncoterms } from "@/lib/incoterms/service"; -import { IncotermsTable } from "@/lib/incoterms/table/incoterms-table"; - -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([ - getIncoterms({ - ...search, - filters: validFilters, - }), - ]); - - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <h2 className="text-2xl font-bold tracking-tight">인코텀즈 관리</h2> - {/* <p className="text-muted-foreground"> - 인코텀즈(Incoterms)를 등록, 수정, 삭제할 수 있습니다. - </p> */} - </div> - </div> - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}></React.Suspense> - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={4} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "8rem"]} - shrinkZero - /> - } - > - <IncotermsTable promises={promises} /> - </React.Suspense> - </Shell> - ); -}
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/items-tech/layout.tsx b/app/[lng]/procurement/(procurement)/items-tech/layout.tsx deleted file mode 100644 index d375059b..00000000 --- a/app/[lng]/procurement/(procurement)/items-tech/layout.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import * as React from "react"
-import { ItemTechContainer } from "@/components/items-tech/item-tech-container"
-import { Shell } from "@/components/shell"
-import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"
-
-// Layout 컴포넌트는 서버 컴포넌트입니다
-export default function ItemsShipLayout({
- children,
-}: {
- children: React.ReactNode
-}) {
- // 아이템 타입 정의
- const itemTypes = [
- { id: "ship", name: "조선 아이템" },
- { id: "top", name: "해양 TOP" },
- { id: "hull", name: "해양 HULL" },
- ]
-
- return (
- <Shell className="gap-4">
- <React.Suspense
- fallback={
- <DataTableSkeleton
- columnCount={6}
- searchableColumnCount={1}
- filterableColumnCount={2}
- cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]}
- shrinkZero
- />
- }
- >
- <ItemTechContainer itemTypes={itemTypes}>
- {children}
- </ItemTechContainer>
- </React.Suspense>
- </Shell>
- )
-}
diff --git a/app/[lng]/procurement/(procurement)/items-tech/page.tsx b/app/[lng]/procurement/(procurement)/items-tech/page.tsx deleted file mode 100644 index 55ac9c63..00000000 --- a/app/[lng]/procurement/(procurement)/items-tech/page.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import * as React from "react"
-import { type SearchParams } from "@/types/table"
-
-import { getValidFilters } from "@/lib/data-table"
-import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"
-import { shipbuildingSearchParamsCache, offshoreTopSearchParamsCache, offshoreHullSearchParamsCache } from "@/lib/items-tech/validations"
-import { getShipbuildingItems, getOffshoreTopItems, getOffshoreHullItems } from "@/lib/items-tech/service"
-import { OffshoreTopTable } from "@/lib/items-tech/table/top/offshore-top-table"
-import { OffshoreHullTable } from "@/lib/items-tech/table/hull/offshore-hull-table"
-
-// 대소문자 문제 해결 - 실제 파일명에 맞게 import
-import { ItemsShipTable } from "@/lib/items-tech/table/ship/Items-ship-table"
-
-interface IndexPageProps {
- searchParams: Promise<SearchParams>
-}
-
-export default async function IndexPage({ searchParams }: IndexPageProps) {
- const params = await searchParams
- const shipbuildingSearch = shipbuildingSearchParamsCache.parse(params)
- const offshoreTopSearch = offshoreTopSearchParamsCache.parse(params)
- const offshoreHullSearch = offshoreHullSearchParamsCache.parse(params)
- const validShipbuildingFilters = getValidFilters(shipbuildingSearch.filters || [])
- const validOffshoreTopFilters = getValidFilters(offshoreTopSearch.filters || [])
- const validOffshoreHullFilters = getValidFilters(offshoreHullSearch.filters || [])
-
-
- // URL에서 아이템 타입 가져오기
- const itemType = params.type || "ship"
-
- return (
- <div>
- {itemType === "ship" && (
- <ItemsShipTable
- promises={Promise.all([
- getShipbuildingItems({
- ...shipbuildingSearch,
- filters: validShipbuildingFilters,
- }),
- ]).then(([result]) => result)}
- />
- )}
-
- {itemType === "top" && (
- <OffshoreTopTable
- promises={Promise.all([
- getOffshoreTopItems({
- ...offshoreTopSearch,
- filters: validOffshoreTopFilters,
- }),
- ]).then(([result]) => result)}
- />
- )}
-
- {itemType === "hull" && (
- <OffshoreHullTable
- promises={Promise.all([
- getOffshoreHullItems({
- ...offshoreHullSearch,
- filters: validOffshoreHullFilters,
- }),
- ]).then(([result]) => result)}
- />
- )}
- </div>
- )
-}
diff --git a/app/[lng]/procurement/(procurement)/items/page.tsx b/app/[lng]/procurement/(procurement)/items/page.tsx deleted file mode 100644 index f8d9a5b1..00000000 --- a/app/[lng]/procurement/(procurement)/items/page.tsx +++ /dev/null @@ -1,68 +0,0 @@ -// app/items/page.tsx (업데이트) -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 { searchParamsCache } from "@/lib/items/validations" -import { getItems } from "@/lib/items/service" -import { ItemsTable } from "@/lib/items/table/items-table" -import { ViewModeToggle } from "@/components/data-table/view-mode-toggle" - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - // pageSize 기반으로 모드 자동 결정 - const isInfiniteMode = search.perPage >= 1_000_000 - - // 페이지네이션 모드일 때만 서버에서 데이터 가져오기 - // 무한 스크롤 모드에서는 클라이언트에서 SWR로 데이터 로드 - const promises = isInfiniteMode - ? undefined - : Promise.all([ - getItems(search), // searchParamsCache의 결과를 그대로 사용 - ]) - - 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"> - 패키지 넘버 - </h2> - {/* <p className="text-muted-foreground"> - S-EDP로부터 수신된 패키지 정보이며 PR 전 입찰, 견적에 사용되며 벤더 데이터, 문서와 연결됩니다. - </p> */} - </div> - </div> - - </div> - - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - {/* DateRangePicker 등 추가 컴포넌트 */} - </React.Suspense> - - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - {/* 통합된 ItemsTable 컴포넌트 사용 */} - <ItemsTable promises={promises} /> - </React.Suspense> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/layout.tsx b/app/[lng]/procurement/(procurement)/layout.tsx deleted file mode 100644 index 82b53307..00000000 --- a/app/[lng]/procurement/(procurement)/layout.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { ReactNode } from 'react'; -import { Header } from '@/components/layout/Header'; -import { SiteFooter } from '@/components/layout/Footer'; - -export default function EvcpLayout({ children }: { children: ReactNode }) { - return ( - <div className="relative flex min-h-svh flex-col bg-background"> - {/* <div className="relative flex min-h-svh flex-col bg-slate-100 "> */} - <Header /> - <main className="flex flex-1 flex-col"> - <div className='container-wrapper'> - {children} - </div> - </main> - <SiteFooter/> - </div> - ); -}
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/menu-list/page.tsx b/app/[lng]/procurement/(procurement)/menu-list/page.tsx deleted file mode 100644 index dee45ab1..00000000 --- a/app/[lng]/procurement/(procurement)/menu-list/page.tsx +++ /dev/null @@ -1,70 +0,0 @@ -// app/evcp/menu-list/page.tsx - -import { Suspense } from "react"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { RefreshCw, Settings } from "lucide-react"; -import { getActiveUsers, getMenuAssignments } from "@/lib/menu-list/servcie"; -import { InitializeButton } from "@/lib/menu-list/table/initialize-button"; -import { MenuListTable } from "@/lib/menu-list/table/menu-list-table"; -import { Shell } from "@/components/shell" -import * as React from "react" - -export default async function MenuListPage() { - // 초기 데이터 로드 - const [menusResult, usersResult] = await Promise.all([ - getMenuAssignments(), - getActiveUsers() - ]); - - 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"> - 메뉴 관리 - </h2> - {/* <p className="text-muted-foreground"> - 각 메뉴별로 담당자를 지정하고 관리할 수 있습니다. - </p> */} - </div> - </div> - - </div> - - - <React.Suspense - fallback={ - "" - } - > - <Card> - <CardHeader> - <CardTitle className="flex items-center gap-2"> - <Settings className="h-5 w-5" /> - 메뉴 리스트 - </CardTitle> - <CardDescription> - 시스템의 모든 메뉴와 담당자 정보를 확인할 수 있습니다. - {menusResult.data?.length > 0 && ( - <span className="ml-2 text-sm"> - 총 {menusResult.data.length}개의 메뉴 - </span> - )} - </CardDescription> - </CardHeader> - <CardContent> - <Suspense fallback={<div className="text-center py-8">로딩 중...</div>}> - <MenuListTable - initialMenus={menusResult.data || []} - initialUsers={usersResult.data || []} - /> - </Suspense> - </CardContent> - </Card> - </React.Suspense> - </Shell> - - ); -}
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/payment-conditions/page.tsx b/app/[lng]/procurement/(procurement)/payment-conditions/page.tsx deleted file mode 100644 index d001a39d..00000000 --- a/app/[lng]/procurement/(procurement)/payment-conditions/page.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import * as React from "react"; -import { type SearchParams } from "@/types/table"; -import { getValidFilters } from "@/lib/data-table"; -import { Shell } from "@/components/shell"; -import { Skeleton } from "@/components/ui/skeleton"; -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; -import { SearchParamsCache } from "@/lib/payment-terms/validations"; -import { getPaymentTerms } from "@/lib/payment-terms/service"; -import { PaymentTermsTable } from "@/lib/payment-terms/table/payment-terms-table"; - -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([ - getPaymentTerms({ - ...search, - filters: validFilters, - }), - ]); - - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <h2 className="text-2xl font-bold tracking-tight">지급 조건 관리</h2> - {/* <p className="text-muted-foreground"> - 지급 조건(Payment Terms)을 등록, 수정, 삭제할 수 있습니다. - </p> */} - </div> - </div> - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}></React.Suspense> - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={4} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "8rem"]} - shrinkZero - /> - } - > - <PaymentTermsTable promises={promises} /> - </React.Suspense> - </Shell> - ); -}
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/po-rfq/page.tsx b/app/[lng]/procurement/(procurement)/po-rfq/page.tsx deleted file mode 100644 index 4a04d6a8..00000000 --- a/app/[lng]/procurement/(procurement)/po-rfq/page.tsx +++ /dev/null @@ -1,61 +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" - -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> - <h2 className="text-2xl font-bold tracking-tight"> - RFQ - </h2> - </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]/procurement/(procurement)/po/page.tsx b/app/[lng]/procurement/(procurement)/po/page.tsx deleted file mode 100644 index b4dd914f..00000000 --- a/app/[lng]/procurement/(procurement)/po/page.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { getPOs } from "@/lib/po/service" -import { searchParamsCache } from "@/lib/po/validations" -import { PoListsTable } from "@/lib/po/table/po-table" - - -interface IndexPageProps { - searchParams: Promise<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([ - getPOs({ - ...search, - filters: validFilters, - }), - ]) - - return ( - <Shell className="gap-2"> - - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - PO 확인 및 전자서명 - </h2> - {/* <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 - /> - } - > - <PoListsTable promises={promises} /> - </React.Suspense> - </Shell> - ) -} diff --git a/app/[lng]/procurement/(procurement)/poa/page.tsx b/app/[lng]/procurement/(procurement)/poa/page.tsx deleted file mode 100644 index 1c244991..00000000 --- a/app/[lng]/procurement/(procurement)/poa/page.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { getChangeOrders } from "@/lib/poa/service" -import { searchParamsCache } from "@/lib/poa/validations" -import { ChangeOrderListsTable } from "@/lib/poa/table/poa-table" - -interface IndexPageProps { - searchParams: Promise<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> - <h2 className="text-2xl font-bold tracking-tight"> - 변경 PO 확인 및 전자서명 - </h2> - {/* <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]/procurement/(procurement)/pq-criteria/[pqListId]/page.tsx b/app/[lng]/procurement/(procurement)/pq-criteria/[pqListId]/page.tsx deleted file mode 100644 index 15cb3bf3..00000000 --- a/app/[lng]/procurement/(procurement)/pq-criteria/[pqListId]/page.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import * as React from "react"
-import { type SearchParams } from "@/types/table"
-import { getValidFilters } from "@/lib/data-table"
-import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"
-import { Shell } from "@/components/shell"
-import { searchParamsCache } from "@/lib/pq/validations"
-import { getPQsByListId } from "@/lib/pq/service"
-import { PqsTable } from "@/lib/pq/pq-criteria/pq-table"
-import { notFound } from "next/navigation"
-
-interface PQDetailPageProps {
- params: Promise<{ pqListId: string }>
- searchParams: Promise<SearchParams>
-}
-
-export default async function PQDetailPage(props: PQDetailPageProps) {
- const params = await props.params
- const searchParams = await props.searchParams
- const search = searchParamsCache.parse(searchParams)
-
- const pqListId = parseInt(params.pqListId)
- if (isNaN(pqListId)) {
- notFound()
- }
-
- // filters가 없는 경우를 처리
- const validFilters = getValidFilters(search.filters)
-
- // PQ 항목들 가져오기
- const promises = Promise.all([
- getPQsByListId(pqListId, {
- ...search,
- filters: validFilters,
- })
- ])
-
- return (
- <Shell className="gap-2">
- <div className="flex items-center justify-between">
- <div>
- <h2 className="text-2xl font-bold tracking-tight">
- PQ 항목 관리
- </h2>
- {/* <p className="text-muted-foreground">
- 선택한 PQ 목록의 세부 항목들을 관리할 수 있습니다.
- </p> */}
- </div>
- </div>
-
- <React.Suspense
- fallback={
- <DataTableSkeleton
- columnCount={8}
- searchableColumnCount={1}
- filterableColumnCount={3}
- cellWidths={["10rem", "15rem", "20rem", "15rem", "10rem", "10rem", "8rem", "8rem"]}
- shrinkZero
- />
- }
- >
- <PqsTable
- promises={promises}
- pqListId={pqListId}
- />
- </React.Suspense>
- </Shell>
- )
-}
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/pq-criteria/page.tsx b/app/[lng]/procurement/(procurement)/pq-criteria/page.tsx deleted file mode 100644 index 1a337cc9..00000000 --- a/app/[lng]/procurement/(procurement)/pq-criteria/page.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import * as React from "react"
-import { type SearchParams } from "@/types/table"
-import { getValidFilters } from "@/lib/data-table"
-import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"
-import { Shell } from "@/components/shell"
-import { searchParamsCache } from "@/lib/pq/validations"
-import { getPQLists } from "@/lib/pq/service"
-import { PqListsTable } from "@/lib/pq/table/pq-lists-table"
-import { getProjects } from "@/lib/pq/service"
-
-interface ProjectPageProps {
- searchParams: Promise<SearchParams>
-}
-
-export default async function ProjectPage(props: ProjectPageProps) {
- const searchParams = await props.searchParams
- const search = searchParamsCache.parse(searchParams)
-
- // filters가 없는 경우를 처리
- const validFilters = getValidFilters(search.filters)
-
- // // 프로젝트별 PQ 데이터 가져오기
- const promises = Promise.all([
- getPQLists({
- ...search,
- filters: validFilters,
- }),
- getProjects()
- ])
-
- return (
- <Shell className="gap-2">
- <div className="flex items-center justify-between">
- <div>
- <h2 className="text-2xl font-bold tracking-tight">
- PQ 리스트 관리
- </h2>
- {/* <p className="text-muted-foreground">
- 협력업체 등록을 위한, 협력업체가 제출할 PQ 항목을: 프로젝트별로 관리할 수 있습니다.
- </p> */}
- </div>
- </div>
-
- <React.Suspense
- fallback={
- <DataTableSkeleton
- columnCount={6}
- searchableColumnCount={1}
- filterableColumnCount={2}
- cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]}
- shrinkZero
- />
- }
- >
- <PqListsTable
- promises={promises}
- />
- </React.Suspense>
- </Shell>
- )
-}
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/pq_new/[vendorId]/[submissionId]/page.tsx b/app/[lng]/procurement/(procurement)/pq_new/[vendorId]/[submissionId]/page.tsx deleted file mode 100644 index b4b51363..00000000 --- a/app/[lng]/procurement/(procurement)/pq_new/[vendorId]/[submissionId]/page.tsx +++ /dev/null @@ -1,206 +0,0 @@ -import * as React from "react"
-import { Metadata } from "next"
-import Link from "next/link"
-import { notFound } from "next/navigation"
-import { ArrowLeft } from "lucide-react"
-import { Shell } from "@/components/shell"
-import { Button } from "@/components/ui/button"
-import { Badge } from "@/components/ui/badge"
-import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
-import { Separator } from "@/components/ui/separator"
-import { getPQById, getPQDataByVendorId } from "@/lib/pq/service"
-import { unstable_noStore as noStore } from 'next/cache'
-import { PQReviewWrapper } from "@/components/pq-input/pq-review-wrapper"
-import { formatDate } from "@/lib/utils"
-
-export const metadata: Metadata = {
- title: "PQ 검토",
- description: "협력업체의 Pre-Qualification 답변을 검토합니다.",
-}
-
-// 페이지가 기본적으로 동적임을 나타냄
-export const dynamic = "force-dynamic"
-
-interface PQReviewPageProps {
- params: Promise<{
- vendorId: string;
- submissionId: string;
- }>
-}
-
-export default async function PQReviewPage(props: PQReviewPageProps) {
- // 캐시 비활성화
- noStore()
-
- const params = await props.params
- const vendorId = parseInt(params.vendorId, 10)
- const submissionId = parseInt(params.submissionId, 10)
-
- try {
- // PQ Submission 정보 조회
- const pqSubmission = await getPQById(submissionId, vendorId)
-
- // PQ 데이터 조회 (질문과 답변)
- const pqData = await getPQDataByVendorId(vendorId, pqSubmission.projectId || undefined)
-
- // 프로젝트 정보 (프로젝트 PQ인 경우)
- const projectInfo = pqSubmission.projectId ? {
- id: pqSubmission.projectId,
- projectCode: pqSubmission.projectCode || '',
- projectName: pqSubmission.projectName || '',
- status: pqSubmission.status,
- submittedAt: pqSubmission.submittedAt,
- } : null
-
- // PQ 유형 및 상태 레이블
- const typeLabel = pqSubmission.type === "GENERAL" ? "일반 PQ" :
- pqSubmission.type === "PROJECT" ? "프로젝트 PQ" :
- pqSubmission.type === "NON_INSPECTION" ? "미실사 PQ" : "일반 PQ"
- const statusLabel = getStatusLabel(pqSubmission.status)
- const statusVariant = getStatusVariant(pqSubmission.status)
-
- // 수정 가능 여부 (SUBMITTED 상태일 때만 가능)
- const canReview = pqSubmission.status === "SUBMITTED"
-
- return (
- <Shell className="gap-6 max-w-5xl">
- <div className="flex items-center justify-between">
- <div className="flex items-center gap-4">
- <Button variant="outline" size="sm" asChild>
- <Link href="/procurement/pq_new">
- <ArrowLeft className="w-4 h-4 mr-2" />
- 목록으로
- </Link>
- </Button>
- <div>
- <h2 className="text-2xl font-bold tracking-tight">
- {pqSubmission.vendorName} - {typeLabel}
- </h2>
- <div className="flex items-center gap-2 mt-1">
- <Badge variant={statusVariant}>{statusLabel}</Badge>
- {projectInfo && (
- <span className="text-muted-foreground">
- {projectInfo.projectName} ({projectInfo.projectCode})
- </span>
- )}
- </div>
- </div>
- </div>
- </div>
-
- {/* 상태별 알림 */}
- {pqSubmission.status === "SUBMITTED" && (
- <Alert>
- <AlertTitle>제출 완료</AlertTitle>
- <AlertDescription>
- 협력업체가 {formatDate(pqSubmission.submittedAt, "kr")}에 PQ를 제출했습니다. 검토 후 승인 또는 거부할 수 있습니다.
- </AlertDescription>
- </Alert>
- )}
-
- {pqSubmission.status === "APPROVED" && (
- <Alert variant="success">
- <AlertTitle>승인됨</AlertTitle>
- <AlertDescription>
- {formatDate(pqSubmission.approvedAt, "kr")}에 승인되었습니다.
- </AlertDescription>
- </Alert>
- )}
-
- {pqSubmission.status === "REJECTED" && (
- <Alert variant="destructive">
- <AlertTitle>거부됨</AlertTitle>
- <AlertDescription>
- {formatDate(pqSubmission.rejectedAt, "kr")}에 거부되었습니다.
- {pqSubmission.rejectReason && (
- <div className="mt-2">
- <strong>사유:</strong> {pqSubmission.rejectReason}
- </div>
- )}
- </AlertDescription>
- </Alert>
- )}
-
- <Separator />
-
- {/* PQ 검토 컴포넌트 */}
- <Tabs defaultValue="review" className="w-full">
- <TabsList>
- <TabsTrigger value="review">PQ 검토</TabsTrigger>
- <TabsTrigger value="vendor-info">협력업체 정보</TabsTrigger>
- </TabsList>
-
- <TabsContent value="review" className="mt-4">
- <PQReviewWrapper
- pqData={pqData}
- vendorId={vendorId}
- pqSubmission={pqSubmission}
- canReview={canReview}
- />
- </TabsContent>
-
- <TabsContent value="vendor-info" className="mt-4">
- <div className="rounded-md border p-4">
- <h3 className="text-lg font-medium mb-4">협력업체 정보</h3>
- <div className="grid grid-cols-2 gap-4">
- <div>
- <p className="text-sm font-medium text-muted-foreground">업체명</p>
- <p>{pqSubmission.vendorName}</p>
- </div>
- <div>
- <p className="text-sm font-medium text-muted-foreground">업체 코드</p>
- <p>{pqSubmission.vendorCode}</p>
- </div>
- <div>
- <p className="text-sm font-medium text-muted-foreground">상태</p>
- <p>{pqSubmission.vendorStatus}</p>
- </div>
- {/* 필요시 추가 정보 표시 */}
- </div>
- </div>
- </TabsContent>
- </Tabs>
- </Shell>
- )
- } catch (error) {
- console.error("Error loading PQ:", error)
- notFound()
- }
-}
-
-// 상태 레이블 함수
-function getStatusLabel(status: string): string {
- switch (status) {
- case "REQUESTED":
- return "요청됨";
- case "IN_PROGRESS":
- return "진행 중";
- case "SUBMITTED":
- return "제출됨";
- case "APPROVED":
- return "승인됨";
- case "REJECTED":
- return "거부됨";
- default:
- return status;
- }
-}
-
-// 상태별 Badge 스타일
-function getStatusVariant(status: string): "default" | "outline" | "secondary" | "destructive" | "success" {
- switch (status) {
- case "REQUESTED":
- return "outline";
- case "IN_PROGRESS":
- return "secondary";
- case "SUBMITTED":
- return "default";
- case "APPROVED":
- return "success";
- case "REJECTED":
- return "destructive";
- default:
- return "outline";
- }
-}
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/pq_new/page.tsx b/app/[lng]/procurement/(procurement)/pq_new/page.tsx deleted file mode 100644 index 6a992ee5..00000000 --- a/app/[lng]/procurement/(procurement)/pq_new/page.tsx +++ /dev/null @@ -1,99 +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 { searchParamsPQReviewCache } from "@/lib/pq/validations"
-import { getPQSubmissions } from "@/lib/pq/service"
-import { PQSubmissionsTable } from "@/lib/pq/pq-review-table-new/vendors-table"
-import { InformationButton } from "@/components/information/information-button"
-export const metadata: Metadata = {
- title: "협력업체 PQ/실사 현황",
- description: "",
-}
-
-interface PQReviewPageProps {
- searchParams: Promise<SearchParams>
-}
-
-export default async function PQReviewPage(props: PQReviewPageProps) {
- const searchParams = await props.searchParams
- const search = searchParamsPQReviewCache.parse(searchParams)
- const validFilters = getValidFilters(search.filters)
-
- // 디버깅 로그 추가
- console.log("=== PQ Page Debug ===");
- console.log("Raw searchParams:", searchParams);
- console.log("Raw basicFilters param:", searchParams.basicFilters);
- console.log("Raw pqBasicFilters param:", searchParams.pqBasicFilters);
- console.log("Parsed search:", search);
- console.log("search.filters:", search.filters);
- console.log("search.basicFilters:", search.basicFilters);
- console.log("search.pqBasicFilters:", search.pqBasicFilters);
- console.log("validFilters:", validFilters);
-
- // 기본 필터 처리 (통일된 이름 사용)
- let basicFilters = []
- if (search.basicFilters && search.basicFilters.length > 0) {
- basicFilters = search.basicFilters
- console.log("Using search.basicFilters:", basicFilters);
- } else if (search.pqBasicFilters && search.pqBasicFilters.length > 0) {
- // 하위 호환성을 위해 기존 이름도 지원
- basicFilters = search.pqBasicFilters
- console.log("Using search.pqBasicFilters:", basicFilters);
- } else {
- console.log("No basic filters found");
- }
-
- // 모든 필터를 합쳐서 처리
- const allFilters = [...validFilters, ...basicFilters]
-
- console.log("Final allFilters:", allFilters);
-
- // 조인 연산자도 통일된 이름 사용
- const joinOperator = search.basicJoinOperator || search.pqBasicJoinOperator || search.joinOperator || 'and';
- console.log("Final joinOperator:", joinOperator);
-
- // Promise.all로 감싸서 전달
- const promises = Promise.all([
- getPQSubmissions({
- ...search,
- filters: allFilters,
- joinOperator,
- })
- ])
-
- 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">
- 협력업체 PQ/실사 현황
- </h2>
- <InformationButton pagePath="evcp/pq_new" />
- </div>
- </div>
- </div>
- </div>
-
- {/* Items처럼 직접 테이블 렌더링 */}
- <React.Suspense
- key={JSON.stringify(searchParams)} // URL 파라미터가 변경될 때마다 강제 리렌더링
- fallback={
- <DataTableSkeleton
- columnCount={8}
- searchableColumnCount={2}
- filterableColumnCount={3}
- cellWidths={["10rem", "15rem", "12rem", "12rem", "8rem", "8rem", "10rem", "8rem"]}
- shrinkZero
- />
- }
- >
- <PQSubmissionsTable promises={promises} />
- </React.Suspense>
- </Shell>
- )
-}
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/project-gtc/page.tsx b/app/[lng]/procurement/(procurement)/project-gtc/page.tsx deleted file mode 100644 index 554f17b0..00000000 --- a/app/[lng]/procurement/(procurement)/project-gtc/page.tsx +++ /dev/null @@ -1,63 +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" - -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> - <h2 className="text-2xl font-bold tracking-tight"> - Project GTC 관리 - </h2> - {/* <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]/procurement/(procurement)/project-vendors/page.tsx b/app/[lng]/procurement/(procurement)/project-vendors/page.tsx deleted file mode 100644 index 525cff07..00000000 --- a/app/[lng]/procurement/(procurement)/project-vendors/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { ProjectAVLTable } from "@/lib/project-avl/table/proejctAVL-table" -import { getProjecTAVL } from "@/lib/project-avl/service" -import { searchProjectAVLParamsCache } from "@/lib/project-avl/validations" - - -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> - <h2 className="text-2xl font-bold tracking-tight"> - 프로젝트 AVL 리스트 - </h2> - {/* <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]/procurement/(procurement)/projects/page.tsx b/app/[lng]/procurement/(procurement)/projects/page.tsx deleted file mode 100644 index 8c332c6c..00000000 --- a/app/[lng]/procurement/(procurement)/projects/page.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { ItemsTable } from "@/lib/items/table/items-table" -import { getProjectLists } from "@/lib/projects/service" -import { ProjectsTable } from "@/lib/projects/table/projects-table" -import { searchParamsProjectsCache } from "@/lib/projects/validation" - - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsProjectsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getProjectLists({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - 수행 프로젝트 리스트 from S-EDP - </h2> - {/* <p className="text-muted-foreground"> - S-EDP로부터 수신하는 프로젝트 리스트입니다. 향후 MDG로 전환됩니다.{" "} - <span className="inline-flex items-center whitespace-nowrap"> - <Ellipsis className="size-3" /> - <span className="ml-1">버튼</span> - </span> - 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. - </p> */} - </div> - </div> - </div> - - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - {/* <DateRangePicker - triggerSize="sm" - triggerClassName="ml-auto w-56 sm:w-60" - align="end" - shallow={false} - /> */} - </React.Suspense> - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <ProjectsTable promises={promises} /> - </React.Suspense> - </Shell> - ) -} diff --git a/app/[lng]/procurement/(procurement)/report/page.tsx b/app/[lng]/procurement/(procurement)/report/page.tsx deleted file mode 100644 index 2782c3ac..00000000 --- a/app/[lng]/procurement/(procurement)/report/page.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import * as React from "react"; -import { Skeleton } from "@/components/ui/skeleton"; -import { Shell } from "@/components/shell"; -import { ErrorBoundary } from "@/components/error-boundary"; -import { getDashboardData } from "@/lib/dashboard/service"; -import { DashboardClient } from "@/lib/dashboard/dashboard-client"; - -// 데이터 fetch 시 비동기 함수 호출 후 await 하므로 static-pre-render 과정에서 dynamic-server-error 발생. -// 따라서, dynamic 속성을 force-dynamic 으로 설정하여 동적 렌더링 처리 -// getDashboardData 함수에 대한 Promise를 넘기는 식으로 수정하게 되면 force-dynamic 선언을 제거해도 됨. -export const dynamic = 'force-dynamic' - -export default async function IndexPage() { - // domain을 명시적으로 전달 - const domain = "procurement"; - - try { - // 서버에서 직접 데이터 fetch - const dashboardData = await getDashboardData(domain); - - return ( - <Shell className="gap-2"> - <DashboardClient initialData={dashboardData} /> - </Shell> - ); - } catch (error) { - console.error("Dashboard data fetch error:", error); - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-center py-12"> - <div className="text-center space-y-2"> - <p className="text-destructive">데이터를 불러오는데 실패했습니다.</p> - <p className="text-muted-foregroucdnd text-sm">{error instanceof Error ? error.message : "알 수 없는 오류가 발생했습니다."}</p> - </div> - </div> - </Shell> - ); - } -} - -function DashboardSkeleton() { - return ( - <div className="space-y-6"> - {/* 헤더 스켈레톤 */} - <div className="flex items-center justify-between"> - <div className="space-y-2"> - <Skeleton className="h-8 w-48" /> - <Skeleton className="h-4 w-72" /> - </div> - <Skeleton className="h-10 w-24" /> - </div> - - {/* 요약 카드 스켈레톤 */} - <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"> - {[...Array(4)].map((_, i) => ( - <div key={i} className="space-y-3 p-6 border rounded-lg"> - <div className="flex items-center justify-between"> - <Skeleton className="h-4 w-16" /> - <Skeleton className="h-4 w-4" /> - </div> - <Skeleton className="h-8 w-12" /> - <Skeleton className="h-3 w-20" /> - </div> - ))} - </div> - - {/* 차트 스켈레톤 */} - <div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> - {[...Array(2)].map((_, i) => ( - <div key={i} className="space-y-4 p-6 border rounded-lg"> - <div className="space-y-2"> - <Skeleton className="h-6 w-32" /> - <Skeleton className="h-4 w-48" /> - </div> - <Skeleton className="h-[300px] w-full" /> - </div> - ))} - </div> - - {/* 탭 스켈레톤 */} - <div className="space-y-4"> - <Skeleton className="h-10 w-64" /> - <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> - {[...Array(6)].map((_, i) => ( - <div key={i} className="space-y-4 p-6 border rounded-lg"> - <Skeleton className="h-6 w-32" /> - <div className="space-y-3"> - <div className="flex justify-between"> - <Skeleton className="h-4 w-16" /> - <Skeleton className="h-4 w-12" /> - </div> - <div className="flex gap-2"> - <Skeleton className="h-6 w-16" /> - <Skeleton className="h-6 w-16" /> - <Skeleton className="h-6 w-16" /> - </div> - <Skeleton className="h-2 w-full" /> - </div> - </div> - ))} - </div> - </div> - </div> - ); -} diff --git a/app/[lng]/procurement/(procurement)/rfq/[id]/cbe/page.tsx b/app/[lng]/procurement/(procurement)/rfq/[id]/cbe/page.tsx deleted file mode 100644 index fb288a98..00000000 --- a/app/[lng]/procurement/(procurement)/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]/procurement/(procurement)/rfq/[id]/layout.tsx b/app/[lng]/procurement/(procurement)/rfq/[id]/layout.tsx deleted file mode 100644 index 92817b4b..00000000 --- a/app/[lng]/procurement/(procurement)/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]/procurement/(procurement)/rfq/[id]/page.tsx b/app/[lng]/procurement/(procurement)/rfq/[id]/page.tsx deleted file mode 100644 index 1a9f4b18..00000000 --- a/app/[lng]/procurement/(procurement)/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]/procurement/(procurement)/rfq/[id]/tbe/page.tsx b/app/[lng]/procurement/(procurement)/rfq/[id]/tbe/page.tsx deleted file mode 100644 index 76eea302..00000000 --- a/app/[lng]/procurement/(procurement)/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]/procurement/(procurement)/rfq/page.tsx b/app/[lng]/procurement/(procurement)/rfq/page.tsx deleted file mode 100644 index 26f49cfb..00000000 --- a/app/[lng]/procurement/(procurement)/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]/procurement/(procurement)/settings/layout.tsx b/app/[lng]/procurement/(procurement)/settings/layout.tsx deleted file mode 100644 index 6c380919..00000000 --- a/app/[lng]/procurement/(procurement)/settings/layout.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { Metadata } from "next" - -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" - -export const metadata: Metadata = { - title: "Settings", - // description: "Advanced form example using react-hook-form and Zod.", -} - - -interface SettingsLayoutProps { - children: React.ReactNode - params: { lng: string } -} - -export default async function SettingsLayout({ - children, - params, -}: { - children: React.ReactNode - params: { lng: string } -}) { - const resolvedParams = await params - const lng = resolvedParams.lng - - - const sidebarNavItems = [ - - { - title: "Account", - href: `/${lng}/evcp/settings`, - }, - { - title: "Preferences", - href: `/${lng}/evcp/settings/preferences`, - } - - - ] - - - return ( - <> - <div className="container py-6"> - <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow"> - <div className="hidden space-y-6 p-10 pb-16 md:block"> - <div className="space-y-0.5"> - <h2 className="text-2xl font-bold tracking-tight">설정</h2> - {/* <p className="text-muted-foreground"> - Manage your account settings and preferences. - </p> */} - </div> - <Separator className="my-6" /> - <div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0"> - <aside className="-mx-4 lg:w-1/5"> - <SidebarNav items={sidebarNavItems} /> - </aside> - <div className="flex-1 ">{children}</div> - </div> - </div> - </section> - </div> - - - </> - ) -} diff --git a/app/[lng]/procurement/(procurement)/settings/page.tsx b/app/[lng]/procurement/(procurement)/settings/page.tsx deleted file mode 100644 index eba5e948..00000000 --- a/app/[lng]/procurement/(procurement)/settings/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { AccountForm } from "@/components/settings/account-form" - -export default function SettingsAccountPage() { - return ( - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium">Account</h3> - {/* <p className="text-sm text-muted-foreground"> - Update your account settings. Set your preferred language and - timezone. - </p> */} - </div> - <Separator /> - <AccountForm /> - </div> - ) -} diff --git a/app/[lng]/procurement/(procurement)/settings/preferences/page.tsx b/app/[lng]/procurement/(procurement)/settings/preferences/page.tsx deleted file mode 100644 index e2a88021..00000000 --- a/app/[lng]/procurement/(procurement)/settings/preferences/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { AppearanceForm } from "@/components/settings/appearance-form" - -export default function SettingsAppearancePage() { - return ( - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium">Preference</h3> - <p className="text-sm text-muted-foreground"> - Customize the preference of the app. - </p> - </div> - <Separator /> - <AppearanceForm /> - </div> - ) -} diff --git a/app/[lng]/procurement/(procurement)/system/admin-users/page.tsx b/app/[lng]/procurement/(procurement)/system/admin-users/page.tsx deleted file mode 100644 index 11a9e9fb..00000000 --- a/app/[lng]/procurement/(procurement)/system/admin-users/page.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { DateRangePicker } from "@/components/date-range-picker" -import { Separator } from "@/components/ui/separator" - -import { searchParamsCache } from "@/lib/admin-users/validations" -import { getAllCompanies, getAllRoles, getUserCountGroupByCompany, getUserCountGroupByRole, getUsers } from "@/lib/admin-users/service" -import { AdmUserTable } from "@/lib/admin-users/table/ausers-table" - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function UserTable(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getUsers({ - ...search, - filters: validFilters, - }), - getUserCountGroupByCompany(), - getUserCountGroupByRole(), - getAllCompanies(), - getAllRoles() - ]) - - return ( - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium">Vendor Admin User Management</h3> - <p className="text-sm text-muted-foreground"> - 협력업체의 유저 전체를 조회하고 어드민 유저를 생성할 수 있는 페이지입니다. 이곳에서 초기 유저를 생성시킬 수 있습니다. <br />생성 후에는 생성된 사용자의 이메일로 생성 통보 이메일이 발송되며 사용자는 이메일과 OTP로 로그인이 가능합니다. - </p> - </div> - <Separator /> - <AdmUserTable promises={promises} /> - </div> - </React.Suspense> - - ) -} diff --git a/app/[lng]/procurement/(procurement)/system/layout.tsx b/app/[lng]/procurement/(procurement)/system/layout.tsx deleted file mode 100644 index 2776ed8b..00000000 --- a/app/[lng]/procurement/(procurement)/system/layout.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Metadata } from "next" - -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" - -export const metadata: Metadata = { - title: "System Setting", - // description: "Advanced form example using react-hook-form and Zod.", -} - - -interface SettingsLayoutProps { - children: React.ReactNode - params: { lng: string } -} - -export default async function SettingsLayout({ - children, - params, -}: { - children: React.ReactNode - params: { lng: string } -}) { - const resolvedParams = await params - const lng = resolvedParams.lng - - - const sidebarNavItems = [ - - { - title: "삼성중공업 사용자", - href: `/${lng}/evcp/system`, - }, - { - title: "Roles", - href: `/${lng}/evcp/system/roles`, - }, - { - title: "권한 통제", - href: `/${lng}/evcp/system/permissions`, - }, - { - title: "협력업체 사용자", - href: `/${lng}/evcp/system/admin-users`, - }, - - { - title: "비밀번호 정책", - href: `/${lng}/evcp/system/password-policy`, - }, - - ] - - - return ( - <> - <div className="container py-6"> - <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow"> - <div className="hidden space-y-6 p-10 pb-16 md:block"> - <div className="space-y-0.5"> - <h2 className="text-2xl font-bold tracking-tight">시스템 설정</h2> - {/* <p className="text-muted-foreground"> - 사용자, 롤, 접근 권한을 관리하세요. - </p> */} - </div> - <Separator className="my-6" /> - <div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0"> - <aside className="-mx-4 lg:w-1/5"> - <SidebarNav items={sidebarNavItems} /> - </aside> - <div className="flex-1 ">{children}</div> - </div> - </div> - </section> - </div> - - - </> - ) -} diff --git a/app/[lng]/procurement/(procurement)/system/page.tsx b/app/[lng]/procurement/(procurement)/system/page.tsx deleted file mode 100644 index fe0a262c..00000000 --- a/app/[lng]/procurement/(procurement)/system/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import * as React from "react" -import { getValidFilters } from "@/lib/data-table" -import { searchParamsCache } from "@/lib/admin-users/validations" -import { getAllRoles, getUsersEVCP } from "@/lib/users/service" -import { getUserCountGroupByRole } from "@/lib/admin-users/service" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { UserTable } from "@/lib/users/table/users-table" - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function SystemUserPage(props: IndexPageProps) { - - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getUsersEVCP({ - ...search, - filters: validFilters, - }), - getUserCountGroupByRole(), - getAllRoles() - ]) - - return ( - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "12rem", "12rem", "12rem"]} - shrinkZero - /> - } - > - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium">SHI Users</h3> - <p className="text-sm text-muted-foreground"> - 시스템 전체 사용자들을 조회하고 관리할 수 있는 페이지입니다. 사용자에게 롤을 할당하는 것으로 메뉴별 권한을 관리할 수 있습니다. - </p> - </div> - <Separator /> - <UserTable promises={promises} /> - </div> - </React.Suspense> - - ) -}
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/system/password-policy/page.tsx b/app/[lng]/procurement/(procurement)/system/password-policy/page.tsx deleted file mode 100644 index 0f14fefe..00000000 --- a/app/[lng]/procurement/(procurement)/system/password-policy/page.tsx +++ /dev/null @@ -1,63 +0,0 @@ -// app/admin/password-policy/page.tsx - -import * as React from "react" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Separator } from "@/components/ui/separator" -import { Alert, AlertDescription } from "@/components/ui/alert" -import { AlertTriangle } from "lucide-react" -import SecuritySettingsTable from "@/components/system/passwordPolicy" -import { getSecuritySettings } from "@/lib/password-policy/service" - - -export default async function PasswordPolicyPage() { - try { - // 보안 설정 데이터 로드 - const securitySettings = await getSecuritySettings() - - return ( - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={4} - searchableColumnCount={0} - filterableColumnCount={0} - cellWidths={["20rem", "30rem", "15rem", "10rem"]} - shrinkZero - /> - } - > - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium">협력업체 사용자 비밀번호 정책 설정</h3> - <p className="text-sm text-muted-foreground"> - 협력업체 사용자들을 위한 비밀번호 정책과 보안 설정을 관리할 수 있습니다. - </p> - </div> - <Separator /> - <SecuritySettingsTable initialSettings={securitySettings} /> - </div> - </React.Suspense> - ) - } catch (error) { - console.error('Failed to load security settings:', error) - - return ( - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium">협력업체 사용자 비밀번호 정책 설정</h3> - <p className="text-sm text-muted-foreground"> - 협력업체 사용자들을 위한 비밀번호 정책과 보안 설정을 관리할 수 있습니다. - </p> - </div> - <Separator /> - <Alert variant="destructive"> - <AlertTriangle className="h-4 w-4" /> - <AlertDescription> - 보안 설정을 불러오는 중 오류가 발생했습니다. 페이지를 새로고침하거나 관리자에게 문의하세요. - </AlertDescription> - </Alert> - </div> - ) - } -}
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/system/permissions/page.tsx b/app/[lng]/procurement/(procurement)/system/permissions/page.tsx deleted file mode 100644 index 6aa2b693..00000000 --- a/app/[lng]/procurement/(procurement)/system/permissions/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import PermissionsTree from "@/components/system/permissionsTree" -import { Separator } from "@/components/ui/separator" - -export default function PermissionsPage() { - return ( - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium">Permissions</h3> - <p className="text-sm text-muted-foreground"> - Set permissions to the menu by Role - </p> - </div> - <Separator /> - <PermissionsTree/> - </div> - ) -} diff --git a/app/[lng]/procurement/(procurement)/system/roles/page.tsx b/app/[lng]/procurement/(procurement)/system/roles/page.tsx deleted file mode 100644 index fe074600..00000000 --- a/app/[lng]/procurement/(procurement)/system/roles/page.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Separator } from "@/components/ui/separator" - -import { searchParamsCache } from "@/lib/roles/validations" -import { searchParamsCache as searchParamsCache2 } from "@/lib/admin-users/validations" -import { RolesTable } from "@/lib/roles/table/roles-table" -import { getRolesWithCount } from "@/lib/roles/services" -import { getUsersAll } from "@/lib/users/service" - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function UserTable(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - const search2 = searchParamsCache2.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getRolesWithCount({ - ...search, - filters: validFilters, - }), - - - ]) - - - const promises2 = Promise.all([ - getUsersAll({ - ...search2, - filters: validFilters, - }, "evcp"), - ]) - - - return ( - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium">Role Management</h3> - <p className="text-sm text-muted-foreground"> - 역할을 생성하고 역할에 유저를 할당할 수 있는 페이지입니다. 역할에 메뉴의 접근 권한 역시 할당할 수 있습니다. - </p> - </div> - <Separator /> - <RolesTable promises={promises} promises2={promises2} /> - </div> - </React.Suspense> - - ) -} diff --git a/app/[lng]/procurement/(procurement)/tbe/page.tsx b/app/[lng]/procurement/(procurement)/tbe/page.tsx deleted file mode 100644 index 1a7fdf86..00000000 --- a/app/[lng]/procurement/(procurement)/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"> - Technical Bid Evaluation - </h2> - <p className="text-muted-foreground"> - 초대된 협력업체에게 TBE를 보낼 수 있습니다. <br/> - 체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. - </p> - </div> - </div> - </div> - - {/* 타입 선택 탭 (Budgetary 제외) */} - <Tabs defaultValue={validType} value={validType} className="w-full"> - <TabsList className="grid grid-cols-2 w-full max-w-md"> - <TabsTrigger value="purchase" asChild> - <a href={getTabUrl('purchase')}>Purchase</a> - </TabsTrigger> - <TabsTrigger value="purchase-budgetary" asChild> - <a href={getTabUrl('purchase-budgetary')}>Purchase Budgetary</a> - </TabsTrigger> - </TabsList> - - <div className="mt-2"> - <p className="text-sm text-muted-foreground"> - {typeConfig[validType].description} - </p> - </div> - </Tabs> - - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <AllTbeTable promises={promises}/> - </React.Suspense> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/vendor-candidates/page.tsx b/app/[lng]/procurement/(procurement)/vendor-candidates/page.tsx deleted file mode 100644 index fb80cf64..00000000 --- a/app/[lng]/procurement/(procurement)/vendor-candidates/page.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" - -import { getVendorCandidateCounts, getVendorCandidates } from "@/lib/vendor-candidates/service" -import { searchParamsCandidateCache } from "@/lib/vendor-candidates/validations" -import { VendorCandidateTable } from "@/lib/vendor-candidates/table/candidates-table" -import { DateRangePicker } from "@/components/date-range-picker" - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCandidateCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getVendorCandidates({ - ...search, - filters: validFilters, - }), - getVendorCandidateCounts() - ]) - - return ( - <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"> - 발굴업체 등록 관리 - </h2> - {/* <p className="text-muted-foreground"> - 수집한 협력업체 후보를 등록하고 초대 메일을 송부할 수 있습니다. - </p> */} - </div> - </div> - </div> - - {/* 수집일 라벨과 DateRangePicker를 함께 배치 */} - <div className="flex items-center justify-start gap-2"> - {/* <span className="text-sm font-medium">수집일 기간 설정: </span> */} - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - <DateRangePicker - triggerSize="sm" - triggerClassName="w-56 sm:w-60" - align="end" - shallow={false} - showClearButton={true} - placeholder="수집일 날짜 범위를 고르세요" - /> - </React.Suspense> - </div> - - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <VendorCandidateTable promises={promises}/> - </React.Suspense> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/vendor-check-list/page.tsx b/app/[lng]/procurement/(procurement)/vendor-check-list/page.tsx deleted file mode 100644 index e6f9ce82..00000000 --- a/app/[lng]/procurement/(procurement)/vendor-check-list/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { getGenralEvaluationsSchema } from "@/lib/general-check-list/validation" -import { GeneralEvaluationsTable } from "@/lib/general-check-list/table/general-check-list-table" -import { getGeneralEvaluations } from "@/lib/general-check-list/service" - - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = getGenralEvaluationsSchema.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getGeneralEvaluations({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - 협력업체 평가자료 문항 관리 - </h2> - {/* <p className="text-muted-foreground"> - 협력업체 평가에 사용되는 정기평가 체크리스트를 관리{" "} - <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 - /> - } - > - <GeneralEvaluationsTable promises={promises} /> - </React.Suspense> - </Shell> - ) -} diff --git a/app/[lng]/procurement/(procurement)/vendor-investigation/page.tsx b/app/[lng]/procurement/(procurement)/vendor-investigation/page.tsx deleted file mode 100644 index af9f3e11..00000000 --- a/app/[lng]/procurement/(procurement)/vendor-investigation/page.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" - -import { VendorsInvestigationTable } from "@/lib/vendor-investigation/table/investigation-table" -import { getVendorsInvestigation } from "@/lib/vendor-investigation/service" -import { searchParamsInvestigationCache } from "@/lib/vendor-investigation/validations" - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsInvestigationCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getVendorsInvestigation({ - ...search, - filters: validFilters, - }), - ]) - - return ( - <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"> - 협력업체 실사 관리 - </h2> - {/* <p className="text-muted-foreground"> - 요청된 Vendor 실사에 대한 스케줄 정보를 관리하고 결과를 입력할 수 있습니다. - - </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 - /> - } - > - <VendorsInvestigationTable promises={promises}/> - </React.Suspense> - </Shell> - ) -} diff --git a/app/[lng]/procurement/(procurement)/vendor-type/page.tsx b/app/[lng]/procurement/(procurement)/vendor-type/page.tsx deleted file mode 100644 index 96169e8a..00000000 --- a/app/[lng]/procurement/(procurement)/vendor-type/page.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { searchParamsCache } from "@/lib/vendor-type/validations" -import { VendorTypesTable } from "@/lib/vendor-type/table/vendorTypes-table" -import { getVendorTypes } from "@/lib/vendor-type/service" - - -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([ - getVendorTypes({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - 업체 유형 - </h2> - {/* <p className="text-muted-foreground"> - 업체 유형을 등록하고 관리할 수 있습니다.{" "} - - </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 - /> - } - > - <VendorTypesTable promises={promises} /> - </React.Suspense> - </Shell> - ) -} diff --git a/app/[lng]/procurement/(procurement)/vendors/[id]/info/items/page.tsx b/app/[lng]/procurement/(procurement)/vendors/[id]/info/items/page.tsx deleted file mode 100644 index 5d5838c6..00000000 --- a/app/[lng]/procurement/(procurement)/vendors/[id]/info/items/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { getVendorItems } from "@/lib/vendors/service" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { searchParamsItemCache } from "@/lib/vendors/validations" -import { VendorItemsTable } from "@/lib/vendors/items-table/item-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise<SearchParams> -} - -export default async function SettingsAccountPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsItemCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - - - const promises = Promise.all([ - getVendorItems({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - // 4) 렌더링 - return ( - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium"> - 공급품목(패키지) - </h3> - <p className="text-sm text-muted-foreground"> - {/* 딜리버리가 가능한 아이템 리스트를 확인할 수 있습니다. */} - </p> - </div> - <Separator /> - <div> - <VendorItemsTable promises={promises} vendorId={idAsNumber}/> - </div> - </div> - ) -}
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/vendors/[id]/info/layout.tsx b/app/[lng]/procurement/(procurement)/vendors/[id]/info/layout.tsx deleted file mode 100644 index 7e2cd4f6..00000000 --- a/app/[lng]/procurement/(procurement)/vendors/[id]/info/layout.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { Metadata } from "next" - -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" -import { findVendorById } from "@/lib/vendors/service" // 가정: 여기에 findVendorById가 있다고 가정 -import { Vendor } from "@/db/schema/vendors" -import { Button } from "@/components/ui/button" -import { ArrowLeft } from "lucide-react" -import Link from "next/link" -export const metadata: Metadata = { - title: "Vendor Detail", -} - -export default async function SettingsLayout({ - children, - params, -}: { - children: React.ReactNode - params: { lng: string , id: string} -}) { - - // 1) URL 파라미터에서 id 추출, Number로 변환 - const resolvedParams = await params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - // 2) DB에서 해당 협력업체 정보 조회 - const vendor: Vendor | null = await findVendorById(idAsNumber) - - // 3) 사이드바 메뉴 - const sidebarNavItems = [ - { - title: "연락처", - href: `/${lng}/evcp/vendors/${id}/info`, - }, - { - title: "공급품목(패키지)", - href: `/${lng}/evcp/vendors/${id}/info/items`, - }, - { - title: "공급품목(자재그룹)", - href: `/${lng}/evcp/vendors/${id}/info/materials`, - }, - { - title: "견적 히스토리", - href: `/${lng}/evcp/vendors/${id}/info/rfq-history`, - }, - { - title: "입찰 히스토리", - href: `/${lng}/evcp/vendors/${id}/info/bid-history`, - }, - { - title: "계약 히스토리", - href: `/${lng}/evcp/vendors/${id}/info/contract-history`, - }, - ] - - 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/vendors`} 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>협력업체 목록으로 돌아가기</span> - </Button> - </Link> - </div> - <div className="space-y-0.5"> - {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} - <h2 className="text-2xl font-bold tracking-tight"> - {vendor - ? `${vendor.vendorCode ?? ""} - ${vendor.vendorName} 상세 정보` - : "Loading Vendor..."} - </h2> - <p className="text-muted-foreground">협력업체 관련 상세사항을 확인하세요.</p> - </div> - <Separator className="my-6" /> - <div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0"> - <aside className="-mx-4 lg:w-1/5"> - <SidebarNav items={sidebarNavItems} /> - </aside> - <div className="flex-1">{children}</div> - </div> - </div> - </section> - </div> - </> - ) -}
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/vendors/[id]/info/materials/page.tsx b/app/[lng]/procurement/(procurement)/vendors/[id]/info/materials/page.tsx deleted file mode 100644 index 0ebb66ba..00000000 --- a/app/[lng]/procurement/(procurement)/vendors/[id]/info/materials/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 { searchParamsMaterialCache } from "@/lib/vendors/validations" -import { getVendorMaterials } from "@/lib/vendors/service" -import { VendorMaterialsTable } from "@/lib/vendors/materials-table/item-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise<SearchParams> -} - -export default async function SettingsAccountPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsMaterialCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - - - const promises = Promise.all([ - getVendorMaterials({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - // 4) 렌더링 - return ( - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium"> - 공급품목(자재 그룹) - </h3> - <p className="text-sm text-muted-foreground"> - {/* 딜리버리가 가능한 공급품목(자재 그룹)을 확인할 수 있습니다. */} - </p> - </div> - <Separator /> - <div> - <VendorMaterialsTable promises={promises} vendorId={idAsNumber}/> - </div> - </div> - ) -}
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/vendors/[id]/info/page.tsx b/app/[lng]/procurement/(procurement)/vendors/[id]/info/page.tsx deleted file mode 100644 index 6279e924..00000000 --- a/app/[lng]/procurement/(procurement)/vendors/[id]/info/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { getVendorContacts } from "@/lib/vendors/service" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { searchParamsContactCache } from "@/lib/vendors/validations" -import { VendorContactsTable } from "@/lib/vendors/contacts-table/contact-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise<SearchParams> -} - -export default async function SettingsAccountPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsContactCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - - - const promises = Promise.all([ - getVendorContacts({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - // 4) 렌더링 - return ( - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium"> - Contacts - </h3> - <p className="text-sm text-muted-foreground"> - 업무별 담당자 정보를 확인하세요. - </p> - </div> - <Separator /> - <div> - <VendorContactsTable promises={promises} vendorId={idAsNumber}/> - </div> - </div> - ) -}
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/vendors/[id]/info/rfq-history/page.tsx b/app/[lng]/procurement/(procurement)/vendors/[id]/info/rfq-history/page.tsx deleted file mode 100644 index c7f8f8b6..00000000 --- a/app/[lng]/procurement/(procurement)/vendors/[id]/info/rfq-history/page.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Separator } from "@/components/ui/separator"
-import { getRfqHistory } from "@/lib/vendors/service"
-import { type SearchParams } from "@/types/table"
-import { getValidFilters } from "@/lib/data-table"
-import { searchParamsRfqHistoryCache } from "@/lib/vendors/validations"
-import { VendorRfqHistoryTable } from "@/lib/vendors/rfq-history-table/rfq-history-table"
-
-interface IndexPageProps {
- // Next.js 13 App Router에서 기본으로 주어지는 객체들
- params: {
- lng: string
- id: string
- }
- searchParams: Promise<SearchParams>
-}
-
-export default async function RfqHistoryPage(props: IndexPageProps) {
- const resolvedParams = await props.params
- const lng = resolvedParams.lng
- const id = resolvedParams.id
-
- const idAsNumber = Number(id)
-
- // 2) SearchParams 파싱 (Zod)
- // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼
- const searchParams = await props.searchParams
- const search = searchParamsRfqHistoryCache.parse(searchParams)
- const validFilters = getValidFilters(search.filters)
-
- const promises = Promise.all([
- getRfqHistory({
- ...search,
- filters: validFilters,
- },
- idAsNumber)
- ])
-
- // 4) 렌더링
- return (
- <div className="space-y-6">
- <div>
- <h3 className="text-lg font-medium">
- RFQ History
- </h3>
- <p className="text-sm text-muted-foreground">
- 협력업체의 RFQ 참여 이력을 확인할 수 있습니다.
- </p>
- </div>
- <Separator />
- <div>
- <VendorRfqHistoryTable promises={promises} />
- </div>
- </div>
- )
-}
\ No newline at end of file diff --git a/app/[lng]/procurement/(procurement)/vendors/page.tsx b/app/[lng]/procurement/(procurement)/vendors/page.tsx deleted file mode 100644 index 02616999..00000000 --- a/app/[lng]/procurement/(procurement)/vendors/page.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" - - -import { searchParamsCache } from "@/lib/vendors/validations" -import { getVendors, getVendorStatusCounts } from "@/lib/vendors/service" -import { VendorsTable } from "@/lib/vendors/table/vendors-table" -import { Ellipsis } from "lucide-react" - -interface IndexPageProps { - searchParams: Promise<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([ - getVendors({ - ...search, - filters: validFilters, - }), - getVendorStatusCounts(), - ]) - - 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"> - 협력업체 관리 - </h2> - {/* <p className="text-muted-foreground"> - 협력업체에 대한 요약 정보를 확인하고{" "} - <span className="inline-flex items-center whitespace-nowrap"> - <Ellipsis className="size-3" /> - <span className="ml-1">버튼</span> - </span> - 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. <br/>벤더의 상태에 따라 가입을 승인해주거나 PQ 요청을 할 수 있고 검토가 완료된 벤더를 기간계 시스템에 전송하여 협력업체 코드를 따올 수 있습니다. - </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 - /> - } - > - <VendorsTable promises={promises} /> - </React.Suspense> - </Shell> - ) -} diff --git a/app/[lng]/procurement/page.tsx b/app/[lng]/procurement/page.tsx deleted file mode 100644 index f9662cb7..00000000 --- a/app/[lng]/procurement/page.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Metadata } from "next" -import { Suspense } from "react" -import { LoginFormSkeleton } from "@/components/login/login-form-skeleton" -import { LoginFormSHI } from "@/components/login/login-form-shi" - -export const metadata: Metadata = { - title: "eVCP Portal", - description: "", -} - -export default function AuthenticationPage() { - - - return ( - <> - <Suspense fallback={<LoginFormSkeleton/>}> - <LoginFormSHI /> - </Suspense> - </> - ) -} diff --git a/app/[lng]/sales/(sales)/bid-projects/page.tsx b/app/[lng]/sales/(sales)/bid-projects/page.tsx deleted file mode 100644 index 38cbf91a..00000000 --- a/app/[lng]/sales/(sales)/bid-projects/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { getBidProjectLists } from "@/lib/bidding-projects/service" -import { searchParamsBidProjectsCache } from "@/lib/bidding-projects/validation" -import { BidProjectsTable } from "@/lib/bidding-projects/table/projects-table" - - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsBidProjectsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getBidProjectLists({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - 견적 프로젝트 관리 - </h2> - {/* <p className="text-muted-foreground"> - SAP(S-ERP)로부터 수신한 견적 프로젝트 데이터입니다. 기술영업의 Budgetary RFQ에서 사용됩니다. - <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 - /> - } - > - <BidProjectsTable promises={promises} /> - </React.Suspense> - </Shell> - ) -} diff --git a/app/[lng]/sales/(sales)/bqcbe/page.tsx b/app/[lng]/sales/(sales)/bqcbe/page.tsx deleted file mode 100644 index 30935645..00000000 --- a/app/[lng]/sales/(sales)/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]/sales/(sales)/bqtbe/page.tsx b/app/[lng]/sales/(sales)/bqtbe/page.tsx deleted file mode 100644 index 3e56cfaa..00000000 --- a/app/[lng]/sales/(sales)/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]/sales/(sales)/budgetary-rfq/[id]/cbe/page.tsx b/app/[lng]/sales/(sales)/budgetary-rfq/[id]/cbe/page.tsx deleted file mode 100644 index 956facd3..00000000 --- a/app/[lng]/sales/(sales)/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]/sales/(sales)/budgetary-rfq/[id]/layout.tsx b/app/[lng]/sales/(sales)/budgetary-rfq/[id]/layout.tsx deleted file mode 100644 index 2b80e64f..00000000 --- a/app/[lng]/sales/(sales)/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]/sales/(sales)/budgetary-rfq/[id]/page.tsx b/app/[lng]/sales/(sales)/budgetary-rfq/[id]/page.tsx deleted file mode 100644 index dd9df563..00000000 --- a/app/[lng]/sales/(sales)/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]/sales/(sales)/budgetary-rfq/[id]/tbe/page.tsx b/app/[lng]/sales/(sales)/budgetary-rfq/[id]/tbe/page.tsx deleted file mode 100644 index ec894e1c..00000000 --- a/app/[lng]/sales/(sales)/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]/sales/(sales)/budgetary-rfq/page.tsx b/app/[lng]/sales/(sales)/budgetary-rfq/page.tsx deleted file mode 100644 index f342bbff..00000000 --- a/app/[lng]/sales/(sales)/budgetary-rfq/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.PURCHASE_BUDGETARY, - title = "Budgetary Quote", - description = "Budgetary Quote를 등록하여 요청 및 응답을 관리할 수 있습니다." -}: RfqPageProps) { - const search = searchParamsCache.parse(await searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getRfqs({ - ...search, - filters: validFilters, - rfqType // 전달받은 rfqType 사용 - }), - getRfqStatusCounts(rfqType), // rfqType 전달 - getAllItems() - ]) - - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - {title} - </h2> - {/* <p className="text-muted-foreground"> - {description} - 기본적인 정보와 RFQ를 위한 아이템 등록 및 첨부를 한 후, - <span className="inline-flex items-center whitespace-nowrap"> - <Ellipsis className="size-3" /> - <span className="ml-1">버튼</span> - </span> 을 클릭하면 "Proceed"를 통해 상세화면으로 이동하여 진행할 수 있습니다. - </p> */} - </div> - </div> - </div> - - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - {/* <DateRangePicker - triggerSize="sm" - triggerClassName="ml-auto w-56 sm:w-60" - align="end" - shallow={false} - /> */} - </React.Suspense> - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <RfqsTable promises={promises} rfqType={rfqType} /> - </React.Suspense> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary-tech-sales-hull/page.tsx b/app/[lng]/sales/(sales)/budgetary-tech-sales-hull/page.tsx deleted file mode 100644 index b1be29db..00000000 --- a/app/[lng]/sales/(sales)/budgetary-tech-sales-hull/page.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { searchParamsHullCache } from "@/lib/techsales-rfq/validations" -import { getTechSalesHullRfqsWithJoin } from "@/lib/techsales-rfq/service" -import { getValidFilters } from "@/lib/data-table" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { RFQListTable } from "@/lib/techsales-rfq/table/rfq-table" -import { type SearchParams } from "@/types/table" -import * as React from "react" - -interface HullRfqPageProps { - searchParams: Promise<SearchParams> -} - -export default async function HullRfqPage(props: HullRfqPageProps) { - // searchParams를 await하여 resolve - const searchParams = await props.searchParams - - // 해양 HULL용 파라미터 파싱 - const search = searchParamsHullCache.parse(searchParams); - const validFilters = getValidFilters(search.filters); - - // 기술영업 해양 Hull RFQ 데이터를 Promise.all로 감싸서 전달 - const promises = Promise.all([ - getTechSalesHullRfqsWithJoin({ - ...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> - <h2 className="text-2xl font-bold tracking-tight"> - 기술영업-해양 Hull RFQ - </h2> - </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" rfqType="HULL" /> - </React.Suspense> - </div> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary-tech-sales-ship/page.tsx b/app/[lng]/sales/(sales)/budgetary-tech-sales-ship/page.tsx deleted file mode 100644 index b7bf9d15..00000000 --- a/app/[lng]/sales/(sales)/budgetary-tech-sales-ship/page.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { searchParamsShipCache } from "@/lib/techsales-rfq/validations" -import { getTechSalesShipRfqsWithJoin } from "@/lib/techsales-rfq/service" -import { getValidFilters } from "@/lib/data-table" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { RFQListTable } from "@/lib/techsales-rfq/table/rfq-table" -import { type SearchParams } from "@/types/table" -import * as React from "react" - -interface RfqPageProps { - searchParams: Promise<SearchParams> -} - -export default async function RfqPage(props: RfqPageProps) { - // searchParams를 await하여 resolve - const searchParams = await props.searchParams - - // 조선용 파라미터 파싱 - const search = searchParamsShipCache.parse(searchParams); - const validFilters = getValidFilters(search.filters); - - // 기술영업 조선 RFQ 데이터를 Promise.all로 감싸서 전달 - const promises = Promise.all([ - getTechSalesShipRfqsWithJoin({ - ...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> - <h2 className="text-2xl font-bold tracking-tight"> - 기술영업-조선 RFQ - </h2> - </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" rfqType="SHIP" /> - </React.Suspense> - </div> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary-tech-sales-top/page.tsx b/app/[lng]/sales/(sales)/budgetary-tech-sales-top/page.tsx deleted file mode 100644 index f84a9794..00000000 --- a/app/[lng]/sales/(sales)/budgetary-tech-sales-top/page.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { searchParamsTopCache } from "@/lib/techsales-rfq/validations" -import { getTechSalesTopRfqsWithJoin } from "@/lib/techsales-rfq/service" -import { getValidFilters } from "@/lib/data-table" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { RFQListTable } from "@/lib/techsales-rfq/table/rfq-table" -import { type SearchParams } from "@/types/table" -import * as React from "react" - -interface HullRfqPageProps { - searchParams: Promise<SearchParams> -} - -export default async function HullRfqPage(props: HullRfqPageProps) { - // searchParams를 await하여 resolve - const searchParams = await props.searchParams - - // 해양 TOP용 파라미터 파싱 - const search = searchParamsTopCache.parse(searchParams); - const validFilters = getValidFilters(search.filters); - - // 기술영업 해양 TOP RFQ 데이터를 Promise.all로 감싸서 전달 - const promises = Promise.all([ - getTechSalesTopRfqsWithJoin({ - ...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> - <h2 className="text-2xl font-bold tracking-tight"> - 기술영업-해양 TOP RFQ - </h2> - </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" rfqType="TOP" /> - </React.Suspense> - </div> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary/[id]/cbe/page.tsx b/app/[lng]/sales/(sales)/budgetary/[id]/cbe/page.tsx deleted file mode 100644 index 956facd3..00000000 --- a/app/[lng]/sales/(sales)/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]/sales/(sales)/budgetary/[id]/layout.tsx b/app/[lng]/sales/(sales)/budgetary/[id]/layout.tsx deleted file mode 100644 index d58d8363..00000000 --- a/app/[lng]/sales/(sales)/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]/sales/(sales)/budgetary/[id]/page.tsx b/app/[lng]/sales/(sales)/budgetary/[id]/page.tsx deleted file mode 100644 index dd9df563..00000000 --- a/app/[lng]/sales/(sales)/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]/sales/(sales)/budgetary/[id]/tbe/page.tsx b/app/[lng]/sales/(sales)/budgetary/[id]/tbe/page.tsx deleted file mode 100644 index ec894e1c..00000000 --- a/app/[lng]/sales/(sales)/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]/sales/(sales)/budgetary/page.tsx b/app/[lng]/sales/(sales)/budgetary/page.tsx deleted file mode 100644 index 15b4cdd4..00000000 --- a/app/[lng]/sales/(sales)/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]/sales/(sales)/dashboard/page.tsx b/app/[lng]/sales/(sales)/dashboard/page.tsx deleted file mode 100644 index 1d61dc16..00000000 --- a/app/[lng]/sales/(sales)/dashboard/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -// app/invalid-access/page.tsx - -export default function InvalidAccessPage() { - return ( - <main style={{ padding: '40px', textAlign: 'center' }}> - <h1>부적절한 접근입니다</h1> - <p> - 협력업체(Vendor)가 EVCP 화면에 접속하거나 <br /> - SHI 계정이 협력업체 화면에 접속하려고 시도하는 경우입니다. - </p> - <p> - <strong>접근 권한이 없으므로, 다른 화면으로 이동해 주세요.</strong> - </p> - </main> - ); - } -
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/esg-check-list/page.tsx b/app/[lng]/sales/(sales)/esg-check-list/page.tsx deleted file mode 100644 index dd97c74c..00000000 --- a/app/[lng]/sales/(sales)/esg-check-list/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { getEsgEvaluations } from "@/lib/esg-check-list/service" -import { getEsgEvaluationsSchema } from "@/lib/esg-check-list/validation" -import { EsgEvaluationsTable } from "@/lib/esg-check-list/table/esg-table" - - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = getEsgEvaluationsSchema.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getEsgEvaluations({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - ESG 자가진단평가 문항 관리 - </h2> - {/* <p className="text-muted-foreground"> - 협력업체 평가에 사용되는 ESG 자가진단표를 관리{" "} - <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 - /> - } - > - <EsgEvaluationsTable promises={promises} /> - </React.Suspense> - </Shell> - ) -} diff --git a/app/[lng]/sales/(sales)/evaluation-check-list/page.tsx b/app/[lng]/sales/(sales)/evaluation-check-list/page.tsx deleted file mode 100644 index 34409524..00000000 --- a/app/[lng]/sales/(sales)/evaluation-check-list/page.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/* IMPORT */
-import { DataTableSkeleton } from '@/components/data-table/data-table-skeleton';
-import { getRegEvalCriteria } from '@/lib/evaluation-criteria/service';
-import { getValidFilters } from '@/lib/data-table';
-import RegEvalCriteriaTable from '@/lib/evaluation-criteria/table/reg-eval-criteria-table';
-import { searchParamsCache } from '@/lib/evaluation-criteria/validations';
-import { Shell } from '@/components/shell';
-import { Skeleton } from '@/components/ui/skeleton';
-import { Suspense } from 'react';
-import { type SearchParams } from '@/types/table';
-
-// ----------------------------------------------------------------------------------------------------
-
-/* TYPES */
-interface EvaluationCriteriaPageProps {
- searchParams: Promise<SearchParams>
-}
-
-// ----------------------------------------------------------------------------------------------------
-
-/* REGULAR EVALUATION CRITERIA PAGE */
-async function EvaluationCriteriaPage(props: EvaluationCriteriaPageProps) {
- const searchParams = await props.searchParams;
- const search = searchParamsCache.parse(searchParams);
- const validFilters = getValidFilters(search.filters);
- const promises = Promise.all([
- getRegEvalCriteria({
- ...search,
- filters: validFilters,
- }),
- ]);
-
- return (
- <Shell className="gap-2">
- <div className="flex items-center justify-between space-y-2">
- <div className="flex items-center justify-between space-y-2">
- <div>
- <h2 className="text-2xl font-bold tracking-tight">
- 협력업체 평가기준표 관리
- </h2>
- {/* <p className="text-muted-foreground">
- 협력업체 평가에 사용되는 평가기준표를 관리{" "}
- <span className="inline-flex items-center whitespace-nowrap">
- <Ellipsis className="size-3" />
- <span className="ml-1">버튼</span>
- </span>
- 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다.
- </p> */}
- </div>
- </div>
- </div>
-
- <Suspense fallback={<Skeleton className="h-7 w-52" />}>
- {/* <DateRangePicker
- triggerSize="sm"
- triggerClassName="ml-auto w-56 sm:w-60"
- align="end"
- shallow={false}
- /> */}
- </Suspense>
- <Suspense
- fallback={
- <DataTableSkeleton
- columnCount={11}
- searchableColumnCount={1}
- filterableColumnCount={2}
- cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]}
- shrinkZero
- />
- }
- >
- <RegEvalCriteriaTable promises={promises} />
- </Suspense>
- </Shell>
- )
-}
-
-// ----------------------------------------------------------------------------------------------------
-
-/* EXPORT */
-export default EvaluationCriteriaPage;
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/evaluation-target-list/page.tsx b/app/[lng]/sales/(sales)/evaluation-target-list/page.tsx deleted file mode 100644 index 56b8ecef..00000000 --- a/app/[lng]/sales/(sales)/evaluation-target-list/page.tsx +++ /dev/null @@ -1,115 +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 { HelpCircle } from "lucide-react" -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover" -import { Button } from "@/components/ui/button" -import { Badge } from "@/components/ui/badge" - -import { getDefaultEvaluationYear, searchParamsEvaluationTargetsCache } from "@/lib/evaluation-target-list/validation" -import { getEvaluationTargets } from "@/lib/evaluation-target-list/service" -import { EvaluationTargetsTable } from "@/lib/evaluation-target-list/table/evaluation-target-table" - -export const metadata: Metadata = { - title: "협력업체 평가 대상 관리", - description: "협력업체 평가 대상을 확정하고 담당자를 지정합니다.", -} - -interface EvaluationTargetsPageProps { - searchParams: Promise<SearchParams> -} - - - -export default async function EvaluationTargetsPage(props: EvaluationTargetsPageProps) { - const searchParams = await props.searchParams - const search = searchParamsEvaluationTargetsCache.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'; - - // 현재 평가년도 (필터에서 가져오거나 기본값 사용) - const currentEvaluationYear = search.evaluationYear || getDefaultEvaluationYear() - - // Promise.all로 감싸서 전달 - const promises = Promise.all([ - getEvaluationTargets({ - ...search, - filters: allFilters, - joinOperator, - }) - ]) - - 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 className="flex items-center gap-2"> - <h2 className="text-2xl font-bold tracking-tight"> - 협력업체 평가 대상 관리 - </h2> - <Badge variant="outline" className="text-sm"> - {currentEvaluationYear}년도 - </Badge> - - </div> - </div> - </div> - - {/* 메인 테이블 (통계는 테이블 내부로 이동) */} - <React.Suspense - key={JSON.stringify(searchParams)} // URL 파라미터가 변경될 때마다 강제 리렌더링 - fallback={ - <DataTableSkeleton - columnCount={12} - searchableColumnCount={2} - filterableColumnCount={6} - cellWidths={[ - "3rem", // checkbox - "5rem", // 평가년도 - "4rem", // 구분 - "8rem", // 벤더코드 - "12rem", // 벤더명 - "4rem", // 내외자 - "6rem", // 자재구분 - "5rem", // 상태 - "5rem", // 의견일치 - "8rem", // 담당자현황 - "10rem", // 관리자의견 - "8rem" // actions - ]} - shrinkZero - /> - } - > - {currentEvaluationYear && - <EvaluationTargetsTable - promises={promises} - evaluationYear={currentEvaluationYear} - /> -} - </React.Suspense> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/evaluation/page.tsx b/app/[lng]/sales/(sales)/evaluation/page.tsx deleted file mode 100644 index 2d8cbed7..00000000 --- a/app/[lng]/sales/(sales)/evaluation/page.tsx +++ /dev/null @@ -1,181 +0,0 @@ -// ================================================================ -// 4. PERIODIC EVALUATIONS PAGE -// ================================================================ - -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 { HelpCircle } from "lucide-react" -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover" -import { Button } from "@/components/ui/button" -import { Badge } from "@/components/ui/badge" -import { PeriodicEvaluationsTable } from "@/lib/evaluation/table/evaluation-table" -import { getPeriodicEvaluations } from "@/lib/evaluation/service" -import { searchParamsEvaluationsCache } from "@/lib/evaluation/validation" - -export const metadata: Metadata = { - title: "협력업체 정기평가", - description: "협력업체 정기평가 진행 현황을 관리합니다.", -} - -interface PeriodicEvaluationsPageProps { - searchParams: Promise<SearchParams> -} - -// 프로세스 안내 팝오버 컴포넌트 -function ProcessGuidePopover() { - return ( - <Popover> - <PopoverTrigger asChild> - <Button variant="ghost" size="icon" className="h-6 w-6"> - <HelpCircle className="h-4 w-4 text-muted-foreground" /> - </Button> - </PopoverTrigger> - <PopoverContent className="w-96" align="start"> - <div className="space-y-3"> - <div className="space-y-1"> - <h4 className="font-medium">정기평가 프로세스</h4> - {/* <p className="text-sm text-muted-foreground"> - 확정된 평가 대상 업체들에 대한 정기평가 절차입니다. - </p> */} - </div> - <div className="space-y-3 text-sm"> - <div className="flex gap-3"> - <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600"> - 1 - </div> - <div> - <p className="font-medium">평가 대상 확정</p> - <p className="text-muted-foreground">평가 대상으로 확정된 업체들의 정기평가가 자동 생성됩니다.</p> - </div> - </div> - <div className="flex gap-3"> - <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600"> - 2 - </div> - <div> - <p className="font-medium">업체 자료 제출</p> - <p className="text-muted-foreground">각 업체는 평가에 필요한 자료를 제출 마감일까지 제출해야 합니다.</p> - </div> - </div> - <div className="flex gap-3"> - <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600"> - 3 - </div> - <div> - <p className="font-medium">평가자 검토</p> - <p className="text-muted-foreground">지정된 평가자들이 평가표를 기반으로 점수를 매기고 검토합니다.</p> - </div> - </div> - <div className="flex gap-3"> - <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600"> - 4 - </div> - <div> - <p className="font-medium">최종 확정</p> - <p className="text-muted-foreground">모든 평가가 완료되면 최종 점수와 등급이 확정됩니다.</p> - </div> - </div> - </div> - </div> - </PopoverContent> - </Popover> - ) -} - -// TODO: 이 함수들은 실제 서비스 파일에서 구현해야 함 -function getDefaultEvaluationYear() { - return new Date().getFullYear() -} - - - -export default async function PeriodicEvaluationsPage(props: PeriodicEvaluationsPageProps) { - const searchParams = await props.searchParams - const search = searchParamsEvaluationsCache.parse(searchParams) - const validFilters = getValidFilters(search.filters || []) - - // 기본 필터 처리 - let basicFilters = [] - if (search.basicFilters && search.basicFilters.length > 0) { - basicFilters = search.basicFilters - } - - // 모든 필터를 합쳐서 처리 - const allFilters = [...validFilters, ...basicFilters] - - // 조인 연산자 - const joinOperator = search.basicJoinOperator || search.joinOperator || 'and'; - - // 현재 평가년도 - const currentEvaluationYear = search.evaluationYear || getDefaultEvaluationYear() - - // Promise.all로 감싸서 전달 - const promises = Promise.all([ - getPeriodicEvaluations({ - ...search, - filters: allFilters, - joinOperator, - }) - ]) - - 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 className="flex items-center gap-2"> - <h2 className="text-2xl font-bold tracking-tight"> - 협력업체 정기평가 - </h2> - <Badge variant="outline" className="text-sm"> - {currentEvaluationYear}년도 - </Badge> - </div> - </div> - </div> - - {/* 메인 테이블 */} - <React.Suspense - key={JSON.stringify(searchParams)} - fallback={ - <DataTableSkeleton - columnCount={15} - searchableColumnCount={2} - filterableColumnCount={8} - cellWidths={[ - "3rem", // checkbox - "5rem", // 평가년도 - "5rem", // 평가기간 - "4rem", // 구분 - "8rem", // 벤더코드 - "12rem", // 벤더명 - "4rem", // 내외자 - "6rem", // 자재구분 - "5rem", // 문서제출 - "4rem", // 제출일 - "4rem", // 마감일 - "4rem", // 총점 - "4rem", // 등급 - "5rem", // 진행상태 - "8rem" // actions - ]} - shrinkZero - /> - } - > - <PeriodicEvaluationsTable - promises={promises} - evaluationYear={currentEvaluationYear} - /> - </React.Suspense> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/faq/manage/actions.ts b/app/[lng]/sales/(sales)/faq/manage/actions.ts deleted file mode 100644 index bc443a8a..00000000 --- a/app/[lng]/sales/(sales)/faq/manage/actions.ts +++ /dev/null @@ -1,48 +0,0 @@ -'use server';
-
-import { promises as fs } from 'fs';
-import path from 'path';
-import { FaqCategory } from '@/components/faq/FaqCard';
-import { fallbackLng } from '@/i18n/settings';
-
-const FAQ_CONFIG_PATH = path.join(process.cwd(), 'config', 'faqDataConfig.ts');
-
-export async function updateFaqData(lng: string, newData: FaqCategory[]) {
- try {
- const fileContent = await fs.readFile(FAQ_CONFIG_PATH, 'utf-8');
- const dataMatch = fileContent.match(/export const faqCategories[^=]*=\s*(\{[\s\S]*\});/);
- if (!dataMatch) {
- throw new Error('FAQ 데이터 형식이 올바르지 않습니다.');
- }
-
- const allData = eval(`(${dataMatch[1]})`);
- const updatedData = {
- ...allData,
- [lng]: newData
- };
-
- const newFileContent = `import { FaqCategory } from "@/components/faq/FaqCard";\n\ninterface LocalizedFaqCategories {\n [lng: string]: FaqCategory[];\n}\n\nexport const faqCategories: LocalizedFaqCategories = ${JSON.stringify(updatedData, null, 4)};`;
- await fs.writeFile(FAQ_CONFIG_PATH, newFileContent, 'utf-8');
-
- return { success: true };
- } catch (error) {
- console.error('FAQ 데이터 업데이트 중 오류 발생:', error);
- return { success: false, error: '데이터 업데이트 중 오류가 발생했습니다.' };
- }
-}
-
-export async function getFaqData(lng: string): Promise<{ data: FaqCategory[] }> {
- try {
- const fileContent = await fs.readFile(FAQ_CONFIG_PATH, 'utf-8');
- const dataMatch = fileContent.match(/export const faqCategories[^=]*=\s*(\{[\s\S]*\});/);
- if (!dataMatch) {
- throw new Error('FAQ 데이터 형식이 올바르지 않습니다.');
- }
-
- const allData = eval(`(${dataMatch[1]})`);
- return { data: allData[lng] || allData[fallbackLng] || [] };
- } catch (error) {
- console.error('FAQ 데이터 읽기 중 오류 발생:', error);
- return { data: [] };
- }
-}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/faq/manage/page.tsx b/app/[lng]/sales/(sales)/faq/manage/page.tsx deleted file mode 100644 index 011bbfa4..00000000 --- a/app/[lng]/sales/(sales)/faq/manage/page.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { FaqManager } from '@/components/faq/FaqManager';
-import { getFaqData, updateFaqData } from './actions';
-import { revalidatePath } from 'next/cache';
-import { FaqCategory } from '@/components/faq/FaqCard';
-
-interface Props {
- params: {
- lng: string;
- }
-}
-
-export default async function FaqManagePage(props: Props) {
- const resolvedParams = await props.params
- const lng = resolvedParams.lng
- const { data } = await getFaqData(lng);
-
- async function handleSave(newData: FaqCategory[]) {
- 'use server';
- await updateFaqData(lng, newData);
- revalidatePath(`/${lng}/evcp/faq`);
- }
-
- return (
- <div className="container py-6">
- <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow">
- <div className="space-y-6 p-10 pb-16">
- <div className="space-y-0.5">
- <h2 className="text-2xl font-bold tracking-tight">FAQ Management</h2>
- <p className="text-muted-foreground">
- Manage FAQ categories and items for {lng.toUpperCase()} language.
- </p>
- </div>
- <FaqManager initialData={data} onSave={handleSave} lng={lng} />
- </div>
- </section>
- </div>
- );
-}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/faq/page.tsx b/app/[lng]/sales/(sales)/faq/page.tsx deleted file mode 100644 index 00956591..00000000 --- a/app/[lng]/sales/(sales)/faq/page.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Separator } from "@/components/ui/separator"
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
-import { faqCategories } from "@/config/faqDataConfig"
-import { FaqCard } from "@/components/faq/FaqCard"
-import { Button } from "@/components/ui/button"
-import { Settings } from "lucide-react"
-import Link from "next/link"
-import { fallbackLng } from "@/i18n/settings"
-
-interface Props {
- params: {
- lng: string;
- }
-}
-
-export default async function FaqPage(props: Props) {
- const resolvedParams = await props.params
- const lng = resolvedParams.lng
- const localizedFaqCategories = faqCategories[lng] || faqCategories[fallbackLng];
-
- return (
- <div className="container py-6">
- <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow">
- <div className="space-y-6 p-10 pb-16">
- <div className="flex justify-between items-center">
- <div className="space-y-0.5">
- <h2 className="text-2xl font-bold tracking-tight">FAQ</h2>
- {/* <p className="text-muted-foreground">
- Find answers to common questions about using the EVCP system.
- </p> */}
- </div>
- <Link href={`/${lng}/evcp/faq/manage`}>
- <Button variant="outline">
- <Settings className="w-4 h-4 mr-2" />
- FAQ 관리
- </Button>
- </Link>
- </div>
- <Separator className="my-6" />
-
- <Tabs defaultValue={localizedFaqCategories[0]?.label} className="space-y-4">
- <TabsList>
- {localizedFaqCategories.map((category) => (
- <TabsTrigger key={category.label} value={category.label}>
- {category.label}
- </TabsTrigger>
- ))}
- </TabsList>
-
- {localizedFaqCategories.map((category) => (
- <TabsContent key={category.label} value={category.label} className="space-y-4">
- {category.items.map((item, index) => (
- <FaqCard key={index} item={item} />
- ))}
- </TabsContent>
- ))}
- </Tabs>
- </div>
- </section>
- </div>
- )
-}
diff --git a/app/[lng]/sales/(sales)/items-tech/layout.tsx b/app/[lng]/sales/(sales)/items-tech/layout.tsx deleted file mode 100644 index d375059b..00000000 --- a/app/[lng]/sales/(sales)/items-tech/layout.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import * as React from "react"
-import { ItemTechContainer } from "@/components/items-tech/item-tech-container"
-import { Shell } from "@/components/shell"
-import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"
-
-// Layout 컴포넌트는 서버 컴포넌트입니다
-export default function ItemsShipLayout({
- children,
-}: {
- children: React.ReactNode
-}) {
- // 아이템 타입 정의
- const itemTypes = [
- { id: "ship", name: "조선 아이템" },
- { id: "top", name: "해양 TOP" },
- { id: "hull", name: "해양 HULL" },
- ]
-
- return (
- <Shell className="gap-4">
- <React.Suspense
- fallback={
- <DataTableSkeleton
- columnCount={6}
- searchableColumnCount={1}
- filterableColumnCount={2}
- cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]}
- shrinkZero
- />
- }
- >
- <ItemTechContainer itemTypes={itemTypes}>
- {children}
- </ItemTechContainer>
- </React.Suspense>
- </Shell>
- )
-}
diff --git a/app/[lng]/sales/(sales)/items-tech/page.tsx b/app/[lng]/sales/(sales)/items-tech/page.tsx deleted file mode 100644 index 55ac9c63..00000000 --- a/app/[lng]/sales/(sales)/items-tech/page.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import * as React from "react"
-import { type SearchParams } from "@/types/table"
-
-import { getValidFilters } from "@/lib/data-table"
-import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"
-import { shipbuildingSearchParamsCache, offshoreTopSearchParamsCache, offshoreHullSearchParamsCache } from "@/lib/items-tech/validations"
-import { getShipbuildingItems, getOffshoreTopItems, getOffshoreHullItems } from "@/lib/items-tech/service"
-import { OffshoreTopTable } from "@/lib/items-tech/table/top/offshore-top-table"
-import { OffshoreHullTable } from "@/lib/items-tech/table/hull/offshore-hull-table"
-
-// 대소문자 문제 해결 - 실제 파일명에 맞게 import
-import { ItemsShipTable } from "@/lib/items-tech/table/ship/Items-ship-table"
-
-interface IndexPageProps {
- searchParams: Promise<SearchParams>
-}
-
-export default async function IndexPage({ searchParams }: IndexPageProps) {
- const params = await searchParams
- const shipbuildingSearch = shipbuildingSearchParamsCache.parse(params)
- const offshoreTopSearch = offshoreTopSearchParamsCache.parse(params)
- const offshoreHullSearch = offshoreHullSearchParamsCache.parse(params)
- const validShipbuildingFilters = getValidFilters(shipbuildingSearch.filters || [])
- const validOffshoreTopFilters = getValidFilters(offshoreTopSearch.filters || [])
- const validOffshoreHullFilters = getValidFilters(offshoreHullSearch.filters || [])
-
-
- // URL에서 아이템 타입 가져오기
- const itemType = params.type || "ship"
-
- return (
- <div>
- {itemType === "ship" && (
- <ItemsShipTable
- promises={Promise.all([
- getShipbuildingItems({
- ...shipbuildingSearch,
- filters: validShipbuildingFilters,
- }),
- ]).then(([result]) => result)}
- />
- )}
-
- {itemType === "top" && (
- <OffshoreTopTable
- promises={Promise.all([
- getOffshoreTopItems({
- ...offshoreTopSearch,
- filters: validOffshoreTopFilters,
- }),
- ]).then(([result]) => result)}
- />
- )}
-
- {itemType === "hull" && (
- <OffshoreHullTable
- promises={Promise.all([
- getOffshoreHullItems({
- ...offshoreHullSearch,
- filters: validOffshoreHullFilters,
- }),
- ]).then(([result]) => result)}
- />
- )}
- </div>
- )
-}
diff --git a/app/[lng]/sales/(sales)/items/page.tsx b/app/[lng]/sales/(sales)/items/page.tsx deleted file mode 100644 index f8d9a5b1..00000000 --- a/app/[lng]/sales/(sales)/items/page.tsx +++ /dev/null @@ -1,68 +0,0 @@ -// app/items/page.tsx (업데이트) -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 { searchParamsCache } from "@/lib/items/validations" -import { getItems } from "@/lib/items/service" -import { ItemsTable } from "@/lib/items/table/items-table" -import { ViewModeToggle } from "@/components/data-table/view-mode-toggle" - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - // pageSize 기반으로 모드 자동 결정 - const isInfiniteMode = search.perPage >= 1_000_000 - - // 페이지네이션 모드일 때만 서버에서 데이터 가져오기 - // 무한 스크롤 모드에서는 클라이언트에서 SWR로 데이터 로드 - const promises = isInfiniteMode - ? undefined - : Promise.all([ - getItems(search), // searchParamsCache의 결과를 그대로 사용 - ]) - - 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"> - 패키지 넘버 - </h2> - {/* <p className="text-muted-foreground"> - S-EDP로부터 수신된 패키지 정보이며 PR 전 입찰, 견적에 사용되며 벤더 데이터, 문서와 연결됩니다. - </p> */} - </div> - </div> - - </div> - - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - {/* DateRangePicker 등 추가 컴포넌트 */} - </React.Suspense> - - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - {/* 통합된 ItemsTable 컴포넌트 사용 */} - <ItemsTable promises={promises} /> - </React.Suspense> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/layout.tsx b/app/[lng]/sales/(sales)/layout.tsx deleted file mode 100644 index 82b53307..00000000 --- a/app/[lng]/sales/(sales)/layout.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { ReactNode } from 'react'; -import { Header } from '@/components/layout/Header'; -import { SiteFooter } from '@/components/layout/Footer'; - -export default function EvcpLayout({ children }: { children: ReactNode }) { - return ( - <div className="relative flex min-h-svh flex-col bg-background"> - {/* <div className="relative flex min-h-svh flex-col bg-slate-100 "> */} - <Header /> - <main className="flex flex-1 flex-col"> - <div className='container-wrapper'> - {children} - </div> - </main> - <SiteFooter/> - </div> - ); -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/project-gtc/page.tsx b/app/[lng]/sales/(sales)/project-gtc/page.tsx deleted file mode 100644 index d5cb467a..00000000 --- a/app/[lng]/sales/(sales)/project-gtc/page.tsx +++ /dev/null @@ -1,63 +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" - -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> - <h2 className="text-2xl font-bold tracking-tight"> - 프로젝트 GTC 관리 - </h2> - {/* <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]/sales/(sales)/project-vendors/page.tsx b/app/[lng]/sales/(sales)/project-vendors/page.tsx deleted file mode 100644 index 525cff07..00000000 --- a/app/[lng]/sales/(sales)/project-vendors/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { ProjectAVLTable } from "@/lib/project-avl/table/proejctAVL-table" -import { getProjecTAVL } from "@/lib/project-avl/service" -import { searchProjectAVLParamsCache } from "@/lib/project-avl/validations" - - -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> - <h2 className="text-2xl font-bold tracking-tight"> - 프로젝트 AVL 리스트 - </h2> - {/* <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]/sales/(sales)/projects/page.tsx b/app/[lng]/sales/(sales)/projects/page.tsx deleted file mode 100644 index 649dd56f..00000000 --- a/app/[lng]/sales/(sales)/projects/page.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { ItemsTable } from "@/lib/items/table/items-table" -import { getProjectLists } from "@/lib/projects/service" -import { ProjectsTable } from "@/lib/projects/table/projects-table" -import { searchParamsProjectsCache } from "@/lib/projects/validation" - - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsProjectsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getProjectLists({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - 수행 프로젝트 리스트 from S-EDP - </h2> - {/* <p className="text-muted-foreground"> - S-EDP로부터 수신하는 프로젝트 리스트입니다. 향후 MDG로 전환됩니다.{" "} - <span className="inline-flex items-center whitespace-nowrap"> - <Ellipsis className="size-3" /> - <span className="ml-1">버튼</span> - </span> - 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. - </p> */} - </div> - </div> - </div> - - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - {/* <DateRangePicker - triggerSize="sm" - triggerClassName="ml-auto w-56 sm:w-60" - align="end" - shallow={false} - /> */} - </React.Suspense> - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <ProjectsTable promises={promises} /> - </React.Suspense> - </Shell> - ) -} diff --git a/app/[lng]/sales/(sales)/report/page.tsx b/app/[lng]/sales/(sales)/report/page.tsx deleted file mode 100644 index 152721cf..00000000 --- a/app/[lng]/sales/(sales)/report/page.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import * as React from "react"; -import { Skeleton } from "@/components/ui/skeleton"; -import { Shell } from "@/components/shell"; -import { ErrorBoundary } from "@/components/error-boundary"; -import { getDashboardData } from "@/lib/dashboard/service"; -import { DashboardClient } from "@/lib/dashboard/dashboard-client"; - -// 데이터 fetch 시 비동기 함수 호출 후 await 하므로 static-pre-render 과정에서 dynamic-server-error 발생. -// 따라서, dynamic 속성을 force-dynamic 으로 설정하여 동적 렌더링 처리 -// getDashboardData 함수에 대한 Promise를 넘기는 식으로 수정하게 되면 force-dynamic 선언을 제거해도 됨. -export const dynamic = 'force-dynamic' - -export default async function IndexPage() { - // domain을 명시적으로 전달 - const domain = "sales"; - - try { - // 서버에서 직접 데이터 fetch - const dashboardData = await getDashboardData(domain); - - return ( - <Shell className="gap-2"> - <DashboardClient initialData={dashboardData} /> - </Shell> - ); - } catch (error) { - console.error("Dashboard data fetch error:", error); - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-center py-12"> - <div className="text-center space-y-2"> - <p className="text-destructive">데이터를 불러오는데 실패했습니다.</p> - <p className="text-muted-foreground text-sm">{error instanceof Error ? error.message : "알 수 없는 오류가 발생했습니다."}</p> - </div> - </div> - </Shell> - ); - } -} - -function DashboardSkeleton() { - return ( - <div className="space-y-6"> - {/* 헤더 스켈레톤 */} - <div className="flex items-center justify-between"> - <div className="space-y-2"> - <Skeleton className="h-8 w-48" /> - <Skeleton className="h-4 w-72" /> - </div> - <Skeleton className="h-10 w-24" /> - </div> - - {/* 요약 카드 스켈레톤 */} - <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"> - {[...Array(4)].map((_, i) => ( - <div key={i} className="space-y-3 p-6 border rounded-lg"> - <div className="flex items-center justify-between"> - <Skeleton className="h-4 w-16" /> - <Skeleton className="h-4 w-4" /> - </div> - <Skeleton className="h-8 w-12" /> - <Skeleton className="h-3 w-20" /> - </div> - ))} - </div> - - {/* 차트 스켈레톤 */} - <div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> - {[...Array(2)].map((_, i) => ( - <div key={i} className="space-y-4 p-6 border rounded-lg"> - <div className="space-y-2"> - <Skeleton className="h-6 w-32" /> - <Skeleton className="h-4 w-48" /> - </div> - <Skeleton className="h-[300px] w-full" /> - </div> - ))} - </div> - - {/* 탭 스켈레톤 */} - <div className="space-y-4"> - <Skeleton className="h-10 w-64" /> - <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> - {[...Array(6)].map((_, i) => ( - <div key={i} className="space-y-4 p-6 border rounded-lg"> - <Skeleton className="h-6 w-32" /> - <div className="space-y-3"> - <div className="flex justify-between"> - <Skeleton className="h-4 w-16" /> - <Skeleton className="h-4 w-12" /> - </div> - <div className="flex gap-2"> - <Skeleton className="h-6 w-16" /> - <Skeleton className="h-6 w-16" /> - <Skeleton className="h-6 w-16" /> - </div> - <Skeleton className="h-2 w-full" /> - </div> - </div> - ))} - </div> - </div> - </div> - ); -} diff --git a/app/[lng]/sales/(sales)/settings/layout.tsx b/app/[lng]/sales/(sales)/settings/layout.tsx deleted file mode 100644 index 6c380919..00000000 --- a/app/[lng]/sales/(sales)/settings/layout.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { Metadata } from "next" - -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" - -export const metadata: Metadata = { - title: "Settings", - // description: "Advanced form example using react-hook-form and Zod.", -} - - -interface SettingsLayoutProps { - children: React.ReactNode - params: { lng: string } -} - -export default async function SettingsLayout({ - children, - params, -}: { - children: React.ReactNode - params: { lng: string } -}) { - const resolvedParams = await params - const lng = resolvedParams.lng - - - const sidebarNavItems = [ - - { - title: "Account", - href: `/${lng}/evcp/settings`, - }, - { - title: "Preferences", - href: `/${lng}/evcp/settings/preferences`, - } - - - ] - - - return ( - <> - <div className="container py-6"> - <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow"> - <div className="hidden space-y-6 p-10 pb-16 md:block"> - <div className="space-y-0.5"> - <h2 className="text-2xl font-bold tracking-tight">설정</h2> - {/* <p className="text-muted-foreground"> - Manage your account settings and preferences. - </p> */} - </div> - <Separator className="my-6" /> - <div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0"> - <aside className="-mx-4 lg:w-1/5"> - <SidebarNav items={sidebarNavItems} /> - </aside> - <div className="flex-1 ">{children}</div> - </div> - </div> - </section> - </div> - - - </> - ) -} diff --git a/app/[lng]/sales/(sales)/settings/page.tsx b/app/[lng]/sales/(sales)/settings/page.tsx deleted file mode 100644 index eba5e948..00000000 --- a/app/[lng]/sales/(sales)/settings/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { AccountForm } from "@/components/settings/account-form" - -export default function SettingsAccountPage() { - return ( - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium">Account</h3> - {/* <p className="text-sm text-muted-foreground"> - Update your account settings. Set your preferred language and - timezone. - </p> */} - </div> - <Separator /> - <AccountForm /> - </div> - ) -} diff --git a/app/[lng]/sales/(sales)/settings/preferences/page.tsx b/app/[lng]/sales/(sales)/settings/preferences/page.tsx deleted file mode 100644 index e2a88021..00000000 --- a/app/[lng]/sales/(sales)/settings/preferences/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { AppearanceForm } from "@/components/settings/appearance-form" - -export default function SettingsAppearancePage() { - return ( - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium">Preference</h3> - <p className="text-sm text-muted-foreground"> - Customize the preference of the app. - </p> - </div> - <Separator /> - <AppearanceForm /> - </div> - ) -} diff --git a/app/[lng]/sales/(sales)/system/admin-users/page.tsx b/app/[lng]/sales/(sales)/system/admin-users/page.tsx deleted file mode 100644 index 11a9e9fb..00000000 --- a/app/[lng]/sales/(sales)/system/admin-users/page.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { DateRangePicker } from "@/components/date-range-picker" -import { Separator } from "@/components/ui/separator" - -import { searchParamsCache } from "@/lib/admin-users/validations" -import { getAllCompanies, getAllRoles, getUserCountGroupByCompany, getUserCountGroupByRole, getUsers } from "@/lib/admin-users/service" -import { AdmUserTable } from "@/lib/admin-users/table/ausers-table" - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function UserTable(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getUsers({ - ...search, - filters: validFilters, - }), - getUserCountGroupByCompany(), - getUserCountGroupByRole(), - getAllCompanies(), - getAllRoles() - ]) - - return ( - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium">Vendor Admin User Management</h3> - <p className="text-sm text-muted-foreground"> - 협력업체의 유저 전체를 조회하고 어드민 유저를 생성할 수 있는 페이지입니다. 이곳에서 초기 유저를 생성시킬 수 있습니다. <br />생성 후에는 생성된 사용자의 이메일로 생성 통보 이메일이 발송되며 사용자는 이메일과 OTP로 로그인이 가능합니다. - </p> - </div> - <Separator /> - <AdmUserTable promises={promises} /> - </div> - </React.Suspense> - - ) -} diff --git a/app/[lng]/sales/(sales)/system/layout.tsx b/app/[lng]/sales/(sales)/system/layout.tsx deleted file mode 100644 index 2776ed8b..00000000 --- a/app/[lng]/sales/(sales)/system/layout.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Metadata } from "next" - -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" - -export const metadata: Metadata = { - title: "System Setting", - // description: "Advanced form example using react-hook-form and Zod.", -} - - -interface SettingsLayoutProps { - children: React.ReactNode - params: { lng: string } -} - -export default async function SettingsLayout({ - children, - params, -}: { - children: React.ReactNode - params: { lng: string } -}) { - const resolvedParams = await params - const lng = resolvedParams.lng - - - const sidebarNavItems = [ - - { - title: "삼성중공업 사용자", - href: `/${lng}/evcp/system`, - }, - { - title: "Roles", - href: `/${lng}/evcp/system/roles`, - }, - { - title: "권한 통제", - href: `/${lng}/evcp/system/permissions`, - }, - { - title: "협력업체 사용자", - href: `/${lng}/evcp/system/admin-users`, - }, - - { - title: "비밀번호 정책", - href: `/${lng}/evcp/system/password-policy`, - }, - - ] - - - return ( - <> - <div className="container py-6"> - <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow"> - <div className="hidden space-y-6 p-10 pb-16 md:block"> - <div className="space-y-0.5"> - <h2 className="text-2xl font-bold tracking-tight">시스템 설정</h2> - {/* <p className="text-muted-foreground"> - 사용자, 롤, 접근 권한을 관리하세요. - </p> */} - </div> - <Separator className="my-6" /> - <div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0"> - <aside className="-mx-4 lg:w-1/5"> - <SidebarNav items={sidebarNavItems} /> - </aside> - <div className="flex-1 ">{children}</div> - </div> - </div> - </section> - </div> - - - </> - ) -} diff --git a/app/[lng]/sales/(sales)/system/page.tsx b/app/[lng]/sales/(sales)/system/page.tsx deleted file mode 100644 index fe0a262c..00000000 --- a/app/[lng]/sales/(sales)/system/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import * as React from "react" -import { getValidFilters } from "@/lib/data-table" -import { searchParamsCache } from "@/lib/admin-users/validations" -import { getAllRoles, getUsersEVCP } from "@/lib/users/service" -import { getUserCountGroupByRole } from "@/lib/admin-users/service" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { UserTable } from "@/lib/users/table/users-table" - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function SystemUserPage(props: IndexPageProps) { - - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getUsersEVCP({ - ...search, - filters: validFilters, - }), - getUserCountGroupByRole(), - getAllRoles() - ]) - - return ( - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "12rem", "12rem", "12rem"]} - shrinkZero - /> - } - > - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium">SHI Users</h3> - <p className="text-sm text-muted-foreground"> - 시스템 전체 사용자들을 조회하고 관리할 수 있는 페이지입니다. 사용자에게 롤을 할당하는 것으로 메뉴별 권한을 관리할 수 있습니다. - </p> - </div> - <Separator /> - <UserTable promises={promises} /> - </div> - </React.Suspense> - - ) -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/system/password-policy/page.tsx b/app/[lng]/sales/(sales)/system/password-policy/page.tsx deleted file mode 100644 index 0f14fefe..00000000 --- a/app/[lng]/sales/(sales)/system/password-policy/page.tsx +++ /dev/null @@ -1,63 +0,0 @@ -// app/admin/password-policy/page.tsx - -import * as React from "react" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Separator } from "@/components/ui/separator" -import { Alert, AlertDescription } from "@/components/ui/alert" -import { AlertTriangle } from "lucide-react" -import SecuritySettingsTable from "@/components/system/passwordPolicy" -import { getSecuritySettings } from "@/lib/password-policy/service" - - -export default async function PasswordPolicyPage() { - try { - // 보안 설정 데이터 로드 - const securitySettings = await getSecuritySettings() - - return ( - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={4} - searchableColumnCount={0} - filterableColumnCount={0} - cellWidths={["20rem", "30rem", "15rem", "10rem"]} - shrinkZero - /> - } - > - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium">협력업체 사용자 비밀번호 정책 설정</h3> - <p className="text-sm text-muted-foreground"> - 협력업체 사용자들을 위한 비밀번호 정책과 보안 설정을 관리할 수 있습니다. - </p> - </div> - <Separator /> - <SecuritySettingsTable initialSettings={securitySettings} /> - </div> - </React.Suspense> - ) - } catch (error) { - console.error('Failed to load security settings:', error) - - return ( - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium">협력업체 사용자 비밀번호 정책 설정</h3> - <p className="text-sm text-muted-foreground"> - 협력업체 사용자들을 위한 비밀번호 정책과 보안 설정을 관리할 수 있습니다. - </p> - </div> - <Separator /> - <Alert variant="destructive"> - <AlertTriangle className="h-4 w-4" /> - <AlertDescription> - 보안 설정을 불러오는 중 오류가 발생했습니다. 페이지를 새로고침하거나 관리자에게 문의하세요. - </AlertDescription> - </Alert> - </div> - ) - } -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/system/permissions/page.tsx b/app/[lng]/sales/(sales)/system/permissions/page.tsx deleted file mode 100644 index 6aa2b693..00000000 --- a/app/[lng]/sales/(sales)/system/permissions/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import PermissionsTree from "@/components/system/permissionsTree" -import { Separator } from "@/components/ui/separator" - -export default function PermissionsPage() { - return ( - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium">Permissions</h3> - <p className="text-sm text-muted-foreground"> - Set permissions to the menu by Role - </p> - </div> - <Separator /> - <PermissionsTree/> - </div> - ) -} diff --git a/app/[lng]/sales/(sales)/system/roles/page.tsx b/app/[lng]/sales/(sales)/system/roles/page.tsx deleted file mode 100644 index fe074600..00000000 --- a/app/[lng]/sales/(sales)/system/roles/page.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Separator } from "@/components/ui/separator" - -import { searchParamsCache } from "@/lib/roles/validations" -import { searchParamsCache as searchParamsCache2 } from "@/lib/admin-users/validations" -import { RolesTable } from "@/lib/roles/table/roles-table" -import { getRolesWithCount } from "@/lib/roles/services" -import { getUsersAll } from "@/lib/users/service" - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function UserTable(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - const search2 = searchParamsCache2.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getRolesWithCount({ - ...search, - filters: validFilters, - }), - - - ]) - - - const promises2 = Promise.all([ - getUsersAll({ - ...search2, - filters: validFilters, - }, "evcp"), - ]) - - - return ( - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium">Role Management</h3> - <p className="text-sm text-muted-foreground"> - 역할을 생성하고 역할에 유저를 할당할 수 있는 페이지입니다. 역할에 메뉴의 접근 권한 역시 할당할 수 있습니다. - </p> - </div> - <Separator /> - <RolesTable promises={promises} promises2={promises2} /> - </div> - </React.Suspense> - - ) -} diff --git a/app/[lng]/sales/(sales)/tbe/page.tsx b/app/[lng]/sales/(sales)/tbe/page.tsx deleted file mode 100644 index 211cf376..00000000 --- a/app/[lng]/sales/(sales)/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/app/[lng]/sales/(sales)/tech-contact-possible-items/page.tsx b/app/[lng]/sales/(sales)/tech-contact-possible-items/page.tsx deleted file mode 100644 index 5bc36790..00000000 --- a/app/[lng]/sales/(sales)/tech-contact-possible-items/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Suspense } from "react"
-import { SearchParams } from "@/types/table"
-import { Shell } from "@/components/shell"
-import { ContactPossibleItemsTable } from "@/lib/contact-possible-items/table/contact-possible-items-table"
-import { getContactPossibleItems } from "@/lib/contact-possible-items/service"
-import { searchParamsCache } from "@/lib/contact-possible-items/validations"
-import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"
-
-
-interface ContactPossibleItemsPageProps {
- searchParams: Promise<SearchParams>
-}
-
-export default async function ContactPossibleItemsPage({
- searchParams,
-}: ContactPossibleItemsPageProps) {
- const resolvedSearchParams = await searchParams
- const search = searchParamsCache.parse(resolvedSearchParams)
-
- const contactPossibleItemsPromise = getContactPossibleItems(search)
-
- 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">
- 담당자별 자재 관리
- </h2>
- {/* <p className="text-muted-foreground">
- 기술영업 담당자별 자재를 관리합니다.
- </p> */}
- </div>
- </div>
- </div>
-
-
- <Suspense
- fallback={
- <DataTableSkeleton
- columnCount={12}
- searchableColumnCount={2}
- filterableColumnCount={3}
- cellWidths={["10rem", "10rem", "12rem", "8rem", "8rem"]}
- shrinkZero
- />
- }
- >
- <ContactPossibleItemsTable
- contactPossibleItemsPromise={contactPossibleItemsPromise}
- />
- </Suspense>
-
- </Shell>
- )
-}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/tech-project-avl/page.tsx b/app/[lng]/sales/(sales)/tech-project-avl/page.tsx deleted file mode 100644 index 4ce018cd..00000000 --- a/app/[lng]/sales/(sales)/tech-project-avl/page.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import * as React from "react"
-import { redirect } from "next/navigation"
-import { getServerSession } from "next-auth/next"
-import { authOptions } from "@/app/api/auth/[...nextauth]/route"
-import { SearchParams } from "@/types/table"
-import { searchParamsCache } from "@/lib/tech-project-avl/validations"
-import { Skeleton } from "@/components/ui/skeleton"
-import { Shell } from "@/components/shell"
-import { AcceptedQuotationsTable } from "@/lib/tech-project-avl/table/accepted-quotations-table"
-import { getAcceptedTechSalesVendorQuotations } from "@/lib/techsales-rfq/service"
-import { getValidFilters } from "@/lib/data-table"
-import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"
-import { Ellipsis } from "lucide-react"
-import { InformationButton } from "@/components/information/information-button"
-export interface PageProps {
- params: Promise<{ lng: string }>
- searchParams: Promise<SearchParams>
-}
-
-export default async function AcceptedQuotationsPage({
- params,
- searchParams,
-}: PageProps) {
- const { lng } = await params
-
- const session = await getServerSession(authOptions)
- if (!session) {
- redirect(`/${lng}/auth/signin`)
- }
-
- const search = await searchParams
- const { page, perPage, sort, filters, search: searchText } = searchParamsCache.parse(search)
- const validFilters = getValidFilters(filters ?? [])
-
- const { data, pageCount } = await getAcceptedTechSalesVendorQuotations({
- page,
- perPage: perPage ?? 10,
- sort,
- search: searchText,
- 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">
- 견적 Result 전송
- </h2>
- <InformationButton pagePath="evcp/tech-project-avl" />
- </div>
- {/* <p className="text-muted-foreground">
- 기술영업 승인 견적서에 대한 요약 정보를 확인하고{" "}
- <span className="inline-flex items-center whitespace-nowrap">
- <Ellipsis className="size-3" />
- <span className="ml-1">버튼</span>
- </span>
- 을 통해 RFQ 코드, 설명, 업체명, 업체 코드 등의 상세 정보를 확인할 수 있습니다.
- </p> */}
- </div>
- </div>
- </div>
-
- <React.Suspense fallback={<Skeleton className="h-7 w-52" />}>
- {/* Date range picker can be added here if needed */}
- </React.Suspense>
-
- <React.Suspense
- fallback={
- <DataTableSkeleton
- columnCount={12}
- searchableColumnCount={2}
- filterableColumnCount={4}
- cellWidths={["10rem", "15rem", "12rem", "10rem", "10rem", "12rem", "8rem", "12rem", "10rem", "8rem", "10rem", "10rem"]}
- shrinkZero
- />
- }
- >
- <AcceptedQuotationsTable
- data={data}
- pageCount={pageCount}
- />
- </React.Suspense>
- </Shell>
- )
-}
diff --git a/app/[lng]/sales/(sales)/tech-vendors/[id]/info/layout.tsx b/app/[lng]/sales/(sales)/tech-vendors/[id]/info/layout.tsx deleted file mode 100644 index 291cd630..00000000 --- a/app/[lng]/sales/(sales)/tech-vendors/[id]/info/layout.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { Metadata } from "next"
-
-import { Separator } from "@/components/ui/separator"
-import { SidebarNav } from "@/components/layout/sidebar-nav"
-import { findTechVendorById } from "@/lib/tech-vendors/service"
-import { TechVendor } from "@/db/schema/techVendors"
-import { Button } from "@/components/ui/button"
-import { ArrowLeft } from "lucide-react"
-import Link from "next/link"
-export const metadata: Metadata = {
- title: "Tech Vendor Detail",
-}
-
-export default async function SettingsLayout({
- children,
- params,
-}: {
- children: React.ReactNode
- params: { lng: string , id: string}
-}) {
-
- // 1) URL 파라미터에서 id 추출, Number로 변환
- const resolvedParams = await params
- const lng = resolvedParams.lng
- const id = resolvedParams.id
-
- const idAsNumber = Number(id)
- // 2) DB에서 해당 협력업체 정보 조회
- const vendor: TechVendor | null = await findTechVendorById(idAsNumber)
-
- // 3) 사이드바 메뉴
- const sidebarNavItems = [
- {
- title: "연락처",
- href: `/${lng}/evcp/tech-vendors/${id}/info`,
- },
- {
- title: "RFQ 히스토리",
- href: `/${lng}/evcp/tech-vendors/${id}/info/rfq-history`,
- },
- {
- title: "자재 리스트",
- href: `/${lng}/evcp/tech-vendors/${id}/info/possible-items`,
- },
- ]
-
- 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/tech-vendors`} 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>기술영업 벤더 목록으로 돌아가기</span>
- </Button>
- </Link>
- </div>
- <div className="space-y-0.5">
- {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */}
- <h2 className="text-2xl font-bold tracking-tight">
- {vendor
- ? `${vendor.vendorCode ?? ""} - ${vendor.vendorName} 상세 정보`
- : "Loading Vendor..."}
- </h2>
- <p className="text-muted-foreground">기술영업 벤더 관련 상세사항을 확인하세요.</p>
- </div>
- <Separator className="my-6" />
- <div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0">
- <aside className="-mx-4 lg:w-1/5">
- <SidebarNav items={sidebarNavItems} />
- </aside>
- <div className="flex-1">{children}</div>
- </div>
- </div>
- </section>
- </div>
- </>
- )
-}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/tech-vendors/[id]/info/page.tsx b/app/[lng]/sales/(sales)/tech-vendors/[id]/info/page.tsx deleted file mode 100644 index 9969a801..00000000 --- a/app/[lng]/sales/(sales)/tech-vendors/[id]/info/page.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Separator } from "@/components/ui/separator"
-import { getTechVendorContacts } from "@/lib/tech-vendors/service"
-import { type SearchParams } from "@/types/table"
-import { getValidFilters } from "@/lib/data-table"
-import { searchParamsContactCache } from "@/lib/tech-vendors/validations"
-import { TechVendorContactsTable } from "@/lib/tech-vendors/contacts-table/contact-table"
-
-interface IndexPageProps {
- // Next.js 13 App Router에서 기본으로 주어지는 객체들
- params: {
- lng: string
- id: string
- }
- searchParams: Promise<SearchParams>
-}
-
-export default async function SettingsAccountPage(props: IndexPageProps) {
- const resolvedParams = await props.params
- const id = resolvedParams.id
-
- const idAsNumber = Number(id)
-
- // 2) SearchParams 파싱 (Zod)
- // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼
- const searchParams = await props.searchParams
- const search = searchParamsContactCache.parse(searchParams)
- const validFilters = getValidFilters(search.filters)
-
-
-
- const promises = Promise.all([
- getTechVendorContacts({
- ...search,
- filters: validFilters,
- },
- idAsNumber)
- ])
- // 4) 렌더링
- return (
- <div className="space-y-6">
- <div>
- <h3 className="text-lg font-medium">
- Contacts
- </h3>
- <p className="text-sm text-muted-foreground">
- 업무별 담당자 정보를 확인하세요.
- </p>
- </div>
- <Separator />
- <div>
- <TechVendorContactsTable promises={promises} vendorId={idAsNumber}/>
- </div>
- </div>
- )
-}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/tech-vendors/[id]/info/possible-items/page.tsx b/app/[lng]/sales/(sales)/tech-vendors/[id]/info/possible-items/page.tsx deleted file mode 100644 index 642c6e32..00000000 --- a/app/[lng]/sales/(sales)/tech-vendors/[id]/info/possible-items/page.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { Separator } from "@/components/ui/separator"
-import { getTechVendorPossibleItems } from "@/lib/tech-vendors/service"
-import { type SearchParams } from "@/types/table"
-import { getValidFilters } from "@/lib/data-table"
-import { searchParamsPossibleItemsCache } from "@/lib/tech-vendors/validations"
-import { TechVendorPossibleItemsTable } from "@/lib/tech-vendors/possible-items/possible-items-table"
-
-interface IndexPageProps {
- // Next.js 13 App Router에서 기본으로 주어지는 객체들
- params: Promise<{
- lng: string
- id: string
- }>
- searchParams: Promise<SearchParams>
-}
-
-export default async function TechVendorPossibleItemsPage(props: IndexPageProps) {
- const resolvedParams = await props.params
- const id = resolvedParams.id
-
- const idAsNumber = Number(id)
- console.log(idAsNumber)
-
- // 2) SearchParams 파싱 (Zod)
- // - "filters", "page", "perPage", "sort" 등 possible items 전용 컬럼
- const searchParams = await props.searchParams
- const search = searchParamsPossibleItemsCache.parse(searchParams)
- const validFilters = getValidFilters(search.filters)
-
- const promises = Promise.all([
- getTechVendorPossibleItems({
- ...search,
- filters: validFilters,
- }, idAsNumber)
- ])
-
- // 4) 렌더링
- return (
- <div className="space-y-6">
- <div>
- <h3 className="text-lg font-medium">
- 공급가능 아이템 목록
- </h3>
- <p className="text-sm text-muted-foreground">
- 해당 벤더가 공급 가능한 아이템 목록을 확인할 수 있습니다.
- </p>
- </div>
- <Separator />
- <div>
- <TechVendorPossibleItemsTable promises={promises} vendorId={idAsNumber} />
- </div>
- </div>
- )
-}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/tech-vendors/[id]/info/rfq-history/page.tsx b/app/[lng]/sales/(sales)/tech-vendors/[id]/info/rfq-history/page.tsx deleted file mode 100644 index 9122d524..00000000 --- a/app/[lng]/sales/(sales)/tech-vendors/[id]/info/rfq-history/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { type SearchParams } from "@/types/table"
-import { getValidFilters } from "@/lib/data-table"
-import { TechVendorRfqHistoryTable } from "@/lib/tech-vendors/rfq-history-table/tech-vendor-rfq-history-table"
-import { getTechVendorRfqHistory } from "@/lib/tech-vendors/service"
-import { searchParamsRfqHistoryCache } from "@/lib/tech-vendors/validations"
-import { Separator } from "@/components/ui/separator"
-
-interface IndexPageProps {
- // Next.js 13 App Router에서 기본으로 주어지는 객체들
- params: {
- lng: string
- id: string
- }
- searchParams: Promise<SearchParams>
-}
-
-export default async function SettingsAccountPage(props: IndexPageProps) {
- const resolvedParams = await props.params
- const id = resolvedParams.id
-
- const idAsNumber = Number(id)
-
- // 2) SearchParams 파싱 (Zod)
- // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼
- const searchParams = await props.searchParams
- const search = searchParamsRfqHistoryCache.parse(searchParams)
- const validFilters = getValidFilters(search.filters)
-
-
-
- const promises = Promise.all([
- getTechVendorRfqHistory({
- ...search,
- filters: validFilters,
- },
- idAsNumber)
- ])
-
- return (
- <div className="space-y-6">
- <div>
- <h3 className="text-lg font-medium">
- RFQ 히스토리
- </h3>
- <p className="text-sm text-muted-foreground">
- 벤더가 참여한 기술영업 RFQ 목록입니다.
- </p>
- </div>
- <Separator />
- <div>
- <TechVendorRfqHistoryTable promises={promises} />
- </div>
- </div>
-
- )
-}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/tech-vendors/page.tsx b/app/[lng]/sales/(sales)/tech-vendors/page.tsx deleted file mode 100644 index e49ba79e..00000000 --- a/app/[lng]/sales/(sales)/tech-vendors/page.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import * as React from "react"
-import { type SearchParams } from "@/types/table"
-
-import { getValidFilters } from "@/lib/data-table"
-import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"
-import { Shell } from "@/components/shell"
-
-import { searchParamsCache } from "@/lib/tech-vendors/validations"
-import { getTechVendors, getTechVendorStatusCounts } from "@/lib/tech-vendors/service"
-import { TechVendorsTable } from "@/lib/tech-vendors/table/tech-vendors-table"
-
-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([
- getTechVendors({
- ...search,
- filters: validFilters,
- }),
- getTechVendorStatusCounts(),
- ])
-
- return (
- <Shell className="gap-4">
- <div className="flex items-center justify-between">
- {/* 왼쪽: 타이틀 & 설명 */}
- <div>
- <div className="flex items-center gap-2">
- <h2 className="text-2xl font-bold tracking-tight">기술영업 협력업체 관리</h2>
- {/* InformationButton은 필요시 추가 */}
- {/* <InformationButton pagePath="evcp/tech-vendors" /> */}
- </div>
- {/* <p className="text-muted-foreground">
- 기술영업 벤더에 대한 요약 정보를 확인하고 관리할 수 있습니다.
- </p> */}
- </div>
- </div>
- <React.Suspense
- fallback={
- <DataTableSkeleton
- columnCount={6}
- searchableColumnCount={1}
- filterableColumnCount={2}
- cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]}
- shrinkZero
- />
- }
- >
- <TechVendorsTable promises={promises} />
- </React.Suspense>
- </Shell>
- )
-}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/vendor-candidates/page.tsx b/app/[lng]/sales/(sales)/vendor-candidates/page.tsx deleted file mode 100644 index f4bee95b..00000000 --- a/app/[lng]/sales/(sales)/vendor-candidates/page.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" - -import { getVendorCandidateCounts, getVendorCandidates } from "@/lib/vendor-candidates/service" -import { searchParamsCandidateCache } from "@/lib/vendor-candidates/validations" -import { VendorCandidateTable } from "@/lib/vendor-candidates/table/candidates-table" -import { DateRangePicker } from "@/components/date-range-picker" - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCandidateCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getVendorCandidates({ - ...search, - filters: validFilters, - }), - getVendorCandidateCounts() - ]) - - return ( - <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"> - 발굴업체 등록 관리 - </h2> - {/* <p className="text-muted-foreground"> - 수집한 협력업체 후보를 등록하고 초대 메일을 송부할 수 있습니다. - </p> */} - </div> - </div> - </div> - - {/* 수집일 라벨과 DateRangePicker를 함께 배치 */} - <div className="flex items-center justify-start gap-2"> - {/* <span className="text-sm font-medium">수집일 기간 설정: </span> */} - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - <DateRangePicker - triggerSize="sm" - triggerClassName="w-56 sm:w-60" - align="end" - shallow={false} - showClearButton={true} - placeholder="수집일 날짜 범위를 고르세요" - /> - </React.Suspense> - </div> - - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <VendorCandidateTable promises={promises}/> - </React.Suspense> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/sales/page.tsx b/app/[lng]/sales/page.tsx deleted file mode 100644 index f9662cb7..00000000 --- a/app/[lng]/sales/page.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Metadata } from "next" -import { Suspense } from "react" -import { LoginFormSkeleton } from "@/components/login/login-form-skeleton" -import { LoginFormSHI } from "@/components/login/login-form-shi" - -export const metadata: Metadata = { - title: "eVCP Portal", - description: "", -} - -export default function AuthenticationPage() { - - - return ( - <> - <Suspense fallback={<LoginFormSkeleton/>}> - <LoginFormSHI /> - </Suspense> - </> - ) -} diff --git a/app/[lng]/test/table/page.tsx b/app/[lng]/test/table/page.tsx new file mode 100644 index 00000000..88d050fc --- /dev/null +++ b/app/[lng]/test/table/page.tsx @@ -0,0 +1,168 @@ +"use client" + +import * as React from "react" +import { ColumnDef } from "@tanstack/react-table" +import { ClientVirtualTable } from "@/components/client-table/client-virtual-table" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" + +// 1. Define the data type +type TestData = { + id: string + name: string + email: string + role: "Admin" | "User" | "Guest" + status: "Active" | "Inactive" | "Pending" + lastLogin: string + amount: number +} + +// 2. Generate dummy data +const generateData = (count: number): TestData[] => { + const roles: TestData["role"][] = ["Admin", "User", "Guest"] + const statuses: TestData["status"][] = ["Active", "Inactive", "Pending"] + + return Array.from({ length: count }).map((_, i) => ({ + id: `ID-${i + 1}`, + name: `User ${i + 1}`, + email: `user${i + 1}@example.com`, + role: roles[Math.floor(Math.random() * roles.length)], + status: statuses[Math.floor(Math.random() * statuses.length)], + lastLogin: new Date(Date.now() - Math.floor(Math.random() * 10000000000)).toISOString().split('T')[0], + amount: Math.floor(Math.random() * 10000), + })) +} + +export default function TestTablePage() { + // State for data + const [data, setData] = React.useState<TestData[]>([]) + const [isLoading, setIsLoading] = React.useState(true) + + // Load data on mount + React.useEffect(() => { + const timer = setTimeout(() => { + setData(generateData(100000)) // Generate 1000 rows + setIsLoading(false) + }, 500) + return () => clearTimeout(timer) + }, []) + + // 3. Define columns + const columns: ColumnDef<TestData>[] = [ + { + accessorKey: "id", + header: "ID", + size: 80, + }, + { + accessorKey: "name", + header: "Name", + size: 150, + }, + { + accessorKey: "email", + header: "Email", + size: 200, + }, + { + accessorKey: "role", + header: "Role", + size: 100, + cell: ({ getValue }) => { + const role = getValue() as string + return ( + <Badge variant={role === "Admin" ? "default" : "secondary"}> + {role} + </Badge> + ) + } + }, + { + accessorKey: "status", + header: "Status", + size: 100, + cell: ({ getValue }) => { + const status = getValue() as string + let color = "bg-gray-500" + if (status === "Active") color = "bg-green-500" + if (status === "Inactive") color = "bg-red-500" + if (status === "Pending") color = "bg-yellow-500" + + return ( + <div className="flex items-center gap-2"> + <div className={`w-2 h-2 rounded-full ${color}`} /> + <span>{status}</span> + </div> + ) + } + }, + { + accessorKey: "amount", + header: "Amount", + size: 200, + cell: ({ getValue }) => { + const amount = getValue() as number + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(amount) + }, + meta: { + align: "right" + } + }, + { + accessorKey: "lastLogin", + header: "Last Login", + size: 120, + }, + { + id: "actions", + header: "Actions", + size: 100, + cell: () => ( + <Button variant="ghost" size="sm">Edit</Button> + ), + enablePinning: true, + } + ] + + return ( + <div className="h-full flex flex-col p-6 space-y-4"> + <div className="flex justify-between items-center"> + <div> + <h1 className="text-2xl font-bold tracking-tight">Virtual Table Test</h1> + <p className="text-muted-foreground"> + Testing the ClientVirtualTable component with 1000 generated rows. + </p> + </div> + <div className="flex gap-2"> + <Button onClick={() => { + setIsLoading(true) + setTimeout(() => { + setData(generateData(5000)) + setIsLoading(false) + }, 500) + }}> + Reload 5k Rows + </Button> + </div> + </div> + + <div className="border rounded-lg overflow-auto h-[1000px]"> + <ClientVirtualTable + data={data} + columns={columns} + height="100%" + isLoading={isLoading} + enablePagination={true} + enableRowSelection={true} + enableGrouping={true} + onRowClick={(row) => console.log("Row clicked:", row.original)} + enableUserPreset={true} + tableKey="test-table" + /> + </div> + </div> + ) +} diff --git a/app/api/procurement-rfqs/[rfqId]/vendors/[vendorId]/comments/route.ts b/app/api/procurement-rfqs/[rfqId]/vendors/[vendorId]/comments/route.ts deleted file mode 100644 index 51430118..00000000 --- a/app/api/procurement-rfqs/[rfqId]/vendors/[vendorId]/comments/route.ts +++ /dev/null @@ -1,145 +0,0 @@ -// app/api/procurement-rfqs/[rfqId]/vendors/[vendorId]/comments/route.ts -import { NextRequest, NextResponse } from "next/server" - -import db from '@/db/db'; -import { getServerSession } from "next-auth/next" -import { authOptions } from "@/app/api/auth/[...nextauth]/route" - -import { procurementRfqComments, procurementRfqAttachments } from "@/db/schema" -import { revalidateTag } from "next/cache" - -// 파일 저장을 위한 유틸리티 -import { writeFile, mkdir } from 'fs/promises' -import { join } from 'path' -import crypto from 'crypto' - -/** - * 코멘트 생성 API 엔드포인트 - */ -export async function POST( - request: NextRequest, - { params }: { params: { rfqId: string; vendorId: string } } -) { - try { - // 인증 확인 - const session = await getServerSession(authOptions); - if (!session?.user) { - return NextResponse.json( - { success: false, message: "인증이 필요합니다" }, - { status: 401 } - ) - } - - const rfqId = parseInt(params.rfqId) - const vendorId = parseInt(params.vendorId) - - // 유효성 검사 - if (isNaN(rfqId) || isNaN(vendorId)) { - return NextResponse.json( - { success: false, message: "유효하지 않은 매개변수입니다" }, - { status: 400 } - ) - } - - // FormData 파싱 - const formData = await request.formData() - const content = formData.get("content") as string - const isVendorComment = formData.get("isVendorComment") === "true" - const files = formData.getAll("attachments") as File[] - - if (!content && files.length === 0) { - return NextResponse.json( - { success: false, message: "내용이나 첨부파일이 필요합니다" }, - { status: 400 } - ) - } - - // 코멘트 생성 - const [comment] = await db - .insert(procurementRfqComments) - .values({ - rfqId, - vendorId, - userId: parseInt(session.user.id), - content, - isVendorComment, - isRead: !isVendorComment, // 본인 메시지는 읽음 처리 - createdAt: new Date(), - updatedAt: new Date(), - }) - .returning() - - // 첨부파일 처리 - const attachments = [] - if (files.length > 0) { - // 디렉토리 생성 - const uploadDir = join(process.cwd(), "public", `rfq-${rfqId}`, `vendor-${vendorId}`, `comment-${comment.id}`) - await mkdir(uploadDir, { recursive: true }) - - // 각 파일 저장 - for (const file of files) { - const buffer = Buffer.from(await file.arrayBuffer()) - const filename = `${Date.now()}-${crypto.randomBytes(8).toString("hex")}-${file.name.replace(/[^a-zA-Z0-9.-]/g, "_")}` - const filePath = join(uploadDir, filename) - - // 파일 쓰기 - await writeFile(filePath, buffer) - - // DB에 첨부파일 정보 저장 - const [attachment] = await db - .insert(procurementRfqAttachments) - .values({ - rfqId, - commentId: comment.id, - fileName: file.name, - fileSize: file.size, - fileType: file.type, - filePath: `/rfq-${rfqId}/vendor-${vendorId}/comment-${comment.id}/${filename}`, - isVendorUpload: isVendorComment, - uploadedBy: parseInt(session.user.id), - vendorId, - uploadedAt: new Date(), - }) - .returning() - - attachments.push({ - id: attachment.id, - fileName: attachment.fileName, - fileSize: attachment.fileSize, - fileType: attachment.fileType, - filePath: attachment.filePath, - uploadedAt: attachment.uploadedAt - }) - } - } - - // 캐시 무효화 - revalidateTag(`rfq-${rfqId}-comments`) - - // 응답 데이터 구성 - const responseData = { - id: comment.id, - rfqId: comment.rfqId, - vendorId: comment.vendorId, - userId: comment.userId, - content: comment.content, - isVendorComment: comment.isVendorComment, - createdAt: comment.createdAt, - updatedAt: comment.updatedAt, - userName: session.user.name, - attachments, - isRead: comment.isRead - } - - return NextResponse.json({ - success: true, - data: { comment: responseData } - }) - } catch (error) { - console.error("코멘트 생성 오류:", error) - return NextResponse.json( - { success: false, message: "코멘트 생성 중 오류가 발생했습니다" }, - { status: 500 } - ) - } -}
\ No newline at end of file diff --git a/app/api/rfq-attachments/download/route.ts b/app/api/rfq-attachments/download/route.ts deleted file mode 100644 index 5a07bc0b..00000000 --- a/app/api/rfq-attachments/download/route.ts +++ /dev/null @@ -1,474 +0,0 @@ -// app/api/rfq-attachments/download/route.ts -import { NextRequest, NextResponse } from 'next/server'; -import { readFile, access, constants, stat } from 'fs/promises'; -import { join, normalize, resolve } from 'path'; -import db from '@/db/db'; -import { bRfqAttachmentRevisions, vendorResponseAttachmentsB } from '@/db/schema'; -import { eq } from 'drizzle-orm'; -import { getServerSession } from 'next-auth'; -import { authOptions } from '@/app/api/auth/[...nextauth]/route'; -import { createFileDownloadLog } from '@/lib/file-download-log/service'; -import rateLimit from '@/lib/rate-limit'; -import { z } from 'zod'; -import { getRequestInfo } from '@/lib/network/get-client-ip'; - -// 허용된 파일 확장자 -const ALLOWED_EXTENSIONS = new Set([ - 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', - 'txt', 'csv', 'png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg', - 'dwg', 'dxf', 'zip', 'rar', '7z' -]); - -// 최대 파일 크기 (50MB) -const MAX_FILE_SIZE = 50 * 1024 * 1024; - -// 다운로드 요청 검증 스키마 -const downloadRequestSchema = z.object({ - path: z.string().min(1, 'File path is required'), - type: z.enum(['client', 'vendor']).optional(), - revisionId: z.string().optional(), - responseFileId: z.string().optional(), -}); - -// 파일 정보 타입 -interface FileRecord { - id: number; - fileName: string; - originalFileName?: string; - filePath: string; - fileSize: number; - fileType?: string; -} - -// 강화된 파일 경로 검증 함수 -function validateFilePath(filePath: string): boolean { - // null, undefined, 빈 문자열 체크 - if (!filePath || typeof filePath !== 'string') { - return false; - } - - // 위험한 패턴 체크 - const dangerousPatterns = [ - /\.\./, // 상위 디렉토리 접근 - /\/\//, // 이중 슬래시 - /[<>:"'|?*]/, // 특수문자 - /[\x00-\x1f]/, // 제어문자 - /\\+/ // 백슬래시 - ]; - - if (dangerousPatterns.some(pattern => pattern.test(filePath))) { - return false; - } - - // 시스템 파일 접근 방지 - const dangerousPaths = ['/etc', '/proc', '/sys', '/var', '/usr', '/root', '/home']; - for (const dangerousPath of dangerousPaths) { - if (filePath.toLowerCase().startsWith(dangerousPath)) { - return false; - } - } - - return true; -} - -// 파일 확장자 검증 -function validateFileExtension(fileName: string): boolean { - const extension = fileName.split('.').pop()?.toLowerCase() || ''; - return ALLOWED_EXTENSIONS.has(extension); -} - -// 안전한 파일명 생성 -function sanitizeFileName(fileName: string): string { - return fileName - .replace(/[^\w\s.-]/g, '_') // 안전하지 않은 문자 제거 - .replace(/\s+/g, '_') // 공백을 언더스코어로 - .substring(0, 255); // 파일명 길이 제한 -} - -export async function GET(request: NextRequest) { - const startTime = Date.now(); - const requestInfo = getRequestInfo(request); - let fileRecord: FileRecord | null = null; - - try { - // Rate limiting 체크 - const limiterResult = await rateLimit(request); - if (!limiterResult.success) { - console.warn('🚨 Rate limit 초과:', { - ip: requestInfo.ip, - userAgent: requestInfo.userAgent - }); - - return NextResponse.json( - { error: "Too many requests" }, - { status: 429 } - ); - } - - // 세션 확인 - const session = await getServerSession(authOptions); - if (!session?.user) { - console.warn('🚨 인증되지 않은 다운로드 시도:', { - ip: requestInfo.ip, - userAgent: requestInfo.userAgent, - path: request.nextUrl.searchParams.get("path") - }); - - return NextResponse.json( - { error: "Unauthorized" }, - { status: 401 } - ); - } - - // 파라미터 검증 - const searchParams = { - path: request.nextUrl.searchParams.get("path"), - type: request.nextUrl.searchParams.get("type"), - revisionId: request.nextUrl.searchParams.get("revisionId"), - responseFileId: request.nextUrl.searchParams.get("responseFileId"), - }; - - const validatedParams = downloadRequestSchema.parse(searchParams); - const { path, type, revisionId, responseFileId } = validatedParams; - - // 파일 경로 보안 검증 - if (!validateFilePath(path)) { - console.warn(`🚨 의심스러운 파일 경로 접근 시도: ${path}`, { - userId: session.user.id, - ip: requestInfo.ip, - userAgent: requestInfo.userAgent - }); - - return NextResponse.json( - { error: "Invalid file path" }, - { status: 400 } - ); - } - - // 경로 정규화 - const normalizedPath = normalize(path.replace(/^\/+/, "")); - - // DB에서 파일 정보 조회 - let dbRecord: FileRecord | null = null; - - if (type === "client" && revisionId) { - // 발주처 첨부파일 리비전 - const [record] = await db - .select({ - id: bRfqAttachmentRevisions.id, - fileName: bRfqAttachmentRevisions.fileName, - originalFileName: bRfqAttachmentRevisions.originalFileName, - filePath: bRfqAttachmentRevisions.filePath, - fileSize: bRfqAttachmentRevisions.fileSize, - fileType: bRfqAttachmentRevisions.fileType, - }) - .from(bRfqAttachmentRevisions) - .where(eq(bRfqAttachmentRevisions.id, Number(revisionId))); - - dbRecord = record; - - } else if (type === "vendor" && responseFileId) { - // 벤더 응답 파일 - const [record] = await db - .select({ - id: vendorResponseAttachmentsB.id, - fileName: vendorResponseAttachmentsB.fileName, - originalFileName: vendorResponseAttachmentsB.originalFileName, - filePath: vendorResponseAttachmentsB.filePath, - fileSize: vendorResponseAttachmentsB.fileSize, - fileType: vendorResponseAttachmentsB.fileType, - }) - .from(vendorResponseAttachmentsB) - .where(eq(vendorResponseAttachmentsB.id, Number(responseFileId))); - - dbRecord = record; - - } else { - // filePath로 직접 검색 (fallback) - 정규화된 경로로 검색 - const [clientRecord] = await db - .select({ - id: bRfqAttachmentRevisions.id, - fileName: bRfqAttachmentRevisions.fileName, - originalFileName: bRfqAttachmentRevisions.originalFileName, - filePath: bRfqAttachmentRevisions.filePath, - fileSize: bRfqAttachmentRevisions.fileSize, - fileType: bRfqAttachmentRevisions.fileType, - }) - .from(bRfqAttachmentRevisions) - .where(eq(bRfqAttachmentRevisions.filePath, normalizedPath)); - - if (clientRecord) { - dbRecord = clientRecord; - } else { - // 벤더 파일에서도 검색 - const [vendorRecord] = await db - .select({ - id: vendorResponseAttachmentsB.id, - fileName: vendorResponseAttachmentsB.fileName, - originalFileName: vendorResponseAttachmentsB.originalFileName, - filePath: vendorResponseAttachmentsB.filePath, - fileSize: vendorResponseAttachmentsB.fileSize, - fileType: vendorResponseAttachmentsB.fileType, - }) - .from(vendorResponseAttachmentsB) - .where(eq(vendorResponseAttachmentsB.filePath, normalizedPath)); - - dbRecord = vendorRecord; - } - } - - // DB에서 파일 정보를 찾지 못한 경우 - if (!dbRecord) { - console.warn("⚠️ DB에서 파일 정보를 찾지 못함:", { - path, - normalizedPath, - userId: session.user.id, - ip: requestInfo.ip - }); - - return NextResponse.json( - { error: "File not found in database" }, - { status: 404 } - ); - } - - fileRecord = dbRecord; - - // 파일명 설정 - const fileName = dbRecord.originalFileName || dbRecord.fileName; - - // 파일 확장자 검증 - if (!validateFileExtension(fileName)) { - console.warn(`🚨 허용되지 않은 파일 타입 다운로드 시도: ${fileName}`, { - userId: session.user.id, - ip: requestInfo.ip - }); - - // 실패 로그 기록 - await createFileDownloadLog({ - fileId: dbRecord.id, - success: false, - errorMessage: 'File type not allowed', - requestId: requestInfo.requestId, - fileInfo: { - fileName, - filePath: path, - fileSize: 0, - } - }); - - return NextResponse.json( - { error: "File type not allowed" }, - { status: 403 } - ); - } - - // 안전한 파일 경로 구성 - const allowedDirs = ["public", "uploads", "storage"]; - let actualPath: string | null = null; - let baseDir: string | null = null; - - // 각 허용된 디렉터리에서 파일 찾기 - for (const dir of allowedDirs) { - baseDir = resolve(process.cwd(), dir); - const testPath = resolve(baseDir, normalizedPath); - - // 경로 탐색 공격 방지 - 허용된 디렉터리 외부 접근 차단 - if (!testPath.startsWith(baseDir)) { - continue; - } - - try { - await access(testPath, constants.R_OK); - actualPath = testPath; - console.log("✅ 파일 발견:", testPath); - break; - } catch (err) { - // 조용히 다음 디렉터리 시도 - } - } - - if (!actualPath || !baseDir) { - console.error("❌ 모든 경로에서 파일을 찾을 수 없음:", { - normalizedPath, - userId: session.user.id, - requestedPath: path - }); - - // 실패 로그 기록 - await createFileDownloadLog({ - fileId: dbRecord.id, - success: false, - errorMessage: 'File not found on server', - requestId: requestInfo.requestId, - fileInfo: { - fileName, - filePath: path, - fileSize: dbRecord.fileSize || 0, - } - }); - - return NextResponse.json( - { error: "File not found on server" }, - { status: 404 } - ); - } - - // 파일 크기 확인 - const stats = await stat(actualPath); - if (stats.size > MAX_FILE_SIZE) { - console.warn(`🚨 파일 크기 초과: ${fileName} (${stats.size} bytes)`, { - userId: session.user.id, - ip: requestInfo.ip - }); - - // 실패 로그 기록 - await createFileDownloadLog({ - fileId: dbRecord.id, - success: false, - errorMessage: 'File too large', - requestId: requestInfo.requestId, - fileInfo: { - fileName, - filePath: path, - fileSize: stats.size, - } - }); - - return NextResponse.json( - { error: "File too large" }, - { status: 413 } - ); - } - - // 파일 읽기 - const fileBuffer = await readFile(actualPath); - - // MIME 타입 결정 - const fileExtension = fileName.split('.').pop()?.toLowerCase() || ''; - let contentType = dbRecord.fileType || 'application/octet-stream'; - - // 확장자에 따른 MIME 타입 매핑 (fallback) - if (!contentType || contentType === 'application/octet-stream') { - const mimeTypes: Record<string, string> = { - 'pdf': 'application/pdf', - 'doc': 'application/msword', - 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'xls': 'application/vnd.ms-excel', - 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'ppt': 'application/vnd.ms-powerpoint', - 'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'txt': 'text/plain; charset=utf-8', - 'csv': 'text/csv; charset=utf-8', - 'png': 'image/png', - 'jpg': 'image/jpeg', - 'jpeg': 'image/jpeg', - 'gif': 'image/gif', - 'bmp': 'image/bmp', - 'svg': 'image/svg+xml', - 'dwg': 'application/acad', - 'dxf': 'application/dxf', - 'zip': 'application/zip', - 'rar': 'application/x-rar-compressed', - '7z': 'application/x-7z-compressed', - }; - - contentType = mimeTypes[fileExtension] || 'application/octet-stream'; - } - - // 안전한 파일명 생성 - const safeFileName = sanitizeFileName(fileName); - - // 보안 헤더와 다운로드용 헤더 설정 - const headers = new Headers(); - headers.set('Content-Type', contentType); - headers.set('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(safeFileName)}`); - headers.set('Content-Length', fileBuffer.length.toString()); - - // 보안 헤더 - headers.set('Cache-Control', 'no-cache, no-store, must-revalidate'); - headers.set('Pragma', 'no-cache'); - headers.set('Expires', '0'); - headers.set('X-Content-Type-Options', 'nosniff'); - headers.set('X-Frame-Options', 'DENY'); - headers.set('X-XSS-Protection', '1; mode=block'); - headers.set('Referrer-Policy', 'strict-origin-when-cross-origin'); - - // 성공 로그 기록 - await createFileDownloadLog({ - fileId: dbRecord.id, - success: true, - requestId: requestInfo.requestId, - fileInfo: { - fileName: safeFileName, - filePath: path, - fileSize: fileBuffer.length, - } - }); - - console.log("✅ 파일 다운로드 성공:", { - fileName: safeFileName, - contentType, - size: fileBuffer.length, - actualPath, - userId: session.user.id, - ip: requestInfo.ip, - downloadDurationMs: Date.now() - startTime - }); - - return new NextResponse(fileBuffer, { - status: 200, - headers, - }); - - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - - console.error('❌ RFQ 첨부파일 다운로드 오류:', { - error: errorMessage, - userId: (await getServerSession(authOptions))?.user?.id, - ip: requestInfo.ip, - path: request.nextUrl.searchParams.get("path"), - downloadDurationMs: Date.now() - startTime - }); - - // 에러 로그 기록 - if (fileRecord?.id) { - try { - await createFileDownloadLog({ - fileId: fileRecord.id, - success: false, - errorMessage, - requestId: requestInfo.requestId, - fileInfo: { - fileName: fileRecord.fileName || 'unknown', - filePath: request.nextUrl.searchParams.get("path") || '', - fileSize: fileRecord.fileSize || 0, - } - }); - } catch (logError) { - console.error('로그 기록 실패:', logError); - } - } - - // Zod 검증 에러 처리 - if (error instanceof z.ZodError) { - return NextResponse.json( - { - error: 'Invalid request parameters', - details: error.errors.map(e => e.message).join(', ') - }, - { status: 400 } - ); - } - - // 에러 정보 최소화 (정보 노출 방지) - return NextResponse.json( - { - error: 'Internal server error', - details: process.env.NODE_ENV === 'development' ? errorMessage : undefined - }, - { status: 500 } - ); - } -}
\ No newline at end of file diff --git a/app/api/tbe-download/route.ts b/app/api/tbe-download/route.ts deleted file mode 100644 index 93eb62db..00000000 --- a/app/api/tbe-download/route.ts +++ /dev/null @@ -1,417 +0,0 @@ -// app/api/tbe-download/route.ts -import { NextRequest, NextResponse } from 'next/server'; -import { readFile, access, constants, stat } from 'fs/promises'; -import { join, normalize, resolve } from 'path'; -import db from '@/db/db'; -import { rfqAttachments, vendorResponseAttachments } from '@/db/schema/rfq'; -import { eq } from 'drizzle-orm'; -import { getServerSession } from 'next-auth'; -import { authOptions } from '@/app/api/auth/[...nextauth]/route'; -import { createFileDownloadLog } from '@/lib/file-download-log/service'; -import rateLimit from '@/lib/rate-limit'; -import { z } from 'zod'; -import { getRequestInfo } from '@/lib/network/get-client-ip'; - -// 허용된 파일 확장자 -const ALLOWED_EXTENSIONS = new Set([ - 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', - 'txt', 'csv', 'png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg', - 'dwg', 'dxf', 'zip', 'rar', '7z' -]); - -// 최대 파일 크기 (50MB) -const MAX_FILE_SIZE = 50 * 1024 * 1024; - -// 다운로드 요청 검증 스키마 -const downloadRequestSchema = z.object({ - path: z.string().min(1, 'File path is required'), -}); - -// 파일 정보 타입 -interface FileRecord { - id: number; - fileName: string; - filePath: string; - fileSize?: number; - fileType?: string; -} - - -// 강화된 파일 경로 검증 함수 -function validateFilePath(filePath: string): boolean { - // null, undefined, 빈 문자열 체크 - if (!filePath || typeof filePath !== 'string') { - return false; - } - - // 위험한 패턴 체크 - const dangerousPatterns = [ - /\.\./, // 상위 디렉토리 접근 - /\/\//, // 이중 슬래시 - /[<>:"'|?*]/, // 특수문자 - /[\x00-\x1f]/, // 제어문자 - /\\+/ // 백슬래시 - ]; - - if (dangerousPatterns.some(pattern => pattern.test(filePath))) { - return false; - } - - // 시스템 파일 접근 방지 - const dangerousPaths = ['/etc', '/proc', '/sys', '/var', '/usr', '/root', '/home']; - for (const dangerousPath of dangerousPaths) { - if (filePath.toLowerCase().startsWith(dangerousPath)) { - return false; - } - } - - return true; -} - -// 파일 확장자 검증 -function validateFileExtension(fileName: string): boolean { - const extension = fileName.split('.').pop()?.toLowerCase() || ''; - return ALLOWED_EXTENSIONS.has(extension); -} - -// 안전한 파일명 생성 -function sanitizeFileName(fileName: string): string { - return fileName - .replace(/[^\w\s.-]/g, '_') // 안전하지 않은 문자 제거 - .replace(/\s+/g, '_') // 공백을 언더스코어로 - .substring(0, 255); // 파일명 길이 제한 -} - -export async function GET(request: NextRequest) { - const startTime = Date.now(); - const requestInfo = getRequestInfo(request); - let fileRecord: FileRecord | null = null; - - try { - // Rate limiting 체크 - const limiterResult = await rateLimit(request); - if (!limiterResult.success) { - console.warn('🚨 Rate limit 초과:', { - ip: requestInfo.ip, - userAgent: requestInfo.userAgent - }); - - return NextResponse.json( - { error: "Too many requests" }, - { status: 429 } - ); - } - - // 세션 확인 - const session = await getServerSession(authOptions); - if (!session?.user) { - console.warn('🚨 인증되지 않은 다운로드 시도:', { - ip: requestInfo.ip, - userAgent: requestInfo.userAgent, - path: request.nextUrl.searchParams.get("path") - }); - - return NextResponse.json( - { error: "Unauthorized" }, - { status: 401 } - ); - } - - // 파라미터 검증 - const searchParams = { - path: request.nextUrl.searchParams.get("path"), - }; - - const validatedParams = downloadRequestSchema.parse(searchParams); - const { path } = validatedParams; - - // 파일 경로 보안 검증 - if (!validateFilePath(path)) { - console.warn(`🚨 의심스러운 파일 경로 접근 시도: ${path}`, { - userId: session.user.id, - ip: requestInfo.ip, - userAgent: requestInfo.userAgent - }); - - return NextResponse.json( - { error: "Invalid file path" }, - { status: 400 } - ); - } - - // 경로 정규화 - const normalizedPath = normalize(path.replace(/^\/+/, "")); - - // DB에서 파일 정보 조회 (정확히 일치하는 filePath로 검색) - const [dbRecord] = await db - .select({ - id: vendorResponseAttachments.id, - fileName: vendorResponseAttachments.fileName, - filePath: vendorResponseAttachments.filePath, - fileType: vendorResponseAttachments.fileType, - }) - .from(vendorResponseAttachments) - .where(eq(vendorResponseAttachments.filePath, normalizedPath)); - - // DB에서 파일 정보를 찾지 못한 경우 - if (!dbRecord) { - console.warn("⚠️ DB에서 파일 정보를 찾지 못함:", { - path, - normalizedPath, - userId: session.user.id, - ip: requestInfo.ip - }); - - return NextResponse.json( - { error: "File not found in database" }, - { status: 404 } - ); - } - - fileRecord = dbRecord; - - // 파일명 설정 - const fileName = dbRecord.fileName; - - // 파일 확장자 검증 - if (!validateFileExtension(fileName)) { - console.warn(`🚨 허용되지 않은 파일 타입 다운로드 시도: ${fileName}`, { - userId: session.user.id, - ip: requestInfo.ip - }); - - // 실패 로그 기록 - await createFileDownloadLog({ - fileId: dbRecord.id, - success: false, - errorMessage: 'File type not allowed', - requestId: requestInfo.requestId, - fileInfo: { - fileName, - filePath: path, - fileSize: 0, - } - }); - - return NextResponse.json( - { error: "File type not allowed" }, - { status: 403 } - ); - } - - // 안전한 파일 경로 구성 - const allowedDirs = ["public", "uploads", "storage"]; - let actualPath: string | null = null; - let baseDir: string | null = null; - - // 각 허용된 디렉터리에서 파일 찾기 - for (const dir of allowedDirs) { - baseDir = resolve(process.cwd(), dir); - const testPath = resolve(baseDir, normalizedPath); - - // 경로 탐색 공격 방지 - 허용된 디렉터리 외부 접근 차단 - if (!testPath.startsWith(baseDir)) { - continue; - } - - try { - await access(testPath, constants.R_OK); - actualPath = testPath; - console.log("✅ 파일 발견:", testPath); - break; - } catch (err) { - console.log("❌ 경로에 파일 없음:", testPath); - } - } - - if (!actualPath || !baseDir) { - console.error("❌ 모든 경로에서 파일을 찾을 수 없음:", { - normalizedPath, - userId: session.user.id, - requestedPath: path, - triedDirs: allowedDirs - }); - - // 실패 로그 기록 - await createFileDownloadLog({ - fileId: dbRecord.id, - success: false, - errorMessage: 'File not found on server', - requestId: requestInfo.requestId, - fileInfo: { - fileName, - filePath: path, - fileSize: dbRecord.fileSize || 0, - } - }); - - return NextResponse.json( - { - error: "File not found on server", - details: { - path: path, - fileName: fileName, - } - }, - { status: 404 } - ); - } - - // 파일 크기 확인 - const stats = await stat(actualPath); - if (stats.size > MAX_FILE_SIZE) { - console.warn(`🚨 파일 크기 초과: ${fileName} (${stats.size} bytes)`, { - userId: session.user.id, - ip: requestInfo.ip - }); - - // 실패 로그 기록 - await createFileDownloadLog({ - fileId: dbRecord.id, - success: false, - errorMessage: 'File too large', - requestId: requestInfo.requestId, - fileInfo: { - fileName, - filePath: path, - fileSize: stats.size, - } - }); - - return NextResponse.json( - { error: "File too large" }, - { status: 413 } - ); - } - - // 파일 읽기 - const fileBuffer = await readFile(actualPath); - - // MIME 타입 결정 - const fileExtension = fileName.split('.').pop()?.toLowerCase() || ''; - let contentType = dbRecord.fileType || 'application/octet-stream'; - - // 확장자에 따른 MIME 타입 매핑 (fallback) - if (!contentType || contentType === 'application/octet-stream') { - const mimeTypes: Record<string, string> = { - 'pdf': 'application/pdf', - 'doc': 'application/msword', - 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'xls': 'application/vnd.ms-excel', - 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'ppt': 'application/vnd.ms-powerpoint', - 'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'txt': 'text/plain; charset=utf-8', - 'csv': 'text/csv; charset=utf-8', - 'png': 'image/png', - 'jpg': 'image/jpeg', - 'jpeg': 'image/jpeg', - 'gif': 'image/gif', - 'bmp': 'image/bmp', - 'svg': 'image/svg+xml', - 'dwg': 'application/acad', - 'dxf': 'application/dxf', - 'zip': 'application/zip', - 'rar': 'application/x-rar-compressed', - '7z': 'application/x-7z-compressed', - }; - - contentType = mimeTypes[fileExtension] || 'application/octet-stream'; - } - - // 안전한 파일명 생성 - const safeFileName = sanitizeFileName(fileName); - - // 보안 헤더와 다운로드용 헤더 설정 - const headers = new Headers(); - headers.set('Content-Type', contentType); - headers.set('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(safeFileName)}`); - headers.set('Content-Length', fileBuffer.length.toString()); - - // 보안 헤더 - headers.set('Cache-Control', 'no-cache, no-store, must-revalidate'); - headers.set('Pragma', 'no-cache'); - headers.set('Expires', '0'); - headers.set('X-Content-Type-Options', 'nosniff'); - headers.set('X-Frame-Options', 'DENY'); - headers.set('X-XSS-Protection', '1; mode=block'); - headers.set('Referrer-Policy', 'strict-origin-when-cross-origin'); - - // 성공 로그 기록 - await createFileDownloadLog({ - fileId: dbRecord.id, - success: true, - requestId: requestInfo.requestId, - fileInfo: { - fileName: safeFileName, - filePath: path, - fileSize: fileBuffer.length, - } - }); - - console.log("✅ TBE 파일 다운로드 성공:", { - fileName: safeFileName, - contentType, - size: fileBuffer.length, - actualPath, - userId: session.user.id, - ip: requestInfo.ip, - downloadDurationMs: Date.now() - startTime - }); - - return new NextResponse(fileBuffer, { - status: 200, - headers, - }); - - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - - console.error('❌ TBE 파일 다운로드 오류:', { - error: errorMessage, - userId: (await getServerSession(authOptions))?.user?.id, - ip: requestInfo.ip, - path: request.nextUrl.searchParams.get("path"), - downloadDurationMs: Date.now() - startTime - }); - - // 에러 로그 기록 - if (fileRecord?.id) { - try { - await createFileDownloadLog({ - fileId: fileRecord.id, - success: false, - errorMessage, - requestId: requestInfo.requestId, - fileInfo: { - fileName: fileRecord.fileName || 'unknown', - filePath: request.nextUrl.searchParams.get("path") || '', - fileSize: fileRecord.fileSize || 0, - } - }); - } catch (logError) { - console.error('로그 기록 실패:', logError); - } - } - - // Zod 검증 에러 처리 - if (error instanceof z.ZodError) { - return NextResponse.json( - { - error: 'Invalid request parameters', - details: error.errors.map(e => e.message).join(', ') - }, - { status: 400 } - ); - } - - // 에러 정보 최소화 (정보 노출 방지) - return NextResponse.json( - { - error: 'Internal server error', - details: process.env.NODE_ENV === 'development' ? errorMessage : undefined - }, - { status: 500 } - ); - } -}
\ No newline at end of file diff --git a/app/api/vendor-responses/update-comment/route.ts b/app/api/vendor-responses/update-comment/route.ts deleted file mode 100644 index f1e4c487..00000000 --- a/app/api/vendor-responses/update-comment/route.ts +++ /dev/null @@ -1,62 +0,0 @@ -// app/api/vendor-responses/update-comment/route.ts -import { NextRequest, NextResponse } from "next/server"; -import db from "@/db/db"; -import { vendorAttachmentResponses } from "@/db/schema"; - -import { getServerSession } from "next-auth/next" -import { authOptions } from "@/app/api/auth/[...nextauth]/route" -import { eq } from "drizzle-orm"; - -export async function POST(request: NextRequest) { - try { - // 인증 확인 - const session = await getServerSession(authOptions); - if (!session?.user?.id) { - return NextResponse.json( - { message: "인증이 필요합니다." }, - { status: 401 } - ); - } - - const body = await request.json(); - const { responseId, responseComment, vendorComment } = body; - - if (!responseId) { - return NextResponse.json( - { message: "응답 ID가 필요합니다." }, - { status: 400 } - ); - } - - // 코멘트만 업데이트 - const [updatedResponse] = await db - .update(vendorAttachmentResponses) - .set({ - responseComment, - vendorComment, - updatedAt: new Date(), - updatedBy:Number(session?.user.id) - }) - .where(eq(vendorAttachmentResponses.id, parseInt(responseId))) - .returning(); - - if (!updatedResponse) { - return NextResponse.json( - { message: "응답을 찾을 수 없습니다." }, - { status: 404 } - ); - } - - return NextResponse.json({ - message: "코멘트가 성공적으로 업데이트되었습니다.", - response: updatedResponse, - }); - - } catch (error) { - console.error("Comment update error:", error); - return NextResponse.json( - { message: "코멘트 업데이트 중 오류가 발생했습니다." }, - { status: 500 } - ); - } -}
\ No newline at end of file diff --git a/app/api/vendor-responses/update/route.ts b/app/api/vendor-responses/update/route.ts deleted file mode 100644 index cf7e551c..00000000 --- a/app/api/vendor-responses/update/route.ts +++ /dev/null @@ -1,118 +0,0 @@ -// app/api/vendor-responses/update/route.ts -import { NextRequest, NextResponse } from "next/server"; -import db from "@/db/db"; -import { vendorAttachmentResponses } from "@/db/schema"; -import { eq } from "drizzle-orm"; -import { getServerSession } from "next-auth/next" -import { authOptions } from "@/app/api/auth/[...nextauth]/route" - -// 리비전 번호를 증가시키는 헬퍼 함수 -function getNextRevision(currentRevision?: string): string { - if (!currentRevision) { - return "Rev.0"; // 첫 번째 응답 - } - - // "Rev.1" -> 1, "Rev.2" -> 2 형태로 숫자 추출 - const match = currentRevision.match(/Rev\.(\d+)/); - if (match) { - const currentNumber = parseInt(match[1]); - return `Rev.${currentNumber + 1}`; - } - - // 형식이 다르면 기본값 반환 - return "Rev.0"; -} - -export async function POST(request: NextRequest) { - try { - // 인증 확인 - const session = await getServerSession(authOptions); - if (!session?.user?.id) { - return NextResponse.json( - { message: "인증이 필요합니다." }, - { status: 401 } - ); - } - - const body = await request.json(); - const { - responseId, - responseStatus, - responseComment, - vendorComment, - respondedAt, - } = body; - - if (!responseId) { - return NextResponse.json( - { message: "응답 ID가 필요합니다." }, - { status: 400 } - ); - } - - // 1. 기존 응답 정보 조회 (현재 respondedRevision 확인) - const existingResponse = await db - .select() - .from(vendorAttachmentResponses) - .where(eq(vendorAttachmentResponses.id, parseInt(responseId))) - .limit(1); - - if (!existingResponse || existingResponse.length === 0) { - return NextResponse.json( - { message: "응답을 찾을 수 없습니다." }, - { status: 404 } - ); - } - - const currentResponse = existingResponse[0]; - - // 2. 벤더 응답 리비전 결정 - let nextRespondedRevision: string; - - - if (responseStatus === "RESPONDED") { - - // 첫 응답이거나 수정 요청 후 재응답인 경우 리비전 증가 - nextRespondedRevision = getNextRevision(currentResponse.respondedRevision); - - } else { - // WAIVED 등 다른 상태는 기존 리비전 유지 - nextRespondedRevision = currentResponse.respondedRevision || ""; - } - - // 3. vendor response 업데이트 - const [updatedResponse] = await db - .update(vendorAttachmentResponses) - .set({ - responseStatus, - respondedRevision: nextRespondedRevision, - responseComment, - vendorComment, - respondedAt: respondedAt ? new Date(respondedAt) : null, - updatedAt: new Date(), - updatedBy:Number(session?.user.id) - }) - .where(eq(vendorAttachmentResponses.id, parseInt(responseId))) - .returning(); - - if (!updatedResponse) { - return NextResponse.json( - { message: "응답 업데이트에 실패했습니다." }, - { status: 500 } - ); - } - - return NextResponse.json({ - message: "응답이 성공적으로 업데이트되었습니다.", - response: updatedResponse, - newRevision: nextRespondedRevision, // 새로운 리비전 정보 반환 - }); - - } catch (error) { - console.error("Response update error:", error); - return NextResponse.json( - { message: "응답 업데이트 중 오류가 발생했습니다." }, - { status: 500 } - ); - } -}
\ No newline at end of file diff --git a/app/api/vendor-responses/upload/route.ts b/app/api/vendor-responses/upload/route.ts deleted file mode 100644 index 111e4bd4..00000000 --- a/app/api/vendor-responses/upload/route.ts +++ /dev/null @@ -1,105 +0,0 @@ -// app/api/vendor-response-attachments/upload/route.ts -import { NextRequest, NextResponse } from "next/server"; -import { writeFile, mkdir } from "fs/promises"; -import { existsSync } from "fs"; -import path from "path"; -import db from "@/db/db"; -import { vendorResponseAttachmentsB } from "@/db/schema"; -import { getServerSession } from "next-auth/next" -import { authOptions } from "@/app/api/auth/[...nextauth]/route" - -export async function POST(request: NextRequest) { - try { - // 인증 확인 - const session = await getServerSession(authOptions); - if (!session?.user?.id) { - return NextResponse.json( - { message: "인증이 필요합니다." }, - { status: 401 } - ); - } - - const formData = await request.formData(); - const responseId = formData.get("responseId") as string; - const file = formData.get("file") as File; - const description = formData.get("description") as string; - - if (!responseId) { - return NextResponse.json( - { message: "응답 ID가 필요합니다." }, - { status: 400 } - ); - } - - if (!file) { - return NextResponse.json( - { message: "파일이 선택되지 않았습니다." }, - { status: 400 } - ); - } - - // 파일 크기 검증 (10MB) - if (file.size > 10 * 1024 * 1024) { - return NextResponse.json( - { message: "파일이 너무 큽니다. (최대 10MB)" }, - { status: 400 } - ); - } - - // 업로드 디렉토리 생성 - const uploadDir = path.join( - process.cwd(), - "public", - "uploads", - "vendor-responses", - responseId - ); - - if (!existsSync(uploadDir)) { - await mkdir(uploadDir, { recursive: true }); - } - - // 고유한 파일명 생성 - const timestamp = Date.now(); - const sanitizedName = file.name.replace(/[^a-zA-Z0-9.-]/g, "_"); - const fileName = `${timestamp}_${sanitizedName}`; - const filePath = `/uploads/vendor-responses/${responseId}/${fileName}`; - const fullPath = path.join(uploadDir, fileName); - - // 파일 저장 - const buffer = Buffer.from(await file.arrayBuffer()); - await writeFile(fullPath, buffer); - - // DB에 파일 정보 저장 - const [insertedFile] = await db - .insert(vendorResponseAttachmentsB) - .values({ - vendorResponseId: parseInt(responseId), - fileName, - originalFileName: file.name, - filePath, - fileSize: file.size, - fileType: file.type || path.extname(file.name).slice(1), - description: description || null, - uploadedBy: parseInt(session.user.id), - }) - .returning(); - - return NextResponse.json({ - id: insertedFile.id, - fileName, - originalFileName: file.name, - filePath, - fileSize: file.size, - fileType: file.type || path.extname(file.name).slice(1), - message: "파일이 성공적으로 업로드되었습니다.", - }); - - } catch (error) { - console.error("File upload error:", error); - return NextResponse.json( - { message: "파일 업로드 중 오류가 발생했습니다." }, - { status: 500 } - ); - } -}
\ No newline at end of file diff --git a/app/api/vendor-responses/waive/route.ts b/app/api/vendor-responses/waive/route.ts deleted file mode 100644 index e732e8d2..00000000 --- a/app/api/vendor-responses/waive/route.ts +++ /dev/null @@ -1,69 +0,0 @@ -// app/api/vendor-responses/waive/route.ts -import { NextRequest, NextResponse } from "next/server"; -import db from "@/db/db"; -import { getServerSession } from "next-auth/next" -import { authOptions } from "@/app/api/auth/[...nextauth]/route" -import { eq } from "drizzle-orm"; -import { vendorAttachmentResponses } from "@/db/schema"; - -export async function POST(request: NextRequest) { - try { - // 인증 확인 - const session = await getServerSession(authOptions); - if (!session?.user?.id) { - return NextResponse.json( - { message: "인증이 필요합니다." }, - { status: 401 } - ); - } - - const body = await request.json(); - const { responseId, responseComment, vendorComment } = body; - - if (!responseId) { - return NextResponse.json( - { message: "응답 ID가 필요합니다." }, - { status: 400 } - ); - } - - if (!responseComment) { - return NextResponse.json( - { message: "포기 사유를 입력해주세요." }, - { status: 400 } - ); - } - - // vendor response를 WAIVED 상태로 업데이트 - const [updatedResponse] = await db - .update(vendorAttachmentResponses) - .set({ - responseStatus: "WAIVED", - responseComment, - vendorComment, - respondedAt: new Date(), - updatedAt: new Date(), - }) - .where(eq(vendorAttachmentResponses.id, parseInt(responseId))) - .returning(); - - if (!updatedResponse) { - return NextResponse.json( - { message: "응답을 찾을 수 없습니다." }, - { status: 404 } - ); - } - - return NextResponse.json({ - message: "응답이 성공적으로 포기 처리되었습니다.", - response: updatedResponse, - }); - - } catch (error) { - console.error("Waive response error:", error); - return NextResponse.json( - { message: "응답 포기 처리 중 오류가 발생했습니다." }, - { status: 500 } - ); - } -}
\ No newline at end of file |
