From c62ec046327fd388ebce04571b55910747e69a3b Mon Sep 17 00:00:00 2001 From: dujinkim Date: Tue, 9 Sep 2025 10:32:34 +0000 Subject: (정희성, 최겸, 대표님) formatDate 변경 등 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/bidding/bidding-info-header.tsx | 21 +- components/bidding/price-adjustment-dialog.tsx | 20 +- components/form-data-stat/form-data-stat-table.tsx | 261 +++++++++++++++++---- components/knox/approval/ApprovalCancel.tsx | 15 +- components/knox/approval/ApprovalDetail.tsx | 16 +- components/knox/approval/ApprovalList.tsx | 16 +- components/pq-input/pq-input-tabs.tsx | 13 +- 7 files changed, 235 insertions(+), 127 deletions(-) (limited to 'components') diff --git a/components/bidding/bidding-info-header.tsx b/components/bidding/bidding-info-header.tsx index c140920b..e109a8ca 100644 --- a/components/bidding/bidding-info-header.tsx +++ b/components/bidding/bidding-info-header.tsx @@ -1,31 +1,12 @@ import { Bidding } from '@/db/schema/bidding' import { Building2, Package, User, DollarSign, Calendar } from 'lucide-react' import { contractTypeLabels, biddingTypeLabels } from '@/db/schema/bidding' +import { formatDate } from '@/lib/utils' interface BiddingInfoHeaderProps { bidding: Bidding } -function formatDate(date: Date | string | null | undefined, locale: 'KR' | 'EN' = 'KR'): string { - if (!date) return '' - - const dateObj = typeof date === 'string' ? new Date(date) : date - - if (locale === 'KR') { - return dateObj.toLocaleDateString('ko-KR', { - year: 'numeric', - month: '2-digit', - day: '2-digit' - }).replace(/\./g, '-').replace(/-$/, '') - } - - return dateObj.toLocaleDateString('en-US', { - year: 'numeric', - month: '2-digit', - day: '2-digit' - }) -} - export function BiddingInfoHeader({ bidding }: BiddingInfoHeaderProps) { return (
diff --git a/components/bidding/price-adjustment-dialog.tsx b/components/bidding/price-adjustment-dialog.tsx index b53f9ef1..982d8b90 100644 --- a/components/bidding/price-adjustment-dialog.tsx +++ b/components/bidding/price-adjustment-dialog.tsx @@ -10,6 +10,7 @@ import { } from '@/components/ui/dialog' import { Badge } from '@/components/ui/badge' import { Separator } from '@/components/ui/separator' +import { formatDate } from '@/lib/utils' interface PriceAdjustmentData { id: number @@ -39,15 +40,6 @@ interface PriceAdjustmentDialogProps { vendorName: string } -function formatDate(date: Date | null | undefined): string { - if (!date) return '-' - return new Date(date).toLocaleDateString('ko-KR', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - }) -} - export function PriceAdjustmentDialog({ open, onOpenChange, @@ -135,11 +127,11 @@ export function PriceAdjustmentDialog({
-

{formatDate(data.referenceDate)}

+

{formatDate(data.referenceDate, "kr")}

-

{formatDate(data.comparisonDate)}

+

{formatDate(data.comparisonDate, "kr")}

@@ -170,7 +162,7 @@ export function PriceAdjustmentDialog({
-

{formatDate(data.adjustmentDate)}

+

{formatDate(data.adjustmentDate, "kr")}

@@ -190,8 +182,8 @@ export function PriceAdjustmentDialog({ {/* 메타 정보 */}
-

작성일: {formatDate(data.createdAt)}

-

수정일: {formatDate(data.updatedAt)}

+

작성일: {formatDate(data.createdAt, "kr")}

+

수정일: {formatDate(data.updatedAt, "kr")}

diff --git a/components/form-data-stat/form-data-stat-table.tsx b/components/form-data-stat/form-data-stat-table.tsx index fe59785d..a56a4e88 100644 --- a/components/form-data-stat/form-data-stat-table.tsx +++ b/components/form-data-stat/form-data-stat-table.tsx @@ -1,10 +1,11 @@ +// components/form-data-stat/form-data-stat-table.tsx "use client"; import * as React from "react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; -import { RefreshCw } from "lucide-react"; +import { RefreshCw, Check, ChevronsUpDown } from "lucide-react"; import { type ColumnDef } from "@tanstack/react-table"; import { ClientDataTableColumnHeaderSimple } from "@/components/client-data-table/data-table-column-simple-header"; import { ClientDataTable } from "@/components/client-data-table/data-table"; @@ -12,17 +13,25 @@ import { cn } from "@/lib/utils"; import { toast } from "sonner"; import type { DataTableAdvancedFilterField } from "@/types/table"; import { Progress } from "@/components/ui/progress"; -import { getVendorFormStatus } from "@/lib/forms/stat"; +import { getVendorFormStatus, getProjectsWithContracts } from "@/lib/forms/stat"; +import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover"; +import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from "@/components/ui/command"; // 타입 정의 interface VendorFormStatus { vendorId: number; vendorName: string; - formCount: number; // 벤더가 가진 form 개수 - tagCount: number; // 벤더가 가진 tag 개수 - totalFields: number; // 입력해야 하는 총 필드 개수 - completedFields: number; // 입력 완료된 필드 개수 - completionRate: number; // 완료율 (%) + formCount: number; + tagCount: number; + totalFields: number; + completedFields: number; + completionRate: number; +} + +interface Project { + id: number; + projectCode: string; + projectName: string; } interface VendorFormStatusTableProps { @@ -45,35 +54,164 @@ const getCompletionBadgeVariant = (rate: number): "default" | "secondary" | "des return "destructive"; }; +// 프로젝트 선택 컴포넌트 +function ProjectSelectorWithContracts({ + selectedProject, + onProjectSelect, + projects, + isLoading +}: { + selectedProject: Project | null; + onProjectSelect: (project: Project | null) => void; + projects: Project[]; + isLoading: boolean; +}) { + const [open, setOpen] = React.useState(false); + const [searchTerm, setSearchTerm] = React.useState(""); + + // 검색어로 필터링 + const filteredProjects = React.useMemo(() => { + if (!searchTerm.trim()) return projects; + + const lowerSearch = searchTerm.toLowerCase(); + return projects.filter( + project => + project.projectCode.toLowerCase().includes(lowerSearch) || + project.projectName.toLowerCase().includes(lowerSearch) + ); + }, [projects, searchTerm]); + + const handleSelectProject = (project: Project | null) => { + onProjectSelect(project); + setOpen(false); + }; + + return ( + + + + + + + + + 검색 결과가 없습니다 + {isLoading ? ( +
로딩 중...
+ ) : ( + + {/* 전체 옵션 추가 */} + handleSelectProject(null)} + > + + 전체 프로젝트 + + {/* 프로젝트 목록 */} + {filteredProjects.map((project) => ( + handleSelectProject(project)} + > + + {project.projectCode} + - {project.projectName} + + ))} + + )} +
+
+
+
+ ); +} + export function VendorFormStatusTable({ initialData = [], }: VendorFormStatusTableProps) { const [data, setData] = React.useState(initialData); const [isRefreshing, setIsRefreshing] = React.useState(false); + const [selectedProject, setSelectedProject] = React.useState(null); + const [projects, setProjects] = React.useState([]); + const [isLoadingProjects, setIsLoadingProjects] = React.useState(false); + + // 프로젝트 목록 로드 + React.useEffect(() => { + const loadProjects = async () => { + setIsLoadingProjects(true); + try { + const projectsWithContracts = await getProjectsWithContracts(); + setProjects(projectsWithContracts); + + // 프로젝트가 하나만 있으면 자동 선택 + if (projectsWithContracts.length === 1) { + setSelectedProject(projectsWithContracts[0]); + } + } catch (error) { + console.error("프로젝트 목록 로드 오류:", error); + toast.error("프로젝트 목록을 불러오는데 실패했습니다."); + } finally { + setIsLoadingProjects(false); + } + }; + + loadProjects(); + }, []); + + // 프로젝트 변경 시 데이터 로드 + React.useEffect(() => { + handleRefresh(); + }, [selectedProject]); // 데이터 새로고침 const handleRefresh = React.useCallback(async () => { setIsRefreshing(true); try { - const result = await getVendorFormStatus(); + const result = await getVendorFormStatus(selectedProject?.id); setData(result); - toast.success("데이터를 새로고침했습니다."); + if (selectedProject) { + toast.success(`${selectedProject.projectCode} 데이터를 새로고침했습니다.`); + } else { + toast.success("전체 프로젝트 데이터를 새로고침했습니다."); + } } catch (error) { console.error("Refresh error:", error); toast.error("새로고침 중 오류가 발생했습니다."); } finally { setIsRefreshing(false); } - }, []); + }, [selectedProject]); - // 초기 데이터 로드 - React.useEffect(() => { - if (initialData.length === 0) { - handleRefresh(); - } - }, []); - - // 컬럼 정의 + // 컬럼 정의 (기존과 동일) const columns: ColumnDef[] = React.useMemo(() => [ { accessorKey: "vendorName", @@ -197,6 +335,39 @@ export function VendorFormStatusTable({ return (
+ {/* 프로젝트 선택 */} + + + 프로젝트 선택 + + +
+
+ +
+ +
+ {selectedProject && ( +
+ 선택된 프로젝트: {selectedProject.projectCode} - {selectedProject.projectName} +
+ )} +
+
+ {/* 요약 카드 */}
@@ -249,33 +420,39 @@ export function VendorFormStatusTable({
- 벤더별 Form 입력 현황 - + + 벤더별 Form 입력 현황 + {selectedProject && ( + + ({selectedProject.projectCode}) + + )} +
- + {data.length > 0 ? ( + + ) : ( +
+ {selectedProject + ? "선택한 프로젝트에 대한 데이터가 없습니다." + : "데이터가 없습니다."} +
+ )}
diff --git a/components/knox/approval/ApprovalCancel.tsx b/components/knox/approval/ApprovalCancel.tsx index 62afce94..e3981cb7 100644 --- a/components/knox/approval/ApprovalCancel.tsx +++ b/components/knox/approval/ApprovalCancel.tsx @@ -15,6 +15,7 @@ import { Loader2, XCircle, AlertTriangle, CheckCircle } from 'lucide-react'; // API 함수 및 타입 import { cancelApproval, getApprovalDetail } from '@/lib/knox-api/approval/approval'; import type { ApprovalDetailResponse } from '@/lib/knox-api/approval/approval'; +import { formatDate } from '@/lib/utils'; // 상태 코드 텍스트 매핑 (mock util 대체) const getStatusText = (status: string) => { @@ -117,18 +118,6 @@ export default function ApprovalCancel({ } }; - const formatDate = (dateString: string) => { - if (!dateString || dateString.length < 14) return dateString; - const year = dateString.substring(0, 4); - const month = dateString.substring(4, 6); - const day = dateString.substring(6, 8); - const hour = dateString.substring(8, 10); - const minute = dateString.substring(10, 12); - const second = dateString.substring(12, 14); - - return `${year}-${month}-${day} ${hour}:${minute}:${second}`; - }; - const getStatusBadgeVariant = (status: string) => { switch (status) { case '2': // 완결 @@ -251,7 +240,7 @@ export default function ApprovalCancel({
-

{formatDate(approvalDetail.sbmDt)}

+

{formatDate(approvalDetail.sbmDt, "kr")}

diff --git a/components/knox/approval/ApprovalDetail.tsx b/components/knox/approval/ApprovalDetail.tsx index c36137b4..1be58d21 100644 --- a/components/knox/approval/ApprovalDetail.tsx +++ b/components/knox/approval/ApprovalDetail.tsx @@ -13,6 +13,7 @@ import { Loader2, Search, FileText, Clock, User, AlertCircle } from 'lucide-reac // API 함수 및 타입 import { getApprovalDetail, getApprovalContent } from '@/lib/knox-api/approval/approval'; import type { ApprovalDetailResponse, ApprovalContentResponse, ApprovalLine } from '@/lib/knox-api/approval/approval'; +import { formatDate } from '@/lib/utils'; // 상태/역할 텍스트 매핑 (mock util 대체) const getStatusText = (status: string) => { @@ -104,19 +105,6 @@ export default function ApprovalDetail({ } }; - const formatDate = (dateString: string) => { - if (!dateString || dateString.length < 14) return dateString; - // YYYYMMDDHHMMSS 형식을 YYYY-MM-DD HH:MM:SS로 변환 - const year = dateString.substring(0, 4); - const month = dateString.substring(4, 6); - const day = dateString.substring(6, 8); - const hour = dateString.substring(8, 10); - const minute = dateString.substring(10, 12); - const second = dateString.substring(12, 14); - - return `${year}-${month}-${day} ${hour}:${minute}:${second}`; - }; - const getSecurityTypeText = (type: string) => { const typeMap: Record = { 'PERSONAL': '개인', @@ -279,7 +267,7 @@ export default function ApprovalDetail({

- {formatDate(approvalData.detail.sbmDt)} + {formatDate(approvalData.detail.sbmDt, "kr")}

diff --git a/components/knox/approval/ApprovalList.tsx b/components/knox/approval/ApprovalList.tsx index 13a13936..25b9618d 100644 --- a/components/knox/approval/ApprovalList.tsx +++ b/components/knox/approval/ApprovalList.tsx @@ -11,6 +11,7 @@ import { Loader2, List, Eye, RefreshCw, AlertCircle } from 'lucide-react'; // API 함수 및 타입 import { getSubmissionList, getApprovalHistory } from '@/lib/knox-api/approval/approval'; import type { SubmissionListResponse, ApprovalHistoryResponse } from '@/lib/knox-api/approval/approval'; +import { formatDate } from '@/lib/utils'; // 상태 텍스트 매핑 (mock util 대체) const getStatusText = (status: string) => { @@ -82,17 +83,6 @@ export default function ApprovalList({ } }; - const formatDate = (dateString: string) => { - if (!dateString || dateString.length < 14) return dateString; - const year = dateString.substring(0, 4); - const month = dateString.substring(4, 6); - const day = dateString.substring(6, 8); - const hour = dateString.substring(8, 10); - const minute = dateString.substring(10, 12); - - return `${year}-${month}-${day} ${hour}:${minute}`; - }; - const getStatusBadgeVariant = (status: string) => { switch (status) { case '2': // 완결 @@ -247,7 +237,7 @@ export default function ApprovalList({ {item.subject} - {formatDate(item.sbmDt)} + {formatDate(item.sbmDt, "kr")} @@ -282,7 +272,7 @@ export default function ApprovalList({ {type === 'history' && ( <> - {item.actionDt ? formatDate(item.actionDt) : '-'} + {item.actionDt ? formatDate(item.actionDt, "kr") : '-'} {item.userId || '-'} diff --git a/components/pq-input/pq-input-tabs.tsx b/components/pq-input/pq-input-tabs.tsx index 7ae6d16a..534e1a05 100644 --- a/components/pq-input/pq-input-tabs.tsx +++ b/components/pq-input/pq-input-tabs.tsx @@ -77,6 +77,7 @@ import { ProjectPQ, } from "@/lib/pq/service" import { PQGroupData } from "@/lib/pq/service" +import { formatDate } from "@/lib/utils" // ---------------------------------------------------------------------- // 1) Define client-side file shapes @@ -573,7 +574,7 @@ export function PQInputTabs({ {projectData.submittedAt && (

제출일

-

{formatDate(projectData.submittedAt)}

+

{formatDate(projectData.submittedAt, "kr")}

)}
@@ -604,16 +605,6 @@ export function PQInputTabs({ } }; - // 날짜 형식화 함수 - const formatDate = (date: Date) => { - if (!date) return "-"; - return new Date(date).toLocaleDateString("ko-KR", { - year: "numeric", - month: "long", - day: "numeric", - }); - }; - // ---------------------------------------------------------------------- // H) Render // ---------------------------------------------------------------------- -- cgit v1.2.3