/** * 스케줄 자동 생성 서비스 * * 이벤트 버스 기반으로 스케줄 자동 생성을 처리합니다. * - TABLE_SELECTION_CHANGE 이벤트로 선택 데이터 추적 * - SCHEDULE_GENERATE_REQUEST 이벤트로 생성 요청 처리 * - SCHEDULE_GENERATE_APPLY 이벤트로 적용 처리 */ import { useState, useEffect, useCallback, useRef } from "react"; import { v2EventBus } from "../events/EventBus"; import { V2_EVENTS } from "../events/types"; import type { ScheduleType, V2ScheduleGenerateRequestEvent, V2ScheduleGenerateApplyEvent } from "../events/types"; import { apiClient } from "@/lib/api/client"; import { toast } from "sonner"; // ============================================================================ // 타입 정의 // ============================================================================ /** 스케줄 생성 설정 */ export interface ScheduleGenerationConfig { // 스케줄 타입 scheduleType: ScheduleType; // 소스 설정 source: { tableName: string; // 소스 테이블명 groupByField: string; // 그룹화 기준 필드 (part_code) quantityField: string; // 수량 필드 (order_qty, balance_qty) dueDateField?: string; // 납기일 필드 (선택) }; // 리소스 매핑 (타임라인 Y축) resource: { type: string; // 'ITEM', 'MACHINE', 'WORKER' 등 idField: string; // part_code, machine_code 등 nameField: string; // part_name, machine_name 등 }; // 생성 규칙 rules: { leadTimeDays?: number; // 리드타임 (일) dailyCapacity?: number; // 일일 생산능력 workingDays?: number[]; // 작업일 [1,2,3,4,5] = 월~금 considerStock?: boolean; // 재고 고려 여부 stockTableName?: string; // 재고 테이블명 stockQtyField?: string; // 재고 수량 필드 safetyStockField?: string; // 안전재고 필드 }; // 타겟 설정 target: { tableName: string; // 스케줄 테이블명 (schedule_mng 또는 전용 테이블) }; } /** 미리보기 결과 */ export interface SchedulePreviewResult { toCreate: any[]; toDelete: any[]; toUpdate: any[]; summary: { createCount: number; deleteCount: number; updateCount: number; totalQty: number; }; } /** 훅 반환 타입 */ export interface UseScheduleGeneratorReturn { // 상태 isLoading: boolean; showConfirmDialog: boolean; previewResult: SchedulePreviewResult | null; // 핸들러 handleConfirm: (confirmed: boolean) => void; closeDialog: () => void; } // ============================================================================ // 유틸리티 함수 // ============================================================================ /** 기본 기간 계산 (현재 월) */ function getDefaultPeriod(): { start: string; end: string } { const now = new Date(); const start = new Date(now.getFullYear(), now.getMonth(), 1); const end = new Date(now.getFullYear(), now.getMonth() + 1, 0); return { start: start.toISOString().split("T")[0], end: end.toISOString().split("T")[0], }; } // ============================================================================ // 스케줄 생성 서비스 훅 // ============================================================================ /** * 스케줄 자동 생성 훅 * * @param scheduleConfig 스케줄 생성 설정 * @returns 상태 및 핸들러 * * @example * ```tsx * const config: ScheduleGenerationConfig = { * scheduleType: "PRODUCTION", * source: { tableName: "sales_order_mng", groupByField: "part_code", quantityField: "balance_qty" }, * resource: { type: "ITEM", idField: "part_code", nameField: "part_name" }, * rules: { leadTimeDays: 3, dailyCapacity: 100 }, * target: { tableName: "schedule_mng" }, * }; * * const { showConfirmDialog, previewResult, handleConfirm } = useScheduleGenerator(config); * ``` */ export function useScheduleGenerator(scheduleConfig?: ScheduleGenerationConfig | null): UseScheduleGeneratorReturn { // 상태 const [selectedData, setSelectedData] = useState([]); const [previewResult, setPreviewResult] = useState(null); const [showConfirmDialog, setShowConfirmDialog] = useState(false); const [isLoading, setIsLoading] = useState(false); const currentRequestIdRef = useRef(""); const currentConfigRef = useRef(null); // 1. 테이블 선택 데이터 추적 (TABLE_SELECTION_CHANGE 이벤트 수신) useEffect(() => { const unsubscribe = v2EventBus.subscribe(V2_EVENTS.TABLE_SELECTION_CHANGE, (payload) => { // scheduleConfig가 있으면 해당 테이블만, 없으면 모든 테이블의 선택 데이터 저장 if (scheduleConfig?.source?.tableName) { if (payload.tableName === scheduleConfig.source.tableName) { setSelectedData(payload.selectedRows); console.log("[useScheduleGenerator] 선택 데이터 업데이트 (특정 테이블):", payload.selectedCount, "건"); } } else { // scheduleConfig가 없으면 모든 테이블의 선택 데이터를 저장 setSelectedData(payload.selectedRows); console.log("[useScheduleGenerator] 선택 데이터 업데이트 (모든 테이블):", payload.selectedCount, "건"); } }); return unsubscribe; }, [scheduleConfig?.source?.tableName]); // 2. 스케줄 생성 요청 처리 (SCHEDULE_GENERATE_REQUEST 수신) useEffect(() => { const unsubscribe = v2EventBus.subscribe( V2_EVENTS.SCHEDULE_GENERATE_REQUEST, async (payload: V2ScheduleGenerateRequestEvent) => { console.log("[useScheduleGenerator] SCHEDULE_GENERATE_REQUEST 수신:", payload); // 이벤트에서 config가 오면 사용, 없으면 기존 scheduleConfig 또는 기본 config 사용 const configToUse = (payload as any).config || scheduleConfig || { // 기본 설정 (생산계획 화면용) scheduleType: payload.scheduleType || "PRODUCTION", source: { tableName: "sales_order_mng", groupByField: "part_code", quantityField: "balance_qty", dueDateField: "delivery_date", // 기준일 필드 (납기일) }, resource: { type: "ITEM", idField: "part_code", nameField: "part_name", }, rules: { leadTimeDays: 3, dailyCapacity: 100, }, target: { tableName: "schedule_mng", }, }; console.log("[useScheduleGenerator] 사용할 config:", configToUse); // scheduleType이 지정되어 있고 config도 있는 경우, 타입 일치 확인 if (scheduleConfig && payload.scheduleType && payload.scheduleType !== scheduleConfig.scheduleType) { console.log("[useScheduleGenerator] scheduleType 불일치, 무시"); return; } // sourceData: 이벤트 페이로드 > 상태 저장된 선택 데이터 > 빈 배열 const dataToUse = payload.sourceData || selectedData; const periodToUse = payload.period || getDefaultPeriod(); console.log("[useScheduleGenerator] 사용할 sourceData:", dataToUse.length, "건"); console.log("[useScheduleGenerator] 사용할 period:", periodToUse); currentRequestIdRef.current = payload.requestId; currentConfigRef.current = configToUse; setIsLoading(true); toast.loading("스케줄 생성 중...", { id: "schedule-generate" }); try { // 미리보기 API 호출 const response = await apiClient.post("/schedule/preview", { config: configToUse, scheduleType: payload.scheduleType, sourceData: dataToUse, period: periodToUse, }); console.log("[useScheduleGenerator] 미리보기 응답:", response.data); if (!response.data.success) { toast.error(response.data.message || "미리보기 생성 실패", { id: "schedule-generate" }); v2EventBus.emit(V2_EVENTS.SCHEDULE_GENERATE_ERROR, { requestId: payload.requestId, error: response.data.message || "미리보기 생성 실패", scheduleType: payload.scheduleType, }); return; } setPreviewResult(response.data.preview); setShowConfirmDialog(true); toast.success("스케줄 미리보기가 생성되었습니다.", { id: "schedule-generate" }); // 미리보기 결과 이벤트 발송 (다른 컴포넌트가 필요할 수 있음) v2EventBus.emit(V2_EVENTS.SCHEDULE_GENERATE_PREVIEW, { requestId: payload.requestId, scheduleType: payload.scheduleType, preview: response.data.preview, }); } catch (error: any) { console.error("[ScheduleGeneratorService] 미리보기 오류:", error); toast.error("스케줄 생성 중 오류가 발생했습니다.", { id: "schedule-generate" }); v2EventBus.emit(V2_EVENTS.SCHEDULE_GENERATE_ERROR, { requestId: payload.requestId, error: error.message, scheduleType: payload.scheduleType, }); } finally { setIsLoading(false); } }, ); return unsubscribe; }, [selectedData, scheduleConfig]); // 3. 스케줄 적용 처리 (SCHEDULE_GENERATE_APPLY 수신) useEffect(() => { const unsubscribe = v2EventBus.subscribe( V2_EVENTS.SCHEDULE_GENERATE_APPLY, async (payload: V2ScheduleGenerateApplyEvent) => { if (payload.requestId !== currentRequestIdRef.current) return; if (!payload.confirmed) { setShowConfirmDialog(false); return; } // 저장된 config 또는 기존 scheduleConfig 사용 const configToUse = currentConfigRef.current || scheduleConfig; setIsLoading(true); toast.loading("스케줄 적용 중...", { id: "schedule-apply" }); try { const response = await apiClient.post("/schedule/apply", { config: configToUse, preview: previewResult, options: { deleteExisting: true, updateMode: "replace" }, }); if (!response.data.success) { toast.error(response.data.message || "스케줄 적용 실패", { id: "schedule-apply" }); return; } // 완료 이벤트 발송 v2EventBus.emit(V2_EVENTS.SCHEDULE_GENERATE_COMPLETE, { requestId: payload.requestId, success: true, applied: response.data.applied, scheduleType: configToUse?.scheduleType || "PRODUCTION", targetTableName: configToUse?.target?.tableName || "schedule_mng", }); // 테이블 새로고침 이벤트 발송 v2EventBus.emit(V2_EVENTS.TABLE_REFRESH, { tableName: configToUse?.target?.tableName || "schedule_mng", }); toast.success(`${response.data.applied?.created || 0}건의 스케줄이 생성되었습니다.`, { id: "schedule-apply", }); setShowConfirmDialog(false); setPreviewResult(null); } catch (error: any) { console.error("[ScheduleGeneratorService] 적용 오류:", error); toast.error("스케줄 적용 중 오류가 발생했습니다.", { id: "schedule-apply" }); } finally { setIsLoading(false); } }, ); return unsubscribe; }, [previewResult, scheduleConfig]); // 확인 다이얼로그 핸들러 const handleConfirm = useCallback((confirmed: boolean) => { v2EventBus.emit(V2_EVENTS.SCHEDULE_GENERATE_APPLY, { requestId: currentRequestIdRef.current, confirmed, }); }, []); // 다이얼로그 닫기 const closeDialog = useCallback(() => { setShowConfirmDialog(false); setPreviewResult(null); }, []); return { isLoading, showConfirmDialog, previewResult, handleConfirm, closeDialog, }; } // ============================================================================ // 스케줄 확인 다이얼로그 컴포넌트 // ============================================================================ export { ScheduleConfirmDialog } from "./ScheduleConfirmDialog";