diff options
Diffstat (limited to 'lib/evaluation-submit')
| -rw-r--r-- | lib/evaluation-submit/evaluation-form.tsx | 12 | ||||
| -rw-r--r-- | lib/evaluation-submit/evaluation-page.tsx | 31 | ||||
| -rw-r--r-- | lib/evaluation-submit/service.ts | 213 | ||||
| -rw-r--r-- | lib/evaluation-submit/table/evaluation-submit-dialog.tsx | 353 |
4 files changed, 176 insertions, 433 deletions
diff --git a/lib/evaluation-submit/evaluation-form.tsx b/lib/evaluation-submit/evaluation-form.tsx index fbdcee69..d51a0369 100644 --- a/lib/evaluation-submit/evaluation-form.tsx +++ b/lib/evaluation-submit/evaluation-form.tsx @@ -90,13 +90,15 @@ export function EvaluationForm({ formData, onSubmit }: EvaluationFormProps) { questions.forEach(question => { const isVariable = question.scoreType === 'variable' + // 선택된 답변 옵션 찾기 + const selectedOption = question.selectedDetailId ? + question.availableOptions.find(opt => opt.detailId === question.selectedDetailId) : null; + initial[question.criteriaId] = { detailId: isVariable ? -1 : question.selectedDetailId, score: isVariable ? - question.currentScore || null : - (question.selectedDetailId ? - question.availableOptions.find(opt => opt.detailId === question.selectedDetailId)?.score || question.currentScore || null - : question.currentScore || null), + (question.currentScore ? Number(question.currentScore) : null) : + (selectedOption?.score ?? (question.currentScore ? Number(question.currentScore) : null)), comment: question.currentComment || "", } }) @@ -108,7 +110,7 @@ export function EvaluationForm({ formData, onSubmit }: EvaluationFormProps) { console.log('Initializing attachments from server data...') const initial: Record<number, AttachmentInfo[]> = {} questions.forEach(question => { - const questionAttachments = question.attachments || [] + const questionAttachments = Array.isArray(question.attachments) ? question.attachments : [] initial[question.criteriaId] = questionAttachments if (questionAttachments.length > 0) { console.log(`Question ${question.criteriaId} has ${questionAttachments.length} attachments:`, questionAttachments) diff --git a/lib/evaluation-submit/evaluation-page.tsx b/lib/evaluation-submit/evaluation-page.tsx index 810ed03e..4497b9ef 100644 --- a/lib/evaluation-submit/evaluation-page.tsx +++ b/lib/evaluation-submit/evaluation-page.tsx @@ -8,7 +8,9 @@ import { Skeleton } from "@/components/ui/skeleton" import { AlertCircle, ArrowLeft, RefreshCw } from "lucide-react" import { Alert, AlertDescription } from "@/components/ui/alert" -import { getEvaluationFormData, EvaluationFormData } from "./service" +import { getEvaluationFormData } from "./service" +import { EvaluationFormData } from "@/types/evaluation-form" + import { EvaluationForm } from "./evaluation-form" /** @@ -174,7 +176,7 @@ export function EvaluationPage() { const [isLoading, setIsLoading] = React.useState(true) const [error, setError] = React.useState<string | null>(null) - const reviewerEvaluationId = params.id ? parseInt(params.id as string) : null + const reviewerEvaluationId = params?.id ? parseInt(params.id as string) : null // 평가 데이터 로드 const loadEvaluationData = React.useCallback(async () => { @@ -187,24 +189,25 @@ export function EvaluationPage() { try { setIsLoading(true) setError(null) - + console.log(`[CLIENT] Loading evaluation data for ID: ${reviewerEvaluationId}`) const data = await getEvaluationFormData(reviewerEvaluationId) if (!data) { - setError("평가 데이터를 찾을 수 없습니다.") + console.warn(`[CLIENT] No evaluation data returned for ID: ${reviewerEvaluationId}`) + setError("평가 데이터를 찾을 수 없습니다. 해당 평가가 존재하지 않거나 접근 권한이 없을 수 있습니다.") return } + + console.log(`[CLIENT] Successfully loaded evaluation data for ID: ${reviewerEvaluationId}`) - setFormData(data) - } catch (err) { - console.error('Failed to load evaluation data:', err) - setError( - err instanceof Error - ? err.message - : "평가 데이터를 불러오는 중 오류가 발생했습니다." - ) - } finally { - setIsLoading(false) + setFormData(data as EvaluationFormData) + } catch (err) { + console.error('[CLIENT] Failed to load evaluation data:', err) + const errorMessage = err instanceof Error ? err.message : "알 수 없는 오류" + setError(`평가 데이터를 불러오는 중 오류가 발생했습니다: ${errorMessage}`) + } finally { + setIsLoading(false) + console.log('[CLIENT] Loading completed') } }, [reviewerEvaluationId]) diff --git a/lib/evaluation-submit/service.ts b/lib/evaluation-submit/service.ts index 21ceb36f..023961de 100644 --- a/lib/evaluation-submit/service.ts +++ b/lib/evaluation-submit/service.ts @@ -94,39 +94,76 @@ function getCategoryFilterByDepartment(departmentCode: string): SQL<unknown> { */ export async function getEvaluationFormData(reviewerEvaluationId: number): Promise<EvaluationFormData | null> { try { + console.log(`[SERVER] getEvaluationFormData called with ID: ${reviewerEvaluationId}`); + + // reviewerEvaluationId 유효성 검사 + if (!reviewerEvaluationId || reviewerEvaluationId <= 0) { + console.error(`[SERVER] Invalid reviewerEvaluationId: ${reviewerEvaluationId}`); + return null; + } + // 1. 리뷰어 평가 정보 조회 (부서 정보 + 평가 대상 정보 포함) - const reviewerEvaluationInfo = await db - .select({ - id: reviewerEvaluations.id, - periodicEvaluationId: reviewerEvaluations.periodicEvaluationId, - evaluationTargetReviewerId: reviewerEvaluations.evaluationTargetReviewerId, - isCompleted: reviewerEvaluations.isCompleted, - // evaluationTargetReviewers 테이블에서 부서 정보 - departmentCode: evaluationTargetReviewers.departmentCode, - // evaluationTargets 테이블에서 division과 materialType 정보 - division: evaluationTargets.division, - materialType: evaluationTargets.materialType, - vendorName: evaluationTargets.vendorName, - vendorCode: evaluationTargets.vendorCode, - }) - .from(reviewerEvaluations) - .leftJoin( - evaluationTargetReviewers, - eq(reviewerEvaluations.evaluationTargetReviewerId, evaluationTargetReviewers.id) - ) - .leftJoin( - evaluationTargets, - eq(evaluationTargetReviewers.evaluationTargetId, evaluationTargets.id) - ) - .where(eq(reviewerEvaluations.id, reviewerEvaluationId)) - .limit(1); + let reviewerEvaluationInfo; + try { + reviewerEvaluationInfo = await db + .select({ + id: reviewerEvaluations.id, + periodicEvaluationId: reviewerEvaluations.periodicEvaluationId, + evaluationTargetReviewerId: reviewerEvaluations.evaluationTargetReviewerId, + isCompleted: reviewerEvaluations.isCompleted, + // evaluationTargetReviewers 테이블에서 부서 정보 + departmentCode: evaluationTargetReviewers.departmentCode, + // evaluationTargets 테이블에서 division과 materialType 정보 + division: evaluationTargets.division, + materialType: evaluationTargets.materialType, + vendorName: evaluationTargets.vendorName, + vendorCode: evaluationTargets.vendorCode, + }) + .from(reviewerEvaluations) + .leftJoin( + evaluationTargetReviewers, + eq(reviewerEvaluations.evaluationTargetReviewerId, evaluationTargetReviewers.id) + ) + .leftJoin( + evaluationTargets, + eq(evaluationTargetReviewers.evaluationTargetId, evaluationTargets.id) + ) + .where(eq(reviewerEvaluations.id, reviewerEvaluationId)) + .limit(1); + } catch (dbError) { + console.error(`[SERVER] Database query failed for ID ${reviewerEvaluationId}:`, dbError); + throw new Error(`데이터베이스 조회 중 오류가 발생했습니다: ${dbError instanceof Error ? dbError.message : 'Unknown database error'}`); + } if (reviewerEvaluationInfo.length === 0) { - throw new Error('Reviewer evaluation not found'); + console.warn(`[SERVER] Reviewer evaluation not found for ID: ${reviewerEvaluationId}`); + return null; } const evaluation = reviewerEvaluationInfo[0]; + // 필수 필드 검증 및 상세 로그 + console.log(`[SERVER] Found evaluation data:`, { + id: evaluation.id, + division: evaluation.division, + materialType: evaluation.materialType, + departmentCode: evaluation.departmentCode, + vendorName: evaluation.vendorName, + vendorCode: evaluation.vendorCode + }); + + if (!evaluation.division || !evaluation.materialType || !evaluation.departmentCode) { + console.error(`[SERVER] Missing required evaluation data for ID ${reviewerEvaluationId}:`, { + id: evaluation.id, + division: evaluation.division, + materialType: evaluation.materialType, + departmentCode: evaluation.departmentCode, + vendorName: evaluation.vendorName, + vendorCode: evaluation.vendorCode + }); + return null; + } + // 1-1. division과 materialType을 기반으로 reviewerType 계산 const reviewerType = calculateReviewerType(evaluation.division, evaluation.materialType); @@ -134,7 +171,9 @@ export async function getEvaluationFormData(reviewerEvaluationId: number): Promi const categoryFilter = getCategoryFilterByDepartment(evaluation.departmentCode); // 3. 해당 부서에 맞는 평가 기준들과 답변 옵션들 조회 - const criteriaWithDetails = await db + let criteriaWithDetails; + try { + criteriaWithDetails = await db .select({ // 질문 정보 (실제 스키마 기준) criteriaId: regEvalCriteria.id, @@ -168,23 +207,40 @@ export async function getEvaluationFormData(reviewerEvaluationId: number): Promi regEvalCriteria.id, regEvalCriteriaDetails.orderIndex ); + } catch (criteriaError) { + console.error(`[SERVER] Failed to fetch evaluation criteria for ID ${reviewerEvaluationId}:`, criteriaError); + throw new Error(`평가 기준 조회 중 오류가 발생했습니다: ${criteriaError instanceof Error ? criteriaError.message : 'Unknown criteria error'}`); + } + + if (!criteriaWithDetails || criteriaWithDetails.length === 0) { + console.warn(`[SERVER] No evaluation criteria found for ID ${reviewerEvaluationId} with department ${evaluation.departmentCode}`); + return null; + } // 4. 기존 응답 데이터 조회 (실제 답변만) - const existingResponses = await db - .select({ - id: reviewerEvaluationDetails.id, - reviewerEvaluationId: reviewerEvaluationDetails.reviewerEvaluationId, - regEvalCriteriaDetailsId: reviewerEvaluationDetails.regEvalCriteriaDetailsId, - score: reviewerEvaluationDetails.score, - comment: reviewerEvaluationDetails.comment, - createdAt: reviewerEvaluationDetails.createdAt, - updatedAt: reviewerEvaluationDetails.updatedAt, - }) - .from(reviewerEvaluationDetails) - .where(eq(reviewerEvaluationDetails.reviewerEvaluationId, reviewerEvaluationId)); + let existingResponses; + try { + existingResponses = await db + .select({ + id: reviewerEvaluationDetails.id, + reviewerEvaluationId: reviewerEvaluationDetails.reviewerEvaluationId, + regEvalCriteriaDetailsId: reviewerEvaluationDetails.regEvalCriteriaDetailsId, + score: reviewerEvaluationDetails.score, + comment: reviewerEvaluationDetails.comment, + createdAt: reviewerEvaluationDetails.createdAt, + updatedAt: reviewerEvaluationDetails.updatedAt, + }) + .from(reviewerEvaluationDetails) + .where(eq(reviewerEvaluationDetails.reviewerEvaluationId, reviewerEvaluationId)); + } catch (responseError) { + console.error(`[SERVER] Failed to fetch existing responses for ID ${reviewerEvaluationId}:`, responseError); + existingResponses = []; // 기본값 설정 + } // 📎 5. 첨부파일 정보 조회 - const attachmentsData = await db + let attachmentsData; + try { + attachmentsData = await db .select({ // 첨부파일 정보 attachmentId: reviewerEvaluationAttachments.id, @@ -229,6 +285,10 @@ export async function getEvaluationFormData(reviewerEvaluationId: number): Promi ) .where(eq(reviewerEvaluationDetails.reviewerEvaluationId, reviewerEvaluationId)) .orderBy(desc(reviewerEvaluationAttachments.createdAt)); + } catch (attachmentError) { + console.error(`[SERVER] Failed to fetch attachments for ID ${reviewerEvaluationId}:`, attachmentError); + attachmentsData = []; // 기본값 설정 + } // 📎 6. 첨부파일을 질문별로 그룹화 const attachmentsByQuestion = new Map<number, AttachmentInfo[]>(); @@ -286,7 +346,7 @@ export async function getEvaluationFormData(reviewerEvaluationId: number): Promi category2: record.category2, item: record.item, classification: record.classification, - range: record.range, + range: record.range || null, scoreType: record.scoreType, remarks: record.remarks, availableOptions: [], @@ -305,10 +365,10 @@ export async function getEvaluationFormData(reviewerEvaluationId: number): Promi // 답변 옵션 추가 const question = questionsMap.get(criteriaId)!; question.availableOptions.push({ - detailId: record.detailId, - detail: record.detail, + detailId: record.detailId || 0, + detail: record.detail || '', score: score, - orderIndex: record.orderIndex, + orderIndex: record.orderIndex || 0, }); }); @@ -392,7 +452,15 @@ export async function getEvaluationFormData(reviewerEvaluationId: number): Promi }; } catch (err) { - console.error('Error in getEvaluationFormData:', err); + console.error(`[SERVER] Error in getEvaluationFormData for ID ${reviewerEvaluationId}:`, err); + // 데이터베이스 연결 오류나 쿼리 실행 오류 등은 여기서 처리 + if (err instanceof Error) { + if (err.message.includes('Connection') || err.message.includes('timeout')) { + console.error(`[SERVER] Database connection error: ${err.message}`); + } else if (err.message.includes('syntax') || err.message.includes('column')) { + console.error(`[SERVER] Database query error: ${err.message}`); + } + } return null; } } @@ -546,12 +614,25 @@ export async function updateEvaluationResponse( selectedDetail = detailResult[0]; } - // 2. reviewerEvaluation 정보 조회 (periodicEvaluationId 포함) + // 2. reviewerEvaluation 정보 조회 (periodicEvaluationId, division, materialType 포함) const reviewerEvaluationInfo = await tx .select({ periodicEvaluationId: reviewerEvaluations.periodicEvaluationId, + // evaluationTargetReviewers 테이블에서 부서 정보 + departmentCode: evaluationTargetReviewers.departmentCode, + // evaluationTargets 테이블에서 division과 materialType 정보 + division: evaluationTargets.division, + materialType: evaluationTargets.materialType, }) .from(reviewerEvaluations) + .leftJoin( + evaluationTargetReviewers, + eq(reviewerEvaluations.evaluationTargetReviewerId, evaluationTargetReviewers.id) + ) + .leftJoin( + evaluationTargets, + eq(evaluationTargetReviewers.evaluationTargetId, evaluationTargets.id) + ) .where(eq(reviewerEvaluations.id, reviewerEvaluationId)) .limit(1); @@ -559,7 +640,14 @@ export async function updateEvaluationResponse( throw new Error('Reviewer evaluation not found'); } - const { periodicEvaluationId } = reviewerEvaluationInfo[0]; + const evaluation = reviewerEvaluationInfo[0]; + + // 필수 필드 검증 + if (!evaluation.division || !evaluation.materialType || !evaluation.departmentCode) { + throw new Error('Missing required evaluation data'); + } + + const { periodicEvaluationId } = evaluation; // 3. periodicEvaluation의 현재 상태 확인 및 업데이트 const currentStatus = await tx @@ -589,12 +677,13 @@ export async function updateEvaluationResponse( score = customScore; } else { // 일반 타입인 경우 리뷰어 타입에 맞는 점수 가져오기 - const evaluationInfo = await getEvaluationFormData(reviewerEvaluationId); - if (!evaluationInfo) { - throw new Error('Evaluation not found'); + if (!selectedDetail) { + throw new Error('Selected detail not found'); } - const calculatedScore = getScoreByReviewerType(selectedDetail!, evaluationInfo.evaluationInfo.reviewerType); + // reviewerType 계산 + const reviewerTypeForScore = calculateReviewerType(evaluation.division, evaluation.materialType); + const calculatedScore = getScoreByReviewerType(selectedDetail, reviewerTypeForScore); if (calculatedScore === null) { throw new Error('Score not found for this reviewer type'); } @@ -697,16 +786,18 @@ export async function updateVariableEvaluationResponse( } // 3. 해당 평가 기준에 대한 기존 응답들 삭제 - await tx - .delete(reviewerEvaluationDetails) - .where( - and( - eq(reviewerEvaluationDetails.reviewerEvaluationId, reviewerEvaluationId), - sql`${reviewerEvaluationDetails.regEvalCriteriaDetailsId} IN ( - SELECT id FROM reg_eval_criteria_details WHERE criteria_id = ${criteriaId} - )` - ) - ); + if (criteriaId) { + await tx + .delete(reviewerEvaluationDetails) + .where( + and( + eq(reviewerEvaluationDetails.reviewerEvaluationId, reviewerEvaluationId), + sql`${reviewerEvaluationDetails.regEvalCriteriaDetailsId} IN ( + SELECT id FROM reg_eval_criteria_details WHERE criteria_id = ${criteriaId} + )` + ) + ); + } // 4. 새로운 응답 생성 (variable 타입은 regEvalCriteriaDetailsId가 null) const [newDetail] = await tx diff --git a/lib/evaluation-submit/table/evaluation-submit-dialog.tsx b/lib/evaluation-submit/table/evaluation-submit-dialog.tsx deleted file mode 100644 index 20ed5f30..00000000 --- a/lib/evaluation-submit/table/evaluation-submit-dialog.tsx +++ /dev/null @@ -1,353 +0,0 @@ -"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 |
