diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-04 04:32:04 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-04 04:32:04 +0000 |
| commit | 0793ef1e3aa2232ce72debd57ba449a699e0c734 (patch) | |
| tree | a6eb1efcb74396890cbba211ae7054a4bc02d82b /lib | |
| parent | b9a109073d11262dd7ed84e25ff3cd0144c0c391 (diff) | |
(최겸) 0704 평가기준표 수정(create, detail, update 및 excel 기능)
Diffstat (limited to 'lib')
9 files changed, 1943 insertions, 1742 deletions
diff --git a/lib/evaluation-criteria/excel/reg-eval-criteria-excel-export.ts b/lib/evaluation-criteria/excel/reg-eval-criteria-excel-export.ts deleted file mode 100644 index b4254b80..00000000 --- a/lib/evaluation-criteria/excel/reg-eval-criteria-excel-export.ts +++ /dev/null @@ -1,303 +0,0 @@ -import { type Table } from "@tanstack/react-table"; -import ExcelJS from "exceljs"; -import { type RegEvalCriteriaView } from "@/db/schema"; -import { - REG_EVAL_CRITERIA_CATEGORY, - REG_EVAL_CRITERIA_CATEGORY2, - REG_EVAL_CRITERIA_ITEM, -} from "@/db/schema"; - -/** - * 평가 기준 데이터를 Excel로 내보내기 - */ -export async function exportRegEvalCriteriaToExcel<TData extends RegEvalCriteriaView>( - table: Table<TData>, - { - filename = "Regular_Evaluation_Criteria", - excludeColumns = ["select", "actions"], - sheetName = "평가 기준", - }: { - filename?: string; - excludeColumns?: string[]; - sheetName?: string; - } = {} -): Promise<void> { - // 워크북 생성 - const workbook = new ExcelJS.Workbook(); - workbook.creator = "EVCP System"; - workbook.created = new Date(); - - // 메인 워크시트 생성 - const worksheet = workbook.addWorksheet(sheetName); - - // 한글 헤더 매핑 - const columnHeaders = [ - { key: "category", header: "평가부문", width: 15 }, - { key: "category2", header: "점수구분", width: 15 }, - { key: "item", header: "항목", width: 15 }, - { key: "classification", header: "구분", width: 20 }, - { key: "range", header: "범위", width: 20 }, - { key: "detail", header: "평가내용", width: 30 }, - { key: "scoreEquipShip", header: "장비-조선 점수", width: 18 }, - { key: "scoreEquipMarine", header: "장비-해양 점수", width: 18 }, - { key: "scoreBulkShip", header: "벌크-조선 점수", width: 18 }, - { key: "scoreBulkMarine", header: "벌크-해양 점수", width: 18 }, - { key: "remarks", header: "비고", width: 20 }, - { key: "id", header: "ID", width: 10 }, - { key: "criteriaId", header: "기준 ID", width: 12 }, - { key: "orderIndex", header: "정렬 순서", width: 12 }, - ].filter(col => !excludeColumns.includes(col.key)); - - // 컬럼 설정 - worksheet.columns = columnHeaders; - - // 헤더 스타일 적용 - const headerRow = worksheet.getRow(1); - headerRow.font = { bold: true }; - headerRow.fill = { - type: "pattern", - pattern: "solid", - fgColor: { argb: "FFCCCCCC" }, - }; - headerRow.alignment = { horizontal: "center", vertical: "middle" }; - - // 데이터 행 추가 - const rowModel = table.getRowModel(); - rowModel.rows.forEach((row) => { - const rowData: Record<string, any> = {}; - columnHeaders.forEach((col) => { - let value = row.original[col.key as keyof RegEvalCriteriaView]; - - // 특정 컬럼들에 대해 한글 라벨로 변환 - if (col.key === "category") { - value = REG_EVAL_CRITERIA_CATEGORY.find(item => item.value === value)?.label || value; - } else if (col.key === "category2") { - value = REG_EVAL_CRITERIA_CATEGORY2.find(item => item.value === value)?.label || value; - } else if (col.key === "item") { - value = REG_EVAL_CRITERIA_ITEM.find(item => item.value === value)?.label || value; - } - - rowData[col.key] = value || ""; - }); - worksheet.addRow(rowData); - }); - - // 셀 스타일 적용 - worksheet.eachRow((row, rowNumber) => { - row.eachCell((cell) => { - cell.border = { - top: { style: "thin" }, - left: { style: "thin" }, - bottom: { style: "thin" }, - right: { style: "thin" }, - }; - - // 데이터 행 가운데 정렬 - if (rowNumber > 1) { - cell.alignment = { horizontal: "center", vertical: "middle" }; - } - }); - }); - - // 유효성 검사 시트 생성 (풀다운용) - const validationSheet = workbook.addWorksheet("ValidationData"); - validationSheet.state = "hidden"; - - // 풀다운 옵션 데이터 추가 - const categoryOptions = REG_EVAL_CRITERIA_CATEGORY.map(item => item.label); - const category2Options = REG_EVAL_CRITERIA_CATEGORY2.map(item => item.label); - const itemOptions = REG_EVAL_CRITERIA_ITEM.map(item => item.label); - - validationSheet.getColumn(1).values = ["평가부문", ...categoryOptions]; - validationSheet.getColumn(2).values = ["점수구분", ...category2Options]; - validationSheet.getColumn(3).values = ["항목", ...itemOptions]; - - // 메인 시트에 데이터 유효성 검사 적용 - const categoryColIndex = columnHeaders.findIndex(col => col.key === "category") + 1; - const category2ColIndex = columnHeaders.findIndex(col => col.key === "category2") + 1; - const itemColIndex = columnHeaders.findIndex(col => col.key === "item") + 1; - - if (categoryColIndex > 0) { - (worksheet as any).dataValidations.add(`${worksheet.getColumn(categoryColIndex).letter}2:${worksheet.getColumn(categoryColIndex).letter}1000`, { - type: "list", - allowBlank: false, - formulae: [`ValidationData!$A$2:$A$${categoryOptions.length + 1}`], - }); - } - - if (category2ColIndex > 0) { - (worksheet as any).dataValidations.add(`${worksheet.getColumn(category2ColIndex).letter}2:${worksheet.getColumn(category2ColIndex).letter}1000`, { - type: "list", - allowBlank: false, - formulae: [`ValidationData!$B$2:$B$${category2Options.length + 1}`], - }); - } - - if (itemColIndex > 0) { - (worksheet as any).dataValidations.add(`${worksheet.getColumn(itemColIndex).letter}2:${worksheet.getColumn(itemColIndex).letter}1000`, { - type: "list", - allowBlank: false, - formulae: [`ValidationData!$C$2:$C$${itemOptions.length + 1}`], - }); - } - - // 파일 다운로드 - const buffer = await workbook.xlsx.writeBuffer(); - const blob = new Blob([buffer], { - type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - }); - const url = URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.download = `${filename}.xlsx`; - link.click(); - URL.revokeObjectURL(url); -} - -/** - * 평가 기준 템플릿 다운로드 - */ -export async function exportRegEvalCriteriaTemplate(): Promise<void> { - // 워크북 생성 - const workbook = new ExcelJS.Workbook(); - workbook.creator = "EVCP System"; - workbook.created = new Date(); - - // 템플릿 워크시트 생성 - const worksheet = workbook.addWorksheet("평가 기준 템플릿"); - - // 한글 헤더 설정 - const templateHeaders = [ - { key: "category", header: "평가부문", width: 15 }, - { key: "category2", header: "점수구분", width: 15 }, - { key: "item", header: "항목", width: 15 }, - { key: "classification", header: "구분", width: 20 }, - { key: "range", header: "범위", width: 20 }, - { key: "detail", header: "평가내용", width: 30 }, - { key: "scoreEquipShip", header: "장비-조선 점수", width: 18 }, - { key: "scoreEquipMarine", header: "장비-해양 점수", width: 18 }, - { key: "scoreBulkShip", header: "벌크-조선 점수", width: 18 }, - { key: "scoreBulkMarine", header: "벌크-해양 점수", width: 18 }, - { key: "remarks", header: "비고", width: 20 }, - { key: "orderIndex", header: "정렬 순서", width: 12 }, - ]; - - // 컬럼 설정 - worksheet.columns = templateHeaders; - - // 헤더 스타일 적용 - const headerRow = worksheet.getRow(1); - headerRow.font = { bold: true }; - headerRow.fill = { - type: "pattern", - pattern: "solid", - fgColor: { argb: "FFCCCCCC" }, - }; - headerRow.alignment = { horizontal: "center", vertical: "middle" }; - - // 유효성 검사 시트 생성 - const validationSheet = workbook.addWorksheet("ValidationData"); - validationSheet.state = "hidden"; - - // 풀다운 옵션 데이터 추가 - const categoryOptions = REG_EVAL_CRITERIA_CATEGORY.map(item => item.label); - const category2Options = REG_EVAL_CRITERIA_CATEGORY2.map(item => item.label); - const itemOptions = REG_EVAL_CRITERIA_ITEM.map(item => item.label); - - validationSheet.getColumn(1).values = ["평가부문", ...categoryOptions]; - validationSheet.getColumn(2).values = ["점수구분", ...category2Options]; - validationSheet.getColumn(3).values = ["항목", ...itemOptions]; - - // 데이터 유효성 검사 적용 - (worksheet as any).dataValidations.add("A2:A1000", { - type: "list", - allowBlank: false, - formulae: [`ValidationData!$A$2:$A$${categoryOptions.length + 1}`], - }); - - (worksheet as any).dataValidations.add("B2:B1000", { - type: "list", - allowBlank: false, - formulae: [`ValidationData!$B$2:$B$${category2Options.length + 1}`], - }); - - (worksheet as any).dataValidations.add("C2:C1000", { - type: "list", - allowBlank: false, - formulae: [`ValidationData!$C$2:$C$${itemOptions.length + 1}`], - }); - - // 테두리 스타일 적용 - headerRow.eachCell((cell) => { - cell.border = { - top: { style: "thin" }, - left: { style: "thin" }, - bottom: { style: "thin" }, - right: { style: "thin" }, - }; - }); - - // 샘플 데이터 추가 (2-3줄) - worksheet.addRow([ - "품질", - "공정", - "품질", - "품질시스템", - "ISO 9001 인증", - "품질경영시스템 운영 현황", - "10", - "10", - "10", - "10", - "품질시스템 운영", - "1" - ]); - - worksheet.addRow([ - "관리자", - "가격", - "납기", - "납기준수", - "최근 3년", - "납기준수율 90% 이상", - "5", - "5", - "5", - "5", - "납기준수 실적", - "2" - ]); - - // 샘플 데이터 행에 테두리 적용 - worksheet.getRow(2).eachCell((cell) => { - cell.border = { - top: { style: "thin" }, - left: { style: "thin" }, - bottom: { style: "thin" }, - right: { style: "thin" }, - }; - cell.alignment = { horizontal: "center", vertical: "middle" }; - }); - - worksheet.getRow(3).eachCell((cell) => { - cell.border = { - top: { style: "thin" }, - left: { style: "thin" }, - bottom: { style: "thin" }, - right: { style: "thin" }, - }; - cell.alignment = { horizontal: "center", vertical: "middle" }; - }); - - // 파일 다운로드 - const buffer = await workbook.xlsx.writeBuffer(); - const blob = new Blob([buffer], { - type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - }); - const url = URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.download = "평가기준_템플릿.xlsx"; - link.click(); - URL.revokeObjectURL(url); -}
\ No newline at end of file diff --git a/lib/evaluation-criteria/excel/reg-eval-criteria-excel-import.ts b/lib/evaluation-criteria/excel/reg-eval-criteria-excel-import.ts deleted file mode 100644 index 92c7a25e..00000000 --- a/lib/evaluation-criteria/excel/reg-eval-criteria-excel-import.ts +++ /dev/null @@ -1,417 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -'use server'; - -/* IMPORT */ -import * as ExcelJS from 'exceljs'; -import { filterColumns } from '@/lib/filter-columns'; -import { - regEvalCriteriaColumnsConfig, -} from '@/config/regEvalCriteriaColumnsConfig'; -import { - REG_EVAL_CRITERIA_CATEGORY2_ENUM, - REG_EVAL_CRITERIA_CATEGORY_ENUM, - REG_EVAL_CRITERIA_ITEM_ENUM, - REG_EVAL_CRITERIA_CATEGORY, - REG_EVAL_CRITERIA_CATEGORY2, - REG_EVAL_CRITERIA_ITEM, - type NewRegEvalCriteria, - type NewRegEvalCriteriaDetails, - type RegEvalCriteria, - type RegEvalCriteriaDetails, - type RegEvalCriteriaView, -} from '@/db/schema'; -import { - createRegEvalCriteriaWithDetails, - modifyRegEvalCriteriaWithDetails, - removeRegEvalCriteria, - getRegEvalCriteria, -} from '../service'; -import db from '@/db/db'; -import { selectRegEvalCriteria } from '../repository'; - -// ---------------------------------------------------------------------------------------------------- - -/* TYPES */ -interface ImportResult { - errorFile: File | null, - errorMessage: string | null, - successMessage?: string, -} -type ExcelRowData = { - criteriaData: NewRegEvalCriteria, - detailList: (NewRegEvalCriteriaDetails & { rowIndex: number, toDelete: boolean })[], -} - -// ---------------------------------------------------------------------------------------------------- - -/* CONSTANTS */ -const HEADER_ROW_INDEX = 1; -const DATA_START_ROW_INDEX = 2; - -// 영문과 한글 헤더 매핑 -const HEADER_MAPPING: Record<string, string> = { - // 영문 헤더 - 'Category': 'category', - 'Score Category': 'category2', - 'Item': 'item', - 'Classification': 'classification', - 'Range': 'range', - 'Detail': 'detail', - 'Remarks': 'remarks', - 'ID': 'id', - 'Criteria ID': 'criteriaId', - 'Order Index': 'orderIndex', - 'Equipment-Shipbuilding Score': 'scoreEquipShip', - 'Equipment-Marine Engineering Score': 'scoreEquipMarine', - 'Bulk-Shipbuilding Score': 'scoreBulkShip', - 'Bulk-Marine Engineering Score': 'scoreBulkMarine', - - // 한글 헤더 - '평가부문': 'category', - '점수구분': 'category2', - '항목': 'item', - '구분': 'classification', - '범위': 'range', - '평가내용': 'detail', - '비고': 'remarks', - '기준 ID': 'criteriaId', - '정렬 순서': 'orderIndex', - '장비-조선 점수': 'scoreEquipShip', - '장비-해양 점수': 'scoreEquipMarine', - '벌크-조선 점수': 'scoreBulkShip', - '벌크-해양 점수': 'scoreBulkMarine', -}; - -// ---------------------------------------------------------------------------------------------------- - -/* FUNCTION FOR IMPORTING EXCEL FILES */ -export async function importRegEvalCriteriaExcel(file: File): Promise<ImportResult> { - try { - const buffer = await file.arrayBuffer(); - const workbook = new ExcelJS.Workbook(); - try { - await workbook.xlsx.load(buffer); - } catch { - throw new Error('유효한 Excel 파일이 아닙니다. 파일을 다시 확인해주세요.'); - } - - const worksheet = workbook.worksheets[0]; - if (!worksheet) { - throw new Error('Excel 파일에 워크시트가 없습니다.'); - }; - if (worksheet.rowCount === 0) { - throw new Error('워크시트에 데이터가 없습니다.'); - } - - const headerRow = worksheet.getRow(HEADER_ROW_INDEX); - if (!headerRow || !Array.isArray(headerRow.values)) { - throw new Error('Excel 파일의 워크시트에서 유효한 헤더 행을 찾지 못했습니다.'); - } - - // 헤더 매핑 생성 - const columnToFieldMap = new Map<number, string>(); - headerRow.eachCell((cell, colIndex) => { - if (typeof cell.value === 'string') { - const headerValue = cell.value.trim(); - const fieldName = HEADER_MAPPING[headerValue]; - if (fieldName) { - columnToFieldMap.set(colIndex, fieldName); - } - } - }); - - // 필수 헤더 확인 - const requiredFields = ['category', 'category2', 'item', 'classification', 'detail']; - const foundFields = new Set(columnToFieldMap.values()); - const missingFields = requiredFields.filter(field => !foundFields.has(field)); - - if (missingFields.length > 0) { - throw new Error(`필수 헤더가 누락되었습니다: ${missingFields.join(', ')}`); - } - const errorRows: { rowIndex: number; message: string }[] = []; - const rowDataList: ExcelRowData[] = []; - const criteriaMap = new Map<string, { - criteria: NewRegEvalCriteria, - criteriaDetails: (NewRegEvalCriteriaDetails & { rowIndex: number, toDelete: boolean })[], - }>(); - - for (let r = DATA_START_ROW_INDEX; r <= worksheet.rowCount; r += 1) { - const row = worksheet.getRow(r); - if (!row) { - continue; - } - - const lastCellValue = row.getCell(row.cellCount).value; - const isDelete = typeof lastCellValue === 'string' && lastCellValue.toLowerCase() === 'd'; - - const rowFields = {} as Record<string, any>; - columnToFieldMap.forEach((fieldName, colIdx) => { - let cellValue = row.getCell(colIdx).value; - - // 한글 라벨을 영문 값으로 변환 - if (fieldName === 'category' && typeof cellValue === 'string') { - const found = REG_EVAL_CRITERIA_CATEGORY.find(item => item.label === cellValue?.toString().trim()); - cellValue = found ? found.value : cellValue; - } else if (fieldName === 'category2' && typeof cellValue === 'string') { - const found = REG_EVAL_CRITERIA_CATEGORY2.find(item => item.label === cellValue?.toString().trim()); - cellValue = found ? found.value : cellValue; - } else if (fieldName === 'item' && typeof cellValue === 'string') { - const found = REG_EVAL_CRITERIA_ITEM.find(item => item.label === cellValue?.toString().trim()); - cellValue = found ? found.value : cellValue; - } - - rowFields[fieldName] = cellValue ?? null; - }); - - const requiredFields = ['category', 'category2', 'item', 'classification', 'detail']; - for (const field of requiredFields) { - if (!rowFields[field]) { - errorRows.push({ rowIndex: r, message: `필수 필드 누락: ${field}` }); - } - } - - if (!REG_EVAL_CRITERIA_CATEGORY_ENUM.includes(rowFields.category)) { - errorRows.push({ rowIndex: r, message: `유효하지 않은 Category 값: ${rowFields.category}` }); - } - - if (!REG_EVAL_CRITERIA_CATEGORY2_ENUM.includes(rowFields.category2)) { - errorRows.push({ rowIndex: r, message: `유효하지 않은 Score Category 값: ${rowFields.category2}` }); - } - - if (!REG_EVAL_CRITERIA_ITEM_ENUM.includes(rowFields.item)) { - errorRows.push({ rowIndex: r, message: `유효하지 않은 Item 값: ${rowFields.item}` }); - } - - const criteriaKey = [ - rowFields.criteriaId ?? '', - rowFields.category, - rowFields.category2, - rowFields.item, - rowFields.classification, - rowFields.range ?? '', - ].join('|'); - - const criteriaDetail: NewRegEvalCriteriaDetails = { - id: rowFields.id, - criteriaId: rowFields.criteriaId, - detail: rowFields.detail, - orderIndex: rowFields.orderIndex, - scoreEquipShip: rowFields.scoreEquipShip, - scoreEquipMarine: rowFields.scoreEquipMarine, - scoreBulkShip: rowFields.scoreBulkShip, - scoreBulkMarine: rowFields.scoreBulkMarine, - }; - - if (!criteriaMap.has(criteriaKey)) { - const criteria: NewRegEvalCriteria = { - id: rowFields.criteriaId, - category: rowFields.category, - category2: rowFields.category2, - item: rowFields.item, - classification: rowFields.classification, - range: rowFields.range, - remarks: rowFields.remarks, - }; - - criteriaMap.set(criteriaKey, { - criteria, - criteriaDetails: [{ - ...criteriaDetail, - rowIndex: r, - toDelete: isDelete, - }], - }); - } else { - const existing = criteriaMap.get(criteriaKey)!; - existing.criteriaDetails.push({ - ...criteriaDetail, - rowIndex: r, - toDelete: isDelete, - }); - } - } - - criteriaMap.forEach(({ criteria, criteriaDetails }) => { - rowDataList.push({ - criteriaData: criteria, - detailList: criteriaDetails, - }); - }); - - if (errorRows.length > 0) { - const workbook = new ExcelJS.Workbook(); - const sheet = workbook.addWorksheet('Error List'); - sheet.columns = [ - { header: 'Row Index', key: 'rowIndex', width: 10 }, - { header: 'Error Message', key: 'message', width: 50 }, - ]; - errorRows.forEach((errorRow) => { - sheet.addRow({ - rowIndex: errorRow.rowIndex, - message: errorRow.message, - }); - }); - const buffer = await workbook.xlsx.writeBuffer(); - const errorFile = new File( - [buffer], - 'error_rows.xlsx', - { - type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - lastModified: Date.now(), - } - ); - - return { - errorFile, - errorMessage: '입력된 데이터 중에서 잘못된 데이터가 있어 오류 파일을 생성했습니다.', - }; - } - - const existingData = await db.transaction(async (tx) => { - return await selectRegEvalCriteria(tx, { limit: Number.MAX_SAFE_INTEGER }); - }); - const existingIds = existingData.map((row) => row.criteriaId!) - const existingIdSet = new Set<number>(existingIds); - - const createList: { - criteriaData: NewRegEvalCriteria, - detailList: Omit<NewRegEvalCriteriaDetails, 'criteriaId'>[], - }[] = []; - const updateList: { - id: number, - criteriaData: Partial<RegEvalCriteria>, - detailList: Partial<RegEvalCriteriaDetails>[], - }[] = []; - const deleteIdList: number[] = []; - - for (const { criteriaData, detailList } of rowDataList) { - const { id: criteriaId } = criteriaData; - const allMarkedForDelete = detailList.every(d => d.toDelete); - if (allMarkedForDelete) { - if (criteriaId && existingIdSet.has(criteriaId)) { - deleteIdList.push(criteriaId); - } - continue; - } - - if (!criteriaId) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { id, ...newCriteriaData } = criteriaData; - const newDetailList = detailList.map(d => { - if (d.id != null) { - throw new Error(`새로운 기준 항목에 ID가 존재합니다: ${d.rowIndex}행`); - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { rowIndex, toDelete, id, criteriaId, ...rest } = d; - return rest; - }); - - createList.push({ - criteriaData: newCriteriaData, - detailList: newDetailList, - }); - } else if (existingIdSet.has(criteriaId)) { - const matchedExistingDetails = existingData.filter(d => d.criteriaId === criteriaId); - const hasDeletedDetail = detailList.some(d => d.toDelete === true); - const hasNewDetail = detailList.some(d => d.id == null); - const matchedExistingCriteria = matchedExistingDetails[0]; - const criteriaChanged = ( - matchedExistingCriteria.category !== criteriaData.category || - matchedExistingCriteria.category2 !== criteriaData.category2 || - matchedExistingCriteria.item !== criteriaData.item || - matchedExistingCriteria.classification !== criteriaData.classification || - matchedExistingCriteria.range !== criteriaData.range || - matchedExistingCriteria.remarks !== criteriaData.remarks - ); - const detailChanged = detailList.some(d => { - if (!d.id) { - return false; - } - const matched = matchedExistingDetails.find(e => e.id === d.id); - if (!matched) { - throw Error(`존재하지 않는 잘못된 ID(${d.id})가 있습니다.`); - } - return ( - matched.detail !== d.detail || - matched.orderIndex !== d.orderIndex || - matched.scoreEquipShip !== d.scoreEquipShip || - matched.scoreEquipMarine !== d.scoreEquipMarine || - matched.scoreBulkShip !== d.scoreBulkShip || - matched.scoreBulkMarine !== d.scoreBulkMarine - ); - }); - - if (hasDeletedDetail || hasNewDetail || criteriaChanged || detailChanged) { - const updatedDetails = detailList - .filter(d => !d.toDelete) - .map(d => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { rowIndex, toDelete, ...rest } = d; - const cleaned = Object.fromEntries( - Object.entries(rest).map(([key, value]) => [ - key, - value === '' ? null : value, - ]) - ); - return cleaned; - }); - - updateList.push({ - id: criteriaId, - criteriaData, - detailList: updatedDetails, - }); - } - } else { - throw Error(`존재하지 않는 잘못된 Criteria ID(${criteriaId})가 있습니다.`); - } - } - - if (createList.length > 0) { - for (const { criteriaData, detailList } of createList) { - await createRegEvalCriteriaWithDetails(criteriaData, detailList); - } - } - if (updateList.length > 0) { - for (const { id, criteriaData, detailList } of updateList) { - await modifyRegEvalCriteriaWithDetails(id, criteriaData, detailList); - } - } - if (deleteIdList.length > 0) { - for (const id of deleteIdList) { - await removeRegEvalCriteria(id); - } - } - - const msg: string[] = []; - if (createList.length > 0) { - msg.push(`${createList.length}건 생성`); - } - if (updateList.length > 0) { - msg.push(`${updateList.length}건 수정`); - } - if (deleteIdList.length > 0) { - msg.push(`${deleteIdList.length}건 삭제`); - } - const successMessage = msg.length > 0 - ? '기준 항목이 정상적으로 ' + msg.join(', ') + '되었습니다.' - : '변경사항이 존재하지 않습니다.'; - - return { - errorFile: null, - errorMessage: null, - successMessage, - }; - } catch (error) { - let message = 'Excel 파일을 읽는 중 오류가 발생했습니다.'; - if (error instanceof Error) { - message = error.message; - } - - return { - errorFile: null, - errorMessage: message, - }; - } -}
\ No newline at end of file diff --git a/lib/evaluation-criteria/service.ts b/lib/evaluation-criteria/service.ts index e204579f..19f2dd81 100644 --- a/lib/evaluation-criteria/service.ts +++ b/lib/evaluation-criteria/service.ts @@ -11,12 +11,10 @@ import { or,count, eq
} from 'drizzle-orm';
import {
- countRegEvalCriteria,
deleteRegEvalCriteria,
deleteRegEvalCriteriaDetails,
insertRegEvalCriteria,
insertRegEvalCriteriaDetails,
- selectRegEvalCriteria,
selectRegEvalCriteriaWithDetails,
updateRegEvalCriteria,
updateRegEvalCriteriaDetails,
@@ -24,17 +22,20 @@ import { import db from '@/db/db';
import { filterColumns } from '@/lib/filter-columns';
import {
- REG_EVAL_CRITERIA_CATEGORY2_ENUM,
- REG_EVAL_CRITERIA_CATEGORY_ENUM,
- REG_EVAL_CRITERIA_ITEM_ENUM,
- regEvalCriteria, regEvalCriteriaDetails, // regEvalCriteriaView 대신 regEvalCriteria 사용
+ regEvalCriteria, regEvalCriteriaDetails,
type NewRegEvalCriteria,
type NewRegEvalCriteriaDetails,
type RegEvalCriteria,
type RegEvalCriteriaDetails,
- type RegEvalCriteriaView,
} from '@/db/schema';
import { type GetRegEvalCriteriaSchema } from './validations';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/app/api/auth/[...nextauth]/route';
+
+/* EXCEL EXPORT SERVER ACTIONS */
+import ExcelJS from "exceljs";
+import { REG_EVAL_CRITERIA_CATEGORY, REG_EVAL_CRITERIA_CATEGORY2, REG_EVAL_CRITERIA_ITEM } from "@/db/schema";
+
// ----------------------------------------------------------------------------------------------------
@@ -42,6 +43,19 @@ import { type GetRegEvalCriteriaSchema } from './validations'; // ----------------------------------------------------------------------------------------------------
+/* HELPER FUNCTION FOR GETTING CURRENT USER ID */
+async function getCurrentUserId(): Promise<number> {
+ try {
+ const session = await getServerSession(authOptions);
+ return session?.user?.id ? Number(session.user.id) : 3; // 기본값 3, 실제 환경에서는 적절한 기본값 설정
+ } catch (error) {
+ console.error('Error getting current user ID:', error);
+ return 3; // 기본값 3
+ }
+}
+
+// ----------------------------------------------------------------------------------------------------
+
/* FUNCTION FOR GETTING CRITERIA - 메인 기준 목록만 가져오기 */
async function getRegEvalCriteria(input: GetRegEvalCriteriaSchema) {
try {
@@ -116,8 +130,6 @@ async function getRegEvalCriteriaWithDetails(id: number) { /* FUNCTION FOR GETTING CRITERIA DETAILS ONLY - 특정 기준의 상세 항목들만 가져오기 */
async function getRegEvalCriteriaDetails(criteriaId: number) {
try {
-
- console.log(criteriaId,"criteriaId")
return await db.transaction(async (tx) => {
const details = await tx
.select()
@@ -135,14 +147,16 @@ async function getRegEvalCriteriaDetails(criteriaId: number) { // ----------------------------------------------------------------------------------------------------
-/* FUNCTION FOR CREATING CRITERIA WITH DETAILS */
-async function createRegEvalCriteriaWithDetails(
+/* FUNCTION FOR CREATING CRITERIA WITH FIXED SCORES */
+async function createRegEvalCriteriaFixed(
criteriaData: NewRegEvalCriteria,
detailList: Omit<NewRegEvalCriteriaDetails, 'criteriaId'>[],
) {
try {
return await db.transaction(async (tx) => {
- const criteria = await insertRegEvalCriteria(tx, criteriaData);
+ const criteria = await insertRegEvalCriteria(tx, {
+ ...criteriaData,
+ });
const criteriaId = criteria.id;
const newDetailList = detailList.map((detailItem, index) => ({
...detailItem,
@@ -158,25 +172,50 @@ async function createRegEvalCriteriaWithDetails( return { ...criteria, criteriaDetails };
});
} catch (error) {
- console.error('Error in Creating New Regular Evaluation Criteria with Details: ', error);
- throw new Error('Failed to Create New Regular Evaluation Criteria with Details');
+ console.error('Error in Creating New Regular Evaluation Criteria (Fixed): ', error);
+ throw new Error('Failed to Create New Regular Evaluation Criteria (Fixed)');
+ }
+}
+
+/* FUNCTION FOR CREATING CRITERIA WITH VARIABLE SCORES */
+async function createRegEvalCriteriaVariable(
+ criteriaData: NewRegEvalCriteria,
+) {
+ try {
+ return await db.transaction(async (tx) => {
+ const criteria = await insertRegEvalCriteria(tx, {
+ ...criteriaData,
+ });
+
+ return { ...criteria, criteriaDetails: [] };
+ });
+ } catch (error) {
+ console.error('Error in Creating New Regular Evaluation Criteria (Variable): ', error);
+ throw new Error('Failed to Create New Regular Evaluation Criteria (Variable)');
}
}
// ----------------------------------------------------------------------------------------------------
-/* FUNCTION FOR MODIFYING CRITERIA WITH DETAILS */
-async function modifyRegEvalCriteriaWithDetails(
+/* FUNCTION FOR MODIFYING CRITERIA WITH FIXED SCORES */
+async function modifyRegEvalCriteriaFixed(
id: number,
criteriaData: Partial<RegEvalCriteria>,
detailList: Partial<RegEvalCriteriaDetails>[],
) {
try {
+ const currentUserId = await getCurrentUserId();
+
return await db.transaction(async (tx) => {
+ // 고정 점수용 criteria 업데이트 (variableScore 필드는 null로 설정)
+ const modifiedCriteria = await updateRegEvalCriteria(tx, id, {
+ ...criteriaData,
+ variableScoreMin: null,
+ variableScoreMax: null,
+ variableScoreUnit: null,
+ updatedBy: currentUserId,
+ });
- console.log(id, criteriaData)
-
- const modifiedCriteria = await updateRegEvalCriteria(tx, id, criteriaData);
const originCriteria = await getRegEvalCriteriaWithDetails(id);
const originCriteriaDetails = originCriteria?.criteriaDetails || [];
const detailIdList = detailList
@@ -186,10 +225,12 @@ async function modifyRegEvalCriteriaWithDetails( (item) => !detailIdList.includes(item.id),
);
+ // 기존 세부사항 삭제
for (const item of toDeleteIdList) {
await deleteRegEvalCriteriaDetails(tx, item.id);
}
+ // 세부사항 업데이트/생성
const criteriaDetails = [];
for (let idx = 0; idx < detailList.length; idx += 1) {
const detailItem = detailList[idx];
@@ -214,8 +255,39 @@ async function modifyRegEvalCriteriaWithDetails( return { ...modifiedCriteria, criteriaDetails };
});
} catch (error) {
- console.error('Error in Modifying Regular Evaluation Criteria with Details: ', error);
- throw new Error('Failed to Modify Regular Evaluation Criteria with Details');
+ console.error('Error in Modifying Regular Evaluation Criteria with Fixed Scores: ', error);
+ throw new Error('Failed to Modify Regular Evaluation Criteria with Fixed Scores');
+ }
+}
+
+/* FUNCTION FOR MODIFYING CRITERIA WITH VARIABLE SCORES */
+async function modifyRegEvalCriteriaVariable(
+ id: number,
+ criteriaData: Partial<RegEvalCriteria>,
+) {
+ try {
+ const currentUserId = await getCurrentUserId();
+
+ return await db.transaction(async (tx) => {
+ // 변동 점수용 criteria 업데이트 (기존 details 모두 삭제)
+ const modifiedCriteria = await updateRegEvalCriteria(tx, id, {
+ ...criteriaData,
+ updatedBy: currentUserId,
+ });
+
+ // 변동 점수에서는 기존 모든 세부사항 삭제
+ const originCriteria = await getRegEvalCriteriaWithDetails(id);
+ const originCriteriaDetails = originCriteria?.criteriaDetails || [];
+
+ for (const item of originCriteriaDetails) {
+ await deleteRegEvalCriteriaDetails(tx, item.id);
+ }
+
+ return { ...modifiedCriteria, criteriaDetails: [] };
+ });
+ } catch (error) {
+ console.error('Error in Modifying Regular Evaluation Criteria with Variable Scores: ', error);
+ throw new Error('Failed to Modify Regular Evaluation Criteria with Variable Scores');
}
}
@@ -245,15 +317,579 @@ async function removeRegEvalCriteriaDetails(id: number) { }
}
+/* FUNCTION FOR GENERATING EXCEL FILE FROM DATABASE */
+async function generateRegEvalCriteriaExcel(): Promise<ArrayBuffer> {
+ try {
+ const workbook = new ExcelJS.Workbook();
+ workbook.creator = "EVCP System";
+ workbook.created = new Date();
+
+ const worksheet = workbook.addWorksheet("평가 기준");
+
+ const columnHeaders = [
+ { key: "category", header: "평가부문", width: 15 },
+ { key: "category2", header: "점수구분", width: 15 },
+ { key: "item", header: "항목", width: 15 },
+ { key: "classification", header: "구분", width: 20 },
+ { key: "range", header: "범위", width: 20 },
+ { key: "remarks", header: "비고", width: 20 },
+ { key: "scoreType", header: "점수 유형", width: 15 },
+ { key: "variableScoreMin", header: "최소 점수", width: 15 },
+ { key: "variableScoreMax", header: "최대 점수", width: 15 },
+ { key: "variableScoreUnit", header: "단위", width: 15 },
+ { key: "detail", header: "평가내용", width: 30 },
+ { key: "scoreEquipShip", header: "기자재-조선", width: 18 },
+ { key: "scoreEquipMarine", header: "기자재-해양", width: 18 },
+ { key: "scoreBulkShip", header: "벌크-조선", width: 18 },
+ { key: "scoreBulkMarine", header: "벌크-해양", width: 18 },
+ { key: "orderIndex", header: "정렬 순서", width: 12 },
+ ];
+
+ worksheet.columns = columnHeaders;
+
+ const headerRow = worksheet.getRow(1);
+ headerRow.font = { bold: true };
+ headerRow.fill = {
+ type: "pattern",
+ pattern: "solid",
+ fgColor: { argb: "FFCCCCCC" },
+ };
+ headerRow.alignment = { horizontal: "center", vertical: "middle" };
+
+ // 실제 DB에서 1:n 관계 데이터 가져오기
+ const allCriteria = await db.transaction(async (tx) => {
+ const criteria = await tx
+ .select()
+ .from(regEvalCriteria)
+ .orderBy(asc(regEvalCriteria.id));
+
+ const result: any[] = [];
+ for (const criterion of criteria) {
+ const details = await tx
+ .select()
+ .from(regEvalCriteriaDetails)
+ .where(eq(regEvalCriteriaDetails.criteriaId, criterion.id))
+ .orderBy(asc(regEvalCriteriaDetails.orderIndex));
+
+ if (details.length === 0) {
+ result.push({
+ category: criterion.category,
+ category2: criterion.category2,
+ item: criterion.item,
+ classification: criterion.classification,
+ range: criterion.range || '',
+ remarks: criterion.remarks || '',
+ scoreType: criterion.scoreType === 'fixed' ? '고정' : '변동',
+ variableScoreMin: criterion.variableScoreMin || '',
+ variableScoreMax: criterion.variableScoreMax || '',
+ variableScoreUnit: criterion.variableScoreUnit || '',
+ detail: '',
+ scoreEquipShip: '',
+ scoreEquipMarine: '',
+ scoreBulkShip: '',
+ scoreBulkMarine: '',
+ orderIndex: '',
+ });
+ } else {
+ for (const detail of details) {
+ result.push({
+ category: criterion.category,
+ category2: criterion.category2,
+ item: criterion.item,
+ classification: criterion.classification,
+ range: criterion.range || '',
+ remarks: criterion.remarks || '',
+ scoreType: criterion.scoreType === 'fixed' ? '고정' : '변동',
+ variableScoreMin: criterion.variableScoreMin || '',
+ variableScoreMax: criterion.variableScoreMax || '',
+ variableScoreUnit: criterion.variableScoreUnit || '',
+ detail: detail.detail,
+ scoreEquipShip: detail.scoreEquipShip || '',
+ scoreEquipMarine: detail.scoreEquipMarine || '',
+ scoreBulkShip: detail.scoreBulkShip || '',
+ scoreBulkMarine: detail.scoreBulkMarine || '',
+ orderIndex: detail.orderIndex?.toString() || '',
+ });
+ }
+ }
+ }
+ return result;
+ });
+
+ allCriteria.forEach((row) => {
+ const rowData: Record<string, unknown> = {};
+ columnHeaders.forEach((col) => {
+ let value = row[col.key as keyof typeof row];
+
+ if (col.key === "category") {
+ value = REG_EVAL_CRITERIA_CATEGORY.find(item => item.value === value)?.label || value;
+ } else if (col.key === "category2") {
+ value = REG_EVAL_CRITERIA_CATEGORY2.find(item => item.value === value)?.label || value;
+ } else if (col.key === "item") {
+ value = REG_EVAL_CRITERIA_ITEM.find(item => item.value === value)?.label || value;
+ }
+
+ rowData[col.key] = value || "";
+ });
+ worksheet.addRow(rowData);
+ });
+
+ // 데이터 검증을 위한 숨겨진 시트 생성
+ const validationSheet = workbook.addWorksheet("ValidationData");
+ validationSheet.state = "hidden";
+
+ const categoryOptions = REG_EVAL_CRITERIA_CATEGORY.map(item => item.label);
+ const category2Options = REG_EVAL_CRITERIA_CATEGORY2.map(item => item.label);
+ const itemOptions = REG_EVAL_CRITERIA_ITEM.map(item => item.label);
+ const scoreTypeOptions = ['고정', '변동'];
+
+ validationSheet.getColumn(1).values = ["평가부문", ...categoryOptions];
+ validationSheet.getColumn(2).values = ["점수구분", ...category2Options];
+ validationSheet.getColumn(3).values = ["항목", ...itemOptions];
+ validationSheet.getColumn(4).values = ["점수 유형", ...scoreTypeOptions];
+
+ // 드롭다운 설정
+ const categoryColIndex = columnHeaders.findIndex(col => col.key === "category") + 1;
+ const category2ColIndex = columnHeaders.findIndex(col => col.key === "category2") + 1;
+ const itemColIndex = columnHeaders.findIndex(col => col.key === "item") + 1;
+ const scoreTypeColIndex = columnHeaders.findIndex(col => col.key === "scoreType") + 1;
+
+ const dataRowCount = allCriteria.length + 1; // 헤더 포함
+ const maxRows = Math.max(dataRowCount + 50, 1000); // 추가 행을 위한 여유
+
+ if (categoryColIndex > 0) {
+ (worksheet as any).dataValidations.add(`${worksheet.getColumn(categoryColIndex).letter}2:${worksheet.getColumn(categoryColIndex).letter}${maxRows}`, {
+ type: "list",
+ allowBlank: false,
+ formulae: [`ValidationData!$A$2:$A$${categoryOptions.length + 1}`],
+ });
+ }
+
+ if (category2ColIndex > 0) {
+ (worksheet as any).dataValidations.add(`${worksheet.getColumn(category2ColIndex).letter}2:${worksheet.getColumn(category2ColIndex).letter}${maxRows}`, {
+ type: "list",
+ allowBlank: false,
+ formulae: [`ValidationData!$B$2:$B$${category2Options.length + 1}`],
+ });
+ }
+
+ if (itemColIndex > 0) {
+ (worksheet as any).dataValidations.add(`${worksheet.getColumn(itemColIndex).letter}2:${worksheet.getColumn(itemColIndex).letter}${maxRows}`, {
+ type: "list",
+ allowBlank: false,
+ formulae: [`ValidationData!$C$2:$C$${itemOptions.length + 1}`],
+ });
+ }
+
+ if (scoreTypeColIndex > 0) {
+ (worksheet as any).dataValidations.add(`${worksheet.getColumn(scoreTypeColIndex).letter}2:${worksheet.getColumn(scoreTypeColIndex).letter}${maxRows}`, {
+ type: "list",
+ allowBlank: false,
+ formulae: [`ValidationData!$D$2:$D$${scoreTypeOptions.length + 1}`],
+ });
+ }
+
+ worksheet.eachRow((row) => {
+ row.eachCell((cell) => {
+ cell.border = {
+ top: { style: "thin" },
+ left: { style: "thin" },
+ bottom: { style: "thin" },
+ right: { style: "thin" },
+ };
+ });
+ });
+
+ return await workbook.xlsx.writeBuffer() as ArrayBuffer;
+ } catch (error) {
+ console.error('Error in generating Excel file:', error);
+ throw new Error('Failed to generate Excel file');
+ }
+}
+
+/* FUNCTION FOR GENERATING EXCEL TEMPLATE */
+async function generateRegEvalCriteriaTemplate(): Promise<ArrayBuffer> {
+ try {
+ const workbook = new ExcelJS.Workbook();
+ workbook.creator = "EVCP System";
+ workbook.created = new Date();
+
+ const worksheet = workbook.addWorksheet("평가 기준 템플릿");
+
+ const templateHeaders = [
+ { key: "category", header: "평가부문", width: 15 },
+ { key: "category2", header: "점수구분", width: 15 },
+ { key: "item", header: "항목", width: 15 },
+ { key: "classification", header: "구분", width: 20 },
+ { key: "range", header: "범위", width: 20 },
+ { key: "remarks", header: "비고", width: 20 },
+ { key: "scoreType", header: "점수 유형", width: 15 },
+ { key: "variableScoreMin", header: "최소 점수", width: 15 },
+ { key: "variableScoreMax", header: "최대 점수", width: 15 },
+ { key: "variableScoreUnit", header: "단위", width: 15 },
+ { key: "detail", header: "평가내용", width: 30 },
+ { key: "scoreEquipShip", header: "기자재-조선", width: 18 },
+ { key: "scoreEquipMarine", header: "기자재-해양", width: 18 },
+ { key: "scoreBulkShip", header: "벌크-조선", width: 18 },
+ { key: "scoreBulkMarine", header: "벌크-해양", width: 18 },
+ { key: "orderIndex", header: "정렬 순서", width: 12 },
+ ];
+
+ worksheet.columns = templateHeaders;
+
+ const headerRow = worksheet.getRow(1);
+ headerRow.font = { bold: true };
+ headerRow.fill = {
+ type: "pattern",
+ pattern: "solid",
+ fgColor: { argb: "FFCCCCCC" },
+ };
+ headerRow.alignment = { horizontal: "center", vertical: "middle" };
+
+ const exampleData = [
+ {
+ category: "품질",
+ category2: "공정",
+ item: "품질",
+ classification: "품질관리시스템",
+ range: "ISO 9001 인증",
+ remarks: "국제 표준 품질관리시스템 인증",
+ scoreType: "고정",
+ variableScoreMin: "",
+ variableScoreMax: "",
+ variableScoreUnit: "",
+ detail: "우수 (ISO 9001 인증보유)",
+ scoreEquipShip: "10",
+ scoreEquipMarine: "10",
+ scoreBulkShip: "8",
+ scoreBulkMarine: "8",
+ orderIndex: "1",
+ },
+ {
+ category: "품질",
+ category2: "공정",
+ item: "품질",
+ classification: "품질관리시스템",
+ range: "ISO 9001 인증",
+ remarks: "국제 표준 품질관리시스템 인증",
+ scoreType: "고정",
+ variableScoreMin: "",
+ variableScoreMax: "",
+ variableScoreUnit: "",
+ detail: "보통 (자체 품질관리시스템)",
+ scoreEquipShip: "6",
+ scoreEquipMarine: "6",
+ scoreBulkShip: "5",
+ scoreBulkMarine: "5",
+ orderIndex: "2",
+ },
+ {
+ category: "품질",
+ category2: "공정",
+ item: "품질",
+ classification: "품질관리시스템",
+ range: "ISO 9001 인증",
+ remarks: "국제 표준 품질관리시스템 인증",
+ scoreType: "고정",
+ variableScoreMin: "",
+ variableScoreMax: "",
+ variableScoreUnit: "",
+ detail: "미흡 (품질관리시스템 미비)",
+ scoreEquipShip: "2",
+ scoreEquipMarine: "2",
+ scoreBulkShip: "1",
+ scoreBulkMarine: "1",
+ orderIndex: "3",
+ },
+ {
+ category: "조달",
+ category2: "가격",
+ item: "납기",
+ classification: "납기준수율",
+ range: "최근 3년간 납기준수율",
+ remarks: "계약대비 실제 납기준수 비율",
+ scoreType: "변동",
+ variableScoreMin: "0",
+ variableScoreMax: "20",
+ variableScoreUnit: "점",
+ detail: "",
+ scoreEquipShip: "",
+ scoreEquipMarine: "",
+ scoreBulkShip: "",
+ scoreBulkMarine: "",
+ orderIndex: "",
+ },
+ ];
+
+ exampleData.forEach((row) => {
+ worksheet.addRow(row);
+ });
+
+
+ const validationSheet = workbook.addWorksheet("ValidationData");
+ validationSheet.state = "hidden";
+
+ const categoryOptions = REG_EVAL_CRITERIA_CATEGORY.map(item => item.label);
+ const category2Options = REG_EVAL_CRITERIA_CATEGORY2.map(item => item.label);
+ const itemOptions = REG_EVAL_CRITERIA_ITEM.map(item => item.label);
+ const scoreTypeOptions = ['고정', '변동'];
+
+ validationSheet.getColumn(1).values = ["평가부문", ...categoryOptions];
+ validationSheet.getColumn(2).values = ["점수구분", ...category2Options];
+ validationSheet.getColumn(3).values = ["항목", ...itemOptions];
+ validationSheet.getColumn(4).values = ["점수 유형", ...scoreTypeOptions];
+
+ const categoryColIndex = templateHeaders.findIndex(col => col.key === "category") + 1;
+ const category2ColIndex = templateHeaders.findIndex(col => col.key === "category2") + 1;
+ const itemColIndex = templateHeaders.findIndex(col => col.key === "item") + 1;
+ const scoreTypeColIndex = templateHeaders.findIndex(col => col.key === "scoreType") + 1;
+
+ if (categoryColIndex > 0) {
+ (worksheet as any).dataValidations.add(`${worksheet.getColumn(categoryColIndex).letter}2:${worksheet.getColumn(categoryColIndex).letter}1000`, {
+ type: "list",
+ allowBlank: false,
+ formulae: [`ValidationData!$A$2:$A$${categoryOptions.length + 1}`],
+ });
+ }
+
+ if (category2ColIndex > 0) {
+ (worksheet as any).dataValidations.add(`${worksheet.getColumn(category2ColIndex).letter}2:${worksheet.getColumn(category2ColIndex).letter}1000`, {
+ type: "list",
+ allowBlank: false,
+ formulae: [`ValidationData!$B$2:$B$${category2Options.length + 1}`],
+ });
+ }
+
+ if (itemColIndex > 0) {
+ (worksheet as any).dataValidations.add(`${worksheet.getColumn(itemColIndex).letter}2:${worksheet.getColumn(itemColIndex).letter}1000`, {
+ type: "list",
+ allowBlank: false,
+ formulae: [`ValidationData!$C$2:$C$${itemOptions.length + 1}`],
+ });
+ }
+
+ if (scoreTypeColIndex > 0) {
+ (worksheet as any).dataValidations.add(`${worksheet.getColumn(scoreTypeColIndex).letter}2:${worksheet.getColumn(scoreTypeColIndex).letter}1000`, {
+ type: "list",
+ allowBlank: false,
+ formulae: [`ValidationData!$D$2:$D$${scoreTypeOptions.length + 1}`],
+ });
+ }
+
+ worksheet.eachRow((row) => {
+ row.eachCell((cell) => {
+ cell.border = {
+ top: { style: "thin" },
+ left: { style: "thin" },
+ bottom: { style: "thin" },
+ right: { style: "thin" },
+ };
+ });
+ });
+
+ return await workbook.xlsx.writeBuffer() as ArrayBuffer;
+ } catch (error) {
+ console.error('Error in generating Excel template:', error);
+ throw new Error('Failed to generate Excel template');
+ }
+}
+
+/* EXCEL IMPORT CLIENT-FRIENDLY SERVER ACTION */
+async function importRegEvalCriteriaExcel(file: File): Promise<{
+ errorFile?: Blob;
+ errorMessage?: string;
+ successMessage?: string;
+}> {
+ try {
+ const arrayBuffer = await file.arrayBuffer();
+ const workbook = new ExcelJS.Workbook();
+ await workbook.xlsx.load(arrayBuffer);
+
+ const worksheet = workbook.getWorksheet(1);
+ if (!worksheet) {
+ return { errorMessage: '워크시트를 찾을 수 없습니다.' };
+ }
+
+ const errors: string[] = [];
+ const groupedData: Map<string, any[]> = new Map();
+
+ // 각 행 처리
+ worksheet.eachRow((row, rowIndex) => {
+ if (rowIndex === 1) return; // 헤더 건너뛰기
+
+ try {
+ const rowData = {
+ category: row.getCell(1).value?.toString()?.trim() || '',
+ category2: row.getCell(2).value?.toString()?.trim() || '',
+ item: row.getCell(3).value?.toString()?.trim() || '',
+ classification: row.getCell(4).value?.toString()?.trim() || '',
+ range: row.getCell(5).value?.toString()?.trim() || '',
+ remarks: row.getCell(6).value?.toString()?.trim() || '',
+ scoreType: row.getCell(7).value?.toString()?.trim() || '',
+ variableScoreMin: row.getCell(8).value || '',
+ variableScoreMax: row.getCell(9).value || '',
+ variableScoreUnit: row.getCell(10).value?.toString()?.trim() || '',
+ detail: row.getCell(11).value?.toString()?.trim() || '',
+ scoreEquipShip: row.getCell(12).value || '',
+ scoreEquipMarine: row.getCell(13).value || '',
+ scoreBulkShip: row.getCell(14).value || '',
+ scoreBulkMarine: row.getCell(15).value || '',
+ orderIndex: row.getCell(16).value || '',
+ };
+
+ // 빈 행 건너뛰기
+ if (!rowData.category && !rowData.category2 && !rowData.item && !rowData.classification) {
+ return;
+ }
+
+ // 필수 필드 검증
+ if (!rowData.category || !rowData.category2 || !rowData.item || !rowData.classification) {
+ errors.push(`행 ${rowIndex}: 필수 필드(평가부문, 점수구분, 항목, 구분)가 누락되었습니다.`);
+ return;
+ }
+
+ // 점수 유형 검증
+ if (rowData.scoreType !== '고정' && rowData.scoreType !== '변동') {
+ errors.push(`행 ${rowIndex}: 점수 유형은 '고정' 또는 '변동'이어야 합니다.`);
+ return;
+ }
+
+ // 변동 점수 유형 검증
+ if (rowData.scoreType === '변동') {
+ if (rowData.scoreEquipShip || rowData.scoreEquipMarine ||
+ rowData.scoreBulkShip || rowData.scoreBulkMarine) {
+ errors.push(`행 ${rowIndex}: 변동 점수 유형에서는 개별 배점을 입력할 수 없습니다.`);
+ return;
+ }
+ if (rowData.detail) {
+ errors.push(`행 ${rowIndex}: 변동 점수 유형에서는 평가내용을 입력할 수 없습니다.`);
+ return;
+ }
+ }
+
+ // 고정 점수 유형 검증
+ if (rowData.scoreType === '고정') {
+ if (!rowData.detail) {
+ errors.push(`행 ${rowIndex}: 고정 점수 유형에서는 평가내용이 필수입니다.`);
+ return;
+ }
+ }
+
+ // 한글 라벨을 영어 값으로 변환
+ const categoryValue = REG_EVAL_CRITERIA_CATEGORY.find(item => item.label === rowData.category)?.value || rowData.category;
+ const category2Value = REG_EVAL_CRITERIA_CATEGORY2.find(item => item.label === rowData.category2)?.value || rowData.category2;
+ const itemValue = REG_EVAL_CRITERIA_ITEM.find(item => item.label === rowData.item)?.value || rowData.item;
+
+ // 데이터 그룹화
+ const groupKey = `${categoryValue}-${category2Value}-${itemValue}-${rowData.classification}`;
+ if (!groupedData.has(groupKey)) {
+ groupedData.set(groupKey, []);
+ }
+ groupedData.get(groupKey)!.push({
+ ...rowData,
+ category: categoryValue,
+ category2: category2Value,
+ item: itemValue,
+ scoreType: rowData.scoreType === '고정' ? 'fixed' : 'variable'
+ });
+
+ } catch (error) {
+ errors.push(`행 ${rowIndex}: 데이터 처리 중 오류가 발생했습니다 - ${error}`);
+ }
+ });
+
+ // 에러가 있으면 에러 파일 생성 (Blob으로 반환)
+ if (errors.length > 0) {
+ const errorWorkbook = new ExcelJS.Workbook();
+ const errorWorksheet = errorWorkbook.addWorksheet('Import Errors');
+
+ errorWorksheet.columns = [
+ { header: '오류 내용', key: 'error', width: 80 },
+ ];
+
+ errorWorksheet.getRow(1).font = { bold: true };
+ errorWorksheet.getRow(1).fill = {
+ type: 'pattern',
+ pattern: 'solid',
+ fgColor: { argb: 'FFFF0000' }
+ };
+
+ errors.forEach(error => {
+ errorWorksheet.addRow({ error });
+ });
+
+ const buffer = await errorWorkbook.xlsx.writeBuffer();
+ const errorFile = new Blob([buffer], {
+ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+ });
+
+ return {
+ errorFile,
+ errorMessage: `${errors.length}개의 오류가 발견되었습니다. 오류 파일을 확인하세요.`
+ };
+ }
+
+ // 성공한 데이터 처리
+ let importedCount = 0;
+ const currentUserId = await getCurrentUserId();
+
+ for (const [, groupData] of groupedData) {
+ const firstRow = groupData[0];
+
+ const criteriaData = {
+ category: firstRow.category,
+ category2: firstRow.category2,
+ item: firstRow.item,
+ classification: firstRow.classification,
+ range: firstRow.range || null,
+ remarks: firstRow.remarks || null,
+ scoreType: firstRow.scoreType,
+ variableScoreMin: firstRow.variableScoreMin ? String(firstRow.variableScoreMin) : null,
+ variableScoreMax: firstRow.variableScoreMax ? String(firstRow.variableScoreMax) : null,
+ variableScoreUnit: firstRow.variableScoreUnit || null,
+ createdBy: currentUserId,
+ updatedBy: currentUserId,
+ };
+
+ if (firstRow.scoreType === 'fixed') {
+ const detailList = groupData.map((detailData, index) => ({
+ detail: detailData.detail,
+ scoreEquipShip: detailData.scoreEquipShip ? String(detailData.scoreEquipShip) : null,
+ scoreEquipMarine: detailData.scoreEquipMarine ? String(detailData.scoreEquipMarine) : null,
+ scoreBulkShip: detailData.scoreBulkShip ? String(detailData.scoreBulkShip) : null,
+ scoreBulkMarine: detailData.scoreBulkMarine ? String(detailData.scoreBulkMarine) : null,
+ orderIndex: detailData.orderIndex ? parseInt(String(detailData.orderIndex)) : index,
+ }));
+
+ await createRegEvalCriteriaFixed(criteriaData, detailList);
+ } else {
+ await createRegEvalCriteriaVariable(criteriaData);
+ }
+
+ importedCount++;
+ }
+
+ return { successMessage: `Excel 파일이 성공적으로 업로드되었습니다. ${importedCount}개의 평가 기준이 추가되었습니다.` };
+ } catch (error) {
+ console.error('Error in Importing Regular Evaluation Criteria from Excel:', error);
+ return { errorMessage: 'Excel 파일 처리 중 오류가 발생했습니다.' };
+ }
+}
+
// ----------------------------------------------------------------------------------------------------
/* EXPORT */
export {
- createRegEvalCriteriaWithDetails,
- modifyRegEvalCriteriaWithDetails,
+ createRegEvalCriteriaFixed,
+ createRegEvalCriteriaVariable,
+ modifyRegEvalCriteriaFixed,
+ modifyRegEvalCriteriaVariable,
getRegEvalCriteria,
getRegEvalCriteriaWithDetails,
- getRegEvalCriteriaDetails, // 새로 추가
+ getRegEvalCriteriaDetails,
removeRegEvalCriteria,
removeRegEvalCriteriaDetails,
+ generateRegEvalCriteriaExcel,
+ generateRegEvalCriteriaTemplate,
+ importRegEvalCriteriaExcel,
};
\ No newline at end of file diff --git a/lib/evaluation-criteria/table/reg-eval-criteria-columns.tsx b/lib/evaluation-criteria/table/reg-eval-criteria-columns.tsx index 88c8107b..d48e097b 100644 --- a/lib/evaluation-criteria/table/reg-eval-criteria-columns.tsx +++ b/lib/evaluation-criteria/table/reg-eval-criteria-columns.tsx @@ -85,6 +85,7 @@ function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<RegEvalCriteri excelHeader: 'Category', type: 'select', }, + size: 50, }, { accessorKey: 'category2', @@ -106,6 +107,7 @@ function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<RegEvalCriteri excelHeader: 'Score Category', type: 'select', }, + size: 50, }, { accessorKey: 'item', @@ -127,6 +129,7 @@ function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<RegEvalCriteri excelHeader: 'Item', type: 'select', }, + size: 50, }, { accessorKey: 'classification', @@ -144,6 +147,7 @@ function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<RegEvalCriteri excelHeader: 'Classification', type: 'text', }, + size: 100, }, { accessorKey: 'range', @@ -280,6 +284,7 @@ function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<RegEvalCriteri excelHeader: 'Remarks', type: 'text', }, + size: 300, }; // [5] HIDDEN ID COLUMNS diff --git a/lib/evaluation-criteria/table/reg-eval-criteria-create-dialog.tsx b/lib/evaluation-criteria/table/reg-eval-criteria-create-dialog.tsx index 2a668ca8..972af75d 100644 --- a/lib/evaluation-criteria/table/reg-eval-criteria-create-dialog.tsx +++ b/lib/evaluation-criteria/table/reg-eval-criteria-create-dialog.tsx @@ -1,7 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ - 'use client'; - /* IMPORT */ import { Button } from '@/components/ui/button'; import { @@ -11,11 +9,12 @@ import { CardHeader, CardTitle, } from '@/components/ui/card'; -import { createRegEvalCriteriaWithDetails } from '../service'; +import { createRegEvalCriteriaFixed, createRegEvalCriteriaVariable } from '../service'; import { Dialog, DialogContent, DialogDescription, + DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; @@ -46,11 +45,11 @@ import { Textarea } from '@/components/ui/textarea'; import { toast } from 'sonner'; import { useForm, useFieldArray } from 'react-hook-form'; import { useEffect, useTransition } from 'react'; +import { useSession } from 'next-auth/react'; import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; // ---------------------------------------------------------------------------------------------------- - /* TYPES */ const regEvalCriteriaFormSchema = z.object({ category: z.string().min(1, '평가부문은 필수 항목입니다.'), @@ -59,18 +58,24 @@ const regEvalCriteriaFormSchema = z.object({ classification: z.string().min(1, '구분은 필수 항목입니다.'), range: z.string().nullable().optional(), remarks: z.string().nullable().optional(), + scoreType: z.enum(['fixed', 'variable']).default('fixed'), + variableScoreMin: z.coerce.number().nullable().optional(), + variableScoreMax: z.coerce.number().nullable().optional(), + variableScoreUnit: z.string().nullable().optional(), criteriaDetails: z.array( z.object({ id: z.number().optional(), - detail: z.string().min(1, '평가내용은 필수 항목입니다.'), + detail: z.string().optional(), scoreEquipShip: z.coerce.number().nullable().optional(), scoreEquipMarine: z.coerce.number().nullable().optional(), scoreBulkShip: z.coerce.number().nullable().optional(), scoreBulkMarine: z.coerce.number().nullable().optional(), }) - ).min(1, '최소 1개의 평가 내용이 필요합니다.'), + ).optional(), }); + type RegEvalCriteriaFormData = z.infer<typeof regEvalCriteriaFormSchema>; + interface CriteriaDetailFormProps { index: number form: any @@ -83,9 +88,8 @@ interface RegEvalCriteriaFormSheetProps { onOpenChange: (open: boolean) => void, onSuccess: () => void, }; - // ---------------------------------------------------------------------------------------------------- - +/* CRITERIA DETAIL FORM COPONENT */ /* CRITERIA DETAIL FORM COPONENT */ function CriteriaDetailForm({ index, @@ -99,7 +103,7 @@ function CriteriaDetailForm({ <Card> <CardHeader> <div className="flex items-center justify-between"> - <CardTitle className="text-lg">Detail Item - {index + 1}</CardTitle> + <CardTitle className="text-lg">평가 옵션 {index + 1}</CardTitle> {canRemove && ( <Button type="button" @@ -127,91 +131,11 @@ function CriteriaDetailForm({ name={`criteriaDetails.${index}.detail`} render={({ field }) => ( <FormItem> - <FormLabel>평가내용</FormLabel> + <FormLabel>평가 옵션 내용</FormLabel> <FormControl> <Textarea - placeholder="평가내용을 입력하세요." - {...field} - disabled={disabled} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name={`criteriaDetails.${index}.scoreEquipShip`} - render={({ field }) => ( - <FormItem> - <FormLabel>배점/기자재/조선</FormLabel> - <FormControl> - <Input - type="number" - step="0.1" - placeholder="배점/기자재/조선" + placeholder="평가 옵션 내용을 입력하세요. (예: 우수, 보통, 미흡)" {...field} - value={field.value ?? 0} - disabled={disabled} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name={`criteriaDetails.${index}.scoreEquipMarine`} - render={({ field }) => ( - <FormItem> - <FormLabel>배점/기자재/해양</FormLabel> - <FormControl> - <Input - type="number" - step="0.1" - placeholder="배점/기자재/해양" - {...field} - value={field.value ?? 0} - disabled={disabled} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name={`criteriaDetails.${index}.scoreBulkShip`} - render={({ field }) => ( - <FormItem> - <FormLabel>배점/벌크/조선</FormLabel> - <FormControl> - <Input - type="number" - step="0.1" - placeholder="배점/벌크/조선" - {...field} - value={field.value ?? 0} - disabled={disabled} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name={`criteriaDetails.${index}.scoreBulkMarine`} - render={({ field }) => ( - <FormItem> - <FormLabel>배점/벌크/해양</FormLabel> - <FormControl> - <Input - type="number" - step="0.1" - placeholder="배점/벌크/해양" - {...field} - value={field.value ?? 0} disabled={disabled} /> </FormControl> @@ -219,11 +143,92 @@ function CriteriaDetailForm({ </FormItem> )} /> + <div className="grid grid-cols-4 gap-4"> + <FormField + control={form.control} + name={`criteriaDetails.${index}.scoreEquipShip`} + render={({ field }) => ( + <FormItem> + <FormLabel>기자재-조선</FormLabel> + <FormControl> + <Input + type="number" + step="0.01" + placeholder="0.00" + {...field} + value={field.value ?? ''} + disabled={disabled} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name={`criteriaDetails.${index}.scoreEquipMarine`} + render={({ field }) => ( + <FormItem> + <FormLabel>기자재-해양</FormLabel> + <FormControl> + <Input + type="number" + step="0.01" + placeholder="0.00" + {...field} + value={field.value ?? ''} + disabled={disabled} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name={`criteriaDetails.${index}.scoreBulkShip`} + render={({ field }) => ( + <FormItem> + <FormLabel>벌크-조선</FormLabel> + <FormControl> + <Input + type="number" + step="0.01" + placeholder="0.00" + {...field} + value={field.value ?? ''} + disabled={disabled} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name={`criteriaDetails.${index}.scoreBulkMarine`} + render={({ field }) => ( + <FormItem> + <FormLabel>벌크-해양</FormLabel> + <FormControl> + <Input + type="number" + step="0.01" + placeholder="0.00" + {...field} + value={field.value ?? ''} + disabled={disabled} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> </CardContent> </Card> ) } - /* CRITERIA FORM SHEET COPONENT */ function RegEvalCriteriaCreateDialog({ open, @@ -231,6 +236,7 @@ function RegEvalCriteriaCreateDialog({ onSuccess, }: RegEvalCriteriaFormSheetProps) { const [isPending, startTransition] = useTransition(); + const { data: session } = useSession(); const form = useForm<RegEvalCriteriaFormData>({ resolver: zodResolver(regEvalCriteriaFormSchema), defaultValues: { @@ -240,6 +246,10 @@ function RegEvalCriteriaCreateDialog({ classification: '', range: '', remarks: '', + scoreType: 'fixed', + variableScoreMin: null, + variableScoreMax: null, + variableScoreUnit: '', criteriaDetails: [ { id: undefined, @@ -252,12 +262,11 @@ function RegEvalCriteriaCreateDialog({ ], }, }); - const { fields, append, remove } = useFieldArray({ control: form.control, name: 'criteriaDetails', }); - + const scoreType = form.watch('scoreType'); useEffect(() => { if (open) { form.reset({ @@ -267,6 +276,10 @@ function RegEvalCriteriaCreateDialog({ classification: '', range: '', remarks: '', + scoreType: 'fixed', + variableScoreMin: null, + variableScoreMax: null, + variableScoreUnit: '', criteriaDetails: [ { id: undefined, @@ -280,32 +293,92 @@ function RegEvalCriteriaCreateDialog({ }) } }, [open, form]); - - const onSubmit = async (data: RegEvalCriteriaFormData) => { + const onSubmit = async (data: any) => { startTransition(async () => { try { - const criteriaData = { - category: data.category, - category2: data.category2, - item: data.item, - classification: data.classification, - range: data.range, - remarks: data.remarks, - }; - const detailList = data.criteriaDetails.map((detailItem) => ({ - id: detailItem.id, - detail: detailItem.detail, - scoreEquipShip: detailItem.scoreEquipShip != null - ? String(detailItem.scoreEquipShip) : null, - scoreEquipMarine: detailItem.scoreEquipMarine != null - ? String(detailItem.scoreEquipMarine) : null, - scoreBulkShip: detailItem.scoreBulkShip != null - ? String(detailItem.scoreBulkShip) : null, - scoreBulkMarine: detailItem.scoreBulkMarine != null - ? String(detailItem.scoreBulkMarine) : null, - })); + const userId = session?.user?.id ? Number(session.user.id) : 1; + + if (data.scoreType === 'fixed') { + // 고정 점수 검증 + if (!data.criteriaDetails || data.criteriaDetails.length === 0) { + toast.error('고정 점수 유형에서는 최소 1개의 평가내용이 필요합니다.'); + return; + } + + // 평가내용이 비어있는지 확인 + const hasEmptyDetail = data.criteriaDetails.some((detail: NonNullable<RegEvalCriteriaFormData['criteriaDetails']>[0]) => + !detail.detail || (detail.detail && detail.detail.trim() === '') + ); + if (hasEmptyDetail) { + toast.error('평가내용을 입력해주세요.'); + return; + } + + const baseCriteriaData = { + category: data.category, + category2: data.category2, + item: data.item, + classification: data.classification, + range: data.range || null, + remarks: data.remarks || null, + scoreType: data.scoreType, + variableScoreMin: null, // 고정 점수에서는 null + variableScoreMax: null, // 고정 점수에서는 null + variableScoreUnit: null, // 고정 점수에서는 null + createdBy: userId, + updatedBy: userId, + }; + + const detailList = data.criteriaDetails.map((detailItem: NonNullable<RegEvalCriteriaFormData['criteriaDetails']>[0]) => ({ + detail: detailItem.detail?.trim() || '', + scoreEquipShip: detailItem.scoreEquipShip != null ? String(detailItem.scoreEquipShip) : null, + scoreEquipMarine: detailItem.scoreEquipMarine != null ? String(detailItem.scoreEquipMarine) : null, + scoreBulkShip: detailItem.scoreBulkShip != null ? String(detailItem.scoreBulkShip) : null, + scoreBulkMarine: detailItem.scoreBulkMarine != null ? String(detailItem.scoreBulkMarine) : null, + })); + + await createRegEvalCriteriaFixed(baseCriteriaData, detailList); - await createRegEvalCriteriaWithDetails(criteriaData, detailList); + } else if (data.scoreType === 'variable') { + // 변동 점수 검증 + if (data.variableScoreMin == null || data.variableScoreMin === '') { + toast.error('변동 점수 유형에서는 최소 점수가 필수입니다.'); + return; + } + if (data.variableScoreMax == null || data.variableScoreMax === '') { + toast.error('변동 점수 유형에서는 최대 점수가 필수입니다.'); + return; + } + if (!data.variableScoreUnit || data.variableScoreUnit.trim() === '') { + toast.error('변동 점수 유형에서는 단위가 필수입니다.'); + return; + } + if (Number(data.variableScoreMin) >= Number(data.variableScoreMax)) { + toast.error('최소 점수는 최대 점수보다 작아야 합니다.'); + return; + } + + const variableCriteriaData = { + category: data.category, + category2: data.category2, + item: data.item, + classification: data.classification, + range: data.range || null, + remarks: data.remarks || null, + scoreType: data.scoreType, + variableScoreMin: String(data.variableScoreMin), + variableScoreMax: String(data.variableScoreMax), + variableScoreUnit: data.variableScoreUnit.trim(), + createdBy: userId, + updatedBy: userId, + }; + + await createRegEvalCriteriaVariable(variableCriteriaData); + } else { + toast.error('올바른 점수 유형을 선택해주세요.'); + return; + } + toast.success('평가 기준표가 생성되었습니다.'); onSuccess(); onOpenChange(false); @@ -317,15 +390,13 @@ function RegEvalCriteriaCreateDialog({ } }) } - if (!open) { return null; } - return ( <Dialog open={open} onOpenChange={onOpenChange}> - <DialogContent className="w-1/3 max-w-[50vw] max-h-[90vh] overflow-y-auto"> - <DialogHeader className="mb-4"> + <DialogContent className="max-w-4xl max-h-[90vh] flex flex-col"> + <DialogHeader className="flex-shrink-0"> <DialogTitle className="font-bold"> 새 협력업체 평가 기준표 생성 </DialogTitle> @@ -334,33 +405,112 @@ function RegEvalCriteriaCreateDialog({ </DialogDescription> </DialogHeader> <Form {...form}> - <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6"> - <ScrollArea className="overflow-y-auto"> + <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col flex-1 min-h-0"> + <ScrollArea className="flex-1 pr-4 overflow-y-auto"> <div className="space-y-6"> <Card className="w-full"> <CardHeader> - <CardTitle>Criterion Info</CardTitle> + <CardTitle>기준 정보</CardTitle> </CardHeader> <CardContent className="space-y-4"> + <div className="grid grid-cols-2 gap-4"> + <FormField + control={form.control} + name="category" + render={({ field }) => ( + <FormItem> + <FormLabel>평가부문</FormLabel> + <FormControl> + <Select onValueChange={field.onChange} value={field.value || ''}> + <SelectTrigger> + <SelectValue placeholder="선택" /> + </SelectTrigger> + <SelectContent> + {REG_EVAL_CRITERIA_CATEGORY.map((option) => ( + <SelectItem key={option.value} value={option.value}> + {option.label} + </SelectItem> + ))} + </SelectContent> + </Select> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name="category2" + render={({ field }) => ( + <FormItem> + <FormLabel>점수구분</FormLabel> + <FormControl> + <Select onValueChange={field.onChange} value={field.value || ''}> + <SelectTrigger> + <SelectValue placeholder="선택" /> + </SelectTrigger> + <SelectContent> + {REG_EVAL_CRITERIA_CATEGORY2.map((option) => ( + <SelectItem key={option.value} value={option.value}> + {option.label} + </SelectItem> + ))} + </SelectContent> + </Select> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name="item" + render={({ field }) => ( + <FormItem> + <FormLabel>항목</FormLabel> + <FormControl> + <Select onValueChange={field.onChange} value={field.value || ''}> + <SelectTrigger> + <SelectValue placeholder="선택" /> + </SelectTrigger> + <SelectContent> + {REG_EVAL_CRITERIA_ITEM.map((option) => ( + <SelectItem key={option.value} value={option.value}> + {option.label} + </SelectItem> + ))} + </SelectContent> + </Select> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name="classification" + render={({ field }) => ( + <FormItem> + <FormLabel>구분</FormLabel> + <FormControl> + <Input placeholder="구분을 입력하세요." {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> <FormField control={form.control} - name="category" + name="range" render={({ field }) => ( <FormItem> - <FormLabel>평가부문</FormLabel> + <FormLabel>평가명</FormLabel> <FormControl> - <Select onValueChange={field.onChange} value={field.value || ''}> - <SelectTrigger> - <SelectValue placeholder="선택" /> - </SelectTrigger> - <SelectContent> - {REG_EVAL_CRITERIA_CATEGORY.map((option) => ( - <SelectItem key={option.value} value={option.value}> - {option.label} - </SelectItem> - ))} - </SelectContent> - </Select> + <Input + placeholder="평가명을 입력하세요." {...field} + value={field.value ?? ''} + /> </FormControl> <FormMessage /> </FormItem> @@ -368,23 +518,16 @@ function RegEvalCriteriaCreateDialog({ /> <FormField control={form.control} - name="category2" + name="remarks" render={({ field }) => ( <FormItem> - <FormLabel>점수구분</FormLabel> + <FormLabel>비고</FormLabel> <FormControl> - <Select onValueChange={field.onChange} value={field.value || ''}> - <SelectTrigger> - <SelectValue placeholder="선택" /> - </SelectTrigger> - <SelectContent> - {REG_EVAL_CRITERIA_CATEGORY2.map((option) => ( - <SelectItem key={option.value} value={option.value}> - {option.label} - </SelectItem> - ))} - </SelectContent> - </Select> + <Textarea + placeholder="비고를 입력하세요." + {...field} + value={field.value ?? ''} + /> </FormControl> <FormMessage /> </FormItem> @@ -392,21 +535,18 @@ function RegEvalCriteriaCreateDialog({ /> <FormField control={form.control} - name="item" + name="scoreType" render={({ field }) => ( <FormItem> - <FormLabel>항목</FormLabel> + <FormLabel>점수 유형</FormLabel> <FormControl> - <Select onValueChange={field.onChange} value={field.value || ''}> + <Select onValueChange={field.onChange} value={field.value}> <SelectTrigger> - <SelectValue placeholder="선택" /> + <SelectValue placeholder="점수 유형 선택" /> </SelectTrigger> <SelectContent> - {REG_EVAL_CRITERIA_ITEM.map((option) => ( - <SelectItem key={option.value} value={option.value}> - {option.label} - </SelectItem> - ))} + <SelectItem value="fixed">고정</SelectItem> + <SelectItem value="variable">변동</SelectItem> </SelectContent> </Select> </FormControl> @@ -414,123 +554,151 @@ function RegEvalCriteriaCreateDialog({ </FormItem> )} /> - <FormField - control={form.control} - name="classification" - render={({ field }) => ( - <FormItem> - <FormLabel>구분</FormLabel> - <FormControl> - <Input placeholder="구분을 입력하세요." {...field} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name="range" - render={({ field }) => ( - <FormItem> - <FormLabel>범위</FormLabel> - <FormControl> - <Input - placeholder="범위를 입력하세요." {...field} - value={field.value ?? ''} + {scoreType === 'variable' && ( + <div className="grid grid-cols-3 gap-4"> + <FormField + control={form.control} + name="variableScoreMin" + render={({ field }) => ( + <FormItem> + <FormLabel>최소점수</FormLabel> + <FormControl> + <Input + type="number" + min="0" + step="1" + placeholder="점수 입력 (0 이상)" + {...field} + value={field.value ?? ''} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name="remarks" - render={({ field }) => ( - <FormItem> - <FormLabel>비고</FormLabel> - <FormControl> - <Textarea - placeholder="비고를 입력하세요." - {...field} - value={field.value ?? ''} + <FormField + control={form.control} + name="variableScoreMax" + render={({ field }) => ( + <FormItem> + <FormLabel>최대점수</FormLabel> + <FormControl> + <Input + type="number" + min="0" + step="1" + placeholder="점수 입력 (0 이상)" + {...field} + value={field.value ?? ''} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name="variableScoreUnit" + render={({ field }) => ( + <FormItem> + <FormLabel>점수단위</FormLabel> + <FormControl> + <Input + placeholder="예: 감점, 가점" + {...field} + value={field.value ?? ''} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - </CardContent> - </Card> - <Card> - <CardHeader> - <div className="flex items-center justify-between"> - <div> - <CardTitle>Evaluation Criteria Item</CardTitle> - <CardDescription> - Set Evaluation Criteria Item. - </CardDescription> </div> - <Button - type="button" - variant="outline" - size="sm" - className="ml-4" - onClick={() => - append({ - id: undefined, - detail: '', - scoreEquipShip: null, - scoreEquipMarine: null, - scoreBulkShip: null, - scoreBulkMarine: null, - }) - } - disabled={isPending} - > - <Plus className="w-4 h-4 mr-2" /> - New Item - </Button> - </div> - </CardHeader> - <CardContent> - <div className="space-y-4"> - {fields.map((field, index) => ( - <CriteriaDetailForm - key={field.id} - index={index} - form={form} - onRemove={() => remove(index)} - canRemove={fields.length > 1} - disabled={isPending} - /> - ))} - </div> + )} </CardContent> </Card> + + {scoreType === 'fixed' && ( + <Card> + <CardHeader> + <div className="flex items-center justify-between"> + <div> + <CardTitle>평가 옵션 설정</CardTitle> + <CardDescription> + 평가 옵션을 설정합니다. + </CardDescription> + </div> + <Button + type="button" + variant="outline" + size="sm" + onClick={() => + append({ + id: undefined, + detail: '', + scoreEquipShip: null, + scoreEquipMarine: null, + scoreBulkShip: null, + scoreBulkMarine: null, + }) + } + disabled={isPending} + > + <Plus className="w-4 h-4 mr-2" /> + 항목 추가 + </Button> + </div> + </CardHeader> + <CardContent> + <div className="space-y-4"> + {fields.map((field, index) => ( + <CriteriaDetailForm + key={field.id} + index={index} + form={form} + onRemove={() => remove(index)} + canRemove={fields.length > 1} + disabled={isPending} + /> + ))} + </div> + </CardContent> + </Card> + )} + + {scoreType === 'variable' && ( + <Card> + <CardHeader> + <CardTitle>변동 점수 설정</CardTitle> + <CardDescription> + 변동 점수 유형에서는 개별 평가 옵션을 설정할 수 없습니다. + 최소/최대 점수와 단위를 설정하여 점수 범위를 정의하세요. + </CardDescription> + </CardHeader> + </Card> + )} </div> </ScrollArea> - <div className="flex justify-end gap-2 pt-4 border-t"> + + <DialogFooter className="flex-shrink-0 mt-4 pt-4 border-t"> <Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={isPending} > - Cancel + 취소 </Button> <Button type="submit" disabled={isPending}> - {isPending ? 'Saving...' : 'Create'} + {isPending ? '저장 중...' : '생성'} </Button> - </div> + </DialogFooter> </form> </Form> </DialogContent> </Dialog> ) } - // ---------------------------------------------------------------------------------------------------- - /* EXPORT */ export default RegEvalCriteriaCreateDialog;
\ No newline at end of file diff --git a/lib/evaluation-criteria/table/reg-eval-criteria-details-sheet.tsx b/lib/evaluation-criteria/table/reg-eval-criteria-details-sheet.tsx index 60ca173b..cfcf6e26 100644 --- a/lib/evaluation-criteria/table/reg-eval-criteria-details-sheet.tsx +++ b/lib/evaluation-criteria/table/reg-eval-criteria-details-sheet.tsx @@ -2,20 +2,20 @@ /* IMPORT */ import { Badge } from '@/components/ui/badge'; -import { Button } from '@/components/ui/button'; + import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Separator } from '@/components/ui/separator'; import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { useEffect, useState } from 'react'; -import { Loader2, X } from 'lucide-react'; +import { Loader2 } from 'lucide-react'; import { REG_EVAL_CRITERIA_CATEGORY, REG_EVAL_CRITERIA_CATEGORY2, REG_EVAL_CRITERIA_ITEM, - type RegEvalCriteriaView, type RegEvalCriteriaDetails, + type RegEvalCriteria, } from '@/db/schema'; import { getRegEvalCriteriaDetails } from '../service'; // 서버 액션 import @@ -23,7 +23,7 @@ import { getRegEvalCriteriaDetails } from '../service'; // 서버 액션 import /* TYPES */ interface RegEvalCriteriaDetailsSheetProps { - criteriaViewData: RegEvalCriteriaView; + criteriaViewData: RegEvalCriteria; open: boolean; onOpenChange: (open: boolean) => void; } @@ -125,7 +125,7 @@ export function RegEvalCriteriaDetailsSheet({ </div> <div> - <p className="text-sm font-medium text-muted-foreground">평가명 (범위)</p> + <p className="text-sm font-medium text-muted-foreground">평가명</p> <p className="text-sm mt-1 font-medium">{criteriaViewData.range || '-'}</p> </div> @@ -140,78 +140,118 @@ export function RegEvalCriteriaDetailsSheet({ <Separator /> - {/* 평가 옵션 및 점수 카드 */} - <Card> - <CardHeader> - <CardTitle className="text-lg">평가 옵션 및 점수</CardTitle> - <p className="text-sm text-muted-foreground"> - 각 평가 옵션에 따른 점수를 확인할 수 있습니다. - </p> - </CardHeader> - <CardContent> - {loading ? ( - <div className="flex justify-center items-center py-8"> - <Loader2 className="h-4 w-4 animate-spin mr-2" /> - <span className="text-sm text-muted-foreground">로딩 중...</span> - </div> - ) : error ? ( - <div className="flex justify-center items-center py-8"> - <div className="text-sm text-destructive">{error}</div> + {/* 점수 정보 카드 - 점수 유형에 따라 다른 UI 표시 */} + {criteriaViewData.scoreType === 'variable' ? ( + /* 변동점수 정보 카드 */ + <Card> + <CardHeader> + <CardTitle className="text-lg">변동점수 설정</CardTitle> + <p className="text-sm text-muted-foreground"> + 이 평가 기준은 변동점수 유형으로 설정되어 있습니다. + </p> + </CardHeader> + <CardContent className="space-y-4"> + <div className="grid grid-cols-3 gap-4"> + <div> + <p className="text-sm font-medium text-muted-foreground">최소점수</p> + <Badge variant="outline" className="mt-1"> + {criteriaViewData.variableScoreMin || '-'} + </Badge> + </div> + <div> + <p className="text-sm font-medium text-muted-foreground">최대점수</p> + <Badge variant="outline" className="mt-1 "> + {criteriaViewData.variableScoreMax || '-'} + </Badge> + </div> + <div> + <p className="text-sm font-medium text-muted-foreground">점수단위</p> + <Badge variant="outline" className="mt-1"> + {criteriaViewData.variableScoreUnit || '-'} + </Badge> + </div> </div> - ) : details.length === 0 ? ( - <div className="flex justify-center items-center py-8"> - <div className="text-sm text-muted-foreground">등록된 평가 옵션이 없습니다.</div> + <div className="p-4 bg-muted rounded-lg"> + <p className="text-sm text-muted-foreground"> + 변동점수 유형에서는 평가 시 설정된 점수 범위 내에서 점수를 부여합니다. + </p> </div> - ) : ( - <div className="border rounded-lg"> - <Table> - <TableHeader> - <TableRow> - <TableHead className="w-12">#</TableHead> - <TableHead className="min-w-[200px]">평가 옵션</TableHead> - <TableHead className="text-center w-24">기자재-조선</TableHead> - <TableHead className="text-center w-24">기자재-해양</TableHead> - <TableHead className="text-center w-24">벌크-조선</TableHead> - <TableHead className="text-center w-24">벌크-해양</TableHead> - </TableRow> - </TableHeader> - <TableBody> - {details.map((detail, index) => ( - <TableRow key={detail.id}> - <TableCell className="font-medium"> - {(detail.orderIndex ?? index) + 1} - </TableCell> - <TableCell className="font-medium"> - {detail.detail} - </TableCell> - <TableCell className="text-center"> - <Badge variant="outline" className="font-mono"> - {formatScore(detail.scoreEquipShip)} - </Badge> - </TableCell> - <TableCell className="text-center"> - <Badge variant="outline" className="font-mono"> - {formatScore(detail.scoreEquipMarine)} - </Badge> - </TableCell> - <TableCell className="text-center"> - <Badge variant="outline" className="font-mono"> - {formatScore(detail.scoreBulkShip)} - </Badge> - </TableCell> - <TableCell className="text-center"> - <Badge variant="outline" className="font-mono"> - {formatScore(detail.scoreBulkMarine)} - </Badge> - </TableCell> + </CardContent> + </Card> + ) : ( + /* 고정점수 - 평가 옵션 및 점수 카드 */ + <Card> + <CardHeader> + <CardTitle className="text-lg">평가 옵션 및 점수</CardTitle> + <p className="text-sm text-muted-foreground"> + 각 평가 옵션에 따른 점수를 확인할 수 있습니다. + </p> + </CardHeader> + <CardContent> + {loading ? ( + <div className="flex justify-center items-center py-8"> + <Loader2 className="h-4 w-4 animate-spin mr-2" /> + <span className="text-sm text-muted-foreground">로딩 중...</span> + </div> + ) : error ? ( + <div className="flex justify-center items-center py-8"> + <div className="text-sm text-destructive">{error}</div> + </div> + ) : details.length === 0 ? ( + <div className="flex justify-center items-center py-8"> + <div className="text-sm text-muted-foreground">등록된 평가 옵션이 없습니다.</div> + </div> + ) : ( + <div className="border rounded-lg"> + <Table> + <TableHeader> + <TableRow> + <TableHead className="w-12">#</TableHead> + <TableHead className="min-w-[200px]">평가 옵션</TableHead> + <TableHead className="text-center w-24">기자재-조선</TableHead> + <TableHead className="text-center w-24">기자재-해양</TableHead> + <TableHead className="text-center w-24">벌크-조선</TableHead> + <TableHead className="text-center w-24">벌크-해양</TableHead> </TableRow> - ))} - </TableBody> - </Table> - </div> - )} - </CardContent> - </Card> + </TableHeader> + <TableBody> + {details.map((detail, index) => ( + <TableRow key={detail.id}> + <TableCell className="font-medium"> + {(detail.orderIndex ?? index) + 1} + </TableCell> + <TableCell className="font-medium"> + {detail.detail} + </TableCell> + <TableCell className="text-center"> + <Badge variant="outline" className="font-mono"> + {formatScore(detail.scoreEquipShip)} + </Badge> + </TableCell> + <TableCell className="text-center"> + <Badge variant="outline" className="font-mono"> + {formatScore(detail.scoreEquipMarine)} + </Badge> + </TableCell> + <TableCell className="text-center"> + <Badge variant="outline" className="font-mono"> + {formatScore(detail.scoreBulkShip)} + </Badge> + </TableCell> + <TableCell className="text-center"> + <Badge variant="outline" className="font-mono"> + {formatScore(detail.scoreBulkMarine)} + </Badge> + </TableCell> + </TableRow> + ))} + </TableBody> + </Table> + </div> + )} + </CardContent> + </Card> + )} </div> </ScrollArea> diff --git a/lib/evaluation-criteria/table/reg-eval-criteria-form-sheet.tsx b/lib/evaluation-criteria/table/reg-eval-criteria-form-sheet.tsx index 3362d810..8f4c4413 100644 --- a/lib/evaluation-criteria/table/reg-eval-criteria-form-sheet.tsx +++ b/lib/evaluation-criteria/table/reg-eval-criteria-form-sheet.tsx @@ -1,586 +1,586 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ +// /* eslint-disable @typescript-eslint/no-explicit-any */ -'use client'; +// 'use client'; -/* IMPORT */ -import { Button } from '@/components/ui/button'; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '@/components/ui/card'; -import { - createRegEvalCriteriaWithDetails, - getRegEvalCriteriaWithDetails, - modifyRegEvalCriteriaWithDetails, -} from '../service'; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@/components/ui/form'; -import { Input } from '@/components/ui/input'; -import { Plus, Trash2 } from 'lucide-react'; -import { - REG_EVAL_CRITERIA_CATEGORY, - REG_EVAL_CRITERIA_CATEGORY2, - REG_EVAL_CRITERIA_ITEM, - type RegEvalCriteriaDetails, - type RegEvalCriteriaView, -} from '@/db/schema'; -import { ScrollArea } from '@/components/ui/scroll-area'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue -} from '@/components/ui/select'; -import { - Sheet, - SheetContent, - SheetDescription, - SheetHeader, - SheetTitle, -} from '@/components/ui/sheet'; -import { Textarea } from '@/components/ui/textarea'; -import { toast } from 'sonner'; -import { useForm, useFieldArray } from 'react-hook-form'; -import { useEffect, useTransition } from 'react'; -import { z } from 'zod'; -import { zodResolver } from '@hookform/resolvers/zod'; +// /* IMPORT */ +// import { Button } from '@/components/ui/button'; +// import { +// Card, +// CardContent, +// CardDescription, +// CardHeader, +// CardTitle, +// } from '@/components/ui/card'; +// import { +// createRegEvalCriteriaWithDetails, +// getRegEvalCriteriaWithDetails, +// modifyRegEvalCriteriaWithDetails, +// } from '../service'; +// import { +// Form, +// FormControl, +// FormField, +// FormItem, +// FormLabel, +// FormMessage, +// } from '@/components/ui/form'; +// import { Input } from '@/components/ui/input'; +// import { Plus, Trash2 } from 'lucide-react'; +// import { +// REG_EVAL_CRITERIA_CATEGORY, +// REG_EVAL_CRITERIA_CATEGORY2, +// REG_EVAL_CRITERIA_ITEM, +// type RegEvalCriteriaDetails, +// type RegEvalCriteriaView, +// } from '@/db/schema'; +// import { ScrollArea } from '@/components/ui/scroll-area'; +// import { +// Select, +// SelectContent, +// SelectItem, +// SelectTrigger, +// SelectValue +// } from '@/components/ui/select'; +// import { +// Sheet, +// SheetContent, +// SheetDescription, +// SheetHeader, +// SheetTitle, +// } from '@/components/ui/sheet'; +// import { Textarea } from '@/components/ui/textarea'; +// import { toast } from 'sonner'; +// import { useForm, useFieldArray } from 'react-hook-form'; +// import { useEffect, useTransition } from 'react'; +// import { z } from 'zod'; +// import { zodResolver } from '@hookform/resolvers/zod'; -// ---------------------------------------------------------------------------------------------------- +// // ---------------------------------------------------------------------------------------------------- -/* TYPES */ -const regEvalCriteriaFormSchema = z.object({ - category: z.string().min(1, '평가부문은 필수 항목입니다.'), - category2: z.string().min(1, '점수구분은 필수 항목입니다.'), - item: z.string().min(1, '항목은 필수 항목입니다.'), - classification: z.string().min(1, '구분은 필수 항목입니다.'), - range: z.string().nullable().optional(), - remarks: z.string().nullable().optional(), - criteriaDetails: z.array( - z.object({ - id: z.number().optional(), - detail: z.string().min(1, '평가내용은 필수 항목입니다.'), - scoreEquipShip: z.coerce.number().nullable().optional(), - scoreEquipMarine: z.coerce.number().nullable().optional(), - scoreBulkShip: z.coerce.number().nullable().optional(), - scoreBulkMarine: z.coerce.number().nullable().optional(), - }) - ).min(1, '최소 1개의 평가 내용이 필요합니다.'), -}); -type RegEvalCriteriaFormData = z.infer<typeof regEvalCriteriaFormSchema>; -interface CriteriaDetailFormProps { - index: number - form: any - onRemove: () => void - canRemove: boolean - disabled?: boolean -} -interface RegEvalCriteriaFormSheetProps { - open: boolean, - onOpenChange: (open: boolean) => void, - criteriaViewData: RegEvalCriteriaView | null, - onSuccess: () => void, -}; +// /* TYPES */ +// const regEvalCriteriaFormSchema = z.object({ +// category: z.string().min(1, '평가부문은 필수 항목입니다.'), +// category2: z.string().min(1, '점수구분은 필수 항목입니다.'), +// item: z.string().min(1, '항목은 필수 항목입니다.'), +// classification: z.string().min(1, '구분은 필수 항목입니다.'), +// range: z.string().nullable().optional(), +// remarks: z.string().nullable().optional(), +// criteriaDetails: z.array( +// z.object({ +// id: z.number().optional(), +// detail: z.string().min(1, '평가내용은 필수 항목입니다.'), +// scoreEquipShip: z.coerce.number().nullable().optional(), +// scoreEquipMarine: z.coerce.number().nullable().optional(), +// scoreBulkShip: z.coerce.number().nullable().optional(), +// scoreBulkMarine: z.coerce.number().nullable().optional(), +// }) +// ).min(1, '최소 1개의 평가 내용이 필요합니다.'), +// }); +// type RegEvalCriteriaFormData = z.infer<typeof regEvalCriteriaFormSchema>; +// interface CriteriaDetailFormProps { +// index: number +// form: any +// onRemove: () => void +// canRemove: boolean +// disabled?: boolean +// } +// interface RegEvalCriteriaFormSheetProps { +// open: boolean, +// onOpenChange: (open: boolean) => void, +// criteriaViewData: RegEvalCriteriaView | null, +// onSuccess: () => void, +// }; -// ---------------------------------------------------------------------------------------------------- +// // ---------------------------------------------------------------------------------------------------- -/* CRITERIA DETAIL FORM COPONENT */ -function CriteriaDetailForm({ - index, - form, - onRemove, - canRemove, - disabled = false, -}: CriteriaDetailFormProps) { +// /* CRITERIA DETAIL FORM COPONENT */ +// function CriteriaDetailForm({ +// index, +// form, +// onRemove, +// canRemove, +// disabled = false, +// }: CriteriaDetailFormProps) { - return ( - <Card> - <CardHeader> - <div className="flex items-center justify-between"> - <CardTitle className="text-lg">Detail Item - {index + 1}</CardTitle> - {canRemove && ( - <Button - type="button" - variant="ghost" - size="sm" - onClick={onRemove} - className="text-destructive hover:text-destructive" - disabled={disabled} - > - <Trash2 className="w-4 h-4" /> - </Button> - )} - </div> - </CardHeader> - <CardContent className="space-y-4"> - <FormField - control={form.control} - name={`criteriaDetails.${index}.id`} - render={({ field }) => ( - <Input type="hidden" {...field} /> - )} - /> - <FormField - control={form.control} - name={`criteriaDetails.${index}.detail`} - render={({ field }) => ( - <FormItem> - <FormLabel>평가내용</FormLabel> - <FormControl> - <Textarea - placeholder="평가내용을 입력하세요." - {...field} - disabled={disabled} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name={`criteriaDetails.${index}.scoreEquipShip`} - render={({ field }) => ( - <FormItem> - <FormLabel>배점/기자재/조선</FormLabel> - <FormControl> - <Input - type="number" - step="0.1" - placeholder="배점/기자재/조선" - {...field} - value={field.value ?? 0} - disabled={disabled} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name={`criteriaDetails.${index}.scoreEquipMarine`} - render={({ field }) => ( - <FormItem> - <FormLabel>배점/기자재/해양</FormLabel> - <FormControl> - <Input - type="number" - step="0.1" - placeholder="배점/기자재/해양" - {...field} - value={field.value ?? 0} - disabled={disabled} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name={`criteriaDetails.${index}.scoreBulkShip`} - render={({ field }) => ( - <FormItem> - <FormLabel>배점/벌크/조선</FormLabel> - <FormControl> - <Input - type="number" - step="0.1" - placeholder="배점/벌크/조선" - {...field} - value={field.value ?? 0} - disabled={disabled} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name={`criteriaDetails.${index}.scoreBulkMarine`} - render={({ field }) => ( - <FormItem> - <FormLabel>배점/벌크/해양</FormLabel> - <FormControl> - <Input - type="number" - step="0.1" - placeholder="배점/벌크/해양" - {...field} - value={field.value ?? 0} - disabled={disabled} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - </CardContent> - </Card> - ) -} +// return ( +// <Card> +// <CardHeader> +// <div className="flex items-center justify-between"> +// <CardTitle className="text-lg">Detail Item - {index + 1}</CardTitle> +// {canRemove && ( +// <Button +// type="button" +// variant="ghost" +// size="sm" +// onClick={onRemove} +// className="text-destructive hover:text-destructive" +// disabled={disabled} +// > +// <Trash2 className="w-4 h-4" /> +// </Button> +// )} +// </div> +// </CardHeader> +// <CardContent className="space-y-4"> +// <FormField +// control={form.control} +// name={`criteriaDetails.${index}.id`} +// render={({ field }) => ( +// <Input type="hidden" {...field} /> +// )} +// /> +// <FormField +// control={form.control} +// name={`criteriaDetails.${index}.detail`} +// render={({ field }) => ( +// <FormItem> +// <FormLabel>평가내용</FormLabel> +// <FormControl> +// <Textarea +// placeholder="평가내용을 입력하세요." +// {...field} +// disabled={disabled} +// /> +// </FormControl> +// <FormMessage /> +// </FormItem> +// )} +// /> +// <FormField +// control={form.control} +// name={`criteriaDetails.${index}.scoreEquipShip`} +// render={({ field }) => ( +// <FormItem> +// <FormLabel>배점/기자재/조선</FormLabel> +// <FormControl> +// <Input +// type="number" +// step="0.1" +// placeholder="배점/기자재/조선" +// {...field} +// value={field.value ?? 0} +// disabled={disabled} +// /> +// </FormControl> +// <FormMessage /> +// </FormItem> +// )} +// /> +// <FormField +// control={form.control} +// name={`criteriaDetails.${index}.scoreEquipMarine`} +// render={({ field }) => ( +// <FormItem> +// <FormLabel>배점/기자재/해양</FormLabel> +// <FormControl> +// <Input +// type="number" +// step="0.1" +// placeholder="배점/기자재/해양" +// {...field} +// value={field.value ?? 0} +// disabled={disabled} +// /> +// </FormControl> +// <FormMessage /> +// </FormItem> +// )} +// /> +// <FormField +// control={form.control} +// name={`criteriaDetails.${index}.scoreBulkShip`} +// render={({ field }) => ( +// <FormItem> +// <FormLabel>배점/벌크/조선</FormLabel> +// <FormControl> +// <Input +// type="number" +// step="0.1" +// placeholder="배점/벌크/조선" +// {...field} +// value={field.value ?? 0} +// disabled={disabled} +// /> +// </FormControl> +// <FormMessage /> +// </FormItem> +// )} +// /> +// <FormField +// control={form.control} +// name={`criteriaDetails.${index}.scoreBulkMarine`} +// render={({ field }) => ( +// <FormItem> +// <FormLabel>배점/벌크/해양</FormLabel> +// <FormControl> +// <Input +// type="number" +// step="0.1" +// placeholder="배점/벌크/해양" +// {...field} +// value={field.value ?? 0} +// disabled={disabled} +// /> +// </FormControl> +// <FormMessage /> +// </FormItem> +// )} +// /> +// </CardContent> +// </Card> +// ) +// } -/* CRITERIA FORM SHEET COPONENT */ -function RegEvalCriteriaFormSheet({ - open, - onOpenChange, - criteriaViewData, - onSuccess, -}: RegEvalCriteriaFormSheetProps) { - const [isPending, startTransition] = useTransition(); - const isUpdateMode = !!criteriaViewData; +// /* CRITERIA FORM SHEET COPONENT */ +// function RegEvalCriteriaFormSheet({ +// open, +// onOpenChange, +// criteriaViewData, +// onSuccess, +// }: RegEvalCriteriaFormSheetProps) { +// const [isPending, startTransition] = useTransition(); +// const isUpdateMode = !!criteriaViewData; - const form = useForm<RegEvalCriteriaFormData>({ - resolver: zodResolver(regEvalCriteriaFormSchema), - defaultValues: { - category: '', - category2: '', - item: '', - classification: '', - range: '', - remarks: '', - criteriaDetails: [ - { - id: undefined, - detail: '', - scoreEquipShip: null, - scoreEquipMarine: null, - scoreBulkShip: null, - scoreBulkMarine: null, - }, - ], - }, - }); +// const form = useForm<RegEvalCriteriaFormData>({ +// resolver: zodResolver(regEvalCriteriaFormSchema), +// defaultValues: { +// category: '', +// category2: '', +// item: '', +// classification: '', +// range: '', +// remarks: '', +// criteriaDetails: [ +// { +// id: undefined, +// detail: '', +// scoreEquipShip: null, +// scoreEquipMarine: null, +// scoreBulkShip: null, +// scoreBulkMarine: null, +// }, +// ], +// }, +// }); - const { fields, append, remove } = useFieldArray({ - control: form.control, - name: 'criteriaDetails', - }); +// const { fields, append, remove } = useFieldArray({ +// control: form.control, +// name: 'criteriaDetails', +// }); - useEffect(() => { - if (open && isUpdateMode && criteriaViewData) { - startTransition(async () => { - try { - const targetData = await getRegEvalCriteriaWithDetails(criteriaViewData.criteriaId!); - if (targetData) { - form.reset({ - category: targetData.category, - category2: targetData.category2, - item: targetData.item, - classification: targetData.classification, - range: targetData.range, - remarks: targetData.remarks, - criteriaDetails: targetData.criteriaDetails?.map((detailItem: RegEvalCriteriaDetails) => ({ - id: detailItem.id, - detail: detailItem.detail, - scoreEquipShip: detailItem.scoreEquipShip !== null - ? Number(detailItem.scoreEquipShip) : null, - scoreEquipMarine: detailItem.scoreEquipMarine !== null - ? Number(detailItem.scoreEquipMarine) : null, - scoreBulkShip: detailItem.scoreBulkShip !== null - ? Number(detailItem.scoreBulkShip) : null, - scoreBulkMarine: detailItem.scoreBulkMarine !== null - ? Number(detailItem.scoreBulkMarine) : null, - })) || [], - }) - } - } catch (error) { - console.error('Error in Loading Regular Evaluation Criteria for Updating:', error) - toast.error(error instanceof Error ? error.message : '편집할 데이터를 불러오는 데 실패했습니다.') - } - }) - } else if (open && !isUpdateMode) { - form.reset({ - category: '', - category2: '', - item: '', - classification: '', - range: '', - remarks: '', - criteriaDetails: [ - { - id: undefined, - detail: '', - scoreEquipShip: null, - scoreEquipMarine: null, - scoreBulkShip: null, - scoreBulkMarine: null, - }, - ], - }) - } - }, [open, isUpdateMode, criteriaViewData, form]); +// useEffect(() => { +// if (open && isUpdateMode && criteriaViewData) { +// startTransition(async () => { +// try { +// const targetData = await getRegEvalCriteriaWithDetails(criteriaViewData.criteriaId!); +// if (targetData) { +// form.reset({ +// category: targetData.category, +// category2: targetData.category2, +// item: targetData.item, +// classification: targetData.classification, +// range: targetData.range, +// remarks: targetData.remarks, +// criteriaDetails: targetData.criteriaDetails?.map((detailItem: RegEvalCriteriaDetails) => ({ +// id: detailItem.id, +// detail: detailItem.detail, +// scoreEquipShip: detailItem.scoreEquipShip !== null +// ? Number(detailItem.scoreEquipShip) : null, +// scoreEquipMarine: detailItem.scoreEquipMarine !== null +// ? Number(detailItem.scoreEquipMarine) : null, +// scoreBulkShip: detailItem.scoreBulkShip !== null +// ? Number(detailItem.scoreBulkShip) : null, +// scoreBulkMarine: detailItem.scoreBulkMarine !== null +// ? Number(detailItem.scoreBulkMarine) : null, +// })) || [], +// }) +// } +// } catch (error) { +// console.error('Error in Loading Regular Evaluation Criteria for Updating:', error) +// toast.error(error instanceof Error ? error.message : '편집할 데이터를 불러오는 데 실패했습니다.') +// } +// }) +// } else if (open && !isUpdateMode) { +// form.reset({ +// category: '', +// category2: '', +// item: '', +// classification: '', +// range: '', +// remarks: '', +// criteriaDetails: [ +// { +// id: undefined, +// detail: '', +// scoreEquipShip: null, +// scoreEquipMarine: null, +// scoreBulkShip: null, +// scoreBulkMarine: null, +// }, +// ], +// }) +// } +// }, [open, isUpdateMode, criteriaViewData, form]); - const onSubmit = async (data: RegEvalCriteriaFormData) => { - startTransition(async () => { - try { - const criteriaData = { - category: data.category, - category2: data.category2, - item: data.item, - classification: data.classification, - range: data.range, - remarks: data.remarks, - }; - const detailList = data.criteriaDetails.map((detailItem) => ({ - id: detailItem.id, - detail: detailItem.detail, - scoreEquipShip: detailItem.scoreEquipShip != null - ? String(detailItem.scoreEquipShip) : null, - scoreEquipMarine: detailItem.scoreEquipMarine != null - ? String(detailItem.scoreEquipMarine) : null, - scoreBulkShip: detailItem.scoreBulkShip != null - ? String(detailItem.scoreBulkShip) : null, - scoreBulkMarine: detailItem.scoreBulkMarine != null - ? String(detailItem.scoreBulkMarine) : null, - })); +// const onSubmit = async (data: RegEvalCriteriaFormData) => { +// startTransition(async () => { +// try { +// const criteriaData = { +// category: data.category, +// category2: data.category2, +// item: data.item, +// classification: data.classification, +// range: data.range, +// remarks: data.remarks, +// }; +// const detailList = data.criteriaDetails.map((detailItem) => ({ +// id: detailItem.id, +// detail: detailItem.detail, +// scoreEquipShip: detailItem.scoreEquipShip != null +// ? String(detailItem.scoreEquipShip) : null, +// scoreEquipMarine: detailItem.scoreEquipMarine != null +// ? String(detailItem.scoreEquipMarine) : null, +// scoreBulkShip: detailItem.scoreBulkShip != null +// ? String(detailItem.scoreBulkShip) : null, +// scoreBulkMarine: detailItem.scoreBulkMarine != null +// ? String(detailItem.scoreBulkMarine) : null, +// })); - if (isUpdateMode && criteriaViewData) { - await modifyRegEvalCriteriaWithDetails(criteriaViewData.criteriaId!, criteriaData, detailList); - toast.success('평가 기준표가 수정되었습니다.'); - } else { - await createRegEvalCriteriaWithDetails(criteriaData, detailList); - toast.success('평가 기준표가 생성되었습니다.'); - } - onSuccess(); - onOpenChange(false); - } catch (error) { - console.error('Error in Saving Regular Evaluation Criteria:', error); - toast.error( - error instanceof Error ? error.message : '평가 기준표 저장 중 오류가 발생했습니다.' - ); - } - }) - } +// if (isUpdateMode && criteriaViewData) { +// await modifyRegEvalCriteriaWithDetails(criteriaViewData.criteriaId!, criteriaData, detailList); +// toast.success('평가 기준표가 수정되었습니다.'); +// } else { +// await createRegEvalCriteriaWithDetails(criteriaData, detailList); +// toast.success('평가 기준표가 생성되었습니다.'); +// } +// onSuccess(); +// onOpenChange(false); +// } catch (error) { +// console.error('Error in Saving Regular Evaluation Criteria:', error); +// toast.error( +// error instanceof Error ? error.message : '평가 기준표 저장 중 오류가 발생했습니다.' +// ); +// } +// }) +// } - if (!open) { - return null; - } +// if (!open) { +// return null; +// } - return ( - <Sheet open={open} onOpenChange={onOpenChange}> - <SheetContent className="w-[900px] sm:max-w-[900px] overflow-y-auto"> - <SheetHeader className="mb-4"> - <SheetTitle className="font-bold"> - {isUpdateMode ? '협력업체 평가 기준표 수정' : '새 협력업체 평가 기준표 생성'} - </SheetTitle> - <SheetDescription> - {isUpdateMode ? '협력업체 평가 기준표의 정보를 수정합니다.' : '새로운 협력업체 평가 기준표를 생성합니다.'} - </SheetDescription> - </SheetHeader> - <Form {...form}> - <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6"> - <ScrollArea className="h-[calc(100vh-200px)] pr-4"> - <div className="space-y-6"> - <Card> - <CardHeader> - <CardTitle>Criterion Info</CardTitle> - </CardHeader> - <CardContent className="space-y-4"> - <FormField - control={form.control} - name="category" - render={({ field }) => ( - <FormItem> - <FormLabel>평가부문</FormLabel> - <FormControl> - <Select onValueChange={field.onChange} value={field.value || ""}> - <SelectTrigger> - <SelectValue placeholder="선택" /> - </SelectTrigger> - <SelectContent> - {REG_EVAL_CRITERIA_CATEGORY.map((option) => ( - <SelectItem key={option.value} value={option.value}> - {option.label} - </SelectItem> - ))} - </SelectContent> - </Select> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name="category2" - render={({ field }) => ( - <FormItem> - <FormLabel>점수구분</FormLabel> - <FormControl> - <Select onValueChange={field.onChange} value={field.value || ""}> - <SelectTrigger> - <SelectValue placeholder="선택" /> - </SelectTrigger> - <SelectContent> - {REG_EVAL_CRITERIA_CATEGORY2.map((option) => ( - <SelectItem key={option.value} value={option.value}> - {option.label} - </SelectItem> - ))} - </SelectContent> - </Select> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name="item" - render={({ field }) => ( - <FormItem> - <FormLabel>항목</FormLabel> - <FormControl> - <Select onValueChange={field.onChange} value={field.value || ""}> - <SelectTrigger> - <SelectValue placeholder="선택" /> - </SelectTrigger> - <SelectContent> - {REG_EVAL_CRITERIA_ITEM.map((option) => ( - <SelectItem key={option.value} value={option.value}> - {option.label} - </SelectItem> - ))} - </SelectContent> - </Select> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name="classification" - render={({ field }) => ( - <FormItem> - <FormLabel>구분</FormLabel> - <FormControl> - <Input placeholder="구분을 입력하세요." {...field} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name="range" - render={({ field }) => ( - <FormItem> - <FormLabel>범위</FormLabel> - <FormControl> - <Input - placeholder="범위를 입력하세요." {...field} - value={field.value ?? ''} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name="remarks" - render={({ field }) => ( - <FormItem> - <FormLabel>비고</FormLabel> - <FormControl> - <Textarea - placeholder="비고를 입력하세요." - {...field} - value={field.value ?? ''} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - </CardContent> - </Card> - <Card> - <CardHeader> - <div className="flex items-center justify-between"> - <div> - <CardTitle>Evaluation Criteria Item</CardTitle> - <CardDescription> - Set Evaluation Criteria Item. - </CardDescription> - </div> - <Button - type="button" - variant="outline" - size="sm" - className="ml-4" - onClick={() => - append({ - id: undefined, - detail: '', - scoreEquipShip: null, - scoreEquipMarine: null, - scoreBulkShip: null, - scoreBulkMarine: null, - }) - } - disabled={isPending} - > - <Plus className="w-4 h-4 mr-2" /> - New Item - </Button> - </div> - </CardHeader> - <CardContent> - <div className="space-y-4"> - {fields.map((field, index) => ( - <CriteriaDetailForm - key={field.id} - index={index} - form={form} - onRemove={() => remove(index)} - canRemove={fields.length > 1} - disabled={isPending} - /> - ))} - </div> - </CardContent> - </Card> - </div> - </ScrollArea> - <div className="flex justify-end gap-2 pt-4 border-t"> - <Button - type="button" - variant="outline" - onClick={() => onOpenChange(false)} - disabled={isPending} - > - Cancel - </Button> - <Button type="submit" disabled={isPending}> - {isPending - ? 'Saving...' - : isUpdateMode - ? 'Modify' - : 'Create'} - </Button> - </div> - </form> - </Form> - </SheetContent> - </Sheet> - ) -} +// return ( +// <Sheet open={open} onOpenChange={onOpenChange}> +// <SheetContent className="w-[900px] sm:max-w-[900px] overflow-y-auto"> +// <SheetHeader className="mb-4"> +// <SheetTitle className="font-bold"> +// {isUpdateMode ? '협력업체 평가 기준표 수정' : '새 협력업체 평가 기준표 생성'} +// </SheetTitle> +// <SheetDescription> +// {isUpdateMode ? '협력업체 평가 기준표의 정보를 수정합니다.' : '새로운 협력업체 평가 기준표를 생성합니다.'} +// </SheetDescription> +// </SheetHeader> +// <Form {...form}> +// <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6"> +// <ScrollArea className="h-[calc(100vh-200px)] pr-4"> +// <div className="space-y-6"> +// <Card> +// <CardHeader> +// <CardTitle>Criterion Info</CardTitle> +// </CardHeader> +// <CardContent className="space-y-4"> +// <FormField +// control={form.control} +// name="category" +// render={({ field }) => ( +// <FormItem> +// <FormLabel>평가부문</FormLabel> +// <FormControl> +// <Select onValueChange={field.onChange} value={field.value || ""}> +// <SelectTrigger> +// <SelectValue placeholder="선택" /> +// </SelectTrigger> +// <SelectContent> +// {REG_EVAL_CRITERIA_CATEGORY.map((option) => ( +// <SelectItem key={option.value} value={option.value}> +// {option.label} +// </SelectItem> +// ))} +// </SelectContent> +// </Select> +// </FormControl> +// <FormMessage /> +// </FormItem> +// )} +// /> +// <FormField +// control={form.control} +// name="category2" +// render={({ field }) => ( +// <FormItem> +// <FormLabel>점수구분</FormLabel> +// <FormControl> +// <Select onValueChange={field.onChange} value={field.value || ""}> +// <SelectTrigger> +// <SelectValue placeholder="선택" /> +// </SelectTrigger> +// <SelectContent> +// {REG_EVAL_CRITERIA_CATEGORY2.map((option) => ( +// <SelectItem key={option.value} value={option.value}> +// {option.label} +// </SelectItem> +// ))} +// </SelectContent> +// </Select> +// </FormControl> +// <FormMessage /> +// </FormItem> +// )} +// /> +// <FormField +// control={form.control} +// name="item" +// render={({ field }) => ( +// <FormItem> +// <FormLabel>항목</FormLabel> +// <FormControl> +// <Select onValueChange={field.onChange} value={field.value || ""}> +// <SelectTrigger> +// <SelectValue placeholder="선택" /> +// </SelectTrigger> +// <SelectContent> +// {REG_EVAL_CRITERIA_ITEM.map((option) => ( +// <SelectItem key={option.value} value={option.value}> +// {option.label} +// </SelectItem> +// ))} +// </SelectContent> +// </Select> +// </FormControl> +// <FormMessage /> +// </FormItem> +// )} +// /> +// <FormField +// control={form.control} +// name="classification" +// render={({ field }) => ( +// <FormItem> +// <FormLabel>구분</FormLabel> +// <FormControl> +// <Input placeholder="구분을 입력하세요." {...field} /> +// </FormControl> +// <FormMessage /> +// </FormItem> +// )} +// /> +// <FormField +// control={form.control} +// name="range" +// render={({ field }) => ( +// <FormItem> +// <FormLabel>범위</FormLabel> +// <FormControl> +// <Input +// placeholder="범위를 입력하세요." {...field} +// value={field.value ?? ''} +// /> +// </FormControl> +// <FormMessage /> +// </FormItem> +// )} +// /> +// <FormField +// control={form.control} +// name="remarks" +// render={({ field }) => ( +// <FormItem> +// <FormLabel>비고</FormLabel> +// <FormControl> +// <Textarea +// placeholder="비고를 입력하세요." +// {...field} +// value={field.value ?? ''} +// /> +// </FormControl> +// <FormMessage /> +// </FormItem> +// )} +// /> +// </CardContent> +// </Card> +// <Card> +// <CardHeader> +// <div className="flex items-center justify-between"> +// <div> +// <CardTitle>Evaluation Criteria Item</CardTitle> +// <CardDescription> +// Set Evaluation Criteria Item. +// </CardDescription> +// </div> +// <Button +// type="button" +// variant="outline" +// size="sm" +// className="ml-4" +// onClick={() => +// append({ +// id: undefined, +// detail: '', +// scoreEquipShip: null, +// scoreEquipMarine: null, +// scoreBulkShip: null, +// scoreBulkMarine: null, +// }) +// } +// disabled={isPending} +// > +// <Plus className="w-4 h-4 mr-2" /> +// New Item +// </Button> +// </div> +// </CardHeader> +// <CardContent> +// <div className="space-y-4"> +// {fields.map((field, index) => ( +// <CriteriaDetailForm +// key={field.id} +// index={index} +// form={form} +// onRemove={() => remove(index)} +// canRemove={fields.length > 1} +// disabled={isPending} +// /> +// ))} +// </div> +// </CardContent> +// </Card> +// </div> +// </ScrollArea> +// <div className="flex justify-end gap-2 pt-4 border-t"> +// <Button +// type="button" +// variant="outline" +// onClick={() => onOpenChange(false)} +// disabled={isPending} +// > +// Cancel +// </Button> +// <Button type="submit" disabled={isPending}> +// {isPending +// ? 'Saving...' +// : isUpdateMode +// ? 'Modify' +// : 'Create'} +// </Button> +// </div> +// </form> +// </Form> +// </SheetContent> +// </Sheet> +// ) +// } -// ---------------------------------------------------------------------------------------------------- +// // ---------------------------------------------------------------------------------------------------- -/* EXPORT */ -export default RegEvalCriteriaFormSheet;
\ No newline at end of file +// /* EXPORT */ +// export default RegEvalCriteriaFormSheet;
\ No newline at end of file diff --git a/lib/evaluation-criteria/table/reg-eval-criteria-table-toolbar-actions.tsx b/lib/evaluation-criteria/table/reg-eval-criteria-table-toolbar-actions.tsx index f066fa92..7594f07f 100644 --- a/lib/evaluation-criteria/table/reg-eval-criteria-table-toolbar-actions.tsx +++ b/lib/evaluation-criteria/table/reg-eval-criteria-table-toolbar-actions.tsx @@ -14,11 +14,9 @@ import { } from '@/components/ui/alert-dialog'; import { Button } from '@/components/ui/button'; import { Download, Plus, Trash2, Upload } from 'lucide-react'; -import { exportRegEvalCriteriaToExcel, exportRegEvalCriteriaTemplate } from '../excel/reg-eval-criteria-excel-export'; -import { importRegEvalCriteriaExcel } from '../excel/reg-eval-criteria-excel-import'; -import { removeRegEvalCriteria } from '../service'; +import { removeRegEvalCriteria, generateRegEvalCriteriaTemplate, generateRegEvalCriteriaExcel, importRegEvalCriteriaExcel } from '../service'; import { toast } from 'sonner'; -import { type RegEvalCriteriaView } from '@/db/schema'; +import { type RegEvalCriteria } from '@/db/schema'; import { type Table } from '@tanstack/react-table'; import { ChangeEvent, useMemo, useRef, useState } from 'react'; @@ -26,7 +24,7 @@ import { ChangeEvent, useMemo, useRef, useState } from 'react'; /* TYPES */ interface RegEvalCriteriaTableToolbarActionsProps { - table: Table<RegEvalCriteriaView>, + table: Table<RegEvalCriteria>, onCreateCriteria: () => void, onRefresh: () => void, } @@ -40,7 +38,7 @@ function RegEvalCriteriaTableToolbarActions(props: RegEvalCriteriaTableToolbarAc const selectedRows = table.getFilteredSelectedRowModel().rows; const hasSelection = selectedRows.length > 0; const selectedIds = useMemo(() => { - return [...new Set(selectedRows.map(row => row.original.criteriaId))]; + return [...new Set(selectedRows.map(row => row.original.id))]; }, [selectedRows]); const fileInputRef = useRef<HTMLInputElement>(null); @@ -117,14 +115,20 @@ function RegEvalCriteriaTableToolbarActions(props: RegEvalCriteriaTableToolbarAc } }; - // Excel Export + // Excel Export (DB에서 1:n 관계 포함하여 전체 데이터 내보내기) const handleExport = async () => { try { - await exportRegEvalCriteriaToExcel(table, { - filename: 'Regular_Evaluation_Criteria', - excludeColumns: ['select', 'actions'], + const buffer = await generateRegEvalCriteriaExcel(); + const blob = new Blob([buffer], { + type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", }); - toast.success('Excel 파일이 다운로드되었습니다.'); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = "평가_기준_전체.xlsx"; + link.click(); + URL.revokeObjectURL(url); + toast.success('Excel 파일이 다운로드되었습니다. (모든 평가내용 포함)'); } catch (error) { console.error('Error in Exporting to Excel: ', error); toast.error('Excel 내보내기 중 오류가 발생했습니다.'); @@ -134,7 +138,16 @@ function RegEvalCriteriaTableToolbarActions(props: RegEvalCriteriaTableToolbarAc // Excel Template Download const handleTemplateDownload = async () => { try { - await exportRegEvalCriteriaTemplate(); + const buffer = await generateRegEvalCriteriaTemplate(); + const blob = new Blob([buffer], { + type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = "평가_기준_템플릿.xlsx"; + link.click(); + URL.revokeObjectURL(url); toast.success('템플릿 파일이 다운로드되었습니다.'); } catch (error) { console.error('Error in Template Download: ', error); diff --git a/lib/evaluation-criteria/table/reg-eval-criteria-update-sheet.tsx b/lib/evaluation-criteria/table/reg-eval-criteria-update-sheet.tsx index bbf4f36d..f48fdf2e 100644 --- a/lib/evaluation-criteria/table/reg-eval-criteria-update-sheet.tsx +++ b/lib/evaluation-criteria/table/reg-eval-criteria-update-sheet.tsx @@ -13,7 +13,8 @@ import { } from '@/components/ui/card'; import { getRegEvalCriteriaWithDetails, - modifyRegEvalCriteriaWithDetails, + modifyRegEvalCriteriaFixed, + modifyRegEvalCriteriaVariable, } from '../service'; import { Form, @@ -64,7 +65,6 @@ const regEvalCriteriaFormSchema = z.object({ classification: z.string().min(1, '구분은 필수 항목입니다.'), range: z.string().nullable().optional(), remarks: z.string().nullable().optional(), - // 새로운 필드들 추가 scoreType: z.enum(['fixed', 'variable']).default('fixed'), variableScoreMin: z.coerce.number().nullable().optional(), variableScoreMax: z.coerce.number().nullable().optional(), @@ -72,13 +72,13 @@ const regEvalCriteriaFormSchema = z.object({ criteriaDetails: z.array( z.object({ id: z.number().optional(), - detail: z.string().min(1, '평가내용은 필수 항목입니다.'), + detail: z.string().optional(), scoreEquipShip: z.coerce.number().nullable().optional(), scoreEquipMarine: z.coerce.number().nullable().optional(), scoreBulkShip: z.coerce.number().nullable().optional(), scoreBulkMarine: z.coerce.number().nullable().optional(), }) - ).min(1, '최소 1개의 평가 내용이 필요합니다.'), + ).optional(), }); type RegEvalCriteriaFormData = z.infer<typeof regEvalCriteriaFormSchema>; @@ -341,38 +341,86 @@ function RegEvalCriteriaUpdateSheet({ }, [open, criteriaData, form]); const onSubmit = async (data: RegEvalCriteriaFormData) => { - startTransition(async () => { try { - const criteriaDataToUpdate = { - category: data.category, - category2: data.category2, - item: data.item, - classification: data.classification, - range: data.range, - remarks: data.remarks, - scoreType: data.scoreType, - variableScoreMin: data.variableScoreMin != null - ? String(data.variableScoreMin) : null, - variableScoreMax: data.variableScoreMax != null - ? String(data.variableScoreMax) : null, - variableScoreUnit: data.variableScoreUnit, - }; - - const detailList = data.criteriaDetails.map((detailItem) => ({ - id: detailItem.id, - detail: detailItem.detail, - scoreEquipShip: detailItem.scoreEquipShip != null - ? String(detailItem.scoreEquipShip) : null, - scoreEquipMarine: detailItem.scoreEquipMarine != null - ? String(detailItem.scoreEquipMarine) : null, - scoreBulkShip: detailItem.scoreBulkShip != null - ? String(detailItem.scoreBulkShip) : null, - scoreBulkMarine: detailItem.scoreBulkMarine != null - ? String(detailItem.scoreBulkMarine) : null, - })); - - await modifyRegEvalCriteriaWithDetails(criteriaData.id, criteriaDataToUpdate, detailList); + if (data.scoreType === 'fixed') { + // 고정 점수 검증 + if (!data.criteriaDetails || data.criteriaDetails.length === 0) { + toast.error('고정 점수 유형에서는 최소 1개의 평가내용이 필요합니다.'); + return; + } + + // 평가내용이 비어있는지 확인 + const hasEmptyDetail = data.criteriaDetails.some((detail: NonNullable<RegEvalCriteriaFormData['criteriaDetails']>[0]) => + !detail.detail || (detail.detail && detail.detail.trim() === '') + ); + if (hasEmptyDetail) { + toast.error('평가내용을 입력해주세요.'); + return; + } + + const criteriaDataToUpdate = { + category: data.category, + category2: data.category2, + item: data.item, + classification: data.classification, + range: data.range || null, + remarks: data.remarks || null, + scoreType: data.scoreType, + variableScoreMin: null, // 고정 점수에서는 null + variableScoreMax: null, // 고정 점수에서는 null + variableScoreUnit: null, // 고정 점수에서는 null + }; + + const detailList = data.criteriaDetails.map((detailItem: NonNullable<RegEvalCriteriaFormData['criteriaDetails']>[0]) => ({ + id: detailItem.id, + detail: detailItem.detail?.trim() || '', + scoreEquipShip: detailItem.scoreEquipShip != null ? String(detailItem.scoreEquipShip) : null, + scoreEquipMarine: detailItem.scoreEquipMarine != null ? String(detailItem.scoreEquipMarine) : null, + scoreBulkShip: detailItem.scoreBulkShip != null ? String(detailItem.scoreBulkShip) : null, + scoreBulkMarine: detailItem.scoreBulkMarine != null ? String(detailItem.scoreBulkMarine) : null, + })); + + await modifyRegEvalCriteriaFixed(criteriaData.id, criteriaDataToUpdate, detailList); + + } else if (data.scoreType === 'variable') { + // 변동 점수 검증 + if (data.variableScoreMin == null) { + toast.error('변동 점수 유형에서는 최소 점수가 필수입니다.'); + return; + } + if (data.variableScoreMax == null) { + toast.error('변동 점수 유형에서는 최대 점수가 필수입니다.'); + return; + } + if (!data.variableScoreUnit || data.variableScoreUnit.trim() === '') { + toast.error('변동 점수 유형에서는 단위가 필수입니다.'); + return; + } + if (Number(data.variableScoreMin) >= Number(data.variableScoreMax)) { + toast.error('최소 점수는 최대 점수보다 작아야 합니다.'); + return; + } + + const criteriaDataToUpdate = { + category: data.category, + category2: data.category2, + item: data.item, + classification: data.classification, + range: data.range || null, + remarks: data.remarks || null, + scoreType: data.scoreType, + variableScoreMin: String(data.variableScoreMin), + variableScoreMax: String(data.variableScoreMax), + variableScoreUnit: data.variableScoreUnit.trim(), + }; + + await modifyRegEvalCriteriaVariable(criteriaData.id, criteriaDataToUpdate); + } else { + toast.error('올바른 점수 유형을 선택해주세요.'); + return; + } + toast.success('평가 기준이 수정되었습니다.'); onSuccess(); onOpenChange(false); @@ -490,30 +538,6 @@ function RegEvalCriteriaUpdateSheet({ /> <FormField control={form.control} - name="scoreType" - render={({ field }) => ( - <FormItem> - <FormLabel>점수유형</FormLabel> - <FormControl> - <Select onValueChange={field.onChange} value={field.value}> - <SelectTrigger> - <SelectValue placeholder="선택" /> - </SelectTrigger> - <SelectContent> - <SelectItem value="fixed">고정점수</SelectItem> - <SelectItem value="variable">변동점수</SelectItem> - </SelectContent> - </Select> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - </div> - - <div className="grid grid-cols-2 gap-4"> - <FormField - control={form.control} name="classification" render={({ field }) => ( <FormItem> @@ -525,6 +549,7 @@ function RegEvalCriteriaUpdateSheet({ </FormItem> )} /> + </div> <FormField control={form.control} name="range" @@ -541,16 +566,55 @@ function RegEvalCriteriaUpdateSheet({ </FormItem> )} /> - </div> - {/* 변동점수 설정 */} + + + <FormField + control={form.control} + name="remarks" + render={({ field }) => ( + <FormItem> + <FormLabel>비고</FormLabel> + <FormControl> + <Textarea + placeholder="비고를 입력하세요." + {...field} + value={field.value ?? ''} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name="scoreType" + render={({ field }) => ( + <FormItem> + <FormLabel>점수유형</FormLabel> + <FormControl> + <Select onValueChange={field.onChange} value={field.value}> + <SelectTrigger> + <SelectValue placeholder="선택" /> + </SelectTrigger> + <SelectContent> + <SelectItem value="fixed">고정점수</SelectItem> + <SelectItem value="variable">변동점수</SelectItem> + </SelectContent> + </Select> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + {/* 변동점수 설정 */} {scoreType === 'variable' && ( <> <Separator /> <div className="space-y-4"> <h4 className="font-medium">변동점수 설정</h4> <div className="grid grid-cols-3 gap-4"> - <FormField + <FormField control={form.control} name="variableScoreMin" render={({ field }) => ( @@ -559,8 +623,9 @@ function RegEvalCriteriaUpdateSheet({ <FormControl> <Input type="number" - step="0.01" - placeholder="0.00" + min="0" + step="1" + placeholder="점수 입력 (0 이상)" {...field} value={field.value ?? ''} /> @@ -578,8 +643,9 @@ function RegEvalCriteriaUpdateSheet({ <FormControl> <Input type="number" - step="0.01" - placeholder="0.00" + min="0" + step="1" + placeholder="점수 입력 (0 이상)" {...field} value={field.value ?? ''} /> @@ -596,7 +662,7 @@ function RegEvalCriteriaUpdateSheet({ <FormLabel>점수단위</FormLabel> <FormControl> <Input - placeholder="예: 점, %" + placeholder="예: 감점, 가점" {...field} value={field.value ?? ''} /> @@ -609,27 +675,9 @@ function RegEvalCriteriaUpdateSheet({ </div> </> )} - - <FormField - control={form.control} - name="remarks" - render={({ field }) => ( - <FormItem> - <FormLabel>비고</FormLabel> - <FormControl> - <Textarea - placeholder="비고를 입력하세요." - {...field} - value={field.value ?? ''} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> </CardContent> </Card> - + {scoreType === 'fixed' && ( <Card> <CardHeader> <div className="flex items-center justify-between"> @@ -638,8 +686,7 @@ function RegEvalCriteriaUpdateSheet({ <CardDescription> {scoreType === 'fixed' ? '각 평가 옵션별 점수를 설정하세요.' - : '평가 옵션을 설정하세요. (점수는 변동점수 설정을 따릅니다.)' - } + : '평가 옵션을 설정하세요. (점수는 변동점수 설정을 따릅니다.)'} </CardDescription> </div> <Button @@ -680,6 +727,18 @@ function RegEvalCriteriaUpdateSheet({ </div> </CardContent> </Card> + )} + {scoreType === 'variable' && ( + <Card> + <CardHeader> + <CardTitle>변동 점수 설정</CardTitle> + <CardDescription> + 변동 점수 유형에서는 개별 평가 옵션을 설정할 수 없습니다. + 최소/최대 점수와 단위를 설정하여 점수 범위를 정의하세요. + </CardDescription> + </CardHeader> + </Card> + )} </div> </div> |
