summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/basic-contract/service.ts89
-rw-r--r--lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx22
-rw-r--r--lib/basic-contract/viewer/SurveyComponent.tsx20
-rw-r--r--lib/basic-contract/viewer/basic-contract-sign-viewer.tsx44
-rw-r--r--lib/compliance/compliance-response-detail.tsx147
-rw-r--r--lib/compliance/services.ts29
6 files changed, 246 insertions, 105 deletions
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;
+ }
+}