ae4fe7a66e
- Added new entries to .gitignore for multi-agent MCP task queue and related rules. - Removed "즉시 저장" (quick insert) options from the ScreenSettingModal and BasicTab components to streamline button configurations. - Cleaned up unused event options in the V2ButtonConfigPanel to enhance clarity and maintainability. These changes aim to improve project organization and simplify the user interface by eliminating redundant options.
338 lines
13 KiB
TypeScript
338 lines
13 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";
|
|
import { showErrorToast } from "@/lib/utils/toastUtils";
|
|
|
|
// ============================================================================
|
|
// 타입 정의
|
|
// ============================================================================
|
|
|
|
/** 스케줄 생성 설정 */
|
|
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.getFullYear()}-${String(start.getMonth() + 1).padStart(2, "0")}-${String(start.getDate()).padStart(2, "0")}`,
|
|
end: `${end.getFullYear()}-${String(end.getMonth() + 1).padStart(2, "0")}-${String(end.getDate()).padStart(2, "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.dismiss("schedule-generate");
|
|
showErrorToast("스케줄 미리보기 생성에 실패했습니다", error, { guidance: "스케줄 설정을 확인하고 다시 시도해 주세요." });
|
|
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.dismiss("schedule-apply");
|
|
showErrorToast("스케줄 적용에 실패했습니다", error, { guidance: "스케줄 설정과 데이터를 확인하고 다시 시도해 주세요." });
|
|
} 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";
|