From 15c3ae6536c264db0508e4fc4aaa59c3e6d1af30 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Tue, 15 Jul 2025 00:50:39 +0000 Subject: (대표님) 기본계약 및 정기평가 작업사항, OCR 변경사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../add-basic-contract-template-dialog.tsx | 387 +++++++++++++------ .../template/basic-contract-template-columns.tsx | 423 ++++++++++++++++---- .../template/update-basicContract-sheet.tsx | 426 ++++++++++++++------- .../table/esg-evaluations-table-columns.tsx | 27 +- lib/evaluation-criteria/service.ts | 2 +- .../table/reg-eval-criteria-columns.tsx | 7 +- lib/evaluation-target-list/service.ts | 30 +- lib/forms/services.ts | 67 +++- .../table/general-check-table-columns.tsx | 56 +-- lib/sedp/sync-form.ts | 12 +- lib/sedp/sync-tag-types.ts | 3 +- lib/techsales-rfq/service.ts | 2 +- lib/welding/service.ts | 45 ++- lib/welding/table/delete-ocr-rows-dialog.tsx | 151 ++++++++ lib/welding/table/exporft-ocr-data.ts | 4 + lib/welding/table/ocr-table-columns.tsx | 4 +- lib/welding/table/ocr-table-toolbar-actions.tsx | 22 +- lib/welding/table/ocr-table.tsx | 12 + 18 files changed, 1290 insertions(+), 390 deletions(-) create mode 100644 lib/welding/table/delete-ocr-rows-dialog.tsx (limited to 'lib') diff --git a/lib/basic-contract/template/add-basic-contract-template-dialog.tsx b/lib/basic-contract/template/add-basic-contract-template-dialog.tsx index cf0986f0..3a83d50f 100644 --- a/lib/basic-contract/template/add-basic-contract-template-dialog.tsx +++ b/lib/basic-contract/template/add-basic-contract-template-dialog.tsx @@ -18,6 +18,8 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Switch } from "@/components/ui/switch"; import { Select, SelectContent, @@ -34,17 +36,31 @@ import { DropzoneInput } from "@/components/ui/dropzone"; import { Progress } from "@/components/ui/progress"; -import { useRouter } from "next/navigation" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Separator } from "@/components/ui/separator"; +import { useRouter } from "next/navigation"; +import { BUSINESS_UNITS } from "@/config/basicContractColumnsConfig"; -// 유효기간 필드가 추가된 계약서 템플릿 스키마 정의 +// 업데이트된 계약서 템플릿 스키마 정의 const templateFormSchema = z.object({ + templateCode: z.string() + .min(1, "템플릿 코드는 필수입니다.") + .max(50, "템플릿 코드는 50자 이하여야 합니다.") + .regex(/^[A-Z0-9_-]+$/, "템플릿 코드는 영문 대문자, 숫자, '_', '-'만 사용 가능합니다."), templateName: z.string().min(1, "템플릿 이름은 필수입니다."), - validityPeriod: z.coerce - .number({ invalid_type_error: "유효기간은 숫자여야 합니다." }) - .int("유효기간은 정수여야 합니다.") - .min(1, "유효기간은 최소 1개월 이상이어야 합니다.") - .max(120, "유효기간은 최대 120개월(10년)을 초과할 수 없습니다.") - .default(12), + revision: z.coerce.number().int().min(1).default(1), + legalReviewRequired: z.boolean().default(false), + + // 적용 범위 + shipBuildingApplicable: z.boolean().default(false), + windApplicable: z.boolean().default(false), + pcApplicable: z.boolean().default(false), + nbApplicable: z.boolean().default(false), + rcApplicable: z.boolean().default(false), + gyApplicable: z.boolean().default(false), + sysApplicable: z.boolean().default(false), + infraApplicable: z.boolean().default(false), + file: z .instanceof(File, { message: "파일을 업로드해주세요." }) .refine((file) => file.size <= 100 * 1024 * 1024, { @@ -55,6 +71,15 @@ const templateFormSchema = z.object({ { message: "PDF 파일만 업로드 가능합니다." } ), status: z.enum(["ACTIVE", "DISPOSED"]).default("ACTIVE"), +}).refine((data) => { + // 적어도 하나의 적용 범위는 선택되어야 함 + const hasAnyScope = BUSINESS_UNITS.some(unit => + data[unit.key as keyof typeof data] as boolean + ); + return hasAnyScope; +}, { + message: "적어도 하나의 적용 범위를 선택해야 합니다.", + path: ["shipBuildingApplicable"], // 에러를 첫 번째 체크박스에 표시 }); type TemplateFormValues = z.infer; @@ -65,12 +90,22 @@ export function AddTemplateDialog() { const [selectedFile, setSelectedFile] = React.useState(null); const [uploadProgress, setUploadProgress] = React.useState(0); const [showProgress, setShowProgress] = React.useState(false); - const router = useRouter() + const router = useRouter(); // 기본값 설정 const defaultValues: Partial = { + templateCode: "", templateName: "", - validityPeriod: 12, // 기본값 1년 + revision: 1, + legalReviewRequired: false, + shipBuildingApplicable: false, + windApplicable: false, + pcApplicable: false, + nbApplicable: false, + rcApplicable: false, + gyApplicable: false, + sysApplicable: false, + infraApplicable: false, status: "ACTIVE", }; @@ -81,11 +116,6 @@ export function AddTemplateDialog() { mode: "onChange", }); - // 폼 값 감시 - const templateName = form.watch("templateName"); - const validityPeriod = form.watch("validityPeriod"); - const file = form.watch("file"); - // 파일 선택 핸들러 const handleFileChange = (files: File[]) => { if (files.length > 0) { @@ -95,6 +125,13 @@ export function AddTemplateDialog() { } }; + // 모든 적용 범위 선택/해제 + const handleSelectAllScopes = (checked: boolean) => { + BUSINESS_UNITS.forEach(unit => { + form.setValue(unit.key as keyof TemplateFormValues, checked); + }); + }; + // 청크 크기 설정 (1MB) const CHUNK_SIZE = 1 * 1024 * 1024; @@ -161,15 +198,25 @@ export function AddTemplateDialog() { throw new Error("파일 업로드에 실패했습니다."); } - // 메타데이터 저장 + // 메타데이터 저장 (업데이트된 필드들 포함) const saveResponse = await fetch('/api/upload/basicContract/complete', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ + templateCode: formData.templateCode, templateName: formData.templateName, - validityPeriod: formData.validityPeriod, // 유효기간 추가 + revision: formData.revision, + legalReviewRequired: formData.legalReviewRequired, + shipBuildingApplicable: formData.shipBuildingApplicable, + windApplicable: formData.windApplicable, + pcApplicable: formData.pcApplicable, + nbApplicable: formData.nbApplicable, + rcApplicable: formData.rcApplicable, + gyApplicable: formData.gyApplicable, + sysApplicable: formData.sysApplicable, + infraApplicable: formData.infraApplicable, status: formData.status, fileName: uploadResult.fileName, filePath: uploadResult.filePath, @@ -215,15 +262,10 @@ export function AddTemplateDialog() { setOpen(nextOpen); } - // 유효기간 선택 옵션 - const validityOptions = [ - { value: "3", label: "3개월" }, - { value: "6", label: "6개월" }, - { value: "12", label: "1년" }, - { value: "24", label: "2년" }, - { value: "36", label: "3년" }, - { value: "60", label: "5년" }, - ]; + // 현재 선택된 적용 범위 수 + const selectedScopesCount = BUSINESS_UNITS.filter(unit => + form.watch(unit.key as keyof TemplateFormValues) + ).length; return ( @@ -232,108 +274,215 @@ export function AddTemplateDialog() { 템플릿 추가 - + 새 기본계약서 템플릿 추가 - 템플릿 이름을 입력하고 계약서 파일을 업로드하세요. + 템플릿 정보를 입력하고 계약서 파일을 업로드하세요. * 표시된 항목은 필수 입력사항입니다.
- - ( - - - 템플릿 이름 * - - - - - - - )} - /> + + {/* 기본 정보 */} + + + 기본 정보 + + +
+ ( + + + 템플릿 코드 * + + + field.onChange(e.target.value.toUpperCase())} + /> + + + 영문 대문자, 숫자, '_', '-'만 사용 가능 + + + + )} + /> - ( - - - 계약 유효기간 * - - - - 계약서의 유효 기간을 설정합니다. 이 기간이 지나면 재계약이 필요합니다. - - - - )} - /> + ( + + 리비전 + + field.onChange(parseInt(e.target.value) || 1)} + /> + + + 템플릿 버전 (기본값: 1) + + + + )} + /> +
- ( - - - 계약서 파일 * - - - - - - - {selectedFile ? selectedFile.name : "PDF 파일을 여기에 드래그하세요"} - - - {selectedFile - ? `파일 크기: ${(selectedFile.size / (1024 * 1024)).toFixed(2)} MB` - : "또는 클릭하여 PDF 파일을 선택하세요 (최대 100MB)"} - - - - - - - - )} - /> - - {showProgress && ( -
-
- 업로드 진행률 - {uploadProgress}% + ( + + + 템플릿 이름 * + + + + + + + )} + /> + + ( + +
+ 법무검토 필요 + + 법무팀 검토가 필요한 템플릿인지 설정 + +
+ + + +
+ )} + /> + + + + {/* 적용 범위 */} + + + 적용 범위 + + 이 템플릿이 적용될 사업부를 선택하세요. ({selectedScopesCount}개 선택됨) + + + +
+ +
- -
- )} + + + +
+ {BUSINESS_UNITS.map((unit) => ( + ( + + + + +
+ + {unit.label} + +
+
+ )} + /> + ))} +
+ + {form.formState.errors.shipBuildingApplicable && ( +

+ {form.formState.errors.shipBuildingApplicable.message} +

+ )} + + + + {/* 파일 업로드 */} + + + 파일 업로드 + + + ( + + + 계약서 파일 * + + + + + + + {selectedFile ? selectedFile.name : "PDF 파일을 여기에 드래그하세요"} + + + {selectedFile + ? `파일 크기: ${(selectedFile.size / (1024 * 1024)).toFixed(2)} MB` + : "또는 클릭하여 PDF 파일을 선택하세요 (최대 100MB)"} + + + + + + + + )} + /> + + {showProgress && ( +
+
+ 업로드 진행률 + {uploadProgress}% +
+ +
+ )} +
+
diff --git a/lib/basic-contract/template/basic-contract-template-columns.tsx b/lib/basic-contract/template/basic-contract-template-columns.tsx index 5f4433d1..3be46791 100644 --- a/lib/basic-contract/template/basic-contract-template-columns.tsx +++ b/lib/basic-contract/template/basic-contract-template-columns.tsx @@ -3,7 +3,7 @@ import * as React from "react" import { type DataTableRowAction } from "@/types/table" import { type ColumnDef } from "@tanstack/react-table" -import { Download, Ellipsis, Paperclip } from "lucide-react" +import { Download, Ellipsis, Paperclip, CheckCircle, XCircle } from "lucide-react" import { toast } from "sonner" import { getErrorMessage } from "@/lib/handle-error" @@ -24,9 +24,10 @@ import { DropdownMenuSubTrigger, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" -import { basicContractTemplateColumnsConfig } from "@/config/basicContractColumnsConfig" +import { scopeHelpers } from "@/config/basicContractColumnsConfig" import { BasicContractTemplate } from "@/db/schema" interface GetColumnsProps { @@ -40,7 +41,7 @@ const handleFileDownload = (filePath: string, fileName: string) => { try { // 전체 URL 생성 const fullUrl = `${window.location.origin}${filePath}`; - + // a 태그를 생성하여 다운로드 실행 const link = document.createElement('a'); link.href = fullUrl; @@ -48,7 +49,7 @@ const handleFileDownload = (filePath: string, fileName: string) => { document.body.appendChild(link); link.click(); document.body.removeChild(link); - + toast.success("파일 다운로드를 시작합니다."); } catch (error) { console.error("파일 다운로드 오류:", error); @@ -97,7 +98,7 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef { const template = row.original; - + return ( - + setRowAction({ row, type: "view" })} + onSelect={() => setRowAction({ row, type: "view" })} > - + {/* */} 상세보기 setRowAction({ row, type: "update" })} + onSelect={() => setRowAction({ row, type: "update" })} > - + {/* */} 수정하기 + setRowAction({ row, type: "delete" })} - className="text-destructive" + onSelect={() => setRowAction({ row, type: "delete" })} > - + {/* */} 삭제하기 + ⌘⌫ diff --git a/lib/evaluation-criteria/service.ts b/lib/evaluation-criteria/service.ts index 19f2dd81..9cb0126f 100644 --- a/lib/evaluation-criteria/service.ts +++ b/lib/evaluation-criteria/service.ts @@ -781,7 +781,7 @@ async function importRegEvalCriteriaExcel(file: File): Promise<{ const itemValue = REG_EVAL_CRITERIA_ITEM.find(item => item.label === rowData.item)?.value || rowData.item; // 데이터 그룹화 - const groupKey = `${categoryValue}-${category2Value}-${itemValue}-${rowData.classification}`; + const groupKey = `${categoryValue}-${category2Value}-${itemValue}-${rowData.classification}-${rowData.range || ''}-${rowData.remarks || ''}`; if (!groupedData.has(groupKey)) { groupedData.set(groupKey, []); } diff --git a/lib/evaluation-criteria/table/reg-eval-criteria-columns.tsx b/lib/evaluation-criteria/table/reg-eval-criteria-columns.tsx index d48e097b..2b7cbfe7 100644 --- a/lib/evaluation-criteria/table/reg-eval-criteria-columns.tsx +++ b/lib/evaluation-criteria/table/reg-eval-criteria-columns.tsx @@ -371,21 +371,20 @@ function getColumns({ setRowAction }: GetColumnsProps): ColumnDef setRowAction({ row, type: "view" })} > - + {/* */} 상세보기 - setRowAction({ row, type: "update" })} > - + {/* */} 수정하기 setRowAction({ row, type: "delete" })} > - + {/* */} 삭제하기 ⌘⌫ diff --git a/lib/evaluation-target-list/service.ts b/lib/evaluation-target-list/service.ts index 6de00329..4559374b 100644 --- a/lib/evaluation-target-list/service.ts +++ b/lib/evaluation-target-list/service.ts @@ -276,6 +276,8 @@ export async function createEvaluationTarget( ) { console.log(input, "input") try { + const session = await getServerSession(authOptions) + return await db.transaction(async (tx) => { // 벤더 정보 조회 const vendor = await tx @@ -352,18 +354,26 @@ export async function createEvaluationTarget( .from(users) .where(inArray(users.id, reviewerIds)); // sql 대신 inArray 사용 - const reviewerAssignments: typeof evaluationTargetReviewers.$inferInsert[] = - input.reviewers.map(r => { - const info = reviewerInfos.find(i => i.id === r.reviewerUserId); - return { + const reviewerAssignments: typeof evaluationTargetReviewers.$inferInsert[] = [ + ...input.reviewers.map(r => { + const info = reviewerInfos.find(i => i.id === r.reviewerUserId); + return { + evaluationTargetId, + departmentCode: r.departmentCode, + departmentNameFrom: info?.departmentName ?? "TEST 부서", + reviewerUserId: r.reviewerUserId, + assignedBy: createdBy, + }; + }), + // session user 추가 + { evaluationTargetId, - departmentCode: r.departmentCode, - departmentNameFrom: info?.departmentName ?? "TEST 부서", - reviewerUserId: r.reviewerUserId, + departmentCode: "admin", + departmentNameFrom: "정기평가 관리자", + reviewerUserId: Number(session.user.id), assignedBy: createdBy, - }; - }); - + } + ]; await tx.insert(evaluationTargetReviewers).values(reviewerAssignments); } diff --git a/lib/forms/services.ts b/lib/forms/services.ts index 7c1219d2..02333095 100644 --- a/lib/forms/services.ts +++ b/lib/forms/services.ts @@ -1072,6 +1072,7 @@ async function transformDataToSEDPFormat( formCode: string, objectCode: string, projectNo: string, + contractItemId: number, // Add contractItemId parameter designerNo: string = "253213" ): Promise { // Create a map for quick column lookup @@ -1092,10 +1093,67 @@ async function transformDataToSEDPFormat( // Cache for UOM factors to avoid duplicate API calls const uomFactorCache = new Map(); + // Cache for packageCode to avoid duplicate DB queries for same tag + const packageCodeCache = new Map(); + // Transform each row const transformedItems = []; for (const row of tableData) { + // Get packageCode for this specific tag + let packageCode = formCode; // fallback to formCode + + if (row.TAG_NO && contractItemId) { + // Check cache first + const cacheKey = `${contractItemId}-${row.TAG_NO}`; + + if (packageCodeCache.has(cacheKey)) { + packageCode = packageCodeCache.get(cacheKey)!; + } else { + try { + // Query to get packageCode for this specific tag + const tagResult = await db.query.tags.findFirst({ + where: and( + eq(tags.contractItemId, contractItemId), + eq(tags.tagNo, row.TAG_NO) + ) + }); + + if (tagResult) { + // Get the contract item + const contractItemResult = await db.query.contractItems.findFirst({ + where: eq(contractItems.id, tagResult.contractItemId) + }); + + if (contractItemResult) { + // Get the first item with this itemId + const itemResult = await db.query.items.findFirst({ + where: eq(items.id, contractItemResult.itemId) + }); + + if (itemResult && itemResult.packageCode) { + packageCode = itemResult.packageCode; + console.log(`Found packageCode for tag ${row.TAG_NO}: ${packageCode}`); + } else { + console.warn(`No item found for contractItem.itemId: ${contractItemResult.itemId}, using fallback`); + } + } else { + console.warn(`No contractItem found for tag ${row.TAG_NO}, using fallback`); + } + } else { + console.warn(`No tag found for contractItemId: ${contractItemId}, tagNo: ${row.TAG_NO}, using fallback`); + } + + // Cache the result (even if it's the fallback value) + packageCodeCache.set(cacheKey, packageCode); + } catch (error) { + console.error(`Error fetching packageCode for tag ${row.TAG_NO}:`, error); + // Use fallback value and cache it + packageCodeCache.set(cacheKey, packageCode); + } + } + } + // Create base SEDP item with required fields const sedpItem: SEDPDataItem = { TAG_NO: row.TAG_NO || "", @@ -1110,7 +1168,7 @@ async function transformDataToSEDPFormat( LAST_REV_YN: true, CRTER_NO: designerNo, CHGER_NO: designerNo, - TYPE: formCode, + TYPE: packageCode, // Use packageCode instead of formCode PROJ_NO: projectNo, REV_NO: "00", CRTE_DTM: currentTimestamp, @@ -1202,19 +1260,19 @@ export async function transformFormDataToSEDP( formCode: string, objectCode: string, projectNo: string, + contractItemId: number, // Add contractItemId parameter designerNo: string = "253213" ): Promise { - // Use the utility function within the async Server Action return transformDataToSEDPFormat( tableData, columnsJSON, formCode, objectCode, projectNo, + contractItemId, // Pass contractItemId designerNo ); } - /** * Get project code by project ID */ @@ -1330,7 +1388,8 @@ export async function sendFormDataToSEDP( columns, formCode, objectCode, - projectCode + projectCode, + contractItemId // Add contractItemId parameter ); // 4. Send to SEDP API diff --git a/lib/general-check-list/table/general-check-table-columns.tsx b/lib/general-check-list/table/general-check-table-columns.tsx index c764686d..e95855a9 100644 --- a/lib/general-check-list/table/general-check-table-columns.tsx +++ b/lib/general-check-list/table/general-check-table-columns.tsx @@ -8,6 +8,7 @@ import { Ellipsis, Pencil, Trash } from "lucide-react"; import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"; import { DeleteEvaluationsDialog } from "./delete-check-lists-dialog"; import { EditEvaluationSheet } from "./update-check-list-sheet"; +import { DropdownMenu, DropdownMenuShortcut, DropdownMenuSeparator, DropdownMenuItem, DropdownMenuContent, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; export interface GeneralEvaluationRow { @@ -95,42 +96,47 @@ export function getGeneralEvaluationColumns(): ColumnDef[] { id: "actions", enableHiding: false, - size: 40, - minSize:80, - cell: ({ row }) => { + size: 80, + cell: function Cell({ row }) { const record = row.original; const [openEdit, setOpenEdit] = React.useState(false); const [openDelete, setOpenDelete] = React.useState(false); return ( - <> - - - - + + + + + + setOpenEdit(true)} + > + {/* */} + 수정하기 + + + setOpenDelete(true)} + > + {/* */} + 삭제하기 + ⌘⌫ + + + - + ); }, }, diff --git a/lib/sedp/sync-form.ts b/lib/sedp/sync-form.ts index 0606f4a9..87de4645 100644 --- a/lib/sedp/sync-form.ts +++ b/lib/sedp/sync-form.ts @@ -891,12 +891,12 @@ async function getContractItemsByItemCodes(itemCodes: string[], projectId: numbe for (const item of itemRecords) { // itemCode가 null이 아닌 경우에만 처리 - if (item.itemCode) { + if (item.packageCode) { const matchedContractItems = contractItemRecords.filter(ci => ci.itemId === item.id); if (matchedContractItems.length > 0) { // 일치하는 모든 contractItem을 배열로 저장 const contractItemIds = matchedContractItems.map(ci => ci.id); - itemCodeToContractItemIds.set(item.itemCode, contractItemIds); + itemCodeToContractItemIds.set(item.packageCode, contractItemIds); } } } @@ -1062,17 +1062,17 @@ export async function saveFormMappingsAndMetas( await tx.delete(formMetas).where(eq(formMetas.projectId, projectId)); if (contractItemIdsWithForms.size) await tx.delete(forms).where(inArray(forms.contractItemId, [...contractItemIdsWithForms])); - const savedMappings = mappingsToSave.length ? await tx.insert(tagTypeClassFormMappings).values(mappingsToSave).returning({ id: tagTypeClassFormMappings.id, formCode: tagTypeClassFormMappings.formCode }) : []; + const savedMappings = mappingsToSave.length ? await tx.insert(tagTypeClassFormMappings).values(mappingsToSave).onConflictDoNothing().returning({ id: tagTypeClassFormMappings.id, formCode: tagTypeClassFormMappings.formCode }) : []; totalSaved += mappingsToSave.length; if (savedMappings.length) { const rows: any[] = []; savedMappings.forEach(m => (templateDataByFormCode.get(m.formCode) || []).forEach(t => rows.push({ formMappingId: m.id, tmplId: t.TMPL_ID, name: t.NAME, tmplType: t.TMPL_TYPE, sprLstSetup: t.SPR_LST_SETUP, grdLstSetup: t.GRD_LST_SETUP, sprItmLstSetup: t.SPR_ITM_LST_SETUP, description: `Template for form ${m.formCode}`, isActive: true, createdAt: new Date(), updatedAt: new Date() }))); - if (rows.length) { await tx.insert(templateItems).values(rows); totalSaved += rows.length; } + if (rows.length) { await tx.insert(templateItems).values(rows).onConflictDoNothing(); totalSaved += rows.length; } } - if (formMetasToSave.length) { await tx.insert(formMetas).values(formMetasToSave); totalSaved += formMetasToSave.length; } - if (formsToSave.length) { await tx.insert(forms).values(formsToSave); totalSaved += formsToSave.length; } + if (formMetasToSave.length) { await tx.insert(formMetas).values(formMetasToSave).onConflictDoNothing(); totalSaved += formMetasToSave.length; } + if (formsToSave.length) { await tx.insert(forms).values(formsToSave).onConflictDoNothing(); totalSaved += formsToSave.length; } }); return totalSaved; diff --git a/lib/sedp/sync-tag-types.ts b/lib/sedp/sync-tag-types.ts index abed9021..ac259cbb 100644 --- a/lib/sedp/sync-tag-types.ts +++ b/lib/sedp/sync-tag-types.ts @@ -31,6 +31,7 @@ interface LinkCode { START: number; LENGTH: number; IS_SEQ: boolean; + REG_EXPS?: string | null; } interface Attribute { @@ -260,7 +261,7 @@ async function processAndSaveTagSubfields( tagTypeCode: tagType.TYPE_ID, attributesId: attributeId, attributesDescription: attribute.DESC || attributeId, - expression: attribute.REG_EXPS || null, + expression: linkCode.REG_EXPS || null, delimiter: linkCode.DL_VAL || null, sortOrder: linkCode.SEQ || 0, updatedAt: new Date() diff --git a/lib/techsales-rfq/service.ts b/lib/techsales-rfq/service.ts index 9689e855..c991aa42 100644 --- a/lib/techsales-rfq/service.ts +++ b/lib/techsales-rfq/service.ts @@ -1386,7 +1386,7 @@ export async function createTechSalesRfqAttachments(params: { const [newAttachment] = await tx.insert(techSalesAttachments).values({ techSalesRfqId, attachmentType, - fileName: uniqueName, + fileName: saveResult.fileName, originalFileName: file.name, filePath: saveResult.publicPath, fileSize: file.size, diff --git a/lib/welding/service.ts b/lib/welding/service.ts index feb6272b..424c4666 100644 --- a/lib/welding/service.ts +++ b/lib/welding/service.ts @@ -1,6 +1,6 @@ "use server"; -import { revalidateTag, unstable_noStore } from "next/cache"; +import { revalidatePath, revalidateTag, unstable_noStore } from "next/cache"; import db from "@/db/db"; import { unstable_cache } from "@/lib/unstable-cache"; import { filterColumns } from "@/lib/filter-columns"; @@ -11,6 +11,7 @@ import { OcrRow, ocrRows, users } from "@/db/schema"; import { countOcrRows, selectOcrRows } from "./repository"; import { getServerSession } from "next-auth/next" import { authOptions } from "@/app/api/auth/[...nextauth]/route" +import { z } from "zod"; @@ -166,6 +167,8 @@ export async function getOcrAllRows(): Promise { sessionId: ocrRows.sessionId, rowIndex: ocrRows.rowIndex, reportNo: ocrRows.reportNo, + fileName: ocrRows.fileName, + inspectionDate: ocrRows.inspectionDate, no: ocrRows.no, identificationNo: ocrRows.identificationNo, tagNo: ocrRows.tagNo, @@ -186,10 +189,50 @@ export async function getOcrAllRows(): Promise { .leftJoin(users, eq(ocrRows.userId, users.id)) .orderBy(desc(ocrRows.createdAt)) + console.log(allRows.length) + return allRows } catch (error) { console.error("Error fetching all OCR rows:", error) throw new Error("Failed to fetch all OCR data") } +} + + +const removeOcrRowsSchema = z.object({ + ids: z.array(z.string().uuid()), +}) + +export async function removeOcrRows( + input: z.infer +) { + try { + const { ids } = removeOcrRowsSchema.parse(input) + + if (ids.length === 0) { + return { + data: null, + error: "삭제할 데이터를 선택해주세요.", + } + } + + // OCR 행들을 삭제 + await db + .delete(ocrRows) + .where(inArray(ocrRows.id, ids)) + + revalidatePath("/partners/ocr") + + return { + data: null, + error: null, + } + } catch (error) { + console.error("OCR 행 삭제 중 오류 발생:", error) + return { + data: null, + error: error instanceof Error ? error.message : "삭제 중 오류가 발생했습니다.", + } + } } \ No newline at end of file diff --git a/lib/welding/table/delete-ocr-rows-dialog.tsx b/lib/welding/table/delete-ocr-rows-dialog.tsx new file mode 100644 index 00000000..8e67eea3 --- /dev/null +++ b/lib/welding/table/delete-ocr-rows-dialog.tsx @@ -0,0 +1,151 @@ +"use client" + +import * as React from "react" +import { type OcrRow } from "@/db/schema" +import { type Row } from "@tanstack/react-table" +import { Loader, Trash } from "lucide-react" +import { toast } from "sonner" + +import { useMediaQuery } from "@/hooks/use-media-query" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer" + +import { removeOcrRows } from "../service" + +interface DeleteOcrRowsDialogProps + extends React.ComponentPropsWithoutRef { + ocrRows: Row["original"][] + showTrigger?: boolean + onSuccess?: () => void +} + +export function DeleteOcrRowsDialog({ + ocrRows, + showTrigger = true, + onSuccess, + ...props +}: DeleteOcrRowsDialogProps) { + const [isDeletePending, startDeleteTransition] = React.useTransition() + const isDesktop = useMediaQuery("(min-width: 640px)") + + function onDelete() { + startDeleteTransition(async () => { + const { error } = await removeOcrRows({ + ids: ocrRows.map((row) => row.id), + }) + + if (error) { + toast.error(error) + return + } + + props.onOpenChange?.(false) + toast.success( + `${ocrRows.length}개의 OCR 데이터가 성공적으로 삭제되었습니다.` + ) + onSuccess?.() + }) + } + + if (isDesktop) { + return ( + + {showTrigger ? ( + + + + ) : null} + + + 정말로 삭제하시겠습니까? + + 이 작업은 되돌릴 수 없습니다. 선택한{" "} + {ocrRows.length}개의 OCR 데이터가 + 서버에서 영구적으로 삭제됩니다. + + + + + + + + + + + ) + } + + return ( + + {showTrigger ? ( + + + + ) : null} + + + 정말로 삭제하시겠습니까? + + 이 작업은 되돌릴 수 없습니다. 선택한{" "} + {ocrRows.length}개의 OCR 데이터가 + 서버에서 영구적으로 삭제됩니다. + + + + + + + + + + + ) +} \ No newline at end of file diff --git a/lib/welding/table/exporft-ocr-data.ts b/lib/welding/table/exporft-ocr-data.ts index 76856808..729b1beb 100644 --- a/lib/welding/table/exporft-ocr-data.ts +++ b/lib/welding/table/exporft-ocr-data.ts @@ -19,6 +19,8 @@ export async function exportOcrDataToExcel( // 컬럼 정의 (OCR 데이터에 맞게 설정) const columns = [ + { key: "fileName", header: "file Name", width: 20 }, + { key: "inspectionDate", header: "inspection Date", width: 20 }, { key: "reportNo", header: "Report No", width: 15 }, { key: "no", header: "No", width: 10 }, { key: "identificationNo", header: "Identification No", width: 20 }, @@ -52,6 +54,8 @@ export async function exportOcrDataToExcel( reportNo: row.reportNo || "", no: row.no || "", identificationNo: row.identificationNo || "", + inspectionDate: row.inspectionDate ? new Date(row.inspectionDate).toLocaleDateString() : "", + fileName: row.fileName || "", tagNo: row.tagNo || "", jointNo: row.jointNo || "", jointType: row.jointType || "", diff --git a/lib/welding/table/ocr-table-columns.tsx b/lib/welding/table/ocr-table-columns.tsx index d4ca9f5f..6413010d 100644 --- a/lib/welding/table/ocr-table-columns.tsx +++ b/lib/welding/table/ocr-table-columns.tsx @@ -346,14 +346,14 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef - { setRowAction({ type: "delete", row }) }} className="text-destructive focus:text-destructive" > Delete - + */} ), diff --git a/lib/welding/table/ocr-table-toolbar-actions.tsx b/lib/welding/table/ocr-table-toolbar-actions.tsx index 03d8cab0..a6a38adc 100644 --- a/lib/welding/table/ocr-table-toolbar-actions.tsx +++ b/lib/welding/table/ocr-table-toolbar-actions.tsx @@ -2,7 +2,7 @@ import * as React from "react" import { type Table } from "@tanstack/react-table" -import { Download, RefreshCcw, Upload, FileText, Loader2, ChevronDown, X, Play, Pause, RotateCcw } from "lucide-react" +import { Download, RefreshCcw, Upload, FileText, Loader2, ChevronDown, X, Play, Pause, RotateCcw, Trash } from "lucide-react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" @@ -48,6 +48,7 @@ import { } from "@/components/ui/file-list" import { getOcrAllRows } from "../service" import { exportOcrDataToExcel } from "./exporft-ocr-data" +import { DeleteOcrRowsDialog } from "./delete-ocr-rows-dialog" interface OcrTableToolbarActionsProps { table: Table @@ -96,6 +97,9 @@ export function OcrTableToolbarActions({ table }: OcrTableToolbarActionsProps) { const [isPaused, setIsPaused] = React.useState(false) const batchControllerRef = React.useRef(null) + // 선택된 행들 + const selectedRows = table.getFilteredSelectedRowModel().rows + // 단일 파일 업로드 다이얼로그 닫기 핸들러 const handleDialogOpenChange = (open: boolean) => { if (!open) { @@ -522,6 +526,14 @@ export function OcrTableToolbarActions({ table }: OcrTableToolbarActionsProps) { } } + // 삭제 후 콜백 - 테이블 새로고침 + const handleDeleteSuccess = () => { + // 선택 해제 + table.resetRowSelection() + // 페이지 새로고침 + window.location.reload() + } + const getStatusBadgeVariant = (status: FileUploadItem['status']) => { switch (status) { case 'pending': return 'secondary' @@ -554,6 +566,14 @@ export function OcrTableToolbarActions({ table }: OcrTableToolbarActionsProps) { return (
+ {/* 선택된 행이 있을 때만 삭제 버튼 표시 */} + {selectedRows.length > 0 && ( + + )} + {/* 단일 파일 OCR 업로드 다이얼로그 */} diff --git a/lib/welding/table/ocr-table.tsx b/lib/welding/table/ocr-table.tsx index e14c53d1..443f5a6b 100644 --- a/lib/welding/table/ocr-table.tsx +++ b/lib/welding/table/ocr-table.tsx @@ -69,6 +69,18 @@ export function OcrTable({ promises }: ItemsTableProps) { type: "text", // group: "Basic Info", }, + { + id: "fileName", + label: "file Name", + type: "text", + // group: "Basic Info", + }, + { + id: "reportNo", + label: "report No", + type: "text", + // group: "Basic Info", + }, { id: "no", label: "No", -- cgit v1.2.3