diff options
Diffstat (limited to 'lib/vendor-investigation')
| -rw-r--r-- | lib/vendor-investigation/approval-actions.ts | 248 | ||||
| -rw-r--r-- | lib/vendor-investigation/handlers.ts | 187 |
2 files changed, 435 insertions, 0 deletions
diff --git a/lib/vendor-investigation/approval-actions.ts b/lib/vendor-investigation/approval-actions.ts new file mode 100644 index 00000000..a75b9b70 --- /dev/null +++ b/lib/vendor-investigation/approval-actions.ts @@ -0,0 +1,248 @@ +/** + * PQ 실사 관련 결재 서버 액션 + * + * 사용자가 UI에서 호출하는 함수들 + * withApproval()을 사용하여 결재 프로세스를 시작 + */ + +'use server'; + +import { withApproval } from '@/lib/approval/approval-workflow'; +import { mapPQInvestigationToTemplateVariables } from './handlers'; +import { debugLog, debugError, debugSuccess } from '@/lib/debug-utils'; + +/** + * 결재를 거쳐 PQ 실사 의뢰를 생성하는 서버 액션 + * + * 사용법 (클라이언트 컴포넌트에서): + * ```typescript + * const result = await requestPQInvestigationWithApproval({ + * pqSubmissionIds: [1, 2, 3], + * vendorNames: "협력사A, 협력사B", + * qmManagerId: 123, + * qmManagerName: "홍길동", + * forecastedAt: new Date('2025-11-01'), + * investigationAddress: "경기도 ...", + * investigationNotes: "...", + * currentUser: { id: 1, epId: 'EP001', email: 'user@example.com' }, + * approvers: ['EP002', 'EP003'] + * }); + * + * if (result.status === 'pending_approval') { + * console.log('결재 ID:', result.approvalId); + * } + * ``` + */ +export async function requestPQInvestigationWithApproval(data: { + pqSubmissionIds: number[]; + vendorNames: string; // 여러 업체명 (쉼표로 구분) + qmManagerId: number; + qmManagerName: string; + qmManagerEmail?: string; + forecastedAt: Date; + investigationAddress: string; + investigationNotes?: string; + currentUser: { id: number; epId: string | null; email?: string }; + approvers?: string[]; // Knox EP ID 배열 (결재선) +}) { + debugLog('[PQInvestigationApproval] 실사 의뢰 결재 서버 액션 시작', { + pqCount: data.pqSubmissionIds.length, + vendorNames: data.vendorNames, + qmManagerId: data.qmManagerId, + qmManagerName: data.qmManagerName, + hasQmEmail: !!data.qmManagerEmail, + userId: data.currentUser.id, + hasEpId: !!data.currentUser.epId, + }); + + // 입력 검증 + if (!data.currentUser.epId) { + debugError('[PQInvestigationApproval] Knox EP ID 없음'); + throw new Error('Knox EP ID가 필요합니다'); + } + + if (data.pqSubmissionIds.length === 0) { + debugError('[PQInvestigationApproval] PQ 제출 ID 없음'); + throw new Error('실사 의뢰할 PQ를 선택해주세요'); + } + + // 1. 템플릿 변수 매핑 + debugLog('[PQInvestigationApproval] 템플릿 변수 매핑 시작'); + const requestedAt = new Date(); + const variables = await mapPQInvestigationToTemplateVariables({ + vendorNames: data.vendorNames, + qmManagerName: data.qmManagerName, + qmManagerEmail: data.qmManagerEmail, + forecastedAt: data.forecastedAt, + investigationAddress: data.investigationAddress, + investigationNotes: data.investigationNotes, + requestedAt, + }); + debugLog('[PQInvestigationApproval] 템플릿 변수 매핑 완료', { + variableKeys: Object.keys(variables), + }); + + // 2. 결재 워크플로우 시작 (템플릿 기반) + debugLog('[PQInvestigationApproval] withApproval 호출'); + const result = await withApproval( + // actionType: 핸들러를 찾을 때 사용할 키 + 'pq_investigation_request', + + // actionPayload: 결재 승인 후 핸들러에 전달될 데이터 + { + pqSubmissionIds: data.pqSubmissionIds, + qmManagerId: data.qmManagerId, + qmManagerName: data.qmManagerName, + forecastedAt: data.forecastedAt, + investigationAddress: data.investigationAddress, + investigationNotes: data.investigationNotes, + vendorNames: data.vendorNames, + }, + + // approvalConfig: 결재 상신 정보 (템플릿 포함) + { + title: `Vendor 실사의뢰 - ${data.vendorNames}`, + description: `${data.vendorNames}에 대한 실사 의뢰`, + templateName: 'Vendor 실사의뢰', // 한국어 템플릿명 + variables, // 치환할 변수들 + approvers: data.approvers, + currentUser: data.currentUser, + } + ); + + debugSuccess('[PQInvestigationApproval] 결재 워크플로우 완료', { + approvalId: result.approvalId, + pendingActionId: result.pendingActionId, + status: result.status, + }); + + return result; +} + +/** + * 결재를 거쳐 PQ 실사 재의뢰를 처리하는 서버 액션 + * + * 사용법 (클라이언트 컴포넌트에서): + * ```typescript + * const result = await reRequestPQInvestigationWithApproval({ + * investigationIds: [1, 2, 3], + * vendorNames: "협력사A, 협력사B", + * currentUser: { id: 1, epId: 'EP001', email: 'user@example.com' }, + * approvers: ['EP002', 'EP003'], + * reason: '재의뢰 사유...' + * }); + * + * if (result.status === 'pending_approval') { + * console.log('결재 ID:', result.approvalId); + * } + * ``` + */ +export async function reRequestPQInvestigationWithApproval(data: { + investigationIds: number[]; + vendorNames: string; // 여러 업체명 (쉼표로 구분) + currentUser: { id: number; epId: string | null; email?: string }; + approvers?: string[]; // Knox EP ID 배열 (결재선) + reason?: string; // 재의뢰 사유 +}) { + debugLog('[PQReRequestApproval] 실사 재의뢰 결재 서버 액션 시작', { + investigationCount: data.investigationIds.length, + vendorNames: data.vendorNames, + userId: data.currentUser.id, + hasEpId: !!data.currentUser.epId, + hasReason: !!data.reason, + }); + + // 입력 검증 + if (!data.currentUser.epId) { + debugError('[PQReRequestApproval] Knox EP ID 없음'); + throw new Error('Knox EP ID가 필요합니다'); + } + + if (data.investigationIds.length === 0) { + debugError('[PQReRequestApproval] 실사 ID 없음'); + throw new Error('재의뢰할 실사를 선택해주세요'); + } + + // 1. 기존 실사 정보 조회 (첫 번째 실사 기준) + debugLog('[PQReRequestApproval] 기존 실사 정보 조회'); + const { default: db } = await import('@/db/db'); + const { vendorInvestigations, users } = await import('@/db/schema'); + const { eq } = await import('drizzle-orm'); + + const results = await db + .select({ + id: vendorInvestigations.id, + forecastedAt: vendorInvestigations.forecastedAt, + investigationAddress: vendorInvestigations.investigationAddress, + qmManagerId: vendorInvestigations.qmManagerId, + qmManagerName: users.name, + qmManagerEmail: users.email, + }) + .from(vendorInvestigations) + .leftJoin(users, eq(vendorInvestigations.qmManagerId, users.id)) + .where(eq(vendorInvestigations.id, data.investigationIds[0])) + .limit(1); + + const existingInvestigation = results[0]; + + if (!existingInvestigation) { + debugError('[PQReRequestApproval] 기존 실사 정보를 찾을 수 없음'); + throw new Error('기존 실사 정보를 찾을 수 없습니다.'); + } + + debugLog('[PQReRequestApproval] 기존 실사 정보 조회 완료', { + investigationId: existingInvestigation.id, + qmManagerName: existingInvestigation.qmManagerName, + forecastedAt: existingInvestigation.forecastedAt, + }); + + // 2. 템플릿 변수 매핑 + debugLog('[PQReRequestApproval] 템플릿 변수 매핑 시작'); + const reRequestedAt = new Date(); + const { mapPQReRequestToTemplateVariables } = await import('./handlers'); + const variables = await mapPQReRequestToTemplateVariables({ + vendorNames: data.vendorNames, + investigationCount: data.investigationIds.length, + reRequestedAt, + reason: data.reason, + // 기존 실사 정보 전달 + forecastedAt: existingInvestigation.forecastedAt || undefined, + investigationAddress: existingInvestigation.investigationAddress || undefined, + qmManagerName: existingInvestigation.qmManagerName || undefined, + qmManagerEmail: existingInvestigation.qmManagerEmail || undefined, + }); + debugLog('[PQReRequestApproval] 템플릿 변수 매핑 완료', { + variableKeys: Object.keys(variables), + }); + + // 2. 결재 워크플로우 시작 (템플릿 기반) + debugLog('[PQReRequestApproval] withApproval 호출'); + const result = await withApproval( + // actionType: 핸들러를 찾을 때 사용할 키 + 'pq_investigation_rerequest', + + // actionPayload: 결재 승인 후 핸들러에 전달될 데이터 + { + investigationIds: data.investigationIds, + vendorNames: data.vendorNames, + }, + + // approvalConfig: 결재 상신 정보 (템플릿 포함) + { + title: `Vendor 실사 재의뢰 - ${data.vendorNames}`, + description: `${data.vendorNames}에 대한 실사 재의뢰`, + templateName: 'Vendor 실사 재의뢰', // 한국어 템플릿명 + variables, // 치환할 변수들 + approvers: data.approvers, + currentUser: data.currentUser, + } + ); + + debugSuccess('[PQReRequestApproval] 재의뢰 결재 워크플로우 완료', { + approvalId: result.approvalId, + pendingActionId: result.pendingActionId, + status: result.status, + }); + + return result; +} diff --git a/lib/vendor-investigation/handlers.ts b/lib/vendor-investigation/handlers.ts new file mode 100644 index 00000000..6c0edbd7 --- /dev/null +++ b/lib/vendor-investigation/handlers.ts @@ -0,0 +1,187 @@ +/** + * PQ 실사 관련 결재 액션 핸들러 + * + * 실제 비즈니스 로직만 포함 (결재 로직은 approval-workflow에서 처리) + */ + +'use server'; + +import { requestInvestigationAction } from '@/lib/pq/service'; +import { debugLog, debugError, debugSuccess } from '@/lib/debug-utils'; + +/** + * PQ 실사 의뢰 핸들러 (결재 승인 후 실행됨) + * + * 이 함수는 직접 호출하지 않고, 결재 워크플로우에서 자동으로 호출됨 + * + * @param payload - withApproval()에서 전달한 actionPayload + */ +export async function requestPQInvestigationInternal(payload: { + pqSubmissionIds: number[]; + qmManagerId: number; + qmManagerName?: string; + forecastedAt: Date; + investigationAddress: string; + investigationNotes?: string; + vendorNames?: string; // 복수 업체 이름 (표시용) +}) { + debugLog('[PQInvestigationHandler] 실사 의뢰 핸들러 시작', { + pqCount: payload.pqSubmissionIds.length, + qmManagerId: payload.qmManagerId, + vendorNames: payload.vendorNames, + }); + + try { + // 실제 실사 의뢰 처리 + debugLog('[PQInvestigationHandler] requestInvestigationAction 호출'); + const result = await requestInvestigationAction( + payload.pqSubmissionIds, + { + qmManagerId: payload.qmManagerId, + forecastedAt: payload.forecastedAt, + investigationAddress: payload.investigationAddress, + investigationNotes: payload.investigationNotes, + } + ); + + if (!result.success) { + debugError('[PQInvestigationHandler] 실사 의뢰 실패', result.error); + throw new Error(result.error || '실사 의뢰에 실패했습니다.'); + } + + debugSuccess('[PQInvestigationHandler] 실사 의뢰 완료', { + count: result.count, + }); + + return { + success: true, + count: result.count, + message: `${result.count}개 업체에 대해 실사가 의뢰되었습니다.`, + }; + } catch (error) { + debugError('[PQInvestigationHandler] 실사 의뢰 중 에러', error); + throw error; + } +} + +/** + * PQ 실사 의뢰 데이터를 결재 템플릿 변수로 매핑 + * + * @param payload - 실사 의뢰 데이터 + * @returns 템플릿 변수 객체 (Record<string, string>) + */ +export async function mapPQInvestigationToTemplateVariables(payload: { + vendorNames: string; // 여러 업체명 (쉼표로 구분) + qmManagerName: string; + qmManagerEmail?: string; + forecastedAt: Date; + investigationAddress: string; + investigationNotes?: string; + requestedAt: Date; +}): Promise<Record<string, string>> { + // 담당자 연락처 (QM담당자 이메일) + const contactInfo = payload.qmManagerEmail + ? `<p>${payload.qmManagerName}: ${payload.qmManagerEmail}</p>` + : `<p>${payload.qmManagerName}</p>`; + + // 실사 사유/목적 (있으면 포함, 없으면 빈 문자열) + const investigationPurpose = payload.investigationNotes || ''; + + return { + 협력사명: payload.vendorNames, + 실사요청일: new Date(payload.requestedAt).toLocaleDateString('ko-KR'), + 실사예정일: new Date(payload.forecastedAt).toLocaleDateString('ko-KR'), + 실사장소: payload.investigationAddress, + QM담당자: payload.qmManagerName, + 담당자연락처: contactInfo, + 실사사유목적: investigationPurpose, + }; +} + +/** + * PQ 실사 재의뢰 핸들러 (결재 승인 후 실행됨) + * + * 이 함수는 직접 호출하지 않고, 결재 워크플로우에서 자동으로 호출됨 + * + * @param payload - withApproval()에서 전달한 actionPayload + */ +export async function reRequestPQInvestigationInternal(payload: { + investigationIds: number[]; + vendorNames?: string; // 복수 업체 이름 (표시용) +}) { + debugLog('[PQReRequestHandler] 실사 재의뢰 핸들러 시작', { + investigationCount: payload.investigationIds.length, + vendorNames: payload.vendorNames, + }); + + try { + // 실제 실사 재의뢰 처리 + const { reRequestInvestigationAction } = await import('@/lib/pq/service'); + debugLog('[PQReRequestHandler] reRequestInvestigationAction 호출'); + + const result = await reRequestInvestigationAction(payload.investigationIds); + + if (!result.success) { + debugError('[PQReRequestHandler] 실사 재의뢰 실패', result.error); + throw new Error(result.error || '실사 재의뢰에 실패했습니다.'); + } + + debugSuccess('[PQReRequestHandler] 실사 재의뢰 완료', { + count: result.count, + }); + + return { + success: true, + count: result.count, + message: `${result.count}개 업체에 대해 실사가 재의뢰되었습니다.`, + }; + } catch (error) { + debugError('[PQReRequestHandler] 실사 재의뢰 중 에러', error); + throw error; + } +} + +/** + * PQ 실사 재의뢰 데이터를 결재 템플릿 변수로 매핑 + * + * @param payload - 실사 재의뢰 데이터 + * @returns 템플릿 변수 객체 (Record<string, string>) + */ +export async function mapPQReRequestToTemplateVariables(payload: { + vendorNames: string; // 여러 업체명 (쉼표로 구분) + investigationCount: number; + canceledDate?: Date; + reRequestedAt: Date; + reason?: string; + // 기존 실사 정보 (재의뢰 시 필요) + forecastedAt?: Date; + investigationAddress?: string; + qmManagerName?: string; + qmManagerEmail?: string; +}): Promise<Record<string, string>> { + // 실사요청일은 재의뢰 요청일로 설정 + const requestDate = new Date(payload.reRequestedAt).toLocaleDateString('ko-KR'); + + // 실사예정일 (기존 실사 정보 사용, 없으면 빈 문자열) + const forecastedDate = payload.forecastedAt + ? new Date(payload.forecastedAt).toLocaleDateString('ko-KR') + : ''; + + // 담당자 연락처 (QM담당자 이메일, 없으면 빈 문자열) + const contactInfo = payload.qmManagerEmail + ? `<p>${payload.qmManagerName || 'QM담당자'}: ${payload.qmManagerEmail}</p>` + : '<p>담당자 정보 없음</p>'; + + // 실사 사유/목적 (재의뢰 사유, 있으면 포함, 없으면 빈 문자열) + const investigationPurpose = payload.reason || ''; + + return { + 협력사명: payload.vendorNames, + 실사요청일: requestDate, + 실사예정일: forecastedDate, + 실사장소: payload.investigationAddress || '', + QM담당자: payload.qmManagerName || '', + 담당자연락처: contactInfo, + 실사사유목적: investigationPurpose, + }; +} |
