summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-02 10:31:53 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-02 10:31:53 +0000
commitd84602bdf369636e29d298a96218bd672de4afd7 (patch)
tree4b03de7ab7807d728e9d34945ca333cf3c080ab6
parent581b415e6707d9f1d0d0b667b84c4314461bfe37 (diff)
(최겸) 기술영업 수정사항 반영 및 구매 PQ/실사 첨부오류 변경
-rw-r--r--lib/pq/service.ts14
-rw-r--r--lib/tech-vendors/service.ts198
-rw-r--r--lib/tech-vendors/table/import-button.tsx76
-rw-r--r--lib/vendor-investigation/service.ts66
-rw-r--r--lib/vendor-investigation/table/update-investigation-sheet.tsx43
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