summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/[lng]/evcp/(evcp)/basic-contract-template/gtc/page.tsx69
-rw-r--r--app/[lng]/evcp/(evcp)/evaluation-target-list/page.tsx80
-rw-r--r--app/[lng]/evcp/(evcp)/evaluation/page.tsx119
-rw-r--r--app/[lng]/evcp/(evcp)/tech-contact-possible-items/page.tsx (renamed from app/[lng]/evcp/(evcp)/contact-possible-items/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/tech-project-avl/page.tsx4
-rw-r--r--app/[lng]/evcp/(evcp)/tech-vendors/page.tsx30
-rw-r--r--app/[lng]/sales/(sales)/tech-contact-possible-items/page.tsx56
-rw-r--r--app/[lng]/sales/(sales)/tech-project-avl/page.tsx11
-rw-r--r--app/[lng]/sales/(sales)/tech-vendors/page.tsx28
-rw-r--r--app/api/basicContract/create-revision/route.ts75
-rw-r--r--app/api/files/[...path]/route.ts9
-rw-r--r--app/api/upload/basicContract/complete/route.ts25
12 files changed, 366 insertions, 140 deletions
diff --git a/app/[lng]/evcp/(evcp)/basic-contract-template/gtc/page.tsx b/app/[lng]/evcp/(evcp)/basic-contract-template/gtc/page.tsx
new file mode 100644
index 00000000..33c504df
--- /dev/null
+++ b/app/[lng]/evcp/(evcp)/basic-contract-template/gtc/page.tsx
@@ -0,0 +1,69 @@
+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 { GtcDocumentsTable } from "@/lib/gtc-contract/status/gtc-contract-table"
+import { getGtcDocuments,getProjectsForFilter,getUsersForFilter } from "@/lib/gtc-contract/service"
+import { searchParamsCache } from "@/lib/gtc-contract/validations"
+
+interface GtcPageProps {
+ searchParams: Promise<SearchParams>
+}
+
+export default async function GtcPage(props: GtcPageProps) {
+ const searchParams = await props.searchParams
+ const search = searchParamsCache.parse(searchParams)
+ const validFilters = getValidFilters(search.filters)
+
+ const promises = Promise.all([
+ getGtcDocuments({
+ ...search,
+ filters: validFilters,
+ }),
+ getProjectsForFilter(),
+ getUsersForFilter()
+ ])
+
+ 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">
+ GTC 목록관리
+ </h2>
+ <InformationButton pagePath="evcp/basic-contract-template/gtc" />
+ </div>
+ </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
+ />
+ }
+ >
+ <GtcDocumentsTable promises={promises} />
+ </React.Suspense>
+ </Shell>
+ )
+} \ No newline at end of file
diff --git a/app/[lng]/evcp/(evcp)/evaluation-target-list/page.tsx b/app/[lng]/evcp/(evcp)/evaluation-target-list/page.tsx
index a0523eea..886d061d 100644
--- a/app/[lng]/evcp/(evcp)/evaluation-target-list/page.tsx
+++ b/app/[lng]/evcp/(evcp)/evaluation-target-list/page.tsx
@@ -1,22 +1,15 @@
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"
+import { EvaluationTargetsTable } from "@/lib/evaluation-target-list/table/evaluation-target-table"
+
export const metadata: Metadata = {
title: "협력업체 평가 대상 관리",
description: "협력업체 평가 대상을 확정하고 담당자를 지정합니다.",
@@ -26,63 +19,38 @@ 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,
- })
+ getEvaluationTargets(search)
])
return (
<Shell className="gap-4">
- {/* 간소화된 헤더 */}
+ {/* Header */}
<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 className="flex items-center gap-2">
+ <h2 className="text-2xl font-bold tracking-tight">
+ 협력업체 평가 대상 관리
+ </h2>
+ <InformationButton pagePath="evcp/evaluation-target-list" />
+ <Badge variant="outline" className="text-sm">
+ {currentEvaluationYear}년도
+ </Badge>
</div>
</div>
-
- {/* 메인 테이블 (통계는 테이블 내부로 이동) */}
+
+ {/* Main Table */}
<React.Suspense
- key={JSON.stringify(searchParams)} // URL 파라미터가 변경될 때마다 강제 리렌더링
+ key={`evaluation-targets-${search.page}-${JSON.stringify(search.filters)}-${search.joinOperator}-${search.search || 'no-search'}`}
fallback={
<DataTableSkeleton
columnCount={12}
@@ -106,12 +74,10 @@ export default async function EvaluationTargetsPage(props: EvaluationTargetsPage
/>
}
>
- {currentEvaluationYear &&
- <EvaluationTargetsTable
- promises={promises}
+ <EvaluationTargetsTable
+ promises={promises}
evaluationYear={currentEvaluationYear}
/>
-}
</React.Suspense>
</Shell>
)
diff --git a/app/[lng]/evcp/(evcp)/evaluation/page.tsx b/app/[lng]/evcp/(evcp)/evaluation/page.tsx
index 4c498104..ae626e58 100644
--- a/app/[lng]/evcp/(evcp)/evaluation/page.tsx
+++ b/app/[lng]/evcp/(evcp)/evaluation/page.tsx
@@ -1,5 +1,5 @@
// ================================================================
-// 4. PERIODIC EVALUATIONS PAGE
+// 4. PERIODIC EVALUATIONS PAGE - 집계 뷰 지원 (정리된 버전)
// ================================================================
import * as React from "react"
@@ -8,7 +8,7 @@ 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 { HelpCircle, BarChart3, List, Info } from "lucide-react"
import {
Popover,
PopoverContent,
@@ -16,10 +16,15 @@ import {
} from "@/components/ui/popover"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
+import { Alert, AlertDescription } from "@/components/ui/alert"
import { PeriodicEvaluationsTable } from "@/lib/evaluation/table/evaluation-table"
-import { getPeriodicEvaluations } from "@/lib/evaluation/service"
-import { searchParamsEvaluationsCache } from "@/lib/evaluation/validation"
+import { getPeriodicEvaluationsWithAggregation } from "@/lib/evaluation/service"
+import {
+ searchParamsEvaluationsCache,
+ type GetEvaluationsSchema
+} from "@/lib/evaluation/validation"
import { InformationButton } from "@/components/information/information-button"
+
export const metadata: Metadata = {
title: "협력업체 정기평가",
description: "협력업체 정기평가 진행 현황을 관리합니다.",
@@ -29,7 +34,7 @@ interface PeriodicEvaluationsPageProps {
searchParams: Promise<SearchParams>
}
-// 프로세스 안내 팝오버 컴포넌트
+// 프로세스 안내 팝오버 컴포넌트 - 집계 뷰 설명 포함
function ProcessGuidePopover() {
return (
<Popover>
@@ -46,6 +51,7 @@ function ProcessGuidePopover() {
확정된 평가 대상 업체들에 대한 정기평가 절차입니다.
</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">
@@ -84,48 +90,68 @@ function ProcessGuidePopover() {
</div>
</div>
</div>
+
+ {/* 집계 뷰 설명 추가 */}
+ <div className="border-t pt-3 space-y-2">
+ <h5 className="font-medium text-sm flex items-center gap-1">
+ <Info className="h-3 w-3" />
+ 보기 모드
+ </h5>
+ <div className="space-y-2 text-xs">
+ <div className="flex items-center gap-2">
+ <List className="h-3 w-3 text-blue-600" />
+ <span className="font-medium">상세 뷰:</span>
+ <span className="text-muted-foreground">모든 평가 기록을 개별적으로 표시</span>
+ </div>
+ <div className="flex items-center gap-2">
+ <BarChart3 className="h-3 w-3 text-purple-600" />
+ <span className="font-medium">집계 뷰:</span>
+ <span className="text-muted-foreground">동일 벤더의 여러 division 평가를 평균으로 통합</span>
+ </div>
+ </div>
+ </div>
</div>
</PopoverContent>
</Popover>
)
}
-// TODO: 이 함수들은 실제 서비스 파일에서 구현해야 함
-function getDefaultEvaluationYear() {
- return new Date().getFullYear()
+// 집계 모드 안내 컴포넌트
+function AggregatedModeNotice({ isAggregated }: { isAggregated: boolean }) {
+ if (!isAggregated) return null;
+
+ return (
+ <Alert className="mb-4 border-purple-200 bg-purple-50">
+ <BarChart3 className="h-4 w-4 text-purple-600" />
+ <AlertDescription className="text-purple-800">
+ 현재 <strong>집계 뷰</strong>로 표시되고 있습니다.
+ 동일 벤더의 여러 division 평가 결과가 평균으로 통합되어 표시됩니다.
+ </AlertDescription>
+ </Alert>
+ );
}
-
-
export default async function PeriodicEvaluationsPage(props: PeriodicEvaluationsPageProps) {
const searchParams = await props.searchParams
+
+ // ✅ nuqs 기반 파라미터 파싱
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()
+ const currentEvaluationYear = search.evaluationYear
- // Promise.all로 감싸서 전달
+ // ✅ 집계 모드를 지원하는 서비스 함수 사용
const promises = Promise.all([
- getPeriodicEvaluations({
+ getPeriodicEvaluationsWithAggregation({
...search,
- filters: allFilters,
- joinOperator,
+ filters: validFilters,
})
])
+ // ✅ 현재 모드 표시용 변수
+ const currentMode = search.aggregated ? "집계" : "상세"
+
return (
<Shell className="gap-4">
{/* 헤더 */}
@@ -137,27 +163,47 @@ export default async function PeriodicEvaluationsPage(props: PeriodicEvaluations
협력업체 정기평가
</h2>
<InformationButton pagePath="evcp/evaluation" />
+ {/* <ProcessGuidePopover /> */}
+ </div>
+ <div className="flex items-center gap-2">
+ <Badge variant="outline" className="text-sm">
+ {currentEvaluationYear}년도
+ </Badge>
+ {/* ✅ 현재 보기 모드 표시 */}
+ <Badge
+ variant={search.aggregated ? "default" : "secondary"}
+ className={`text-xs ${search.aggregated ? 'bg-purple-600' : ''}`}
+ >
+ <div className="flex items-center gap-1">
+ {search.aggregated ? (
+ <BarChart3 className="h-3 w-3" />
+ ) : (
+ <List className="h-3 w-3" />
+ )}
+ {currentMode} 뷰
+ </div>
+ </Badge>
</div>
- <Badge variant="outline" className="text-sm">
- {currentEvaluationYear}년도
- </Badge>
</div>
</div>
</div>
+
+ {/* ✅ 집계 모드 안내 */}
+ <AggregatedModeNotice isAggregated={search.aggregated} />
{/* 메인 테이블 */}
<React.Suspense
- key={JSON.stringify(searchParams)}
+ key={JSON.stringify(searchParams)} // 집계 모드 변경 시에도 리렌더링
fallback={
<DataTableSkeleton
- columnCount={15}
+ columnCount={search.aggregated ? 17 : 15} // 집계 모드에서 컬럼 추가
searchableColumnCount={2}
filterableColumnCount={8}
cellWidths={[
"3rem", // checkbox
"5rem", // 평가년도
- "5rem", // 평가기간
- "4rem", // 구분
+ "5rem", // 평가기간 or 평가수 (집계 모드)
+ "6rem", // 구분 (집계 모드에서 더 넓게)
"8rem", // 벤더코드
"12rem", // 벤더명
"4rem", // 내외자
@@ -168,7 +214,8 @@ export default async function PeriodicEvaluationsPage(props: PeriodicEvaluations
"4rem", // 총점
"4rem", // 등급
"5rem", // 진행상태
- "8rem" // actions
+ "8rem", // actions
+ ...(search.aggregated ? ["5rem", "5rem"] : []) // 집계 모드 추가 컬럼
]}
shrinkZero
/>
@@ -177,6 +224,8 @@ export default async function PeriodicEvaluationsPage(props: PeriodicEvaluations
<PeriodicEvaluationsTable
promises={promises}
evaluationYear={currentEvaluationYear}
+ // ✅ 현재 뷰 모드를 테이블 컴포넌트에 전달
+ initialViewMode={search.aggregated ? "aggregated" : "detailed"}
/>
</React.Suspense>
</Shell>
diff --git a/app/[lng]/evcp/(evcp)/contact-possible-items/page.tsx b/app/[lng]/evcp/(evcp)/tech-contact-possible-items/page.tsx
index 5bc36790..5bc36790 100644
--- a/app/[lng]/evcp/(evcp)/contact-possible-items/page.tsx
+++ b/app/[lng]/evcp/(evcp)/tech-contact-possible-items/page.tsx
diff --git a/app/[lng]/evcp/(evcp)/tech-project-avl/page.tsx b/app/[lng]/evcp/(evcp)/tech-project-avl/page.tsx
index 5ae0d09a..4ce018cd 100644
--- a/app/[lng]/evcp/(evcp)/tech-project-avl/page.tsx
+++ b/app/[lng]/evcp/(evcp)/tech-project-avl/page.tsx
@@ -52,12 +52,12 @@ export default async function AcceptedQuotationsPage({
<InformationButton pagePath="evcp/tech-project-avl" />
</div>
{/* <p className="text-muted-foreground">
- 기술영업 견적 Result 전송 정보를 확인하고{" "}
+ 기술영업 승인 견적서에 대한 요약 정보를 확인하고{" "}
<span className="inline-flex items-center whitespace-nowrap">
<Ellipsis className="size-3" />
<span className="ml-1">버튼</span>
</span>
- 을 통해 견적 Result 전송 정보를 확인할 수 있습니다.
+ 을 통해 RFQ 코드, 설명, 업체명, 업체 코드 등의 상세 정보를 확인할 수 있습니다.
</p> */}
</div>
</div>
diff --git a/app/[lng]/evcp/(evcp)/tech-vendors/page.tsx b/app/[lng]/evcp/(evcp)/tech-vendors/page.tsx
index 8f542f59..736a7bad 100644
--- a/app/[lng]/evcp/(evcp)/tech-vendors/page.tsx
+++ b/app/[lng]/evcp/(evcp)/tech-vendors/page.tsx
@@ -8,7 +8,6 @@ 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"
-import { TechVendorContainer } from "@/components/tech-vendors/tech-vendor-container"
interface IndexPageProps {
searchParams: Promise<SearchParams>
@@ -20,14 +19,6 @@ export default async function IndexPage(props: IndexPageProps) {
const validFilters = getValidFilters(search.filters)
- // 벤더 타입 정의
- const vendorTypes = [
- { id: "all", name: "전체", value: "" },
- { id: "ship", name: "조선", value: "조선" },
- { id: "top", name: "해양TOP", value: "해양TOP" },
- { id: "hull", name: "해양HULL", value: "해양HULL" },
- ]
-
const promises = Promise.all([
getTechVendors({
...search,
@@ -37,7 +28,20 @@ export default async function IndexPage(props: IndexPageProps) {
])
return (
- <Shell className="gap-4">
+ <Shell variant="fullscreen" className="h-full">
+ <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
@@ -49,10 +53,8 @@ export default async function IndexPage(props: IndexPageProps) {
/>
}
>
- <TechVendorContainer vendorTypes={vendorTypes}>
- <TechVendorsTable promises={promises} />
- </TechVendorContainer>
+ <TechVendorsTable promises={promises} />
</React.Suspense>
</Shell>
)
-} \ No newline at end of file
+} \ 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
new file mode 100644
index 00000000..5bc36790
--- /dev/null
+++ b/app/[lng]/sales/(sales)/tech-contact-possible-items/page.tsx
@@ -0,0 +1,56 @@
+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
index 3a86f840..4ce018cd 100644
--- a/app/[lng]/sales/(sales)/tech-project-avl/page.tsx
+++ b/app/[lng]/sales/(sales)/tech-project-avl/page.tsx
@@ -11,7 +11,7 @@ import { getAcceptedTechSalesVendorQuotations } from "@/lib/techsales-rfq/servic
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>
@@ -45,9 +45,12 @@ export default async function AcceptedQuotationsPage({
<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">
- 승인된 견적서(해양TOP,HULL)
- </h2>
+ <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">
diff --git a/app/[lng]/sales/(sales)/tech-vendors/page.tsx b/app/[lng]/sales/(sales)/tech-vendors/page.tsx
index 8f542f59..e49ba79e 100644
--- a/app/[lng]/sales/(sales)/tech-vendors/page.tsx
+++ b/app/[lng]/sales/(sales)/tech-vendors/page.tsx
@@ -8,7 +8,6 @@ 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"
-import { TechVendorContainer } from "@/components/tech-vendors/tech-vendor-container"
interface IndexPageProps {
searchParams: Promise<SearchParams>
@@ -20,14 +19,6 @@ export default async function IndexPage(props: IndexPageProps) {
const validFilters = getValidFilters(search.filters)
- // 벤더 타입 정의
- const vendorTypes = [
- { id: "all", name: "전체", value: "" },
- { id: "ship", name: "조선", value: "조선" },
- { id: "top", name: "해양TOP", value: "해양TOP" },
- { id: "hull", name: "해양HULL", value: "해양HULL" },
- ]
-
const promises = Promise.all([
getTechVendors({
...search,
@@ -38,6 +29,19 @@ export default async function IndexPage(props: IndexPageProps) {
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
@@ -49,10 +53,8 @@ export default async function IndexPage(props: IndexPageProps) {
/>
}
>
- <TechVendorContainer vendorTypes={vendorTypes}>
- <TechVendorsTable promises={promises} />
- </TechVendorContainer>
+ <TechVendorsTable promises={promises} />
</React.Suspense>
</Shell>
)
-} \ No newline at end of file
+} \ No newline at end of file
diff --git a/app/api/basicContract/create-revision/route.ts b/app/api/basicContract/create-revision/route.ts
new file mode 100644
index 00000000..69dc4c8f
--- /dev/null
+++ b/app/api/basicContract/create-revision/route.ts
@@ -0,0 +1,75 @@
+// app/api/basicContract/create-revision/route.ts
+import { NextRequest, NextResponse } from "next/server";
+import { unstable_noStore } from "next/cache";
+import { z } from "zod";
+import { getErrorMessage } from "@/lib/handle-error";
+import { createBasicContractTemplateRevision } from "@/lib/basic-contract/service";
+
+// 리비전 생성 스키마
+const createRevisionSchema = z.object({
+ baseTemplateId: z.string().uuid(),
+ templateName: z.string().min(1),
+ revision: z.number().int().min(1),
+ legalReviewRequired: z.boolean(),
+ shipBuildingApplicable: z.boolean(),
+ windApplicable: z.boolean(),
+ pcApplicable: z.boolean(),
+ nbApplicable: z.boolean(),
+ rcApplicable: z.boolean(),
+ gyApplicable: z.boolean(),
+ sysApplicable: z.boolean(),
+ infraApplicable: z.boolean(),
+ fileName: z.string().min(1),
+ filePath: z.string().min(1),
+});
+
+export async function POST(request: NextRequest) {
+ unstable_noStore();
+
+ try {
+ // 요청 본문 파싱
+ const body = await request.json();
+ const validatedData = createRevisionSchema.parse(body);
+
+ // 같은 템플릿 이름에 대해 리비전이 이미 존재하는지 확인하는 로직은
+ // 서비스 함수에서 처리됨
+
+ // 새 리비전 생성
+ const { data: newRevision, error } = await createBasicContractTemplateRevision(validatedData);
+
+ if (error) {
+ return NextResponse.json(
+ { success: false, error },
+ { status: 400 }
+ );
+ }
+
+ return NextResponse.json({
+ success: true,
+ data: newRevision,
+ message: `${validatedData.templateName} v${validatedData.revision} 리비전이 성공적으로 생성되었습니다.`
+ });
+
+ } catch (error) {
+ console.error("Create revision API error:", error);
+
+ if (error instanceof z.ZodError) {
+ return NextResponse.json(
+ {
+ success: false,
+ error: "입력 데이터가 올바르지 않습니다.",
+ details: error.errors
+ },
+ { status: 400 }
+ );
+ }
+
+ return NextResponse.json(
+ {
+ success: false,
+ error: getErrorMessage(error)
+ },
+ { status: 500 }
+ );
+ }
+} \ No newline at end of file
diff --git a/app/api/files/[...path]/route.ts b/app/api/files/[...path]/route.ts
index a3bd67af..c9d530de 100644
--- a/app/api/files/[...path]/route.ts
+++ b/app/api/files/[...path]/route.ts
@@ -36,7 +36,14 @@ const isAllowedPath = (requestedPath: string): boolean => {
'vendorFormReportSample',
'vendorFormData',
'uploads',
- 'tech-sales'
+ 'tech-sales',
+ 'techsales-rfq',
+ 'tech-vendors',
+ 'vendor-investigation',
+ 'vendor-responses',
+ 'vendor-evaluation',
+ 'vendor-evaluation-submit',
+ 'vendor-attachments',
];
return allowedPaths.some(allowed =>
diff --git a/app/api/upload/basicContract/complete/route.ts b/app/api/upload/basicContract/complete/route.ts
index 6398c5eb..b22e99f1 100644
--- a/app/api/upload/basicContract/complete/route.ts
+++ b/app/api/upload/basicContract/complete/route.ts
@@ -1,27 +1,24 @@
import { NextRequest, NextResponse } from 'next/server';
import { createBasicContractTemplate } from '@/lib/basic-contract/service';
import { revalidatePath ,revalidateTag} from 'next/cache';
+import { createBasicContractTemplateSchema } from '@/lib/basic-contract/validations';
export async function POST(request: NextRequest) {
try {
- const { templateName,validityPeriod, status, fileName, filePath } = await request.json();
+ const json = await request.json();
+ const parsed = createBasicContractTemplateSchema.safeParse(json);
- if (!templateName || !fileName || !filePath) {
- return NextResponse.json({ success: false, error: '필수 정보가 누락되었습니다' }, { status: 400 });
+ if (!parsed.success) {
+ return NextResponse.json(
+ { success: false, error: parsed.error.flatten() },
+ { status: 400 }
+ );
}
-
- // DB에 저장
- const { data, error } = await createBasicContractTemplate({
- templateName,
- validityPeriod,
- status,
- fileName,
- filePath
- });
-
+
+ const { data, error } = await createBasicContractTemplate(parsed.data);
+
revalidatePath('/evcp/basic-contract-templates');
- revalidatePath('/'); // 루트 경로 무효화도 시도
revalidateTag("basic-contract-templates");
if (error) {