diff options
Diffstat (limited to 'lib/vendor-regular-registrations')
4 files changed, 391 insertions, 17 deletions
diff --git a/lib/vendor-regular-registrations/approval-actions.ts b/lib/vendor-regular-registrations/approval-actions.ts new file mode 100644 index 00000000..02c7e412 --- /dev/null +++ b/lib/vendor-regular-registrations/approval-actions.ts @@ -0,0 +1,99 @@ +/** + * 정규업체 등록 관련 결재 서버 액션 + * + * 사용자가 UI에서 호출하는 함수들 + * withApproval()을 사용하여 결재 프로세스를 시작 + */ + +'use server'; + +import { withApproval } from '@/lib/approval/approval-workflow'; +import { mapRegistrationToTemplateVariables } from './handlers'; +import { debugLog, debugError, debugSuccess } from '@/lib/debug-utils'; +import type { RegistrationRequestData } from '@/components/vendor-regular-registrations/registration-request-dialog'; + +/** + * 결재를 거쳐 정규업체 등록을 처리하는 서버 액션 + * + * 사용법 (클라이언트 컴포넌트에서): + * ```typescript + * const result = await registerVendorWithApproval({ + * registrationId: 123, + * requestData: registrationData, + * 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 registerVendorWithApproval(data: { + registrationId: number; + requestData: RegistrationRequestData; + vendorId?: number; // vendors 테이블에서 정보를 가져오기 위한 vendorId + currentUser: { id: number; epId: string | null; email?: string }; + approvers?: string[]; // Knox EP ID 배열 (결재선) +}) { + debugLog('[VendorRegistrationApproval] 정규업체 등록 결재 서버 액션 시작', { + registrationId: data.registrationId, + companyName: data.requestData.companyNameKor, + businessNumber: data.requestData.businessNumber, + userId: data.currentUser.id, + hasEpId: !!data.currentUser.epId, + }); + + // 입력 검증 + if (!data.currentUser.epId) { + debugError('[VendorRegistrationApproval] Knox EP ID 없음'); + throw new Error('Knox EP ID가 필요합니다'); + } + + if (!data.registrationId) { + debugError('[VendorRegistrationApproval] 등록 ID 없음'); + throw new Error('등록 ID가 필요합니다'); + } + + // 1. 템플릿 변수 매핑 + debugLog('[VendorRegistrationApproval] 템플릿 변수 매핑 시작'); + const requestedAt = new Date(); + const variables = await mapRegistrationToTemplateVariables({ + requestData: data.requestData, + requestedAt, + vendorId: data.vendorId, + }); + debugLog('[VendorRegistrationApproval] 템플릿 변수 매핑 완료', { + variableKeys: Object.keys(variables), + }); + + // 2. 결재 워크플로우 시작 (템플릿 기반) + debugLog('[VendorRegistrationApproval] withApproval 호출'); + const result = await withApproval( + // actionType: 핸들러를 찾을 때 사용할 키 + 'vendor_regular_registration', + + // actionPayload: 결재 승인 후 핸들러에 전달될 데이터 + { + registrationId: data.registrationId, + requestData: data.requestData, + }, + + // approvalConfig: 결재 상신 정보 (템플릿 포함) + { + title: `정규업체 등록 - ${data.requestData.companyNameKor}`, + description: `${data.requestData.companyNameKor} 정규업체 등록 요청`, + templateName: '정규업체 등록', // 한국어 템플릿명 + variables, // 치환할 변수들 + approvers: data.approvers, + currentUser: data.currentUser, + } + ); + + debugSuccess('[VendorRegistrationApproval] 결재 워크플로우 완료', { + approvalId: result.approvalId, + status: result.status, + }); + + return result; +} diff --git a/lib/vendor-regular-registrations/handlers.ts b/lib/vendor-regular-registrations/handlers.ts new file mode 100644 index 00000000..4b21263d --- /dev/null +++ b/lib/vendor-regular-registrations/handlers.ts @@ -0,0 +1,180 @@ +/** + * 정규업체 등록 관련 결재 액션 핸들러 + * + * 실제 비즈니스 로직만 포함 (결재 로직은 approval-workflow에서 처리) + */ + +'use server'; + +import { submitRegistrationRequest } from './service'; +import { debugLog, debugError, debugSuccess } from '@/lib/debug-utils'; +import type { RegistrationRequestData } from '@/components/vendor-regular-registrations/registration-request-dialog'; +import db from '@/db/db'; +import { eq } from 'drizzle-orm'; +import { vendors } from '@/db/schema/vendors'; +import { vendorAdditionalInfo } from '@/db/schema/vendorRegistrations'; + +/** + * 정규업체 등록 핸들러 (결재 승인 후 실행됨) + * + * 이 함수는 직접 호출하지 않고, 결재 워크플로우에서 자동으로 호출됨 + * + * @param payload - withApproval()에서 전달한 actionPayload + */ +export async function registerVendorInternal(payload: { + registrationId: number; + requestData: RegistrationRequestData; +}) { + debugLog('[VendorRegistrationHandler] 정규업체 등록 핸들러 시작', { + registrationId: payload.registrationId, + companyName: payload.requestData.companyNameKor, + }); + + try { + // 실제 정규업체 등록 처리 + debugLog('[VendorRegistrationHandler] submitRegistrationRequest 호출'); + const result = await submitRegistrationRequest( + payload.registrationId, + payload.requestData + ); + + if (!result.success) { + debugError('[VendorRegistrationHandler] 정규업체 등록 실패', result.error); + throw new Error(result.error || '정규업체 등록에 실패했습니다.'); + } + + debugSuccess('[VendorRegistrationHandler] 정규업체 등록 완료', { + registrationId: payload.registrationId, + }); + + return { + success: true, + message: '정규업체 등록이 완료되었습니다.', + }; + } catch (error) { + debugError('[VendorRegistrationHandler] 정규업체 등록 중 에러', error); + throw error; + } +} + +/** + * 정규업체 등록 데이터를 결재 템플릿 변수로 매핑 + * + * 제공된 HTML 템플릿의 변수명에 맞춰 매핑 + * + * @param payload - 정규업체 등록 데이터 + * @returns 템플릿 변수 객체 (Record<string, string>) + */ +export async function mapRegistrationToTemplateVariables(payload: { + requestData: RegistrationRequestData; + requestedAt: Date; + vendorId?: number; // vendors 테이블에서 정보를 가져오기 위한 vendorId +}): Promise<Record<string, string>> { + const { requestData, requestedAt, vendorId } = payload; + + // vendors 테이블에서 추가 정보 가져오기 + let vendorInfo: any = {}; + if (vendorId) { + try { + const vendorResult = await db + .select({ + postalCode: vendors.postalCode, + businessSize: vendors.businessSize, + addressDetail: vendors.addressDetail, + // FAX, 사업유형, 산업유형은 vendors 테이블에 없으므로 빈 값으로 처리 + }) + .from(vendors) + .where(eq(vendors.id, vendorId)) + .limit(1); + + vendorInfo = vendorResult[0] || {}; + } catch (error) { + console.warn('[Template Variables] Failed to fetch vendor info:', error); + } + } + // 추가정보 조회 + let additionalInfo = { + businessType: '', + industryType: '', + companySize: '', + revenue: '', + factoryEstablishedDate: '', + preferredContractTerms: '', + }; + + if (vendorId) { + const additionalInfoResult = await db + .select({ + businessType: vendorAdditionalInfo.businessType, + industryType: vendorAdditionalInfo.industryType, + companySize: vendorAdditionalInfo.companySize, + revenue: vendorAdditionalInfo.revenue, + factoryEstablishedDate: vendorAdditionalInfo.factoryEstablishedDate, + preferredContractTerms: vendorAdditionalInfo.preferredContractTerms, + }) + .from(vendorAdditionalInfo) + .where(eq(vendorAdditionalInfo.vendorId, vendorId)) + .limit(1); + + additionalInfo = additionalInfoResult[0] || additionalInfo; + } + + console.log('[Template Variables] Additional info:', additionalInfo); + const variables = { + // 협력업체 기본정보 (템플릿의 정확한 변수명 사용) + ' 협력업체 기본정보-사업자번호 ': requestData.businessNumber || '', + ' 협력업체 기본정보-업체명 ': requestData.companyNameKor || '', + ' 협력업체 기본정보-대표자명 ': requestData.representativeNameKor || '', + ' 협력업체 기본정보 대표전화 ': requestData.headOfficePhone || '', + ' 협력업체 기본정보 -FAX ': '', // FAX 정보는 vendors 테이블에 없으므로 빈 문자열 + ' 협력업체 기본정보 -E-mail ': requestData.representativeEmail || '', + ' 협력업체 기본정보-우편번호 ': vendorInfo.postalCode || '', // vendors 테이블에서 우편번호 가져오기 + ' 협력업체 기본정보-회사주소': requestData.headOfficeAddress || '', + ' 협력업체 기본정보-상세주소': vendorInfo.addressDetail || '', // 상세주소는 벤더 상세주소로 + ' 협력업체 기본정보-사업유형': additionalInfo.businessType || '', // 주요품목을 사업유형으로 사용 + ' 협력업체 기본정보-산업유형': additionalInfo.industryType || '', // 주요품목을 산업유형으로도 사용 + ' 협력업체 기본정보-회사규모': additionalInfo.companySize || '', // 기업규모 + + // 담당자 연락처 (각 담당자별로 동일한 정보 반복 - 템플릿에서 여러 번 사용됨) + ' 협력업체 관리-상세보기-영업담당자-담당자명 ': requestData.businessContacts.sales.name || '', + ' 협력업체 관리-상세보기-영업담당자-직급 ': requestData.businessContacts.sales.position || '', + ' 협력업체 관리-상세보기-영업담당자-부서 ': requestData.businessContacts.sales.department || '', + ' 협력업체 관리-상세보기-영업담당자-담당업무 ': requestData.businessContacts.sales.responsibility || '', + ' 협력업체 관리-상세보기-영업담당자-이메일 ': requestData.businessContacts.sales.email || '', + ' 협력업체 관리-상세보기-설계담당자-담당자명 ': requestData.businessContacts.design.name || '', + ' 협력업체 관리-상세보기-설계담당자-직급 ': requestData.businessContacts.design.position || '', + ' 협력업체 관리-상세보기-설계담당자-부서 ': requestData.businessContacts.design.department || '', + ' 협력업체 관리-상세보기-설계담당자-담당업무 ': requestData.businessContacts.design.responsibility || '', + ' 협력업체 관리-상세보기-설계담당자-이메일 ': requestData.businessContacts.design.email || '', + ' 협력업체 관리-상세보기-납기담당자-담당자명 ': requestData.businessContacts.delivery.name || '', + ' 협력업체 관리-상세보기-납기담당자-직급 ': requestData.businessContacts.delivery.position || '', + ' 협력업체 관리-상세보기-납기담당자-부서 ': requestData.businessContacts.delivery.department || '', + ' 협력업체 관리-상세보기-납기담당자-담당업무 ': requestData.businessContacts.delivery.responsibility || '', + ' 협력업체 관리-상세보기-납기담당자-이메일 ': requestData.businessContacts.delivery.email || '', + ' 협력업체 관리-상세보기-품질담당자-담당자명 ': requestData.businessContacts.quality.name || '', + ' 협력업체 관리-상세보기-품질담당자-직급 ': requestData.businessContacts.quality.position || '', + ' 협력업체 관리-상세보기-품질담당자-부서 ': requestData.businessContacts.quality.department || '', + ' 협력업체 관리-상세보기-품질담당자-담당업무 ': requestData.businessContacts.quality.responsibility || '', + ' 협력업체 관리-상세보기-품질담당자-이메일 ': requestData.businessContacts.quality.email || '', + ' 협력업체 관리-상세보기-세금계산서담당자-담당자명 ': requestData.businessContacts.taxInvoice.name || '', + ' 협력업체 관리-상세보기-세금계산서담당자-직급 ': requestData.businessContacts.taxInvoice.position || '', + ' 협력업체 관리-상세보기-세금계산서담당자-부서 ': requestData.businessContacts.taxInvoice.department || '', + ' 협력업체 관리-상세보기-세금계산서담당자-담당업무 ': requestData.businessContacts.taxInvoice.responsibility || '', + ' 협력업체 관리-상세보기-세금계산서담당자-이메일 ': requestData.businessContacts.taxInvoice.email || '', + + // 기본계약서 현황 (정규업체 등록 시점에는 아직 계약서가 없으므로 빈 값) + '정규업체등록관리-문서현황-계약동의현황-계약유형 ': '정규업체 등록 요청', + '정규업체등록관리-문서현황-계약동의현황-상태 ': '등록 대기', + '정규업체등록관리-문서현황-계약동의현황-서약일자 ': new Date(requestedAt).toLocaleDateString('ko-KR'), + }; + + // 디버깅을 위한 로그 출력 + console.log('[Template Variables] Generated variables:', Object.keys(variables)); + console.log('[Template Variables] Sample values:', { + companyName: variables[' 협력업체 기본정보-업체명 '], + businessNumber: variables[' 협력업체 기본정보-사업자번호 '], + representative: variables[' 협력업체 기본정보-대표자명 '], + }); + + return variables; +} diff --git a/lib/vendor-regular-registrations/service.ts b/lib/vendor-regular-registrations/service.ts index e163b147..372212fc 100644 --- a/lib/vendor-regular-registrations/service.ts +++ b/lib/vendor-regular-registrations/service.ts @@ -1238,11 +1238,8 @@ export async function submitRegistrationRequest( console.log('✅ MDG 송신 성공:', mdgResult.message);
}
- // TODO: Knox 결재 연동
- // - 사업자등록증, 신용평가보고서, 개인정보동의서, 통장사본
- // - 실사결과 보고서
- // - CP문서, GTC문서, 비밀유지계약서
- // await initiateKnoxApproval(registrationRequestData);
+ // Knox 결재 연동은 별도의 결재 워크플로우에서 처리됩니다.
+ // UI에서 registerVendorWithApproval()을 호출하여 결재 프로세스를 시작합니다.
console.log("✅ 정규업체 등록 요청 데이터:", {
registrationId,
@@ -1260,7 +1257,7 @@ export async function submitRegistrationRequest( return {
success: true,
- message: `정규업체 등록 요청이 성공적으로 제출되었습니다.\n${mdgResult.success ? 'MDG 인터페이스 연동이 완료되었습니다.' : 'MDG 인터페이스 연동에 실패했습니다. (재시도 가능)'}\nKnox 결재 시스템 연동은 추후 구현 예정입니다.`
+ message: `정규업체 등록 요청이 성공적으로 제출되었습니다.\n${mdgResult.success ? 'MDG 인터페이스 연동이 완료되었습니다.' : 'MDG 인터페이스 연동에 실패했습니다. (재시도 가능)'}\n결재 승인 후 정규업체 등록이 완료됩니다.`
};
} catch (error) {
@@ -1406,6 +1403,7 @@ export async function sendRegistrationRequestToMDG( };
// MDG로 데이터 전송
+ console.log('📤 MDG 송신 데이터:', mdgData);
const result = await sendTestVendorDataToMDG(mdgData);
console.log('📤 MDG 송신 결과:', result);
diff --git a/lib/vendor-regular-registrations/table/vendor-regular-registrations-table-toolbar-actions.tsx b/lib/vendor-regular-registrations/table/vendor-regular-registrations-table-toolbar-actions.tsx index df2ab53a..d88cd7b7 100644 --- a/lib/vendor-regular-registrations/table/vendor-regular-registrations-table-toolbar-actions.tsx +++ b/lib/vendor-regular-registrations/table/vendor-regular-registrations-table-toolbar-actions.tsx @@ -15,7 +15,12 @@ import { import { useState } from "react"
import { SkipReasonDialog } from "@/components/vendor-regular-registrations/skip-reason-dialog"
import { RegistrationRequestDialog } from "@/components/vendor-regular-registrations/registration-request-dialog"
+import { ApprovalPreviewDialog } from "@/components/approval/ApprovalPreviewDialog"
import { useRouter } from "next/navigation"
+import { useSession } from "next-auth/react"
+import { registerVendorWithApproval } from "../approval-actions"
+import { mapRegistrationToTemplateVariables } from "../handlers"
+import type { RegistrationRequestData } from "@/components/vendor-regular-registrations/registration-request-dialog"
interface VendorRegularRegistrationsTableToolbarActionsProps {
table: Table<VendorRegularRegistration>
@@ -25,6 +30,8 @@ export function VendorRegularRegistrationsTableToolbarActions({ table,
}: VendorRegularRegistrationsTableToolbarActionsProps) {
const router = useRouter()
+ const { data: session } = useSession()
+
const [syncLoading, setSyncLoading] = useState<{
missingContract: boolean;
additionalInfo: boolean;
@@ -43,6 +50,7 @@ export function VendorRegularRegistrationsTableToolbarActions({ legalReview: false,
})
+ // 2-step 결재 프로세스를 위한 상태
const [registrationRequestDialog, setRegistrationRequestDialog] = useState<{
open: boolean;
registration: VendorRegularRegistration | null;
@@ -51,6 +59,18 @@ export function VendorRegularRegistrationsTableToolbarActions({ registration: null,
})
+ const [approvalDialog, setApprovalDialog] = useState<{
+ open: boolean;
+ registration: VendorRegularRegistration | null;
+ }>({
+ open: false,
+ registration: null,
+ })
+
+ // 결재를 위한 중간 상태 저장
+ const [registrationFormData, setRegistrationFormData] = useState<RegistrationRequestData | null>(null)
+ const [approvalVariables, setApprovalVariables] = useState<Record<string, string>>({})
+
const selectedRows = table.getFilteredSelectedRowModel().rows.map(row => row.original)
@@ -129,7 +149,7 @@ export function VendorRegularRegistrationsTableToolbarActions({ }
};
- // 등록요청 핸들러
+ // 등록요청 핸들러 - Step 1: 정보 입력
const handleRegistrationRequest = () => {
const approvalReadyRows = selectedRows.filter(row => row.status === "approval_ready");
@@ -149,22 +169,73 @@ export function VendorRegularRegistrationsTableToolbarActions({ });
};
- const handleRegistrationRequestSubmit = async (requestData: any) => {
- if (!registrationRequestDialog.registration) return;
+ // 등록요청 정보 입력 완료 - Step 1에서 Step 2로 전환
+ const handleRegistrationRequestSubmit = async (requestData: RegistrationRequestData) => {
+ if (!registrationRequestDialog.registration || !session?.user) return;
+
+ try {
+ // 폼 데이터 저장
+ setRegistrationFormData(requestData);
+
+ // 결재 템플릿 변수 생성
+ const requestedAt = new Date();
+ const variables = await mapRegistrationToTemplateVariables({
+ requestData,
+ requestedAt,
+ });
+
+ setApprovalVariables(variables);
+
+ // RegistrationRequestDialog 닫고 ApprovalPreviewDialog 열기
+ setRegistrationRequestDialog({ open: false, registration: null });
+ setApprovalDialog({
+ open: true,
+ registration: registrationRequestDialog.registration,
+ });
+ } catch (error) {
+ console.error("결재 준비 중 오류 발생:", error);
+ toast.error("결재 준비 중 오류가 발생했습니다.");
+ }
+ };
+
+ // 결재 상신 - Step 2: 결재선 선택 후 최종 상신
+ const handleApprovalSubmit = async (approvers: any[]) => {
+ if (!approvalDialog.registration || !registrationFormData || !session?.user) {
+ toast.error("세션 정보가 없습니다.");
+ return;
+ }
setSyncLoading(prev => ({ ...prev, registrationRequest: true }));
try {
- const result = await submitRegistrationRequest(registrationRequestDialog.registration.id, requestData);
- if (result.success) {
- toast.success(result.message);
- setRegistrationRequestDialog({ open: false, registration: null });
+ // 결재선에서 EP ID 추출 (상신자 제외)
+ const approverEpIds = approvers
+ .filter((line) => line.seq !== "0" && line.epId)
+ .map((line) => line.epId!);
+
+ // 결재 워크플로우 시작
+ const result = await registerVendorWithApproval({
+ registrationId: approvalDialog.registration.id,
+ requestData: registrationFormData,
+ vendorId: approvalDialog.registration.vendorId, // vendors 테이블에서 정보를 가져오기 위한 vendorId
+ currentUser: {
+ id: Number(session.user.id),
+ epId: session.user.epId || null,
+ email: session.user.email || undefined,
+ },
+ approvers: approverEpIds,
+ });
+
+ if (result.status === 'pending_approval') {
+ // 성공 시에만 상태 초기화 및 페이지 리로드
+ setRegistrationFormData(null);
+ setApprovalVariables({});
+ setApprovalDialog({ open: false, registration: null });
+ toast.success("정규업체 등록 결재가 상신되었습니다.");
router.refresh();
- } else {
- toast.error(result.error);
}
} catch (error) {
- console.error("등록요청 오류:", error);
- toast.error("등록요청 중 오류가 발생했습니다.");
+ console.error("결재 상신 중 오류:", error);
+ toast.error("결재 상신 중 오류가 발생했습니다.");
} finally {
setSyncLoading(prev => ({ ...prev, registrationRequest: false }));
}
@@ -233,6 +304,32 @@ export function VendorRegularRegistrationsTableToolbarActions({ registration={registrationRequestDialog.registration}
onSubmit={handleRegistrationRequestSubmit}
/>
+
+ {/* 결재 미리보기 Dialog - 정규업체 등록 */}
+ {session?.user && approvalDialog.registration && (
+ <ApprovalPreviewDialog
+ open={approvalDialog.open}
+ onOpenChange={(open) => {
+ setApprovalDialog(prev => ({ ...prev, open }));
+ if (!open) {
+ // 다이얼로그가 닫히면 폼 데이터도 초기화
+ setRegistrationFormData(null);
+ setApprovalVariables({});
+ }
+ }}
+ templateName="정규업체 등록"
+ variables={approvalVariables}
+ title={`정규업체 등록 - ${approvalDialog.registration.companyName}`}
+ description={`${approvalDialog.registration.companyName} 정규업체 등록 요청`}
+ currentUser={{
+ id: Number(session.user.id),
+ epId: session.user.epId || null,
+ name: session.user.name || null,
+ email: session.user.email || '',
+ }}
+ onSubmit={handleApprovalSubmit}
+ />
+ )}
</div>
)
}
|
