From 04bd1965c3699a4b29ed9c9627574bfeedd3d6c6 Mon Sep 17 00:00:00 2001
From: joonhoekim <26rote@gmail.com>
Date: Thu, 23 Oct 2025 18:44:19 +0900
Subject: (김준회) SWP 문서 업로드 (Submisssion) 초기 개발건
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../evcp/(evcp)/(eng)/swp-document-upload/page.tsx | 58 ++++++
.../swp-document-upload/swp-document-page.tsx | 231 +++++++++++++++++++++
.../(partners)/swp-document-upload/page.tsx | 57 +++++
.../swp-document-upload/vendor-document-page.tsx | 230 ++++++++++++++++++++
4 files changed, 576 insertions(+)
create mode 100644 app/[lng]/evcp/(evcp)/(eng)/swp-document-upload/page.tsx
create mode 100644 app/[lng]/evcp/(evcp)/(eng)/swp-document-upload/swp-document-page.tsx
create mode 100644 app/[lng]/partners/(partners)/swp-document-upload/page.tsx
create mode 100644 app/[lng]/partners/(partners)/swp-document-upload/vendor-document-page.tsx
(limited to 'app')
diff --git a/app/[lng]/evcp/(evcp)/(eng)/swp-document-upload/page.tsx b/app/[lng]/evcp/(evcp)/(eng)/swp-document-upload/page.tsx
new file mode 100644
index 00000000..25a0bfe6
--- /dev/null
+++ b/app/[lng]/evcp/(evcp)/(eng)/swp-document-upload/page.tsx
@@ -0,0 +1,58 @@
+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 (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default async function SwpDocumentUploadPage({
+ searchParams,
+}: {
+ searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
+}) {
+ const params = await searchParams;
+
+ return (
+
+ {/* 헤더 */}
+
+
+ SWP 문서 관리
+
+ 외부 시스템(SWP)에서 문서 및 첨부파일을 조회하고 동기화합니다.
+ 문서 → 리비전 → 파일 계층 구조로 확인할 수 있습니다.
+
+
+
+
+ {/* 메인 컨텐츠 */}
+ }>
+
+
+
+ );
+}
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
new file mode 100644
index 00000000..eedb68e2
--- /dev/null
+++ b/app/[lng]/evcp/(evcp)/(eng)/swp-document-upload/swp-document-page.tsx
@@ -0,0 +1,231 @@
+"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([]);
+ const [total, setTotal] = useState(0);
+ const [page, setPage] = useState(initialPage);
+ const [pageSize] = useState(initialPageSize);
+ const [totalPages, setTotalPages] = useState(0);
+ const [filters, setFilters] = useState(initialFilters);
+ const [projects, setProjects] = useState>([]);
+ 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(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 (
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ {error}
+
+ );
+ }
+
+ return (
+
+ {/* 통계 카드 */}
+
+
+
+ 총 문서
+ {stats.total_documents.toLocaleString()}
+
+
+
+
+ 총 리비전
+ {stats.total_revisions.toLocaleString()}
+
+
+
+
+ 총 파일
+ {stats.total_files.toLocaleString()}
+
+
+
+
+ 마지막 동기화
+
+ {stats.last_sync
+ ? new Date(stats.last_sync).toLocaleDateString("ko-KR")
+ : "없음"}
+
+
+
+
+
+ {/* 안내 메시지 */}
+ {documents.length === 0 && !filters.projNo && (
+
+
+
+ 시작하려면 프로젝트를 선택하고 SWP 동기화 버튼을 클릭하세요.
+
+
+ )}
+
+ {/* 메인 테이블 */}
+
+
+
+
+
+
+
+
+
+ );
+}
+
diff --git a/app/[lng]/partners/(partners)/swp-document-upload/page.tsx b/app/[lng]/partners/(partners)/swp-document-upload/page.tsx
new file mode 100644
index 00000000..25eb52aa
--- /dev/null
+++ b/app/[lng]/partners/(partners)/swp-document-upload/page.tsx
@@ -0,0 +1,57 @@
+import { Suspense } from "react";
+import { Skeleton } from "@/components/ui/skeleton";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import VendorDocumentPage from "./vendor-document-page";
+
+export const metadata = {
+ title: "문서 조회 및 업로드",
+ description: "협력업체 문서 조회 및 파일 업로드",
+};
+
+// ============================================================================
+// 로딩 스켈레톤
+// ============================================================================
+
+function VendorDocumentSkeleton() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default async function DocumentUploadPage({
+ searchParams,
+}: {
+ searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
+}) {
+ const params = await searchParams;
+
+ return (
+
+ {/* 헤더 */}
+
+
+ 문서 조회 및 업로드
+
+ 프로젝트별 할당된 문서를 조회하고 파일을 업로드할 수 있습니다.
+
+
+
+
+ {/* 메인 컨텐츠 */}
+ }>
+
+
+
+ );
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..f2469c29
--- /dev/null
+++ b/app/[lng]/partners/(partners)/swp-document-upload/vendor-document-page.tsx
@@ -0,0 +1,230 @@
+"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 {
+ fetchVendorDocuments,
+ fetchVendorProjects,
+ fetchVendorSwpStats,
+ type SwpTableFilters,
+ type SwpDocumentWithStats,
+} from "@/lib/swp/vendor-actions";
+
+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);
+
+ // 상태 관리
+ const [documents, setDocuments] = useState([]);
+ const [total, setTotal] = useState(0);
+ const [page, setPage] = useState(initialPage);
+ const [pageSize] = useState(initialPageSize);
+ const [totalPages, setTotalPages] = useState(0);
+ const [filters, setFilters] = useState(initialFilters);
+ const [projects, setProjects] = useState>([]);
+ const [stats, setStats] = useState({
+ total_documents: 0,
+ total_revisions: 0,
+ total_files: 0,
+ uploaded_files: 0,
+ last_sync: null as Date | null,
+ });
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ // 초기 데이터 로드
+ useEffect(() => {
+ loadInitialData();
+ }, []);
+
+ // 필터 변경 시 데이터 재로드
+ useEffect(() => {
+ if (!isLoading) {
+ loadDocuments();
+ }
+ }, [filters, page]);
+
+ const loadInitialData = async () => {
+ try {
+ setIsLoading(true);
+ setError(null);
+
+ // 병렬로 데이터 로드
+ const [projectsData, statsData, documentsData] = await Promise.all([
+ fetchVendorProjects(),
+ fetchVendorSwpStats(),
+ fetchVendorDocuments({
+ 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 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 handleFiltersChange = (newFilters: SwpTableFilters) => {
+ setFilters(newFilters);
+ setPage(1); // 필터 변경 시 첫 페이지로
+ };
+
+ const handlePageChange = (newPage: number) => {
+ setPage(newPage);
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ {error}
+
+ );
+ }
+
+ return (
+
+ {/* 통계 카드 */}
+
+
+
+ 할당된 문서
+ {stats.total_documents.toLocaleString()}
+
+
+
+
+ 총 리비전
+ {stats.total_revisions.toLocaleString()}
+
+
+
+
+ 총 파일
+ {stats.total_files.toLocaleString()}
+
+
+
+
+ 업로드한 파일
+
+ {stats.uploaded_files.toLocaleString()}
+
+
+
+
+
+ {/* 안내 메시지 */}
+ {documents.length === 0 && !filters.projNo && (
+
+
+
+ 프로젝트를 선택하여 할당된 문서를 확인하세요.
+
+
+ )}
+
+ {/* 메인 테이블 */}
+
+
+
+
+
+
+
+
+
+ );
+}
+
--
cgit v1.2.3