From 95bbe9c583ff841220da1267630e7b2025fc36dc Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 19 Jun 2025 09:44:28 +0000 Subject: (대표님) 20250619 1844 KST 작업사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/b-rfq/service.ts | 11 - lib/esg-check-list/repository.ts | 133 ++++ lib/esg-check-list/service.ts | 601 ++++++++++++++ .../table/esg-evaluation-delete-dialog.tsx | 168 ++++ .../table/esg-evaluation-details-sheet.tsx | 188 +++++ .../table/esg-evaluation-form-sheet.tsx | 492 ++++++++++++ .../table/esg-evaluations-table-columns.tsx | 357 +++++++++ .../esg-evaluations-table-toolbar-actions.tsx | 184 +++++ lib/esg-check-list/table/esg-excel-import.tsx | 399 ++++++++++ lib/esg-check-list/table/esg-table.tsx | 236 ++++++ lib/esg-check-list/table/excel-actions.tsx | 233 ++++++ lib/esg-check-list/table/excel-utils.tsx | 304 +++++++ lib/esg-check-list/validation.ts | 30 + lib/evaluation-target-list/service.ts | 395 +++++++++ .../table/evaluation-target-table.tsx | 452 +++++++++++ .../table/evaluation-targets-columns.tsx | 345 ++++++++ .../table/evaluation-targets-filter-sheet.tsx | 756 ++++++++++++++++++ .../table/evaluation-targets-toolbar-actions.tsx | 298 +++++++ .../manual-create-evaluation-target-dialog.tsx | 772 ++++++++++++++++++ lib/evaluation-target-list/validation.ts | 169 ++++ lib/forms/services.ts | 4 +- lib/general-check-list/repository.ts | 49 ++ lib/general-check-list/service.ts | 245 ++++++ .../table/add-check-list-dialog.tsx | 112 +++ .../table/delete-check-lists-dialog.tsx | 106 +++ .../table/general-check-list-table.tsx | 63 ++ .../table/general-check-table-columns.tsx | 138 ++++ .../table/update-check-list-sheet.tsx | 162 ++++ lib/general-check-list/validation.ts | 30 + lib/incoterms/service.ts | 149 ++++ lib/incoterms/table/incoterms-add-dialog.tsx | 162 ++++ lib/incoterms/table/incoterms-edit-sheet.tsx | 146 ++++ lib/incoterms/table/incoterms-table-columns.tsx | 102 +++ lib/incoterms/table/incoterms-table-toolbar.tsx | 16 + lib/incoterms/table/incoterms-table.tsx | 116 +++ lib/incoterms/validations.ts | 33 + lib/payment-terms/service.ts | 151 ++++ .../table/payment-terms-add-dialog.tsx | 162 ++++ .../table/payment-terms-edit-sheet.tsx | 147 ++++ .../table/payment-terms-table-columns.tsx | 102 +++ .../table/payment-terms-table-toolbar.tsx | 16 + lib/payment-terms/table/payment-terms-table.tsx | 127 +++ lib/payment-terms/validations.ts | 34 + .../vendor-response/quotation-editor.tsx | 2 + lib/vendor-evaluation-submit/service.ts | 885 +++++++++++++++++++++ .../table/esg-evaluation-form-sheet.tsx | 503 ++++++++++++ .../table/evaluation-submissions-table-columns.tsx | 641 +++++++++++++++ .../table/evaluation-submit-dialog.tsx | 353 ++++++++ .../table/general-evaluation-form-sheet.tsx | 609 ++++++++++++++ .../table/submit-table.tsx | 212 +++++ lib/vendor-evaluation-submit/validation.ts | 30 + 51 files changed, 12117 insertions(+), 13 deletions(-) create mode 100644 lib/esg-check-list/repository.ts create mode 100644 lib/esg-check-list/service.ts create mode 100644 lib/esg-check-list/table/esg-evaluation-delete-dialog.tsx create mode 100644 lib/esg-check-list/table/esg-evaluation-details-sheet.tsx create mode 100644 lib/esg-check-list/table/esg-evaluation-form-sheet.tsx create mode 100644 lib/esg-check-list/table/esg-evaluations-table-columns.tsx create mode 100644 lib/esg-check-list/table/esg-evaluations-table-toolbar-actions.tsx create mode 100644 lib/esg-check-list/table/esg-excel-import.tsx create mode 100644 lib/esg-check-list/table/esg-table.tsx create mode 100644 lib/esg-check-list/table/excel-actions.tsx create mode 100644 lib/esg-check-list/table/excel-utils.tsx create mode 100644 lib/esg-check-list/validation.ts create mode 100644 lib/evaluation-target-list/service.ts create mode 100644 lib/evaluation-target-list/table/evaluation-target-table.tsx create mode 100644 lib/evaluation-target-list/table/evaluation-targets-columns.tsx create mode 100644 lib/evaluation-target-list/table/evaluation-targets-filter-sheet.tsx create mode 100644 lib/evaluation-target-list/table/evaluation-targets-toolbar-actions.tsx create mode 100644 lib/evaluation-target-list/table/manual-create-evaluation-target-dialog.tsx create mode 100644 lib/evaluation-target-list/validation.ts create mode 100644 lib/general-check-list/repository.ts create mode 100644 lib/general-check-list/service.ts create mode 100644 lib/general-check-list/table/add-check-list-dialog.tsx create mode 100644 lib/general-check-list/table/delete-check-lists-dialog.tsx create mode 100644 lib/general-check-list/table/general-check-list-table.tsx create mode 100644 lib/general-check-list/table/general-check-table-columns.tsx create mode 100644 lib/general-check-list/table/update-check-list-sheet.tsx create mode 100644 lib/general-check-list/validation.ts create mode 100644 lib/incoterms/service.ts create mode 100644 lib/incoterms/table/incoterms-add-dialog.tsx create mode 100644 lib/incoterms/table/incoterms-edit-sheet.tsx create mode 100644 lib/incoterms/table/incoterms-table-columns.tsx create mode 100644 lib/incoterms/table/incoterms-table-toolbar.tsx create mode 100644 lib/incoterms/table/incoterms-table.tsx create mode 100644 lib/incoterms/validations.ts create mode 100644 lib/payment-terms/service.ts create mode 100644 lib/payment-terms/table/payment-terms-add-dialog.tsx create mode 100644 lib/payment-terms/table/payment-terms-edit-sheet.tsx create mode 100644 lib/payment-terms/table/payment-terms-table-columns.tsx create mode 100644 lib/payment-terms/table/payment-terms-table-toolbar.tsx create mode 100644 lib/payment-terms/table/payment-terms-table.tsx create mode 100644 lib/payment-terms/validations.ts create mode 100644 lib/vendor-evaluation-submit/service.ts create mode 100644 lib/vendor-evaluation-submit/table/esg-evaluation-form-sheet.tsx create mode 100644 lib/vendor-evaluation-submit/table/evaluation-submissions-table-columns.tsx create mode 100644 lib/vendor-evaluation-submit/table/evaluation-submit-dialog.tsx create mode 100644 lib/vendor-evaluation-submit/table/general-evaluation-form-sheet.tsx create mode 100644 lib/vendor-evaluation-submit/table/submit-table.tsx create mode 100644 lib/vendor-evaluation-submit/validation.ts (limited to 'lib') diff --git a/lib/b-rfq/service.ts b/lib/b-rfq/service.ts index c5398e6c..83f0bbb5 100644 --- a/lib/b-rfq/service.ts +++ b/lib/b-rfq/service.ts @@ -227,9 +227,6 @@ export async function createRfqAction(input: CreateRfqInput) { rfqCode: bRfqs.rfqCode, }) - // 관련 페이지 캐시 무효화 - revalidateTag(tag.rfqDashboard); - revalidateTag(tag.rfq(id)); return { @@ -584,9 +581,6 @@ export async function confirmDocuments(rfqId: number) { }) .where(eq(bRfqs.id, rfqId)) - revalidateTag(tag.rfq(rfqId)); - revalidateTag(tag.rfqDashboard); - revalidateTag(tag.rfqAttachments(rfqId)); return { success: true, @@ -1245,7 +1239,6 @@ export async function removeInitialRfqs(input: RemoveInitialRfqsSchema) { await tx.delete(initialRfq).where(inArray(initialRfq.id, ids)) }) - revalidateTag(tag.initialRfqDetail) return { data: null, @@ -1298,7 +1291,6 @@ export async function modifyInitialRfq(input: ModifyInitialRfqInput) { .where(eq(initialRfq.id, id)) }) - revalidateTag(tag.initialRfqDetail) return { data: null, @@ -1675,9 +1667,6 @@ export async function sendBulkInitialRfqEmails(input: BulkEmailInput) { } } - // 9. 페이지 새로고침 - revalidateTag(tag.initialRfqDetail) - revalidateTag(tag.rfqDashboard) // 📋 RFQ 대시보드도 새로고침 return { diff --git a/lib/esg-check-list/repository.ts b/lib/esg-check-list/repository.ts new file mode 100644 index 00000000..dfe04eb3 --- /dev/null +++ b/lib/esg-check-list/repository.ts @@ -0,0 +1,133 @@ +import db from "@/db/db"; +import { esgAnswerOptions, esgEvaluationItems, esgEvaluations, esgEvaluationsView, projects } from "@/db/schema"; +import { Item, items } from "@/db/schema/items"; +import { formListsView, tagTypeClassFormMappings } from "@/db/schema/vendorData"; +import { + eq, + inArray, + not, + asc, + desc, + and, + ilike, + gte, + lte, + count, + gt, +} from "drizzle-orm"; +import { PgTransaction } from "drizzle-orm/pg-core"; +// import { DatabaseConnection } from '@/types/database'; + + +export async function selectEsgEvaluations( + tx: PgTransaction, + params: { + where?: any; + orderBy?: (ReturnType | ReturnType)[]; + offset?: number; + limit?: number; + } +) { + + const { where, orderBy, offset = 0, limit = 10 } = params; + + return await tx + .select() + .from(esgEvaluationsView) + .where(where) + .orderBy(...(orderBy ?? [asc(esgEvaluationsView.createdAt)])) + .offset(offset ?? 0) + .limit(limit ?? 10); +} + +export async function countEsgEvaluations( + tx: PgTransaction, + where?: any +) { + const result = await tx + .select({ count: count() }) + .from(esgEvaluationsView) + .where(where); + + return result[0]?.count ?? 0; +} + +// 상세 데이터 조회 (평가항목과 답변 옵션 포함) +export async function getEsgEvaluationWithDetails( + tx: PgTransaction, + id: number +) { + // 메인 평가표 정보 + const evaluation = await tx + .select() + .from(esgEvaluations) + .where(eq(esgEvaluations.id, id)) + .limit(1); + + if (!evaluation[0]) return null; + + // 평가항목들과 답변 옵션들 + const items = await tx + .select({ + // 평가항목 필드들 + itemId: esgEvaluationItems.id, + evaluationItem: esgEvaluationItems.evaluationItem, + evaluationItemDescription: esgEvaluationItems.evaluationItemDescription, + itemOrderIndex: esgEvaluationItems.orderIndex, + itemIsActive: esgEvaluationItems.isActive, + itemCreatedAt: esgEvaluationItems.createdAt, + itemUpdatedAt: esgEvaluationItems.updatedAt, + // 답변 옵션 필드들 + optionId: esgAnswerOptions.id, + answerText: esgAnswerOptions.answerText, + score: esgAnswerOptions.score, + optionOrderIndex: esgAnswerOptions.orderIndex, + optionIsActive: esgAnswerOptions.isActive, + optionCreatedAt: esgAnswerOptions.createdAt, + optionUpdatedAt: esgAnswerOptions.updatedAt, + }) + .from(esgEvaluationItems) + .leftJoin(esgAnswerOptions, eq(esgEvaluationItems.id, esgAnswerOptions.esgEvaluationItemId)) + .where(eq(esgEvaluationItems.esgEvaluationId, id)) + .orderBy( + asc(esgEvaluationItems.orderIndex), + asc(esgAnswerOptions.orderIndex) + ); + + + + // 데이터 구조화 + const itemsMap = new Map(); + + items.forEach((row) => { + if (!itemsMap.has(row.itemId)) { + itemsMap.set(row.itemId, { + id: row.itemId, + evaluationItem: row.evaluationItem, + evaluationItemDescription: row.evaluationItemDescription, + orderIndex: row.itemOrderIndex, + isActive: row.itemIsActive, + createdAt: row.itemCreatedAt, + updatedAt: row.itemUpdatedAt, + answerOptions: [], + }); + } + + if (row.optionId) { + itemsMap.get(row.itemId).answerOptions.push({ + id: row.optionId, + answerText: row.answerText, + score: row.score, + orderIndex: row.optionOrderIndex, + isActive: row.optionIsActive, + createdAt: row.optionCreatedAt, + updatedAt: row.optionUpdatedAt, + }); + } + }); + + return { + ...evaluation[0], + evaluationItems: Array.from(itemsMap.values()), + }; +} \ No newline at end of file diff --git a/lib/esg-check-list/service.ts b/lib/esg-check-list/service.ts new file mode 100644 index 00000000..500cd82c --- /dev/null +++ b/lib/esg-check-list/service.ts @@ -0,0 +1,601 @@ +'use server' + +import { and, asc, desc, ilike, or } from 'drizzle-orm'; +import db from '@/db/db'; +import { filterColumns } from "@/lib/filter-columns"; + + +import { + esgEvaluations, + esgEvaluationItems, + esgAnswerOptions, + NewEsgEvaluation, + NewEsgEvaluationItem, + NewEsgAnswerOption, + EsgEvaluationWithItems, + esgEvaluationsView +} from '@/db/schema'; +import { eq } from 'drizzle-orm'; +import { GetEsgEvaluationsSchema } from './validation'; +import { countEsgEvaluations, getEsgEvaluationWithDetails, selectEsgEvaluations } from './repository'; + +// ============ 조회 함수들 ============ + +export async function getEsgEvaluations(input: GetEsgEvaluationsSchema) { + try { + const offset = (input.page - 1) * input.perPage; + + // 고급 필터링 + const advancedWhere = filterColumns({ + table: esgEvaluationsView, + filters: input.filters, + joinOperator: input.joinOperator, + }); + + // 전역 검색 + let globalWhere; + if (input.search) { + const s = `%${input.search}%`; + globalWhere = or( + ilike(esgEvaluationsView.serialNumber, s), + ilike(esgEvaluationsView.category, s), + ilike(esgEvaluationsView.inspectionItem, s) + ); + } + + const finalWhere = and(advancedWhere, globalWhere); + + // 정렬 + const orderBy = input.sort.length > 0 + ? input.sort.map((item) => { + return item.desc + ? desc(esgEvaluationsView[item.id]) + : asc(esgEvaluationsView[item.id]); + }) + : [desc(esgEvaluationsView.createdAt)]; + + // 데이터 조회 + const { data, total } = await db.transaction(async (tx) => { + const data = await selectEsgEvaluations(tx, { + where: finalWhere, + orderBy, + offset, + limit: input.perPage, + }); + + const total = await countEsgEvaluations(tx, finalWhere); + return { data, total }; + }); + + const pageCount = Math.ceil(total / input.perPage); + return { data, pageCount }; + } catch (err) { + console.error('Error in getEsgEvaluations:', err); + return { data: [], pageCount: 0 }; + } +} + +// 단일 평가표 상세 조회 (평가항목과 답변 옵션 포함) +export async function getEsgEvaluationDetails(id: number) { + try { + return await db.transaction(async (tx) => { + return await getEsgEvaluationWithDetails(tx, id); + }); + } catch (err) { + console.error('Error in getEsgEvaluationDetails:', err); + return null; + } +} + +// ============ 생성 함수들 ============ + +export async function createEsgEvaluation(data: NewEsgEvaluation) { + try { + return await db.transaction(async (tx) => { + const [result] = await tx + .insert(esgEvaluations) + .values(data) + .returning(); + return result; + }); + } catch (err) { + console.error('Error creating ESG evaluation:', err); + throw new Error('Failed to create ESG evaluation'); + } +} + +export async function createEsgEvaluationWithItems( + evaluationData: NewEsgEvaluation, + items: Array<{ + evaluationItem: string; + orderIndex?: number; + answerOptions: Array<{ + answerText: string; + score: number; + orderIndex?: number; + }>; + }> +) { + try { + return await db.transaction(async (tx) => { + // 1. 평가표 생성 + const [evaluation] = await tx + .insert(esgEvaluations) + .values(evaluationData) + .returning(); + + // 2. 평가항목들 생성 + for (let i = 0; i < items.length; i++) { + const item = items[i]; + const [evaluationItem] = await tx + .insert(esgEvaluationItems) + .values({ + esgEvaluationId: evaluation.id, + evaluationItem: item.evaluationItem, + orderIndex: item.orderIndex ?? i, + }) + .returning(); + + // 3. 답변 옵션들 생성 + if (item.answerOptions.length > 0) { + await tx.insert(esgAnswerOptions).values( + item.answerOptions.map((option, optionIndex) => ({ + esgEvaluationItemId: evaluationItem.id, + answerText: option.answerText, + score: option.score.toString(), + orderIndex: option.orderIndex ?? optionIndex, + })) + ); + } + } + + return evaluation; + }); + } catch (err) { + console.error('Error creating ESG evaluation with items:', err); + throw new Error('Failed to create ESG evaluation with items'); + } +} + +// ============ 수정 함수들 ============ + +export async function updateEsgEvaluation( + id: number, + data: Partial +) { + try { + return await db.transaction(async (tx) => { + const [result] = await tx + .update(esgEvaluations) + .set({ ...data, updatedAt: new Date() }) + .where(eq(esgEvaluations.id, id)) + .returning(); + return result; + }); + } catch (err) { + console.error('Error updating ESG evaluation:', err); + throw new Error('Failed to update ESG evaluation'); + } +} + +export async function updateEsgEvaluationItem( + id: number, + data: Partial +) { + try { + return await db.transaction(async (tx) => { + const [result] = await tx + .update(esgEvaluationItems) + .set({ ...data, updatedAt: new Date() }) + .where(eq(esgEvaluationItems.id, id)) + .returning(); + return result; + }); + } catch (err) { + console.error('Error updating ESG evaluation item:', err); + throw new Error('Failed to update ESG evaluation item'); + } +} + +export async function updateEsgAnswerOption( + id: number, + data: Partial +) { + try { + return await db.transaction(async (tx) => { + const [result] = await tx + .update(esgAnswerOptions) + .set({ ...data, updatedAt: new Date() }) + .where(eq(esgAnswerOptions.id, id)) + .returning(); + return result; + }); + } catch (err) { + console.error('Error updating ESG answer option:', err); + throw new Error('Failed to update ESG answer option'); + } +} + +// ============ 삭제 함수들 ============ + +export async function deleteEsgEvaluation(id: number) { + try { + return await db.transaction(async (tx) => { + // Cascade delete가 설정되어 있어서 평가항목과 답변옵션들도 자동 삭제됨 + const [result] = await tx + .delete(esgEvaluations) + .where(eq(esgEvaluations.id, id)) + .returning(); + return result; + }); + } catch (err) { + console.error('Error deleting ESG evaluation:', err); + throw new Error('Failed to delete ESG evaluation'); + } +} + +export async function deleteEsgEvaluationItem(id: number) { + try { + return await db.transaction(async (tx) => { + // Cascade delete가 설정되어 있어서 답변옵션들도 자동 삭제됨 + const [result] = await tx + .delete(esgEvaluationItems) + .where(eq(esgEvaluationItems.id, id)) + .returning(); + return result; + }); + } catch (err) { + console.error('Error deleting ESG evaluation item:', err); + throw new Error('Failed to delete ESG evaluation item'); + } +} + +export async function deleteEsgAnswerOption(id: number) { + try { + return await db.transaction(async (tx) => { + const [result] = await tx + .delete(esgAnswerOptions) + .where(eq(esgAnswerOptions.id, id)) + .returning(); + return result; + }); + } catch (err) { + console.error('Error deleting ESG answer option:', err); + throw new Error('Failed to delete ESG answer option'); + } +} + +// ============ 소프트 삭제 함수들 ============ + +export async function softDeleteEsgEvaluation(id: number) { + return updateEsgEvaluation(id, { isActive: false }); +} + +export async function softDeleteEsgEvaluationItem(id: number) { + return updateEsgEvaluationItem(id, { isActive: false }); +} + +export async function softDeleteEsgAnswerOption(id: number) { + return updateEsgAnswerOption(id, { isActive: false }); +} + + + +export async function updateEsgEvaluationWithItems( + id: number, + evaluationData: { + serialNumber: string; + category: string; + inspectionItem: string; + }, + items: Array<{ + evaluationItem: string; + evaluationItemDescription: string; + answerOptions: Array<{ + answerText: string; + score: number; + }>; + }> +) { + try { + return await db.transaction(async (tx) => { + // 1. 기본 정보 수정 + const [updatedEvaluation] = await tx + .update(esgEvaluations) + .set({ + ...evaluationData, + updatedAt: new Date(), + }) + .where(eq(esgEvaluations.id, id)) + .returning(); + + // 2. 기존 평가항목들과 답변 옵션들 모두 삭제 (cascade delete로 답변옵션도 함께 삭제됨) + await tx + .delete(esgEvaluationItems) + .where(eq(esgEvaluationItems.esgEvaluationId, id)); + + // 3. 새로운 평가항목들과 답변 옵션들 생성 + for (let i = 0; i < items.length; i++) { + const item = items[i]; + + const [evaluationItem] = await tx + .insert(esgEvaluationItems) + .values({ + esgEvaluationId: id, + evaluationItem: item.evaluationItem, + orderIndex: i, + }) + .returning(); + + // 답변 옵션들 생성 + if (item.answerOptions.length > 0) { + await tx.insert(esgAnswerOptions).values( + item.answerOptions.map((option, optionIndex) => ({ + esgEvaluationItemId: evaluationItem.id, + answerText: option.answerText, + score: option.score.toString(), + orderIndex: optionIndex, + })) + ); + } + } + + return updatedEvaluation; + }); + } catch (err) { + console.error('Error updating ESG evaluation with items:', err); + + // 시리얼 번호 중복 에러 처리 + if (err instanceof Error && err.message.includes('unique')) { + throw new Error('이미 존재하는 시리얼번호입니다.'); + } + + throw new Error('평가표 수정에 실패했습니다.'); + } +} + +// ============ 소프트 삭제 버전 (데이터 보존) ============ + +export async function updateEsgEvaluationWithItemsSoft( + id: number, + evaluationData: { + serialNumber: string; + category: string; + inspectionItem: string; + }, + items: Array<{ + evaluationItem: string; + answerOptions: Array<{ + answerText: string; + score: number; + }>; + }> +) { + try { + return await db.transaction(async (tx) => { + // 1. 기본 정보 수정 + const [updatedEvaluation] = await tx + .update(esgEvaluations) + .set({ + ...evaluationData, + updatedAt: new Date(), + }) + .where(eq(esgEvaluations.id, id)) + .returning(); + + // 2. 기존 평가항목들 소프트 삭제 + await tx + .update(esgEvaluationItems) + .set({ isActive: false, updatedAt: new Date() }) + .where(eq(esgEvaluationItems.esgEvaluationId, id)); + + // 기존 답변 옵션들도 소프트 삭제 + const existingItems = await tx + .select({ id: esgEvaluationItems.id }) + .from(esgEvaluationItems) + .where(eq(esgEvaluationItems.esgEvaluationId, id)); + + for (const item of existingItems) { + await tx + .update(esgAnswerOptions) + .set({ isActive: false, updatedAt: new Date() }) + .where(eq(esgAnswerOptions.esgEvaluationItemId, item.id)); + } + + // 3. 새로운 평가항목들과 답변 옵션들 생성 + for (let i = 0; i < items.length; i++) { + const item = items[i]; + + const [evaluationItem] = await tx + .insert(esgEvaluationItems) + .values({ + esgEvaluationId: id, + evaluationItem: item.evaluationItem, + orderIndex: i, + }) + .returning(); + + // 답변 옵션들 생성 + if (item.answerOptions.length > 0) { + await tx.insert(esgAnswerOptions).values( + item.answerOptions.map((option, optionIndex) => ({ + esgEvaluationItemId: evaluationItem.id, + answerText: option.answerText, + score: option.score.toString(), + orderIndex: optionIndex, + })) + ); + } + } + + return updatedEvaluation; + }); + } catch (err) { + console.error('Error updating ESG evaluation with items (soft):', err); + + if (err instanceof Error && err.message.includes('unique')) { + throw new Error('이미 존재하는 시리얼번호입니다.'); + } + + throw new Error('평가표 수정에 실패했습니다.'); + } +} + +// ============ 생성 함수 개선 (에러 처리 추가) ============ + +export async function createEsgEvaluationWithItemsEnhanced( + evaluationData: { + serialNumber: string; + category: string; + inspectionItem: string; + }, + items: Array<{ + evaluationItem: string; + evaluationItemDescription: string; + answerOptions: Array<{ + answerText: string; + score: number; + }>; + }> +) { + try { + return await db.transaction(async (tx) => { + // 1. 평가표 생성 + const [evaluation] = await tx + .insert(esgEvaluations) + .values(evaluationData) + .returning(); + + // 2. 평가항목들과 답변 옵션들 생성 + for (let i = 0; i < items.length; i++) { + const item = items[i]; + + const [evaluationItem] = await tx + .insert(esgEvaluationItems) + .values({ + esgEvaluationId: evaluation.id, + evaluationItem: item.evaluationItem, + evaluationItemDescription: item.evaluationItemDescription, + orderIndex: i, + }) + .returning(); + + // 답변 옵션들 생성 + if (item.answerOptions.length > 0) { + await tx.insert(esgAnswerOptions).values( + item.answerOptions.map((option, optionIndex) => ({ + esgEvaluationItemId: evaluationItem.id, + answerText: option.answerText, + score: option.score.toString(), + orderIndex: optionIndex, + })) + ); + } + } + + return evaluation; + }); + } catch (err) { + console.error('Error creating ESG evaluation with items:', err); + + // 시리얼 번호 중복 에러 처리 + if (err instanceof Error && err.message.includes('unique')) { + throw new Error('이미 존재하는 시리얼번호입니다.'); + } + + throw new Error('평가표 생성에 실패했습니다.'); + } +} + +export async function deleteEsgEvaluationsBatch(ids: number[]) { + try { + if (ids.length === 0) { + throw new Error('삭제할 평가표가 없습니다.'); + } + + return await db.transaction(async (tx) => { + let deletedCount = 0; + + for (const id of ids) { + try { + // 각 평가표 삭제 (cascade delete로 관련 데이터도 함께 삭제됨) + await tx + .delete(esgEvaluations) + .where(eq(esgEvaluations.id, id)); + + deletedCount++; + } catch (error) { + console.error(`Error deleting evaluation ${id}:`, error); + // 개별 삭제 실패는 로그만 남기고 계속 진행 + } + } + + return { + total: ids.length, + deleted: deletedCount, + failed: ids.length - deletedCount + }; + }); + } catch (err) { + console.error('Error in batch delete ESG evaluations:', err); + throw new Error('평가표 일괄 삭제에 실패했습니다.'); + } +} + +export async function softDeleteEsgEvaluationsBatch(ids: number[]) { + try { + if (ids.length === 0) { + throw new Error('삭제할 평가표가 없습니다.'); + } + + return await db.transaction(async (tx) => { + let deletedCount = 0; + + for (const id of ids) { + try { + // 평가표 소프트 삭제 + await tx + .update(esgEvaluations) + .set({ + isActive: false, + updatedAt: new Date(), + }) + .where(eq(esgEvaluations.id, id)); + + // 관련 평가항목들 소프트 삭제 + await tx + .update(esgEvaluationItems) + .set({ isActive: false, updatedAt: new Date() }) + .where(eq(esgEvaluationItems.esgEvaluationId, id)); + + // 관련 답변 옵션들 소프트 삭제 + const evaluationItems = await tx + .select({ id: esgEvaluationItems.id }) + .from(esgEvaluationItems) + .where(eq(esgEvaluationItems.esgEvaluationId, id)); + + for (const item of evaluationItems) { + await tx + .update(esgAnswerOptions) + .set({ isActive: false, updatedAt: new Date() }) + .where(eq(esgAnswerOptions.esgEvaluationItemId, item.id)); + } + + deletedCount++; + } catch (error) { + console.error(`Error soft deleting evaluation ${id}:`, error); + // 개별 삭제 실패는 로그만 남기고 계속 진행 + } + } + + return { + total: ids.length, + deleted: deletedCount, + failed: ids.length - deletedCount + }; + }); + } catch (err) { + console.error('Error in batch soft delete ESG evaluations:', err); + throw new Error('평가표 일괄 삭제에 실패했습니다.'); + } +} \ No newline at end of file diff --git a/lib/esg-check-list/table/esg-evaluation-delete-dialog.tsx b/lib/esg-check-list/table/esg-evaluation-delete-dialog.tsx new file mode 100644 index 00000000..ac667483 --- /dev/null +++ b/lib/esg-check-list/table/esg-evaluation-delete-dialog.tsx @@ -0,0 +1,168 @@ +"use client" + +import { EsgEvaluationsView } from "@/db/schema" +import React from "react" +import { toast } from "sonner" +import { useTransition } from "react" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Badge } from "@/components/ui/badge" + +// 서비스 함수 import +import { deleteEsgEvaluationsBatch, softDeleteEsgEvaluationsBatch } from "../service" + +interface EsgEvaluationBatchDeleteDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + evaluations: EsgEvaluationsView[] + onSuccess: () => void + useSoftDelete?: boolean // 소프트 삭제 사용 여부 +} + +export function EsgEvaluationBatchDeleteDialog({ + open, + onOpenChange, + evaluations, + onSuccess, + useSoftDelete = false, +}: EsgEvaluationBatchDeleteDialogProps) { + const [isPending, startTransition] = useTransition() + + const evaluationCount = evaluations.length + const isSingle = evaluationCount === 1 + + const handleDelete = async () => { + if (evaluationCount === 0) return + + startTransition(async () => { + try { + const ids = evaluations.map((evaluation) => evaluation.id) + + let result + if (useSoftDelete) { + result = await softDeleteEsgEvaluationsBatch(ids) + } else { + result = await deleteEsgEvaluationsBatch(ids) + } + + // 성공 메시지 + if (result.failed > 0) { + toast.warning( + `${result.deleted}개 삭제 완료, ${result.failed}개 실패했습니다.` + ) + } else { + toast.success( + isSingle + ? '평가표가 삭제되었습니다.' + : `${result.deleted}개의 평가표가 삭제되었습니다.` + ) + } + + onSuccess() + onOpenChange(false) + } catch (error) { + console.error('Error deleting evaluations:', error) + toast.error( + error instanceof Error ? error.message : '삭제 중 오류가 발생했습니다.' + ) + } + }) + } + + if (evaluationCount === 0) return null + + return ( + + + + + {isSingle ? '평가표 삭제' : `평가표 일괄 삭제 (${evaluationCount}개)`} + + + {isSingle ? ( + <> + 정말로 이 ESG 평가표를 삭제하시겠습니까? +
+ 이 작업은 되돌릴 수 없으며, 연관된 모든 평가항목과 답변옵션들도 함께 삭제됩니다. + + ) : ( + <> + 선택된 {evaluationCount}개의 ESG 평가표를 삭제하시겠습니까? +
+ 이 작업은 되돌릴 수 없으며, 연관된 모든 평가항목과 답변옵션들도 함께 삭제됩니다. + + )} +
+
+ + {/* 삭제될 평가표 목록 */} +
+

+ {isSingle ? '삭제될 평가표:' : '삭제될 평가표 목록:'} +

+ + 5 ? "h-[200px]" : "h-auto"}> +
+ {evaluations.map((evaluation, index) => ( +
+
+
+
+ + {evaluation.serialNumber} + + + {evaluation.category} + +
+

+ {evaluation.inspectionItem} +

+
+ 평가항목: {evaluation.totalEvaluationItems}개 + 답변옵션: {evaluation.totalAnswerOptions}개 +
+
+
+
+ ))} +
+
+
+ + + + + +
+
+ ) +} \ No newline at end of file diff --git a/lib/esg-check-list/table/esg-evaluation-details-sheet.tsx b/lib/esg-check-list/table/esg-evaluation-details-sheet.tsx new file mode 100644 index 00000000..a954b58f --- /dev/null +++ b/lib/esg-check-list/table/esg-evaluation-details-sheet.tsx @@ -0,0 +1,188 @@ +"use client" + +import * as React from "react" +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm, useFieldArray } from "react-hook-form" +import { z } from "zod" +import { toast } from "sonner" +import { Plus, X, GripVertical, Trash2 } from "lucide-react" + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { Badge } from "@/components/ui/badge" +import { Separator } from "@/components/ui/separator" +import { ScrollArea } from "@/components/ui/scroll-area" + +import { getEsgEvaluationDetails } from "../service" + +interface EsgEvaluationDetailsSheetProps { + open: boolean + onOpenChange: (open: boolean) => void + evaluationId: number | null + } + + export function EsgEvaluationDetailsSheet({ + open, + onOpenChange, + evaluationId, + }: EsgEvaluationDetailsSheetProps) { + const [evaluation, setEvaluation] = React.useState(null) + const [isLoading, setIsLoading] = React.useState(false) + + console.log(evaluation) + + // 데이터 로드 + React.useEffect(() => { + if (open && evaluationId) { + setIsLoading(true) + getEsgEvaluationDetails(evaluationId) + .then(setEvaluation) + .catch((error) => { + console.error('Error loading evaluation details:', error) + toast.error('평가표 상세 정보를 불러오는데 실패했습니다.') + }) + .finally(() => setIsLoading(false)) + } + }, [open, evaluationId]) + + if (!open) return null + + return ( + + + + ESG 평가표 상세보기 + + 평가표의 상세 정보와 평가항목들을 확인합니다. + + + + {isLoading ? ( +
+
+ 로딩 중... +
+ ) : evaluation ? ( +
+
+ {/* 기본 정보 */} + + + 기본 정보 + + +
+ +

{evaluation.serialNumber}

+
+
+ +

+ {evaluation.category} +

+
+
+ +

{evaluation.inspectionItem}

+
+
+
+ + {/* 평가항목들 */} + + + 평가항목들 ({evaluation.evaluationItems?.length || 0}개) + + + {evaluation.evaluationItems?.length > 0 ? ( + + {evaluation.evaluationItems.map((item: any, index: number) => ( + + +
+ {index + 1} + {item.evaluationItem} +
+
+ +
+
+ 답변 옵션들 ({item.answerOptions?.length || 0}개) +
+ {item.answerOptions?.map((option: any, optionIndex: number) => ( +
+
+ + {optionIndex + 1} + + {option.answerText} +
+ + {Math.floor(Number(option.score))}점 + + +
+ ))} +
+
+
+ ))} +
+ ) : ( +

+ 등록된 평가항목이 없습니다. +

+ )} +
+
+
+
+ ) : ( +
+ 평가표를 찾을 수 없습니다. +
+ )} +
+
+ ) + } \ No newline at end of file diff --git a/lib/esg-check-list/table/esg-evaluation-form-sheet.tsx b/lib/esg-check-list/table/esg-evaluation-form-sheet.tsx new file mode 100644 index 00000000..be5ea735 --- /dev/null +++ b/lib/esg-check-list/table/esg-evaluation-form-sheet.tsx @@ -0,0 +1,492 @@ +"use client" + +import * as React from "react" +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm, useFieldArray } from "react-hook-form" +import { z } from "zod" +import { toast } from "sonner" +import { Plus, X, Trash2 } from "lucide-react" +import { useTransition } from "react" + +import { Button } from "@/components/ui/button" +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { ScrollArea } from "@/components/ui/scroll-area" + +// 기존 서비스 함수 import +import { + getEsgEvaluationDetails, + createEsgEvaluationWithItemsEnhanced, + updateEsgEvaluationWithItems +} from "../service" // 기존 서비스 파일 경로에 맞게 수정 + +import { EsgEvaluationsView } from "@/db/schema" + +// 폼 스키마 정의 +const evaluationFormSchema = z.object({ + serialNumber: z.string().min(1, "시리얼번호는 필수입니다"), + category: z.string().min(1, "분류는 필수입니다"), + inspectionItem: z.string().min(1, "점검항목은 필수입니다"), + evaluationItems: z.array( + z.object({ + evaluationItem: z.string().min(1, "평가항목은 필수입니다"), + evaluationItemDescription: z.string().min(1, "평가항목 설명은 필수입니다"), + answerOptions: z.array( + z.object({ + answerText: z.string().min(1, "답변 내용은 필수입니다"), + score: z.coerce.number().min(0, "점수는 0 이상이어야 합니다"), + }) + ).min(1, "최소 1개의 답변 옵션이 필요합니다"), + }) + ).min(1, "최소 1개의 평가항목이 필요합니다"), +}) + +type EvaluationFormData = z.infer + +interface EsgEvaluationFormSheetProps { + open: boolean + onOpenChange: (open: boolean) => void + evaluation: EsgEvaluationsView | null + onSuccess: () => void +} + +export function EsgEvaluationFormSheet({ + open, + onOpenChange, + evaluation, + onSuccess, +}: EsgEvaluationFormSheetProps) { + const [isPending, startTransition] = useTransition() + const isEdit = !!evaluation + + const form = useForm({ + resolver: zodResolver(evaluationFormSchema), + defaultValues: { + serialNumber: "", + category: "", + inspectionItem: "", + evaluationItems: [ + { + evaluationItem: "", + evaluationItemDescription: "", + answerOptions: [ + { answerText: "", score: 0 }, + { answerText: "", score: 0 }, + ], + }, + ], + }, + }) + + + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: "evaluationItems", + }) + + // 편집 모드일 때 기존 데이터 로드 + React.useEffect(() => { + if (open && isEdit && evaluation) { + // 기존 서비스 함수를 사용하여 상세 데이터 로드 + startTransition(async () => { + try { + const details = await getEsgEvaluationDetails(evaluation.id) + console.log(details) + + if (details) { + form.reset({ + serialNumber: details.serialNumber, + category: details.category, + inspectionItem: details.inspectionItem, + evaluationItems: details.evaluationItems?.map((item) => ({ + evaluationItem: item.evaluationItem, + evaluationItemDescription: item.evaluationItemDescription, + answerOptions: item.answerOptions?.map((option) => ({ + answerText: option.answerText, + score: parseFloat(option.score), + })) || [], + })) || [], + }) + } + } catch (error) { + console.error('Error loading evaluation for edit:', error) + toast.error(error instanceof Error ? error.message : '편집할 데이터를 불러오는데 실패했습니다.') + } + }) + } else if (open && !isEdit) { + // 새 생성 모드 + form.reset({ + serialNumber: "", + category: "", + inspectionItem: "", + evaluationItems: [ + { + evaluationItem: "", + evaluationItemDescription: "", + answerOptions: [ + { answerText: "", score: 0 }, + { answerText: "", score: 0 }, + ], + }, + ], + }) + } + }, [open, isEdit, evaluation, form]) + + const onSubmit = async (data: EvaluationFormData) => { + startTransition(async () => { + try { + // 폼 데이터를 서비스 함수에 맞는 형태로 변환 + const evaluationData = { + serialNumber: data.serialNumber, + category: data.category, + inspectionItem: data.inspectionItem, + } + + const items = data.evaluationItems.map(item => ({ + evaluationItem: item.evaluationItem, + evaluationItemDescription: item.evaluationItemDescription, + answerOptions: item.answerOptions.map(option => ({ + answerText: option.answerText, + score: option.score, + })) + })) + + if (isEdit && evaluation) { + // 수정 - 전체 평가표 수정 + await updateEsgEvaluationWithItems(evaluation.id, evaluationData, items) + toast.success('평가표가 수정되었습니다.') + } else { + // 생성 - 평가표와 항목들 함께 생성 + await createEsgEvaluationWithItemsEnhanced(evaluationData, items) + toast.success('평가표가 생성되었습니다.') + } + + onSuccess() + onOpenChange(false) + } catch (error) { + console.error('Error saving evaluation:', error) + toast.error( + error instanceof Error ? error.message : '저장 중 오류가 발생했습니다.' + ) + } + }) + } + + if (!open) return null + + return ( + + + + {/* 고정 헤더 */} + + + {isEdit ? 'ESG 평가표 수정' : '새 ESG 평가표 생성'} + + + {isEdit + ? '평가표의 정보를 수정합니다.' + : '새로운 ESG 평가표를 생성합니다.'} + + + +
+ + {/* 스크롤 가능한 콘텐츠 영역 */} + +
+ {/* 기본 정보 */} + + + 기본 정보 + + + ( + + 시리얼번호 + + + + + + )} + /> + + ( + + 분류 + + + + + + )} + /> + + ( + + 점검항목 + + + + + + )} + /> + + + + {/* 평가항목들 */} + + +
+
+ 평가항목들 + + 각 평가항목과 해당 답변 옵션들을 설정합니다. + +
+ +
+
+ +
+ {fields.map((field, index) => ( + remove(index)} + canRemove={fields.length > 1} + disabled={isPending} + /> + ))} +
+
+
+
+
+ + {/* 고정 버튼 영역 */} +
+ + +
+
+ +
+
+ ) +} + +// 평가항목 개별 폼 컴포넌트 +interface EvaluationItemFormProps { + index: number + form: any + onRemove: () => void + canRemove: boolean + disabled?: boolean +} + +function EvaluationItemForm({ + index, + form, + onRemove, + canRemove, + disabled = false, +}: EvaluationItemFormProps) { + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: `evaluationItems.${index}.answerOptions`, + }) + + + return ( + + +
+ 평가항목 {index + 1} + {canRemove && ( + + )} +
+
+ + ( + + 평가항목 + + + + + + )} + /> + ( + + 평가항목 설명 + +