summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/[lng]/evcp/(evcp)/(eng)/swp-document-upload/page.tsx58
-rw-r--r--app/[lng]/evcp/(evcp)/(eng)/swp-document-upload/swp-document-page.tsx231
-rw-r--r--app/[lng]/partners/(partners)/swp-document-upload/vendor-document-page.tsx346
-rw-r--r--app/api/swp/upload/route.ts57
4 files changed, 239 insertions, 453 deletions
diff --git a/app/[lng]/evcp/(evcp)/(eng)/swp-document-upload/page.tsx b/app/[lng]/evcp/(evcp)/(eng)/swp-document-upload/page.tsx
deleted file mode 100644
index 25a0bfe6..00000000
--- a/app/[lng]/evcp/(evcp)/(eng)/swp-document-upload/page.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import { Suspense } from "react";
-import { Skeleton } from "@/components/ui/skeleton";
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
-import SwpDocumentPage from "./swp-document-page";
-
-export const metadata = {
- title: "SWP 문서 관리",
- description: "SWP 시스템 문서 조회 및 동기화",
-};
-
-// ============================================================================
-// 로딩 스켈레톤
-// ============================================================================
-
-function SwpDocumentSkeleton() {
- return (
- <Card>
- <CardHeader>
- <div className="flex items-center justify-between">
- <Skeleton className="h-8 w-32" />
- <Skeleton className="h-10 w-40" />
- </div>
- </CardHeader>
- <CardContent className="space-y-4">
- <Skeleton className="h-32 w-full" />
- <Skeleton className="h-96 w-full" />
- </CardContent>
- </Card>
- );
-}
-
-export default async function SwpDocumentUploadPage({
- searchParams,
-}: {
- searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
-}) {
- const params = await searchParams;
-
- return (
- <div className="container mx-auto py-6 space-y-6">
- {/* 헤더 */}
- <Card>
- <CardHeader>
- <CardTitle className="text-2xl">SWP 문서 관리</CardTitle>
- <CardDescription>
- 외부 시스템(SWP)에서 문서 및 첨부파일을 조회하고 동기화합니다.
- 문서 → 리비전 → 파일 계층 구조로 확인할 수 있습니다.
- </CardDescription>
- </CardHeader>
- </Card>
-
- {/* 메인 컨텐츠 */}
- <Suspense fallback={<SwpDocumentSkeleton />}>
- <SwpDocumentPage searchParams={params} />
- </Suspense>
- </div>
- );
-}
diff --git a/app/[lng]/evcp/(evcp)/(eng)/swp-document-upload/swp-document-page.tsx b/app/[lng]/evcp/(evcp)/(eng)/swp-document-upload/swp-document-page.tsx
deleted file mode 100644
index eedb68e2..00000000
--- a/app/[lng]/evcp/(evcp)/(eng)/swp-document-upload/swp-document-page.tsx
+++ /dev/null
@@ -1,231 +0,0 @@
-"use client";
-
-import { useState, useEffect, useTransition } from "react";
-import { useRouter, useSearchParams } from "next/navigation";
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
-import { Alert, AlertDescription } from "@/components/ui/alert";
-import { Skeleton } from "@/components/ui/skeleton";
-import { InfoIcon } from "lucide-react";
-import { SwpTable } from "@/lib/swp/table/swp-table";
-import { SwpTableToolbar } from "@/lib/swp/table/swp-table-toolbar";
-import {
- fetchSwpDocuments,
- fetchProjectList,
- fetchSwpStats,
- type SwpTableFilters,
- type SwpDocumentWithStats,
-} from "@/lib/swp/actions";
-
-interface SwpDocumentPageProps {
- searchParams: { [key: string]: string | string[] | undefined };
-}
-
-export default function SwpDocumentPage({ searchParams }: SwpDocumentPageProps) {
- const router = useRouter();
- const params = useSearchParams();
- const [isPending, startTransition] = useTransition();
-
- // URL에서 필터 파라미터 추출
- const initialFilters: SwpTableFilters = {
- projNo: (searchParams.projNo as string) || "",
- docNo: (searchParams.docNo as string) || "",
- docTitle: (searchParams.docTitle as string) || "",
- pkgNo: (searchParams.pkgNo as string) || "",
- vndrCd: (searchParams.vndrCd as string) || "",
- stage: (searchParams.stage as string) || "",
- };
-
- const initialPage = parseInt((searchParams.page as string) || "1", 10);
- const initialPageSize = parseInt((searchParams.pageSize as string) || "100", 10);
-
- // 상태 관리
- const [documents, setDocuments] = useState<SwpDocumentWithStats[]>([]);
- const [total, setTotal] = useState(0);
- const [page, setPage] = useState(initialPage);
- const [pageSize] = useState(initialPageSize);
- const [totalPages, setTotalPages] = useState(0);
- const [filters, setFilters] = useState<SwpTableFilters>(initialFilters);
- const [projects, setProjects] = useState<Array<{ PROJ_NO: string; PROJ_NM: string }>>([]);
- const [stats, setStats] = useState({
- total_documents: 0,
- total_revisions: 0,
- total_files: 0,
- last_sync: null as Date | null,
- });
- const [isLoading, setIsLoading] = useState(true);
- const [error, setError] = useState<string | null>(null);
-
- // 초기 데이터 로드
- useEffect(() => {
- loadInitialData();
- }, []);
-
- // 필터 변경 시 데이터 재로드
- useEffect(() => {
- if (!isLoading) {
- loadDocuments();
- }
- }, [filters, page]);
-
- const loadInitialData = async () => {
- try {
- setIsLoading(true);
- setError(null);
-
- // 병렬로 데이터 로드
- const [projectsData, statsData, documentsData] = await Promise.all([
- fetchProjectList(),
- fetchSwpStats(),
- fetchSwpDocuments({
- page,
- pageSize,
- filters: Object.keys(initialFilters).length > 0 ? initialFilters : undefined,
- }),
- ]);
-
- setProjects(projectsData);
- setStats(statsData);
- setDocuments(documentsData.data);
- setTotal(documentsData.total);
- setTotalPages(documentsData.totalPages);
- } catch (err) {
- console.error("초기 데이터 로드 실패:", err);
- setError(err instanceof Error ? err.message : "데이터 로드 실패");
- } finally {
- setIsLoading(false);
- }
- };
-
- const loadDocuments = async () => {
- startTransition(async () => {
- try {
- const data = await fetchSwpDocuments({
- page,
- pageSize,
- filters: Object.keys(filters).some((key) => filters[key as keyof SwpTableFilters])
- ? filters
- : undefined,
- });
-
- setDocuments(data.data);
- setTotal(data.total);
- setTotalPages(data.totalPages);
-
- // URL 업데이트
- const params = new URLSearchParams();
- if (filters.projNo) params.set("projNo", filters.projNo);
- if (filters.docNo) params.set("docNo", filters.docNo);
- if (filters.docTitle) params.set("docTitle", filters.docTitle);
- if (filters.pkgNo) params.set("pkgNo", filters.pkgNo);
- if (filters.vndrCd) params.set("vndrCd", filters.vndrCd);
- if (filters.stage) params.set("stage", filters.stage);
- if (page !== 1) params.set("page", page.toString());
-
- router.push(`?${params.toString()}`, { scroll: false });
- } catch (err) {
- console.error("문서 로드 실패:", err);
- setError(err instanceof Error ? err.message : "문서 로드 실패");
- }
- });
- };
-
- const handleFiltersChange = (newFilters: SwpTableFilters) => {
- setFilters(newFilters);
- setPage(1); // 필터 변경 시 첫 페이지로
- };
-
- const handlePageChange = (newPage: number) => {
- setPage(newPage);
- };
-
- if (isLoading) {
- return (
- <Card>
- <CardHeader>
- <Skeleton className="h-8 w-48" />
- <Skeleton className="h-4 w-96" />
- </CardHeader>
- <CardContent className="space-y-4">
- <Skeleton className="h-32 w-full" />
- <Skeleton className="h-96 w-full" />
- </CardContent>
- </Card>
- );
- }
-
- if (error) {
- return (
- <Alert variant="destructive">
- <AlertDescription>{error}</AlertDescription>
- </Alert>
- );
- }
-
- return (
- <div className="space-y-6">
- {/* 통계 카드 */}
- <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
- <Card>
- <CardHeader className="pb-3">
- <CardDescription>총 문서</CardDescription>
- <CardTitle className="text-3xl">{stats.total_documents.toLocaleString()}</CardTitle>
- </CardHeader>
- </Card>
- <Card>
- <CardHeader className="pb-3">
- <CardDescription>총 리비전</CardDescription>
- <CardTitle className="text-3xl">{stats.total_revisions.toLocaleString()}</CardTitle>
- </CardHeader>
- </Card>
- <Card>
- <CardHeader className="pb-3">
- <CardDescription>총 파일</CardDescription>
- <CardTitle className="text-3xl">{stats.total_files.toLocaleString()}</CardTitle>
- </CardHeader>
- </Card>
- <Card>
- <CardHeader className="pb-3">
- <CardDescription>마지막 동기화</CardDescription>
- <CardTitle className="text-lg">
- {stats.last_sync
- ? new Date(stats.last_sync).toLocaleDateString("ko-KR")
- : "없음"}
- </CardTitle>
- </CardHeader>
- </Card>
- </div>
-
- {/* 안내 메시지 */}
- {documents.length === 0 && !filters.projNo && (
- <Alert>
- <InfoIcon className="h-4 w-4" />
- <AlertDescription>
- 시작하려면 프로젝트를 선택하고 <strong>SWP 동기화</strong> 버튼을 클릭하세요.
- </AlertDescription>
- </Alert>
- )}
-
- {/* 메인 테이블 */}
- <Card>
- <CardHeader>
- <SwpTableToolbar
- filters={filters}
- onFiltersChange={handleFiltersChange}
- projects={projects}
- />
- </CardHeader>
- <CardContent>
- <SwpTable
- initialData={documents}
- total={total}
- page={page}
- pageSize={pageSize}
- totalPages={totalPages}
- onPageChange={handlePageChange}
- />
- </CardContent>
- </Card>
- </div>
- );
-}
-
diff --git a/app/[lng]/partners/(partners)/swp-document-upload/vendor-document-page.tsx b/app/[lng]/partners/(partners)/swp-document-upload/vendor-document-page.tsx
index ba78bfdf..dc6fbe7c 100644
--- a/app/[lng]/partners/(partners)/swp-document-upload/vendor-document-page.tsx
+++ b/app/[lng]/partners/(partners)/swp-document-upload/vendor-document-page.tsx
@@ -1,11 +1,11 @@
"use client";
-import { useState, useEffect, useTransition } from "react";
-import { useRouter, useSearchParams } from "next/navigation";
+import { useState, useEffect, useMemo, useCallback } from "react";
+import { useDropzone } from "react-dropzone";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Skeleton } from "@/components/ui/skeleton";
-import { InfoIcon } from "lucide-react";
+import { InfoIcon, Upload } from "lucide-react";
import { SwpTable } from "@/lib/swp/table/swp-table";
import { SwpTableToolbar } from "@/lib/swp/table/swp-table-toolbar";
import {
@@ -14,36 +14,20 @@ import {
fetchVendorSwpStats,
getVendorSessionInfo,
} from "@/lib/swp/vendor-actions";
-import { type SwpTableFilters, type SwpDocumentWithStats } from "@/lib/swp/actions";
+import type { DocumentListItem } from "@/lib/swp/document-service";
+import { toast } from "sonner";
interface VendorDocumentPageProps {
searchParams: { [key: string]: string | string[] | undefined };
}
export default function VendorDocumentPage({ searchParams }: VendorDocumentPageProps) {
- const router = useRouter();
- const params = useSearchParams();
- const [isPending, startTransition] = useTransition();
-
- // URL에서 필터 파라미터 추출 (vndrCd는 제외 - 서버에서 자동 설정)
- const initialFilters: SwpTableFilters = {
- projNo: (searchParams.projNo as string) || "",
- docNo: (searchParams.docNo as string) || "",
- docTitle: (searchParams.docTitle as string) || "",
- pkgNo: (searchParams.pkgNo as string) || "",
- stage: (searchParams.stage as string) || "",
- };
-
- const initialPage = parseInt((searchParams.page as string) || "1", 10);
- const initialPageSize = parseInt((searchParams.pageSize as string) || "100", 10);
+ // URL에서 프로젝트 번호만 사용 (나머지는 클라이언트 필터링)
+ const initialProjNo = (searchParams.projNo as string) || "";
// 상태 관리
- const [documents, setDocuments] = useState<SwpDocumentWithStats[]>([]);
- const [total, setTotal] = useState(0);
- const [page, setPage] = useState(initialPage);
- const [pageSize] = useState(initialPageSize);
- const [totalPages, setTotalPages] = useState(0);
- const [filters, setFilters] = useState<SwpTableFilters>(initialFilters);
+ const [documents, setDocuments] = useState<DocumentListItem[]>([]);
+ const [projNo, setProjNo] = useState(initialProjNo);
const [projects, setProjects] = useState<Array<{ PROJ_NO: string; PROJ_NM: string }>>([]);
const [stats, setStats] = useState({
total_documents: 0,
@@ -59,91 +43,132 @@ export default function VendorDocumentPage({ searchParams }: VendorDocumentPageP
companyId: number;
} | null>(null);
const [isLoading, setIsLoading] = useState(true);
+ const [isRefreshing, setIsRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);
- // 초기 데이터 로드
- useEffect(() => {
- loadInitialData();
- }, []);
+ // 클라이언트 필터
+ const [searchFilters, setSearchFilters] = useState({
+ docNo: (searchParams.docNo as string) || "",
+ docTitle: (searchParams.docTitle as string) || "",
+ pkgNo: (searchParams.pkgNo as string) || "",
+ stage: (searchParams.stage as string) || "",
+ });
- // 필터 변경 시 데이터 재로드
- useEffect(() => {
- if (!isLoading) {
- loadDocuments();
+ // Dropzone 설정
+ const [droppedFiles, setDroppedFiles] = useState<File[]>([]);
+
+ // 파일 드롭 핸들러
+ const onDrop = useCallback((acceptedFiles: File[]) => {
+ if (acceptedFiles.length > 0) {
+ setDroppedFiles(acceptedFiles);
}
- }, [filters, page]);
+ }, []);
+
+ const { getRootProps, getInputProps, isDragActive } = useDropzone({
+ onDrop,
+ noClick: true, // 클릭으로 파일 선택 비활성화 (버튼을 통해서만 선택)
+ noKeyboard: true,
+ });
- const loadInitialData = async () => {
+ const loadInitialData = useCallback(async () => {
try {
setIsLoading(true);
setError(null);
- // 병렬로 데이터 로드 (벤더 정보 포함)
- const [vendorInfoData, projectsData, statsData, documentsData] = await Promise.all([
+ // 병렬로 데이터 로드
+ const [vendorInfoData, projectsData] = await Promise.all([
getVendorSessionInfo(),
fetchVendorProjects(),
- fetchVendorSwpStats(),
- fetchVendorDocuments({
- page,
- pageSize,
- filters: Object.keys(initialFilters).length > 0 ? initialFilters : undefined,
- }),
]);
setVendorInfo(vendorInfoData);
setProjects(projectsData);
- setStats(statsData);
- setDocuments(documentsData.data);
- setTotal(documentsData.total);
- setTotalPages(documentsData.totalPages);
+
+ // 초기 프로젝트가 있으면 문서 로드
+ if (initialProjNo) {
+ const [documentsData, statsData] = await Promise.all([
+ fetchVendorDocuments(initialProjNo),
+ fetchVendorSwpStats(initialProjNo),
+ ]);
+ setDocuments(documentsData);
+ setStats(statsData);
+ }
} catch (err) {
console.error("초기 데이터 로드 실패:", err);
setError(err instanceof Error ? err.message : "데이터 로드 실패");
+ toast.error("데이터 로드 실패");
+ } finally {
+ setIsLoading(false);
}
- setIsLoading(false); // finally 대신 여기서 호출
- };
+ }, [initialProjNo]);
- const loadDocuments = async () => {
- startTransition(async () => {
- try {
- const data = await fetchVendorDocuments({
- page,
- pageSize,
- filters: Object.keys(filters).some((key) => filters[key as keyof SwpTableFilters])
- ? filters
- : undefined,
- });
-
- setDocuments(data.data);
- setTotal(data.total);
- setTotalPages(data.totalPages);
-
- // URL 업데이트
- const params = new URLSearchParams();
- if (filters.projNo) params.set("projNo", filters.projNo);
- if (filters.docNo) params.set("docNo", filters.docNo);
- if (filters.docTitle) params.set("docTitle", filters.docTitle);
- if (filters.pkgNo) params.set("pkgNo", filters.pkgNo);
- if (filters.stage) params.set("stage", filters.stage);
- if (page !== 1) params.set("page", page.toString());
-
- router.push(`?${params.toString()}`, { scroll: false });
- } catch (err) {
- console.error("문서 로드 실패:", err);
- setError(err instanceof Error ? err.message : "문서 로드 실패");
- }
- });
+ const loadDocuments = useCallback(async () => {
+ if (!projNo) return;
+
+ try {
+ setIsRefreshing(true);
+ setError(null);
+
+ const [documentsData, statsData] = await Promise.all([
+ fetchVendorDocuments(projNo),
+ fetchVendorSwpStats(projNo),
+ ]);
+
+ setDocuments(documentsData);
+ setStats(statsData);
+ toast.success("문서 목록을 갱신했습니다");
+ } catch (err) {
+ console.error("문서 로드 실패:", err);
+ setError(err instanceof Error ? err.message : "문서 로드 실패");
+ toast.error("문서 로드 실패");
+ } finally {
+ setIsRefreshing(false);
+ }
+ }, [projNo]);
+
+ // 초기 데이터 로드
+ useEffect(() => {
+ loadInitialData();
+ }, [loadInitialData]);
+
+ // 프로젝트 변경 시 문서 재로드
+ useEffect(() => {
+ if (!isLoading && projNo) {
+ loadDocuments();
+ }
+ }, [projNo, isLoading, loadDocuments]);
+
+ const handleProjectChange = (newProjNo: string) => {
+ setProjNo(newProjNo);
};
- const handleFiltersChange = (newFilters: SwpTableFilters) => {
- setFilters(newFilters);
- setPage(1); // 필터 변경 시 첫 페이지로
+ const handleFiltersChange = (filters: typeof searchFilters) => {
+ setSearchFilters(filters);
};
- const handlePageChange = (newPage: number) => {
- setPage(newPage);
+ const handleRefresh = () => {
+ loadDocuments();
};
+ // 클라이언트 사이드 필터링
+ const filteredDocuments = useMemo(() => {
+ return documents.filter((doc) => {
+ if (searchFilters.docNo && !doc.DOC_NO.toLowerCase().includes(searchFilters.docNo.toLowerCase())) {
+ return false;
+ }
+ if (searchFilters.docTitle && !doc.DOC_TITLE.toLowerCase().includes(searchFilters.docTitle.toLowerCase())) {
+ return false;
+ }
+ if (searchFilters.pkgNo && !doc.PKG_NO?.toLowerCase().includes(searchFilters.pkgNo.toLowerCase())) {
+ return false;
+ }
+ if (searchFilters.stage && doc.STAGE !== searchFilters.stage) {
+ return false;
+ }
+ return true;
+ });
+ }, [documents, searchFilters]);
+
if (isLoading) {
return (
<Card>
@@ -159,78 +184,101 @@ export default function VendorDocumentPage({ searchParams }: VendorDocumentPageP
);
}
-
return (
- <div className="space-y-6">
- {/* 에러 메시지 */}
- {error && (
- <Alert variant="destructive">
- <AlertDescription>{error}</AlertDescription>
- </Alert>
+ <div {...getRootProps()} className="relative">
+ <input {...getInputProps()} />
+
+ {/* 드래그 오버레이 */}
+ {isDragActive && (
+ <div className="fixed inset-0 z-50 bg-primary/20 backdrop-blur-sm flex items-center justify-center">
+ <div className="bg-background border-2 border-dashed border-primary rounded-lg p-12 text-center space-y-4">
+ <Upload className="h-16 w-16 mx-auto text-primary animate-bounce" />
+ <div className="space-y-2">
+ <p className="text-2xl font-semibold">파일을 여기에 드롭하세요</p>
+ <p className="text-muted-foreground">
+ 여러 파일을 한 번에 업로드할 수 있습니다
+ </p>
+ </div>
+ </div>
+ </div>
)}
- {/* 통계 카드 */}
- <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
- <Card>
- <CardHeader className="pb-3">
- <CardDescription>할당된 문서</CardDescription>
- <CardTitle className="text-3xl">{stats.total_documents.toLocaleString()}</CardTitle>
- </CardHeader>
- </Card>
- <Card>
- <CardHeader className="pb-3">
- <CardDescription>총 리비전</CardDescription>
- <CardTitle className="text-3xl">{stats.total_revisions.toLocaleString()}</CardTitle>
- </CardHeader>
- </Card>
- <Card>
- <CardHeader className="pb-3">
- <CardDescription>총 파일</CardDescription>
- <CardTitle className="text-3xl">{stats.total_files.toLocaleString()}</CardTitle>
- </CardHeader>
- </Card>
+ <div className="space-y-6">
+ {/* 에러 메시지 */}
+ {error && (
+ <Alert variant="destructive">
+ <AlertDescription>{error}</AlertDescription>
+ </Alert>
+ )}
+
+ {/* 통계 카드 */}
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
+ <Card>
+ <CardHeader className="pb-3">
+ <CardDescription>할당된 문서</CardDescription>
+ <CardTitle className="text-3xl">{stats.total_documents.toLocaleString()}</CardTitle>
+ </CardHeader>
+ </Card>
+ <Card>
+ <CardHeader className="pb-3">
+ <CardDescription>총 리비전</CardDescription>
+ <CardTitle className="text-3xl">{stats.total_revisions.toLocaleString()}</CardTitle>
+ </CardHeader>
+ </Card>
+ <Card>
+ <CardHeader className="pb-3">
+ <CardDescription>총 파일</CardDescription>
+ <CardTitle className="text-3xl">{stats.total_files.toLocaleString()}</CardTitle>
+ </CardHeader>
+ </Card>
+ <Card>
+ <CardHeader className="pb-3">
+ <CardDescription>업로드한 파일</CardDescription>
+ <CardTitle className="text-3xl text-green-600">
+ {stats.uploaded_files.toLocaleString()}
+ </CardTitle>
+ </CardHeader>
+ </Card>
+ </div>
+
+ {/* 안내 메시지 */}
+ {documents.length === 0 && !projNo && (
+ <Alert>
+ <InfoIcon className="h-4 w-4" />
+ <AlertDescription>
+ 프로젝트를 선택하여 할당된 문서를 확인하세요.
+ </AlertDescription>
+ </Alert>
+ )}
+
+ {/* 메인 테이블 */}
<Card>
- <CardHeader className="pb-3">
- <CardDescription>업로드한 파일</CardDescription>
- <CardTitle className="text-3xl text-green-600">
- {stats.uploaded_files.toLocaleString()}
- </CardTitle>
+ <CardHeader>
+ <SwpTableToolbar
+ projNo={projNo}
+ filters={searchFilters}
+ onProjNoChange={handleProjectChange}
+ onFiltersChange={handleFiltersChange}
+ onRefresh={handleRefresh}
+ isRefreshing={isRefreshing}
+ projects={projects}
+ vendorCode={vendorInfo?.vendorCode}
+ droppedFiles={droppedFiles}
+ onFilesProcessed={() => setDroppedFiles([])}
+ documents={filteredDocuments}
+ userId={String(vendorInfo?.vendorId || "")}
+ />
</CardHeader>
+ <CardContent>
+ <SwpTable
+ documents={filteredDocuments}
+ projNo={projNo}
+ vendorCode={vendorInfo?.vendorCode || ""}
+ userId={String(vendorInfo?.vendorId || "")}
+ />
+ </CardContent>
</Card>
</div>
-
- {/* 안내 메시지 */}
- {documents.length === 0 && !filters.projNo && (
- <Alert>
- <InfoIcon className="h-4 w-4" />
- <AlertDescription>
- 프로젝트를 선택하여 할당된 문서를 확인하세요.
- </AlertDescription>
- </Alert>
- )}
-
- {/* 메인 테이블 */}
- <Card>
- <CardHeader>
- <SwpTableToolbar
- filters={filters}
- onFiltersChange={handleFiltersChange}
- projects={projects}
- vendorCode={vendorInfo?.vendorCode}
- />
- </CardHeader>
- <CardContent>
- <SwpTable
- initialData={documents}
- total={total}
- page={page}
- pageSize={pageSize}
- totalPages={totalPages}
- onPageChange={handlePageChange}
- />
- </CardContent>
- </Card>
</div>
);
}
-
diff --git a/app/api/swp/upload/route.ts b/app/api/swp/upload/route.ts
index d17fcff7..b38c4ff4 100644
--- a/app/api/swp/upload/route.ts
+++ b/app/api/swp/upload/route.ts
@@ -26,7 +26,8 @@ interface InBoxFileInfo {
}
/**
- * 파일명 파싱: [OWN_DOC_NO]_[REV_NO]_[STAGE]_[YYYYMMDDhhmmss].[확장자]
+ * 파일명 파싱: [DOC_NO]_[REV_NO]_[STAGE]_[자유-파일명].[확장자]
+ * 자유 파일명에는 언더스코어가 포함될 수 있음
*/
function parseFileName(fileName: string) {
const lastDotIndex = fileName.lastIndexOf(".");
@@ -35,23 +36,38 @@ function parseFileName(fileName: string) {
const parts = nameWithoutExt.split("_");
- if (parts.length !== 4) {
+ // 최소 4개 파트 필요: docNo, revNo, stage, fileName
+ if (parts.length < 4) {
throw new Error(
`잘못된 파일명 형식입니다: ${fileName}. ` +
- `형식: [OWN_DOC_NO]_[REV_NO]_[STAGE]_[YYYYMMDDhhmmss].확장자`
+ `형식: [DOC_NO]_[REV_NO]_[STAGE]_[파일명].확장자 (언더스코어 최소 3개 필요)`
);
}
- const [ownDocNo, revNo, stage, timestamp] = parts;
+ // 앞에서부터 3개는 고정: docNo, revNo, stage
+ const ownDocNo = parts[0];
+ const revNo = parts[1];
+ const stage = parts[2];
+
+ // 나머지는 자유 파일명 (언더스코어 포함 가능)
+ const customFileName = parts.slice(3).join("_");
- if (!/^\d{14}$/.test(timestamp)) {
- throw new Error(
- `잘못된 타임스탬프 형식입니다: ${timestamp}. ` +
- `YYYYMMDDhhmmss 형식이어야 합니다.`
- );
- }
+ return { ownDocNo, revNo, stage, fileName: customFileName, extension };
+}
- return { ownDocNo, revNo, stage, timestamp, extension };
+/**
+ * 현재 시간을 YYYYMMDDhhmmss 형식으로 반환
+ */
+function generateTimestamp(): string {
+ const now = new Date();
+ const year = now.getFullYear().toString();
+ const month = (now.getMonth() + 1).toString().padStart(2, "0");
+ const day = now.getDate().toString().padStart(2, "0");
+ const hours = now.getHours().toString().padStart(2, "0");
+ const minutes = now.getMinutes().toString().padStart(2, "0");
+ const seconds = now.getSeconds().toString().padStart(2, "0");
+
+ return `${year}${month}${day}${hours}${minutes}${seconds}`;
}
/**
@@ -171,6 +187,10 @@ export async function POST(request: NextRequest) {
const inBoxFileInfos: InBoxFileInfo[] = [];
const swpMountDir = process.env.SWP_MOUNT_DIR || "/mnt/swp-smb-dir/";
+
+ // 업로드 시점의 timestamp 생성 (모든 파일에 동일한 timestamp 사용)
+ const uploadTimestamp = generateTimestamp();
+ console.log(`[upload] 업로드 타임스탬프 생성: ${uploadTimestamp}`);
for (const file of files) {
try {
@@ -178,8 +198,8 @@ export async function POST(request: NextRequest) {
const parsed = parseFileName(file.name);
console.log(`[upload] 파일명 파싱:`, parsed);
- // 네트워크 경로 생성
- const networkPath = path.join(swpMountDir, projNo, cpyCd, parsed.timestamp, file.name);
+ // 네트워크 경로 생성 (timestamp를 경로에만 사용)
+ const networkPath = path.join(swpMountDir, projNo, cpyCd, uploadTimestamp, file.name);
// 파일 중복 체크
try {
@@ -245,8 +265,8 @@ export async function POST(request: NextRequest) {
matchesOriginal: buffer.slice(0, 20).equals(verifyBuffer.slice(0, 20))
});
- // InBox 파일 정보 준비
- const fldPath = `\\\\${projNo}\\\\${cpyCd}\\\\${parsed.timestamp}`;
+ // InBox 파일 정보 준비 (FLD_PATH에 업로드 timestamp 사용)
+ const fldPath = `\\\\${projNo}\\\\${cpyCd}\\\\${uploadTimestamp}`;
inBoxFileInfos.push({
CPY_CD: cpyCd,
@@ -339,12 +359,19 @@ export async function POST(request: NextRequest) {
console.log(`[upload] 완료:`, { success, message, result });
+ // 동기화 완료 정보 추가
+ const syncCompleted = result.successCount > 0;
+ const syncTimestamp = new Date().toISOString();
+
return NextResponse.json({
success,
message,
successCount: result.successCount,
failedCount: result.failedCount,
details: result.details,
+ syncCompleted,
+ syncTimestamp,
+ affectedVndrCd: vndrCd,
});
} catch (error) {
console.error("[upload] 오류:", error);