From e9897d416b3e7327bbd4d4aef887eee37751ae82 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 27 Jun 2025 01:16:20 +0000 Subject: (대표님) 20250627 오전 10시 작업사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/b-rfq/service.ts | 2 + lib/evaluation-criteria/service.ts | 389 +++++++++- .../table/reg-eval-criteria-columns.tsx | 69 +- .../table/reg-eval-criteria-create-dialog.tsx | 536 ++++++++++++++ .../table/reg-eval-criteria-delete-dialog.tsx | 8 +- .../reg-eval-criteria-table-toolbar-actions.tsx | 80 +- .../table/reg-eval-criteria-table.tsx | 42 +- .../table/reg-eval-criteria-update-sheet.tsx | 554 ++++++++++++++ lib/evaluation-criteria/validations.ts | 5 +- lib/evaluation-target-list/service.ts | 22 +- .../table/evaluation-target-table.tsx | 109 ++- .../table/evaluation-targets-columns.tsx | 4 +- .../table/evaluation-targets-toolbar-actions.tsx | 4 +- lib/evaluation/service.ts | 266 ++++++- lib/evaluation/table/evaluation-columns.tsx | 341 ++++++--- lib/evaluation/table/evaluation-table.tsx | 24 +- .../table/periodic-evaluation-action-dialogs.tsx | 373 ++++++++++ .../table/periodic-evaluations-toolbar-actions.tsx | 218 ++++++ lib/password-policy/service.ts | 225 ++++++ lib/techsales-rfq/service.ts | 2 +- lib/users/auth/partners-auth.ts | 374 ++++++++++ lib/users/auth/passwordUtil.ts | 608 +++++++++++++++ lib/users/auth/validataions-password.ts | 230 ++++++ lib/users/auth/verifyCredentails.ts | 620 ++++++++++++++++ lib/users/service.ts | 70 +- lib/users/verifyOtp.ts | 26 - lib/vendor-document-list/dolce-upload-service.ts | 18 +- lib/vendor-document-list/import-service.ts | 816 +++++++++++++++++++-- lib/vendor-document-list/service.ts | 18 +- .../ship/enhanced-doc-table-columns.tsx | 3 + .../ship/import-from-dolce-button.tsx | 82 ++- .../table/evaluation-submissions-table-columns.tsx | 94 ++- lib/vendor-users/repository.ts | 172 +++++ lib/vendor-users/service.ts | 491 +++++++++++++ lib/vendor-users/table/add-ausers-dialog.tsx | 291 ++++++++ lib/vendor-users/table/ausers-table-columns.tsx | 228 ++++++ .../table/ausers-table-floating-bar.tsx | 302 ++++++++ .../table/ausers-table-toolbar-actions.tsx | 118 +++ lib/vendor-users/table/ausers-table.tsx | 163 ++++ lib/vendor-users/table/delete-ausers-dialog.tsx | 149 ++++ lib/vendor-users/table/update-auser-sheet.tsx | 273 +++++++ lib/vendor-users/validations.ts | 83 +++ lib/vendors/repository.ts | 2 +- lib/vendors/service.ts | 45 +- lib/vendors/validations.ts | 146 +++- 45 files changed, 8291 insertions(+), 404 deletions(-) create mode 100644 lib/evaluation-criteria/table/reg-eval-criteria-create-dialog.tsx create mode 100644 lib/evaluation-criteria/table/reg-eval-criteria-update-sheet.tsx create mode 100644 lib/evaluation/table/periodic-evaluation-action-dialogs.tsx create mode 100644 lib/evaluation/table/periodic-evaluations-toolbar-actions.tsx create mode 100644 lib/password-policy/service.ts create mode 100644 lib/users/auth/partners-auth.ts create mode 100644 lib/users/auth/passwordUtil.ts create mode 100644 lib/users/auth/validataions-password.ts create mode 100644 lib/users/auth/verifyCredentails.ts create mode 100644 lib/vendor-users/repository.ts create mode 100644 lib/vendor-users/service.ts create mode 100644 lib/vendor-users/table/add-ausers-dialog.tsx create mode 100644 lib/vendor-users/table/ausers-table-columns.tsx create mode 100644 lib/vendor-users/table/ausers-table-floating-bar.tsx create mode 100644 lib/vendor-users/table/ausers-table-toolbar-actions.tsx create mode 100644 lib/vendor-users/table/ausers-table.tsx create mode 100644 lib/vendor-users/table/delete-ausers-dialog.tsx create mode 100644 lib/vendor-users/table/update-auser-sheet.tsx create mode 100644 lib/vendor-users/validations.ts (limited to 'lib') diff --git a/lib/b-rfq/service.ts b/lib/b-rfq/service.ts index 83f0bbb5..8aa79084 100644 --- a/lib/b-rfq/service.ts +++ b/lib/b-rfq/service.ts @@ -40,6 +40,8 @@ export async function getRFQDashboard(input: GetRFQDashboardSchema) { const rfqFilterMapping = createRFQFilterMapping(); const joinedTables = getRFQJoinedTables(); + console.log(input, "견적 인풋") + // 1) 고급 필터 조건 let advancedWhere: SQL | undefined = undefined; if (input.filters && input.filters.length > 0) { diff --git a/lib/evaluation-criteria/service.ts b/lib/evaluation-criteria/service.ts index ec23c9e4..5d5e5b8f 100644 --- a/lib/evaluation-criteria/service.ts +++ b/lib/evaluation-criteria/service.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + 'use server'; /* IMPORT */ @@ -20,18 +22,61 @@ import { updateRegEvalCriteriaDetails, } from './repository'; import db from '@/db/db'; +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, regEvalCriteriaView, type NewRegEvalCriteria, type NewRegEvalCriteriaDetails, type RegEvalCriteria, type RegEvalCriteriaDetails, + type RegEvalCriteriaView, } from '@/db/schema'; import { type GetRegEvalCriteriaSchema } from './validations'; // ---------------------------------------------------------------------------------------------------- +/* 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 = 2; +const DATA_START_ROW_INDEX = 3; +const EXCEL_HEADERS = [ + 'Category', + 'Score Category', + 'Item', + 'Classification', + 'Range', + 'Detail', + 'Remarks', + 'ID', + 'Criteria ID', + 'Order Index', + 'Equipment-Shipbuilding Score', + 'Equipment-Marine Engineering Score', + 'Bulk-Shipbuilding Score', + 'Bulk-Marine Engineering Score', +]; + +// ---------------------------------------------------------------------------------------------------- + /* FUNCTION FOR GETTING CRITERIA */ async function getRegEvalCriteria(input: GetRegEvalCriteriaSchema) { try { @@ -109,7 +154,7 @@ async function createRegEvalCriteriaWithDetails( const newDetailList = detailList.map((detailItem, index) => ({ ...detailItem, criteriaId, - orderIndex: index, + orderIndex: detailItem.orderIndex || index, })); const criteriaDetails: NewRegEvalCriteriaDetails[] = []; @@ -136,10 +181,6 @@ async function modifyRegEvalCriteriaWithDetails( try { return await db.transaction(async (tx) => { const modifiedCriteria = await updateRegEvalCriteria(tx, id, criteriaData); - - console.log('here!'); - console.log(detailList); - const originCriteria = await getRegEvalCriteriaWithDetails(id); const originCriteriaDetails = originCriteria?.criteriaDetails || []; const detailIdList = detailList @@ -167,7 +208,7 @@ async function modifyRegEvalCriteriaWithDetails( ...detailItem, criteriaId: id, detail: detailItem.detail!, - orderIndex: idx, + orderIndex: detailItem.orderIndex || idx, }; const insertedDetail = await insertRegEvalCriteriaDetails(tx, newDetailItem); criteriaDetails.push(insertedDetail); @@ -210,12 +251,348 @@ async function removeRegEvalCriteriaDetails(id: number) { // ---------------------------------------------------------------------------------------------------- +/* FUNCTION FOR IMPORTING EXCEL FILES */ +async function importRegEvalCriteriaExcel(file: File): Promise { + 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 || headerRow.cellCount < EXCEL_HEADERS.length || !Array.isArray(headerRow.values)) { + throw new Error('Excel 파일의 워크시트에서 유효한 헤더 행을 찾지 못했습니다.'); + } + const headerValues = headerRow?.values?.slice(1); + const isHeaderMatched = EXCEL_HEADERS.every((header, idx) => { + const actualHeader = (headerValues[idx] ?? '').toString().trim(); + return actualHeader === header; + }); + if (!isHeaderMatched) { + throw new Error('Excel 파일의 워크시트에서 유효한 헤더 행을 찾지 못했습니다.'); + } + + const columnIndexMap = new Map(); + headerRow.eachCell((cell, colIndex) => { + if (typeof cell.value === 'string') { + columnIndexMap.set(cell.value.trim(), colIndex); + } + }); + + const columnToFieldMap = new Map(); + regEvalCriteriaColumnsConfig.forEach((cfg) => { + if (!cfg.excelHeader) { + return; + } + const colIndex = columnIndexMap.get(cfg.excelHeader.trim()); + if (colIndex !== undefined) { + columnToFieldMap.set(colIndex, cfg.id); + } + }); + const errorRows: { rowIndex: number; message: string }[] = []; + const rowDataList: ExcelRowData[] = []; + const criteriaMap = new Map(); + + 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; + columnToFieldMap.forEach((fieldId, colIdx) => { + const cellValue = row.getCell(colIdx).value; + rowFields[fieldId] = 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, + }); + }); + // console.log('원본 데이터: '); + // console.dir(rowDataList, { depth: null }); + + 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(existingIds); + + // console.log('기존 데이터: '); + // console.dir(existingData, { depth: null }); + + const createList: { + criteriaData: NewRegEvalCriteria, + detailList: Omit[], + }[] = []; + const updateList: { + id: number, + criteriaData: Partial, + detailList: Partial[], + }[] = []; + 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})가 있습니다.`); + } + } + console.log('생성: '); + console.dir(createList, { depth: null }); + console.log('업뎃: '); + console.dir(updateList, { depth: null }); + console.log('삭제: ', deleteIdList); + + 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, + }; + } +} + +// ---------------------------------------------------------------------------------------------------- + /* EXPORT */ export { createRegEvalCriteriaWithDetails, modifyRegEvalCriteriaWithDetails, getRegEvalCriteria, getRegEvalCriteriaWithDetails, + importRegEvalCriteriaExcel, removeRegEvalCriteria, removeRegEvalCriteriaDetails, }; \ 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 7367fabb..77e6118d 100644 --- a/lib/evaluation-criteria/table/reg-eval-criteria-columns.tsx +++ b/lib/evaluation-criteria/table/reg-eval-criteria-columns.tsx @@ -15,8 +15,8 @@ import { import { PenToolIcon, TrashIcon } from 'lucide-react'; import { REG_EVAL_CRITERIA_CATEGORY, - REG_EVAL_CRITERIA_ITEM, REG_EVAL_CRITERIA_CATEGORY2, + REG_EVAL_CRITERIA_ITEM, type RegEvalCriteriaView, } from '@/db/schema'; import { type ColumnDef } from '@tanstack/react-table'; @@ -85,12 +85,12 @@ function getColumns({ setRowAction }: GetColumnsProps): ColumnDef ( ), cell: ({ row }) => { - const value = row.getValue('scoreCategory'); + const value = row.getValue('category2'); const label = REG_EVAL_CRITERIA_CATEGORY2.find(item => item.value === value)?.label ?? value; return ( @@ -250,7 +250,7 @@ function getColumns({ setRowAction }: GetColumnsProps): ColumnDef