# 스케줄 자동 생성 기능 구현 가이드

> 버전: 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<any[]>([]);
  const [previewResult, setPreviewResult] = useState<any>(null);
  const [showConfirmDialog, setShowConfirmDialog] = useState(false);
  const [currentRequestId, setCurrentRequestId] = useState<string>("");
  
  // 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);
  });
}
```

---

## 12. 생산기간(리드타임) 산출 - 현재 상태 및 개선 방안

> 작성일: 2026-03-16 | 상태: 검토 대기 (스키마 변경 전 상의 필요)

### 12.1 현재 구현 상태

**생산일수 계산 로직** (`productionPlanService.ts`):

```
생산일수 = ceil(계획수량 / 일생산능력)
종료일 = 납기일 - 안전리드타임
시작일 = 종료일 - 생산일수
```

**현재 기본값 (하드코딩):**

| 항목 | 현재값 | 위치 |
|------|--------|------|
| 일생산능력 (daily_capacity) | 800 EA/일 | `productionPlanService.ts` 기본값 |
| 시간당 능력 (hourly_capacity) | 100 EA/시간 | `productionPlanService.ts` 기본값 |
| 안전리드타임 (safety_lead_time) | 1일 | 옵션 기본값 |
| 반제품 리드타임 (lead_time) | 1일 | `production_plan_mng` 기본값 |

**문제점:**
- `item_info`에 생산 파라미터 컬럼이 없음
- 모든 품목이 동일한 기본값(800EA/일)으로 계산됨
- 업체별/품목별 생산능력 차이를 반영 불가

### 12.2 개선 방향 (상의 후 결정)

**1단계 (품목 마스터 기반) - 권장:**

`item_info` 테이블에 컬럼 추가:
- `lead_time_days`: 리드타임 (일)
- `daily_capacity`: 일생산능력
- `min_lot_size`: 최소 생산 단위 (선택)
- `setup_time`: 셋업시간 (선택)

자동 스케줄 생성 시 품목 마스터 조회 → 값 없으면 기본값 사용 (하위 호환)

**2단계 (설비별 능력) - 고객 요청 시:**

별도 테이블 `item_equipment_capacity`:
- 품목 + 설비 조합별 생산능력 관리
- 동일 품목이라도 설비에 따라 능력 다를 때

**3단계 (공정 라우팅) - 대기업 대응:**

공정 순서 + 공정별 소요시간 전체 관리
- 현재 시점에서는 불필요

### 12.3 반제품 계획 생성 현황

**구현 완료 항목:**
- API: `POST /production/generate-semi-schedule/preview` (미리보기)
- API: `POST /production/generate-semi-schedule` (실제 생성)
- BOM 기반 소요량 자동 계산
- 타임라인 컴포넌트 내 "반제품 계획 생성" 버튼 (완제품 탭에서만 표시)
- 반제품 탭: linkedFilter 제거, staticFilters만 사용 (전체 반제품 표시)

**반제품 생산기간 계산:**
- 반제품 납기일 = 완제품 시작일
- 반제품 시작일 = 완제품 시작일 - lead_time (기본 1일)
- BOM 소요량 = 완제품 계획수량 x BOM 수량

**테스트 BOM 데이터:**

| 완제품 | 반제품 | BOM 수량 |
|--------|--------|----------|
| ITEM-001 (탑씰 Type A) | SEMI-001 (탑씰 필름 A) | 2 EA/개 |
| ITEM-001 (탑씰 Type A) | SEMI-002 (탑씰 접착제) | 0.5 KG/개 |
| ITEM-002 (탑씰 Type B) | SEMI-003 (탑씰 필름 B) | 3 EA/개 |
| ITEM-002 (탑씰 Type B) | SEMI-004 (탑씰 코팅제) | 0.3 KG/개 |
