diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-02 10:31:53 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-02 10:31:53 +0000 |
| commit | d84602bdf369636e29d298a96218bd672de4afd7 (patch) | |
| tree | 4b03de7ab7807d728e9d34945ca333cf3c080ab6 | |
| parent | 581b415e6707d9f1d0d0b667b84c4314461bfe37 (diff) | |
(최겸) 기술영업 수정사항 반영 및 구매 PQ/실사 첨부오류 변경
| -rw-r--r-- | lib/pq/service.ts | 14 | ||||
| -rw-r--r-- | lib/tech-vendors/service.ts | 198 | ||||
| -rw-r--r-- | lib/tech-vendors/table/import-button.tsx | 76 | ||||
| -rw-r--r-- | lib/vendor-investigation/service.ts | 66 | ||||
| -rw-r--r-- | lib/vendor-investigation/table/update-investigation-sheet.tsx | 43 |
5 files changed, 326 insertions, 71 deletions
diff --git a/lib/pq/service.ts b/lib/pq/service.ts index 989f8d5c..40d81302 100644 --- a/lib/pq/service.ts +++ b/lib/pq/service.ts @@ -3706,13 +3706,13 @@ export async function updateInvestigationDetailsAction(input: { try {
console.log(`📁 실사 첨부파일 처리 중: ${file.name} (${file.size} bytes)`);
- // saveFile을 사용하여 파일 저장
- const saveResult = await saveFile({
+ // saveDRMFile을 사용하여 파일 저장
+ const saveResult = await saveDRMFile(
file,
- directory: `vendor-investigation/${input.investigationId}`,
- originalName: file.name,
- userId: "investigation-update"
- });
+ decryptWithServerAction,
+ `vendor-investigation/${input.investigationId}`,
+ "investigation-update"
+ );
if (!saveResult.success) {
console.error(`❌ 파일 저장 실패: ${file.name}`, saveResult.error);
@@ -3738,7 +3738,7 @@ export async function updateInvestigationDetailsAction(input: { // DB에 첨부파일 레코드 생성
await tx.insert(vendorInvestigationAttachments).values({
investigationId: input.investigationId,
- fileName: saveResult.originalName!,
+ fileName: saveResult.fileName!,
originalFileName: file.name,
filePath: saveResult.publicPath!,
fileSize: file.size,
diff --git a/lib/tech-vendors/service.ts b/lib/tech-vendors/service.ts index f5380889..e8dcb5dc 100644 --- a/lib/tech-vendors/service.ts +++ b/lib/tech-vendors/service.ts @@ -1423,29 +1423,138 @@ export async function importTechVendorsFromExcel( }); continue; } + // 1. 이메일로 기존 벤더 중복 체크 - const existingVendor = await tx.query.techVendors.findFirst({ + let existingVendor = await tx.query.techVendors.findFirst({ where: eq(techVendors.email, vendor.email), - columns: { id: true, vendorName: true, email: true } + columns: { id: true, vendorName: true, vendorCode: true, email: true } }); + // 2. 이메일이 중복되지 않은 경우 벤더 코드나 이름으로 추가 확인 + if (!existingVendor && vendor.vendorCode) { + existingVendor = await tx.query.techVendors.findFirst({ + where: eq(techVendors.vendorCode, vendor.vendorCode), + columns: { id: true, vendorName: true, vendorCode: true, email: true } + }); + } + + // 3. 벤더 코드도 일치하지 않는 경우 벤더 이름으로 확인 + if (!existingVendor) { + existingVendor = await tx.query.techVendors.findFirst({ + where: eq(techVendors.vendorName, vendor.vendorName), + columns: { id: true, vendorName: true, vendorCode: true, email: true } + }); + } + + // 4. 일치하는 벤더가 있는 경우 처리 if (existingVendor) { - console.log("이미 존재하는 벤더 스킵:", vendor.vendorName, vendor.email); + console.log("기존 벤더에 담당자 추가:", existingVendor.vendorName, vendor.email); + + // 기존 벤더의 벤더 타입 업데이트 (새로운 타입 추가) + const existingVendorFull = await tx.query.techVendors.findFirst({ + where: eq(techVendors.id, existingVendor.id), + columns: { id: true, techVendorType: true } + }); + + if (existingVendorFull) { + const existingTypes = existingVendorFull.techVendorType ? existingVendorFull.techVendorType.split(',').map(t => t.trim()) : []; + const newType = vendor.techVendorType.trim(); + + // 새로운 타입이 기존에 없는 경우에만 추가 + if (!existingTypes.includes(newType)) { + const updatedTypes = [...existingTypes, newType]; + const updatedTypeString = updatedTypes.join(', '); + + await tx.update(techVendors) + .set({ techVendorType: updatedTypeString }) + .where(eq(techVendors.id, existingVendor.id)); + + console.log(`벤더 타입 업데이트: ${existingVendorFull.techVendorType} -> ${updatedTypeString}`); + } + } + + // 담당자 정보를 기존 벤더에 추가 + let contactName = vendor.vendorName; + let contactEmail = vendor.email; + + // vendor.contacts가 있고, contactName이 있으면 contactName 사용 + if (vendor.contacts && vendor.contacts.length > 0 && vendor.contacts[0].contactName) { + contactName = vendor.contacts[0].contactName; + // 만약 contactEmail이 있으면 그걸 사용, 없으면 vendor.email 사용 + if (vendor.contacts[0].contactEmail) { + contactEmail = vendor.contacts[0].contactEmail; + } + } + + // 담당자 이메일 중복 체크 + const existingContact = await tx.query.techVendorContacts.findFirst({ + where: and( + eq(techVendorContacts.vendorId, existingVendor.id), + eq(techVendorContacts.contactEmail, contactEmail) + ), + columns: { id: true, contactEmail: true } + }); + + if (existingContact) { + console.log("담당자 이메일 중복:", contactEmail); + errors.push({ + vendorName: vendor.vendorName, + email: vendor.email, + error: `담당자 이메일 '${contactEmail}'이(가) 이미 등록되어 있습니다` + }); + } else { + // 담당자 생성 + await tx.insert(techVendorContacts).values({ + vendorId: existingVendor.id, + contactName: contactName, + contactPosition: null, + contactEmail: contactEmail, + contactPhone: null, + contactCountry: null, + isPrimary: false, + }); + console.log("담당자 추가 성공:", contactName, contactEmail); + } + + // 기존 벤더에 담당자 추가했으므로 벤더 생성은 스킵하고 유저 생성으로 넘어감 skippedVendors.push({ vendorName: vendor.vendorName, email: vendor.email, - reason: `이미 등록된 이메일입니다 (기존 업체: ${existingVendor.vendorName})` + reason: `기존 벤더에 담당자 추가됨 (기존 업체: ${existingVendor.vendorName})` }); - continue; + + // 유저 생성 (기존 벤더의 담당자로 추가된 경우) + if (contactEmail) { + console.log("유저 생성 시도:", contactEmail); + + // 이미 존재하는 유저인지 확인 + const existingUser = await tx.query.users.findFirst({ + where: eq(users.email, contactEmail), + columns: { id: true } + }); + + if (!existingUser) { + // 유저가 존재하지 않는 경우 생성 + await tx.insert(users).values({ + name: contactName, + email: contactEmail, + techCompanyId: existingVendor.id, + domain: "partners", + }); + console.log("유저 생성 성공"); + } else { + // 이미 존재하는 유저라면 techCompanyId 업데이트 + await tx.update(users) + .set({ techCompanyId: existingVendor.id }) + .where(eq(users.id, existingUser.id)); + console.log("이미 존재하는 유저, techCompanyId 업데이트:", existingUser.id); + } + } + + continue; // 벤더 생성 부분으로 넘어가지 않음 } // 2. 벤더 생성 - console.log("벤더 생성 시도:", { - vendorName: vendor.vendorName, - email: vendor.email, - techVendorType: vendor.techVendorType - }); - const [newVendor] = await tx.insert(techVendors).values({ vendorName: vendor.vendorName, vendorCode: vendor.vendorCode || null, @@ -1486,30 +1595,21 @@ export async function importTechVendorsFromExcel( }); console.log("담당자 생성 성공:", contact.contactName, contact.contactEmail); } - - // // 벤더 이메일을 주 담당자의 이메일로 업데이트 - // const primaryContact = vendor.contacts.find(c => c.isPrimary) || vendor.contacts[0]; - // if (primaryContact && primaryContact.contactEmail !== vendor.email) { - // await tx.update(techVendors) - // .set({ email: primaryContact.contactEmail }) - // .where(eq(techVendors.id, newVendor.id)); - // console.log("벤더 이메일 업데이트:", primaryContact.contactEmail); - // } } - // else { - // // 담당자 정보가 없는 경우 벤더 정보로 기본 담당자 생성 - // console.log("기본 담당자 생성"); - // await tx.insert(techVendorContacts).values({ - // vendorId: newVendor.id, - // contactName: vendor.representativeName || vendor.vendorName || "기본 담당자", - // contactPosition: null, - // contactEmail: vendor.email, - // contactPhone: vendor.representativePhone || vendor.phone || null, - // contactCountry: vendor.country || null, - // isPrimary: true, - // }); - // console.log("기본 담당자 생성 성공:", vendor.email); - // } + else { + // 담당자 정보가 없는 경우 벤더 정보로 기본 담당자 생성 + console.log("기본 담당자 생성"); + await tx.insert(techVendorContacts).values({ + vendorId: newVendor.id, + contactName: vendor.representativeName || vendor.vendorName || "기본 담당자", + contactPosition: null, + contactEmail: vendor.email, + contactPhone: vendor.representativePhone || vendor.phone || null, + contactCountry: vendor.country || null, + isPrimary: true, + }); + console.log("기본 담당자 생성 성공:", vendor.email); + } // 3. 유저 생성 (이메일이 있는 경우) if (vendor.email) { @@ -3038,11 +3138,37 @@ export async function importPossibleItemsFromExcel( continue } - const vendor = await findVendorByEmail(row.vendorEmail.trim()) + let vendor = await findVendorByEmail(row.vendorEmail.trim()) + + // 2. 벤더 테이블에서 찾을 수 없는 경우, 담당자 테이블에서 찾기 + if (!vendor) { + console.log(`벤더 테이블에서 찾을 수 없음, 담당자 테이블에서 검색: ${row.vendorEmail}`) + + // 담당자 테이블에서 해당 이메일로 검색 + const contact = await db.query.techVendorContacts.findFirst({ + where: eq(techVendorContacts.contactEmail, row.vendorEmail.trim()), + columns: { vendorId: true, contactEmail: true } + }) + + if (contact) { + console.log(`담당자 테이블에서 찾음, 벤더 ID: ${contact.vendorId}`) + + // 해당 벤더 정보 가져오기 + vendor = await db.query.techVendors.findFirst({ + where: eq(techVendors.id, contact.vendorId), + columns: { id: true, vendorName: true, email: true } + }) + + if (vendor) { + console.log(`담당자를 통해 벤더 찾음: ${vendor.vendorName}`) + } + } + } + if (!vendor) { result.failedRows.push({ row: rowNumber, - error: `벤더 이메일 '${row.vendorEmail}'을(를) 찾을 수 없습니다.`, + error: `벤더 이메일 '${row.vendorEmail}'을(를) 찾을 수 없습니다. (벤더 테이블과 담당자 테이블 모두에서 검색 실패)`, vendorEmail: row.vendorEmail, itemCode: row.itemCode, itemType: row.itemType as "조선" | "해양TOP" | "해양HULL", diff --git a/lib/tech-vendors/table/import-button.tsx b/lib/tech-vendors/table/import-button.tsx index 1d3bf242..b268d29d 100644 --- a/lib/tech-vendors/table/import-button.tsx +++ b/lib/tech-vendors/table/import-button.tsx @@ -101,8 +101,14 @@ export function ImportTechVendorButton({ onSuccess }: ImportTechVendorButtonProp // 헤더를 기반으로 인덱스 매핑 생성
const headerMapping: Record<string, number> = {};
headerValues.forEach((value, index) => {
- if (typeof value === 'string') {
- headerMapping[value] = index;
+ // 리치텍스트를 일반 텍스트로 변환
+ const cleanValue = typeof value === 'string' ? value :
+ value && typeof value === 'object' && 'richText' in value ?
+ value.richText?.map((rt: any) => rt.text || '').join('') || '' :
+ String(value || '');
+
+ if (cleanValue && cleanValue.trim()) {
+ headerMapping[cleanValue] = index;
}
});
@@ -156,7 +162,19 @@ export function ImportTechVendorButton({ onSuccess }: ImportTechVendorButtonProp // 헤더 매핑에 따라 데이터 추출
Object.entries(headerMapping).forEach(([header, index]) => {
- rowData[header] = values[index] || "";
+ const rawValue = values[index];
+
+ // 리치텍스트를 일반 텍스트로 변환
+ let cleanValue = "";
+ if (typeof rawValue === 'string') {
+ cleanValue = rawValue;
+ } else if (rawValue && typeof rawValue === 'object' && 'richText' in rawValue) {
+ cleanValue = rawValue.richText?.map((rt: any) => rt.text || '').join('') || '';
+ } else if (rawValue !== null && rawValue !== undefined) {
+ cleanValue = String(rawValue);
+ }
+
+ rowData[header] = cleanValue;
});
// 빈 행이 아닌 경우만 추가
@@ -271,6 +289,58 @@ export function ImportTechVendorButton({ onSuccess }: ImportTechVendorButtonProp toast.error(result.error || "벤더 가져오기에 실패했습니다.");
}
+ // 에러가 있으면 에러 엑셀 다운로드 (성공/실패 상관없이)
+ if (result.details?.errors && result.details.errors.length > 0) {
+ try {
+ // 에러 데이터를 Excel로 변환
+ const errorWorkbook = new ExcelJS.Workbook();
+ const errorWorksheet = errorWorkbook.addWorksheet("오류내역");
+
+ // 헤더 추가
+ errorWorksheet.columns = [
+ { header: "업체명", key: "vendorName", width: 25 },
+ { header: "이메일", key: "email", width: 30 },
+ { header: "오류내용", key: "error", width: 80, style: { alignment: { wrapText: true }, font: { color: { argb: "FFFF0000" } } } },
+ ];
+
+ // 헤더 스타일 설정
+ const headerRow = errorWorksheet.getRow(1);
+ headerRow.font = { bold: true };
+ headerRow.fill = {
+ type: "pattern",
+ pattern: "solid",
+ fgColor: { argb: "FFFFCCCC" },
+ };
+
+ // 오류 데이터 추가
+ result.details.errors.forEach(errorItem => {
+ errorWorksheet.addRow({
+ vendorName: errorItem.vendorName || "N/A",
+ email: errorItem.email || "N/A",
+ error: errorItem.error || "알 수 없는 오류",
+ });
+ });
+
+ const buffer = await errorWorkbook.xlsx.writeBuffer();
+ const blob = new Blob([buffer], {
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ });
+ const url = window.URL.createObjectURL(blob);
+ const link = document.createElement("a");
+ link.href = url;
+ link.download = "벤더_import_에러.xlsx";
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ window.URL.revokeObjectURL(url);
+
+ toast.info("에러 내역 파일이 다운로드되었습니다.");
+ } catch (excelError) {
+ console.error("에러 엑셀 생성 실패:", excelError);
+ toast.error("에러 엑셀 생성에 실패했습니다.");
+ }
+ }
+
// 상태 초기화 및 다이얼로그 닫기
setFile(null);
setOpen(false);
diff --git a/lib/vendor-investigation/service.ts b/lib/vendor-investigation/service.ts index f0eb411e..c5097e75 100644 --- a/lib/vendor-investigation/service.ts +++ b/lib/vendor-investigation/service.ts @@ -15,6 +15,8 @@ import { v4 as uuid } from "uuid" import { vendorsLogs } from "@/db/schema"; import { cache } from "react" import { deleteFile } from "../file-stroage"; +import { saveDRMFile } from "../file-stroage"; +import { decryptWithServerAction } from "@/components/drm/drmUtils"; export async function getVendorsInvestigation(input: GetVendorsInvestigationSchema) { return unstable_cache( @@ -629,4 +631,66 @@ export const getAllItems = cache(async () => { console.error("Error fetching all items:", error) throw new Error("Failed to fetch items") } -})
\ No newline at end of file +}) + +/** + * Create vendor investigation attachment + */ +export async function createVendorInvestigationAttachmentAction(input: { + investigationId: number; + file: File; + userId?: string; +}) { + unstable_noStore(); + + try { + console.log(`📎 실사 첨부파일 생성 시작: ${input.file.name}`); + + // 1. saveDRMFile을 사용하여 파일 저장 + const saveResult = await saveDRMFile( + input.file, + decryptWithServerAction, + `vendor-investigation/${input.investigationId}`, + input.userId + ); + + if (!saveResult.success) { + throw new Error(`파일 저장 실패: ${input.file.name} - ${saveResult.error}`); + } + + console.log(`✅ 파일 저장 완료: ${input.file.name} -> ${saveResult.fileName}`); + + // 2. DB에 첨부파일 레코드 생성 + const [insertedAttachment] = await db + .insert(vendorInvestigationAttachments) + .values({ + investigationId: input.investigationId, + fileName: saveResult.fileName!, + originalFileName: input.file.name, + filePath: saveResult.publicPath!, + fileSize: input.file.size, + mimeType: input.file.type || 'application/octet-stream', + attachmentType: 'DOCUMENT', // 또는 파일 타입에 따라 결정 + createdAt: new Date(), + updatedAt: new Date(), + }) + .returning(); + + console.log(`✅ 첨부파일 DB 레코드 생성 완료: ID ${insertedAttachment.id}`); + + // 3. 캐시 무효화 + revalidateTag(`vendor-investigation-${input.investigationId}`); + revalidateTag("vendor-investigations"); + + return { + success: true, + attachment: insertedAttachment, + }; + } catch (error) { + console.error(`❌ 실사 첨부파일 생성 실패: ${input.file.name}`, error); + return { + success: false, + error: error instanceof Error ? error.message : "알 수 없는 오류", + }; + } +}
\ No newline at end of file diff --git a/lib/vendor-investigation/table/update-investigation-sheet.tsx b/lib/vendor-investigation/table/update-investigation-sheet.tsx index 14350815..37d1b2cd 100644 --- a/lib/vendor-investigation/table/update-investigation-sheet.tsx +++ b/lib/vendor-investigation/table/update-investigation-sheet.tsx @@ -64,7 +64,7 @@ import { updateVendorInvestigationSchema, type UpdateVendorInvestigationSchema, } from "../validations" -import { updateVendorInvestigationAction, getInvestigationAttachments, deleteInvestigationAttachment } from "../service" +import { updateVendorInvestigationAction, getInvestigationAttachments, deleteInvestigationAttachment, createVendorInvestigationAttachmentAction } from "../service" import { VendorInvestigationsViewWithContacts } from "@/config/vendorInvestigationsColumnsConfig" import prettyBytes from "pretty-bytes" import { downloadFile } from "@/lib/file-download" @@ -183,15 +183,7 @@ export function UpdateVendorInvestigationSheet({ if (!investigation) return try { - const response = await fetch(`/api/vendor-investigations/${investigation.investigationId}/attachments?attachmentId=${attachmentId}`, { - method: "DELETE", - }) - - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.error || "첨부파일 삭제 실패") - } - + await deleteInvestigationAttachment(attachmentId) toast.success("첨부파일이 삭제되었습니다.") // 목록 새로고침 loadExistingAttachments(investigation.investigationId) @@ -409,23 +401,26 @@ export function UpdateVendorInvestigationSheet({ // 파일 업로드 함수 const uploadFiles = async (files: File[], investigationId: number) => { const uploadPromises = files.map(async (file) => { - const formData = new FormData() - formData.append("file", file) - - const response = await fetch(`/api/vendor-investigations/${investigationId}/attachments`, { - method: "POST", - body: formData, - }) + try { + // 서버 액션을 호출하여 파일 저장 및 DB 레코드 생성 + const result = await createVendorInvestigationAttachmentAction({ + investigationId, + file, + userId: undefined // 필요시 사용자 ID 추가 + }); + + if (!result.success) { + throw new Error(result.error || "파일 업로드 실패"); + } - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.error || "파일 업로드 실패") + return result.attachment; + } catch (error) { + console.error(`파일 업로드 실패: ${file.name}`, error); + throw error; } + }); - return await response.json() - }) - - return await Promise.all(uploadPromises) + return await Promise.all(uploadPromises); } // Submit handler |
