- .gitignore를 sam/ 기반에서 루트 기반으로 변경 - sam/docs/ 하위 문서를 루트로 이동 (contracts, features, guides, plans 등) - sam/ 폴더 삭제 (docker, coocon 포함)
566 lines
21 KiB
Markdown
566 lines
21 KiB
Markdown
# 결재관리 워크플로우 상세
|
|
|
|
> **작성일**: 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
|