Files
sam-api/app/Swagger/v1/DocumentApi.php
권혁성 fb06975d97 feat:문서관리 Phase 4.1 - DocumentTemplate API + 결재 워크플로우 활성화
- DocumentTemplate 모델 6개 생성 (Template, ApprovalLine, BasicField, Section, SectionItem, Column)
- DocumentTemplateService (list/show) + DocumentTemplateController (index/show)
- GET /v1/document-templates, GET /v1/document-templates/{id} 라우트
- DocumentTemplateApi.php Swagger (7개 스키마, 2개 엔드포인트)
- Document 결재 워크플로우 4개 엔드포인트 활성화 (submit/approve/reject/cancel)
- ApproveRequest, RejectRequest FormRequest 생성
- DocumentApi.php Swagger에 결재 엔드포인트 4개 추가
- Document.template() 참조 경로 수정 (DocumentTemplate → Documents 네임스페이스)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 09:39:55 +09:00

485 lines
25 KiB
PHP

<?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() {}
// =========================================================================
// 결재 워크플로우
// =========================================================================
/**
* @OA\Post(
* path="/api/v1/documents/{id}/submit",
* tags={"Documents"},
* summary="결재 제출",
* description="DRAFT 또는 REJECTED 상태의 문서를 결재 요청합니다 (PENDING 상태로 변경).",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\Parameter(name="id", in="path", required=true, description="문서 ID", @OA\Schema(type="integer")),
*
* @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 submit() {}
/**
* @OA\Post(
* path="/api/v1/documents/{id}/approve",
* tags={"Documents"},
* summary="결재 승인",
* description="현재 사용자의 결재 단계를 승인합니다. 모든 단계 완료 시 문서가 APPROVED 상태로 변경됩니다.",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\Parameter(name="id", in="path", required=true, description="문서 ID", @OA\Schema(type="integer")),
*
* @OA\RequestBody(
* required=false,
*
* @OA\JsonContent(
*
* @OA\Property(property="comment", type="string", example="승인합니다.", nullable=true, description="결재 의견")
* )
* ),
*
* @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 approve() {}
/**
* @OA\Post(
* path="/api/v1/documents/{id}/reject",
* tags={"Documents"},
* summary="결재 반려",
* description="현재 사용자의 결재 단계를 반려합니다. 문서가 REJECTED 상태로 변경됩니다.",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\Parameter(name="id", in="path", required=true, description="문서 ID", @OA\Schema(type="integer")),
*
* @OA\RequestBody(
* required=true,
*
* @OA\JsonContent(
* required={"comment"},
*
* @OA\Property(property="comment", type="string", example="검사 기준 미달로 반려합니다.", description="반려 사유 (필수)")
* )
* ),
*
* @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 reject() {}
/**
* @OA\Post(
* path="/api/v1/documents/{id}/cancel",
* tags={"Documents"},
* summary="결재 취소/회수",
* description="작성자만 DRAFT 또는 PENDING 상태의 문서를 취소할 수 있습니다. CANCELLED 상태로 변경됩니다.",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\Parameter(name="id", in="path", required=true, description="문서 ID", @OA\Schema(type="integer")),
*
* @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 cancel() {}
}