diff options
Diffstat (limited to 'lib/basic-contract/service.ts')
| -rw-r--r-- | lib/basic-contract/service.ts | 506 |
1 files changed, 506 insertions, 0 deletions
diff --git a/lib/basic-contract/service.ts b/lib/basic-contract/service.ts index 64a50d14..58463f16 100644 --- a/lib/basic-contract/service.ts +++ b/lib/basic-contract/service.ts @@ -11,9 +11,18 @@ import { BasicContractTemplate,
basicContractTemplates,
basicContractView,
+ complianceQuestionOptions,
+ complianceQuestions,
+ complianceResponseAnswers,
+ complianceResponseFiles,
+ complianceResponses,
+ complianceSurveyTemplates,
vendorAttachments,
vendors,
type BasicContractTemplate as DBBasicContractTemplate,
+ type NewComplianceResponse,
+ type NewComplianceResponseAnswer,
+ type NewComplianceResponseFile
} from "@/db/schema";
import {
@@ -1172,3 +1181,500 @@ export async function getVendorAttachments(vendorId: number) { };
}
}
+
+// 설문조사 템플릿 전체 데이터 타입
+export interface SurveyTemplateWithQuestions {
+ id: number;
+ name: string;
+ description: string | null;
+ version: string;
+ questions: SurveyQuestion[];
+}
+
+export interface SurveyQuestion {
+ id: number;
+ questionNumber: string;
+ questionText: string;
+ questionType: string;
+ isRequired: boolean;
+ hasDetailText: boolean;
+ hasFileUpload: boolean;
+ parentQuestionId: number | null;
+ conditionalValue: string | null;
+ displayOrder: number;
+ options: SurveyQuestionOption[];
+}
+
+export interface SurveyQuestionOption {
+ id: number;
+ optionValue: string;
+ optionText: string;
+ allowsOtherInput: boolean;
+ displayOrder: number;
+}
+
+/**
+ * 활성화된 첫 번째 설문조사 템플릿과 관련 데이터를 모두 가져오기
+ */
+export async function getActiveSurveyTemplate(): Promise<SurveyTemplateWithQuestions | null> {
+ try {
+ // 1. 활성화된 첫 번째 템플릿 가져오기
+ const template = await db
+ .select()
+ .from(complianceSurveyTemplates)
+ .where(eq(complianceSurveyTemplates.isActive, true))
+ .orderBy(complianceSurveyTemplates.id)
+ .limit(1);
+
+ if (!template || template.length === 0) {
+ console.log('활성화된 설문조사 템플릿이 없습니다.');
+ return null;
+ }
+
+ const templateData = template[0];
+
+ // 2. 해당 템플릿의 모든 질문 가져오기 (displayOrder 순)
+ const questions = await db
+ .select()
+ .from(complianceQuestions)
+ .where(eq(complianceQuestions.templateId, templateData.id))
+ .orderBy(asc(complianceQuestions.displayOrder));
+
+ // 3. 각 질문의 옵션들 가져오기
+ const questionIds = questions.map(q => q.id);
+ const allOptions = questionIds.length > 0
+ ? await db
+ .select()
+ .from(complianceQuestionOptions)
+ .where(inArray(complianceQuestionOptions.questionId, questionIds))
+ .orderBy(
+ complianceQuestionOptions.questionId,
+ asc(complianceQuestionOptions.displayOrder)
+ )
+ : [];
+
+
+ // 4. 질문별로 옵션들 그룹화
+ const optionsByQuestionId = allOptions.reduce((acc, option) => {
+ if (!acc[option.questionId]) {
+ acc[option.questionId] = [];
+ }
+ acc[option.questionId].push({
+ id: option.id,
+ optionValue: option.optionValue,
+ optionText: option.optionText,
+ allowsOtherInput: option.allowsOtherInput,
+ displayOrder: option.displayOrder,
+ });
+ return acc;
+ }, {} as Record<number, SurveyQuestionOption[]>);
+
+ // 5. 최종 데이터 구성
+ const questionsWithOptions: SurveyQuestion[] = questions.map(question => ({
+ id: question.id,
+ questionNumber: question.questionNumber,
+ questionText: question.questionText,
+ questionType: question.questionType,
+ isRequired: question.isRequired,
+ hasDetailText: question.hasDetailText,
+ hasFileUpload: question.hasFileUpload,
+ parentQuestionId: question.parentQuestionId,
+ conditionalValue: question.conditionalValue,
+ displayOrder: question.displayOrder,
+ options: optionsByQuestionId[question.id] || [],
+ }));
+
+ return {
+ id: templateData.id,
+ name: templateData.name,
+ description: templateData.description,
+ version: templateData.version,
+ questions: questionsWithOptions,
+ };
+
+ } catch (error) {
+ console.error('설문조사 템플릿 로드 실패:', error);
+ return null;
+ }
+}
+
+/**
+ * 특정 템플릿 ID로 설문조사 템플릿 가져오기
+ */
+export async function getSurveyTemplateById(templateId: number): Promise<SurveyTemplateWithQuestions | null> {
+ try {
+ const template = await db
+ .select()
+ .from(complianceSurveyTemplates)
+ .where(eq(complianceSurveyTemplates.id, templateId))
+ .limit(1);
+
+ if (!template || template.length === 0) {
+ return null;
+ }
+
+ const templateData = template[0];
+
+ const questions = await db
+ .select()
+ .from(complianceQuestions)
+ .where(eq(complianceQuestions.templateId, templateId))
+ .orderBy(asc(complianceQuestions.displayOrder));
+
+ const questionIds = questions.map(q => q.id);
+ const allOptions = questionIds.length > 0
+ ? await db
+ .select()
+ .from(complianceQuestionOptions)
+ .where(
+ complianceQuestionOptions.questionId.in ?
+ complianceQuestionOptions.questionId.in(questionIds) :
+ eq(complianceQuestionOptions.questionId, questionIds[0])
+ )
+ .orderBy(
+ complianceQuestionOptions.questionId,
+ asc(complianceQuestionOptions.displayOrder)
+ )
+ : [];
+
+ const optionsByQuestionId = allOptions.reduce((acc, option) => {
+ if (!acc[option.questionId]) {
+ acc[option.questionId] = [];
+ }
+ acc[option.questionId].push({
+ id: option.id,
+ optionValue: option.optionValue,
+ optionText: option.optionText,
+ allowsOtherInput: option.allowsOtherInput,
+ displayOrder: option.displayOrder,
+ });
+ return acc;
+ }, {} as Record<number, SurveyQuestionOption[]>);
+
+ const questionsWithOptions: SurveyQuestion[] = questions.map(question => ({
+ id: question.id,
+ questionNumber: question.questionNumber,
+ questionText: question.questionText,
+ questionType: question.questionType,
+ isRequired: question.isRequired,
+ hasDetailText: question.hasDetailText,
+ hasFileUpload: question.hasFileUpload,
+ parentQuestionId: question.parentQuestionId,
+ conditionalValue: question.conditionalValue,
+ displayOrder: question.displayOrder,
+ options: optionsByQuestionId[question.id] || [],
+ }));
+
+ return {
+ id: templateData.id,
+ name: templateData.name,
+ description: templateData.description,
+ version: templateData.version,
+ questions: questionsWithOptions,
+ };
+
+ } catch (error) {
+ console.error('설문조사 템플릿 로드 실패:', error);
+ return null;
+ }
+}
+
+
+// 설문 답변 데이터 타입 정의
+export interface SurveyAnswerData {
+ questionId: number;
+ answerValue?: string;
+ detailText?: string;
+ otherText?: string;
+ files?: File[];
+}
+
+// 설문조사 완료 요청 데이터 타입
+export interface CompleteSurveyRequest {
+ contractId: number;
+ templateId: number;
+ answers: SurveyAnswerData[];
+ progressStatus?: any; // 진행 상태 정보 (옵션)
+}
+
+// 서버 액션: 설문조사 완료 처리
+export async function completeSurvey(data: CompleteSurveyRequest) {
+ try {
+ console.log('🚀 설문조사 완료 처리 시작:', {
+ contractId: data.contractId,
+ templateId: data.templateId,
+ answersCount: data.answers?.length || 0
+ });
+
+ // 입력 검증
+ if (!data.contractId || !data.templateId || !data.answers?.length) {
+ throw new Error('필수 데이터가 누락되었습니다.');
+ }
+
+ // 트랜잭션으로 처리
+ const result = await db.transaction(async (tx) => {
+ // 1. complianceResponses 테이블 upsert
+ console.log('📋 complianceResponses 처리 중...');
+
+ // 기존 응답 확인
+ const existingResponse = await tx
+ .select()
+ .from(complianceResponses)
+ .where(
+ and(
+ eq(complianceResponses.basicContractId, data.contractId),
+ eq(complianceResponses.templateId, data.templateId)
+ )
+ )
+ .limit(1);
+
+ let responseId: number;
+
+ if (existingResponse.length > 0) {
+ // 기존 응답 업데이트
+ const updateData = {
+ status: 'COMPLETED' as const,
+ completedAt: new Date(),
+ updatedAt: new Date()
+ };
+
+ await tx
+ .update(complianceResponses)
+ .set(updateData)
+ .where(eq(complianceResponses.id, existingResponse[0].id));
+
+ responseId = existingResponse[0].id;
+ console.log(`✅ 기존 응답 업데이트 완료: ID ${responseId}`);
+ } else {
+ // 새 응답 생성
+ const newResponse: NewComplianceResponse = {
+ basicContractId: data.contractId,
+ templateId: data.templateId,
+ status: 'COMPLETED',
+ completedAt: new Date()
+ };
+
+ const insertResult = await tx
+ .insert(complianceResponses)
+ .values(newResponse)
+ .returning({ id: complianceResponses.id });
+
+ responseId = insertResult[0].id;
+ console.log(`✅ 새 응답 생성 완료: ID ${responseId}`);
+ }
+
+ // 2. 기존 답변들 삭제 (파일도 함께 삭제됨 - CASCADE 설정 필요)
+ console.log('🗑️ 기존 답변들 삭제 중...');
+
+ // 먼저 기존 답변에 연결된 파일들 삭제
+ const existingAnswers = await tx
+ .select({ id: complianceResponseAnswers.id })
+ .from(complianceResponseAnswers)
+ .where(eq(complianceResponseAnswers.responseId, responseId));
+
+ if (existingAnswers.length > 0) {
+ const answerIds = existingAnswers.map(a => a.id);
+
+ // 파일들 먼저 삭제
+ for (const answerId of answerIds) {
+ await tx
+ .delete(complianceResponseFiles)
+ .where(eq(complianceResponseFiles.answerId, answerId));
+ }
+
+ // 답변들 삭제
+ await tx
+ .delete(complianceResponseAnswers)
+ .where(eq(complianceResponseAnswers.responseId, responseId));
+ }
+
+ // 3. 새로운 답변들 생성
+ console.log('📝 새로운 답변들 생성 중...');
+ const createdAnswers: { questionId: number; answerId: number; files?: File[] }[] = [];
+
+ for (const answer of data.answers) {
+ // 빈 답변은 스킵 (선택적 질문의 경우)
+ if (!answer.answerValue && !answer.detailText && (!answer.files || answer.files.length === 0)) {
+ continue;
+ }
+
+ const newAnswer: NewComplianceResponseAnswer = {
+ responseId,
+ questionId: answer.questionId,
+ answerValue: answer.answerValue || null,
+ detailText: answer.detailText || null,
+ otherText: answer.otherText || null,
+ // percentageValue는 필요시 추가 처리
+ };
+
+ const answerResult = await tx
+ .insert(complianceResponseAnswers)
+ .values(newAnswer)
+ .returning({ id: complianceResponseAnswers.id });
+
+ const answerId = answerResult[0].id;
+
+ createdAnswers.push({
+ questionId: answer.questionId,
+ answerId,
+ files: answer.files
+ });
+
+ console.log(`✅ 답변 생성: 질문 ${answer.questionId} -> 답변 ID ${answerId}`);
+ }
+
+ // 4. 파일 업로드 처리 (실제 파일 저장은 별도 로직 필요)
+ console.log('📎 파일 업로드 처리 중...');
+
+ for (const answerWithFiles of createdAnswers) {
+ if (answerWithFiles.files && answerWithFiles.files.length > 0) {
+ for (const file of answerWithFiles.files) {
+ // TODO: 실제 파일 저장 로직 구현 필요
+ // 현재는 파일 메타데이터만 저장
+
+
+ // 파일 저장 경로 생성 (예시)
+ const fileName = file.name;
+ const filePath = `/uploads/compliance/${data.contractId}/${responseId}/${answerWithFiles.answerId}/${fileName}`;
+
+ const fileUpload = await saveFile({file,filePath })
+
+ const newFile: NewComplianceResponseFile = {
+ answerId: answerWithFiles.answerId,
+ fileName,
+ filePath,
+ fileSize: file.size,
+ mimeType: file.type || 'application/octet-stream'
+ };
+
+ await tx
+ .insert(complianceResponseFiles)
+ .values(newFile);
+
+ console.log(`📎 파일 메타데이터 저장: ${fileName}`);
+ }
+ }
+ }
+
+ return {
+ responseId,
+ answersCount: createdAnswers.length,
+ success: true
+ };
+ });
+
+ console.log('🎉 설문조사 완료 처리 성공:', result);
+
+
+ return {
+ success: true,
+ message: '설문조사가 성공적으로 완료되었습니다.',
+ data: result
+ };
+
+ } catch (error) {
+ console.error('❌ 설문조사 완료 처리 실패:', error);
+
+ return {
+ success: false,
+ message: error instanceof Error ? error.message : '설문조사 저장에 실패했습니다.',
+ data: null
+ };
+ }
+}
+
+// 설문조사 응답 조회 서버 액션
+export async function getSurveyResponse(contractId: number, templateId: number) {
+ try {
+ const response = await db
+ .select()
+ .from(complianceResponses)
+ .where(
+ and(
+ eq(complianceResponses.basicContractId, contractId),
+ eq(complianceResponses.templateId, templateId)
+ )
+ )
+ .limit(1);
+
+ if (response.length === 0) {
+ return { success: true, data: null };
+ }
+
+ // 답변들과 파일들도 함께 조회
+ const answers = await db
+ .select({
+ id: complianceResponseAnswers.id,
+ questionId: complianceResponseAnswers.questionId,
+ answerValue: complianceResponseAnswers.answerValue,
+ detailText: complianceResponseAnswers.detailText,
+ otherText: complianceResponseAnswers.otherText,
+ percentageValue: complianceResponseAnswers.percentageValue,
+ })
+ .from(complianceResponseAnswers)
+ .where(eq(complianceResponseAnswers.responseId, response[0].id));
+
+ // 각 답변의 파일들 조회
+ const answersWithFiles = await Promise.all(
+ answers.map(async (answer) => {
+ const files = await db
+ .select()
+ .from(complianceResponseFiles)
+ .where(eq(complianceResponseFiles.answerId, answer.id));
+
+ return {
+ ...answer,
+ files
+ };
+ })
+ );
+
+ return {
+ success: true,
+ data: {
+ response: response[0],
+ answers: answersWithFiles
+ }
+ };
+
+ } catch (error) {
+ console.error('❌ 설문조사 응답 조회 실패:', error);
+
+ return {
+ success: false,
+ message: error instanceof Error ? error.message : '설문조사 응답 조회에 실패했습니다.',
+ data: null
+ };
+ }
+}
+
+// 파일 업로드를 위한 별도 서버 액션 (실제 파일 저장 로직)
+export async function uploadSurveyFile(file: File, contractId: number, answerId: number) {
+ try {
+ // TODO: 실제 파일 저장 구현
+ // 예: AWS S3, 로컬 파일시스템, 등등
+
+ // 현재는 예시 구현
+ const fileName = `${Date.now()}-${file.name}`;
+ const filePath = `/uploads/compliance/${contractId}/${answerId}/${fileName}`;
+
+ // 실제로는 여기서 파일을 물리적으로 저장해야 함
+ // const savedPath = await saveFileToStorage(file, filePath);
+
+ return {
+ success: true,
+ filePath,
+ fileName: file.name,
+ fileSize: file.size,
+ mimeType: file.type
+ };
+
+ } catch (error) {
+ console.error('❌ 파일 업로드 실패:', error);
+
+ return {
+ success: false,
+ message: error instanceof Error ? error.message : '파일 업로드에 실패했습니다.'
+ };
+ }
+}
\ No newline at end of file |
