summaryrefslogtreecommitdiff
path: root/lib/evaluation-submit
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-30 06:41:26 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-30 06:41:26 +0000
commit9e3458481a65bb5572b7f1916e7c068b54a434c5 (patch)
tree27cc8dfd5fc0ed2efba4b87998caf6b2747ad312 /lib/evaluation-submit
parentf9afa89a4f27283f5b115cd89ececa08145b5c89 (diff)
(최겸) 구매 협력업체 정기평가, 가입승인, 기본계약 리비전 등
Diffstat (limited to 'lib/evaluation-submit')
-rw-r--r--lib/evaluation-submit/evaluation-form.tsx12
-rw-r--r--lib/evaluation-submit/evaluation-page.tsx31
-rw-r--r--lib/evaluation-submit/service.ts213
-rw-r--r--lib/evaluation-submit/table/evaluation-submit-dialog.tsx353
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