# 문서 관리 시스템 개발 계획 > **작성일**: 2025-01-28 > **목적**: 문서 템플릿 기반 실제 문서 작성/결재/관리 시스템 > **상태**: 📋 계획 수립 --- ## 📍 현재 진행 상태 | 항목 | 내용 | |------|------| | **마지막 완료 작업** | Phase 2 - MNG 관리자 패널 구현 ✅ | | **다음 작업** | - (Phase 3 보류) | | **진행률** | 8/12 (67%) | | **보류 항목** | 결재 워크플로우 (submit/approve/reject/cancel) - 기존 시스템 연동 필요
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 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 '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 '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 'integer', ]; public function document(): BelongsTo { return $this->belongsTo(Document::class); } } ``` ### 3.3 Service 코드 템플릿 ```php 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 $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 스킬로 생성되었습니다.*