# 수주-작업지시-출하 하이브리드 연동 구조 구현 계획

> **작성일**: 2025-01-19
> **목적**: Order → WorkOrder → Shipment 간 FK 연결 강화 및 상태 동기화 로직 구현
> **기준 문서**: `api/app/Models/Orders/Order.php`, `api/app/Models/Production/WorkOrder.php`, `api/app/Models/Tenants/Shipment.php`
> **상태**: 📋 계획 수립 완료 (Serena ID: order-integration-state)

---

## 📍 현재 진행 상태

| 항목 | 내용 |
|------|------|
| **마지막 완료 작업** | Phase 4: 작업완료 시 자동 출하 생성 기능 구현 |
| **다음 작업** | ✅ 모든 Phase 완료 |
| **진행률** | 4/4 Phase (100%) |
| **마지막 업데이트** | 2025-01-19 |

---

## 1. 개요

### 1.1 배경

현재 SAM 시스템은 수주(Order), 작업지시(WorkOrder), 출하(Shipment)가 독립적으로 운영되고 있습니다.

**현재 문제점:**
- `shipments` 테이블에 `work_order_id` FK가 없음
- 작업 완료 시 출하로 자동 연결되지 않음
- Order의 전체 진행 상태를 추적할 수 없음
- 데이터 정합성 보장이 어려움

**목표:**
- 하이브리드 마스터-디테일 구조로 전환
- `orders.status_code`로 전체 진행 상태 추적
- 각 단계별 상태 변경 시 연관 테이블 자동 동기화

### 1.2 목표 구조

```
┌─────────────────────────────────────────────────────────────────┐
│                        목표 구조                                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   orders (마스터)                                                │
│   ├─ status_code: 전체 진행상태 추적                             │
│   │   DRAFT → CONFIRMED → IN_PRODUCTION → PRODUCED              │
│   │   → SHIPPING → SHIPPED → COMPLETED                          │
│   │                                                              │
│   ├──(1:N)──▶ work_orders (생산 상세)                           │
│   │              ├─ sales_order_id FK ✅ (기존)                  │
│   │              └─ status: 생산 프로세스 상태                   │
│   │                                                              │
│   └──(1:N)──▶ shipments (출하 상세)                             │
│                  ├─ order_id FK ✅ (기존)                        │
│                  ├─ work_order_id FK 🆕 (신규 추가)              │
│                  └─ status: 출하 프로세스 상태                   │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘
```

### 1.3 기준 원칙

```
┌─────────────────────────────────────────────────────────────────┐
│  🎯 핵심 원칙                                                    │
├─────────────────────────────────────────────────────────────────┤
│  1. orders.status_code = 전체 프로세스의 Single Source of Truth │
│  2. 하위 테이블(work_orders, shipments)은 상세 정보만 관리       │
│  3. 상태 변경 시 상위 테이블 자동 동기화                         │
│  4. 기존 데이터 호환성 유지 (work_order_id는 nullable)           │
└─────────────────────────────────────────────────────────────────┘
```

### 1.4 변경 승인 정책

| 분류 | 예시 | 승인 |
|------|------|------|
| ✅ 즉시 가능 | 모델 관계 추가, 상수 추가, 문서 수정 | 불필요 |
| ⚠️ 컨펌 필요 | 마이그레이션, 서비스 로직 변경, 상태 동기화 | **필수** |
| 🔴 금지 | 기존 테이블 구조 파괴적 변경, 기존 API 삭제 | 별도 협의 |

### 1.5 준수 규칙

- `docs/quickstart/quick-start.md` - 빠른 시작 가이드
- `docs/standards/quality-checklist.md` - 품질 체크리스트
- `CLAUDE.md` - SAM API Development Rules

---

## 2. 대상 범위

### 2.1 Phase 1: DB 스키마 수정

| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 1.1 | `shipments` 테이블에 `work_order_id` FK 추가 마이그레이션 | ⏳ | nullable, index 포함 |

### 2.2 Phase 2: 모델 관계 추가

| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 2.1 | Order 모델에 `shipments()` HasMany 관계 추가 | ⏳ | |
| 2.2 | WorkOrder 모델에 `shipments()` HasMany 관계 추가 | ⏳ | |
| 2.3 | Shipment 모델에 `workOrder()` BelongsTo 관계 추가 | ⏳ | |
| 2.4 | Shipment 모델에 `work_order_id` fillable 추가 | ⏳ | |

### 2.3 Phase 3: Order 상태 확장 및 동기화 로직

| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 3.1 | Order 모델에 생산/출하 관련 상태 상수 추가 | ⏳ | IN_PRODUCTION, PRODUCED, SHIPPING, SHIPPED |
| 3.2 | WorkOrderService에 Order 상태 동기화 로직 추가 | ⏳ | 상태 변경 시 Order.status_code 업데이트 |
| 3.3 | ShipmentService에 Order 상태 동기화 로직 추가 | ⏳ | 상태 변경 시 Order.status_code 업데이트 |

### 2.4 Phase 4: 연동 기능 (선택)

| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 4.1 | ShipmentService.store()에 work_order_id 연결 로직 추가 | ⏳ | 출하 생성 시 WorkOrder 선택 가능 |
| 4.2 | WorkOrder 완료 시 Shipment 자동 생성 옵션 | ⏳ | 선택적 기능 |

---

## 3. 작업 절차

### 3.1 단계별 절차

```
Phase 1: DB 스키마 수정
└── 1.1 마이그레이션 생성 및 실행
    ├── add_work_order_id_to_shipments_table.php
    ├── work_order_id FK (nullable)
    └── index 추가

Phase 2: 모델 관계 추가
├── 2.1 Order.php - shipments() HasMany
├── 2.2 WorkOrder.php - shipments() HasMany
├── 2.3 Shipment.php - workOrder() BelongsTo
└── 2.4 Shipment.php - fillable에 work_order_id 추가

Phase 3: 상태 동기화
├── 3.1 Order.php - 상태 상수 확장
│   ├── STATUS_IN_PRODUCTION = 'IN_PRODUCTION'
│   ├── STATUS_PRODUCED = 'PRODUCED'
│   ├── STATUS_SHIPPING = 'SHIPPING'
│   └── STATUS_SHIPPED = 'SHIPPED'
├── 3.2 WorkOrderService.php - syncOrderStatus() 메서드 추가
│   ├── in_progress → Order: IN_PRODUCTION
│   ├── completed → Order: PRODUCED
│   └── shipped → Order: (Shipment 생성 시)
└── 3.3 ShipmentService.php - syncOrderStatus() 메서드 추가
    ├── scheduled/ready → Order: SHIPPING (첫 출하 생성 시)
    └── completed → Order: SHIPPED (모든 출하 완료 시)

Phase 4: 연동 기능 (선택)
├── 4.1 ShipmentService.store() - work_order_id 파라미터 추가
└── 4.2 WorkOrderService.updateStatus() - 자동 Shipment 생성 옵션
```

### 3.2 상태 흐름도

```
┌─────────────────────────────────────────────────────────────────┐
│                     전체 상태 흐름                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  [Order]                                                         │
│  DRAFT ──▶ CONFIRMED ──▶ IN_PRODUCTION ──▶ PRODUCED             │
│                │              │               │                  │
│                ▼              ▼               ▼                  │
│           WorkOrder      WorkOrder       WorkOrder              │
│           생성           in_progress     completed              │
│                                               │                  │
│                                               ▼                  │
│  ──────────────────────▶ SHIPPING ──▶ SHIPPED ──▶ COMPLETED     │
│                              │           │                       │
│                              ▼           ▼                       │
│                         Shipment    Shipment                    │
│                         생성        completed                   │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘
```

---

## 4. 상세 작업 내용

### 4.1 Phase 1: DB 스키마 수정

#### 1.1 마이그레이션: shipments 테이블에 work_order_id 추가

**파일**: `api/database/migrations/2025_01_19_XXXXXX_add_work_order_id_to_shipments_table.php`

```php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('shipments', function (Blueprint $table) {
            $table->foreignId('work_order_id')
                ->nullable()
                ->after('order_id')
                ->comment('작업지시 ID');

            $table->index(['tenant_id', 'work_order_id']);
        });
    }

    public function down(): void
    {
        Schema::table('shipments', function (Blueprint $table) {
            $table->dropIndex(['tenant_id', 'work_order_id']);
            $table->dropColumn('work_order_id');
        });
    }
};
```

---

### 4.2 Phase 2: 모델 관계 추가

#### 2.1 Order 모델 - shipments() 관계

**파일**: `api/app/Models/Orders/Order.php`

```php
use App\Models\Tenants\Shipment;

/**
 * 출하 목록
 */
public function shipments(): HasMany
{
    return $this->hasMany(Shipment::class, 'order_id');
}
```

#### 2.2 WorkOrder 모델 - shipments() 관계

**파일**: `api/app/Models/Production/WorkOrder.php`

```php
use App\Models\Tenants\Shipment;

/**
 * 출하 목록
 */
public function shipments(): HasMany
{
    return $this->hasMany(Shipment::class);
}
```

#### 2.3-2.4 Shipment 모델 수정

**파일**: `api/app/Models/Tenants/Shipment.php`

```php
use App\Models\Production\WorkOrder;

// fillable에 추가
protected $fillable = [
    // ... 기존 필드들
    'work_order_id',  // 추가
];

// casts에 추가
protected $casts = [
    // ... 기존 캐스트들
    'work_order_id' => 'integer',  // 추가
];

/**
 * 작업지시 관계
 */
public function workOrder(): BelongsTo
{
    return $this->belongsTo(WorkOrder::class);
}
```

---

### 4.3 Phase 3: Order 상태 확장 및 동기화 로직

#### 3.1 Order 모델 - 상태 상수 확장

**파일**: `api/app/Models/Orders/Order.php`

```php
// 기존 상태
public const STATUS_DRAFT = 'DRAFT';
public const STATUS_CONFIRMED = 'CONFIRMED';
public const STATUS_IN_PROGRESS = 'IN_PROGRESS';
public const STATUS_COMPLETED = 'COMPLETED';
public const STATUS_CANCELLED = 'CANCELLED';

// 신규 상태 추가
public const STATUS_IN_PRODUCTION = 'IN_PRODUCTION';  // 생산중
public const STATUS_PRODUCED = 'PRODUCED';            // 생산완료
public const STATUS_SHIPPING = 'SHIPPING';            // 출하중
public const STATUS_SHIPPED = 'SHIPPED';              // 출하완료

/**
 * 전체 상태 목록
 */
public const STATUSES = [
    self::STATUS_DRAFT,
    self::STATUS_CONFIRMED,
    self::STATUS_IN_PRODUCTION,
    self::STATUS_PRODUCED,
    self::STATUS_SHIPPING,
    self::STATUS_SHIPPED,
    self::STATUS_COMPLETED,
    self::STATUS_CANCELLED,
];

/**
 * 상태 라벨
 */
public const STATUS_LABELS = [
    self::STATUS_DRAFT => '임시저장',
    self::STATUS_CONFIRMED => '확정',
    self::STATUS_IN_PRODUCTION => '생산중',
    self::STATUS_PRODUCED => '생산완료',
    self::STATUS_SHIPPING => '출하중',
    self::STATUS_SHIPPED => '출하완료',
    self::STATUS_COMPLETED => '완료',
    self::STATUS_CANCELLED => '취소',
];
```

#### 3.2 WorkOrderService - Order 상태 동기화

**파일**: `api/app/Services/WorkOrderService.php`

```php
use App\Models\Orders\Order;

/**
 * Order 상태 동기화
 * WorkOrder 상태 변경 시 Order.status_code 업데이트
 */
private function syncOrderStatus(WorkOrder $workOrder): void
{
    if (!$workOrder->sales_order_id) {
        return;
    }

    $order = Order::find($workOrder->sales_order_id);
    if (!$order) {
        return;
    }

    $newStatus = null;

    switch ($workOrder->status) {
        case WorkOrder::STATUS_IN_PROGRESS:
        case WorkOrder::STATUS_WAITING:
        case WorkOrder::STATUS_PENDING:
            // 하나라도 진행중이면 생산중
            $newStatus = Order::STATUS_IN_PRODUCTION;
            break;

        case WorkOrder::STATUS_COMPLETED:
            // 모든 작업지시가 완료되었는지 확인
            $allCompleted = WorkOrder::where('sales_order_id', $order->id)
                ->whereNotIn('status', [WorkOrder::STATUS_COMPLETED, WorkOrder::STATUS_SHIPPED])
                ->doesntExist();

            if ($allCompleted) {
                $newStatus = Order::STATUS_PRODUCED;
            }
            break;
    }

    if ($newStatus && $order->status_code !== $newStatus) {
        $order->update(['status_code' => $newStatus]);

        $this->auditLogger->log(
            $order->tenant_id,
            'order',
            $order->id,
            'status_synced_from_work_order',
            ['status_code' => $order->getOriginal('status_code')],
            ['status_code' => $newStatus, 'work_order_id' => $workOrder->id]
        );
    }
}
```

**updateStatus() 메서드에 호출 추가:**

```php
public function updateStatus(int $id, string $status, ?array $resultData = null)
{
    // ... 기존 로직 ...

    return DB::transaction(function () use ($workOrder, $status, $resultData, $tenantId, $userId) {
        // ... 기존 상태 변경 로직 ...

        $workOrder->save();

        // Order 상태 동기화 추가
        $this->syncOrderStatus($workOrder);

        // ... 나머지 로직 ...
    });
}
```

#### 3.3 ShipmentService - Order 상태 동기화

**파일**: `api/app/Services/ShipmentService.php`

```php
use App\Models\Orders\Order;

/**
 * Order 상태 동기화
 * Shipment 상태 변경 시 Order.status_code 업데이트
 */
private function syncOrderStatus(Shipment $shipment): void
{
    if (!$shipment->order_id) {
        return;
    }

    $order = Order::find($shipment->order_id);
    if (!$order) {
        return;
    }

    $newStatus = null;

    switch ($shipment->status) {
        case 'scheduled':
        case 'ready':
        case 'shipping':
            // 출하 프로세스 시작
            if (!in_array($order->status_code, [Order::STATUS_SHIPPING, Order::STATUS_SHIPPED, Order::STATUS_COMPLETED])) {
                $newStatus = Order::STATUS_SHIPPING;
            }
            break;

        case 'completed':
            // 모든 출하가 완료되었는지 확인
            $allCompleted = Shipment::where('order_id', $order->id)
                ->where('status', '!=', 'completed')
                ->doesntExist();

            if ($allCompleted) {
                $newStatus = Order::STATUS_SHIPPED;
            }
            break;
    }

    if ($newStatus && $order->status_code !== $newStatus) {
        $order->update(['status_code' => $newStatus]);
    }
}
```

**store() 및 updateStatus() 메서드에 호출 추가:**

```php
public function store(array $data): Shipment
{
    // ... 기존 로직 ...

    return DB::transaction(function () use ($data, $tenantId, $userId) {
        // ... 기존 생성 로직 ...

        // Order 상태 동기화 추가
        $this->syncOrderStatus($shipment);

        return $shipment->load('items');
    });
}

public function updateStatus(int $id, string $status, ?array $additionalData = null): Shipment
{
    // ... 기존 로직 ...

    $shipment->update($updateData);

    // Order 상태 동기화 추가
    $this->syncOrderStatus($shipment);

    return $shipment->load('items');
}
```

---

### 4.4 Phase 4: 연동 기능 (선택)

#### 4.1 ShipmentService.store() - work_order_id 연결

**파일**: `api/app/Services/ShipmentService.php`

```php
public function store(array $data): Shipment
{
    return DB::transaction(function () use ($data, $tenantId, $userId) {
        $shipment = Shipment::create([
            // ... 기존 필드들 ...
            'work_order_id' => $data['work_order_id'] ?? null,  // 추가
        ]);

        // WorkOrder가 있으면 상태를 shipped로 변경
        if ($shipment->work_order_id) {
            $workOrder = WorkOrder::find($shipment->work_order_id);
            if ($workOrder && $workOrder->status === WorkOrder::STATUS_COMPLETED) {
                $workOrder->update([
                    'status' => WorkOrder::STATUS_SHIPPED,
                    'shipped_at' => now(),
                ]);
            }
        }

        // ... 나머지 로직 ...
    });
}
```

#### 4.2 ShipmentStoreRequest - work_order_id 검증

**파일**: `api/app/Http/Requests/Shipment/ShipmentStoreRequest.php`

```php
public function rules(): array
{
    return [
        // ... 기존 규칙들 ...
        'work_order_id' => ['nullable', 'integer', 'exists:work_orders,id'],
    ];
}
```

---

## 5. 컨펌 대기 목록

| # | 항목 | 변경 내용 | 영향 범위 | 상태 |
|---|------|----------|----------|------|
| 1 | 마이그레이션 | shipments에 work_order_id FK 추가 | DB | ⏳ 컨펌 필요 |
| 2 | Order 상태 확장 | 4개 상태 추가 (IN_PRODUCTION, PRODUCED, SHIPPING, SHIPPED) | Order 모델, API | ⏳ 컨펌 필요 |
| 3 | 상태 동기화 로직 | WorkOrder/Shipment 상태 변경 시 Order 자동 업데이트 | 서비스 로직 | ⏳ 컨펌 필요 |

---

## 6. 변경 이력

| 날짜 | 항목 | 변경 내용 | 파일 | 승인 |
|------|------|----------|------|------|
| 2025-01-19 | - | 계획 문서 초안 작성 | - | - |

---

## 7. 참고 문서

- **빠른 시작**: `docs/quickstart/quick-start.md`
- **품질 체크리스트**: `docs/standards/quality-checklist.md`
- **SAM API 규칙**: `CLAUDE.md`
- **DB 스키마**: `docs/specs/database-schema.md`

### 분석된 기존 파일

| 파일 | 역할 |
|------|------|
| `api/app/Models/Orders/Order.php` | 수주 마스터 모델 |
| `api/app/Models/Production/WorkOrder.php` | 작업지시 모델 |
| `api/app/Models/Tenants/Shipment.php` | 출하 모델 |
| `api/app/Services/WorkOrderService.php` | 작업지시 비즈니스 로직 |
| `api/app/Services/ShipmentService.php` | 출하 비즈니스 로직 |
| `api/database/migrations/2025_12_26_100000_create_work_orders_table.php` | 작업지시 테이블 |
| `api/database/migrations/2025_12_26_150604_create_shipments_table.php` | 출하 테이블 |

---

## 8. 세션 및 메모리 관리 정책

### 8.1 세션 시작 시
```javascript
read_memory("order-integration-state")     // 상태 파악
read_memory("order-integration-snapshot")  // 사고 흐름 복구
```

### 8.2 Serena 메모리 구조
- `order-integration-state`: { phase, progress, next_step, last_decision }
- `order-integration-snapshot`: 현재까지의 논의 및 코드 변경점 요약
- `order-integration-rules`: 해당 작업에서 결정된 규칙들

---

## 9. 검증 결과

> 작업 완료 후 이 섹션에 검증 결과 추가

### 9.1 테스트 케이스

| 시나리오 | 예상 결과 | 실제 결과 | 상태 |
|----------|----------|----------|------|
| WorkOrder 생성 (in_progress) | Order.status = IN_PRODUCTION | - | ⏳ |
| WorkOrder 완료 (completed) | Order.status = PRODUCED | - | ⏳ |
| Shipment 생성 | Order.status = SHIPPING | - | ⏳ |
| Shipment 완료 | Order.status = SHIPPED | - | ⏳ |
| 모든 프로세스 완료 | Order.status = COMPLETED | - | ⏳ |

### 9.2 성공 기준

| 기준 | 달성 | 비고 |
|------|------|------|
| shipments.work_order_id FK 추가 완료 | ⏳ | |
| 모델 관계 정상 동작 | ⏳ | |
| Order 상태 자동 동기화 | ⏳ | |
| 기존 데이터 호환성 유지 | ⏳ | |

---

## 10. 자기완결성 점검 결과

### 10.1 체크리스트 검증

| # | 검증 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 1 | 작업 목적이 명확한가? | ✅ | 섹션 1.1 배경 |
| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 9.2 |
| 3 | 작업 범위가 구체적인가? | ✅ | 섹션 2 대상 범위 |
| 4 | 의존성이 명시되어 있는가? | ✅ | Phase별 순서 정의 |
| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 7 참고 문서 |
| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 3, 4 상세 코드 포함 |
| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 9.1 테스트 케이스 |
| 8 | 모호한 표현이 없는가? | ✅ | 구체적 코드 예시 포함 |

### 10.2 새 세션 시뮬레이션 테스트

| 질문 | 답변 가능 | 참조 섹션 |
|------|:--------:|----------|
| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 |
| Q2. 어디서부터 시작해야 하는가? | ✅ | 현재 진행 상태, 3.1 절차 |
| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 섹션 4 상세 작업 내용 |
| Q4. 작업 완료 확인 방법은? | ✅ | 9. 검증 결과 |
| Q5. 막혔을 때 참고 문서는? | ✅ | 7. 참고 문서 |

**결과**: 5/5 통과 → ✅ 자기완결성 확보

---

*이 문서는 /sc:plan 스킬로 생성되었습니다.*