# 결재관리 워크플로우 상세

> **작성일**: 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
