# Phase 4.2 — 결재 연동 (Document ↔ Approval 브릿지)

> **작성일**: 2026-02-27
> **목적**: 검사 문서(Document)를 기존 결재 시스템(Approval)에 연동하여 /approval/inbox에서 결재 처리 가능하게 함
> **상위 문서**: [`integrated-master-plan.md`](./integrated-master-plan.md) Phase 4
> **상태**: 🔄 진행중

---

## 📍 현재 진행 상태

| 항목 | 내용 |
|------|------|
| **마지막 완료 작업** | Step 6: Frontend — 문서 상세에서 결재 상신 기능 |
| **다음 작업** | 전체 완료 |
| **진행률** | 6/6 (100%) ✅ |
| **마지막 업데이트** | 2026-02-27 |

---

## 1. 개요

### 1.1 배경

현재 SAM에는 두 개의 독립 시스템이 존재:

| 시스템 | 용도 | 테이블 | UI |
|--------|------|--------|-----|
| **Document** | 검사 성적서, 작업일지 저장 | `documents`, `document_approvals`, `document_data` | 검사 화면에서 생성 |
| **Approval** | 결재 워크플로우 | `approvals`, `approval_steps` | `/approval/inbox` 등 완성 |

**문제**: 검사 문서 작성 후 결재 워크플로우가 없음. `/approval/inbox`에 검사 문서가 나타나지 않음.

### 1.2 목표

```
검사 데이터 입력 → Document 저장 (DRAFT)
    ↓
"결재 상신" → Approval 자동 생성 (PENDING) + 결재 단계 생성
    ↓
/approval/inbox 리스트에 표시
    ↓
결재자 클릭 → 검사 성적서 렌더링 (FqcDocumentContent + ApprovalLineBox)
    ↓
승인/반려 → Document 상태 동기화 + 결재란에 승인자 이름 반영
```

### 1.3 범위

- **대상**: 모든 문서 유형 (중간검사 4종 + 작업일지 3종 + 제품검사 + 수입검사)
- **방식**: Document 상신 시 Approval 자동 생성 (브릿지 패턴)
- **UI**: 기존 `/approval/inbox` + `DocumentDetailModalV2` 활용

### 1.4 변경 승인 정책

| 분류 | 예시 | 승인 |
|------|------|------|
| ✅ 즉시 가능 | 필드 추가, 시더, 프론트 타입 추가 | 불필요 |
| ⚠️ 컨펌 필요 | `approvals` 테이블 컬럼 추가, 서비스 로직 변경 | **필수** |
| 🔴 금지 | 기존 Document/Approval 동작 변경 | 별도 협의 |

---

## 2. 현재 아키텍처 분석

### 2.1 Document 시스템

```
documents (23건, 전부 DRAFT)
├── template_id → document_templates (27종)
├── document_approvals (step 순차 결재)
│   ├── user_id + step + role(작성/검토/승인) + status
│   └── 순서대로 진행 → 모두 승인 시 Document APPROVED
├── document_data (EAV 패턴)
└── linkable_type/linkable_id (다형성 참조)

상태: DRAFT → PENDING → APPROVED | REJECTED | CANCELLED
```

### 2.2 Approval 시스템

```
approvals (30건: pending 14, approved 8, draft 6, rejected 2)
├── form_id → approval_forms (4종: 품의서, 지출결의서, 비용견적서)
├── approval_steps (step_order 순차)
│   ├── approver_id + step_type(approval/agreement/reference) + status
│   └── 현재 차례만 처리 가능
├── content (JSON - 비정형 데이터)
└── drafter_id (기안자)

상태: draft → pending → approved | rejected | cancelled
```

### 2.3 Template 결재선 현황 (DB 확인)

| template_id | 이름 | 결재선 |
|:-----------:|------|--------|
| 57 | 조인트바 중간검사 | 작성(판매) → 검토(생산) → 승인(품질/QC) |
| 58 | 슬랫 중간검사 | 작성(판매) → 검토(생산) → 승인(품질/QC) |
| 59 | 스크린 중간검사 | 작성(판매) → 검토(생산) → 승인(품질/QC) |
| 60 | 절곡품 중간검사 | 작성(판매) → 검토(생산) → 승인(품질/QC) |
| 62 | 스크린 작업일지 | 작성(판매/전진) → 검토(생산) → 승인(품질/QC) |
| 63 | 슬랫 작업일지 | 작성(생산) → 승인 → 승인 → 승인 (4단계) |
| 64 | 절곡 작업일지 | 작성(생산) → 승인 → 승인 → 승인 (4단계) |
| 65 | 제품검사 성적서 | 작성(품질) → 검토(품질/QC) → 승인(경영/대표) |

> ※ 수입검사 일부(id=6, 19~30)는 결재선 미설정

---

## 3. 브릿지 설계

### 3.1 아키텍처

```
┌─────────────────────────────────────────────────────────┐
│  Document System (데이터)     Approval System (워크플로우)  │
│                                                          │
│  documents ─────────────── approvals                     │
│  (검사 데이터, EAV)    ←→   (linkable_type=Document)      │
│                        브릿지  (linkable_id=document.id)  │
│  document_approvals         approval_steps               │
│  (결재란 표시용)       ←동기화→ (실제 결재 진행)              │
│                                                          │
│  document_templates         approval_forms               │
│  (양식 구조 정의)           (결재 양식: 'document')         │
└─────────────────────────────────────────────────────────┘
```

### 3.2 데이터 흐름

```
[Document 생성]
  └─ DocumentService::create()
     ├─ documents 레코드 생성 (DRAFT)
     ├─ document_approvals 생성 (user_id + step + role)
     └─ document_data 저장 (EAV)

[결재 상신] ← 새로 구현
  └─ DocumentService::submit()
     ├─ Document.status = PENDING
     ├─ Approval 자동 생성 ← NEW
     │   ├─ form_id = 'document' form
     │   ├─ linkable_type = 'App\Models\Documents\Document'
     │   ├─ linkable_id = document.id
     │   ├─ title = document.title
     │   ├─ content = { document_id, template_id, document_no }
     │   └─ status = 'pending'
     └─ ApprovalSteps 생성 ← NEW
         └─ document_approvals → approval_steps 매핑
            ├─ user_id → approver_id
            ├─ step → step_order
            └─ role → step_type (작성=approval, 검토=approval, 승인=approval)

[결재함 표시] ← 기존 동작 활용
  └─ ApprovalService::inbox()
     └─ 기존 쿼리에 자동 포함 (approvals 테이블이므로)

[결재 처리] ← 후속 동기화 추가
  └─ ApprovalService::approve() / reject()
     ├─ approval_steps 상태 변경 (기존)
     └─ 후크: linkable_type=Document인 경우 ← NEW
         ├─ document_approvals 해당 step 동기화
         ├─ Document.status 동기화
         └─ 승인자 이름 반영 (acted_at 기록)

[문서 렌더링] ← 프론트 확장
  └─ DocumentDetailModalV2
     ├─ approval.linkable_type === 'Document' 감지
     ├─ Document + Template 데이터 로딩
     └─ FqcDocumentContent + ApprovalLineBox 렌더링
```

### 3.3 role → step_type 매핑

| document_approvals.role | approval_steps.step_type | 비고 |
|------------------------|--------------------------|------|
| 작성 / writer / 담당자 | `approval` | 첫 단계 (작성자 확인) |
| 검토 / reviewer | `approval` | 중간 단계 |
| 승인 / approver / 부서장 / QC / 대표 | `approval` | 최종 단계 |

> 현재 `reference`(참조) 역할은 document_template_approval_lines에 미사용 → 추후 필요 시 확장

---

## 4. 작업 항목

### Step 1: DB 마이그레이션

| # | 작업 | 파일 | 상태 |
|---|------|------|:----:|
| 1.1 | `approvals` 테이블에 `linkable_type`, `linkable_id` 컬럼 추가 | `migrations/xxxx_add_linkable_to_approvals.php` | ⏳ |
| 1.2 | `approval_forms`에 문서 결재용 양식 시더 | `seeders/DocumentApprovalFormSeeder.php` | ⏳ |

**마이그레이션 상세:**
```php
// 1.1 approvals 테이블
Schema::table('approvals', function (Blueprint $table) {
    $table->string('linkable_type')->nullable()->after('attachments');
    $table->unsignedBigInteger('linkable_id')->nullable()->after('linkable_type');
    $table->index(['linkable_type', 'linkable_id']);
});

// 1.2 approval_forms 시더
ApprovalForm::create([
    'tenant_id' => $tenantId,
    'name' => '문서 결재',
    'code' => 'document',
    'category' => '문서',
    'template' => json_encode([]),  // 문서 자체가 양식
    'is_active' => true,
]);
```

### Step 2: Backend — Document → Approval 브릿지

| # | 작업 | 파일 | 상태 |
|---|------|------|:----:|
| 2.1 | `DocumentService::submit()` 수정 — Approval 자동 생성 | `app/Services/DocumentService.php` | ⏳ |
| 2.2 | Approval 모델에 `linkable` 관계 추가 | `app/Models/Tenants/Approval.php` | ⏳ |
| 2.3 | Document API에 결재 상태 포함 | `DocumentService::show()` | ⏳ |

**2.1 핵심 로직:**
```php
// DocumentService::submit() 내부에 추가
public function submit(int $id): Document
{
    $document = Document::findOrFail($id);
    // ... 기존 검증 로직 ...

    $document->update(['status' => Document::STATUS_PENDING, 'submitted_at' => now()]);

    // === NEW: Approval 자동 생성 ===
    $this->createApprovalBridge($document);

    return $document;
}

private function createApprovalBridge(Document $document): void
{
    $form = ApprovalForm::where('code', 'document')
        ->where('tenant_id', $document->tenant_id)
        ->first();

    if (!$form) return; // 양식 미등록 시 스킵

    $approval = Approval::create([
        'tenant_id' => $document->tenant_id,
        'document_number' => $document->document_no,
        'form_id' => $form->id,
        'title' => $document->title,
        'content' => [
            'document_id' => $document->id,
            'template_id' => $document->template_id,
            'document_no' => $document->document_no,
        ],
        'status' => Approval::STATUS_PENDING,
        'drafter_id' => $document->created_by,
        'drafted_at' => now(),
        'current_step' => 1,
        'linkable_type' => Document::class,
        'linkable_id' => $document->id,
    ]);

    // document_approvals → approval_steps 변환
    $docApprovals = $document->approvals()
        ->orderBy('step')
        ->get();

    foreach ($docApprovals as $docApproval) {
        ApprovalStep::create([
            'approval_id' => $approval->id,
            'step_order' => $docApproval->step,
            'step_type' => ApprovalLine::STEP_TYPE_APPROVAL,
            'approver_id' => $docApproval->user_id,
            'status' => ApprovalStep::STATUS_PENDING,
        ]);
    }
}
```

### Step 3: Backend — Approval → Document 동기화

| # | 작업 | 파일 | 상태 |
|---|------|------|:----:|
| 3.1 | `ApprovalService::approve()` 후크 — Document 동기화 | `app/Services/ApprovalService.php` | ⏳ |
| 3.2 | `ApprovalService::reject()` 후크 — Document 동기화 | `app/Services/ApprovalService.php` | ⏳ |
| 3.3 | `ApprovalService::cancel()` 후크 — Document 동기화 | `app/Services/ApprovalService.php` | ⏳ |

**3.1 핵심 로직:**
```php
// ApprovalService::approve() 내부, 기존 로직 뒤에 추가
private function syncToDocument(Approval $approval): void
{
    if ($approval->linkable_type !== Document::class) return;

    $document = Document::find($approval->linkable_id);
    if (!$document) return;

    // approval_steps → document_approvals 동기화
    foreach ($approval->steps as $step) {
        $docApproval = $document->approvals()
            ->where('step', $step->step_order)
            ->first();

        if ($docApproval && $step->status !== ApprovalStep::STATUS_PENDING) {
            $docApproval->update([
                'status' => strtoupper($step->status), // pending→PENDING, approved→APPROVED
                'acted_at' => $step->acted_at,
                'comment' => $step->comment,
            ]);
        }
    }

    // Document 전체 상태 동기화
    $documentStatus = match ($approval->status) {
        'approved' => Document::STATUS_APPROVED,
        'rejected' => Document::STATUS_REJECTED,
        'cancelled' => Document::STATUS_CANCELLED,
        default => Document::STATUS_PENDING,
    };

    $document->update([
        'status' => $documentStatus,
        'completed_at' => in_array($approval->status, ['approved', 'rejected']) ? now() : null,
    ]);
}
```

### Step 4: Backend — Document 조회 API 확장

| # | 작업 | 파일 | 상태 |
|---|------|------|:----:|
| 4.1 | Approval `show()` — linkable 문서 데이터 포함 | `app/Services/ApprovalService.php` | ⏳ |
| 4.2 | Document API 응답에 approval_id 포함 | `DocumentResource` 또는 `show()` | ⏳ |

**4.1 핵심:**
```php
// ApprovalService::show() 내부
// 기존 with() 관계에 추가
$approval->load(['steps.approver', 'form']);

// linkable이 Document인 경우 문서 데이터 포함
if ($approval->linkable_type === Document::class) {
    $approval->loadMissing('linkable.template', 'linkable.data', 'linkable.approvals.user');
}
```

### Step 5: Frontend — /approval/inbox 확장

| # | 작업 | 파일 | 상태 |
|---|------|------|:----:|
| 5.1 | ApprovalBox 문서 유형 필터 추가 | `components/approval/ApprovalBox/index.tsx` | ⏳ |
| 5.2 | DocumentDetailModalV2에 document 렌더링 case 추가 | `components/approval/DocumentDetailModalV2.tsx` | ⏳ |
| 5.3 | Approval actions에 문서 조회 함수 추가 | `components/approval/ApprovalBox/actions.ts` | ⏳ |
| 5.4 | 타입 정의 확장 | `types` 또는 해당 파일 | ⏳ |

**5.1 문서 유형 필터:**
```typescript
// ApprovalType에 추가
type ApprovalType = 'expense_estimate' | 'expense_report' | 'proposal' | 'document';

// 필터 옵션 추가
{ value: 'document', label: '문서 결재' }
```

**5.2 문서 렌더링:**
```typescript
// DocumentDetailModalV2 내 renderDocument() 확장
case 'document':
  return (
    <DocumentWrapper>
      <DocumentHeader
        title={linkedDocument.template.title}
        customApproval={<ApprovalLineBox approvals={approvalLineData} />}
      />
      <FqcDocumentContent
        ref={contentRef}
        templateData={linkedDocument.template}
        documentData={linkedDocument.data}
        readonly={true}
      />
    </DocumentWrapper>
  );
```

### Step 6: Frontend — 문서 상세에서 상신 기능

| # | 작업 | 파일 | 상태 |
|---|------|------|:----:|
| 6.1 | 문서 상세 화면에 "결재 상신" 버튼 | InspectionReportModal.tsx | ✅ |
| 6.2 | 상신 API 호출 (DocumentService submit) | production/WorkOrders/actions.ts | ✅ |

> ※ 현재 문서가 모두 DRAFT 상태이므로, 상신 버튼이 동작하면 바로 /approval/inbox에 표시됨

---

## 5. 수정 파일 목록

### Backend (api/)

| 파일 | 변경 내용 |
|------|----------|
| `database/migrations/xxxx_add_linkable_to_approvals.php` | **신규** — linkable 컬럼 추가 |
| `database/seeders/DocumentApprovalFormSeeder.php` | **신규** — 문서 결재 양식 시더 |
| `app/Models/Tenants/Approval.php` | linkable 관계 추가, fillable 확장 |
| `app/Services/DocumentService.php` | submit()에 브릿지 로직 추가 |
| `app/Services/ApprovalService.php` | approve/reject/cancel에 동기화 후크 |

### Frontend (react/)

| 파일 | 변경 내용 |
|------|----------|
| `components/approval/ApprovalBox/index.tsx` | 문서 유형 필터 추가 |
| `components/approval/ApprovalBox/actions.ts` | 문서 데이터 조회 함수 |
| `components/approval/DocumentDetailModalV2.tsx` | document case 렌더링 추가 |
| 타입 파일 | ApprovalType 확장, LinkedDocument 타입 |

---

## 6. 검증 시나리오

### T1: 브릿지 생성 확인
```
검사 문서 생성 (DRAFT) → submit() 호출
→ approvals 레코드 생성됨 (linkable_type=Document, status=pending)
→ approval_steps 생성됨 (document_approvals 기반)
```

### T2: 결재함 표시 확인
```
/approval/inbox 접속
→ 문서 결재 항목이 리스트에 표시됨
→ 문서유형 필터 '문서 결재' 적용 시 필터됨
```

### T3: 문서 렌더링 확인
```
결재함에서 항목 클릭
→ DocumentDetailModalV2 열림
→ 검사 성적서 내용 표시 (FqcDocumentContent)
→ 결재란 표시 (ApprovalLineBox)
```

### T4: 승인 동기화 확인
```
결재자가 승인 처리
→ approval_steps 상태 approved
→ document_approvals 해당 step 동기화 (status=APPROVED, acted_at)
→ 모든 단계 완료 시 documents.status=APPROVED
```

### T5: 반려 동기화 확인
```
결재자가 반려 처리
→ approval.status = rejected
→ documents.status = REJECTED
→ document_approvals 해당 step 동기화
```

### T6: 결재란 승인자 표시
```
승인 완료 후 문서 보기
→ ApprovalLineBox에 승인자 이름, 시각 표시
→ 각 단계별 상태 아이콘 (✓/✗/⏱)
```

---

## 7. 참고 문서

- **마스터 플랜**: `docs/dev_plans/integrated-master-plan.md`
- **Phase 3 상세**: `docs/dev_plans/integrated-phase-3.md`
- **품질 체크리스트**: `QUALITY_CHECKLIST.md`

---

## 8. 세션 관리

### 세션 시작 시
```
1. 이 문서 읽기 → 현재 진행 상태 확인
2. 마지막 완료 Step 확인 → 다음 Step 진행
```

### 작업 중
```
각 Step 완료 시 → 상태 ⏳→✅ 업데이트
컨펌 필요 시 → 사용자 확인
```

---

*이 문서는 /plan 스킬 기반으로 생성되었습니다.*
