diff options
Diffstat (limited to 'lib/vendors/service.ts')
| -rw-r--r-- | lib/vendors/service.ts | 551 |
1 files changed, 445 insertions, 106 deletions
diff --git a/lib/vendors/service.ts b/lib/vendors/service.ts index 2da16888..8f095c0e 100644 --- a/lib/vendors/service.ts +++ b/lib/vendors/service.ts @@ -2,7 +2,7 @@ import { revalidateTag, unstable_noStore } from "next/cache"; import db from "@/db/db"; -import { vendorAttachments, VendorContact, vendorContacts, vendorItemsView, vendorPossibleItems, vendors, type Vendor } from "@/db/schema/vendors"; +import { vendorAttachments, VendorContact, vendorContacts, vendorDetailView, vendorInvestigations, vendorInvestigationsView, vendorItemsView, vendorPossibleItems, vendors, type Vendor } from "@/db/schema/vendors"; import logger from '@/lib/logger'; import { filterColumns } from "@/lib/filter-columns"; @@ -38,7 +38,7 @@ import type { GetRfqHistorySchema, } from "./validations"; -import { asc, desc, ilike, inArray, and, or, gte, lte, eq, isNull } from "drizzle-orm"; +import { asc, desc, ilike, inArray, and, or, gte, lte, eq, isNull, count } from "drizzle-orm"; import { rfqItems, rfqs, vendorRfqView } from "@/db/schema/rfq"; import path from "path"; import fs from "fs/promises"; @@ -48,8 +48,10 @@ import { promises as fsPromises } from 'fs'; import { sendEmail } from "../mail/sendEmail"; import { PgTransaction } from "drizzle-orm/pg-core"; import { items } from "@/db/schema/items"; -import { id_ID } from "@faker-js/faker"; import { users } from "@/db/schema/users"; +import { getServerSession } from "next-auth"; +import { authOptions } from "@/app/api/auth/[...nextauth]/route"; +import { projects, vendorProjectPQs } from "@/db/schema"; /* ----------------------------------------------------- @@ -178,7 +180,9 @@ export async function getVendorStatusCounts() { "REJECTED": 0, "IN_PQ": 0, "PQ_FAILED": 0, + "PQ_APPROVED": 0, "APPROVED": 0, + "READY_TO_SEND": 0, "PQ_SUBMITTED": 0 }; @@ -275,7 +279,7 @@ export async function createVendor(params: { vendorData: CreateVendorData // 기존의 일반 첨부파일 files?: File[] - + // 신용평가 / 현금흐름 등급 첨부 creditRatingFiles?: File[] cashFlowRatingFiles?: File[] @@ -288,25 +292,25 @@ export async function createVendor(params: { }[] }) { unstable_noStore() // Next.js 서버 액션 캐싱 방지 - + try { const { vendorData, files = [], creditRatingFiles = [], cashFlowRatingFiles = [], contacts } = params - + // 이메일 중복 검사 - 이미 users 테이블에 존재하는지 확인 const existingUser = await db .select({ id: users.id }) .from(users) .where(eq(users.email, vendorData.email)) .limit(1); - + // 이미 사용자가 존재하면 에러 반환 if (existingUser.length > 0) { - return { - data: null, - error: `이미 등록된 이메일입니다. 다른 이메일을 사용해주세요. (Email ${vendorData.email} already exists in the system)` + return { + data: null, + error: `이미 등록된 이메일입니다. 다른 이메일을 사용해주세요. (Email ${vendorData.email} already exists in the system)` }; } - + await db.transaction(async (tx) => { // 1) Insert the vendor (확장 필드도 함께) const [newVendor] = await insertVendor(tx, { @@ -319,36 +323,36 @@ export async function createVendor(params: { website: vendorData.website || null, status: vendorData.status ?? "PENDING_REVIEW", taxId: vendorData.taxId, - + // 대표자 정보 representativeName: vendorData.representativeName || null, representativeBirth: vendorData.representativeBirth || null, representativeEmail: vendorData.representativeEmail || null, representativePhone: vendorData.representativePhone || null, corporateRegistrationNumber: vendorData.corporateRegistrationNumber || null, - + // 신용/현금흐름 creditAgency: vendorData.creditAgency || null, creditRating: vendorData.creditRating || null, cashFlowRating: vendorData.cashFlowRating || null, }) - + // 2) If there are attached files, store them // (2-1) 일반 첨부 if (files.length > 0) { await storeVendorFiles(tx, newVendor.id, files, "GENERAL") } - + // (2-2) 신용평가 파일 if (creditRatingFiles.length > 0) { await storeVendorFiles(tx, newVendor.id, creditRatingFiles, "CREDIT_RATING") } - + // (2-3) 현금흐름 파일 if (cashFlowRatingFiles.length > 0) { await storeVendorFiles(tx, newVendor.id, cashFlowRatingFiles, "CASH_FLOW_RATING") } - + for (const contact of contacts) { await tx.insert(vendorContacts).values({ vendorId: newVendor.id, @@ -360,7 +364,7 @@ export async function createVendor(params: { }) } }) - + revalidateTag("vendors") return { data: null, error: null } } catch (error) { @@ -665,21 +669,21 @@ export async function getItemsForVendor(vendorId: number) { // 해당 vendorId가 이미 가지고 있는 itemCode 목록을 서브쿼리로 구함 // 그 아이템코드를 제외(notIn)하여 모든 items 테이블에서 조회 const itemsData = await db - .select({ - itemCode: items.itemCode, - itemName: items.itemName, - description: items.description, - }) - .from(items) - .leftJoin( - vendorPossibleItems, - eq(items.itemCode, vendorPossibleItems.itemCode) - ) - // vendorPossibleItems.vendorId가 이 vendorId인 행이 없는(즉 아직 등록되지 않은) 아이템만 - .where( - isNull(vendorPossibleItems.id) // 또는 isNull(vendorPossibleItems.itemCode) - ) - .orderBy(asc(items.itemName)) + .select({ + itemCode: items.itemCode, + itemName: items.itemName, + description: items.description, + }) + .from(items) + .leftJoin( + vendorPossibleItems, + eq(items.itemCode, vendorPossibleItems.itemCode) + ) + // vendorPossibleItems.vendorId가 이 vendorId인 행이 없는(즉 아직 등록되지 않은) 아이템만 + .where( + isNull(vendorPossibleItems.id) // 또는 isNull(vendorPossibleItems.itemCode) + ) + .orderBy(asc(items.itemName)) return { data: itemsData.map((item) => ({ @@ -843,14 +847,15 @@ export async function getRfqHistory(input: GetRfqHistorySchema, vendorId: number export async function checkJoinPortal(taxID: string) { try { // 이미 등록된 회사가 있는지 검색 - const result = await db.select().from(vendors).where(eq(vendors.taxId, taxID)).limit(1) + const result = await db.query.vendors.findFirst({ + where: eq(vendors.taxId, taxID) + }); - if (result.length > 0) { + if (result) { // 이미 가입되어 있음 - // data에 예시로 vendorName이나 다른 정보를 담아 반환 return { success: false, - data: result[0].vendorName ?? "Already joined", + data: result.vendorName ?? "Already joined", } } @@ -888,11 +893,9 @@ interface CreateCompanyInput { export async function downloadVendorAttachments(vendorId: number, fileId?: number) { try { // 벤더 정보 조회 - const vendor = await db.select() - .from(vendors) - .where(eq(vendors.id, vendorId)) - .limit(1) - .then(rows => rows[0]); + const vendor = await db.query.vendors.findFirst({ + where: eq(vendors.id, vendorId) + }); if (!vendor) { throw new Error(`벤더 정보를 찾을 수 없습니다. (ID: ${vendorId})`); @@ -1007,6 +1010,7 @@ export async function cleanupTempFiles(fileName: string) { interface ApproveVendorsInput { ids: number[]; + projectId?: number | null } /** @@ -1014,7 +1018,7 @@ interface ApproveVendorsInput { */ export async function approveVendors(input: ApproveVendorsInput) { unstable_noStore(); - + try { // 트랜잭션 내에서 벤더 상태 업데이트, 유저 생성 및 이메일 발송 const result = await db.transaction(async (tx) => { @@ -1027,7 +1031,7 @@ export async function approveVendors(input: ApproveVendorsInput) { }) .where(inArray(vendors.id, input.ids)) .returning(); - + // 2. 업데이트된 벤더 정보 조회 const updatedVendors = await tx .select({ @@ -1037,21 +1041,22 @@ export async function approveVendors(input: ApproveVendorsInput) { }) .from(vendors) .where(inArray(vendors.id, input.ids)); - + // 3. 각 벤더에 대한 유저 계정 생성 await Promise.all( updatedVendors.map(async (vendor) => { if (!vendor.email) return; // 이메일이 없으면 스킵 - + // 이미 존재하는 유저인지 확인 - const existingUser = await tx - .select({ id: users.id }) - .from(users) - .where(eq(users.email, vendor.email)) - .limit(1); - + const existingUser = await db.query.users.findFirst({ + where: eq(users.email, vendor.email), + columns: { + id: true + } + }); + // 유저가 존재하지 않는 경우에만 생성 - if (existingUser.length === 0) { + if (!existingUser) { await tx.insert(users).values({ name: vendor.vendorName, email: vendor.email, @@ -1061,20 +1066,20 @@ export async function approveVendors(input: ApproveVendorsInput) { } }) ); - + // 4. 각 벤더에게 이메일 발송 await Promise.all( updatedVendors.map(async (vendor) => { if (!vendor.email) return; // 이메일이 없으면 스킵 - + try { const userLang = "en"; // 기본값, 필요시 벤더 언어 설정에서 가져오기 - - const subject = + + const subject = "[eVCP] Admin Account Created"; - + const loginUrl = "http://3.36.56.124:3000/en/login"; - + await sendEmail({ to: vendor.email, subject, @@ -1091,25 +1096,44 @@ export async function approveVendors(input: ApproveVendorsInput) { } }) ); - + return updated; }); - + // 캐시 무효화 revalidateTag("vendors"); revalidateTag("vendor-status-counts"); revalidateTag("users"); // 유저 캐시도 무효화 - + return { data: result, error: null }; } catch (err) { console.error("Error approving vendors:", err); return { data: null, error: getErrorMessage(err) }; } } + export async function requestPQVendors(input: ApproveVendorsInput) { unstable_noStore(); - + try { + // 프로젝트 정보 가져오기 (projectId가 있는 경우) + let projectInfo = null; + if (input.projectId) { + const project = await db + .select({ + id: projects.id, + projectCode: projects.code, + projectName: projects.name, + }) + .from(projects) + .where(eq(projects.id, input.projectId)) + .limit(1); + + if (project.length > 0) { + projectInfo = project[0]; + } + } + // 트랜잭션 내에서 벤더 상태 업데이트 및 이메일 발송 const result = await db.transaction(async (tx) => { // 1. 벤더 상태 업데이트 @@ -1121,7 +1145,7 @@ export async function requestPQVendors(input: ApproveVendorsInput) { }) .where(inArray(vendors.id, input.ids)) .returning(); - + // 2. 업데이트된 벤더 정보 조회 const updatedVendors = await tx .select({ @@ -1131,28 +1155,51 @@ export async function requestPQVendors(input: ApproveVendorsInput) { }) .from(vendors) .where(inArray(vendors.id, input.ids)); - - // 3. 각 벤더에게 이메일 발송 + + // 3. 프로젝트 PQ인 경우, vendorProjectPQs 테이블에 레코드 추가 + if (input.projectId && projectInfo) { + // 각 벤더에 대해 프로젝트 PQ 연결 생성 + const vendorProjectPQsData = input.ids.map(vendorId => ({ + vendorId, + projectId: input.projectId!, + status: "REQUESTED", + createdAt: new Date(), + updatedAt: new Date(), + })); + + await tx.insert(vendorProjectPQs).values(vendorProjectPQsData); + } + + // 4. 각 벤더에게 이메일 발송 await Promise.all( updatedVendors.map(async (vendor) => { if (!vendor.email) return; // 이메일이 없으면 스킵 - + try { const userLang = "en"; // 기본값, 필요시 벤더 언어 설정에서 가져오기 - - const subject = - "[eVCP] You are invited to submit PQ"; - - const loginUrl = "http://3.36.56.124:3000/en/login"; - + + // 프로젝트 PQ인지 일반 PQ인지에 따라 제목 변경 + const subject = input.projectId + ? `[eVCP] You are invited to submit Project PQ for ${projectInfo?.projectCode || 'a project'}` + : "[eVCP] You are invited to submit PQ"; + + // 로그인 URL에 프로젝트 ID 추가 (프로젝트 PQ인 경우) + const baseLoginUrl = "http://3.36.56.124:3000/en/login"; + const loginUrl = input.projectId + ? `${baseLoginUrl}?projectId=${input.projectId}` + : baseLoginUrl; + await sendEmail({ to: vendor.email, subject, - template: "pq", // 이메일 템플릿 이름 + template: input.projectId ? "project-pq" : "pq", // 프로젝트별 템플릿 사용 context: { vendorName: vendor.vendorName, loginUrl, language: userLang, + projectCode: projectInfo?.projectCode || '', + projectName: projectInfo?.projectName || '', + hasProject: !!input.projectId, }, }); } catch (emailError) { @@ -1161,17 +1208,20 @@ export async function requestPQVendors(input: ApproveVendorsInput) { } }) ); - + return updated; }); - + // 캐시 무효화 revalidateTag("vendors"); revalidateTag("vendor-status-counts"); - + if (input.projectId) { + revalidateTag(`project-${input.projectId}`); + } + return { data: result, error: null }; } catch (err) { - console.error("Error approving vendors:", err); + console.error("Error requesting PQ from vendors:", err); return { data: null, error: getErrorMessage(err) }; } } @@ -1190,46 +1240,40 @@ export async function sendVendors(input: SendVendorsInput) { // 트랜잭션 내에서 진행 const result = await db.transaction(async (tx) => { // 1. 선택된 벤더 중 APPROVED 상태인 벤더만 필터링 - const approvedVendors = await tx - .select() - .from(vendors) - .where( - and( - inArray(vendors.id, input.ids), - eq(vendors.status, "APPROVED") - ) - ); + const approvedVendors = await db.query.vendors.findMany({ + where: and( + inArray(vendors.id, input.ids), + eq(vendors.status, "APPROVED") + ) + }); if (!approvedVendors.length) { throw new Error("No approved vendors found in the selection"); } - // 벤더별 처리 결과를 저장할 배열 const results = []; // 2. 각 벤더에 대해 처리 for (const vendor of approvedVendors) { // 2-1. 벤더 연락처 정보 조회 - const contacts = await tx - .select() - .from(vendorContacts) - .where(eq(vendorContacts.vendorId, vendor.id)); + const contacts = await db.query.vendorContacts.findMany({ + where: eq(vendorContacts.vendorId, vendor.id) + }); // 2-2. 벤더 가능 아이템 조회 - const possibleItems = await tx - .select() - .from(vendorPossibleItems) - .where(eq(vendorPossibleItems.vendorId, vendor.id)); - + const possibleItems = await db.query.vendorPossibleItems.findMany({ + where: eq(vendorPossibleItems.vendorId, vendor.id) + }); // 2-3. 벤더 첨부파일 조회 - const attachments = await tx - .select({ - id: vendorAttachments.id, - fileName: vendorAttachments.fileName, - filePath: vendorAttachments.filePath, - }) - .from(vendorAttachments) - .where(eq(vendorAttachments.vendorId, vendor.id)); + const attachments = await db.query.vendorAttachments.findMany({ + where: eq(vendorAttachments.vendorId, vendor.id), + columns: { + id: true, + fileName: true, + filePath: true + } + }); + // 2-4. 벤더 정보를 기간계 시스템에 전송 (NextJS API 라우트 사용) const vendorData = { @@ -1287,7 +1331,7 @@ export async function sendVendors(input: SendVendorsInput) { const subject = "[eVCP] Vendor Registration Completed"; - const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://3.36.56.124:3000' + const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://3.36.56.124:3000' const portalUrl = `${baseUrl}/en/partners`; @@ -1343,3 +1387,298 @@ export async function sendVendors(input: SendVendorsInput) { } } + +interface RequestInfoProps { + ids: number[]; +} + +export async function requestInfo({ ids }: RequestInfoProps) { + try { + // 1. 벤더 정보 가져오기 + const vendorList = await db.query.vendors.findMany({ + where: inArray(vendors.id, ids), + }); + + if (!vendorList.length) { + return { error: "벤더 정보를 찾을 수 없습니다." }; + } + + // 2. 각 벤더에게 이메일 보내기 + for (const vendor of vendorList) { + // 이메일이 없는 경우 스킵 + if (!vendor.email) continue; + + // 벤더 정보 페이지 URL 생성 + const vendorInfoUrl = `${process.env.NEXT_PUBLIC_APP_URL}/partners/info?vendorId=${vendor.id}`; + + // 벤더에게 이메일 보내기 + await sendEmail({ + to: vendor.email, + subject: "[EVCP] 추가 정보 요청 / Additional Information Request", + template: "vendor-additional-info", + context: { + vendorName: vendor.vendorName, + vendorInfoUrl: vendorInfoUrl, + language: "ko", // 기본 언어 설정, 벤더의 선호 언어가 있다면 그것을 사용할 수 있음 + }, + }); + } + + // 3. 성공적으로 처리됨 + return { success: true }; + } catch (error) { + console.error("벤더 정보 요청 중 오류 발생:", error); + return { error: "벤더 정보 요청 중 오류가 발생했습니다. 다시 시도해 주세요." }; + } +} + + +export async function getVendorDetailById(id: number) { + try { + // View를 통해 벤더 정보 조회 + const vendor = await db + .select() + .from(vendorDetailView) + .where(eq(vendorDetailView.id, id)) + .limit(1) + .then(rows => rows[0] || null); + + if (!vendor) { + return null; + } + + // JSON 문자열로 반환된 contacts와 attachments를 JavaScript 객체로 파싱 + const contacts = typeof vendor.contacts === 'string' + ? JSON.parse(vendor.contacts) + : vendor.contacts; + + const attachments = typeof vendor.attachments === 'string' + ? JSON.parse(vendor.attachments) + : vendor.attachments; + + // 파싱된 데이터로 반환 + return { + ...vendor, + contacts, + attachments + }; + } catch (error) { + console.error("Error fetching vendor detail:", error); + throw new Error("Failed to fetch vendor detail"); + } +} + +export type UpdateVendorInfoData = { + id: number + vendorName: string + website?: string + address?: string + email: string + phone?: string + country?: string + representativeName?: string + representativeBirth?: string + representativeEmail?: string + representativePhone?: string + corporateRegistrationNumber?: string + creditAgency?: string + creditRating?: string + cashFlowRating?: string +} + +export type ContactInfo = { + id?: number + contactName: string + contactPosition?: string + contactEmail: string + contactPhone?: string + isPrimary?: boolean +} + +/** + * 벤더 정보를 업데이트하는 함수 + */ +export async function updateVendorInfo(params: { + vendorData: UpdateVendorInfoData + files?: File[] + creditRatingFiles?: File[] + cashFlowRatingFiles?: File[] + contacts: ContactInfo[] + filesToDelete?: number[] // 삭제할 파일 ID 목록 +}) { + try { + const { + vendorData, + files = [], + creditRatingFiles = [], + cashFlowRatingFiles = [], + contacts, + filesToDelete = [] + } = params + + // 세션 및 권한 확인 + const session = await getServerSession(authOptions) + if (!session?.user || !session.user.companyId) { + return { data: null, error: "권한이 없습니다. 로그인이 필요합니다." }; + } + + const companyId = Number(session.user.companyId); + + // 자신의 회사 정보만 수정 가능 (관리자는 모든 회사 정보 수정 가능) + if ( + // !session.user.isAdmin && + vendorData.id !== companyId) { + return { data: null, error: "자신의 회사 정보만 수정할 수 있습니다." }; + } + + // 트랜잭션으로 업데이트 수행 + await db.transaction(async (tx) => { + // 1. 벤더 정보 업데이트 + await tx.update(vendors).set({ + vendorName: vendorData.vendorName, + address: vendorData.address || null, + email: vendorData.email, + phone: vendorData.phone || null, + website: vendorData.website || null, + country: vendorData.country || null, + representativeName: vendorData.representativeName || null, + representativeBirth: vendorData.representativeBirth || null, + representativeEmail: vendorData.representativeEmail || null, + representativePhone: vendorData.representativePhone || null, + corporateRegistrationNumber: vendorData.corporateRegistrationNumber || null, + creditAgency: vendorData.creditAgency || null, + creditRating: vendorData.creditRating || null, + cashFlowRating: vendorData.cashFlowRating || null, + updatedAt: new Date(), + }).where(eq(vendors.id, vendorData.id)) + + // 2. 연락처 정보 관리 + // 2-1. 기존 연락처 가져오기 + const existingContacts = await tx + .select() + .from(vendorContacts) + .where(eq(vendorContacts.vendorId, vendorData.id)) + + // 2-2. 기존 연락처 ID 목록 + const existingContactIds = existingContacts.map(c => c.id) + + // 2-3. 업데이트할 연락처와 새로 추가할 연락처 분류 + const contactsToUpdate = contacts.filter(c => c.id && existingContactIds.includes(c.id)) + const contactsToAdd = contacts.filter(c => !c.id) + + // 2-4. 삭제할 연락처 (기존에 있지만 새 목록에 없는 것) + const contactIdsToKeep = contactsToUpdate.map(c => c.id) + .filter((id): id is number => id !== undefined) + const contactIdsToDelete = existingContactIds.filter(id => !contactIdsToKeep.includes(id)) + + // 2-5. 연락처 삭제 + if (contactIdsToDelete.length > 0) { + await tx + .delete(vendorContacts) + .where(and( + eq(vendorContacts.vendorId, vendorData.id), + inArray(vendorContacts.id, contactIdsToDelete) + )) + } + + // 2-6. 연락처 업데이트 + for (const contact of contactsToUpdate) { + if (contact.id !== undefined) { + await tx + .update(vendorContacts) + .set({ + contactName: contact.contactName, + contactPosition: contact.contactPosition || null, + contactEmail: contact.contactEmail, + contactPhone: contact.contactPhone || null, + isPrimary: contact.isPrimary || false, + updatedAt: new Date(), + }) + .where(and( + eq(vendorContacts.id, contact.id), + eq(vendorContacts.vendorId, vendorData.id) + )) + } + } + + // 2-7. 연락처 추가 + for (const contact of contactsToAdd) { + await tx + .insert(vendorContacts) + .values({ + vendorId: vendorData.id, + contactName: contact.contactName, + contactPosition: contact.contactPosition || null, + contactEmail: contact.contactEmail, + contactPhone: contact.contactPhone || null, + isPrimary: contact.isPrimary || false, + }) + } + + // 3. 파일 삭제 처리 + if (filesToDelete.length > 0) { + // 3-1. 삭제할 파일 정보 가져오기 + const attachmentsToDelete = await tx + .select() + .from(vendorAttachments) + .where(and( + eq(vendorAttachments.vendorId, vendorData.id), + inArray(vendorAttachments.id, filesToDelete) + )) + + // 3-2. 파일 시스템에서 파일 삭제 + for (const attachment of attachmentsToDelete) { + try { + // 파일 경로는 /public 기준이므로 process.cwd()/public을 앞에 붙임 + const filePath = path.join(process.cwd(), 'public', attachment.filePath.replace(/^\//, '')) + await fs.access(filePath, fs.constants.F_OK) // 파일 존재 확인 + await fs.unlink(filePath) // 파일 삭제 + } catch (error) { + console.warn(`Failed to delete file for attachment ${attachment.id}:`, error) + // 파일 삭제 실패해도 DB에서는 삭제 진행 + } + } + + // 3-3. DB에서 파일 기록 삭제 + await tx + .delete(vendorAttachments) + .where(and( + eq(vendorAttachments.vendorId, vendorData.id), + inArray(vendorAttachments.id, filesToDelete) + )) + } + + // 4. 새 파일 저장 (제공된 storeVendorFiles 함수 활용) + // 4-1. 일반 파일 저장 + if (files.length > 0) { + await storeVendorFiles(tx, vendorData.id, files, "GENERAL"); + } + + // 4-2. 신용평가 파일 저장 + if (creditRatingFiles.length > 0) { + await storeVendorFiles(tx, vendorData.id, creditRatingFiles, "CREDIT_RATING"); + } + + // 4-3. 현금흐름 파일 저장 + if (cashFlowRatingFiles.length > 0) { + await storeVendorFiles(tx, vendorData.id, cashFlowRatingFiles, "CASH_FLOW_RATING"); + } + }) + + // 캐시 무효화 + revalidateTag("vendors") + revalidateTag(`vendor-${vendorData.id}`) + + return { + data: { + success: true, + message: '벤더 정보가 성공적으로 업데이트되었습니다.', + vendorId: vendorData.id + }, + error: null + } + } catch (error) { + console.error("Vendor info update error:", error); + return { data: null, error: getErrorMessage(error) } + } +}
\ No newline at end of file |
