# 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 ( } /> ); ``` ### 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 스킬 기반으로 생성되었습니다.*