diff options
| author | 0-Zz-ang <s1998319@gmail.com> | 2025-10-28 14:57:14 +0900 |
|---|---|---|
| committer | 0-Zz-ang <s1998319@gmail.com> | 2025-10-28 14:57:14 +0900 |
| commit | 5b0994f2af11c77b61ac59df6211ccb20fae4d44 (patch) | |
| tree | 3cdd7d9039b058d9cde776536e4c38275a720178 | |
| parent | 06cf51e5dd14e118fa8dbb8c666d78ace61cbf9b (diff) | |
(박서영)준법설문 관련요구사항 반영
| -rw-r--r-- | app/[lng]/evcp/(evcp)/(master-data)/compliance/[templateId]/responses/[responseId]/page.tsx | 4 | ||||
| -rw-r--r-- | lib/basic-contract/service.ts | 89 | ||||
| -rw-r--r-- | lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx | 22 | ||||
| -rw-r--r-- | lib/basic-contract/viewer/SurveyComponent.tsx | 20 | ||||
| -rw-r--r-- | lib/basic-contract/viewer/basic-contract-sign-viewer.tsx | 44 | ||||
| -rw-r--r-- | lib/compliance/compliance-response-detail.tsx | 147 | ||||
| -rw-r--r-- | lib/compliance/services.ts | 29 |
7 files changed, 248 insertions, 107 deletions
diff --git a/app/[lng]/evcp/(evcp)/(master-data)/compliance/[templateId]/responses/[responseId]/page.tsx b/app/[lng]/evcp/(evcp)/(master-data)/compliance/[templateId]/responses/[responseId]/page.tsx index 73d9bbac..af6bfda1 100644 --- a/app/[lng]/evcp/(evcp)/(master-data)/compliance/[templateId]/responses/[responseId]/page.tsx +++ b/app/[lng]/evcp/(evcp)/(master-data)/compliance/[templateId]/responses/[responseId]/page.tsx @@ -5,7 +5,7 @@ import { ComplianceResponseDetail } from "@/lib/compliance/compliance-response-d import { getComplianceResponse, getComplianceResponseAnswers, - getComplianceResponseFiles, + getComplianceResponseFilesByResponseId, getComplianceSurveyTemplate, getComplianceQuestions } from "@/lib/compliance/services" @@ -29,7 +29,7 @@ export default async function ResponseDetailPage({ params }: ResponseDetailPageP const promises = Promise.all([ getComplianceResponse(responseIdAsNumber), getComplianceResponseAnswers(responseIdAsNumber), - getComplianceResponseFiles(responseIdAsNumber), + getComplianceResponseFilesByResponseId(responseIdAsNumber), getComplianceSurveyTemplate(templateIdAsNumber), getComplianceQuestions(templateIdAsNumber) ]); diff --git a/lib/basic-contract/service.ts b/lib/basic-contract/service.ts index 8999a109..123d2367 100644 --- a/lib/basic-contract/service.ts +++ b/lib/basic-contract/service.ts @@ -543,16 +543,62 @@ export async function requestBasicContractInfo({ if (!vendor.email) return; // 이메일이 없으면 스킵
try {
+ const isComplianceTemplate = template.templateName?.includes('준법');
+ let selectedTemplateId = template.id;
+ let selectedTemplate = template;
+
+ if (isComplianceTemplate) {
+ const vendorUser = await db.query.users.findFirst({
+ where: and(
+ eq(users.email, vendor.email),
+ eq(users.domain, 'partners')
+ )
+ });
+
+ const userLanguage = vendorUser?.language || 'en'; // 기본값은 영어
+
+ if (userLanguage === 'ko') {
+ // 한글 준법서약 템플릿 찾기
+ const koreanTemplate = await db.query.basicContractTemplates.findFirst({
+ where: and(
+ sql`${basicContractTemplates.templateName} LIKE '%준법%'`,
+ sql`${basicContractTemplates.templateName} NOT LIKE '%영문%'`,
+ eq(basicContractTemplates.status, 'ACTIVE')
+ )
+ });
+
+ if (koreanTemplate) {
+ selectedTemplateId = koreanTemplate.id;
+ selectedTemplate = koreanTemplate;
+ }
+ } else {
+ // 영문 준법서약 템플릿 찾기
+ const englishTemplate = await db.query.basicContractTemplates.findFirst({
+ where: and(
+ sql`${basicContractTemplates.templateName} LIKE '%준법%'`,
+ sql`${basicContractTemplates.templateName} LIKE '%영문%'`,
+ eq(basicContractTemplates.status, 'ACTIVE')
+ )
+ });
+
+ if (englishTemplate) {
+ selectedTemplateId = englishTemplate.id;
+ selectedTemplate = englishTemplate;
+ console.log(`✅ 영어 사용자 ${vendor.vendorName}에게 영문 준법서약 템플릿 전송`);
+ }
+ }
+ }
+
// 3-1. basic_contract 테이블에 레코드 추가
const [newContract] = await db
.insert(basicContract)
.values({
- templateId: template.id,
+ templateId: selectedTemplateId, // 언어별로 선택된 템플릿 ID 사용
vendorId: vendor.id,
requestedBy: requestedBy,
status: "PENDING",
- fileName: template.fileName, // 템플릿 파일 이름 사용
- filePath: template.filePath, // 템플릿 파일 경로 사용
+ fileName: selectedTemplate.fileName, // 선택된 템플릿 파일 이름 사용
+ filePath: selectedTemplate.filePath, // 선택된 템플릿 파일 경로 사용
})
.returning();
@@ -1322,22 +1368,45 @@ export interface SurveyQuestionOption { /**
* 활성화된 첫 번째 설문조사 템플릿과 관련 데이터를 모두 가져오기
*/
-export async function getActiveSurveyTemplate(): Promise<SurveyTemplateWithQuestions | null> {
+export async function getActiveSurveyTemplate(language: string = 'ko'): Promise<SurveyTemplateWithQuestions | null> {
try {
- // 1. 활성화된 첫 번째 템플릿 가져오기
- const template = await db
+ // 1. 활성화된 템플릿 가져오기 (언어에 따라 필터링)
+ const templates = await db
.select()
.from(complianceSurveyTemplates)
.where(eq(complianceSurveyTemplates.isActive, true))
- .orderBy(complianceSurveyTemplates.id)
- .limit(1);
+ .orderBy(complianceSurveyTemplates.id);
- if (!template || template.length === 0) {
+ if (!templates || templates.length === 0) {
console.log('활성화된 설문조사 템플릿이 없습니다.');
return null;
}
- const templateData = template[0];
+ // 언어에 따라 적절한 템플릿 선택
+ let templateData;
+ if (language === 'en') {
+ // 영문 템플릿 찾기 (이름에 '영문' 또는 'English' 포함)
+ templateData = templates.find(t =>
+ t.name.includes('영문') ||
+ t.name.toLowerCase().includes('english') ||
+ t.name.toLowerCase().includes('en')
+ );
+ } else {
+ // 한글 템플릿 찾기 (영문이 아닌 것)
+ templateData = templates.find(t =>
+ !t.name.includes('영문') &&
+ !t.name.toLowerCase().includes('english') &&
+ !t.name.toLowerCase().includes('en')
+ );
+ }
+
+ // 적절한 템플릿을 찾지 못하면 첫 번째 템플릿 사용
+ if (!templateData) {
+ console.log(`언어 '${language}'에 맞는 템플릿을 찾지 못해 기본 템플릿을 사용합니다.`);
+ templateData = templates[0];
+ }
+
+ console.log(`✅ 선택된 설문조사 템플릿: ${templateData.name} (언어: ${language})`);
// 2. 해당 템플릿의 모든 질문 가져오기 (displayOrder 순)
const questions = await db
diff --git a/lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx b/lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx index 9a140b27..c6f82fc8 100644 --- a/lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx +++ b/lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx @@ -30,6 +30,7 @@ import { BasicContractView } from "@/db/schema" import { downloadFile, quickPreview } from "@/lib/file-download" import { toast } from "sonner" import { useRouter } from "next/navigation" +import { getComplianceResponseByBasicContractId } from "@/lib/compliance/services" interface GetColumnsProps { setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<BasicContractView> | null>> @@ -151,7 +152,26 @@ export function getDetailColumns({ <Mail className="mr-2 h-4 w-4" /> 재발송 </DropdownMenuItem> - <DropdownMenuItem onClick={() => setRowAction({ type: "view", row })}> + <DropdownMenuItem onClick={async () => { + // 준법서약 템플릿인 경우 compliance 응답 페이지로 이동 + if (contract.templateName?.includes('준법')) { + try { + const response = await getComplianceResponseByBasicContractId(contract.id); + + if (response) { + router.push(`/evcp/compliance/${response.templateId}/responses/${response.id}`); + } else { + toast.error("준법서약 응답을 찾을 수 없습니다."); + setRowAction({ type: "view", row }); + } + } catch (error) { + console.error("Error fetching compliance response:", error); + toast.error("응답 정보를 가져오는데 실패했습니다."); + } + } else { + setRowAction({ type: "view", row }); + } + }}> <FileText className="mr-2 h-4 w-4" /> 상세 정보 </DropdownMenuItem> diff --git a/lib/basic-contract/viewer/SurveyComponent.tsx b/lib/basic-contract/viewer/SurveyComponent.tsx index 299fe6fa..8662155e 100644 --- a/lib/basic-contract/viewer/SurveyComponent.tsx +++ b/lib/basic-contract/viewer/SurveyComponent.tsx @@ -34,6 +34,7 @@ interface SurveyComponentProps { contractId?: number; surveyTemplate: SurveyTemplateWithQuestions | null; surveyLoading: boolean; + surveyLoadAttempted?: boolean; // 로드 시도 여부 추가 conditionalHandler: ConditionalSurveyHandler | null; onSurveyComplete?: () => void; onSurveyDataUpdate: (data: any) => void; @@ -45,6 +46,7 @@ export const SurveyComponent: React.FC<SurveyComponentProps> = ({ contractId, surveyTemplate, surveyLoading, + surveyLoadAttempted = false, conditionalHandler, onSurveyComplete, onSurveyDataUpdate, @@ -487,7 +489,21 @@ export const SurveyComponent: React.FC<SurveyComponentProps> = ({ ); } + if (!surveyTemplate) { + if (!surveyLoadAttempted) { + return ( + <div className="h-full w-full"> + <Card className="h-full"> + <CardContent className="flex flex-col items-center justify-center h-full py-12"> + <Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-4" /> + <p className="text-sm text-muted-foreground">설문조사를 준비하는 중...</p> + </CardContent> + </Card> + </div> + ); + } + return ( <div className="h-full w-full"> <Card className="h-full"> @@ -836,8 +852,8 @@ export const SurveyComponent: React.FC<SurveyComponentProps> = ({ {uploadedFiles[question.id].map((file, index) => ( <div key={index} className="flex items-center space-x-2 text-sm"> <FileText className="h-4 w-4 text-blue-500" /> - <span>{file.fileName}</span> - <span className="text-gray-500">({(file.fileSize / 1024).toFixed(1)} KB)</span> + <span>{file.name || file.fileName}</span> + <span className="text-gray-500">({((file.size || file.fileSize || 0) / 1024).toFixed(1)} KB)</span> </div> ))} </div> diff --git a/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx b/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx index 77bfaf41..b6024b29 100644 --- a/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx +++ b/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx @@ -641,6 +641,7 @@ export function BasicContractSignViewer({ const [isInitialLoaded, setIsInitialLoaded] = useState<boolean>(false); const [surveyTemplate, setSurveyTemplate] = useState<SurveyTemplateWithQuestions | null>(null); const [surveyLoading, setSurveyLoading] = useState<boolean>(false); + const [surveyLoadAttempted, setSurveyLoadAttempted] = useState<boolean>(false); const [gtcCommentStatus, setGtcCommentStatus] = useState<{ hasComments: boolean; commentCount: number; @@ -756,16 +757,16 @@ export function BasicContractSignViewer({ useEffect(() => { setShowDialog(isOpen); - // 구매자 모드가 아닐 때만 설문조사 템플릿 로드 - if (isOpen && isComplianceTemplate && !surveyTemplate && mode !== 'buyer') { - loadSurveyTemplate(); - } - if (isOpen) { setIsInitialLoaded(false); currentDocumentPath.current = ""; } - }, [isOpen, isComplianceTemplate, mode]); + + // 구매자 모드가 아닐 때만 설문조사 템플릿 로드 + if (isOpen && isComplianceTemplate && !surveyTemplate && !surveyLoading && mode !== 'buyer') { + loadSurveyTemplate(); + } + }, [isOpen, isComplianceTemplate, surveyTemplate, surveyLoading, mode]); useEffect(() => { if (!filePath) return; @@ -790,13 +791,28 @@ export function BasicContractSignViewer({ }, [filePath, instance]); const loadSurveyTemplate = async () => { + // 이미 로딩 중이거나 템플릿이 있으면 중복 호출 방지 + if (surveyLoading || surveyTemplate) { + return; + } + setSurveyLoading(true); + setSurveyLoadAttempted(true); // 로드 시도 표시 try { - const template = await getActiveSurveyTemplate(); + + // 계약서 템플릿 이름에서 언어 판단 + let language = 'ko'; // 기본값 한글 + if (templateName && (templateName.includes('영문') || templateName.toLowerCase().includes('english'))) { + language = 'en'; + } + + + const template = await getActiveSurveyTemplate(language); + setSurveyTemplate(template); } catch (error) { - console.error('📛 설문조사 템플릿 로드 실패:', error); + setSurveyTemplate(null); } finally { setSurveyLoading(false); @@ -1046,15 +1062,17 @@ export function BasicContractSignViewer({ const handleTabChange = async (newTab: string) => { setActiveTab(newTab); - if (newTab === "survey" || newTab === "clauses") return; - - const currentInstance = webViewerInstance.current || instance; - if (!currentInstance || fileLoading) return; + // survey 탭으로 변경 시 템플릿 로드 확인 if (newTab === 'survey' && !surveyTemplate && !surveyLoading && mode !== 'buyer') { loadSurveyTemplate(); } + if (newTab === "survey" || newTab === "clauses") return; + + const currentInstance = webViewerInstance.current || instance; + if (!currentInstance || fileLoading) return; + let targetFile: FileInfo | undefined; if (newTab === "main") { targetFile = allFiles.find(f => f.type === "main"); @@ -1272,6 +1290,7 @@ export function BasicContractSignViewer({ contractId={contractId} surveyTemplate={surveyTemplate} surveyLoading={surveyLoading} + surveyLoadAttempted={surveyLoadAttempted} conditionalHandler={conditionalHandler} onSurveyComplete={onSurveyComplete} onSurveyDataUpdate={setSurveyData} @@ -1456,6 +1475,7 @@ export function BasicContractSignViewer({ contractId={contractId} surveyTemplate={surveyTemplate} surveyLoading={surveyLoading} + surveyLoadAttempted={surveyLoadAttempted} conditionalHandler={conditionalHandler} onSurveyComplete={onSurveyComplete} onSurveyDataUpdate={setSurveyData} diff --git a/lib/compliance/compliance-response-detail.tsx b/lib/compliance/compliance-response-detail.tsx index af12469c..709f3ede 100644 --- a/lib/compliance/compliance-response-detail.tsx +++ b/lib/compliance/compliance-response-detail.tsx @@ -6,21 +6,7 @@ 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 { Button } from "@/components/ui/button" import { FileText, Users, @@ -37,15 +23,15 @@ import { getComplianceResponseFilesByResponseId, getComplianceSurveyTemplate, getComplianceQuestions, - getComplianceQuestionOptions } from "./services" interface ComplianceResponseDetailProps { templateId: number responseId: number + promises?: Promise<any[]> } -export function ComplianceResponseDetail({ templateId, responseId }: ComplianceResponseDetailProps) { +export function ComplianceResponseDetail({ templateId, responseId, promises }: ComplianceResponseDetailProps) { const [response, setResponse] = React.useState<any>(null) const [answers, setAnswers] = React.useState<any[]>([]) const [files, setFiles] = React.useState<any[]>([]) @@ -56,13 +42,19 @@ export function ComplianceResponseDetail({ templateId, responseId }: ComplianceR 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) - ]) + let responseData, answersData, filesData, templateData, questionsData; + + if (promises) { + [responseData, answersData, filesData, templateData, questionsData] = await promises; + } else { + [responseData, answersData, filesData, templateData, questionsData] = await Promise.all([ + getComplianceResponse(responseId), + getComplianceResponseAnswers(responseId), + getComplianceResponseFilesByResponseId(responseId), + getComplianceSurveyTemplate(templateId), + getComplianceQuestions(templateId) + ]); + } setResponse(responseData) setAnswers(answersData) @@ -77,7 +69,7 @@ export function ComplianceResponseDetail({ templateId, responseId }: ComplianceR } fetchResponseData() - }, [templateId, responseId]) + }, [templateId, responseId, promises]) const getStatusIcon = (status: string) => { switch (status) { @@ -115,9 +107,9 @@ export function ComplianceResponseDetail({ templateId, responseId }: ComplianceR return question ? question.questionNumber : '-' } - const getQuestionType = (questionId: number) => { + const hasFileUpload = (questionId: number) => { const question = questions.find(q => q.id === questionId) - return question ? question.questionType : '-' + return question ? question.hasFileUpload : false } // 파일 다운로드 핸들러 @@ -252,24 +244,18 @@ export function ComplianceResponseDetail({ templateId, responseId }: ComplianceR 아직 답변이 없습니다. </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"> + <div className="space-y-4"> + {answers.map((answer) => ( + <div key={answer.id} className="border rounded-lg p-4 space-y-3"> + <div className="flex items-center gap-2 pb-3 border-b"> + <Badge variant="outline"> + {getQuestionNumber(answer.questionId)} + </Badge> + <span className="font-medium"> + {getQuestionText(answer.questionId)} + </span> + </div> + <div className="space-y-3"> {/* 답변 값 */} {answer.answerValue && ( <div> @@ -302,39 +288,41 @@ export function ComplianceResponseDetail({ templateId, responseId }: ComplianceR </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> + + {hasFileUpload(answer.questionId) && ( + <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> - <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> + ) : ( + <p className="text-sm text-muted-foreground">첨부된 파일이 없습니다</p> + )} + </div> </div> - </div> + )} {/* 답변 생성일 */} <div className="text-xs text-muted-foreground"> @@ -343,11 +331,10 @@ export function ComplianceResponseDetail({ templateId, responseId }: ComplianceR '-' } </div> - </div> - </AccordionContent> - </AccordionItem> + </div> + </div> ))} - </Accordion> + </div> )} </CardContent> </Card> diff --git a/lib/compliance/services.ts b/lib/compliance/services.ts index de67598b..2d3ec092 100644 --- a/lib/compliance/services.ts +++ b/lib/compliance/services.ts @@ -932,3 +932,32 @@ export async function getTemplatesRelatedDataCount(templateIds: number[]) { return { totalQuestions: 0, totalResponses: 0, details: [] }; } } + +// basic_contract_id로 compliance response 조회 +export async function getComplianceResponseByBasicContractId(basicContractId: number) { + try { + if (!basicContractId || isNaN(basicContractId) || basicContractId <= 0) { + console.error(`Invalid basicContractId: ${basicContractId}`); + return null; + } + + const [response] = await db + .select({ + id: complianceResponses.id, + templateId: complianceResponses.templateId, + basicContractId: complianceResponses.basicContractId, + status: complianceResponses.status, + completedAt: complianceResponses.completedAt, + createdAt: complianceResponses.createdAt, + updatedAt: complianceResponses.updatedAt, + }) + .from(complianceResponses) + .where(eq(complianceResponses.basicContractId, basicContractId)) + .limit(1); + + return response || null; + } catch (error) { + console.error("Error fetching compliance response by basic contract id:", error); + return null; + } +} |
