From 08b73d56c2d887931cecdf2b0af6b277381763e6 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Thu, 6 Nov 2025 17:44:59 +0900 Subject: (김준회) 결재 프리뷰 공통컴포넌트 작성 및 index.ts --> client.ts 분리 (서버사이드 코드가 번들링되어 클라측에서 실행되는 문제 해결 목적) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/vendors/approval-actions.ts | 177 ++++++++++++++++++++++++++++++---------- 1 file changed, 133 insertions(+), 44 deletions(-) (limited to 'lib/vendors/approval-actions.ts') diff --git a/lib/vendors/approval-actions.ts b/lib/vendors/approval-actions.ts index 69d09caa..1ce96078 100644 --- a/lib/vendors/approval-actions.ts +++ b/lib/vendors/approval-actions.ts @@ -2,9 +2,132 @@ import { ApprovalSubmissionSaga } from '@/lib/approval' import type { ApprovalConfig } from '@/lib/approval/types' +import { htmlTableConverter, htmlDescriptionList } from '@/lib/approval' import db from '@/db/db' -import { vendors } from '@/db/schema/vendors' -import { inArray } from 'drizzle-orm' +import { vendors, vendorTypes } from '@/db/schema/vendors' +import { inArray, eq } from 'drizzle-orm' + +/** + * 벤더 승인 결재 템플릿 변수 생성 + * + * @param vendorIds - 벤더 ID 목록 + * @param currentUserEmail - 현재 사용자 이메일 + * @returns 템플릿 변수 객체 및 벤더 정보 + */ +export async function prepareVendorApprovalVariables( + vendorIds: number[], + currentUserEmail?: string +) { + // 벤더 정보 조회 + const vendorRecords = await db + .select({ + id: vendors.id, + vendorName: vendors.vendorName, + vendorCode: vendors.vendorCode, + email: vendors.email, + status: vendors.status, + taxId: vendors.taxId, + country: vendors.country, + representativeName: vendors.representativeName, + phone: vendors.phone, + website: vendors.website, + representativeEmail: vendors.representativeEmail, + postalCode: vendors.postalCode, + address: vendors.address, + addressDetail: vendors.addressDetail, + corporateRegistrationNumber: vendors.corporateRegistrationNumber, + businessSize: vendors.businessSize, + vendorTypeId: vendors.vendorTypeId, + vendorTypeName: vendorTypes.nameKo, + }) + .from(vendors) + .leftJoin(vendorTypes, eq(vendors.vendorTypeId, vendorTypes.id)) + .where(inArray(vendors.id, vendorIds)) + + if (vendorRecords.length === 0) { + throw new Error(`벤더를 찾을 수 없습니다: ${vendorIds.join(', ')}`) + } + + // PENDING_REVIEW 상태가 아닌 벤더 확인 + const invalidVendors = vendorRecords.filter(v => v.status !== 'PENDING_REVIEW') + if (invalidVendors.length > 0) { + throw new Error( + `가입 신청 중(PENDING_REVIEW) 상태의 벤더만 승인할 수 있습니다. ` + + `잘못된 상태: ${invalidVendors.map(v => `${v.vendorName}(${v.status})`).join(', ')}` + ) + } + + // 업체규모 매핑 + const businessSizeMap: Record = { + 'A': '대기업', + 'B': '중견기업', + 'C': '중소기업', + 'D': '소기업', + } + + // 벤더 목록 테이블 생성 + const vendorListTable = await htmlTableConverter( + vendorRecords.map(v => ({ + vendorName: v.vendorName || '-', + representativeName: v.representativeName || '-', + vendorType: v.vendorTypeName || '-', + businessSize: v.businessSize ? businessSizeMap[v.businessSize] || v.businessSize : '-', + phone: v.phone || '-', + email: v.representativeEmail || '-', + })), + [ + { key: 'vendorName', label: '업체명' }, + { key: 'representativeName', label: '대표자명' }, + { key: 'vendorType', label: '업체유형' }, + { key: 'businessSize', label: '기업규모' }, + { key: 'phone', label: '전화번호' }, + { key: 'email', label: '이메일' }, + ] + ) + + // 벤더별 상세 정보 + const vendorDetailsHtml = ( + await Promise.all( + vendorRecords.map(async (v) => { + const details = await htmlDescriptionList([ + { label: '업체명', value: v.vendorName || '-' }, + { label: '대표자명', value: v.representativeName || '-' }, + { label: '업체유형', value: v.vendorTypeName || '-' }, + { label: '기업규모', value: v.businessSize ? businessSizeMap[v.businessSize] || v.businessSize : '-' }, + { label: '국가', value: v.country || '-' }, + { label: '우편번호', value: v.postalCode || '-' }, + { label: '주소', value: v.address || '-' }, + { label: '상세주소', value: v.addressDetail || '-' }, + { label: '전화번호', value: v.phone || '-' }, + { label: '이메일', value: v.representativeEmail || '-' }, + { label: '홈페이지', value: v.website || '-' }, + { label: '사업자등록번호', value: v.taxId || '-' }, + { label: '법인등록번호', value: v.corporateRegistrationNumber || '-' }, + ]) + return `

${v.vendorName}

${details}
` + }) + ) + ).join('') + + const variables: Record = { + '업체수': String(vendorRecords.length), + '업체목록': vendorRecords.map(v => v.vendorName).join(', '), + '업체목록테이블': vendorListTable, + '업체상세정보': vendorDetailsHtml, + '요청일': new Date().toLocaleDateString('ko-KR', { + year: 'numeric', + month: 'long', + day: 'numeric', + }), + '요청자': currentUserEmail || '시스템', + } + + return { + variables, + vendorRecords, + vendorNames: vendorRecords.map(v => v.vendorName).join(', '), + } +} /** * 벤더 가입 승인 결재 상신 @@ -33,49 +156,15 @@ export async function approveVendorsWithApproval(input: { throw new Error('승인할 벤더를 선택해주세요.') } - // 2. 벤더 정보 조회 - const vendorRecords = await db - .select({ - id: vendors.id, - vendorName: vendors.vendorName, - vendorCode: vendors.vendorCode, - email: vendors.email, - status: vendors.status, - taxId: vendors.taxId, - country: vendors.country, - }) - .from(vendors) - .where(inArray(vendors.id, input.vendorIds)) - - if (vendorRecords.length === 0) { - throw new Error(`벤더를 찾을 수 없습니다: ${input.vendorIds.join(', ')}`) - } - - // 3. PENDING_REVIEW 상태가 아닌 벤더 확인 - const invalidVendors = vendorRecords.filter(v => v.status !== 'PENDING_REVIEW') - if (invalidVendors.length > 0) { - throw new Error( - `가입 신청 중(PENDING_REVIEW) 상태의 벤더만 승인할 수 있습니다. ` + - `잘못된 상태: ${invalidVendors.map(v => `${v.vendorName}(${v.status})`).join(', ')}` - ) - } - - console.log(`[Vendor Approval Action] ${vendorRecords.length}개 벤더 조회 완료`) - - // 4. 템플릿 변수 준비 (TODO: 실제 템플릿에 맞게 수정 필요) - const variables: Record = { - // TODO: 다음 대화에서 제공될 템플릿에 맞게 변수 매핑 - '업체수': String(vendorRecords.length), - '업체목록': vendorRecords.map(v => - `${v.vendorName} (${v.vendorCode || '코드 미할당'})` - ).join('\n'), - '요청일': new Date().toLocaleDateString('ko-KR'), - '요청자': input.currentUser.email || 'Unknown', - } + // 2. 템플릿 변수 준비 + const { variables, vendorRecords, vendorNames } = await prepareVendorApprovalVariables( + input.vendorIds, + input.currentUser.email + ) - console.log(`[Vendor Approval Action] 템플릿 변수 준비 완료`) + console.log(`[Vendor Approval Action] ${vendorRecords.length}개 벤더 조회 및 템플릿 변수 준비 완료`) - // 5. 결재 상신 (Saga 패턴) + // 3. 결재 상신 (Saga 패턴) const saga = new ApprovalSubmissionSaga( 'vendor_approval', // 핸들러 타입 (handlers-registry에 등록될 키) { @@ -84,7 +173,7 @@ export async function approveVendorsWithApproval(input: { }, { title: `벤더 가입 승인 요청 - ${vendorRecords.length}개 업체`, - description: `${vendorRecords.map(v => v.vendorName).join(', ')} 의 가입을 승인합니다.`, + description: `${vendorNames} 의 가입을 승인합니다.`, templateName: '벤더 가입 승인 요청', variables, approvers: input.approvers, -- cgit v1.2.3