feat: 문서 관리 시스템 Route 및 Swagger 구현 (Phase 1.8)

- Document API Route 등록 (CRUD 5개 엔드포인트)
- Swagger 문서 작성 (Document, DocumentApproval, DocumentData, DocumentAttachment 스키마)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-01-28 21:29:10 +09:00
parent 9bceaab8a3
commit e06b0637fa
3 changed files with 364 additions and 0 deletions

View File

@@ -0,0 +1,337 @@
<?php
namespace App\Swagger\v1;
/**
* @OA\Tag(name="Documents", description="문서 관리")
*
* @OA\Schema(
* schema="Document",
* type="object",
* description="문서 정보",
*
* @OA\Property(property="id", type="integer", example=1, description="문서 ID"),
* @OA\Property(property="tenant_id", type="integer", example=1, description="테넌트 ID"),
* @OA\Property(property="template_id", type="integer", example=5, description="템플릿 ID"),
* @OA\Property(property="document_no", type="string", example="DOC-20260128-0001", description="문서번호"),
* @OA\Property(property="title", type="string", example="2026년 1월 휴가 신청서", description="문서 제목"),
* @OA\Property(property="status", type="string", enum={"DRAFT","PENDING","APPROVED","REJECTED","CANCELLED"}, example="DRAFT", description="상태"),
* @OA\Property(property="linkable_type", type="string", example="App\\Models\\Order", nullable=true, description="연결 모델 타입"),
* @OA\Property(property="linkable_id", type="integer", example=100, nullable=true, description="연결 모델 ID"),
* @OA\Property(property="submitted_at", type="string", format="date-time", example="2026-01-28T10:00:00Z", nullable=true, description="결재 요청일"),
* @OA\Property(property="completed_at", type="string", format="date-time", example="2026-01-28T15:00:00Z", nullable=true, description="결재 완료일"),
* @OA\Property(property="created_by", type="integer", example=10, description="생성자 ID"),
* @OA\Property(property="template", type="object", nullable=true, description="템플릿 정보",
* @OA\Property(property="id", type="integer", example=5),
* @OA\Property(property="name", type="string", example="휴가 신청서"),
* @OA\Property(property="category", type="string", example="HR")
* ),
* @OA\Property(property="creator", type="object", nullable=true, description="생성자 정보",
* @OA\Property(property="id", type="integer", example=10),
* @OA\Property(property="name", type="string", example="홍길동")
* ),
* @OA\Property(property="approvals", type="array", description="결재선", @OA\Items(ref="#/components/schemas/DocumentApproval")),
* @OA\Property(property="data", type="array", description="문서 데이터", @OA\Items(ref="#/components/schemas/DocumentData")),
* @OA\Property(property="attachments", type="array", description="첨부파일", @OA\Items(ref="#/components/schemas/DocumentAttachment")),
* @OA\Property(property="created_at", type="string", format="date-time", example="2026-01-28T09:00:00Z"),
* @OA\Property(property="updated_at", type="string", format="date-time", example="2026-01-28T09:00:00Z")
* )
*
* @OA\Schema(
* schema="DocumentApproval",
* type="object",
* description="문서 결재 정보",
*
* @OA\Property(property="id", type="integer", example=1),
* @OA\Property(property="document_id", type="integer", example=1),
* @OA\Property(property="user_id", type="integer", example=5),
* @OA\Property(property="step", type="integer", example=1, description="결재 순서"),
* @OA\Property(property="role", type="string", example="승인", description="역할"),
* @OA\Property(property="status", type="string", enum={"PENDING","APPROVED","REJECTED"}, example="PENDING", description="상태"),
* @OA\Property(property="comment", type="string", example="승인합니다.", nullable=true, description="결재 의견"),
* @OA\Property(property="acted_at", type="string", format="date-time", nullable=true, description="결재 처리일"),
* @OA\Property(property="user", type="object", nullable=true, description="결재자 정보",
* @OA\Property(property="id", type="integer", example=5),
* @OA\Property(property="name", type="string", example="김부장")
* )
* )
*
* @OA\Schema(
* schema="DocumentData",
* type="object",
* description="문서 데이터 (EAV)",
*
* @OA\Property(property="id", type="integer", example=1),
* @OA\Property(property="document_id", type="integer", example=1),
* @OA\Property(property="section_id", type="integer", example=1, nullable=true, description="섹션 ID"),
* @OA\Property(property="column_id", type="integer", example=1, nullable=true, description="컬럼 ID"),
* @OA\Property(property="row_index", type="integer", example=0, description="행 인덱스"),
* @OA\Property(property="field_key", type="string", example="start_date", description="필드 키"),
* @OA\Property(property="field_value", type="string", example="2026-01-28", nullable=true, description="값")
* )
*
* @OA\Schema(
* schema="DocumentAttachment",
* type="object",
* description="문서 첨부파일",
*
* @OA\Property(property="id", type="integer", example=1),
* @OA\Property(property="document_id", type="integer", example=1),
* @OA\Property(property="file_id", type="integer", example=100),
* @OA\Property(property="attachment_type", type="string", enum={"general","signature","image","reference"}, example="general", description="첨부 유형"),
* @OA\Property(property="description", type="string", example="증빙서류", nullable=true, description="설명"),
* @OA\Property(property="file", type="object", nullable=true, description="파일 정보",
* @OA\Property(property="id", type="integer", example=100),
* @OA\Property(property="original_name", type="string", example="document.pdf"),
* @OA\Property(property="mime_type", type="string", example="application/pdf"),
* @OA\Property(property="size", type="integer", example=1024000)
* )
* )
*
* @OA\Schema(
* schema="DocumentCreateRequest",
* type="object",
* required={"template_id", "title"},
* description="문서 생성 요청",
*
* @OA\Property(property="template_id", type="integer", example=5, description="템플릿 ID"),
* @OA\Property(property="title", type="string", example="2026년 1월 휴가 신청서", description="문서 제목"),
* @OA\Property(property="linkable_type", type="string", example="App\\Models\\Order", nullable=true, description="연결 모델 타입"),
* @OA\Property(property="linkable_id", type="integer", example=100, nullable=true, description="연결 모델 ID"),
* @OA\Property(property="approvers", type="array", description="결재선", @OA\Items(
* type="object",
* required={"user_id"},
* @OA\Property(property="user_id", type="integer", example=5, description="결재자 ID"),
* @OA\Property(property="role", type="string", example="승인", description="역할")
* )),
* @OA\Property(property="data", type="array", description="문서 데이터", @OA\Items(
* type="object",
* required={"field_key"},
* @OA\Property(property="section_id", type="integer", example=1, nullable=true),
* @OA\Property(property="column_id", type="integer", example=1, nullable=true),
* @OA\Property(property="row_index", type="integer", example=0),
* @OA\Property(property="field_key", type="string", example="start_date"),
* @OA\Property(property="field_value", type="string", example="2026-01-28", nullable=true)
* )),
* @OA\Property(property="attachments", type="array", description="첨부파일", @OA\Items(
* type="object",
* required={"file_id"},
* @OA\Property(property="file_id", type="integer", example=100),
* @OA\Property(property="attachment_type", type="string", enum={"general","signature","image","reference"}, example="general"),
* @OA\Property(property="description", type="string", example="증빙서류", nullable=true)
* ))
* )
*
* @OA\Schema(
* schema="DocumentUpdateRequest",
* type="object",
* description="문서 수정 요청",
*
* @OA\Property(property="title", type="string", example="2026년 1월 휴가 신청서 (수정)", description="문서 제목"),
* @OA\Property(property="linkable_type", type="string", example="App\\Models\\Order", nullable=true, description="연결 모델 타입"),
* @OA\Property(property="linkable_id", type="integer", example=100, nullable=true, description="연결 모델 ID"),
* @OA\Property(property="approvers", type="array", description="결재선 (전체 교체)", @OA\Items(
* type="object",
* required={"user_id"},
* @OA\Property(property="user_id", type="integer", example=5),
* @OA\Property(property="role", type="string", example="승인")
* )),
* @OA\Property(property="data", type="array", description="문서 데이터 (전체 교체)", @OA\Items(
* type="object",
* required={"field_key"},
* @OA\Property(property="section_id", type="integer", nullable=true),
* @OA\Property(property="column_id", type="integer", nullable=true),
* @OA\Property(property="row_index", type="integer"),
* @OA\Property(property="field_key", type="string"),
* @OA\Property(property="field_value", type="string", nullable=true)
* )),
* @OA\Property(property="attachments", type="array", description="첨부파일 (전체 교체)", @OA\Items(
* type="object",
* required={"file_id"},
* @OA\Property(property="file_id", type="integer"),
* @OA\Property(property="attachment_type", type="string", enum={"general","signature","image","reference"}),
* @OA\Property(property="description", type="string", nullable=true)
* ))
* )
*/
class DocumentApi
{
/**
* @OA\Get(
* path="/api/v1/documents",
* tags={"Documents"},
* summary="문서 목록 조회",
* description="필터/검색/페이지네이션으로 문서 목록을 조회합니다.",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\Parameter(name="status", in="query", description="상태 필터", @OA\Schema(type="string", enum={"DRAFT","PENDING","APPROVED","REJECTED","CANCELLED"})),
* @OA\Parameter(name="template_id", in="query", description="템플릿 ID 필터", @OA\Schema(type="integer")),
* @OA\Parameter(name="search", in="query", description="검색어 (문서번호, 제목)", @OA\Schema(type="string")),
* @OA\Parameter(name="from_date", in="query", description="시작일 이후", @OA\Schema(type="string", format="date")),
* @OA\Parameter(name="to_date", in="query", description="종료일 이전", @OA\Schema(type="string", format="date")),
* @OA\Parameter(name="sort_by", in="query", description="정렬 기준", @OA\Schema(type="string", enum={"created_at","document_no","title","status","submitted_at","completed_at"}, default="created_at")),
* @OA\Parameter(name="sort_dir", in="query", description="정렬 방향", @OA\Schema(type="string", enum={"asc","desc"}, default="desc")),
* @OA\Parameter(ref="#/components/parameters/Page"),
* @OA\Parameter(ref="#/components/parameters/Size"),
*
* @OA\Response(
* response=200,
* description="조회 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
*
* @OA\Property(
* property="data",
* type="object",
* @OA\Property(property="current_page", type="integer", example=1),
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/Document")),
* @OA\Property(property="per_page", type="integer", example=20),
* @OA\Property(property="total", type="integer", example=50)
* )
* )
* }
* )
* ),
*
* @OA\Response(response=400, description="잘못된 요청", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function index() {}
/**
* @OA\Get(
* path="/api/v1/documents/{id}",
* tags={"Documents"},
* summary="문서 상세 조회",
* description="ID 기준 문서 상세 정보를 조회합니다. 템플릿, 결재선, 데이터, 첨부파일 포함.",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\Parameter(name="id", in="path", required=true, description="문서 ID", @OA\Schema(type="integer", example=1)),
*
* @OA\Response(
* response=200,
* description="조회 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Document"))
* }
* )
* ),
*
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=404, description="문서를 찾을 수 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function show() {}
/**
* @OA\Post(
* path="/api/v1/documents",
* tags={"Documents"},
* summary="문서 생성",
* description="템플릿 기반으로 새 문서를 생성합니다. 결재선, 데이터, 첨부파일을 함께 저장할 수 있습니다.",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\RequestBody(
* required=true,
*
* @OA\JsonContent(ref="#/components/schemas/DocumentCreateRequest")
* ),
*
* @OA\Response(
* response=201,
* description="생성 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Document"))
* }
* )
* ),
*
* @OA\Response(response=400, description="잘못된 요청", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function store() {}
/**
* @OA\Patch(
* path="/api/v1/documents/{id}",
* tags={"Documents"},
* summary="문서 수정",
* description="DRAFT 또는 REJECTED 상태의 문서만 수정 가능합니다. REJECTED 상태에서 수정하면 DRAFT로 변경됩니다.",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\Parameter(name="id", in="path", required=true, description="문서 ID", @OA\Schema(type="integer")),
*
* @OA\RequestBody(
* required=true,
*
* @OA\JsonContent(ref="#/components/schemas/DocumentUpdateRequest")
* ),
*
* @OA\Response(
* response=200,
* description="수정 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Document"))
* }
* )
* ),
*
* @OA\Response(response=400, description="잘못된 요청 (수정 불가 상태)", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=404, description="문서를 찾을 수 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function update() {}
/**
* @OA\Delete(
* path="/api/v1/documents/{id}",
* tags={"Documents"},
* summary="문서 삭제",
* description="DRAFT 상태의 문서만 삭제 가능합니다 (소프트 삭제).",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\Parameter(name="id", in="path", required=true, description="문서 ID", @OA\Schema(type="integer")),
*
* @OA\Response(
* response=200,
* description="삭제 성공",
*
* @OA\JsonContent(
*
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="삭제되었습니다."),
* @OA\Property(property="data", type="boolean", example=true)
* )
* ),
*
* @OA\Response(response=400, description="잘못된 요청 (삭제 불가 상태)", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=404, description="문서를 찾을 수 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function destroy() {}
}

View File

@@ -35,6 +35,7 @@
require __DIR__.'/api/v1/design.php';
require __DIR__.'/api/v1/files.php';
require __DIR__.'/api/v1/boards.php';
require __DIR__.'/api/v1/documents.php';
require __DIR__.'/api/v1/common.php';
// 공유 링크 다운로드 (인증 불필요 - auth.apikey 그룹 밖)

View File

@@ -0,0 +1,26 @@
<?php
/**
* 문서 관리 API 라우트 (v1)
*
* - 문서 CRUD
* - 결재 워크플로우 (보류 - 기존 시스템 연동 필요)
*/
use App\Http\Controllers\Api\V1\Documents\DocumentController;
use Illuminate\Support\Facades\Route;
Route::prefix('documents')->group(function () {
// 문서 CRUD
Route::get('/', [DocumentController::class, 'index'])->name('v1.documents.index');
Route::get('/{id}', [DocumentController::class, 'show'])->whereNumber('id')->name('v1.documents.show');
Route::post('/', [DocumentController::class, 'store'])->name('v1.documents.store');
Route::patch('/{id}', [DocumentController::class, 'update'])->whereNumber('id')->name('v1.documents.update');
Route::delete('/{id}', [DocumentController::class, 'destroy'])->whereNumber('id')->name('v1.documents.destroy');
// 결재 워크플로우 (보류 - 기존 시스템 연동 필요)
// Route::post('/{id}/submit', [DocumentController::class, 'submit'])->name('v1.documents.submit');
// Route::post('/{id}/approve', [DocumentController::class, 'approve'])->name('v1.documents.approve');
// Route::post('/{id}/reject', [DocumentController::class, 'reject'])->name('v1.documents.reject');
// Route::post('/{id}/cancel', [DocumentController::class, 'cancel'])->name('v1.documents.cancel');
});