summaryrefslogtreecommitdiff
path: root/lib/compliance/compliance-response-detail.tsx
diff options
context:
space:
mode:
author0-Zz-ang <s1998319@gmail.com>2025-08-22 13:47:37 +0900
committer0-Zz-ang <s1998319@gmail.com>2025-08-22 13:47:37 +0900
commitfefca6304eefea94f41057f9f934b0e19ceb54bb (patch)
treef4914faa83e242a68d27feac58ebf0c527302cd2 /lib/compliance/compliance-response-detail.tsx
parentdbdae213e39b82ff8ee565df0774bd2f72f06140 (diff)
(박서영)Compliance 설문/응답 리스트 생성
Diffstat (limited to 'lib/compliance/compliance-response-detail.tsx')
-rw-r--r--lib/compliance/compliance-response-detail.tsx400
1 files changed, 400 insertions, 0 deletions
diff --git a/lib/compliance/compliance-response-detail.tsx b/lib/compliance/compliance-response-detail.tsx
new file mode 100644
index 00000000..af12469c
--- /dev/null
+++ b/lib/compliance/compliance-response-detail.tsx
@@ -0,0 +1,400 @@
+"use client"
+
+import * as React from "react"
+import { format } from "date-fns"
+import { ko } from "date-fns/locale"
+
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Badge } from "@/components/ui/badge"
+import { Separator } from "@/components/ui/separator"
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow
+} from "@/components/ui/table"
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from "@/components/ui/accordion"
+import {
+ FileText,
+ Users,
+ CheckCircle,
+ Clock,
+ AlertCircle,
+ Download,
+ File
+} from "lucide-react"
+
+import {
+ getComplianceResponse,
+ getComplianceResponseAnswers,
+ getComplianceResponseFilesByResponseId,
+ getComplianceSurveyTemplate,
+ getComplianceQuestions,
+ getComplianceQuestionOptions
+} from "./services"
+
+interface ComplianceResponseDetailProps {
+ templateId: number
+ responseId: number
+}
+
+export function ComplianceResponseDetail({ templateId, responseId }: ComplianceResponseDetailProps) {
+ const [response, setResponse] = React.useState<any>(null)
+ const [answers, setAnswers] = React.useState<any[]>([])
+ const [files, setFiles] = React.useState<any[]>([])
+ const [template, setTemplate] = React.useState<any>(null)
+ const [questions, setQuestions] = React.useState<any[]>([])
+ const [loading, setLoading] = React.useState(true)
+
+ React.useEffect(() => {
+ const fetchResponseData = async () => {
+ try {
+ const [responseData, answersData, filesData, templateData, questionsData] = await Promise.all([
+ getComplianceResponse(responseId),
+ getComplianceResponseAnswers(responseId),
+ getComplianceResponseFilesByResponseId(responseId),
+ getComplianceSurveyTemplate(templateId),
+ getComplianceQuestions(templateId)
+ ])
+
+ setResponse(responseData)
+ setAnswers(answersData)
+ setFiles(filesData)
+ setTemplate(templateData)
+ setQuestions(questionsData)
+ } catch (error) {
+ console.error("Error fetching response data:", error)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ fetchResponseData()
+ }, [templateId, responseId])
+
+ const getStatusIcon = (status: string) => {
+ switch (status) {
+ case 'COMPLETED':
+ return <CheckCircle className="h-4 w-4 text-green-600" />
+ case 'IN_PROGRESS':
+ return <Clock className="h-4 w-4 text-yellow-600" />
+ case 'REVIEWED':
+ return <CheckCircle className="h-4 w-4 text-blue-600" />
+ default:
+ return <AlertCircle className="h-4 w-4 text-gray-600" />
+ }
+ }
+
+ const getStatusText = (status: string) => {
+ switch (status) {
+ case 'COMPLETED':
+ return '완료'
+ case 'IN_PROGRESS':
+ return '진행중'
+ case 'REVIEWED':
+ return '검토완료'
+ default:
+ return '알 수 없음'
+ }
+ }
+
+ const getQuestionText = (questionId: number) => {
+ const question = questions.find(q => q.id === questionId)
+ return question ? question.questionText : '질문을 찾을 수 없습니다'
+ }
+
+ const getQuestionNumber = (questionId: number) => {
+ const question = questions.find(q => q.id === questionId)
+ return question ? question.questionNumber : '-'
+ }
+
+ const getQuestionType = (questionId: number) => {
+ const question = questions.find(q => q.id === questionId)
+ return question ? question.questionType : '-'
+ }
+
+ // 파일 다운로드 핸들러
+ const handleFileDownload = async (file: any) => {
+ try {
+ // 파일 다운로드 API 호출
+ const response = await fetch(`/api/compliance/files/download?fileId=${file.id}`);
+
+ if (!response.ok) {
+ throw new Error('파일 다운로드에 실패했습니다');
+ }
+
+ // Blob으로 파일 데이터 받기
+ const blob = await response.blob();
+
+ // 임시 URL 생성하여 다운로드
+ const url = window.URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = file.fileName;
+ document.body.appendChild(link);
+ link.click();
+
+ // 정리
+ document.body.removeChild(link);
+ window.URL.revokeObjectURL(url);
+
+ console.log("✅ 파일 다운로드 성공:", file.fileName);
+ } catch (error) {
+ console.error("❌ 파일 다운로드 실패:", error);
+ alert(`파일 다운로드에 실패했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`);
+ }
+ }
+
+ if (loading) {
+ return (
+ <div className="space-y-4">
+ <div className="h-8 w-48 bg-muted animate-pulse rounded" />
+ <div className="h-64 w-full bg-muted animate-pulse rounded" />
+ </div>
+ )
+ }
+
+ if (!response) {
+ return (
+ <div className="text-center py-8 text-muted-foreground">
+ 응답을 찾을 수 없습니다.
+ </div>
+ )
+ }
+
+ return (
+ <div className="space-y-6">
+ {/* 응답 정보 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <Users className="h-5 w-5" />
+ 설문조사 응답 정보
+ </CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+ <div>
+ <label className="text-sm font-medium text-muted-foreground">템플릿</label>
+ <p className="mt-1">{template?.name || '-'}</p>
+ </div>
+ <div>
+ <label className="text-sm font-medium text-muted-foreground">업체명</label>
+ <p className="mt-1">
+ {response.vendorName ? (
+ <span>{response.vendorName}</span>
+ ) : (
+ <span className="text-muted-foreground">기본계약 ID: {response.basicContractId}</span>
+ )}
+ </p>
+ {response.vendorCode && (
+ <p className="text-xs text-muted-foreground mt-1">Vendor Code: {response.vendorCode}</p>
+ )}
+ </div>
+ <div>
+ <label className="text-sm font-medium text-muted-foreground">상태</label>
+ <div className="flex items-center gap-2 mt-1">
+ {getStatusIcon(response.status)}
+ <Badge variant={response.status === 'COMPLETED' ? 'default' : 'secondary'}>
+ {getStatusText(response.status)}
+ </Badge>
+ </div>
+ </div>
+ <div>
+ <label className="text-sm font-medium text-muted-foreground">완료일</label>
+ <p className="mt-1">
+ {response.completedAt ?
+ format(new Date(response.completedAt), 'yyyy-MM-dd HH:mm', { locale: ko }) :
+ '-'
+ }
+ </p>
+ </div>
+ <div>
+ <label className="text-sm font-medium text-muted-foreground">생성일</label>
+ <p className="mt-1">
+ {response.createdAt ?
+ format(new Date(response.createdAt), 'yyyy-MM-dd HH:mm', { locale: ko }) :
+ '-'
+ }
+ </p>
+ </div>
+ <div>
+ <label className="text-sm font-medium text-muted-foreground">수정일</label>
+ <p className="mt-1">
+ {response.updatedAt ?
+ format(new Date(response.updatedAt), 'yyyy-MM-dd HH:mm', { locale: ko }) :
+ '-'
+ }
+ </p>
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+
+ {/* 답변 목록 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <FileText className="h-5 w-5" />
+ 답변 목록 ({answers.length}개)
+ </CardTitle>
+ </CardHeader>
+ <CardContent>
+ {answers.length === 0 ? (
+ <div className="text-center py-8 text-muted-foreground">
+ 아직 답변이 없습니다.
+ </div>
+ ) : (
+ <Accordion type="single" collapsible className="w-full">
+ {answers.map((answer, index) => (
+ <AccordionItem key={answer.id} value={`answer-${answer.id}`}>
+ <AccordionTrigger className="text-left">
+ <div className="flex items-center gap-2">
+ <Badge variant="outline">
+ {getQuestionNumber(answer.questionId)}
+ </Badge>
+ <span className="font-medium">
+ {getQuestionText(answer.questionId)}
+ </span>
+ <Badge variant="secondary">
+ {getQuestionType(answer.questionId)}
+ </Badge>
+ </div>
+ </AccordionTrigger>
+ <AccordionContent>
+ <div className="space-y-3 pt-2">
+ {/* 답변 값 */}
+ {answer.answerValue && (
+ <div>
+ <label className="text-sm font-medium text-muted-foreground">답변</label>
+ <p className="mt-1 p-2 bg-muted rounded">{answer.answerValue}</p>
+ </div>
+ )}
+
+ {/* 상세 설명 */}
+ {answer.detailText && (
+ <div>
+ <label className="text-sm font-medium text-muted-foreground">상세 설명</label>
+ <p className="mt-1 p-2 bg-muted rounded">{answer.detailText}</p>
+ </div>
+ )}
+
+ {/* 기타 텍스트 */}
+ {answer.otherText && (
+ <div>
+ <label className="text-sm font-medium text-muted-foreground">기타 입력</label>
+ <p className="mt-1 p-2 bg-muted rounded">{answer.otherText}</p>
+ </div>
+ )}
+
+ {/* 퍼센트 값 */}
+ {answer.percentageValue && (
+ <div>
+ <label className="text-sm font-medium text-muted-foreground">퍼센트 값</label>
+ <p className="mt-1 p-2 bg-muted rounded">{answer.percentageValue}%</p>
+ </div>
+ )}
+
+ {/* 첨부파일 */}
+ <div>
+ <label className="text-sm font-medium text-muted-foreground">첨부파일</label>
+ <div className="mt-1">
+ {files.filter(file => file.answerId === answer.id).length > 0 ? (
+ <div className="space-y-2">
+ {files
+ .filter(file => file.answerId === answer.id)
+ .map((file) => (
+ <div key={file.id} className="flex items-center justify-between p-2 bg-muted rounded">
+ <div className="flex items-center gap-2">
+ <File className="h-4 w-4 text-muted-foreground" />
+ <span className="text-sm">{file.fileName}</span>
+ <span className="text-xs text-muted-foreground">
+ ({file.fileSize ? `${(file.fileSize / 1024).toFixed(1)} KB` : '크기 정보 없음'})
+ </span>
+ </div>
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={() => handleFileDownload(file)}
+ className="h-6 w-6 p-0"
+ >
+ <Download className="h-3 w-3" />
+ </Button>
+ </div>
+ ))}
+ </div>
+ ) : (
+ <p className="text-sm text-muted-foreground">첨부된 파일이 없습니다</p>
+ )}
+ </div>
+ </div>
+
+ {/* 답변 생성일 */}
+ <div className="text-xs text-muted-foreground">
+ 답변일: {answer.createdAt ?
+ format(new Date(answer.createdAt), 'yyyy-MM-dd HH:mm', { locale: ko }) :
+ '-'
+ }
+ </div>
+ </div>
+ </AccordionContent>
+ </AccordionItem>
+ ))}
+ </Accordion>
+ )}
+ </CardContent>
+ </Card>
+
+ {/* 검토 정보 */}
+ {response.reviewedBy && (
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <CheckCircle className="h-5 w-5" />
+ 검토 정보
+ </CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+ <div>
+ <label className="text-sm font-medium text-muted-foreground">검토자</label>
+ <p className="mt-1">
+ {response.reviewerName ? (
+ <span>{response.reviewerName}</span>
+ ) : (
+ <span className="text-muted-foreground">사용자 ID: {response.reviewedBy}</span>
+ )}
+ </p>
+ {response.reviewerEmail && (
+ <p className="text-xs text-muted-foreground mt-1">{response.reviewerEmail}</p>
+ )}
+ </div>
+ <div>
+ <label className="text-sm font-medium text-muted-foreground">검토일</label>
+ <p className="mt-1">
+ {response.reviewedAt ?
+ format(new Date(response.reviewedAt), 'yyyy-MM-dd HH:mm', { locale: ko }) :
+ '-'
+ }
+ </p>
+ </div>
+ </div>
+ {response.reviewNotes && (
+ <div>
+ <label className="text-sm font-medium text-muted-foreground">검토 의견</label>
+ <p className="mt-1 p-2 bg-muted rounded">{response.reviewNotes}</p>
+ </div>
+ )}
+ </CardContent>
+ </Card>
+ )}
+ </div>
+ )
+}