summaryrefslogtreecommitdiff
path: root/lib/evaluation-submit/table
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-07-07 01:44:45 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-07-07 01:44:45 +0000
commit90f79a7a691943a496f67f01c1e493256070e4de (patch)
tree37275fde3ae08c2bca384fbbc8eb378de7e39230 /lib/evaluation-submit/table
parentfbb3b7f05737f9571b04b0a8f4f15c0928de8545 (diff)
(대표님) 변경사항 20250707 10시 43분 - unstaged 변경사항 추가
Diffstat (limited to 'lib/evaluation-submit/table')
-rw-r--r--lib/evaluation-submit/table/evaluation-submissions-table-columns.tsx556
-rw-r--r--lib/evaluation-submit/table/evaluation-submit-dialog.tsx353
-rw-r--r--lib/evaluation-submit/table/submit-table.tsx281
3 files changed, 1190 insertions, 0 deletions
diff --git a/lib/evaluation-submit/table/evaluation-submissions-table-columns.tsx b/lib/evaluation-submit/table/evaluation-submissions-table-columns.tsx
new file mode 100644
index 00000000..1ec0284f
--- /dev/null
+++ b/lib/evaluation-submit/table/evaluation-submissions-table-columns.tsx
@@ -0,0 +1,556 @@
+"use client"
+
+import * as React from "react"
+import { type DataTableRowAction } from "@/types/table"
+import { type ColumnDef } from "@tanstack/react-table"
+import {
+ Ellipsis,
+ InfoIcon,
+ PenToolIcon,
+ FileTextIcon,
+ ClipboardListIcon,
+ CheckIcon,
+ XIcon,
+ ClockIcon,
+ Send,
+ User,
+ Calendar
+} from "lucide-react"
+
+import { formatDate, formatCurrency } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+import { Checkbox } from "@/components/ui/checkbox"
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+import { Badge } from "@/components/ui/badge"
+import { useRouter } from "next/navigation"
+
+import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
+import { ReviewerEvaluationView } from "@/db/schema"
+
+
+type NextRouter = ReturnType<typeof useRouter>;
+
+interface GetColumnsProps {
+ setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<ReviewerEvaluationView> | null>>
+ router: NextRouter;
+
+}
+
+/**
+ * 평가 진행 상태에 따른 배지 스타일
+ */
+const getProgressBadge = (isCompleted: boolean, completedAt: Date | null) => {
+ if (isCompleted && completedAt) {
+ return {
+ variant: "default" as const,
+ icon: <CheckIcon className="h-3 w-3" />,
+ label: "완료",
+ className: "bg-green-100 text-green-800 border-green-200"
+ }
+ } else {
+ return {
+ variant: "secondary" as const,
+ icon: <ClockIcon className="h-3 w-3" />,
+ label: "미완료"
+ }
+ }
+}
+
+/**
+ * 정기평가 상태에 따른 배지 스타일
+ */
+const getPeriodicStatusBadge = (status: string) => {
+ switch (status) {
+ case 'PENDING':
+ return {
+ variant: "secondary" as const,
+ icon: <ClockIcon className="h-3 w-3" />,
+ label: "대기중"
+ }
+
+ case 'PENDING_SUBMISSION':
+ return {
+ variant: "secondary" as const,
+ icon: <ClockIcon className="h-3 w-3" />,
+ label: "업체 제출 대기중"
+ }
+ case 'IN_PROGRESS':
+ return {
+ variant: "default" as const,
+ icon: <PenToolIcon className="h-3 w-3" />,
+ label: "진행중"
+ }
+ case 'REVIEW':
+ return {
+ variant: "outline" as const,
+ icon: <ClipboardListIcon className="h-3 w-3" />,
+ label: "검토중"
+ }
+ case 'COMPLETED':
+ return {
+ variant: "default" as const,
+ icon: <CheckIcon className="h-3 w-3" />,
+ label: "완료",
+ className: "bg-green-100 text-green-800 border-green-200"
+ }
+ default:
+ return {
+ variant: "secondary" as const,
+ icon: null,
+ label: status
+ }
+ }
+}
+
+/**
+ * 평가 제출 테이블 컬럼 정의
+ */
+export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef<ReviewerEvaluationView>[] {
+
+ // ----------------------------------------------------------------
+ // 1) select 컬럼 (체크박스)
+ // ----------------------------------------------------------------
+ const selectColumn: ColumnDef<ReviewerEvaluationView> = {
+ id: "select",
+ header: ({ table }) => (
+ <Checkbox
+ checked={
+ table.getIsAllPageRowsSelected() ||
+ (table.getIsSomePageRowsSelected() && "indeterminate")
+ }
+ onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
+ aria-label="Select all"
+ className="translate-y-0.5"
+ />
+ ),
+ cell: ({ row }) => (
+ <Checkbox
+ checked={row.getIsSelected()}
+ onCheckedChange={(value) => row.toggleSelected(!!value)}
+ aria-label="Select row"
+ className="translate-y-0.5"
+ />
+ ),
+ enableSorting: false,
+ enableHiding: false,
+ size: 40,
+ }
+
+ // ----------------------------------------------------------------
+ // 2) 기본 정보 컬럼들
+ // ----------------------------------------------------------------
+ const basicColumns: ColumnDef<ReviewerEvaluationView>[] = [
+ {
+ accessorKey: "evaluationYear",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="평가연도" />
+ ),
+ cell: ({ row }) => (
+ <Badge variant="outline">
+ {row.getValue("evaluationYear")}년
+ </Badge>
+ ),
+ size: 80,
+ },
+
+ {
+ id: "vendorInfo",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="협력업체" />
+ ),
+ cell: ({ row }) => {
+ const vendorName = row.original.vendorName;
+ const vendorCode = row.original.vendorCode;
+ const domesticForeign = row.original.domesticForeign;
+
+ return (
+ <div className="space-y-1">
+ <div className="font-medium">{vendorName}</div>
+ <div className="text-sm text-muted-foreground">
+ {vendorCode} • {domesticForeign === 'DOMESTIC' ? 'D' : 'F'}
+ </div>
+ </div>
+ );
+ },
+ enableSorting: false,
+ size: 200,
+ },
+
+
+ {
+ id: "materialType",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="자재구분" />
+ ),
+ cell: ({ row }) => {
+ const materialType = row.original.materialType;
+ const material = materialType ==="BULK" ? "벌크": materialType ==="EQUIPMENT" ? "기자재" :"기자재/벌크"
+
+ return (
+ <div className="space-y-1">
+ <div className="font-medium">{material}</div>
+
+ </div>
+ );
+ },
+ enableSorting: false,
+ },
+
+ {
+ id: "division",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="division" />
+ ),
+ cell: ({ row }) => {
+ const division = row.original.division;
+ const divisionKR = division === "PLANT"?"해양":"조선";
+
+ return (
+ <div className="space-y-1">
+ <div className="font-medium">{divisionKR}</div>
+
+ </div>
+ );
+ },
+ enableSorting: false,
+ },
+ ]
+
+ // ----------------------------------------------------------------
+ // 3) 상태 정보 컬럼들
+ // ----------------------------------------------------------------
+ const statusColumns: ColumnDef<ReviewerEvaluationView>[] = [
+ {
+ id: "evaluationProgress",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="평가 진행상태" />
+ ),
+ cell: ({ row }) => {
+ const isCompleted = row.original.isCompleted;
+ const completedAt = row.original.completedAt;
+ const badgeInfo = getProgressBadge(isCompleted, completedAt);
+
+ return (
+ <div className="space-y-1">
+ <Badge
+ variant={badgeInfo.variant}
+ className={`flex items-center gap-1 ${badgeInfo.className || ''}`}
+ >
+ {badgeInfo.icon}
+ {badgeInfo.label}
+ </Badge>
+ {completedAt && (
+ <div className="text-xs text-muted-foreground">
+ {formatDate(completedAt,"KR")}
+ </div>
+ )}
+ </div>
+ );
+ },
+ size: 130,
+ },
+
+ {
+ id: "periodicStatus",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="정기평가 상태" />
+ ),
+ cell: ({ row }) => {
+ const status = row.original.periodicStatus;
+ const badgeInfo = getPeriodicStatusBadge(status);
+
+ return (
+ <Badge
+ variant={badgeInfo.variant}
+ className={`flex items-center gap-1 ${badgeInfo.className || ''}`}
+ >
+ {badgeInfo.icon}
+ {badgeInfo.label}
+ </Badge>
+ );
+ },
+ size: 120,
+ },
+
+ // {
+ // id: "submissionInfo",
+ // header: ({ column }) => (
+ // <DataTableColumnHeaderSimple column={column} title="제출정보" />
+ // ),
+ // cell: ({ row }) => {
+ // // const submissionDate = row.original.submittedAt;
+ // const completedAt = row.original.completedAt;
+
+ // return (
+ // <div className="space-y-1">
+ // <div className="flex items-center gap-1">
+ // <Badge variant={submissionDate ? "default" : "secondary"}>
+ // {submissionDate ? "제출완료" : "미제출"}
+ // </Badge>
+ // </div>
+
+ // {completedAt && (
+ // <div className="text-xs text-muted-foreground">
+ // 평가완료: {formatDate(completedAt, "KR")}
+ // </div>
+ // )}
+ // {/* {submissionDate && (
+ // <div className="text-xs text-muted-foreground">
+ // 제출: {formatDate(submissionDate, "KR")}
+ // </div>
+ // )} */}
+
+ // </div>
+ // );
+ // },
+ // enableSorting: false,
+ // size: 140,
+ // },
+ ]
+
+ // ----------------------------------------------------------------
+ // 4) 점수 및 평가 정보 컬럼들
+ // ----------------------------------------------------------------
+ const scoreColumns: ColumnDef<ReviewerEvaluationView>[] = [
+ {
+ id: "periodicScores",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="정기평가 점수" />
+ ),
+ cell: ({ row }) => {
+ const finalScore = row.original.periodicFinalScore;
+ const finalGrade = row.original.periodicFinalGrade;
+ const evaluationScore = row.original.periodicEvaluationScore;
+ const evaluationGrade = row.original.periodicEvaluationGrade;
+
+ return (
+ <div className="text-center space-y-1">
+ {finalScore && finalGrade ? (
+ <div className="space-y-1">
+ <div className="font-medium text-blue-600">
+ 최종: {parseFloat(finalScore.toString()).toFixed(1)}점
+ </div>
+ <Badge variant="outline">{finalGrade}</Badge>
+ </div>
+ ) : evaluationScore && evaluationGrade ? (
+ <div className="space-y-1">
+ <div className="font-medium">
+ {parseFloat(evaluationScore.toString()).toFixed(1)}점
+ </div>
+ <Badge variant="outline">{evaluationGrade}</Badge>
+ </div>
+ ) : (
+ <span className="text-muted-foreground">미산정</span>
+ )}
+ </div>
+ );
+ },
+ enableSorting: false,
+ size: 120,
+ },
+
+ ]
+
+
+
+ // ----------------------------------------------------------------
+ // 6) 메타데이터 컬럼들
+ // ----------------------------------------------------------------
+ const metaColumns: ColumnDef<ReviewerEvaluationView>[] = [
+ {
+ accessorKey: "reviewerEvaluationCreatedAt",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="생성일" />
+ ),
+ cell: ({ row }) => {
+ const date = row.getValue("reviewerEvaluationCreatedAt") as Date;
+ return formatDate(date);
+ },
+ size: 140,
+ },
+ {
+ accessorKey: "reviewerEvaluationUpdatedAt",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="수정일" />
+ ),
+ cell: ({ row }) => {
+ const date = row.getValue("reviewerEvaluationUpdatedAt") as Date;
+ return formatDate(date);
+ },
+ size: 140,
+ },
+ ]
+
+ // ----------------------------------------------------------------
+ // 7) actions 컬럼 (드롭다운 메뉴)
+ // ----------------------------------------------------------------
+ const actionsColumn: ColumnDef<ReviewerEvaluationView> = {
+ id: "actions",
+ header: "작업",
+ enableHiding: false,
+ cell: function Cell({ row }) {
+ const isCompleted = row.original.isCompleted;
+ const reviewerEvaluationId = row.original.reviewerEvaluationId;
+
+ return (
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button variant="ghost" size="icon">
+ <Ellipsis className="h-4 w-4" />
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent align="end">
+ <DropdownMenuItem
+ onClick={() => router.push(`/evcp/evaluation-input/${reviewerEvaluationId}`)}
+ >
+ {isCompleted ? "완료된 평가보기":"평가 작성하기"}
+ </DropdownMenuItem>
+
+ </DropdownMenuContent>
+ </DropdownMenu>
+ )
+ },
+ size: 80,
+ }
+
+ // ----------------------------------------------------------------
+ // 8) 최종 컬럼 배열
+ // ----------------------------------------------------------------
+ return [
+ selectColumn,
+ ...basicColumns,
+ {
+ id: "statusInfo",
+ header: "상태 정보",
+ columns: statusColumns,
+ },
+ {
+ id: "scoreInfo",
+ header: "점수 및 평가",
+ columns: scoreColumns,
+ },
+
+ {
+ id: "metadata",
+ header: "메타데이터",
+ columns: metaColumns,
+ },
+ actionsColumn,
+ ]
+}
+
+// ----------------------------------------------------------------
+// 9) 컬럼 설정 (필터링용)
+// ----------------------------------------------------------------
+export const evaluationSubmissionsColumnsConfig = [
+ {
+ id: "reviewerEvaluationId",
+ label: "평가 ID",
+ group: "기본 정보",
+ type: "text",
+ excelHeader: "Evaluation ID",
+ },
+ {
+ id: "vendorName",
+ label: "협력업체명",
+ group: "기본 정보",
+ type: "text",
+ excelHeader: "Vendor Name",
+ },
+ {
+ id: "vendorCode",
+ label: "협력업체 코드",
+ group: "기본 정보",
+ type: "text",
+ excelHeader: "Vendor Code",
+ },
+ {
+ id: "evaluationYear",
+ label: "평가연도",
+ group: "기본 정보",
+ type: "number",
+ excelHeader: "Evaluation Year",
+ },
+ {
+ id: "departmentCode",
+ label: "부서코드",
+ group: "기본 정보",
+ type: "text",
+ excelHeader: "Department Code",
+ },
+ {
+ id: "isCompleted",
+ label: "완료 여부",
+ group: "상태 정보",
+ type: "select",
+ options: [
+ { label: "완료", value: "true" },
+ { label: "미완료", value: "false" },
+ ],
+ excelHeader: "Is Completed",
+ },
+ {
+ id: "periodicStatus",
+ label: "정기평가 상태",
+ group: "상태 정보",
+ type: "select",
+ options: [
+ { label: "대기중", value: "PENDING" },
+ { label: "진행중", value: "IN_PROGRESS" },
+ { label: "검토중", value: "REVIEW" },
+ { label: "완료", value: "COMPLETED" },
+ ],
+ excelHeader: "Periodic Status",
+ },
+ {
+ id: "documentsSubmitted",
+ label: "문서 제출여부",
+ group: "상태 정보",
+ type: "select",
+ options: [
+ { label: "제출완료", value: "true" },
+ { label: "미제출", value: "false" },
+ ],
+ excelHeader: "Documents Submitted",
+ },
+ {
+ id: "periodicFinalScore",
+ label: "최종점수",
+ group: "점수 정보",
+ type: "number",
+ excelHeader: "Final Score",
+ },
+ {
+ id: "periodicFinalGrade",
+ label: "최종등급",
+ group: "점수 정보",
+ type: "text",
+ excelHeader: "Final Grade",
+ },
+ {
+ id: "reviewerEvaluationCreatedAt",
+ label: "생성일",
+ group: "메타데이터",
+ type: "date",
+ excelHeader: "Created At",
+ },
+ {
+ id: "reviewerEvaluationUpdatedAt",
+ label: "수정일",
+ group: "메타데이터",
+ type: "date",
+ excelHeader: "Updated At",
+ },
+] as const; \ No newline at end of file
diff --git a/lib/evaluation-submit/table/evaluation-submit-dialog.tsx b/lib/evaluation-submit/table/evaluation-submit-dialog.tsx
new file mode 100644
index 00000000..20ed5f30
--- /dev/null
+++ b/lib/evaluation-submit/table/evaluation-submit-dialog.tsx
@@ -0,0 +1,353 @@
+"use client"
+
+import * as React from "react"
+import {
+ AlertTriangleIcon,
+ CheckCircleIcon,
+ SendIcon,
+ XCircleIcon,
+ FileTextIcon,
+ ClipboardListIcon,
+ LoaderIcon
+} from "lucide-react"
+
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog"
+import {
+ Alert,
+ AlertDescription,
+ AlertTitle,
+} from "@/components/ui/alert"
+import { Button } from "@/components/ui/button"
+import { Badge } from "@/components/ui/badge"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { toast } from "sonner"
+
+// Progress 컴포넌트 (간단한 구현)
+function Progress({ value, className }: { value: number; className?: string }) {
+ return (
+ <div className={`w-full bg-gray-200 rounded-full overflow-hidden ${className}`}>
+ <div
+ className={`h-full bg-blue-600 transition-all duration-300 ${
+ value === 100 ? 'bg-green-500' : value >= 50 ? 'bg-blue-500' : 'bg-yellow-500'
+ }`}
+ style={{ width: `${Math.min(100, Math.max(0, value))}%` }}
+ />
+ </div>
+ )
+}
+
+import {
+ getEvaluationSubmissionCompleteness,
+ updateEvaluationSubmissionStatus
+} from "../service"
+import type { EvaluationSubmissionWithVendor } from "../service"
+
+interface EvaluationSubmissionDialogProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ submission: EvaluationSubmissionWithVendor | null
+ onSuccess: () => void
+}
+
+type CompletenessData = {
+ general: {
+ total: number
+ completed: number
+ percentage: number
+ isComplete: boolean
+ }
+ esg: {
+ total: number
+ completed: number
+ percentage: number
+ averageScore: number
+ isComplete: boolean
+ }
+ overall: {
+ isComplete: boolean
+ totalItems: number
+ completedItems: number
+ }
+}
+
+export function EvaluationSubmissionDialog({
+ open,
+ onOpenChange,
+ submission,
+ onSuccess,
+}: EvaluationSubmissionDialogProps) {
+ const [isLoading, setIsLoading] = React.useState(false)
+ const [isSubmitting, setIsSubmitting] = React.useState(false)
+ const [completeness, setCompleteness] = React.useState<CompletenessData | null>(null)
+
+ // 완성도 데이터 로딩
+ React.useEffect(() => {
+ if (open && submission?.id) {
+ loadCompleteness()
+ }
+ }, [open, submission?.id])
+
+ const loadCompleteness = async () => {
+ if (!submission?.id) return
+
+ setIsLoading(true)
+ try {
+ const data = await getEvaluationSubmissionCompleteness(submission.id)
+ setCompleteness(data)
+ } catch (error) {
+ console.error('Error loading completeness:', error)
+ toast.error('완성도 정보를 불러오는데 실패했습니다.')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ // 제출하기
+ const handleSubmit = async () => {
+ if (!submission?.id || !completeness) return
+
+ if (!completeness.overall.isComplete) {
+ toast.error('모든 평가 항목을 완료해야 제출할 수 있습니다.')
+ return
+ }
+
+ setIsSubmitting(true)
+ try {
+ await updateEvaluationSubmissionStatus(submission.id, 'submitted')
+ toast.success('평가가 성공적으로 제출되었습니다.')
+ onSuccess()
+ } catch (error: any) {
+ console.error('Error submitting evaluation:', error)
+ toast.error(error.message || '제출에 실패했습니다.')
+ } finally {
+ setIsSubmitting(false)
+ }
+ }
+
+ const isKorean = submission?.vendor.countryCode === 'KR'
+
+ if (isLoading) {
+ return (
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="sm:max-w-[500px]">
+ <div className="flex items-center justify-center py-8">
+ <div className="text-center space-y-4">
+ <LoaderIcon className="h-8 w-8 animate-spin mx-auto" />
+ <p>완성도를 확인하는 중...</p>
+ </div>
+ </div>
+ </DialogContent>
+ </Dialog>
+ )
+ }
+
+ return (
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="sm:max-w-[600px]">
+ <DialogHeader>
+ <DialogTitle className="flex items-center gap-2">
+ <SendIcon className="h-5 w-5" />
+ 평가 제출하기
+ </DialogTitle>
+ <DialogDescription>
+ {submission?.vendor.vendorName}의 {submission?.evaluationYear}년 평가를 제출합니다.
+ </DialogDescription>
+ </DialogHeader>
+
+ {completeness && (
+ <div className="space-y-6">
+ {/* 전체 완성도 카드 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-base flex items-center justify-between">
+ <span>전체 완성도</span>
+ <Badge
+ variant={completeness.overall.isComplete ? "default" : "secondary"}
+ className={
+ completeness.overall.isComplete
+ ? "bg-green-100 text-green-800 border-green-200"
+ : ""
+ }
+ >
+ {completeness.overall.isComplete ? "완료" : "미완료"}
+ </Badge>
+ </CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div className="space-y-2">
+ <div className="flex items-center justify-between text-sm">
+ <span>전체 진행률</span>
+ <span className="font-medium">
+ {completeness.overall.completedItems}/{completeness.overall.totalItems}개 완료
+ </span>
+ </div>
+ <Progress
+ value={
+ completeness.overall.totalItems > 0
+ ? (completeness.overall.completedItems / completeness.overall.totalItems) * 100
+ : 0
+ }
+ className="h-2"
+ />
+ <p className="text-xs text-muted-foreground">
+ {completeness.overall.totalItems > 0
+ ? Math.round((completeness.overall.completedItems / completeness.overall.totalItems) * 100)
+ : 0}% 완료
+ </p>
+ </div>
+ </CardContent>
+ </Card>
+
+ {/* 세부 완성도 */}
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+ {/* 일반평가 */}
+ <Card>
+ <CardHeader className="pb-3">
+ <CardTitle className="text-sm flex items-center gap-2">
+ <FileTextIcon className="h-4 w-4" />
+ 일반평가
+ {completeness.general.isComplete ? (
+ <CheckCircleIcon className="h-4 w-4 text-green-600" />
+ ) : (
+ <XCircleIcon className="h-4 w-4 text-red-600" />
+ )}
+ </CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-3">
+ <div className="space-y-1">
+ <div className="flex items-center justify-between text-xs">
+ <span>응답 완료</span>
+ <span className="font-medium">
+ {completeness.general.completed}/{completeness.general.total}개
+ </span>
+ </div>
+ <Progress value={completeness.general.percentage} className="h-1" />
+ <p className="text-xs text-muted-foreground">
+ {completeness.general.percentage.toFixed(0)}% 완료
+ </p>
+ </div>
+
+ {!completeness.general.isComplete && (
+ <p className="text-xs text-red-600">
+ {completeness.general.total - completeness.general.completed}개 항목이 미완료입니다.
+ </p>
+ )}
+ </CardContent>
+ </Card>
+
+ {/* ESG평가 */}
+ {isKorean ? (
+ <Card>
+ <CardHeader className="pb-3">
+ <CardTitle className="text-sm flex items-center gap-2">
+ <ClipboardListIcon className="h-4 w-4" />
+ ESG평가
+ {completeness.esg.isComplete ? (
+ <CheckCircleIcon className="h-4 w-4 text-green-600" />
+ ) : (
+ <XCircleIcon className="h-4 w-4 text-red-600" />
+ )}
+ </CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-3">
+ <div className="space-y-1">
+ <div className="flex items-center justify-between text-xs">
+ <span>응답 완료</span>
+ <span className="font-medium">
+ {completeness.esg.completed}/{completeness.esg.total}개
+ </span>
+ </div>
+ <Progress value={completeness.esg.percentage} className="h-1" />
+ <p className="text-xs text-muted-foreground">
+ {completeness.esg.percentage.toFixed(0)}% 완료
+ </p>
+ </div>
+
+ {completeness.esg.completed > 0 && (
+ <div className="text-xs">
+ <span className="text-muted-foreground">평균 점수: </span>
+ <span className="font-medium text-blue-600">
+ {completeness.esg.averageScore.toFixed(1)}점
+ </span>
+ </div>
+ )}
+
+ {!completeness.esg.isComplete && (
+ <p className="text-xs text-red-600">
+ {completeness.esg.total - completeness.esg.completed}개 항목이 미완료입니다.
+ </p>
+ )}
+ </CardContent>
+ </Card>
+ ) : (
+ <Card>
+ <CardHeader className="pb-3">
+ <CardTitle className="text-sm flex items-center gap-2">
+ <ClipboardListIcon className="h-4 w-4" />
+ ESG평가
+ </CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="text-center text-muted-foreground">
+ <Badge variant="outline">해당없음</Badge>
+ <p className="text-xs mt-2">한국 업체가 아니므로 ESG 평가가 제외됩니다.</p>
+ </div>
+ </CardContent>
+ </Card>
+ )}
+ </div>
+
+ {/* 제출 상태 알림 */}
+ {completeness.overall.isComplete ? (
+ <Alert>
+ <CheckCircleIcon className="h-4 w-4" />
+ <AlertTitle>제출 준비 완료</AlertTitle>
+ <AlertDescription>
+ 모든 평가 항목이 완료되었습니다. 제출하시겠습니까?
+ </AlertDescription>
+ </Alert>
+ ) : (
+ <Alert variant="destructive">
+ <AlertTriangleIcon className="h-4 w-4" />
+ <AlertTitle>제출 불가</AlertTitle>
+ <AlertDescription>
+ 아직 완료되지 않은 평가 항목이 있습니다. 모든 항목을 완료한 후 제출해 주세요.
+ </AlertDescription>
+ </Alert>
+ )}
+ </div>
+ )}
+
+ <DialogFooter>
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
+ 취소
+ </Button>
+ <Button
+ onClick={handleSubmit}
+ disabled={!completeness?.overall.isComplete || isSubmitting}
+ className="min-w-[100px]"
+ >
+ {isSubmitting ? (
+ <>
+ <LoaderIcon className="mr-2 h-4 w-4 animate-spin" />
+ 제출 중...
+ </>
+ ) : (
+ <>
+ <SendIcon className="mr-2 h-4 w-4" />
+ 제출하기
+ </>
+ )}
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ )
+} \ No newline at end of file
diff --git a/lib/evaluation-submit/table/submit-table.tsx b/lib/evaluation-submit/table/submit-table.tsx
new file mode 100644
index 00000000..9000c48b
--- /dev/null
+++ b/lib/evaluation-submit/table/submit-table.tsx
@@ -0,0 +1,281 @@
+"use client"
+
+import * as React from "react"
+import type {
+ DataTableAdvancedFilterField,
+ DataTableFilterField,
+ DataTableRowAction,
+} from "@/types/table"
+
+import { useDataTable } from "@/hooks/use-data-table"
+import { DataTable } from "@/components/data-table/data-table"
+import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"
+
+import { getSHIEvaluationSubmissions } from "../service"
+import { getColumns } from "./evaluation-submissions-table-columns"
+import { useRouter } from "next/navigation"
+import { ReviewerEvaluationView } from "@/db/schema"
+
+interface EvaluationSubmissionsTableProps {
+ promises: Promise<
+ [
+ Awaited<ReturnType<typeof getSHIEvaluationSubmissions>>,
+ ]
+ >
+}
+
+export function SHIEvaluationSubmissionsTable({ promises }: EvaluationSubmissionsTableProps) {
+ // 1. 데이터 로딩 상태 관리
+ const [isLoading, setIsLoading] = React.useState(true)
+ const [tableData, setTableData] = React.useState<{
+ data: ReviewerEvaluationView[]
+ pageCount: number
+ }>({ data: [], pageCount: 0 })
+ const router = useRouter()
+
+ console.log(tableData)
+
+
+ // 2. 행 액션 상태 관리
+ const [rowAction, setRowAction] =
+ React.useState<DataTableRowAction<ReviewerEvaluationView> | null>(null)
+
+ // 3. Promise 해결을 useEffect로 처리
+ React.useEffect(() => {
+ promises
+ .then(([result]) => {
+ setTableData(result)
+ setIsLoading(false)
+ })
+ // .catch((error) => {
+ // console.error('Failed to load evaluation submissions:', error)
+ // setIsLoading(false)
+ // })
+ }, [promises])
+
+ // 4. 컬럼 정의
+ const columns = React.useMemo(
+ () => getColumns({ setRowAction , router}),
+ [setRowAction, router]
+ )
+
+ // 5. 필터 필드 정의
+ const filterFields: DataTableFilterField<ReviewerEvaluationView>[] = [
+ {
+ id: "isCompleted",
+ label: "완료상태",
+ placeholder: "완료상태 선택...",
+ },
+ {
+ id: "periodicStatus",
+ label: "정기평가 상태",
+ placeholder: "상태 선택...",
+ },
+ {
+ id: "evaluationYear",
+ label: "평가연도",
+ placeholder: "연도 선택...",
+ },
+ {
+ id: "departmentCode",
+ label: "담당부서",
+ placeholder: "부서 선택...",
+ },
+ ]
+
+ const advancedFilterFields: DataTableAdvancedFilterField<ReviewerEvaluationView>[] = [
+ {
+ id: "reviewerEvaluationId",
+ label: "평가 ID",
+ type: "text",
+ },
+ {
+ id: "vendorName",
+ label: "협력업체명",
+ type: "text",
+ },
+ {
+ id: "vendorCode",
+ label: "협력업체 코드",
+ type: "text",
+ },
+ {
+ id: "evaluationYear",
+ label: "평가연도",
+ type: "number",
+ },
+ {
+ id: "departmentCode",
+ label: "부서코드",
+ type: "select",
+ options: [
+ { label: "구매평가", value: "ORDER_EVAL" },
+ { label: "조달평가", value: "PROCUREMENT_EVAL" },
+ { label: "품질평가", value: "QUALITY_EVAL" },
+ { label: "CS평가", value: "CS_EVAL" },
+ { label: "관리자", value: "ADMIN_EVAL" },
+ ],
+ },
+ {
+ id: "division",
+ label: "사업부",
+ type: "select",
+ options: [
+ { label: "조선", value: "SHIP" },
+ { label: "플랜트", value: "PLANT" },
+ ],
+ },
+ {
+ id: "materialType",
+ label: "자재유형",
+ type: "select",
+ options: [
+ { label: "장비", value: "EQUIPMENT" },
+ { label: "벌크", value: "BULK" },
+ { label: "장비+벌크", value: "EQUIPMENT_BULK" },
+ ],
+ },
+ {
+ id: "domesticForeign",
+ label: "국내/해외",
+ type: "select",
+ options: [
+ { label: "국내", value: "DOMESTIC" },
+ { label: "해외", value: "FOREIGN" },
+ ],
+ },
+ {
+ id: "isCompleted",
+ label: "평가완료 여부",
+ type: "select",
+ options: [
+ { label: "완료", value: "true" },
+ { label: "미완료", value: "false" },
+ ],
+ },
+ {
+ id: "periodicStatus",
+ label: "정기평가 상태",
+ type: "select",
+ options: [
+ { label: "대기중", value: "PENDING" },
+ { label: "진행중", value: "IN_PROGRESS" },
+ { label: "검토중", value: "REVIEW" },
+ { label: "완료", value: "COMPLETED" },
+ ],
+ },
+ {
+ id: "documentsSubmitted",
+ label: "문서 제출여부",
+ type: "select",
+ options: [
+ { label: "제출완료", value: "true" },
+ { label: "미제출", value: "false" },
+ ],
+ },
+ {
+ id: "periodicFinalScore",
+ label: "최종점수",
+ type: "number",
+ },
+ {
+ id: "periodicFinalGrade",
+ label: "최종등급",
+ type: "text",
+ },
+ {
+ id: "ldClaimCount",
+ label: "LD 클레임 건수",
+ type: "number",
+ },
+ {
+ id: "submissionDate",
+ label: "제출일",
+ type: "date",
+ },
+ {
+ id: "submissionDeadline",
+ label: "제출마감일",
+ type: "date",
+ },
+ {
+ id: "completedAt",
+ label: "완료일시",
+ type: "date",
+ },
+ {
+ id: "reviewerEvaluationCreatedAt",
+ label: "생성일",
+ type: "date",
+ },
+ {
+ id: "reviewerEvaluationUpdatedAt",
+ label: "수정일",
+ type: "date",
+ },
+ ]
+
+ // 6. 데이터 테이블 설정
+ const { table } = useDataTable({
+ data: tableData.data,
+ columns,
+ pageCount: tableData.pageCount,
+ filterFields,
+ enablePinning: true,
+ enableAdvancedFilter: true,
+ initialState: {
+ sorting: [{ id: "reviewerEvaluationUpdatedAt", desc: true }],
+ columnPinning: { left: ["select"], right: ["actions"] },
+ },
+ getRowId: (originalRow) => String(originalRow.reviewerEvaluationId),
+ shallow: false,
+ clearOnDefault: true,
+ })
+
+ // 7. 데이터 새로고침 함수
+ const handleRefresh = React.useCallback(() => {
+ setIsLoading(true)
+ router.refresh()
+ }, [router])
+
+ // 8. 각종 성공 핸들러
+ const handleActionSuccess = React.useCallback(() => {
+ setRowAction(null)
+ table.resetRowSelection()
+ handleRefresh()
+ }, [handleRefresh, table])
+
+ // 9. 로딩 상태 표시
+ if (isLoading) {
+ return (
+ <div className="flex items-center justify-center h-32">
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
+ <span className="ml-2">평가 제출 목록을 불러오는 중...</span>
+ </div>
+ )
+ }
+
+ return (
+ <>
+ {/* 메인 테이블 */}
+ <DataTable table={table}>
+ <DataTableAdvancedToolbar
+ table={table}
+ filterFields={advancedFilterFields}
+ shallow={false}
+ >
+ {/* 추가 툴바 버튼들이 필요하면 여기에 */}
+ </DataTableAdvancedToolbar>
+ </DataTable>
+
+ {/* 행 액션 모달들 - 필요에 따라 구현 */}
+ {/* {rowAction?.type === "view_detail" && (
+ <EvaluationDetailDialog
+ row={rowAction.row}
+ onClose={() => setRowAction(null)}
+ onSuccess={handleActionSuccess}
+ />
+ )} */}
+ </>
+ )
+} \ No newline at end of file