summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/evaluation-criteria/excel/reg-eval-criteria-excel-export.ts303
-rw-r--r--lib/evaluation-criteria/excel/reg-eval-criteria-excel-import.ts417
-rw-r--r--lib/evaluation-criteria/service.ts684
-rw-r--r--lib/evaluation-criteria/table/reg-eval-criteria-columns.tsx5
-rw-r--r--lib/evaluation-criteria/table/reg-eval-criteria-create-dialog.tsx680
-rw-r--r--lib/evaluation-criteria/table/reg-eval-criteria-details-sheet.tsx188
-rw-r--r--lib/evaluation-criteria/table/reg-eval-criteria-form-sheet.tsx1138
-rw-r--r--lib/evaluation-criteria/table/reg-eval-criteria-table-toolbar-actions.tsx37
-rw-r--r--lib/evaluation-criteria/table/reg-eval-criteria-update-sheet.tsx233
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>