- README.md: 시스템 개요, 아키텍처, DB 스키마, 상태 관리, 권한 매트릭스 - workflows.md: 워크플로우 상세 (승인/반려/회수/보류/전결/복사재기안) - api-reference.md: API 엔드포인트 20개 명세 - ui-screens.md: UI 화면 구성 및 인터랙션 - INDEX.md에 결재관리 문서 등록
21 KiB
21 KiB
결재관리 워크플로우 상세
작성일: 2026-02-28 상태: Phase 2 구현 완료 관련: README.md | API 명세 | UI 화면
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 처리 로직
- 문서번호 자동 채번 (
APR-YYMMDD-001형식) numbering_sequences테이블로 일일 순번 관리- 결재선 설정 시
approval_steps저장 + 사용자 정보 스냅샷 (이름, 부서, 직급) 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 처리 로직
isSubmittable()검증 →draft또는rejected인지 확인- 결재/합의 step 존재 확인
- 반려 후 재상신인 경우: 모든 step을
pending으로 초기화 (comment, acted_at도 초기화) 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 처리 로직
isActionable()검증 →pending상태인지 확인getCurrentApproverStep()→ 현재 차례 step 조회- 현재 step →
approved+ comment + acted_at - 다음 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 처리 로직
isActionable()검증- 현재 결재자 확인
- 반려 사유 빈 값 체크
- 현재 step →
rejected+ comment + acted_at - 문서 →
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 회수 가능 판단 로직
// 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 처리 로직
isCancellable()검증 →pending또는on_hold- 기안자 확인
- 첫 번째 결재/합의 step의 상태 확인 →
pending/on_hold이 아니면 거부 - 모든
pending/on_holdsteps →skipped - 문서 →
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 처리 로직
isHoldable()검증 →pending상태인지 확인getCurrentApproverStep()→ 현재 차례 step 조회- 현재 결재자 확인 (
approver_id === auth()->id()) - 보류 사유 빈 값 체크
- 현재 step →
on_hold+ comment + acted_at - 문서 →
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 처리 로직
isHoldReleasable()검증 →on_hold상태인지 확인on_hold상태인 step 조회- 해당 step의
approver_id가 현재 사용자인지 확인 - step →
pending+ comment/acted_at 초기화 - 문서 →
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 처리 로직
isActionable()검증getCurrentApproverStep()→ 현재 차례 step 조회- 현재 결재자 확인
- 현재 step →
approved+approval_type = 'pre_decided'+ comment + acted_at - 이후 모든 pending 결재/합의 steps →
skipped - 문서 →
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 처리 로직
isCopyable()검증 →approved/rejected/cancelled중 하나- 기안자 확인
- 새 문서 생성:
- 새 문서번호 채번
- 원본의 양식, 제목, 내용, 본문, 긴급 여부, 부서 복사
parent_doc_id = 원본.idstatus = 'draft',current_step = 0
- 결재선 복사: 원본의 모든 steps를 새 문서에 복사 (모두
pending상태) - 새 문서의 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 처리 로직
- 현재 사용자의 참조 step 조회
is_read = false인 step →is_read = true,read_at = now()- 이미 열람한 경우 중복 업데이트 없음 (
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 아님 | "삭제할 수 없는 상태입니다." |
관련 문서
최종 업데이트: 2026-02-28