summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-10-23 18:44:19 +0900
committerjoonhoekim <26rote@gmail.com>2025-10-23 18:44:19 +0900
commit04bd1965c3699a4b29ed9c9627574bfeedd3d6c6 (patch)
tree691b9a6e844a788937a240d47e77e8cfa848a88a /app
parent535e234dbd674bf2e5ecf344e03ed8ae5b2cbd6c (diff)
(김준회) SWP 문서 업로드 (Submisssion) 초기 개발건
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/page.tsx57
-rw-r--r--app/[lng]/partners/(partners)/swp-document-upload/vendor-document-page.tsx230
4 files changed, 576 insertions, 0 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
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 (
+ <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
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<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/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 (
+ <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 DocumentUploadPage({
+ 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">문서 조회 및 업로드</CardTitle>
+ <CardDescription>
+ 프로젝트별 할당된 문서를 조회하고 파일을 업로드할 수 있습니다.
+ </CardDescription>
+ </CardHeader>
+ </Card>
+
+ {/* 메인 컨텐츠 */}
+ <Suspense fallback={<VendorDocumentSkeleton />}>
+ <VendorDocumentPage searchParams={params} />
+ </Suspense>
+ </div>
+ );
+} \ 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<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,
+ uploaded_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([
+ 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 (
+ <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-3xl text-green-600">
+ {stats.uploaded_files.toLocaleString()}
+ </CardTitle>
+ </CardHeader>
+ </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}
+ mode="vendor"
+ />
+ </CardHeader>
+ <CardContent>
+ <SwpTable
+ initialData={documents}
+ total={total}
+ page={page}
+ pageSize={pageSize}
+ totalPages={totalPages}
+ onPageChange={handlePageChange}
+ mode="vendor"
+ />
+ </CardContent>
+ </Card>
+ </div>
+ );
+}
+