From e9897d416b3e7327bbd4d4aef887eee37751ae82 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 27 Jun 2025 01:16:20 +0000 Subject: (대표님) 20250627 오전 10시 작업사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/evaluation/service.ts | 266 ++++++++++++++- lib/evaluation/table/evaluation-columns.tsx | 341 +++++++++++++------ lib/evaluation/table/evaluation-table.tsx | 24 +- .../table/periodic-evaluation-action-dialogs.tsx | 373 +++++++++++++++++++++ .../table/periodic-evaluations-toolbar-actions.tsx | 218 ++++++++++++ 5 files changed, 1099 insertions(+), 123 deletions(-) create mode 100644 lib/evaluation/table/periodic-evaluation-action-dialogs.tsx create mode 100644 lib/evaluation/table/periodic-evaluations-toolbar-actions.tsx (limited to 'lib/evaluation') diff --git a/lib/evaluation/service.ts b/lib/evaluation/service.ts index 3cc4ca7d..19e41dff 100644 --- a/lib/evaluation/service.ts +++ b/lib/evaluation/service.ts @@ -1,5 +1,8 @@ +'use server' + import db from "@/db/db" import { + evaluationSubmissions, periodicEvaluationsView, type PeriodicEvaluationView } from "@/db/schema" @@ -9,7 +12,7 @@ import { count, desc, ilike, - or, + or, sql , eq, avg, type SQL } from "drizzle-orm" import { filterColumns } from "@/lib/filter-columns" @@ -17,6 +20,7 @@ import { GetEvaluationTargetsSchema } from "../evaluation-target-list/validation export async function getPeriodicEvaluations(input: GetEvaluationTargetsSchema) { try { + const offset = (input.page - 1) * input.perPage; // ✅ getEvaluationTargets 방식과 동일한 필터링 처리 @@ -115,6 +119,8 @@ export async function getPeriodicEvaluations(input: GetEvaluationTargetsSchema) .offset(offset); const pageCount = Math.ceil(total / input.perPage); + + console.log(periodicEvaluationsData,"periodicEvaluationsData") return { data: periodicEvaluationsData, pageCount, total }; } catch (err) { @@ -122,4 +128,262 @@ export async function getPeriodicEvaluations(input: GetEvaluationTargetsSchema) // ✅ getEvaluationTargets 방식과 동일한 에러 반환 (total 포함) return { data: [], pageCount: 0, total: 0 }; } + } + + export interface PeriodicEvaluationsStats { + total: number + pendingSubmission: number + submitted: number + inReview: number + reviewCompleted: number + finalized: number + averageScore: number | null + completionRate: number + averageFinalScore: number | null + documentsSubmittedCount: number + documentsNotSubmittedCount: number + reviewProgress: { + totalReviewers: number + completedReviewers: number + pendingReviewers: number + reviewCompletionRate: number + } + } + + export async function getPeriodicEvaluationsStats(evaluationYear: number): Promise { + try { + // 기본 WHERE 조건: 해당 연도의 평가만 + const baseWhere = eq(periodicEvaluationsView.evaluationYear, evaluationYear) + + // 1. 전체 통계 조회 + const totalStatsResult = await db + .select({ + total: count(), + averageScore: avg(periodicEvaluationsView.totalScore), + averageFinalScore: avg(periodicEvaluationsView.finalScore), + }) + .from(periodicEvaluationsView) + .where(baseWhere) + + const totalStats = totalStatsResult[0] || { + total: 0, + averageScore: null, + averageFinalScore: null + } + + // 2. 상태별 카운트 조회 + const statusStatsResult = await db + .select({ + status: periodicEvaluationsView.status, + count: count(), + }) + .from(periodicEvaluationsView) + .where(baseWhere) + .groupBy(periodicEvaluationsView.status) + + // 상태별 카운트를 객체로 변환 + const statusCounts = statusStatsResult.reduce((acc, item) => { + acc[item.status] = item.count + return acc + }, {} as Record) + + // 3. 문서 제출 상태 통계 + const documentStatsResult = await db + .select({ + documentsSubmitted: periodicEvaluationsView.documentsSubmitted, + count: count(), + }) + .from(periodicEvaluationsView) + .where(baseWhere) + .groupBy(periodicEvaluationsView.documentsSubmitted) + + const documentCounts = documentStatsResult.reduce((acc, item) => { + if (item.documentsSubmitted) { + acc.submitted = item.count + } else { + acc.notSubmitted = item.count + } + return acc + }, { submitted: 0, notSubmitted: 0 }) + + // 4. 리뷰어 진행 상황 통계 + const reviewProgressResult = await db + .select({ + totalReviewers: sql`SUM(${periodicEvaluationsView.totalReviewers})`.as('total_reviewers'), + completedReviewers: sql`SUM(${periodicEvaluationsView.completedReviewers})`.as('completed_reviewers'), + pendingReviewers: sql`SUM(${periodicEvaluationsView.pendingReviewers})`.as('pending_reviewers'), + }) + .from(periodicEvaluationsView) + .where(baseWhere) + + const reviewProgress = reviewProgressResult[0] || { + totalReviewers: 0, + completedReviewers: 0, + pendingReviewers: 0, + } + + // 5. 완료율 계산 + const finalizedCount = statusCounts['FINALIZED'] || 0 + const totalCount = totalStats.total + const completionRate = totalCount > 0 ? Math.round((finalizedCount / totalCount) * 100) : 0 + + // 6. 리뷰 완료율 계산 + const reviewCompletionRate = reviewProgress.totalReviewers > 0 + ? Math.round((reviewProgress.completedReviewers / reviewProgress.totalReviewers) * 100) + : 0 + + // 7. 평균 점수 포맷팅 (소수점 1자리) + const formatScore = (score: string | number | null): number | null => { + if (score === null || score === undefined) return null + return Math.round(Number(score) * 10) / 10 + } + + return { + total: totalCount, + pendingSubmission: statusCounts['PENDING_SUBMISSION'] || 0, + submitted: statusCounts['SUBMITTED'] || 0, + inReview: statusCounts['IN_REVIEW'] || 0, + reviewCompleted: statusCounts['REVIEW_COMPLETED'] || 0, + finalized: finalizedCount, + averageScore: formatScore(totalStats.averageScore), + averageFinalScore: formatScore(totalStats.averageFinalScore), + completionRate, + documentsSubmittedCount: documentCounts.submitted, + documentsNotSubmittedCount: documentCounts.notSubmitted, + reviewProgress: { + totalReviewers: reviewProgress.totalReviewers, + completedReviewers: reviewProgress.completedReviewers, + pendingReviewers: reviewProgress.pendingReviewers, + reviewCompletionRate, + }, + } + + } catch (error) { + console.error('Error in getPeriodicEvaluationsStats:', error) + // 에러 발생 시 기본값 반환 + return { + total: 0, + pendingSubmission: 0, + submitted: 0, + inReview: 0, + reviewCompleted: 0, + finalized: 0, + averageScore: null, + averageFinalScore: null, + completionRate: 0, + documentsSubmittedCount: 0, + documentsNotSubmittedCount: 0, + reviewProgress: { + totalReviewers: 0, + completedReviewers: 0, + pendingReviewers: 0, + reviewCompletionRate: 0, + }, + } + } + } + + + + interface RequestDocumentsData { + periodicEvaluationId: number + companyId: number + evaluationYear: number + evaluationRound: string + message: string + } + + export async function requestDocumentsFromVendors(data: RequestDocumentsData[]) { + try { + // 각 평가에 대해 evaluationSubmissions 레코드 생성 + const submissions = await Promise.all( + data.map(async (item) => { + // 이미 해당 periodicEvaluationId와 companyId로 생성된 submission이 있는지 확인 + const existingSubmission = await db.query.evaluationSubmissions.findFirst({ + where: and( + eq(evaluationSubmissions.periodicEvaluationId, item.periodicEvaluationId), + eq(evaluationSubmissions.companyId, item.companyId) + ) + }) + + if (existingSubmission) { + // 이미 존재하면 reviewComments만 업데이트 + const [updated] = await db + .update(evaluationSubmissions) + .set({ + reviewComments: item.message, + updatedAt: new Date() + }) + .where(eq(evaluationSubmissions.id, existingSubmission.id)) + .returning() + + return updated + } else { + // 새로 생성 + const [created] = await db + .insert(evaluationSubmissions) + .values({ + periodicEvaluationId: item.periodicEvaluationId, + companyId: item.companyId, + evaluationYear: item.evaluationYear, + evaluationRound: item.evaluationRound, + submissionStatus: 'draft', // 기본값 + reviewComments: item.message, + // 진행률 관련 필드들은 기본값 0으로 설정됨 + totalGeneralItems: 0, + completedGeneralItems: 0, + totalEsgItems: 0, + completedEsgItems: 0, + isActive: true + }) + .returning() + + return created + } + }) + ) + + + return { + success: true, + message: `${submissions.length}개 업체에 자료 요청이 완료되었습니다.`, + submissions + } + + } catch (error) { + console.error("Error requesting documents from vendors:", error) + return { + success: false, + message: "자료 요청 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "Unknown error" + } + } + } + + // 기존 요청 상태 확인 함수 추가 + export async function checkExistingSubmissions(periodicEvaluationIds: number[]) { + try { + const existingSubmissions = await db.query.evaluationSubmissions.findMany({ + where: (submissions) => { + // periodicEvaluationIds 배열에 포함된 ID들을 확인 + return periodicEvaluationIds.length === 1 + ? eq(submissions.periodicEvaluationId, periodicEvaluationIds[0]) + : periodicEvaluationIds.length > 1 + ? or(...periodicEvaluationIds.map(id => eq(submissions.periodicEvaluationId, id))) + : eq(submissions.id, -1) // 빈 배열인 경우 결과 없음 + }, + columns: { + id: true, + periodicEvaluationId: true, + companyId: true, + createdAt: true, + reviewComments: true + } + }) + + return existingSubmissions + } catch (error) { + console.error("Error checking existing submissions:", error) + return [] + } } \ No newline at end of file diff --git a/lib/evaluation/table/evaluation-columns.tsx b/lib/evaluation/table/evaluation-columns.tsx index 821e8182..10aa7704 100644 --- a/lib/evaluation/table/evaluation-columns.tsx +++ b/lib/evaluation/table/evaluation-columns.tsx @@ -144,14 +144,14 @@ export function getPeriodicEvaluationsColumns({setRowAction}: GetColumnsProps): }, // ░░░ 평가기간 ░░░ - { - accessorKey: "evaluationPeriod", - header: ({ column }) => , - cell: ({ row }) => ( - {row.getValue("evaluationPeriod")} - ), - size: 100, - }, + // { + // accessorKey: "evaluationPeriod", + // header: ({ column }) => , + // cell: ({ row }) => ( + // {row.getValue("evaluationPeriod")} + // ), + // size: 100, + // }, // ░░░ 구분 ░░░ { @@ -202,12 +202,113 @@ export function getPeriodicEvaluationsColumns({setRowAction}: GetColumnsProps): }, ] }, + + { + accessorKey: "finalScore", + header: ({ column }) => , + cell: ({ row }) => { + const finalScore = row.getValue("finalScore"); + return finalScore ? ( + {finalScore.toFixed(1)} + ) : ( + - + ); + }, + size: 90, + }, + { + accessorKey: "finalGrade", + header: ({ column }) => , + cell: ({ row }) => { + const finalGrade = row.getValue("finalGrade"); + return finalGrade ? ( + + {finalGrade} + + ) : ( + - + ); + }, + size: 90, + }, + + // ═══════════════════════════════════════════════════════════════ + // 진행 현황 + // ═══════════════════════════════════════════════════════════════ + { + header: "평가자 진행 현황", + columns: [ + { + accessorKey: "status", + header: ({ column }) => , + cell: ({ row }) => { + const status = row.getValue("status"); + return ( + + {getStatusLabel(status)} + + ); + }, + size: 100, + }, + + { + id: "reviewProgress", + header: ({ column }) => , + cell: ({ row }) => { + const totalReviewers = row.original.totalReviewers || 0; + const completedReviewers = row.original.completedReviewers || 0; + + return getProgressBadge(completedReviewers, totalReviewers); + }, + size: 120, + }, + + { + accessorKey: "reviewCompletedAt", + header: ({ column }) => , + cell: ({ row }) => { + const completedAt = row.getValue("reviewCompletedAt"); + return completedAt ? ( + + {new Intl.DateTimeFormat("ko-KR", { + month: "2-digit", + day: "2-digit", + }).format(new Date(completedAt))} + + ) : ( + - + ); + }, + size: 100, + }, + + { + accessorKey: "finalizedAt", + header: ({ column }) => , + cell: ({ row }) => { + const finalizedAt = row.getValue("finalizedAt"); + return finalizedAt ? ( + + {new Intl.DateTimeFormat("ko-KR", { + month: "2-digit", + day: "2-digit", + }).format(new Date(finalizedAt))} + + ) : ( + - + ); + }, + size: 80, + }, + ] + }, // ═══════════════════════════════════════════════════════════════ // 제출 현황 // ═══════════════════════════════════════════════════════════════ { - header: "제출 현황", + header: "협력업체 제출 현황", columns: [ { accessorKey: "documentsSubmitted", @@ -266,6 +367,8 @@ export function getPeriodicEvaluationsColumns({setRowAction}: GetColumnsProps): ] }, + + // ═══════════════════════════════════════════════════════════════ // 평가 점수 // ═══════════════════════════════════════════════════════════════ @@ -273,12 +376,12 @@ export function getPeriodicEvaluationsColumns({setRowAction}: GetColumnsProps): header: "평가 점수", columns: [ { - accessorKey: "totalScore", - header: ({ column }) => , + accessorKey: "processScore", + header: ({ column }) => , cell: ({ row }) => { - const score = row.getValue("totalScore"); + const score = row.getValue("processScore"); return score ? ( - {score.toFixed(1)} + {Number(score).toFixed(1)} ) : ( - ); @@ -287,156 +390,176 @@ export function getPeriodicEvaluationsColumns({setRowAction}: GetColumnsProps): }, { - accessorKey: "evaluationGrade", - header: ({ column }) => , + accessorKey: "priceScore", + header: ({ column }) => , cell: ({ row }) => { - const grade = row.getValue("evaluationGrade"); - return grade ? ( - {grade} + const score = row.getValue("priceScore"); + return score ? ( + {Number(score).toFixed(1)} ) : ( - ); }, - size: 60, + size: 80, }, - + { - accessorKey: "finalScore", - header: ({ column }) => , + accessorKey: "deliveryScore", + header: ({ column }) => , cell: ({ row }) => { - const finalScore = row.getValue("finalScore"); - return finalScore ? ( - {finalScore.toFixed(1)} + const score = row.getValue("deliveryScore"); + return score ? ( + {Number(score).toFixed(1)} ) : ( - ); }, - size: 90, + size: 80, + }, + + { + accessorKey: "selfEvaluationScore", + header: ({ column }) => , + cell: ({ row }) => { + const score = row.getValue("selfEvaluationScore"); + return score ? ( + {Number(score).toFixed(1)} + ) : ( + - + ); + }, + size: 80, }, + // ✅ 합계 - 4개 점수의 합으로 계산 { - accessorKey: "finalGrade", - header: ({ column }) => , + id: "totalScore", + header: ({ column }) => , cell: ({ row }) => { - const finalGrade = row.getValue("finalGrade"); - return finalGrade ? ( - - {finalGrade} - + const processScore = Number(row.getValue("processScore") || 0); + const priceScore = Number(row.getValue("priceScore") || 0); + const deliveryScore = Number(row.getValue("deliveryScore") || 0); + const selfEvaluationScore = Number(row.getValue("selfEvaluationScore") || 0); + + const total = processScore + priceScore + deliveryScore + selfEvaluationScore; + + return total > 0 ? ( + + {total.toFixed(1)} + ) : ( - ); }, - size: 90, + size: 80, }, - ] - }, - // ═══════════════════════════════════════════════════════════════ - // 진행 현황 - // ═══════════════════════════════════════════════════════════════ - { - header: "진행 현황", - columns: [ { - accessorKey: "status", - header: ({ column }) => , + accessorKey: "participationBonus", + header: ({ column }) => , cell: ({ row }) => { - const status = row.getValue("status"); - return ( - - {getStatusLabel(status)} - + const score = row.getValue("participationBonus"); + return score ? ( + +{Number(score).toFixed(1)} + ) : ( + - ); }, size: 100, }, { - id: "reviewProgress", - header: ({ column }) => , + accessorKey: "qualityDeduction", + header: ({ column }) => , cell: ({ row }) => { - const totalReviewers = row.original.totalReviewers || 0; - const completedReviewers = row.original.completedReviewers || 0; - - return getProgressBadge(completedReviewers, totalReviewers); + const score = row.getValue("qualityDeduction"); + return score ? ( + -{Number(score).toFixed(1)} + ) : ( + - + ); }, - size: 120, + size: 100, }, + // ✅ 새로운 평가점수 컬럼 추가 { - accessorKey: "reviewCompletedAt", - header: ({ column }) => , + id: "evaluationScore", + header: ({ column }) => , cell: ({ row }) => { - const completedAt = row.getValue("reviewCompletedAt"); - return completedAt ? ( - - {new Intl.DateTimeFormat("ko-KR", { - month: "2-digit", - day: "2-digit", - }).format(new Date(completedAt))} + const processScore = Number(row.getValue("processScore") || 0); + const priceScore = Number(row.getValue("priceScore") || 0); + const deliveryScore = Number(row.getValue("deliveryScore") || 0); + const selfEvaluationScore = Number(row.getValue("selfEvaluationScore") || 0); + const participationBonus = Number(row.getValue("participationBonus") || 0); + const qualityDeduction = Number(row.getValue("qualityDeduction") || 0); + + const totalScore = processScore + priceScore + deliveryScore + selfEvaluationScore; + const evaluationScore = totalScore + participationBonus - qualityDeduction; + + return totalScore > 0 ? ( + + {evaluationScore.toFixed(1)} ) : ( - ); }, - size: 100, + size: 90, }, { - accessorKey: "finalizedAt", - header: ({ column }) => , + accessorKey: "evaluationGrade", + header: ({ column }) => , cell: ({ row }) => { - const finalizedAt = row.getValue("finalizedAt"); - return finalizedAt ? ( - - {new Intl.DateTimeFormat("ko-KR", { - month: "2-digit", - day: "2-digit", - }).format(new Date(finalizedAt))} - + const grade = row.getValue("evaluationGrade"); + return grade ? ( + {grade} ) : ( - ); }, size: 80, }, + ] }, + + // ░░░ Actions ░░░ - { - id: "actions", - enableHiding: false, - size: 40, - minSize: 40, - cell: ({ row }) => { - return ( -
- - - -
- ); - }, - }, + // { + // id: "actions", + // enableHiding: false, + // size: 40, + // minSize: 40, + // cell: ({ row }) => { + // return ( + //
+ // + + // + //
+ // ); + // }, + // }, ]; } \ No newline at end of file diff --git a/lib/evaluation/table/evaluation-table.tsx b/lib/evaluation/table/evaluation-table.tsx index a628475d..9e32debb 100644 --- a/lib/evaluation/table/evaluation-table.tsx +++ b/lib/evaluation/table/evaluation-table.tsx @@ -23,7 +23,8 @@ import { useMemo } from "react" import { PeriodicEvaluationFilterSheet } from "./evaluation-filter-sheet" import { getPeriodicEvaluationsColumns } from "./evaluation-columns" import { PeriodicEvaluationView } from "@/db/schema" -import { getPeriodicEvaluations } from "../service" +import { getPeriodicEvaluations, getPeriodicEvaluationsStats } from "../service" +import { PeriodicEvaluationsTableToolbarActions } from "./periodic-evaluations-toolbar-actions" interface PeriodicEvaluationsTableProps { promises: Promise<[Awaited>]> @@ -44,17 +45,9 @@ function PeriodicEvaluationsStats({ evaluationYear }: { evaluationYear: number } try { setIsLoading(true) setError(null) - // TODO: getPeriodicEvaluationsStats 구현 필요 - const statsData = { - total: 150, - pendingSubmission: 25, - submitted: 45, - inReview: 30, - reviewCompleted: 35, - finalized: 15, - averageScore: 82.5, - completionRate: 75 - } + + // 실제 통계 함수 호출 + const statsData = await getPeriodicEvaluationsStats(evaluationYear) if (isMounted) { setStats(statsData) @@ -76,7 +69,7 @@ function PeriodicEvaluationsStats({ evaluationYear }: { evaluationYear: number } return () => { isMounted = false } - }, []) + }, [evaluationYear]) // evaluationYear 의존성 추가 if (isLoading) { return ( @@ -230,6 +223,8 @@ export function PeriodicEvaluationsTable({ promises, evaluationYear, className } const [promiseData] = React.use(promises) const tableData = promiseData + console.log(tableData) + const getSearchParam = React.useCallback((key: string, defaultValue?: string): string => { return searchParams?.get(key) ?? defaultValue ?? ""; }, [searchParams]); @@ -453,6 +448,9 @@ export function PeriodicEvaluationsTable({ promises, evaluationYear, className } onRenamePreset={renamePreset} /> + {/* TODO: PeriodicEvaluationsTableToolbarActions 구현 */} diff --git a/lib/evaluation/table/periodic-evaluation-action-dialogs.tsx b/lib/evaluation/table/periodic-evaluation-action-dialogs.tsx new file mode 100644 index 00000000..30ff9535 --- /dev/null +++ b/lib/evaluation/table/periodic-evaluation-action-dialogs.tsx @@ -0,0 +1,373 @@ +"use client" + +import * as React from "react" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Label } from "@/components/ui/label" +import { Textarea } from "@/components/ui/textarea" +import { Badge } from "@/components/ui/badge" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { FileText, Users, Calendar, Send } from "lucide-react" +import { toast } from "sonner" +import { PeriodicEvaluationView } from "@/db/schema" +import { checkExistingSubmissions, requestDocumentsFromVendors } from "../service" + + +// ================================================================ +// 2. 협력업체 자료 요청 다이얼로그 +// ================================================================ +interface RequestDocumentsDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + evaluations: PeriodicEvaluationView[] + onSuccess: () => void +} + +interface EvaluationWithSubmissionStatus extends PeriodicEvaluationView { + hasExistingSubmission?: boolean + submissionDate?: Date | null +} + +export function RequestDocumentsDialog({ + open, + onOpenChange, + evaluations, + onSuccess, +}: RequestDocumentsDialogProps) { + + console.log(evaluations) + + const [isLoading, setIsLoading] = React.useState(false) + const [isCheckingStatus, setIsCheckingStatus] = React.useState(false) + const [message, setMessage] = React.useState("") + const [evaluationsWithStatus, setEvaluationsWithStatus] = React.useState([]) + + // 제출대기 상태인 평가들만 필터링 + const pendingEvaluations = React.useMemo(() => + evaluations.filter(e => e.status === "PENDING_SUBMISSION"), + [evaluations] + ) + + React.useEffect(() => { + if (!open) return; + + // 대기 중 평가가 없으면 초기화 + if (pendingEvaluations.length === 0) { + setEvaluationsWithStatus([]); + setIsCheckingStatus(false); + return; + } + + // 상태 확인 + (async () => { + setIsCheckingStatus(true); + try { + const ids = pendingEvaluations.map(e => e.id); + const existing = await checkExistingSubmissions(ids); + + setEvaluationsWithStatus( + pendingEvaluations.map(e => ({ + ...e, + hasExistingSubmission: existing.some(s => s.periodicEvaluationId === e.id), + submissionDate: existing.find(s => s.periodicEvaluationId === e.id)?.createdAt ?? null, + })), + ); + } catch (err) { + console.error(err); + setEvaluationsWithStatus( + pendingEvaluations.map(e => ({ ...e, hasExistingSubmission: false })), + ); + } finally { + setIsCheckingStatus(false); + } + })(); + }, [open, pendingEvaluations]); // 함수 대신 값에만 의존 + + // 새 요청과 재요청 분리 + const newRequests = evaluationsWithStatus.filter(e => !e.hasExistingSubmission) + const reRequests = evaluationsWithStatus.filter(e => e.hasExistingSubmission) + + const handleSubmit = async () => { + if (!message.trim()) { + toast.error("요청 메시지를 입력해주세요.") + return + } + + setIsLoading(true) + try { + // 서버 액션 데이터 준비 + const requestData = evaluationsWithStatus.map(evaluation => ({ + periodicEvaluationId: evaluation.id, + companyId: evaluation.vendorId, + evaluationYear: evaluation.evaluationYear, + evaluationRound: evaluation.evaluationPeriod, + message: message.trim() + })) + + // 서버 액션 호출 + const result = await requestDocumentsFromVendors(requestData) + + if (result.success) { + toast.success(result.message) + onSuccess() + onOpenChange(false) + setMessage("") + } else { + toast.error(result.message) + } + } catch (error) { + console.error('Error requesting documents:', error) + toast.error("자료 요청 발송 중 오류가 발생했습니다.") + } finally { + setIsLoading(false) + } + } + + return ( + + + + + + 협력업체 자료 요청 + + + 선택된 평가의 협력업체들에게 평가 자료 제출을 요청합니다. + + + +
+ {isCheckingStatus ? ( +
+
요청 상태를 확인하고 있습니다...
+
+ ) : ( + <> + {/* 신규 요청 대상 업체 */} + {newRequests.length > 0 && ( + + + + 신규 요청 대상 ({newRequests.length}개 업체) + + + + {newRequests.map((evaluation) => ( +
+ {evaluation.vendorName} +
+ {evaluation.vendorCode} + 신규 +
+
+ ))} +
+
+ )} + + {/* 재요청 대상 업체 */} + {reRequests.length > 0 && ( + + + + 재요청 대상 ({reRequests.length}개 업체) + + + + {reRequests.map((evaluation) => ( +
+ {evaluation.vendorName} +
+ {evaluation.vendorCode} + + 재요청 + + {evaluation.submissionDate && ( + + {new Intl.DateTimeFormat("ko-KR", { + month: "2-digit", + day: "2-digit", + }).format(new Date(evaluation.submissionDate))} + + )} +
+
+ ))} +
+
+ )} + + {/* 요청 대상이 없는 경우 */} + {!isCheckingStatus && evaluationsWithStatus.length === 0 && ( + + +
+ 요청할 수 있는 평가가 없습니다. +
+
+
+ )} + + )} + + {/* 요청 메시지 */} +
+ +