diff options
Diffstat (limited to 'lib/compliance/services.ts')
| -rw-r--r-- | lib/compliance/services.ts | 899 |
1 files changed, 899 insertions, 0 deletions
diff --git a/lib/compliance/services.ts b/lib/compliance/services.ts new file mode 100644 index 00000000..03fae071 --- /dev/null +++ b/lib/compliance/services.ts @@ -0,0 +1,899 @@ +'use server' + +import db from "@/db/db"; +import { eq, desc, count, and, ne, or, ilike, asc } from "drizzle-orm"; +import { revalidatePath } from "next/cache"; +import { + complianceSurveyTemplates, + complianceQuestions, + complianceQuestionOptions, + complianceResponses, + complianceResponseAnswers, + complianceResponseFiles, +} from "@/db/schema/compliance"; +import { users } from "@/db/schema"; +import { basicContract, basicContractTemplates } from "@/db/schema/basicContractDocumnet"; +import { vendors } from "@/db/schema/vendors"; + +// 설문조사 템플릿 목록 조회 (페이지네이션 포함) +export async function getComplianceSurveyTemplatesWithPagination() { + try { + + + const templates = await db + .select() + .from(complianceSurveyTemplates) + .where(eq(complianceSurveyTemplates.isActive, true)) + .orderBy(desc(complianceSurveyTemplates.createdAt)); + + + + return { + data: templates, + pageCount: Math.ceil(templates.length / 10) + }; + } catch (error) { + console.error("Error fetching compliance survey templates:", error); + throw error; + } +} + +// 정렬 기능이 포함된 설문조사 템플릿 목록 조회 +export async function getComplianceSurveyTemplatesWithSorting(sort?: { id: string; desc: boolean }[]) { + try { + + + // 정렬 설정 + let orderBy = [desc(complianceSurveyTemplates.createdAt)]; + + if (sort && sort.length > 0) { + const validSortFields = ['id', 'name', 'description', 'version', 'isActive', 'createdAt', 'updatedAt']; + const validSorts = sort.filter(item => validSortFields.includes(item.id)); + + if (validSorts.length > 0) { + orderBy = validSorts.map((item) => { + switch (item.id) { + case 'id': + return item.desc ? desc(complianceSurveyTemplates.id) : asc(complianceSurveyTemplates.id); + case 'name': + return item.desc ? desc(complianceSurveyTemplates.name) : asc(complianceSurveyTemplates.name); + case 'description': + return item.desc ? desc(complianceSurveyTemplates.description) : asc(complianceSurveyTemplates.description); + case 'version': + return item.desc ? desc(complianceSurveyTemplates.version) : asc(complianceSurveyTemplates.version); + case 'isActive': + return item.desc ? desc(complianceSurveyTemplates.isActive) : asc(complianceSurveyTemplates.isActive); + case 'createdAt': + return item.desc ? desc(complianceSurveyTemplates.createdAt) : asc(complianceSurveyTemplates.createdAt); + case 'updatedAt': + return item.desc ? desc(complianceSurveyTemplates.updatedAt) : asc(complianceSurveyTemplates.updatedAt); + default: + return desc(complianceSurveyTemplates.createdAt); + } + }); + } + } + + const templates = await db + .select() + .from(complianceSurveyTemplates) + .where(eq(complianceSurveyTemplates.isActive, true)) + .orderBy(...orderBy); + return { + data: templates, + pageCount: Math.ceil(templates.length / 10) + }; + } catch (error) { + console.error("Error fetching compliance survey templates with sorting:", error); + throw error; + } +} + +// items 서비스와 동일한 구조의 함수 추가 +export async function getComplianceSurveyTemplates(input: { + page: number; + perPage: number; + search?: string; + filters?: Array<{ id: string; value: string }>; + joinOperator?: 'and' | 'or'; + sort?: { id: string; desc: boolean }[]; +}) { + try { + const safePerPage = Math.min(input.perPage, 100); + const offset = (input.page - 1) * safePerPage; + + let whereClause = eq(complianceSurveyTemplates.isActive, true); + + // 검색 기능 + if (input.search) { + const searchTerm = `%${input.search}%`; + whereClause = and( + eq(complianceSurveyTemplates.isActive, true), + or( + ilike(complianceSurveyTemplates.name, searchTerm), + ilike(complianceSurveyTemplates.description, searchTerm), + ilike(complianceSurveyTemplates.version, searchTerm) + ) + )!; + } + + // 정렬 - 안전한 방식으로 처리 + let orderBy = [desc(complianceSurveyTemplates.createdAt)]; + + if (input.sort && input.sort.length > 0) { + const validSortFields = ['id', 'name', 'description', 'version', 'isActive', 'createdAt', 'updatedAt']; + const validSorts = input.sort.filter(item => validSortFields.includes(item.id)); + + if (validSorts.length > 0) { + orderBy = validSorts.map((item) => { + switch (item.id) { + case 'id': + return item.desc ? desc(complianceSurveyTemplates.id) : asc(complianceSurveyTemplates.id); + case 'name': + return item.desc ? desc(complianceSurveyTemplates.name) : asc(complianceSurveyTemplates.name); + case 'description': + return item.desc ? desc(complianceSurveyTemplates.description) : asc(complianceSurveyTemplates.description); + case 'version': + return item.desc ? desc(complianceSurveyTemplates.version) : asc(complianceSurveyTemplates.version); + case 'isActive': + return item.desc ? desc(complianceSurveyTemplates.isActive) : asc(complianceSurveyTemplates.isActive); + case 'createdAt': + return item.desc ? desc(complianceSurveyTemplates.createdAt) : asc(complianceSurveyTemplates.createdAt); + case 'updatedAt': + return item.desc ? desc(complianceSurveyTemplates.updatedAt) : asc(complianceSurveyTemplates.updatedAt); + default: + return desc(complianceSurveyTemplates.createdAt); + } + }); + } + } + + const templates = await db + .select() + .from(complianceSurveyTemplates) + .where(whereClause) + .orderBy(...orderBy) + .limit(safePerPage) + .offset(offset); + + const totalCount = await db + .select({ count: count() }) + .from(complianceSurveyTemplates) + .where(whereClause); + + const total = totalCount[0]?.count || 0; + const pageCount = Math.ceil(total / safePerPage); + + return { data: templates, pageCount }; + } catch (error) { + console.error("Error fetching compliance survey templates:", error); + return { data: [], pageCount: 0 }; + } +} + +// 특정 템플릿 조회 +export async function getComplianceSurveyTemplate(templateId: number) { + try { + const [template] = await db + .select() + .from(complianceSurveyTemplates) + .where(eq(complianceSurveyTemplates.id, templateId)); + + return template; + } catch (error) { + console.error("Error fetching compliance survey template:", error); + throw error; + } +} + +// 템플릿 수정 +export async function updateComplianceSurveyTemplate(templateId: number, data: { + name?: string; + description?: string; + version?: string; + isActive?: boolean; +}) { + try { + const [template] = await db + .update(complianceSurveyTemplates) + .set({ + ...data, + updatedAt: new Date(), + }) + .where(eq(complianceSurveyTemplates.id, templateId)) + .returning(); + + revalidatePath('/evcp/compliance'); + return template; + } catch (error) { + console.error("Error updating compliance survey template:", error); + throw error; + } +} + +// 템플릿의 질문들 조회 +export async function getComplianceQuestions(templateId: number) { + try { + const questions = await db + .select() + .from(complianceQuestions) + .where(eq(complianceQuestions.templateId, templateId)) + .orderBy(complianceQuestions.displayOrder); + + return questions; + } catch (error) { + console.error("Error fetching compliance questions:", error); + throw error; + } +} + +// 질문의 옵션들 조회 +export async function getComplianceQuestionOptions(questionId: number) { + try { + const options = await db + .select() + .from(complianceQuestionOptions) + .where(eq(complianceQuestionOptions.questionId, questionId)) + .orderBy(complianceQuestionOptions.displayOrder); + + return options; + } catch (error) { + console.error("Error fetching compliance question options:", error); + throw error; + } +} + +// 선택 가능한 부모 질문들 조회 (조건부 질문용) +export async function getSelectableParentQuestions(templateId: number, excludeQuestionId?: number) { + try { + const questions = await db + .select({ + id: complianceQuestions.id, + questionNumber: complianceQuestions.questionNumber, + questionText: complianceQuestions.questionText, + questionType: complianceQuestions.questionType, + }) + .from(complianceQuestions) + .where( + and( + eq(complianceQuestions.templateId, templateId), + or( + eq(complianceQuestions.questionType, 'RADIO'), + eq(complianceQuestions.questionType, 'CHECKBOX'), + eq(complianceQuestions.questionType, 'DROPDOWN') + ), + excludeQuestionId ? ne(complianceQuestions.id, excludeQuestionId) : undefined + ) + ) + .orderBy(complianceQuestions.displayOrder); + + return questions; + } catch (error) { + console.error("Error fetching selectable parent questions:", error); + throw error; + } +} + +// 템플릿의 질문 개수 조회 +export async function getComplianceQuestionsCount(templateId: number) { + try { + const [result] = await db + .select({ count: count() }) + .from(complianceQuestions) + .where(eq(complianceQuestions.templateId, templateId)); + + return Number(result?.count || 0); + } catch (error) { + console.error("Error fetching compliance questions count:", error); + return 0; + } +} + +// 질문번호 중복 여부 확인 (템플릿 내 고유) +export async function isQuestionNumberDuplicated( + templateId: number, + questionNumber: string, + excludeQuestionId?: number +) { + const whereClause = excludeQuestionId + ? and( + eq(complianceQuestions.templateId, templateId), + eq(complianceQuestions.questionNumber, questionNumber), + ne(complianceQuestions.id, excludeQuestionId) + ) + : and( + eq(complianceQuestions.templateId, templateId), + eq(complianceQuestions.questionNumber, questionNumber) + ); + + const [row] = await db + .select({ count: count() }) + .from(complianceQuestions) + .where(whereClause); + + return Number(row?.count ?? 0) > 0; +} + +// 새로운 질문 생성 +export async function createComplianceQuestion(data: { + templateId: number; + questionNumber: string; + questionText: string; + questionType: string; + isRequired: boolean; + hasDetailText: boolean; + hasFileUpload: boolean; + displayOrder: number; + parentQuestionId?: number | null; + conditionalValue?: string; +}) { + try { + // 중복 검사 (템플릿 내 질문번호 고유) + const duplicated = await isQuestionNumberDuplicated( + data.templateId, + data.questionNumber + ); + if (duplicated) { + const error = new Error("DUPLICATE_QUESTION_NUMBER"); + throw error; + } + + const [question] = await db + .insert(complianceQuestions) + .values(data) + .returning(); + + return question; + } catch (error) { + console.error("Error creating compliance question:", error); + throw error; + } +} + +// 질문 수정 +export async function updateComplianceQuestion(questionId: number, data: { + questionNumber?: string; + questionText?: string; + questionType?: string; + isRequired?: boolean; + hasDetailText?: boolean; + hasFileUpload?: boolean; + displayOrder?: number; + isConditional?: boolean; + parentQuestionId?: number | null; + conditionalValue?: string; +}) { + try { + // 질문번호 변경 시 중복 검사 + if (typeof data.questionNumber === 'string' && data.questionNumber.trim().length > 0) { + // 현재 질문의 템플릿 ID 조회 + const [current] = await db + .select({ templateId: complianceQuestions.templateId }) + .from(complianceQuestions) + .where(eq(complianceQuestions.id, questionId)); + + if (current) { + const duplicated = await isQuestionNumberDuplicated( + current.templateId, + data.questionNumber, + questionId + ); + if (duplicated) { + const error = new Error("DUPLICATE_QUESTION_NUMBER"); + throw error; + } + } + } + + const [question] = await db + .update(complianceQuestions) + .set(data) + .where(eq(complianceQuestions.id, questionId)) + .returning(); + + return question; + } catch (error) { + console.error("Error updating compliance question:", error); + throw error; + } +} + +// 질문 삭제 +export async function deleteComplianceQuestion(questionId: number) { + try { + // 먼저 관련된 옵션들을 삭제 + await db + .delete(complianceQuestionOptions) + .where(eq(complianceQuestionOptions.questionId, questionId)); + + // 그 다음 질문을 삭제 + const [question] = await db + .delete(complianceQuestions) + .where(eq(complianceQuestions.id, questionId)) + .returning(); + + return question; + } catch (error) { + console.error("Error deleting compliance question:", error); + throw error; + } +} + +// 질문 옵션 생성 +export async function createComplianceQuestionOption(data: { + questionId: number; + optionText: string; + optionValue: string; + displayOrder: number; + allowsOtherInput?: boolean; + isCorrect?: boolean; +}) { + try { + const [option] = await db + .insert(complianceQuestionOptions) + .values(data) + .returning(); + + return option; + } catch (error) { + console.error("Error creating compliance question option:", error); + throw error; + } +} + +// 질문 옵션 수정 +export async function updateComplianceQuestionOption(optionId: number, data: { + optionText?: string; + optionValue?: string; + displayOrder?: number; + allowsOtherInput?: boolean; + isCorrect?: boolean; +}) { + try { + const [option] = await db + .update(complianceQuestionOptions) + .set(data) + .where(eq(complianceQuestionOptions.id, optionId)) + .returning(); + + return option; + } catch (error) { + console.error("Error updating compliance question option:", error); + throw error; + } +} + +// 질문 옵션 삭제 +export async function deleteComplianceQuestionOption(optionId: number) { + try { + const [option] = await db + .delete(complianceQuestionOptions) + .where(eq(complianceQuestionOptions.id, optionId)) + .returning(); + + return option; + } catch (error) { + console.error("Error deleting compliance question option:", error); + throw error; + } +} + +// 템플릿의 응답들 조회 +export async function getComplianceResponses(templateId: number) { + try { + const responses = await db + .select() + .from(complianceResponses) + .where(eq(complianceResponses.templateId, templateId)) + .orderBy(desc(complianceResponses.createdAt)); + + return responses; + } catch (error) { + console.error("Error fetching compliance responses:", error); + throw error; + } +} + +// 템플릿의 응답들과 답변들을 함께 조회 (페이지네이션 포함) +export async function getComplianceResponsesWithPagination(templateId: number) { + try { + const responses = await db + .select({ + id: complianceResponses.id, + basicContractId: complianceResponses.basicContractId, + templateId: complianceResponses.templateId, + status: complianceResponses.status, + completedAt: complianceResponses.completedAt, + reviewedBy: complianceResponses.reviewedBy, + reviewedAt: complianceResponses.reviewedAt, + reviewNotes: complianceResponses.reviewNotes, + createdAt: complianceResponses.createdAt, + updatedAt: complianceResponses.updatedAt, + answersCount: count(complianceResponseAnswers.id), + reviewerName: users.name, + templateName: complianceSurveyTemplates.name, + // Vendor 정보 추가 + vendorId: vendors.id, + vendorName: vendors.vendorName, + vendorCode: vendors.vendorCode, + // Basic Contract 정보 추가 + contractName: basicContractTemplates.templateName, + }) + .from(complianceResponses) + .leftJoin(complianceResponseAnswers, eq(complianceResponses.id, complianceResponseAnswers.responseId)) + .leftJoin(users, eq(complianceResponses.reviewedBy, users.id)) + .leftJoin(complianceSurveyTemplates, eq(complianceResponses.templateId, complianceSurveyTemplates.id)) + // Basic Contract와 Vendor 정보를 위한 JOIN 추가 + .leftJoin(basicContract, eq(complianceResponses.basicContractId, basicContract.id)) + .leftJoin(vendors, eq(basicContract.vendorId, vendors.id)) + .leftJoin(basicContractTemplates, eq(basicContract.templateId, basicContractTemplates.id)) + .where(eq(complianceResponses.templateId, templateId)) + .groupBy(complianceResponses.id, users.name, complianceSurveyTemplates.name, vendors.id, vendors.vendorName, vendors.vendorCode, basicContractTemplates.templateName) + .orderBy(desc(complianceResponses.createdAt)); + + return { + data: responses, + pageCount: Math.ceil(responses.length / 10) + }; + } catch (error) { + console.error("Error fetching compliance responses with answers:", error); + throw error; + } +} + +// 템플릿별 응답 통계 조회 +export async function getComplianceResponseStats(templateId: number) { + try { + const responses = await db + .select({ + status: complianceResponses.status, + count: count() + }) + .from(complianceResponses) + .where(eq(complianceResponses.templateId, templateId)) + .groupBy(complianceResponses.status); + + const stats = { + inProgress: 0, + completed: 0, + reviewed: 0, + total: 0 + }; + + responses.forEach(response => { + const count = Number(response.count); + stats.total += count; + + switch (response.status) { + case 'IN_PROGRESS': + stats.inProgress = count; + break; + case 'COMPLETED': + stats.completed = count; + break; + case 'REVIEWED': + stats.reviewed = count; + break; + } + }); + + return stats; + } catch (error) { + console.error("Error fetching compliance response stats:", error); + return { inProgress: 0, completed: 0, reviewed: 0, total: 0 }; + } +} + +// 특정 응답 조회 +export async function getComplianceResponse(responseId: number) { + try { + const [response] = await db + .select({ + id: complianceResponses.id, + basicContractId: complianceResponses.basicContractId, + templateId: complianceResponses.templateId, + status: complianceResponses.status, + completedAt: complianceResponses.completedAt, + reviewedBy: complianceResponses.reviewedBy, + reviewedAt: complianceResponses.reviewedAt, + reviewNotes: complianceResponses.reviewNotes, + createdAt: complianceResponses.createdAt, + updatedAt: complianceResponses.updatedAt, + // 검토자 정보 추가 + reviewerName: users.name, + reviewerEmail: users.email, + // Vendor 정보 추가 + vendorId: vendors.id, + vendorName: vendors.vendorName, + vendorCode: vendors.vendorCode, + }) + .from(complianceResponses) + .leftJoin(users, eq(complianceResponses.reviewedBy, users.id)) + .leftJoin(basicContract, eq(complianceResponses.basicContractId, basicContract.id)) + .leftJoin(vendors, eq(basicContract.vendorId, vendors.id)) + .where(eq(complianceResponses.id, responseId)); + + return response; + } catch (error) { + console.error("Error fetching compliance response:", error); + throw error; + } +} + +// 응답의 답변들 조회 +export async function getComplianceResponseAnswers(responseId: number) { + try { + const answers = await db + .select() + .from(complianceResponseAnswers) + .where(eq(complianceResponseAnswers.responseId, responseId)); + + return answers; + } catch (error) { + console.error("Error fetching compliance response answers:", error); + throw error; + } +} + +// 답변의 첨부파일들 조회 +export async function getComplianceResponseFiles(answerId: number) { + try { + const files = await db + .select() + .from(complianceResponseFiles) + .where(eq(complianceResponseFiles.answerId, answerId)); + + return files; + } catch (error) { + console.error("Error fetching compliance response files:", error); + throw error; + } +} + +// 응답의 모든 첨부파일들 조회 (responseId로) +export async function getComplianceResponseFilesByResponseId(responseId: number) { + try { + const files = await db + .select({ + id: complianceResponseFiles.id, + answerId: complianceResponseFiles.answerId, + fileName: complianceResponseFiles.fileName, + filePath: complianceResponseFiles.filePath, + fileSize: complianceResponseFiles.fileSize, + mimeType: complianceResponseFiles.mimeType, + uploadedAt: complianceResponseFiles.uploadedAt, + }) + .from(complianceResponseFiles) + .innerJoin(complianceResponseAnswers, eq(complianceResponseFiles.answerId, complianceResponseAnswers.id)) + .where(eq(complianceResponseAnswers.responseId, responseId)); + + return files; + } catch (error) { + console.error("Error fetching compliance response files by responseId:", error); + throw error; + } +} + +// 기본계약별 응답 조회 +export async function getComplianceResponseByContract(basicContractId: number) { + try { + const [response] = await db + .select() + .from(complianceResponses) + .where(eq(complianceResponses.basicContractId, basicContractId)); + + return response; + } catch (error) { + console.error("Error fetching compliance response by contract:", error); + throw error; + } +} + +// 설문조사 응답 생성 +export async function createComplianceResponse(data: { + basicContractId: number; + templateId: number; + status?: string; +}) { + try { + const [response] = await db + .insert(complianceResponses) + .values({ + basicContractId: data.basicContractId, + templateId: data.templateId, + status: data.status || 'IN_PROGRESS', + }) + .returning(); + + return response; + } catch (error) { + console.error("Error creating compliance response:", error); + throw error; + } +} + +// 답변 저장 +export async function saveComplianceResponseAnswer(data: { + responseId: number; + questionId: number; + answerValue?: string; + detailText?: string; + otherText?: string; + percentageValue?: string; +}) { + try { + const [answer] = await db + .insert(complianceResponseAnswers) + .values(data) + .returning(); + + return answer; + } catch (error) { + console.error("Error saving compliance response answer:", error); + throw error; + } +} + +// 응답 상태 업데이트 +export async function updateComplianceResponseStatus(responseId: number, status: string) { + try { + const [response] = await db + .update(complianceResponses) + .set({ status }) + .where(eq(complianceResponses.id, responseId)) + .returning(); + + return response; + } catch (error) { + console.error("Error updating compliance response status:", error); + throw error; + } +} + +// 설문조사 템플릿 생성 +export async function createComplianceSurveyTemplate(data: { + name: string; + description: string; + version: string; + isActive?: boolean; +}) { + try { + const [template] = await db + .insert(complianceSurveyTemplates) + .values({ + name: data.name, + description: data.description, + version: data.version, + isActive: data.isActive ?? true, + }) + .returning(); + + return template; + } catch (error) { + console.error("Error creating compliance survey template:", error); + throw error; + } +} + +// 서버 액션: 템플릿 생성 +export async function createTemplateAction(formData: FormData) { + try { + + + + + + const name = formData.get("name") as string + const description = formData.get("description") as string + const version = formData.get("version") as string + const isActive = formData.get("isActive") === "true" + + + + // 필수 필드 검증 + if (!name || !description || !version) { + return { error: "필수 필드가 누락되었습니다." } + } + + // 템플릿 생성 + await createComplianceSurveyTemplate({ + name, + description, + version, + isActive, + }) + + // 페이지 캐시 무효화 + revalidatePath("/evcp/compliance") + + return { success: true } + } catch (error) { + console.error("Error creating template:", error) + return { error: "템플릿 생성 중 오류가 발생했습니다." } + } +} + +// 설문조사 템플릿 삭제 (비활성화) +export async function deleteComplianceSurveyTemplate(templateId: number) { + try { + + const [template] = await db + .update(complianceSurveyTemplates) + .set({ + isActive: false, + updatedAt: new Date() + }) + .where(eq(complianceSurveyTemplates.id, templateId)) + .returning(); + + console.log(`✅ 템플릿 ${templateId} 삭제 완료:`, template); + + // 캐시 무효화 + revalidatePath("/evcp/compliance"); + + return template; + } catch (error) { + console.error("Error deleting compliance survey template:", error); + throw error; + } +} + +// 서버 액션: 템플릿 삭제 +export async function deleteTemplateAction(templateId: number) { + try { + await deleteComplianceSurveyTemplate(templateId); + revalidatePath("/evcp/compliance"); + return { success: true }; + } catch (error) { + console.error("Error deleting template:", error); + return { error: "템플릿 삭제 중 오류가 발생했습니다." }; + } +} + +// 템플릿의 연결된 데이터 개수 조회 +export async function getTemplateRelatedDataCount(templateId: number) { + try { + const [questionsCount, responsesCount] = await Promise.all([ + db + .select({ count: count() }) + .from(complianceQuestions) + .where(eq(complianceQuestions.templateId, templateId)), + db + .select({ count: count() }) + .from(complianceResponses) + .where(eq(complianceResponses.templateId, templateId)), + ]); + + return { + questionsCount: questionsCount[0]?.count || 0, + responsesCount: responsesCount[0]?.count || 0, + }; + } catch (error) { + console.error("Error getting template related data count:", error); + return { questionsCount: 0, responsesCount: 0 }; + } +} + +// 여러 템플릿의 연결된 데이터 개수 조회 +export async function getTemplatesRelatedDataCount(templateIds: number[]) { + try { + const results = await Promise.all( + templateIds.map(async (templateId) => { + const data = await getTemplateRelatedDataCount(templateId); + return { templateId, ...data }; + }) + ); + + const totalQuestions = results.reduce((sum, result) => sum + result.questionsCount, 0); + const totalResponses = results.reduce((sum, result) => sum + result.responsesCount, 0); + + return { + totalQuestions, + totalResponses, + details: results, + }; + } catch (error) { + console.error("Error getting templates related data count:", error); + return { totalQuestions: 0, totalResponses: 0, details: [] }; + } +} |
