summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/[lng]/engineering/(engineering)/document-list-only/layout.tsx17
-rw-r--r--app/[lng]/engineering/(engineering)/document-list-only/page.tsx98
-rw-r--r--app/[lng]/engineering/(engineering)/document-list-ship/page.tsx141
-rw-r--r--app/[lng]/engineering/(engineering)/vendor-data/form/[packageId]/[formId]/[projectId]/[contractId]/page.tsx79
-rw-r--r--app/[lng]/engineering/(engineering)/vendor-data/layout.tsx67
-rw-r--r--app/[lng]/engineering/(engineering)/vendor-data/page.tsx28
-rw-r--r--app/[lng]/engineering/(engineering)/vendor-data/tag/[id]/page.tsx43
-rw-r--r--app/[lng]/evcp/(evcp)/document-list-ship/page.tsx141
-rw-r--r--app/[lng]/evcp/(evcp)/gtc/[id]/page.tsx (renamed from app/[lng]/evcp/(evcp)/basic-contract-template/gtc/[id]/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/gtc/page.tsx (renamed from app/[lng]/evcp/(evcp)/basic-contract-template/gtc/page.tsx)0
-rw-r--r--app/[lng]/evcp/(evcp)/information/page.tsx108
-rw-r--r--app/[lng]/evcp/(evcp)/menu-list/page.tsx22
-rw-r--r--app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/basic-info-client.tsx206
-rw-r--r--app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/types.ts4
-rw-r--r--app/api/files/[...path]/route.ts3
-rw-r--r--app/api/upload/basicContract/chunk/route.ts156
16 files changed, 959 insertions, 154 deletions
diff --git a/app/[lng]/engineering/(engineering)/document-list-only/layout.tsx b/app/[lng]/engineering/(engineering)/document-list-only/layout.tsx
new file mode 100644
index 00000000..17e78c0a
--- /dev/null
+++ b/app/[lng]/engineering/(engineering)/document-list-only/layout.tsx
@@ -0,0 +1,17 @@
+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
new file mode 100644
index 00000000..5b49a6ef
--- /dev/null
+++ b/app/[lng]/engineering/(engineering)/document-list-only/page.tsx
@@ -0,0 +1,98 @@
+// 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
new file mode 100644
index 00000000..321ce909
--- /dev/null
+++ b/app/[lng]/engineering/(engineering)/document-list-ship/page.tsx
@@ -0,0 +1,141 @@
+// 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>
+ <InformationButton pagePath="partners/document-list-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">
+ <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>
+ <h2 className="text-2xl font-bold tracking-tight">
+ 조선 Document Management
+ </h2>
+ <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)/vendor-data/form/[packageId]/[formId]/[projectId]/[contractId]/page.tsx b/app/[lng]/engineering/(engineering)/vendor-data/form/[packageId]/[formId]/[projectId]/[contractId]/page.tsx
new file mode 100644
index 00000000..f69aa525
--- /dev/null
+++ b/app/[lng]/engineering/(engineering)/vendor-data/form/[packageId]/[formId]/[projectId]/[contractId]/page.tsx
@@ -0,0 +1,79 @@
+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
new file mode 100644
index 00000000..7d00359c
--- /dev/null
+++ b/app/[lng]/engineering/(engineering)/vendor-data/layout.tsx
@@ -0,0 +1,67 @@
+// 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
new file mode 100644
index 00000000..ddc21a2b
--- /dev/null
+++ b/app/[lng]/engineering/(engineering)/vendor-data/page.tsx
@@ -0,0 +1,28 @@
+// 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
new file mode 100644
index 00000000..7250732f
--- /dev/null
+++ b/app/[lng]/engineering/(engineering)/vendor-data/tag/[id]/page.tsx
@@ -0,0 +1,43 @@
+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]/evcp/(evcp)/document-list-ship/page.tsx b/app/[lng]/evcp/(evcp)/document-list-ship/page.tsx
new file mode 100644
index 00000000..321ce909
--- /dev/null
+++ b/app/[lng]/evcp/(evcp)/document-list-ship/page.tsx
@@ -0,0 +1,141 @@
+// 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>
+ <InformationButton pagePath="partners/document-list-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">
+ <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>
+ <h2 className="text-2xl font-bold tracking-tight">
+ 조선 Document Management
+ </h2>
+ <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]/evcp/(evcp)/basic-contract-template/gtc/[id]/page.tsx b/app/[lng]/evcp/(evcp)/gtc/[id]/page.tsx
index 0f783375..0f783375 100644
--- a/app/[lng]/evcp/(evcp)/basic-contract-template/gtc/[id]/page.tsx
+++ b/app/[lng]/evcp/(evcp)/gtc/[id]/page.tsx
diff --git a/app/[lng]/evcp/(evcp)/basic-contract-template/gtc/page.tsx b/app/[lng]/evcp/(evcp)/gtc/page.tsx
index 33c504df..33c504df 100644
--- a/app/[lng]/evcp/(evcp)/basic-contract-template/gtc/page.tsx
+++ b/app/[lng]/evcp/(evcp)/gtc/page.tsx
diff --git a/app/[lng]/evcp/(evcp)/information/page.tsx b/app/[lng]/evcp/(evcp)/information/page.tsx
index db383c32..c87471ae 100644
--- a/app/[lng]/evcp/(evcp)/information/page.tsx
+++ b/app/[lng]/evcp/(evcp)/information/page.tsx
@@ -1,58 +1,52 @@
-import * as React from "react"
-import type { Metadata } from "next"
-import { unstable_noStore as noStore } from "next/cache"
-
-import { Shell } from "@/components/shell"
-import { getInformationLists } from "@/lib/information/service"
-import { InformationClient } from "@/components/information/information-client"
-import { InformationButton } from "@/components/information/information-button"
-
-export const metadata: Metadata = {
- title: "인포메이션 관리",
- description: "페이지별 도움말 및 첨부파일을 관리합니다.",
-}
-
-interface InformationPageProps {
- params: Promise<{ lng: string }>
-}
-
-export default async function InformationPage({ params }: InformationPageProps) {
- noStore()
-
- const { lng } = await params
-
- // 초기 데이터 로딩
- const initialData = await getInformationLists({
- page: 1,
- perPage: 500,
- search: "",
- sort: [{ id: "createdAt", desc: true }],
- flags: [],
- filters: [],
- joinOperator: "and",
- pagePath: "",
- pageName: "",
- informationContent: "",
- isActive: null,
- from: "",
- to: "",
- })
-
- 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/information" />
- </div>
- </div>
- </div>
- </div>
- <InformationClient lng={lng} initialData={initialData?.data || []} />
- </Shell>
- )
+import * as React from "react"
+import type { Metadata } from "next"
+import { unstable_noStore as noStore } from "next/cache"
+
+import { Shell } from "@/components/shell"
+import { getInformationLists } from "@/lib/information/service"
+import { InformationClient } from "@/components/information/information-client"
+import { InformationButton } from "@/components/information/information-button"
+import { useTranslation } from "@/i18n"
+
+export const metadata: Metadata = {
+ title: "인포메이션 관리",
+ description: "페이지별 도움말 및 첨부파일을 관리합니다.",
+}
+
+interface InformationPageProps {
+ params: Promise<{ lng: string }>
+}
+
+export default async function InformationPage({ params }: InformationPageProps) {
+ noStore()
+
+ const { lng } = await params
+ const { t } = await useTranslation(lng, 'menu')
+
+ // 초기 데이터 로딩 (간단화)
+ const initialData = await getInformationLists()
+
+ // 서버사이드에서 번역된 데이터 생성
+ const translatedData = initialData?.data?.map(item => ({
+ ...item,
+ translatedPageName: t(item.pageName)
+ })) || []
+
+ 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/information" />
+ </div>
+ </div>
+ </div>
+ </div>
+ <InformationClient initialData={translatedData} />
+ </Shell>
+ )
} \ No newline at end of file
diff --git a/app/[lng]/evcp/(evcp)/menu-list/page.tsx b/app/[lng]/evcp/(evcp)/menu-list/page.tsx
index 6645127f..5a1f71a5 100644
--- a/app/[lng]/evcp/(evcp)/menu-list/page.tsx
+++ b/app/[lng]/evcp/(evcp)/menu-list/page.tsx
@@ -10,13 +10,31 @@ import { MenuListTable } from "@/lib/menu-list/table/menu-list-table";
import { Shell } from "@/components/shell"
import * as React from "react"
import { InformationButton } from "@/components/information/information-button";
-export default async function MenuListPage() {
+import { useTranslation } from "@/i18n";
+interface MenuListPageProps {
+ params: Promise<{ lng: string }>
+}
+
+export default async function MenuListPage({ params }: MenuListPageProps) {
+ const { lng } = await params
+ const { t } = await useTranslation(lng, 'menu')
+
// 초기 데이터 로드
const [menusResult, usersResult] = await Promise.all([
getMenuAssignments(),
getActiveUsers()
]);
+ // 서버사이드에서 번역된 메뉴 데이터 생성
+ const translatedMenus = menusResult.data?.map(menu => ({
+ ...menu,
+ sectionTitle: menu.sectionTitle || "",
+ translatedMenuTitle: t(menu.menuTitle || ""),
+ translatedSectionTitle: t(menu.sectionTitle || ""),
+ translatedMenuGroup: menu.menuGroup ? t(menu.menuGroup) : null,
+ translatedMenuDescription: menu.menuDescription ? t(menu.menuDescription) : null
+ })) || [];
+
return (
<Shell className="gap-2">
<div className="flex items-center justify-between space-y-2">
@@ -60,7 +78,7 @@ export default async function MenuListPage() {
<CardContent>
<Suspense fallback={<div className="text-center py-8">로딩 중...</div>}>
<MenuListTable
- initialMenus={menusResult.data || []}
+ initialMenus={translatedMenus}
initialUsers={usersResult.data || []}
/>
</Suspense>
diff --git a/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/basic-info-client.tsx b/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/basic-info-client.tsx
index d00bfaa8..e92edc11 100644
--- a/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/basic-info-client.tsx
+++ b/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/basic-info-client.tsx
@@ -18,6 +18,12 @@ import { toast } from "sonner";
import { VendorData, VendorFormData, VendorAttachment } from "./types";
import { updateVendorData } from "./actions";
import { noDataString } from "./constants";
+import { PQSimpleDialog } from "@/components/vendor-info/pq-simple-dialog";
+import { SiteVisitDetailDialog } from "@/lib/site-visit/site-visit-detail-dialog";
+import { DocumentStatusDialog } from "@/components/vendor-regular-registrations/document-status-dialog";
+import { AdditionalInfoDialog } from "@/components/vendor-regular-registrations/additional-info-dialog";
+import { getSiteVisitRequestsByVendorId } from "@/lib/site-visit/service";
+import { fetchVendorRegistrationStatus } from "@/lib/vendor-regular-registrations/service";
import {
Table,
TableBody,
@@ -314,6 +320,16 @@ export default function BasicInfoClient({
}: BasicInfoClientProps) {
const [editMode, setEditMode] = useState(false);
const [isPending, startTransition] = useTransition();
+
+ // 다이얼로그 상태
+ const [pqDialogOpen, setPqDialogOpen] = useState(false);
+ const [siteVisitDialogOpen, setSiteVisitDialogOpen] = useState(false);
+ const [contractDialogOpen, setContractDialogOpen] = useState(false);
+ const [additionalInfoDialogOpen, setAdditionalInfoDialogOpen] = useState(false);
+
+ // 각 다이얼로그에 필요한 데이터 상태
+ const [selectedSiteVisitRequest, setSelectedSiteVisitRequest] = useState<any>(null);
+ const [registrationData, setRegistrationData] = useState<any>(null);
const [formData, setFormData] = useState<VendorFormData>({
vendorName: initialData?.vendorName || "",
representativeName: initialData?.representativeName || "",
@@ -326,6 +342,8 @@ export default function BasicInfoClient({
fax: initialData?.fax || "",
email: initialData?.email || "",
address: initialData?.address || "",
+ addressDetail: initialData?.addressDetail || "",
+ postalCode: initialData?.postalCode || "",
businessSize: initialData?.businessSize || "",
country: initialData?.country || "",
website: initialData?.website || "",
@@ -363,6 +381,8 @@ export default function BasicInfoClient({
fax: initialData?.fax || "",
email: initialData?.email || "",
address: initialData?.address || "",
+ addressDetail: initialData?.addressDetail || "",
+ postalCode: initialData?.postalCode || "",
businessSize: initialData?.businessSize || "",
country: initialData?.country || "",
website: initialData?.website || "",
@@ -387,6 +407,56 @@ export default function BasicInfoClient({
);
};
+ // PQ 조회 핸들러
+ const handlePQView = () => {
+ setPqDialogOpen(true);
+ };
+
+ // 실사 정보 조회 핸들러
+ const handleSiteVisitView = async () => {
+ try {
+ const siteVisitRequests = await getSiteVisitRequestsByVendorId(parseInt(vendorId));
+ if (siteVisitRequests.length === 0) {
+ toast.info("실사 정보가 없습니다.");
+ return;
+ }
+ setSelectedSiteVisitRequest(siteVisitRequests[0]); // 첫 번째 실사 정보 선택
+ setSiteVisitDialogOpen(true);
+ } catch (error) {
+ console.error("실사 정보 조회 오류:", error);
+ toast.error("실사 정보를 불러오는데 실패했습니다.");
+ }
+ };
+
+ // 기본계약 현황 조회 핸들러
+ const handleContractView = async () => {
+ try {
+ const result = await fetchVendorRegistrationStatus(parseInt(vendorId));
+ if (!result.success || !result.data) {
+ toast.info("기본계약 정보가 없습니다.");
+ return;
+ }
+
+ // 등록 데이터가 있는지 확인
+ const registrationRecord = result.data;
+ if (!registrationRecord || !registrationRecord.documentSubmissions) {
+ toast.info("정규등록 정보가 없습니다.");
+ return;
+ }
+
+ setRegistrationData(registrationRecord);
+ setContractDialogOpen(true);
+ } catch (error) {
+ console.error("기본계약 정보 조회 오류:", error);
+ toast.error("기본계약 정보를 불러오는데 실패했습니다.");
+ }
+ };
+
+ // 추가정보 조회 핸들러
+ const handleAdditionalInfoView = () => {
+ setAdditionalInfoDialogOpen(true);
+ };
+
if (!initialData) {
return (
<div className="p-6 bg-background max-w-full">
@@ -459,12 +529,12 @@ export default function BasicInfoClient({
fieldKey="vendorName"
onChange={(value) => updateField("vendorName", value)}
/>
- <InfoItem
+ {/* <InfoItem
title="설립일"
// 현재 필드 없고 linter error 나도 무시. createdAt은 데이터베이스 생성시점이므로 잘못된 필드.
value={initialData.establishmentDate}
type="readonly"
- />
+ /> */}
<InfoItem
title="대표전화"
value={formData.phone}
@@ -473,14 +543,14 @@ export default function BasicInfoClient({
fieldKey="phone"
onChange={(value) => updateField("phone", value)}
/>
- <InfoItem
+ {/* <InfoItem
title="팩스"
value={formData.fax}
isEditable={true}
editMode={editMode}
fieldKey="fax"
onChange={(value) => updateField("fax", value)}
- />
+ /> */}
<InfoItem
title="업체유형"
value={formData.businessType}
@@ -489,7 +559,7 @@ export default function BasicInfoClient({
fieldKey="businessType"
onChange={(value) => updateField("businessType", value)}
/>
- <InfoItem
+ {/* <InfoItem
title="소개자료"
value={`회사: ${
attachmentsByType.COMPANY_INTRO?.length || 0
@@ -498,7 +568,7 @@ export default function BasicInfoClient({
editMode={editMode}
type="file-button"
onFileButtonClick={() => handleFileManagement("소개자료")}
- />
+ /> */}
<InfoItem
title="정기평가 등급"
value={
@@ -529,6 +599,22 @@ export default function BasicInfoClient({
onChange={(value) => updateField("address", value)}
/>
<InfoItem
+ title="상세주소"
+ value={formData.addressDetail}
+ isEditable={true}
+ editMode={editMode}
+ fieldKey="addressDetail"
+ onChange={(value) => updateField("addressDetail", value)}
+ />
+ <InfoItem
+ title="우편번호"
+ value={formData.postalCode}
+ isEditable={true}
+ editMode={editMode}
+ fieldKey="postalCode"
+ onChange={(value) => updateField("postalCode", value)}
+ />
+ <InfoItem
title="E-mail"
value={formData.email}
isEditable={true}
@@ -536,7 +622,7 @@ export default function BasicInfoClient({
fieldKey="email"
onChange={(value) => updateField("email", value)}
/>
- <InfoItem
+ {/* <InfoItem
title="사업유형"
value={formData.businessType}
isEditable={true}
@@ -556,7 +642,7 @@ export default function BasicInfoClient({
]}
onChange={(value) => updateField("businessSize", value)}
placeholder="기업규모를 선택하세요"
- />
+ /> */}
<InfoItem
title="사업자등록증"
value={`${
@@ -586,11 +672,11 @@ export default function BasicInfoClient({
}
isEditable={true}
/>
- <InfoItem
+ {/* <InfoItem
title="그룹사"
value={initialData.classificationInfo?.groupCompany || null}
isEditable={true}
- />
+ /> */}
<InfoItem
title="국가"
value={formData.country}
@@ -602,13 +688,13 @@ export default function BasicInfoClient({
<InfoItem
title="선호언어"
value={
- initialData.classificationInfo?.preferredLanguage || null
+ initialData.classificationInfo?.preferredLanguage || ""
}
isEditable={true}
/>
<InfoItem
title="산업유형"
- value={initialData.classificationInfo?.industryType || null}
+ value={initialData.classificationInfo?.industryType || ""}
isEditable={true}
/>
<InfoItem
@@ -622,7 +708,7 @@ export default function BasicInfoClient({
<InfoItem
title="당사거래비중"
value={
- initialData.evaluationInfo?.companyTransactionRatio || null
+ initialData.evaluationInfo?.companyTransactionRatio || ""
}
type="readonly"
/>
@@ -633,7 +719,7 @@ export default function BasicInfoClient({
<Separator />
{/* 상세정보 */}
- <InfoSection
+ {/* <InfoSection
title="상세정보"
column1={
<div className="space-y-2">
@@ -749,12 +835,12 @@ export default function BasicInfoClient({
</div>
</div>
}
- />
+ /> */}
- <Separator />
+ {/* <Separator /> */}
{/* 매출정보 */}
- <WideInfoSection
+ {/* <WideInfoSection
title="매출정보"
subtitle="(3개년)"
noPadding={true}
@@ -898,12 +984,12 @@ export default function BasicInfoClient({
</TableBody>
</Table>
}
- />
+ /> */}
- <Separator />
+ {/* <Separator /> */}
{/* 실사정보 */}
- <InfoSection
+ {/* <InfoSection
title="실사정보"
subtitle="(3년)"
column1={
@@ -1004,12 +1090,12 @@ export default function BasicInfoClient({
</div>
</div>
}
- />
+ /> */}
- <Separator />
+ {/* <Separator /> */}
{/* 계약정보 */}
- <InfoSection
+ {/* <InfoSection
title="계약정보"
column1={
<div className="space-y-2">
@@ -1101,8 +1187,80 @@ export default function BasicInfoClient({
))}
</div>
}
- />
+ /> */}
+
+ {/* <Separator /> */}
+
+ {/* 추가 조회 기능 버튼들 */}
+ <div className="border rounded-lg p-6">
+ <div className="text-lg font-semibold mb-4">상세 정보 조회</div>
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
+ <Button
+ variant="outline"
+ onClick={handlePQView}
+ className="h-20 flex flex-col items-center justify-center space-y-2"
+ >
+ <div className="text-sm font-medium">PQ 조회</div>
+ <div className="text-xs text-muted-foreground">제출된 PQ 정보 확인</div>
+ </Button>
+
+ <Button
+ variant="outline"
+ onClick={handleSiteVisitView}
+ className="h-20 flex flex-col items-center justify-center space-y-2"
+ >
+ <div className="text-sm font-medium">실사 정보</div>
+ <div className="text-xs text-muted-foreground">협력업체 방문실사 조회</div>
+ </Button>
+
+ <Button
+ variant="outline"
+ onClick={handleContractView}
+ className="h-20 flex flex-col items-center justify-center space-y-2"
+ >
+ <div className="text-sm font-medium">정규업체 등록 현황</div>
+ <div className="text-xs text-muted-foreground">정규업체 등록 현황 보기</div>
+ </Button>
+
+ <Button
+ variant="outline"
+ onClick={handleAdditionalInfoView}
+ className="h-20 flex flex-col items-center justify-center space-y-2"
+ >
+ <div className="text-sm font-medium">추가정보</div>
+ <div className="text-xs text-muted-foreground">업체 추가정보 조회</div>
+ </Button>
+ </div>
+ </div>
</div>
+
+ {/* 다이얼로그들 */}
+ <PQSimpleDialog
+ open={pqDialogOpen}
+ onOpenChange={setPqDialogOpen}
+ vendorId={vendorId}
+ />
+
+ <SiteVisitDetailDialog
+ isOpen={siteVisitDialogOpen}
+ onOpenChange={setSiteVisitDialogOpen}
+ selectedRequest={selectedSiteVisitRequest}
+ />
+
+ {registrationData && (
+ <DocumentStatusDialog
+ open={contractDialogOpen}
+ onOpenChange={setContractDialogOpen}
+ registration={registrationData}
+ />
+ )}
+
+ <AdditionalInfoDialog
+ open={additionalInfoDialogOpen}
+ onOpenChange={setAdditionalInfoDialogOpen}
+ vendorId={parseInt(vendorId)}
+ readonly={true}
+ />
</div>
);
}
diff --git a/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/types.ts b/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/types.ts
index 510ae361..ead3a44c 100644
--- a/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/types.ts
+++ b/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/types.ts
@@ -118,6 +118,8 @@ export interface VendorData {
vendorCode: string;
taxId: string;
address: string;
+ addressDetail: string;
+ postalCode: string;
businessSize: string;
country: string;
phone: string;
@@ -163,6 +165,8 @@ export interface VendorFormData {
representativeBirth: string;
representativePhone: string;
representativeEmail: string;
+ addressDetail: string;
+ postalCode: string;
phone: string;
fax: string;
email: string;
diff --git a/app/api/files/[...path]/route.ts b/app/api/files/[...path]/route.ts
index 9ef11bc7..0bc2e22f 100644
--- a/app/api/files/[...path]/route.ts
+++ b/app/api/files/[...path]/route.ts
@@ -45,6 +45,9 @@ const isAllowedPath = (requestedPath: string): boolean => {
'vendor-evaluation',
'evaluation-attachments',
'vendor-attachments',
+ 'vendor-attachments/nda',
+ 'vendors/nda',
+ 'vendors',
'pq',
'pq/vendor'
];
diff --git a/app/api/upload/basicContract/chunk/route.ts b/app/api/upload/basicContract/chunk/route.ts
index 383a8f36..db3d591e 100644
--- a/app/api/upload/basicContract/chunk/route.ts
+++ b/app/api/upload/basicContract/chunk/route.ts
@@ -33,83 +33,97 @@ export async function POST(request: NextRequest) {
console.log(`📦 청크 저장 완료: ${chunkIndex + 1}/${totalChunks} (${buffer.length} bytes)`);
// 마지막 청크인 경우 모든 청크를 합쳐 최종 파일 생성
- if (chunkIndex === totalChunks - 1) {
- console.log(`🔄 파일 병합 시작: ${filename}`);
-
- try {
- // 모든 청크를 순서대로 읽어서 병합
- const chunks: Buffer[] = [];
- let totalSize = 0;
-
- for (let i = 0; i < totalChunks; i++) {
- const chunkData = await readFile(path.join(tempDir, `chunk-${i}`));
- chunks.push(chunkData);
- totalSize += chunkData.length;
- }
-
- // 모든 청크를 하나의 Buffer로 병합
- const mergedBuffer = Buffer.concat(chunks, totalSize);
-
- const mergedFile = new File([mergedBuffer], filename, {
- type: chunk.type || 'application/octet-stream',
- lastModified: Date.now(),
- });
-
- const saveResult = await saveDRMFile(
- mergedFile,
- decryptWithServerAction, // 복호화 함수
- 'basicContract/template', // 저장 디렉토리
- // userId // 선택
- );
+// 마지막 청크인 경우 모든 청크를 합쳐 최종 파일 생성
+if (chunkIndex === totalChunks - 1) {
+ console.log(`🔄 파일 병합 시작: ${filename}`);
+
+ try {
+ // 모든 청크를 순서대로 읽어서 병합
+ const chunks: Buffer[] = [];
+ let totalSize = 0;
+
+ for (let i = 0; i < totalChunks; i++) {
+ const chunkData = await readFile(path.join(tempDir, `chunk-${i}`));
+ chunks.push(chunkData);
+ totalSize += chunkData.length;
+ }
+
+ // 모든 청크를 하나의 Buffer로 병합
+ const mergedBuffer = Buffer.concat(chunks, totalSize);
+ console.log(`📄 병합 완료: ${filename} (총 ${totalSize} bytes)`);
+ // 환경에 따른 저장 방식 선택
+ const isProduction = process.env.NODE_ENV === 'production';
+ let saveResult;
-
- console.log(`📄 병합 완료: ${filename} (총 ${totalSize} bytes)`);
+ if (isProduction) {
+ // Production: DRM 파일 처리
+ console.log(`🔐 Production 환경 - DRM 파일 처리: ${filename}`);
+
+ const mergedFile = new File([mergedBuffer], filename, {
+ type: chunk.type || 'application/octet-stream',
+ lastModified: Date.now(),
+ });
- // 공용 함수를 사용하여 파일 저장
- // const saveResult = await saveBuffer({
- // buffer: mergedBuffer,
- // fileName: filename,
- // directory: 'basicContract/template',
- // originalName: filename
- // });
+ saveResult = await saveDRMFile(
+ mergedFile,
+ decryptWithServerAction, // 복호화 함수
+ 'basicContract/template', // 저장 디렉토리
+ // userId // 선택사항
+ );
+ } else {
+ // Development: 일반 파일 저장
+ console.log(`🛠️ Development 환경 - 일반 파일 저장: ${filename}`);
+
+ saveResult = await saveBuffer({
+ buffer: mergedBuffer,
+ fileName: filename,
+ directory: 'basicContract/template',
+ originalName: filename
+ });
+ }
- // 임시 파일 정리 (비동기로 처리)
- rm(tempDir, { recursive: true, force: true })
- .then(() => console.log(`🗑️ 임시 파일 정리 완료: ${fileId}`))
- .catch((e: unknown) => console.error('청크 정리 오류:', e));
+ // 임시 파일 정리 (비동기로 처리)
+ rm(tempDir, { recursive: true, force: true })
+ .then(() => console.log(`🗑️ 임시 파일 정리 완료: ${fileId}`))
+ .catch((e: unknown) => console.error('청크 정리 오류:', e));
- if (saveResult.success) {
- console.log(`✅ 최종 파일 저장 완료: ${saveResult.fileName}`);
-
- return NextResponse.json({
- success: true,
- fileName: filename,
- filePath: saveResult.publicPath,
- hashedFileName: saveResult.fileName,
- fileSize: totalSize
- });
- } else {
- console.error('파일 저장 실패:', saveResult.error);
- return NextResponse.json({
- success: false,
- error: saveResult.error || '파일 저장에 실패했습니다'
- }, { status: 500 });
- }
-
- } catch (mergeError) {
- console.error('파일 병합 오류:', mergeError);
-
- // 오류 발생 시 임시 파일 정리
- rm(tempDir, { recursive: true, force: true })
- .catch((e: unknown) => console.error('임시 파일 정리 오류:', e));
-
- return NextResponse.json({
- success: false,
- error: '파일 병합 중 오류가 발생했습니다'
- }, { status: 500 });
- }
+ if (saveResult.success) {
+ const envPrefix = isProduction ? '🔐' : '🛠️';
+ console.log(`${envPrefix} 최종 파일 저장 완료: ${saveResult.fileName}`);
+
+ return NextResponse.json({
+ success: true,
+ fileName: filename,
+ filePath: saveResult.publicPath,
+ hashedFileName: saveResult.fileName,
+ fileSize: totalSize,
+ environment: isProduction ? 'production' : 'development',
+ processingType: isProduction ? 'DRM' : 'standard'
+ });
+ } else {
+ const envPrefix = isProduction ? '🔐' : '🛠️';
+ console.error(`${envPrefix} 파일 저장 실패:`, saveResult.error);
+ return NextResponse.json({
+ success: false,
+ error: saveResult.error || '파일 저장에 실패했습니다',
+ environment: isProduction ? 'production' : 'development'
+ }, { status: 500 });
}
+
+ } catch (mergeError) {
+ console.error('파일 병합 오류:', mergeError);
+
+ // 오류 발생 시 임시 파일 정리
+ rm(tempDir, { recursive: true, force: true })
+ .catch((e: unknown) => console.error('임시 파일 정리 오류:', e));
+
+ return NextResponse.json({
+ success: false,
+ error: '파일 병합 중 오류가 발생했습니다'
+ }, { status: 500 });
+ }
+}
return NextResponse.json({
success: true,