docs: 문서 관리 시스템 Phase 1.5 문서 업데이트
- Phase 1.5 변경 내용 문서 추가 - 계획 문서 진행률 업데이트 (25%) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
59
changes/20260128_document_management_phase1_5.md
Normal file
59
changes/20260128_document_management_phase1_5.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# 변경 내용 요약
|
||||
|
||||
**날짜:** 2026-01-28
|
||||
**작업자:** Claude
|
||||
**Phase:** 1.5 - Service 생성
|
||||
|
||||
## 📋 변경 개요
|
||||
문서 관리 시스템의 DocumentService 클래스를 생성하여 문서 CRUD 및 결재 워크플로우 비즈니스 로직을 구현했습니다.
|
||||
|
||||
## 📁 수정된 파일
|
||||
- `app/Services/DocumentService.php` (신규) - 문서 관리 서비스
|
||||
|
||||
## 🔧 상세 변경 사항
|
||||
|
||||
### 1. DocumentService 구현
|
||||
|
||||
**주요 기능:**
|
||||
|
||||
#### 문서 목록/상세
|
||||
- `list(array $params)` - 페이징, 필터링, 검색 지원
|
||||
- `show(int $id)` - 상세 조회 (템플릿, 결재선, 데이터, 첨부파일 포함)
|
||||
|
||||
#### 문서 생성/수정/삭제
|
||||
- `create(array $data)` - 문서 생성 (결재선, 데이터, 첨부파일 포함)
|
||||
- `update(int $id, array $data)` - 문서 수정 (반려 상태 → DRAFT 전환)
|
||||
- `destroy(int $id)` - 문서 삭제 (DRAFT 상태만 가능)
|
||||
|
||||
#### 결재 처리
|
||||
- `submit(int $id)` - 결재 요청 (DRAFT → PENDING)
|
||||
- `approve(int $id, ?string $comment)` - 결재 승인
|
||||
- `reject(int $id, string $comment)` - 결재 반려
|
||||
- `cancel(int $id)` - 결재 취소/회수 (작성자만)
|
||||
|
||||
#### 헬퍼 메서드
|
||||
- `generateDocumentNo()` - 문서번호 생성 (DOC-YYYYMMDD-NNNN)
|
||||
- `createApprovals()` - 결재선 생성
|
||||
- `saveDocumentData()` - 문서 데이터 저장 (EAV)
|
||||
- `attachFiles()` - 첨부파일 연결
|
||||
|
||||
**구현 특징:**
|
||||
- Service-First 아키텍처 준수
|
||||
- Multi-tenancy 지원 (tenantId() 필터링)
|
||||
- DB 트랜잭션 처리
|
||||
- 순차 결재 로직 (이전 단계 완료 확인)
|
||||
- i18n 에러 메시지 키 사용
|
||||
|
||||
## ✅ 테스트 체크리스트
|
||||
- [x] PHP 문법 검사 통과
|
||||
- [x] Service 클래스 로드 성공
|
||||
- [x] Pint 포맷팅 완료
|
||||
- [ ] API 통합 테스트 (Phase 1.6 이후)
|
||||
|
||||
## ⚠️ 배포 시 주의사항
|
||||
특이사항 없음
|
||||
|
||||
## 🔗 관련 문서
|
||||
- Phase 1.1: 마이그레이션 (`20260128_document_management_phase1_1.md`)
|
||||
- Phase 1.2: 모델 생성 (별도 문서 없음, 커밋 참조)
|
||||
- 계획 문서: `docs/plans/document-management-system-plan.md`
|
||||
962
plans/document-management-system-plan.md
Normal file
962
plans/document-management-system-plan.md
Normal file
@@ -0,0 +1,962 @@
|
||||
# 문서 관리 시스템 개발 계획
|
||||
|
||||
> **작성일**: 2025-01-28
|
||||
> **목적**: 문서 템플릿 기반 실제 문서 작성/결재/관리 시스템
|
||||
> **상태**: 📋 계획 수립
|
||||
|
||||
---
|
||||
|
||||
## 📍 현재 진행 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| **마지막 완료 작업** | Phase 1.5 - Service 생성 ✅ |
|
||||
| **다음 작업** | Phase 1.6 - Controller 생성 (DocumentController) |
|
||||
| **진행률** | 3/12 (25%) |
|
||||
| **마지막 업데이트** | 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/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/create.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/` | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 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 스킬로 생성되었습니다.*
|
||||
Reference in New Issue
Block a user