Files
sam-docs/sam/docs/features/approvals/workflows.md
김보곤 490477421d docs: [approvals] 결재관리 시스템 문서 4종 작성
- README.md: 시스템 개요, 아키텍처, DB 스키마, 상태 관리, 권한 매트릭스
- workflows.md: 워크플로우 상세 (승인/반려/회수/보류/전결/복사재기안)
- api-reference.md: API 엔드포인트 20개 명세
- ui-screens.md: UI 화면 구성 및 인터랙션
- INDEX.md에 결재관리 문서 등록
2026-02-28 00:09:08 +09:00

21 KiB

결재관리 워크플로우 상세

작성일: 2026-02-28 상태: Phase 2 구현 완료 관련: README.md | API 명세 | UI 화면


1. 개요

이 문서는 결재관리 시스템의 각 동작(Action)에 대한 상세 워크플로우를 정의한다. 모든 워크플로우는 ApprovalService에서 트랜잭션으로 처리된다.

1.1 용어 정의

용어 설명
기안자 결재 문서를 작성한 사람 (drafter_id)
현재 결재자 결재선에서 현재 차례인 사람 (가장 작은 step_orderpending step)
결재자 step_typeapproval 또는 agreement인 참여자
참조자 step_typereference인 참여자 (의사결정 권한 없음)
전결 현재 결재자가 이후 모든 결재를 건너뛰고 즉시 최종 승인

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 회수 가능 판단 로직

// 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 아님 "삭제할 수 없는 상태입니다."

관련 문서


최종 업데이트: 2026-02-28