docs: [plans] Phase 4.2 결재 연동 계획 문서 추가
- Document ↔ Approval 브릿지 설계 및 구현 계획 - 6단계 구현 완료 상태 기록 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
494
plans/phase4-approval-integration-plan.md
Normal file
494
plans/phase4-approval-integration-plan.md
Normal file
@@ -0,0 +1,494 @@
|
||||
# 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/plans/integrated-master-plan.md`
|
||||
- **Phase 3 상세**: `docs/plans/integrated-phase-3.md`
|
||||
- **품질 체크리스트**: `QUALITY_CHECKLIST.md`
|
||||
|
||||
---
|
||||
|
||||
## 8. 세션 관리
|
||||
|
||||
### 세션 시작 시
|
||||
```
|
||||
1. 이 문서 읽기 → 현재 진행 상태 확인
|
||||
2. 마지막 완료 Step 확인 → 다음 Step 진행
|
||||
```
|
||||
|
||||
### 작업 중
|
||||
```
|
||||
각 Step 완료 시 → 상태 ⏳→✅ 업데이트
|
||||
컨펌 필요 시 → 사용자 확인
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*이 문서는 /plan 스킬 기반으로 생성되었습니다.*
|
||||
Reference in New Issue
Block a user