# 스케줄 자동 생성 기능 구현 가이드 > 버전: 2.0 > 최종 수정: 2025-02-02 > 적용 화면: 생산계획관리, 설비계획관리, 출하계획관리 등 ## 1. 개요 ### 1.1 기능 설명 좌측 테이블에서 선택한 데이터(수주, 작업지시 등)를 기반으로 우측 타임라인에 스케줄을 자동 생성하는 기능입니다. ### 1.2 주요 특징 - **범용성**: 설정 기반으로 다양한 화면에서 재사용 가능 - **미리보기**: 적용 전 변경사항 확인 가능 - **소스 추적**: 스케줄이 어디서 생성되었는지 추적 가능 - **연결 필터**: 좌측 선택 시 우측 타임라인 자동 필터링 - **이벤트 버스 기반**: 컴포넌트 간 느슨한 결합 (Loose Coupling) ### 1.3 아키텍처 원칙 **이벤트 버스 패턴**을 활용하여 컴포넌트 간 직접 참조를 제거합니다: ``` ┌─────────────────┐ 이벤트 발송 ┌─────────────────┐ │ v2-button │ ──────────────────▶ │ EventBus │ │ (발송만 함) │ │ (중재자) │ └─────────────────┘ └────────┬────────┘ │ ┌────────────────────────────┼────────────────────────────┐ │ │ │ ▼ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ ScheduleService │ │ v2-timeline │ │ 기타 리스너 │ │ (처리 담당) │ │ (갱신) │ │ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ ``` **장점**: - 버튼은 데이터가 어디서 오는지 알 필요 없음 - 테이블은 누가 데이터를 사용하는지 알 필요 없음 - 컴포넌트 교체/추가 시 기존 코드 수정 불필요 --- ## 2. 데이터 흐름 ### 2.1 전체 흐름도 ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ 분할 패널 (SplitPanelLayout) │ ├───────────────────────────────┬─────────────────────────────────────────────┤ │ 좌측 패널 │ 우측 패널 │ │ │ │ │ ┌─────────────────────────┐ │ ┌─────────────────────────────────────┐ │ │ │ v2-table-grouped │ │ │ 자동 스케줄 생성 버튼 │ │ │ │ (수주 목록) │ │ │ ↓ │ │ │ │ │ │ │ ① 좌측 선택 데이터 가져오기 │ │ │ │ ☑ ITEM-001 (탕핑 A) │──┼──│ ② 백엔드 API 호출 (미리보기) │ │ │ │ └ SO-2025-101 │ │ │ ③ 변경사항 다이얼로그 표시 │ │ │ │ └ SO-2025-102 │ │ │ ④ 적용 API 호출 │ │ │ │ ☐ ITEM-002 (탕핑 B) │ │ │ ⑤ 타임라인 새로고침 │ │ │ │ └ SO-2025-201 │ │ └─────────────────────────────────────┘ │ │ └─────────────────────────┘ │ │ │ │ │ ┌─────────────────────────────────────┐ │ │ │ linkedFilter │ │ v2-timeline-scheduler │ │ │ └──────────────────┼──│ (생산 타임라인) │ │ │ │ │ │ │ │ │ │ part_code = 선택된 품목 필터링 │ │ │ │ └─────────────────────────────────────┘ │ └───────────────────────────────┴─────────────────────────────────────────────┘ ``` ### 2.2 단계별 데이터 흐름 | 단계 | 동작 | 데이터 | |------|------|--------| | 1 | 좌측 테이블에서 품목 선택 | `selectedItems[]` (그룹 선택 시 자식 포함) | | 2 | 자동 스케줄 생성 버튼 클릭 | 버튼 액션 실행 | | 3 | 미리보기 API 호출 | `{ config, sourceData, period }` | | 4 | 변경사항 다이얼로그 표시 | `{ toCreate, toDelete, summary }` | | 5 | 적용 API 호출 | `{ config, preview, options }` | | 6 | 타임라인 새로고침 | `TABLE_REFRESH` 이벤트 발송 | | 7 | 다음 방문 시 좌측 선택 | `linkedFilter`로 우측 자동 필터링 | --- ## 3. 테이블 구조 설계 ### 3.1 범용 스케줄 테이블 (schedule_mng) ```sql CREATE TABLE schedule_mng ( schedule_id SERIAL PRIMARY KEY, company_code VARCHAR(20) NOT NULL, -- 스케줄 기본 정보 schedule_type VARCHAR(50) NOT NULL, -- 'PRODUCTION', 'SHIPPING', 'MAINTENANCE' 등 schedule_name VARCHAR(200), -- 리소스 연결 (타임라인 Y축) resource_type VARCHAR(50) NOT NULL, -- 'ITEM', 'MACHINE', 'WORKER' 등 resource_id VARCHAR(50) NOT NULL, -- 품목코드, 설비코드 등 resource_name VARCHAR(200), -- 일정 start_date TIMESTAMP NOT NULL, end_date TIMESTAMP NOT NULL, -- 수량/값 plan_qty NUMERIC(15,3), actual_qty NUMERIC(15,3), -- 상태 status VARCHAR(20) DEFAULT 'PLANNED', -- PLANNED, IN_PROGRESS, COMPLETED, CANCELLED -- 소스 추적 (어디서 생성되었는지) source_table VARCHAR(100), -- 'sales_order_mng', 'work_order_mng' 등 source_id VARCHAR(50), -- 소스 테이블의 PK source_group_key VARCHAR(100), -- 그룹 키 (품목코드 등) -- 자동 생성 여부 auto_generated BOOLEAN DEFAULT FALSE, generated_at TIMESTAMP, generated_by VARCHAR(50), -- 메타데이터 (추가 정보 JSON) metadata JSONB, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), CONSTRAINT fk_schedule_company FOREIGN KEY (company_code) REFERENCES company_mng(company_code) ); -- 인덱스 CREATE INDEX idx_schedule_company ON schedule_mng(company_code); CREATE INDEX idx_schedule_type ON schedule_mng(schedule_type); CREATE INDEX idx_schedule_resource ON schedule_mng(resource_type, resource_id); CREATE INDEX idx_schedule_source ON schedule_mng(source_table, source_id); CREATE INDEX idx_schedule_date ON schedule_mng(start_date, end_date); CREATE INDEX idx_schedule_status ON schedule_mng(status); ``` ### 3.2 소스-스케줄 매핑 테이블 (N:M 관계) ```sql -- 하나의 스케줄이 여러 소스에서 생성될 수 있음 CREATE TABLE schedule_source_mapping ( mapping_id SERIAL PRIMARY KEY, company_code VARCHAR(20) NOT NULL, schedule_id INTEGER REFERENCES schedule_mng(schedule_id) ON DELETE CASCADE, -- 소스 정보 source_table VARCHAR(100) NOT NULL, source_id VARCHAR(50) NOT NULL, source_qty NUMERIC(15,3), -- 해당 소스에서 기여한 수량 created_at TIMESTAMP DEFAULT NOW(), CONSTRAINT fk_mapping_company FOREIGN KEY (company_code) REFERENCES company_mng(company_code) ); CREATE INDEX idx_mapping_schedule ON schedule_source_mapping(schedule_id); CREATE INDEX idx_mapping_source ON schedule_source_mapping(source_table, source_id); ``` ### 3.3 테이블 관계도 ``` ┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐ │ sales_order_mng │ │ schedule_mng │ │ schedule_source_ │ │ (소스 테이블) │ │ (스케줄 테이블) │ │ mapping │ ├─────────────────────┤ ├─────────────────────┤ ├─────────────────────┤ │ order_id (PK) │───────│ source_id │ │ mapping_id (PK) │ │ part_code │ │ schedule_id (PK) │──1:N──│ schedule_id (FK) │ │ order_qty │ │ resource_id │ │ source_table │ │ balance_qty │ │ start_date │ │ source_id │ │ due_date │ │ end_date │ │ source_qty │ └─────────────────────┘ │ plan_qty │ └─────────────────────┘ │ status │ │ auto_generated │ └─────────────────────┘ ``` --- ## 4. 스케줄 생성 설정 구조 ### 4.1 TypeScript 인터페이스 ```typescript // 화면 레벨 설정 (screen_definitions 또는 screen_layouts_v2에 저장) interface ScheduleGenerationConfig { // 스케줄 타입 scheduleType: "PRODUCTION" | "SHIPPING" | "MAINTENANCE" | "WORK_ASSIGN"; // 소스 설정 (컴포넌트 ID 불필요 - 이벤트로 데이터 수신) 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 또는 전용 테이블) }; } ``` > **주의**: 기존 설계와 달리 `source.componentId`와 `target.timelineComponentId`가 제거되었습니다. > 이벤트 버스를 통해 데이터가 전달되므로 컴포넌트 ID를 직접 참조할 필요가 없습니다. ### 4.2 화면별 설정 예시 #### 생산계획관리 화면 ```json { "scheduleType": "PRODUCTION", "source": { "tableName": "sales_order_mng", "groupByField": "part_code", "quantityField": "balance_qty", "dueDateField": "due_date" }, "resource": { "type": "ITEM", "idField": "part_code", "nameField": "part_name" }, "rules": { "leadTimeDays": 3, "dailyCapacity": 100, "workingDays": [1, 2, 3, 4, 5], "considerStock": true, "stockTableName": "inventory_mng", "stockQtyField": "current_qty", "safetyStockField": "safety_stock" }, "target": { "tableName": "schedule_mng" } } ``` #### 설비계획관리 화면 ```json { "scheduleType": "MAINTENANCE", "source": { "tableName": "work_order_mng", "groupByField": "machine_code", "quantityField": "work_hours" }, "resource": { "type": "MACHINE", "idField": "machine_code", "nameField": "machine_name" }, "rules": { "workingDays": [1, 2, 3, 4, 5, 6] }, "target": { "tableName": "schedule_mng" } } ``` --- ## 5. 백엔드 API 설계 ### 5.1 미리보기 API ```typescript // POST /api/schedule/preview interface PreviewRequest { config: ScheduleGenerationConfig; sourceData: any[]; // 선택된 소스 데이터 period: { start: string; // ISO 날짜 문자열 end: string; }; } interface PreviewResponse { success: boolean; preview: { toCreate: ScheduleItem[]; // 생성될 스케줄 toDelete: ScheduleItem[]; // 삭제될 기존 스케줄 toUpdate: ScheduleItem[]; // 수정될 스케줄 summary: { createCount: number; deleteCount: number; updateCount: number; totalQty: number; }; }; } ``` ### 5.2 적용 API ```typescript // POST /api/schedule/apply interface ApplyRequest { config: ScheduleGenerationConfig; preview: PreviewResponse["preview"]; options: { deleteExisting: boolean; // 기존 스케줄 삭제 여부 updateMode: "replace" | "merge"; }; } interface ApplyResponse { success: boolean; applied: { created: number; deleted: number; updated: number; }; } ``` ### 5.3 스케줄 조회 API (타임라인용) ```typescript // GET /api/schedule/list interface ListQuery { scheduleType: string; resourceType: string; resourceId?: string; // 필터링 (linkedFilter에서 사용) startDate: string; endDate: string; } interface ListResponse { success: boolean; data: ScheduleItem[]; total: number; } ``` --- ## 6. 이벤트 버스 기반 구현 ### 6.1 이벤트 타입 정의 ```typescript // frontend/lib/v2-core/events/types.ts에 추가 export const V2_EVENTS = { // ... 기존 이벤트들 // 스케줄 생성 이벤트 SCHEDULE_GENERATE_REQUEST: "v2:schedule:generate:request", SCHEDULE_GENERATE_PREVIEW: "v2:schedule:generate:preview", SCHEDULE_GENERATE_APPLY: "v2:schedule:generate:apply", SCHEDULE_GENERATE_COMPLETE: "v2:schedule:generate:complete", SCHEDULE_GENERATE_ERROR: "v2:schedule:generate:error", } as const; /** 스케줄 생성 요청 이벤트 */ export interface V2ScheduleGenerateRequestEvent { requestId: string; scheduleType: "PRODUCTION" | "MAINTENANCE" | "SHIPPING" | "WORK_ASSIGN"; sourceData?: any[]; // 선택 데이터 (없으면 TABLE_SELECTION_CHANGE로 받은 데이터 사용) period?: { start: string; end: string }; } /** 스케줄 미리보기 결과 이벤트 */ export interface V2ScheduleGeneratePreviewEvent { requestId: string; preview: { toCreate: any[]; toDelete: any[]; summary: { createCount: number; deleteCount: number; totalQty: number }; }; } /** 스케줄 적용 이벤트 */ export interface V2ScheduleGenerateApplyEvent { requestId: string; confirmed: boolean; } /** 스케줄 생성 완료 이벤트 */ export interface V2ScheduleGenerateCompleteEvent { requestId: string; success: boolean; applied: { created: number; deleted: number }; scheduleType: string; } ``` ### 6.2 버튼 설정 (간소화) ```json { "componentType": "v2-button-primary", "componentId": "btn_auto_schedule", "componentConfig": { "label": "자동 스케줄 생성", "variant": "default", "icon": "Calendar", "action": { "type": "event", "eventName": "SCHEDULE_GENERATE_REQUEST", "eventPayload": { "scheduleType": "PRODUCTION" } } } } ``` > **핵심**: 버튼은 이벤트만 발송하고, 데이터가 어디서 오는지 알 필요 없음 ### 6.3 스케줄 생성 서비스 (이벤트 리스너) ```typescript // frontend/lib/v2-core/services/ScheduleGeneratorService.ts import { v2EventBus, V2_EVENTS } from "@/lib/v2-core"; import apiClient from "@/lib/api/client"; import { toast } from "sonner"; export function useScheduleGenerator(scheduleConfig: ScheduleGenerationConfig) { const [selectedData, setSelectedData] = useState([]); const [previewResult, setPreviewResult] = useState(null); const [showConfirmDialog, setShowConfirmDialog] = useState(false); const [currentRequestId, setCurrentRequestId] = useState(""); // 1. 테이블 선택 데이터 추적 (TABLE_SELECTION_CHANGE 이벤트 수신) useEffect(() => { const unsubscribe = v2EventBus.on( V2_EVENTS.TABLE_SELECTION_CHANGE, (payload) => { // 설정된 소스 테이블과 일치하는 경우에만 저장 if (payload.tableName === scheduleConfig.source.tableName) { setSelectedData(payload.selectedRows); } } ); return unsubscribe; }, [scheduleConfig.source.tableName]); // 2. 스케줄 생성 요청 처리 (SCHEDULE_GENERATE_REQUEST 수신) useEffect(() => { const unsubscribe = v2EventBus.on( V2_EVENTS.SCHEDULE_GENERATE_REQUEST, async (payload) => { // 스케줄 타입이 일치하는 경우에만 처리 if (payload.scheduleType !== scheduleConfig.scheduleType) { return; } const dataToUse = payload.sourceData || selectedData; if (dataToUse.length === 0) { toast.warning("품목을 선택해주세요."); return; } setCurrentRequestId(payload.requestId); try { // 미리보기 API 호출 const response = await apiClient.post("/api/schedule/preview", { config: scheduleConfig, sourceData: dataToUse, period: payload.period || getDefaultPeriod(), }); if (!response.data.success) { toast.error(response.data.message); v2EventBus.emit(V2_EVENTS.SCHEDULE_GENERATE_ERROR, { requestId: payload.requestId, error: response.data.message, }); return; } setPreviewResult(response.data.preview); setShowConfirmDialog(true); // 미리보기 결과 이벤트 발송 (다른 컴포넌트가 필요할 수 있음) v2EventBus.emit(V2_EVENTS.SCHEDULE_GENERATE_PREVIEW, { requestId: payload.requestId, preview: response.data.preview, }); } catch (error: any) { toast.error("스케줄 생성 중 오류가 발생했습니다."); v2EventBus.emit(V2_EVENTS.SCHEDULE_GENERATE_ERROR, { requestId: payload.requestId, error: error.message, }); } } ); return unsubscribe; }, [selectedData, scheduleConfig]); // 3. 스케줄 적용 처리 (SCHEDULE_GENERATE_APPLY 수신) useEffect(() => { const unsubscribe = v2EventBus.on( V2_EVENTS.SCHEDULE_GENERATE_APPLY, async (payload) => { if (payload.requestId !== currentRequestId) return; if (!payload.confirmed) { setShowConfirmDialog(false); return; } try { const response = await apiClient.post("/api/schedule/apply", { config: scheduleConfig, preview: previewResult, options: { deleteExisting: true, updateMode: "replace" }, }); if (!response.data.success) { toast.error(response.data.message); return; } // 완료 이벤트 발송 v2EventBus.emit(V2_EVENTS.SCHEDULE_GENERATE_COMPLETE, { requestId: payload.requestId, success: true, applied: response.data.applied, scheduleType: scheduleConfig.scheduleType, }); // 테이블 새로고침 이벤트 발송 v2EventBus.emit(V2_EVENTS.TABLE_REFRESH, { tableName: scheduleConfig.target.tableName, }); toast.success(`${response.data.applied.created}건의 스케줄이 생성되었습니다.`); setShowConfirmDialog(false); } catch (error: any) { toast.error("스케줄 적용 중 오류가 발생했습니다."); } } ); return unsubscribe; }, [currentRequestId, previewResult, scheduleConfig]); // 확인 다이얼로그 핸들러 const handleConfirm = (confirmed: boolean) => { v2EventBus.emit(V2_EVENTS.SCHEDULE_GENERATE_APPLY, { requestId: currentRequestId, confirmed, }); }; return { showConfirmDialog, previewResult, handleConfirm, }; } function getDefaultPeriod() { 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], }; } ``` ### 6.4 타임라인 컴포넌트 (이벤트 수신) ```typescript // v2-timeline-scheduler에서 이벤트 수신 useEffect(() => { // 스케줄 생성 완료 시 자동 새로고침 const unsubscribe1 = v2EventBus.on( V2_EVENTS.SCHEDULE_GENERATE_COMPLETE, (payload) => { if (payload.success && payload.scheduleType === config.scheduleType) { fetchSchedules(); } } ); // TABLE_REFRESH 이벤트로도 새로고침 const unsubscribe2 = v2EventBus.on( V2_EVENTS.TABLE_REFRESH, (payload) => { if (payload.tableName === config.selectedTable) { fetchSchedules(); } } ); return () => { unsubscribe1(); unsubscribe2(); }; }, [config.selectedTable, config.scheduleType]); ``` ### 6.5 버튼 액션 핸들러 (이벤트 발송) ```typescript // frontend/lib/utils/buttonActions.ts // 기존 handleButtonAction에 추가 case "event": const eventName = action.eventName as keyof typeof V2_EVENTS; const eventPayload = { requestId: crypto.randomUUID(), ...action.eventPayload, }; v2EventBus.emit(V2_EVENTS[eventName], eventPayload); return true; ``` --- ## 7. 컴포넌트 연동 설정 ### 7.1 분할 패널 연결 필터 (linkedFilters) 좌측 테이블 선택 시 우측 타임라인 자동 필터링: ```json { "componentType": "v2-split-panel-layout", "componentConfig": { "linkedFilters": [ { "sourceComponentId": "order_table", "sourceField": "part_code", "targetColumn": "resource_id" } ] } } ``` ### 7.2 타임라인 설정 ```json { "componentType": "v2-timeline-scheduler", "componentId": "production_timeline", "componentConfig": { "selectedTable": "production_plan_mng", "fieldMapping": { "id": "schedule_id", "resourceId": "resource_id", "title": "schedule_name", "startDate": "start_date", "endDate": "end_date", "status": "status" }, "useLinkedFilter": true } } ``` ### 7.3 이벤트 흐름도 (Event-Driven) ``` [좌측 테이블 선택] │ ▼ v2-table-grouped.onSelectionChange │ ▼ emit(TABLE_SELECTION_CHANGE) │ ├───────────────────────────────────────────────────┐ │ │ ▼ ▼ ScheduleGeneratorService SplitPanelContext (selectedData 저장) (linkedFilter 업데이트) │ ▼ v2-timeline-scheduler (자동 필터링) [자동 스케줄 생성 버튼 클릭] │ ▼ emit(SCHEDULE_GENERATE_REQUEST) │ ▼ ScheduleGeneratorService (이벤트 리스너) │ ├─── selectedData (이미 저장됨) │ ▼ POST /api/schedule/preview │ ▼ emit(SCHEDULE_GENERATE_PREVIEW) │ ▼ 확인 다이얼로그 표시 │ ▼ (확인 클릭) emit(SCHEDULE_GENERATE_APPLY) │ ▼ POST /api/schedule/apply │ ├─── emit(SCHEDULE_GENERATE_COMPLETE) │ ├─── emit(TABLE_REFRESH) │ ▼ v2-timeline-scheduler (on TABLE_REFRESH) │ ▼ fetchSchedules() → 화면 갱신 ``` ### 7.4 이벤트 시퀀스 다이어그램 ``` ┌──────────┐ ┌──────────┐ ┌──────────────┐ ┌──────────┐ ┌──────────┐ │ Table │ │ Button │ │ ScheduleSvc │ │ Backend │ │ Timeline │ └────┬─────┘ └────┬─────┘ └──────┬───────┘ └────┬─────┘ └────┬─────┘ │ │ │ │ │ │ SELECT │ │ │ │ ├──────────────────────────────▶ │ │ │ │ TABLE_SELECTION_CHANGE │ │ │ │ │ │ │ │ │ │ CLICK │ │ │ │ ├────────────────▶│ │ │ │ │ SCHEDULE_GENERATE_REQUEST │ │ │ │ │ │ │ │ │ ├────────────────▶│ │ │ │ │ POST /preview │ │ │ │ │◀────────────────┤ │ │ │ │ │ │ │ │ │ CONFIRM DIALOG │ │ │ │ │─────────────────│ │ │ │ │ │ │ │ │ ├────────────────▶│ │ │ │ │ POST /apply │ │ │ │ │◀────────────────┤ │ │ │ │ │ │ │ │ ├─────────────────────────────────▶ │ │ │ SCHEDULE_GENERATE_COMPLETE │ │ │ │ │ │ │ ├─────────────────────────────────▶ │ │ │ TABLE_REFRESH │ │ │ │ │ │ │ │ │ ├──▶ refresh │ │ │ │ │ ``` --- ## 8. 범용성 활용 가이드 ### 8.1 다른 화면에서 재사용 | 화면 | 소스 테이블 | 그룹 필드 | 스케줄 타입 | 리소스 타입 | |------|------------|----------|------------|------------| | 생산계획 | sales_order_mng | part_code | PRODUCTION | ITEM | | 설비계획 | work_order_mng | machine_code | MAINTENANCE | MACHINE | | 출하계획 | shipment_order_mng | customer_code | SHIPPING | CUSTOMER | | 작업자 배치 | task_mng | worker_id | WORK_ASSIGN | WORKER | ### 8.2 새 화면 추가 시 체크리스트 - [ ] 소스 테이블 정의 (어떤 데이터를 선택할 것인지) - [ ] 그룹화 기준 필드 정의 (품목, 설비, 고객 등) - [ ] 스케줄 테이블 생성 또는 기존 schedule_mng 사용 - [ ] ScheduleGenerationConfig 작성 - [ ] 버튼에 scheduleConfig 설정 - [ ] 분할 패널 linkedFilters 설정 - [ ] 타임라인 fieldMapping 설정 --- ## 9. 구현 순서 | 단계 | 작업 | 상태 | |------|------|------| | 1 | 테이블 마이그레이션 (schedule_mng, schedule_source_mapping) | 대기 | | 2 | 백엔드 API (scheduleController, scheduleService) | 대기 | | 3 | 버튼 액션 핸들러 (autoGenerateSchedule) | 대기 | | 4 | 확인 다이얼로그 (기존 AlertDialog 활용) | 대기 | | 5 | 타임라인 linkedFilter 연동 | 대기 | | 6 | 테스트 및 검증 | 대기 | --- ## 10. 참고 사항 ### 관련 컴포넌트 - `v2-table-grouped`: 그룹화된 테이블 (소스 데이터, TABLE_SELECTION_CHANGE 발송) - `v2-timeline-scheduler`: 타임라인 스케줄러 (TABLE_REFRESH 수신) - `v2-button-primary`: 액션 버튼 (SCHEDULE_GENERATE_REQUEST 발송) - `v2-split-panel-layout`: 분할 패널 ### 관련 파일 - `frontend/lib/v2-core/events/types.ts`: 이벤트 타입 정의 - `frontend/lib/v2-core/events/EventBus.ts`: 이벤트 버스 - `frontend/lib/v2-core/services/ScheduleGeneratorService.ts`: 스케줄 생성 서비스 (이벤트 리스너) - `frontend/lib/utils/buttonActions.ts`: 버튼 액션 핸들러 (이벤트 발송) - `backend-node/src/services/scheduleService.ts`: 스케줄 서비스 - `backend-node/src/controllers/scheduleController.ts`: 스케줄 컨트롤러 ### 특이 사항 - v2-table-grouped의 `selectedItems`는 그룹 선택 시 자식 행까지 포함됨 - 스케줄 생성 시 기존 스케줄과 비교하여 변경사항만 적용 (미리보기 제공) - source_table, source_id로 소스 추적 가능 - **컴포넌트 ID 직접 참조 없음** - 이벤트 버스로 느슨한 결합 --- ## 11. 이벤트 버스 패턴의 장점 ### 11.1 기존 방식 vs 이벤트 버스 방식 | 항목 | 기존 (직접 참조) | 이벤트 버스 | |------|------------------|-------------| | 결합도 | 강 (componentId 필요) | 약 (이벤트명만 필요) | | 버튼 설정 | `source.componentId: "order_table"` | `eventPayload.scheduleType: "PRODUCTION"` | | 컴포넌트 교체 | 설정 수정 필요 | 이벤트만 발송/수신하면 됨 | | 테스트 | 컴포넌트 모킹 필요 | 이벤트 발송으로 테스트 가능 | | 디버깅 | 쉬움 | 이벤트 로깅 필요 | ### 11.2 확장성 새로운 컴포넌트 추가 시: 1. 기존 컴포넌트 수정 불필요 2. 새 컴포넌트에서 이벤트 구독만 추가 3. 이벤트 페이로드 구조만 유지하면 됨 ```typescript // 새로운 컴포넌트에서 스케줄 생성 완료 이벤트 구독 useEffect(() => { const unsubscribe = v2EventBus.on( V2_EVENTS.SCHEDULE_GENERATE_COMPLETE, (payload) => { // 새로운 로직 추가 console.log("스케줄 생성 완료:", payload); } ); return unsubscribe; }, []); ``` ### 11.3 디버깅 팁 ```typescript // 이벤트 디버깅용 전역 리스너 (개발 환경에서만) if (process.env.NODE_ENV === "development") { v2EventBus.on("*", (event, payload) => { console.log(`[EventBus] ${event}:`, payload); }); } ```