# 결재관리 워크플로우 상세 > **작성일**: 2026-02-28 > **상태**: Phase 2 구현 완료 > **관련**: [README.md](README.md) | [API 명세](api-reference.md) | [UI 화면](ui-screens.md) --- ## 1. 개요 이 문서는 결재관리 시스템의 각 동작(Action)에 대한 상세 워크플로우를 정의한다. 모든 워크플로우는 `ApprovalService`에서 트랜잭션으로 처리된다. ### 1.1 용어 정의 | 용어 | 설명 | |------|------| | **기안자** | 결재 문서를 작성한 사람 (`drafter_id`) | | **현재 결재자** | 결재선에서 현재 차례인 사람 (가장 작은 `step_order`의 `pending` step) | | **결재자** | `step_type`이 `approval` 또는 `agreement`인 참여자 | | **참조자** | `step_type`이 `reference`인 참여자 (의사결정 권한 없음) | | **전결** | 현재 결재자가 이후 모든 결재를 건너뛰고 즉시 최종 승인 | --- ## 2. 기안 작성 (createApproval) ### 2.1 흐름 ``` 사용자 → [양식 선택] → [제목/본문 입력] → [결재선 설정] → [임시저장] │ ▼ 새 Approval 생성 status = 'draft' current_step = 0 ``` ### 2.2 조건 - 모든 로그인 사용자가 작성 가능 - `form_id` 필수 (양식 선택) - 결재선(steps)은 저장 시 선택사항 (상신 시 필수) ### 2.3 처리 로직 1. 문서번호 자동 채번 (`APR-YYMMDD-001` 형식) 2. `numbering_sequences` 테이블로 일일 순번 관리 3. 결재선 설정 시 `approval_steps` 저장 + 사용자 정보 스냅샷 (이름, 부서, 직급) 4. `status = 'draft'`, `current_step = 0` --- ## 3. 상신 (submit) ### 3.1 흐름 ``` 기안자 → [상신 버튼] → 유효성 검사 → 결재선 검사 → 상신 완료 │ ▼ status = 'pending' current_step = 1 drafted_at = now() ``` ### 3.2 조건 | 조건 | 설명 | |------|------| | 문서 상태 | `draft` 또는 `rejected` | | 결재선 | 결재/합의 step 1명 이상 필수 | | 요청자 | 기안자만 | ### 3.3 처리 로직 1. `isSubmittable()` 검증 → `draft` 또는 `rejected`인지 확인 2. 결재/합의 step 존재 확인 3. **반려 후 재상신인 경우**: 모든 step을 `pending`으로 초기화 (comment, acted_at도 초기화) 4. `status → pending`, `drafted_at → now()`, `current_step → 1` ### 3.4 반려 후 재상신 ``` rejected 문서 │ ├── 기안자가 내용 수정 (updateApproval) │ └── 상신 (submit) ├── 모든 steps → pending (초기화) ├── status → pending └── current_step → 1 (처음부터 다시) ``` > 반려 후 재상신 시 결재선이 초기화되므로, 이전 결재 의견(comment)은 사라진다. --- ## 4. 승인 (approve) ### 4.1 흐름 ``` 현재 결재자 → [의견 입력(선택)] → [승인 버튼] │ ┌──────────┴──────────┐ │ 현재 step │ │ status → 'approved' │ │ comment → (입력값) │ │ acted_at → now() │ └──────────┬──────────┘ │ ┌─────────────────┴─────────────────┐ │ │ 다음 pending step 있음 마지막 결재자 │ │ current_step 갱신 status → 'approved' (다음 순서 결재자 대기) completed_at → now() ``` ### 4.2 조건 | 조건 | 설명 | |------|------| | 문서 상태 | `pending` | | 요청자 | 현재 차례 결재자 (`approver_id === auth()->id()`) | ### 4.3 처리 로직 1. `isActionable()` 검증 → `pending` 상태인지 확인 2. `getCurrentApproverStep()` → 현재 차례 step 조회 3. 현재 step → `approved` + comment + acted_at 4. 다음 pending 결재/합의 step 조회 - **있으면**: `current_step` 갱신 - **없으면**: 문서 `approved` + `completed_at` ### 4.4 순차결재 순서 결정 ``` step_order = 1 (결재) → step_order = 2 (합의) → step_order = 3 (결재) │ │ │ 1번째 승인 → 2번째 승인 → 3번째 승인 → 문서 완료 ``` > 결재와 합의는 동일한 순차 흐름을 따른다. `step_order` 순서대로 처리된다. --- ## 5. 반려 (reject) ### 5.1 흐름 ``` 현재 결재자 → [반려 사유 입력(필수)] → [반려 버튼] │ ┌──────────┴──────────┐ │ 현재 step │ │ status → 'rejected' │ │ comment → (사유) │ │ acted_at → now() │ └──────────┬──────────┘ │ ▼ 문서 status → 'rejected' completed_at → now() ``` ### 5.2 조건 | 조건 | 설명 | |------|------| | 문서 상태 | `pending` | | 요청자 | 현재 차례 결재자 | | 반려 사유 | **필수** (빈 값 불가) | ### 5.3 처리 로직 1. `isActionable()` 검증 2. 현재 결재자 확인 3. 반려 사유 빈 값 체크 4. 현재 step → `rejected` + comment + acted_at 5. 문서 → `rejected` + completed_at ### 5.4 반려 후 가능한 동작 ``` rejected 문서 │ ├── 기안자가 수정 → 재상신 (submit) │ └── 결재선 초기화, 처음부터 다시 진행 │ └── 기안자가 복사 재기안 (copyForRedraft) └── 새 문서 생성 (draft), 원본은 그대로 유지 ``` --- ## 6. 회수 (cancel) ### 6.1 흐름 ``` 기안자 → [회수 사유 입력(선택)] → [회수 버튼] │ ┌──────────┴──────────┐ │ 회수 가능 여부 판단 │ │ (첫 결재자 미처리?) │ └──────────┬──────────┘ │ ┌───────────┴───────────┐ │ │ 첫 결재자 첫 결재자 이미 pending/on_hold 승인/반려 │ │ 회수 진행 회수 불가 │ (에러 반환) ▼ 모든 pending/on_hold steps → 'skipped' 문서 status → 'cancelled' recall_reason → (입력값) completed_at → now() ``` ### 6.2 조건 | 조건 | 설명 | |------|------| | 문서 상태 | `pending` 또는 `on_hold` | | 요청자 | 기안자만 (`drafter_id === auth()->id()`) | | 첫 결재자 상태 | `pending` 또는 `on_hold` (이미 처리했으면 불가) | ### 6.3 회수 가능 판단 로직 ```php // 1단계: 문서 상태 확인 $approval->isCancellable() // pending 또는 on_hold // 2단계: 기안자 확인 $approval->drafter_id === auth()->id() // 3단계: 첫 결재자 상태 확인 $firstStep = steps.approvalOnly().orderBy('step_order').first() $firstStep->status === 'pending' || 'on_hold' // 미처리 상태여야 함 ``` ### 6.4 처리 로직 1. `isCancellable()` 검증 → `pending` 또는 `on_hold` 2. 기안자 확인 3. 첫 번째 결재/합의 step의 상태 확인 → `pending`/`on_hold`이 아니면 거부 4. 모든 `pending`/`on_hold` steps → `skipped` 5. 문서 → `cancelled` + `recall_reason` + `completed_at` --- ## 7. 보류 (hold) ### 7.1 흐름 ``` 현재 결재자 → [보류 사유 입력(필수)] → [보류 버튼] │ ┌──────────┴──────────┐ │ 현재 step │ │ status → 'on_hold' │ │ comment → (사유) │ │ acted_at → now() │ └──────────┬──────────┘ │ ▼ 문서 status → 'on_hold' ``` ### 7.2 조건 | 조건 | 설명 | |------|------| | 문서 상태 | `pending` (`isHoldable()`) | | 요청자 | 현재 차례 결재자 | | 보류 사유 | **필수** (빈 값 불가) | ### 7.3 처리 로직 1. `isHoldable()` 검증 → `pending` 상태인지 확인 2. `getCurrentApproverStep()` → 현재 차례 step 조회 3. 현재 결재자 확인 (`approver_id === auth()->id()`) 4. 보류 사유 빈 값 체크 5. 현재 step → `on_hold` + comment + acted_at 6. 문서 → `on_hold` ### 7.4 보류 상태의 영향 ``` on_hold 상태에서: ├── 다른 결재자는 아무 동작 불가 (결재 흐름 중단) ├── 기안자는 회수 가능 (첫 결재자가 미처리 상태이면) └── 보류한 결재자만 보류 해제 가능 ``` --- ## 8. 보류 해제 (releaseHold) ### 8.1 흐름 ``` 보류한 결재자 → [보류 해제 버튼] │ ┌──────────┴──────────┐ │ on_hold step │ │ status → 'pending' │ │ comment → null │ │ acted_at → null │ └──────────┬──────────┘ │ ▼ 문서 status → 'pending' (결재 흐름 재개) ``` ### 8.2 조건 | 조건 | 설명 | |------|------| | 문서 상태 | `on_hold` (`isHoldReleasable()`) | | 요청자 | 보류한 본인만 (`on_hold` step의 `approver_id === auth()->id()`) | ### 8.3 처리 로직 1. `isHoldReleasable()` 검증 → `on_hold` 상태인지 확인 2. `on_hold` 상태인 step 조회 3. 해당 step의 `approver_id`가 현재 사용자인지 확인 4. step → `pending` + comment/acted_at 초기화 5. 문서 → `pending` --- ## 9. 전결 (preDecide) ### 9.1 흐름 ``` 현재 결재자 → [의견 입력(선택)] → [전결 버튼] → 확인 팝업 │ ┌──────────┴──────────┐ │ 현재 step │ │ status → 'approved' │ │ approval_type → │ │ 'pre_decided' │ │ comment → (입력값) │ │ acted_at → now() │ └──────────┬──────────┘ │ ▼ 이후 모든 pending approval/agreement steps → status = 'skipped' │ ▼ 문서 status → 'approved' completed_at → now() ``` ### 9.2 조건 | 조건 | 설명 | |------|------| | 문서 상태 | `pending` (`isActionable()`) | | 요청자 | 현재 차례 결재자 | ### 9.3 처리 로직 1. `isActionable()` 검증 2. `getCurrentApproverStep()` → 현재 차례 step 조회 3. 현재 결재자 확인 4. 현재 step → `approved` + `approval_type = 'pre_decided'` + comment + acted_at 5. 이후 모든 pending 결재/합의 steps → `skipped` 6. 문서 → `approved` + `completed_at` ### 9.4 전결 예시 ``` step_order=1 (이사장, 결재) → approved (normal) step_order=2 (부장, 결재) → approved (pre_decided) ← 여기서 전결 step_order=3 (과장, 합의) → skipped (전결로 건너뜀) step_order=4 (팀장, 결재) → skipped (전결로 건너뜀) step_order=5 (참조자, 참조) → (참조는 영향 없음, 그대로 유지) 문서 → approved, completed_at = now() ``` > 전결은 결재/합의 step만 건너뛴다. 참조 step은 영향받지 않는다. --- ## 10. 복사 재기안 (copyForRedraft) ### 10.1 흐름 ``` 기안자 → [복사하여 재기안 버튼] │ ▼ ┌─────────────────────────────┐ │ 원본 문서에서 복사 │ │ ├── form_id │ │ ├── title │ │ ├── content (양식 데이터) │ │ ├── body │ │ ├── is_urgent │ │ ├── department_id │ │ └── 결재선 (모두 pending) │ └─────────────┬───────────────┘ │ ▼ 새 문서 생성 (status = 'draft') parent_doc_id = 원본.id 새 문서번호 채번 │ ▼ 수정 페이지로 이동 (/approval-mgmt/{newId}/edit) ``` ### 10.2 조건 | 조건 | 설명 | |------|------| | 원본 문서 상태 | `approved`, `rejected`, `cancelled` (`isCopyable()`) | | 요청자 | 기안자만 (`drafter_id === auth()->id()`) | ### 10.3 처리 로직 1. `isCopyable()` 검증 → `approved`/`rejected`/`cancelled` 중 하나 2. 기안자 확인 3. 새 문서 생성: - 새 문서번호 채번 - 원본의 양식, 제목, 내용, 본문, 긴급 여부, 부서 복사 - `parent_doc_id = 원본.id` - `status = 'draft'`, `current_step = 0` 4. 결재선 복사: 원본의 모든 steps를 새 문서에 복사 (모두 `pending` 상태) 5. 새 문서의 edit 페이지로 리다이렉트 ### 10.4 원본과의 관계 ``` 원본 문서 (approved/rejected/cancelled) │ └── parent_doc_id로 연결 │ ▼ 새 문서 (draft) ├── 상세 페이지에서 "원본 문서" 링크 표시 └── 기안자가 내용 수정 후 상신 가능 ``` --- ## 11. 참조 열람 추적 (markAsRead) ### 11.1 흐름 ``` 참조자 → [참조함 목록에서 문서 클릭] │ ├── markAsRead API 호출 │ ├── is_read → true │ └── read_at → now() │ └── 상세 페이지로 이동 ``` ### 11.2 조건 | 조건 | 설명 | |------|------| | 요청자 | 해당 문서의 참조자 (`step_type = 'reference'`) | ### 11.3 처리 로직 1. 현재 사용자의 참조 step 조회 2. `is_read = false`인 step → `is_read = true`, `read_at = now()` 3. 이미 열람한 경우 중복 업데이트 없음 (`where('is_read', false)`) --- ## 12. 전체 상태 전이 요약 ``` ┌───────────────────────────────────────────────────────────────────┐ │ │ │ draft ──submit()──→ pending ──approve()──→ (다음 step 또는) │ │ ▲ │ │ approved │ │ │ │ │ │ │ │ │ ├──reject()──→ rejected │ │ │ │ │ │ │ │ │ │ │ ├── 수정 → submit() │ │ │ │ │ │ (재상신, draft X) │ │ │ │ │ │ │ │ │ │ │ └── copyForRedraft() │ │ │ │ │ → 새 draft 생성 │ │ │ │ │ │ │ │ │ ├──hold()──→ on_hold │ │ │ │ │ │ │ │ │ │ │ ├── releaseHold() │ │ │ │ │ │ → pending 복원 │ │ │ │ │ │ │ │ │ │ │ └── cancel() (기안자) │ │ │ │ │ → cancelled │ │ │ │ │ │ │ │ │ ├──preDecide()──→ approved │ │ │ │ │ (이후 steps → skipped) │ │ │ │ │ │ │ │ │ └──cancel()──→ cancelled │ │ │ │ (기안자, 첫결재자 미처리 시) │ │ │ │ │ │ │ │ │ └── copyForRedraft() │ │ │ │ → 새 draft 생성 │ │ │ │ │ │ │ └── approved ──copyForRedraft() │ │ │ → 새 draft 생성 │ │ │ │ │ └── updateApproval() (draft/rejected 상태에서 수정) │ │ │ └───────────────────────────────────────────────────────────────────┘ ``` --- ## 13. 에러 케이스 정리 | 동작 | 에러 조건 | 에러 메시지 | |------|----------|------------| | submit | 상태가 draft/rejected 아님 | "상신할 수 없는 상태입니다." | | submit | 결재선 없음 | "결재선을 설정해주세요." | | approve | 상태가 pending 아님 | "승인할 수 없는 상태입니다." | | approve | 현재 결재자 아님 | "현재 결재자가 아닙니다." | | reject | 상태가 pending 아님 | "반려할 수 없는 상태입니다." | | reject | 사유 미입력 | "반려 사유를 입력해주세요." | | cancel | 상태가 pending/on_hold 아님 | "회수할 수 없는 상태입니다." | | cancel | 기안자 아님 | "기안자만 회수할 수 있습니다." | | cancel | 첫 결재자 이미 처리 | "첫 번째 결재자가 이미 처리하여 회수할 수 없습니다." | | hold | 상태가 pending 아님 | "보류할 수 없는 상태입니다." | | hold | 현재 결재자 아님 | "현재 결재자가 아닙니다." | | hold | 사유 미입력 | "보류 사유를 입력해주세요." | | releaseHold | 상태가 on_hold 아님 | "보류 해제할 수 없는 상태입니다." | | releaseHold | 보류한 본인 아님 | "보류한 결재자만 해제할 수 있습니다." | | preDecide | 상태가 pending 아님 | "전결할 수 없는 상태입니다." | | preDecide | 현재 결재자 아님 | "현재 결재자가 아닙니다." | | copyForRedraft | 상태가 approved/rejected/cancelled 아님 | "복사할 수 없는 상태입니다." | | copyForRedraft | 기안자 아님 | "기안자만 복사할 수 있습니다." | | update | 상태가 draft/rejected 아님 | "수정할 수 없는 상태입니다." | | delete | 상태가 draft 아님 | "삭제할 수 없는 상태입니다." | --- ## 관련 문서 - [README.md](README.md) — 시스템 전체 개요 - [API 명세](api-reference.md) — 엔드포인트별 요청/응답 - [UI 화면 구성](ui-screens.md) — 화면별 동작 --- **최종 업데이트**: 2026-02-28