summaryrefslogtreecommitdiff
path: root/lib/vendor-investigation
diff options
context:
space:
mode:
Diffstat (limited to 'lib/vendor-investigation')
-rw-r--r--lib/vendor-investigation/approval-actions.ts248
-rw-r--r--lib/vendor-investigation/handlers.ts187
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,
+ };
+}