From 61b67c361959bfa43bc71caab49ee9ba399f09e5 Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 2 Feb 2026 17:37:13 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20=EC=9E=90=EB=8F=99=20=EC=8A=A4=EC=BC=80?= =?UTF-8?q?=EC=A4=84=20=EC=83=9D=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20=EB=AC=B8=EC=84=9C?= =?UTF-8?q?=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 생산계획 목록에 자동 스케줄 생성 기능에 대한 상세 가이드를 추가하였습니다. - 스케줄 생성의 데이터 흐름과 설정을 명확히 설명하였으며, JSON 형식의 설정 예시를 포함하였습니다. - 버튼 설정 및 연결 필터 설정에 대한 정보를 추가하여 사용자가 기능을 쉽게 이해할 수 있도록 하였습니다. - 구현 상태를 체크리스트 형식으로 정리하여 각 항목의 진행 상황을 명시하였습니다. --- .../schedule-auto-generation-guide.md | 894 ++++++++++++++++++ .../03_production/production-plan.md | 104 +- frontend/lib/v2-core/events/types.ts | 71 ++ 3 files changed, 1067 insertions(+), 2 deletions(-) create mode 100644 docs/screen-implementation-guide/00_analysis/schedule-auto-generation-guide.md diff --git a/docs/screen-implementation-guide/00_analysis/schedule-auto-generation-guide.md b/docs/screen-implementation-guide/00_analysis/schedule-auto-generation-guide.md new file mode 100644 index 00000000..02699843 --- /dev/null +++ b/docs/screen-implementation-guide/00_analysis/schedule-auto-generation-guide.md @@ -0,0 +1,894 @@ +# 스케줄 자동 생성 기능 구현 가이드 + +> 버전: 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); + }); +} +``` diff --git a/docs/screen-implementation-guide/03_production/production-plan.md b/docs/screen-implementation-guide/03_production/production-plan.md index aaba82b6..aa09cb47 100644 --- a/docs/screen-implementation-guide/03_production/production-plan.md +++ b/docs/screen-implementation-guide/03_production/production-plan.md @@ -1209,17 +1209,117 @@ v2-table-list (생산계획 목록) --- -## 16. 관련 문서 +## 16. 자동 스케줄 생성 기능 + +> 상세 가이드: [스케줄 자동 생성 기능 구현 가이드](../00_analysis/schedule-auto-generation-guide.md) + +### 16.1 개요 + +좌측 수주 테이블에서 품목을 선택하고 "자동 스케줄 생성" 버튼을 클릭하면, 선택된 품목들에 대한 생산 스케줄이 자동으로 생성되어 우측 타임라인에 표시됩니다. + +### 16.2 데이터 흐름 + +``` +1. 좌측 v2-table-grouped에서 품목 선택 (그룹 선택 시 자식 포함) +2. "자동 스케줄 생성" 버튼 클릭 +3. 백엔드 API에서 미리보기 생성 (생성/삭제/수정될 스케줄) +4. 변경사항 확인 다이얼로그 표시 +5. 확인 시 스케줄 적용 및 타임라인 새로고침 +6. 다음 방문 시: 좌측 선택 → linkedFilter로 우측 자동 필터링 +``` + +### 16.3 스케줄 생성 설정 + +```json +{ + "scheduleType": "PRODUCTION", + "source": { + "componentId": "order_table", + "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" + }, + "target": { + "tableName": "production_plan_mng", + "timelineComponentId": "production_timeline" + } +} +``` + +### 16.4 버튼 설정 + +```json +{ + "componentType": "v2-button-primary", + "componentId": "btn_auto_schedule", + "componentConfig": { + "label": "자동 스케줄 생성", + "variant": "default", + "icon": "Calendar", + "action": { + "type": "custom", + "customAction": "autoGenerateSchedule", + "scheduleConfig": { /* 위 설정 */ } + } + } +} +``` + +### 16.5 연결 필터 설정 (linkedFilters) + +좌측 테이블 선택 시 우측 타임라인 자동 필터링: + +```json +{ + "linkedFilters": [ + { + "sourceComponentId": "order_table", + "sourceField": "part_code", + "targetColumn": "resource_id" + } + ] +} +``` + +### 16.6 구현 상태 + +| 항목 | 상태 | 비고 | +|------|:----:|------| +| schedule_mng 테이블 | ⏳ 대기 | 범용 스케줄 테이블 | +| /api/schedule/preview API | ⏳ 대기 | 미리보기 | +| /api/schedule/apply API | ⏳ 대기 | 적용 | +| autoGenerateSchedule 버튼 액션 | ⏳ 대기 | buttonActions.ts | +| 확인 다이얼로그 | ⏳ 대기 | 기존 AlertDialog 활용 | +| linkedFilter 연동 | ⏳ 대기 | 타임라인 필터링 | + +--- + +## 17. 관련 문서 - [수주관리](../02_sales/order.md) - [품목정보](../01_master-data/item-info.md) - [설비관리](../05_equipment/equipment-info.md) - [BOM관리](../01_master-data/bom.md) - [작업지시](./work-order.md) +- **[스케줄 자동 생성 기능 가이드](../00_analysis/schedule-auto-generation-guide.md)** --- -## 17. 참고: 표준 가이드 +## 18. 참고: 표준 가이드 - [화면개발 표준 가이드](../화면개발_표준_가이드.md) - [V2 컴포넌트 사용 가이드](../00_analysis/v2-component-usage-guide.md) diff --git a/frontend/lib/v2-core/events/types.ts b/frontend/lib/v2-core/events/types.ts index 8d0075c5..a33e7684 100644 --- a/frontend/lib/v2-core/events/types.ts +++ b/frontend/lib/v2-core/events/types.ts @@ -53,6 +53,13 @@ export const V2_EVENTS = { RELATED_BUTTON_REGISTER: "v2:related-button:register", RELATED_BUTTON_UNREGISTER: "v2:related-button:unregister", RELATED_BUTTON_SELECT: "v2:related-button:select", + + // 스케줄 자동 생성 + 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 type V2EventName = (typeof V2_EVENTS)[keyof typeof V2_EVENTS]; @@ -230,6 +237,64 @@ export interface V2RelatedButtonSelectEvent { selectedData: any[]; } +// ============================================================================ +// 스케줄 자동 생성 이벤트 +// ============================================================================ + +/** 스케줄 타입 */ +export type ScheduleType = "PRODUCTION" | "MAINTENANCE" | "SHIPPING" | "WORK_ASSIGN"; + +/** 스케줄 생성 요청 이벤트 */ +export interface V2ScheduleGenerateRequestEvent { + requestId: string; + scheduleType: ScheduleType; + sourceData?: any[]; // 선택 데이터 (없으면 TABLE_SELECTION_CHANGE로 받은 데이터 사용) + period?: { start: string; end: string }; +} + +/** 스케줄 미리보기 결과 이벤트 */ +export interface V2ScheduleGeneratePreviewEvent { + requestId: string; + scheduleType: ScheduleType; + preview: { + toCreate: any[]; + toDelete: any[]; + toUpdate: any[]; + summary: { + createCount: number; + deleteCount: number; + updateCount: number; + totalQty: number; + }; + }; +} + +/** 스케줄 적용 이벤트 */ +export interface V2ScheduleGenerateApplyEvent { + requestId: string; + confirmed: boolean; +} + +/** 스케줄 생성 완료 이벤트 */ +export interface V2ScheduleGenerateCompleteEvent { + requestId: string; + success: boolean; + applied: { + created: number; + deleted: number; + updated: number; + }; + scheduleType: ScheduleType; + targetTableName: string; +} + +/** 스케줄 생성 에러 이벤트 */ +export interface V2ScheduleGenerateErrorEvent { + requestId: string; + error: string; + scheduleType?: ScheduleType; +} + // ============================================================================ // 이벤트 타입 맵핑 (타입 안전성을 위한) // ============================================================================ @@ -268,6 +333,12 @@ export interface V2EventPayloadMap { [V2_EVENTS.RELATED_BUTTON_REGISTER]: V2RelatedButtonRegisterEvent; [V2_EVENTS.RELATED_BUTTON_UNREGISTER]: V2RelatedButtonUnregisterEvent; [V2_EVENTS.RELATED_BUTTON_SELECT]: V2RelatedButtonSelectEvent; + + [V2_EVENTS.SCHEDULE_GENERATE_REQUEST]: V2ScheduleGenerateRequestEvent; + [V2_EVENTS.SCHEDULE_GENERATE_PREVIEW]: V2ScheduleGeneratePreviewEvent; + [V2_EVENTS.SCHEDULE_GENERATE_APPLY]: V2ScheduleGenerateApplyEvent; + [V2_EVENTS.SCHEDULE_GENERATE_COMPLETE]: V2ScheduleGenerateCompleteEvent; + [V2_EVENTS.SCHEDULE_GENERATE_ERROR]: V2ScheduleGenerateErrorEvent; } // ============================================================================