# 문서 관리 시스템 개발 계획

> **작성일**: 2025-01-28
> **목적**: 문서 템플릿 기반 실제 문서 작성/결재/관리 시스템
> **상태**: 📋 계획 수립

---

## 📍 현재 진행 상태

| 항목 | 내용 |
|------|------|
| **마지막 완료 작업** | Phase 2 - MNG 관리자 패널 구현 ✅ |
| **다음 작업** | - (Phase 3 보류) |
| **진행률** | 8/12 (67%) |
| **보류 항목** | 결재 워크플로우 (submit/approve/reject/cancel) - 기존 시스템 연동 필요<br>Phase 3 React 연동 - 사용자 직접 구현 또는 추후 진행 |
| **마지막 업데이트** | 2026-01-28 |

---

## 0. 빠른 시작 가이드

### 0.1 전제 조건

```bash
# Docker 서비스 실행 확인
docker ps | grep sam

# 예상 결과: sam-api-1, sam-mng-1, sam-mysql-1, sam-nginx-1 실행 중
```

### 0.2 프로젝트 경로

| 프로젝트 | 경로 | 설명 |
|----------|------|------|
| API | `/Users/kent/Works/@KD_SAM/SAM/api` | Laravel 12 REST API |
| MNG | `/Users/kent/Works/@KD_SAM/SAM/mng` | Laravel 12 + Blade 관리자 |
| React | `/Users/kent/Works/@KD_SAM/SAM/react` | Next.js 15 프론트엔드 |

### 0.3 작업 시작 명령어

```bash
# 1. API 마이그레이션 상태 확인
docker exec sam-api-1 php artisan migrate:status

# 2. 새 마이그레이션 생성
docker exec sam-api-1 php artisan make:migration create_documents_table

# 3. 마이그레이션 실행
docker exec sam-api-1 php artisan migrate

# 4. 모델 생성
docker exec sam-api-1 php artisan make:model Document

# 5. 코드 포맷팅
docker exec sam-api-1 ./vendor/bin/pint
```

### 0.4 작업 순서 요약

```
Phase 1 (API)
├── 1.1 마이그레이션 파일 생성 → 컨펌 필요
├── 1.2 마이그레이션 실행
├── 1.3 모델 생성 (Document, DocumentApproval, DocumentData)
├── 1.4 Service 생성 (DocumentService)
├── 1.5 Controller 생성 (DocumentController)
└── 1.6 Swagger 문서

Phase 2 (MNG)
├── 2.1 모델 복사/수정
├── 2.2 문서 목록 화면
├── 2.3 문서 상세/편집 화면
└── 2.4 문서 생성 화면

Phase 3 (React)
├── 3.1 문서 작성 컴포넌트
├── 3.2 결재선 지정 UI
└── 3.3 수입검사 연동
```

---

## 1. 개요

### 1.1 배경

현재 SAM 시스템에는 문서 템플릿 관리 기능이 존재하나, 실제 문서를 작성하고 관리하는 기능이 없음.

**현재 상태:**
- ✅ MNG: 문서 템플릿 관리 (`/document-templates`)
- ❌ 실제 문서 작성/관리 기능 없음
- ❌ 결재 시스템과 연동 없음

**목표:**
- 템플릿 기반 동적 문서 생성
- 결재 시스템 연동
- 수입검사/입고등록에서 실사용

### 1.2 시스템 구조

```
┌─────────────────────────────────────────────────────────────────┐
│                        문서 관리 시스템                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  [MNG 관리자]                    [React 사용자]                   │
│  ┌──────────────┐               ┌──────────────┐                │
│  │ 템플릿 관리   │               │ 문서 작성    │                 │
│  │ 문서 관리    │               │ 결재 처리    │                 │
│  └──────────────┘               └──────────────┘                │
│         │                              │                        │
│         └──────────────┬───────────────┘                        │
│                        ▼                                        │
│                   [API Server]                                  │
│                        │                                        │
│                        ▼                                        │
│                   [Database]                                    │
│  documents, document_approvals, document_data                   │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘
```

### 1.3 변경 승인 정책

| 분류 | 예시 | 승인 |
|------|------|------|
| ✅ 즉시 가능 | 필드 추가, 문서 수정 | 불필요 |
| ⚠️ 컨펌 필요 | 마이그레이션, 새 API | **필수** |
| 🔴 금지 | 기존 테이블 변경 | 별도 협의 |

---

## 2. 대상 범위

### 2.1 Phase 1: Database & API

| # | 작업 항목 | 상태 | 파일 경로 |
|---|----------|:----:|----------|
| 1.1 | 마이그레이션 생성 | ✅ | `api/database/migrations/2026_01_28_200000_create_documents_table.php` |
| 1.2 | Document 모델 | ✅ | `api/app/Models/Documents/Document.php` |
| 1.3 | DocumentApproval 모델 | ✅ | `api/app/Models/Documents/DocumentApproval.php` |
| 1.4 | DocumentData 모델 | ✅ | `api/app/Models/Documents/DocumentData.php` |
| 1.5 | DocumentService | ⏳ | `api/app/Services/DocumentService.php` |
| 1.6 | DocumentController | ⏳ | `api/app/Http/Controllers/Api/V1/DocumentController.php` |
| 1.7 | FormRequest | ⏳ | `api/app/Http/Requests/Document/` |
| 1.8 | Swagger 문서 | ⏳ | `api/app/Swagger/v1/DocumentApi.php` |

### 2.2 Phase 2: MNG 관리 화면

| # | 작업 항목 | 상태 | 파일 경로 |
|---|----------|:----:|----------|
| 2.1 | Document 모델 | ✅ | `mng/app/Models/Documents/Document.php` |
| 2.2 | DocumentController | ✅ | `mng/app/Http/Controllers/DocumentController.php` |
| 2.3 | 문서 목록 뷰 | ✅ | `mng/resources/views/documents/index.blade.php` |
| 2.4 | 문서 상세 뷰 | ✅ | `mng/resources/views/documents/show.blade.php` |
| 2.5 | 문서 생성/수정 뷰 | ✅ | `mng/resources/views/documents/edit.blade.php` |
| 2.6 | API Controller | ✅ | `mng/app/Http/Controllers/Api/Admin/DocumentApiController.php` |

### 2.3 Phase 3: React 연동 (⏸️ 보류)

| # | 작업 항목 | 상태 | 파일 경로 |
|---|----------|:----:|----------|
| 3.1 | 문서 작성 컴포넌트 | ⏸️ | `react/src/components/document-system/DocumentForm/` |
| 3.2 | API actions | ⏸️ | `react/src/components/document-system/actions.ts` |
| 3.3 | 수입검사 연동 | ⏸️ | `react/src/components/material/ReceivingManagement/` |

> **보류 사유**: 사용자 직접 구현 또는 추후 진행 예정

---

## 3. 상세 설계

### 3.1 Database Schema (마이그레이션 파일)

```php
<?php
// api/database/migrations/2026_01_29_000000_create_documents_table.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::create('documents', function (Blueprint $table) {
            $table->id();
            $table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
            $table->foreignId('template_id')->constrained('document_templates');

            // 문서 정보
            $table->string('document_no', 50)->comment('문서번호');
            $table->string('title', 255)->comment('문서 제목');
            $table->enum('status', ['DRAFT', 'PENDING', 'APPROVED', 'REJECTED', 'CANCELLED'])
                  ->default('DRAFT')->comment('상태');

            // 연결 정보 (다형성)
            $table->string('linkable_type', 100)->nullable()->comment('연결 타입');
            $table->unsignedBigInteger('linkable_id')->nullable()->comment('연결 ID');

            // 메타 정보
            $table->foreignId('created_by')->constrained('users');
            $table->timestamp('submitted_at')->nullable()->comment('결재 요청일');
            $table->timestamp('completed_at')->nullable()->comment('결재 완료일');

            $table->timestamps();
            $table->softDeletes();

            $table->index(['tenant_id', 'status']);
            $table->index('document_no');
            $table->index(['linkable_type', 'linkable_id']);
        });

        // 문서 결재
        Schema::create('document_approvals', function (Blueprint $table) {
            $table->id();
            $table->foreignId('document_id')->constrained()->cascadeOnDelete();
            $table->foreignId('user_id')->constrained();

            $table->unsignedTinyInteger('step')->default(1)->comment('결재 순서');
            $table->string('role', 50)->comment('역할 (작성/검토/승인)');
            $table->enum('status', ['PENDING', 'APPROVED', 'REJECTED'])
                  ->default('PENDING')->comment('상태');

            $table->text('comment')->nullable()->comment('결재 의견');
            $table->timestamp('acted_at')->nullable()->comment('결재 처리일');

            $table->timestamps();

            $table->index(['document_id', 'step']);
        });

        // 문서 데이터
        Schema::create('document_data', function (Blueprint $table) {
            $table->id();
            $table->foreignId('document_id')->constrained()->cascadeOnDelete();

            $table->unsignedBigInteger('section_id')->nullable()->comment('섹션 ID');
            $table->unsignedBigInteger('column_id')->nullable()->comment('컬럼 ID');
            $table->unsignedSmallInteger('row_index')->default(0)->comment('행 인덱스');

            $table->string('field_key', 100)->comment('필드 키');
            $table->text('field_value')->nullable()->comment('값');

            $table->timestamps();

            $table->index(['document_id', 'section_id']);
        });

        // 문서 첨부파일
        Schema::create('document_attachments', function (Blueprint $table) {
            $table->id();
            $table->foreignId('document_id')->constrained()->cascadeOnDelete();
            $table->foreignId('file_id')->constrained('files');

            $table->string('attachment_type', 50)->default('general')->comment('유형');
            $table->string('description', 255)->nullable();

            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('document_attachments');
        Schema::dropIfExists('document_data');
        Schema::dropIfExists('document_approvals');
        Schema::dropIfExists('documents');
    }
};
```

### 3.2 Model 코드 템플릿

```php
<?php
// api/app/Models/Documents/Document.php

namespace App\Models\Documents;

use App\Traits\BelongsToTenant;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\SoftDeletes;

class Document extends Model
{
    use BelongsToTenant, SoftDeletes;

    protected $fillable = [
        'tenant_id',
        'template_id',
        'document_no',
        'title',
        'status',
        'linkable_type',
        'linkable_id',
        'created_by',
        'submitted_at',
        'completed_at',
    ];

    protected $casts = [
        'submitted_at' => 'datetime',
        'completed_at' => 'datetime',
    ];

    // === 상태 상수 ===
    public const STATUS_DRAFT = 'DRAFT';
    public const STATUS_PENDING = 'PENDING';
    public const STATUS_APPROVED = 'APPROVED';
    public const STATUS_REJECTED = 'REJECTED';
    public const STATUS_CANCELLED = 'CANCELLED';

    // === 관계 ===
    public function template(): BelongsTo
    {
        return $this->belongsTo(\App\Models\DocumentTemplate::class, 'template_id');
    }

    public function approvals(): HasMany
    {
        return $this->hasMany(DocumentApproval::class)->orderBy('step');
    }

    public function data(): HasMany
    {
        return $this->hasMany(DocumentData::class);
    }

    public function attachments(): HasMany
    {
        return $this->hasMany(DocumentAttachment::class);
    }

    public function linkable(): MorphTo
    {
        return $this->morphTo();
    }

    public function creator(): BelongsTo
    {
        return $this->belongsTo(\App\Models\User::class, 'created_by');
    }

    // === 스코프 ===
    public function scopeStatus($query, string $status)
    {
        return $query->where('status', $status);
    }

    // === 헬퍼 ===
    public function canEdit(): bool
    {
        return $this->status === self::STATUS_DRAFT;
    }

    public function canSubmit(): bool
    {
        return $this->status === self::STATUS_DRAFT;
    }
}
```

```php
<?php
// api/app/Models/Documents/DocumentApproval.php

namespace App\Models\Documents;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class DocumentApproval extends Model
{
    protected $fillable = [
        'document_id',
        'user_id',
        'step',
        'role',
        'status',
        'comment',
        'acted_at',
    ];

    protected $casts = [
        'step' => 'integer',
        'acted_at' => 'datetime',
    ];

    public const STATUS_PENDING = 'PENDING';
    public const STATUS_APPROVED = 'APPROVED';
    public const STATUS_REJECTED = 'REJECTED';

    public function document(): BelongsTo
    {
        return $this->belongsTo(Document::class);
    }

    public function user(): BelongsTo
    {
        return $this->belongsTo(\App\Models\User::class);
    }
}
```

```php
<?php
// api/app/Models/Documents/DocumentData.php

namespace App\Models\Documents;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class DocumentData extends Model
{
    protected $table = 'document_data';

    protected $fillable = [
        'document_id',
        'section_id',
        'column_id',
        'row_index',
        'field_key',
        'field_value',
    ];

    protected $casts = [
        'row_index' => 'integer',
    ];

    public function document(): BelongsTo
    {
        return $this->belongsTo(Document::class);
    }
}
```

### 3.3 Service 코드 템플릿

```php
<?php
// api/app/Services/DocumentService.php

namespace App\Services;

use App\Models\Documents\Document;
use App\Models\Documents\DocumentApproval;
use App\Models\Documents\DocumentData;
use Illuminate\Pagination\LengthAwarePaginator;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class DocumentService extends Service
{
    /**
     * 문서 목록 조회
     */
    public function list(array $params): LengthAwarePaginator
    {
        $query = Document::query()
            ->where('tenant_id', $this->tenantId())
            ->with(['template:id,name,category', 'creator:id,name']);

        // 필터
        if (!empty($params['status'])) {
            $query->where('status', $params['status']);
        }
        if (!empty($params['template_id'])) {
            $query->where('template_id', $params['template_id']);
        }
        if (!empty($params['search'])) {
            $search = $params['search'];
            $query->where(function ($q) use ($search) {
                $q->where('document_no', 'like', "%{$search}%")
                  ->orWhere('title', 'like', "%{$search}%");
            });
        }

        return $query->orderByDesc('id')
            ->paginate($params['per_page'] ?? 20);
    }

    /**
     * 문서 상세 조회
     */
    public function show(int $id): Document
    {
        $document = Document::with([
            'template.approvalLines',
            'template.sections.items',
            'template.columns',
            'approvals.user:id,name',
            'data',
            'creator:id,name',
        ])->find($id);

        if (!$document || $document->tenant_id !== $this->tenantId()) {
            throw new NotFoundHttpException(__('error.not_found'));
        }

        return $document;
    }

    /**
     * 문서 생성
     */
    public function create(array $data): Document
    {
        $document = Document::create([
            'tenant_id' => $this->tenantId(),
            'template_id' => $data['template_id'],
            'document_no' => $this->generateDocumentNo($data['template_id']),
            'title' => $data['title'],
            'status' => Document::STATUS_DRAFT,
            'linkable_type' => $data['linkable_type'] ?? null,
            'linkable_id' => $data['linkable_id'] ?? null,
            'created_by' => $this->apiUserId(),
        ]);

        // 결재선 생성
        if (!empty($data['approvers'])) {
            foreach ($data['approvers'] as $step => $approver) {
                DocumentApproval::create([
                    'document_id' => $document->id,
                    'user_id' => $approver['user_id'],
                    'step' => $step + 1,
                    'role' => $approver['role'],
                    'status' => DocumentApproval::STATUS_PENDING,
                ]);
            }
        }

        // 데이터 저장
        if (!empty($data['data'])) {
            foreach ($data['data'] as $item) {
                DocumentData::create([
                    'document_id' => $document->id,
                    'section_id' => $item['section_id'] ?? null,
                    'column_id' => $item['column_id'] ?? null,
                    'row_index' => $item['row_index'] ?? 0,
                    'field_key' => $item['field_key'],
                    'field_value' => $item['field_value'],
                ]);
            }
        }

        return $document->fresh(['approvals', 'data']);
    }

    /**
     * 결재 요청 (DRAFT → PENDING)
     */
    public function submit(int $id): Document
    {
        $document = $this->show($id);

        if (!$document->canSubmit()) {
            throw new BadRequestHttpException(__('error.invalid_status'));
        }

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

        return $document->fresh();
    }

    /**
     * 결재 승인
     */
    public function approve(int $id, ?string $comment = null): Document
    {
        $document = $this->show($id);
        $userId = $this->apiUserId();

        // 현재 사용자의 결재 단계 찾기
        $approval = $document->approvals
            ->where('user_id', $userId)
            ->where('status', DocumentApproval::STATUS_PENDING)
            ->first();

        if (!$approval) {
            throw new BadRequestHttpException(__('error.not_your_turn'));
        }

        $approval->update([
            'status' => DocumentApproval::STATUS_APPROVED,
            'comment' => $comment,
            'acted_at' => now(),
        ]);

        // 모든 결재 완료 확인
        $allApproved = $document->approvals()
            ->where('status', '!=', DocumentApproval::STATUS_APPROVED)
            ->doesntExist();

        if ($allApproved) {
            $document->update([
                'status' => Document::STATUS_APPROVED,
                'completed_at' => now(),
            ]);
        }

        return $document->fresh(['approvals']);
    }

    /**
     * 결재 반려
     */
    public function reject(int $id, string $comment): Document
    {
        $document = $this->show($id);
        $userId = $this->apiUserId();

        $approval = $document->approvals
            ->where('user_id', $userId)
            ->where('status', DocumentApproval::STATUS_PENDING)
            ->first();

        if (!$approval) {
            throw new BadRequestHttpException(__('error.not_your_turn'));
        }

        $approval->update([
            'status' => DocumentApproval::STATUS_REJECTED,
            'comment' => $comment,
            'acted_at' => now(),
        ]);

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

        return $document->fresh(['approvals']);
    }

    /**
     * 문서번호 생성
     */
    private function generateDocumentNo(int $templateId): string
    {
        $prefix = 'DOC';
        $date = now()->format('Ymd');
        $count = Document::where('tenant_id', $this->tenantId())
            ->whereDate('created_at', today())
            ->count() + 1;

        return sprintf('%s-%s-%04d', $prefix, $date, $count);
    }
}
```

### 3.4 Controller 코드 템플릿

```php
<?php
// api/app/Http/Controllers/Api/V1/DocumentController.php

namespace App\Http\Controllers\Api\V1;

use App\Helpers\ApiResponse;
use App\Http\Controllers\Controller;
use App\Http\Requests\Document\CreateDocumentRequest;
use App\Http\Requests\Document\ApproveDocumentRequest;
use App\Http\Requests\Document\RejectDocumentRequest;
use App\Services\DocumentService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class DocumentController extends Controller
{
    public function __construct(
        private readonly DocumentService $service
    ) {}

    public function index(Request $request): JsonResponse
    {
        return ApiResponse::handle(
            fn () => $this->service->list($request->all()),
            __('message.fetched')
        );
    }

    public function show(int $id): JsonResponse
    {
        return ApiResponse::handle(
            fn () => $this->service->show($id),
            __('message.fetched')
        );
    }

    public function store(CreateDocumentRequest $request): JsonResponse
    {
        return ApiResponse::handle(
            fn () => $this->service->create($request->validated()),
            __('message.created')
        );
    }

    public function submit(int $id): JsonResponse
    {
        return ApiResponse::handle(
            fn () => $this->service->submit($id),
            __('message.document.submitted')
        );
    }

    public function approve(int $id, ApproveDocumentRequest $request): JsonResponse
    {
        return ApiResponse::handle(
            fn () => $this->service->approve($id, $request->comment),
            __('message.document.approved')
        );
    }

    public function reject(int $id, RejectDocumentRequest $request): JsonResponse
    {
        return ApiResponse::handle(
            fn () => $this->service->reject($id, $request->comment),
            __('message.document.rejected')
        );
    }
}
```

### 3.5 API Routes

```php
// api/routes/api.php 에 추가

Route::prefix('v1')->middleware(['auth.apikey'])->group(function () {
    // ... 기존 라우트 ...

    // 문서 관리
    Route::prefix('documents')->middleware(['auth:sanctum'])->group(function () {
        Route::get('/', [DocumentController::class, 'index']);
        Route::post('/', [DocumentController::class, 'store']);
        Route::get('/{id}', [DocumentController::class, 'show']);
        Route::put('/{id}', [DocumentController::class, 'update']);
        Route::delete('/{id}', [DocumentController::class, 'destroy']);

        // 결재
        Route::post('/{id}/submit', [DocumentController::class, 'submit']);
        Route::post('/{id}/approve', [DocumentController::class, 'approve']);
        Route::post('/{id}/reject', [DocumentController::class, 'reject']);
        Route::post('/{id}/cancel', [DocumentController::class, 'cancel']);
    });
});
```

### 3.6 문서 상태 흐름

```
┌──────────────────────────────────────────────────────────────┐
│                                                               │
│   DRAFT ──submit──> PENDING ──approve──> APPROVED            │
│     │                  │                                      │
│     │                  │──reject──> REJECTED                  │
│     │                  │                   │                  │
│     │                  │──cancel──> CANCELLED                 │
│     │                              │                          │
│     └──────────────────<──edit─────┘ (반려 시 수정 후 재요청)  │
│                                                               │
└──────────────────────────────────────────────────────────────┘
```

---

## 4. 기존 코드 참조 (인라인)

### 4.1 기존 템플릿 테이블 구조

```
document_templates (기존)
├── id, tenant_id, name, category, title
├── company_name, company_address, company_contact
├── footer_remark_label, footer_judgement_label
├── footer_judgement_options (JSON)
└── is_active, timestamps, soft_deletes

document_template_approval_lines (기존)
├── id, template_id, name, dept, role, sort_order
└── timestamps

document_template_sections (기존)
├── id, template_id, title, image_path, sort_order
└── timestamps

document_template_section_items (기존)
├── id, section_id, category, item, standard
├── method, frequency, regulation, sort_order
└── timestamps

document_template_columns (기존)
├── id, template_id, label, width, column_type
├── group_name, sub_labels (JSON), sort_order
└── timestamps
```

### 4.2 API Service 기본 클래스

```php
// api/app/Services/Service.php (기존)
abstract class Service
{
    protected function tenantIdOrNull(): ?int;  // 테넌트 ID (없으면 null)
    protected function tenantId(): int;         // 테넌트 ID (없으면 400 예외)
    protected function apiUserId(): int;        // 사용자 ID (없으면 401 예외)
}
```

### 4.3 API Response 헬퍼

```php
// api/app/Helpers/ApiResponse.php (기존)
use App\Helpers\ApiResponse;

// 성공 응답
ApiResponse::success($data, $message, $debug, $statusCode);

// 에러 응답
ApiResponse::error($message, $code, $error);

// 컨트롤러에서 사용 (권장)
ApiResponse::handle(fn () => $this->service->method(), __('message.xxx'));
```

### 4.4 React 결재선 컴포넌트 위치

```
react/src/components/approval/DocumentCreate/ApprovalLineSection.tsx
- 직원 목록에서 결재자 선택
- getEmployees() 호출로 직원 목록 조회
- ApprovalPerson[] 형태로 결재선 관리
```

---

## 5. 작업 절차

### Step 1: 마이그레이션 생성 (⚠️ 컨펌 필요)

```bash
# 1. 마이그레이션 파일 생성
docker exec sam-api-1 php artisan make:migration create_documents_table

# 2. 위 3.1 스키마 코드 붙여넣기

# 3. 마이그레이션 실행
docker exec sam-api-1 php artisan migrate
```

### Step 2: 모델 생성

```bash
# Documents 폴더 생성 후 모델 파일 생성
mkdir -p api/app/Models/Documents

# 위 3.2 모델 코드 각각 생성
```

### Step 3: Service & Controller

```bash
# Service 생성
# api/app/Services/DocumentService.php

# Controller 생성
# api/app/Http/Controllers/Api/V1/DocumentController.php

# Routes 추가
# api/routes/api.php
```

### Step 4: MNG 화면

```bash
# mng/app/Models/Document.php
# mng/app/Http/Controllers/DocumentController.php
# mng/resources/views/documents/*.blade.php
```

### Step 5: React 연동

```bash
# react/src/components/document-system/DocumentForm/
# react/src/components/document-system/actions.ts
```

---

## 6. 컨펌 대기 목록

| # | 항목 | 변경 내용 | 영향 범위 | 상태 |
|---|------|----------|----------|------|
| 1 | DB 스키마 | 4개 테이블 신규 생성 | api/database | ⏳ 대기 |

---

## 7. 변경 이력

| 날짜 | 항목 | 변경 내용 | 파일 | 승인 |
|------|------|----------|------|------|
| 2025-01-28 | - | 계획 문서 작성 | - | - |
| 2025-01-28 | - | 자기완결성 보완 | - | - |
| 2026-01-28 | Phase 1.1 | 마이그레이션 파일 생성 및 실행 | `2026_01_28_200000_create_documents_table.php` | ✅ |
| 2026-01-28 | Phase 1.2 | 모델 생성 (Document, DocumentApproval, DocumentData, DocumentAttachment) | `api/app/Models/Documents/` | ✅ |
| 2026-01-28 | Phase 2 | MNG 관리자 패널 구현 (모델, 컨트롤러, 뷰, API) | `mng/app/Models/Documents/`, `mng/app/Http/Controllers/`, `mng/resources/views/documents/` | ✅ |

---

## 8. 검증 결과

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

### 8.1 테스트 케이스

| 시나리오 | 예상 결과 | 실제 결과 | 상태 |
|----------|----------|----------|------|
| 마이그레이션 실행 | 4개 테이블 생성 | 4개 테이블 생성 (524ms) | ✅ |
| 문서 생성 API | 201 Created | - | ⏳ |
| 결재 요청 | DRAFT → PENDING | - | ⏳ |
| 결재 승인 | PENDING → APPROVED | - | ⏳ |
| 결재 반려 | PENDING → REJECTED | - | ⏳ |

---

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

### 9.1 체크리스트 검증

| # | 검증 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 1 | 작업 목적이 명확한가? | ✅ | 1.1 배경 섹션 |
| 2 | 성공 기준이 정의되어 있는가? | ✅ | 8.1 테스트 케이스 |
| 3 | 작업 범위가 구체적인가? | ✅ | 2. 대상 범위 (파일 경로 포함) |
| 4 | 의존성이 명시되어 있는가? | ✅ | 0.1 전제 조건 |
| 5 | 참고 파일 경로가 정확한가? | ✅ | 4. 기존 코드 참조 |
| 6 | 단계별 절차가 실행 가능한가? | ✅ | 5. 작업 절차 |
| 7 | 검증 방법이 명시되어 있는가? | ✅ | 8. 검증 결과 |
| 8 | 모호한 표현이 없는가? | ✅ | 코드 템플릿 제공 |

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

| 질문 | 답변 가능 | 참조 섹션 |
|------|:--------:|----------|
| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 |
| Q2. 어디서부터 시작해야 하는가? | ✅ | 0.4 작업 순서, 5. 작업 절차 |
| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 2. 대상 범위 |
| Q4. 작업 완료 확인 방법은? | ✅ | 8. 검증 결과 |
| Q5. 막혔을 때 참고 문서는? | ✅ | 4. 기존 코드 참조 |

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

---

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