335 lines
12 KiB
TypeScript
335 lines
12 KiB
TypeScript
/**
|
|
* 스케줄 자동 생성 서비스
|
|
*
|
|
* 이벤트 버스 기반으로 스케줄 자동 생성을 처리합니다.
|
|
* - 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<any[]>([]);
|
|
const [previewResult, setPreviewResult] = useState<SchedulePreviewResult | null>(null);
|
|
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const currentRequestIdRef = useRef<string>("");
|
|
const currentConfigRef = useRef<ScheduleGenerationConfig | null>(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";
|