docs: [approvals] 결재관리 시스템 문서 4종 작성

- README.md: 시스템 개요, 아키텍처, DB 스키마, 상태 관리, 권한 매트릭스
- workflows.md: 워크플로우 상세 (승인/반려/회수/보류/전결/복사재기안)
- api-reference.md: API 엔드포인트 20개 명세
- ui-screens.md: UI 화면 구성 및 인터랙션
- INDEX.md에 결재관리 문서 등록
This commit is contained in:
김보곤
2026-02-28 00:09:08 +09:00
parent 359dc5d029
commit 490477421d
5 changed files with 2085 additions and 130 deletions

View File

@@ -0,0 +1,295 @@
# 결재관리 시스템
> **작성일**: 2026-02-28
> **상태**: Phase 2 구현 완료
> **프로젝트**: SAM MNG (관리자 웹)
> **우선순위**: 🔴 필수
---
## 1. 개요
### 1.1 목적
SAM MNG 전자결재 시스템. 기안부터 최종 승인, 반려, 회수, 보류, 전결, 참조까지 기업 결재 프로세스를 디지털화한다.
### 1.2 문서 구조
| 문서 | 설명 |
|------|------|
| **README.md** (이 문서) | 시스템 전체 개요, 아키텍처, 상태 관리 |
| [workflows.md](workflows.md) | 상세 워크플로우 (승인/반려/회수/보류/전결/복사재기안) |
| [api-reference.md](api-reference.md) | API 엔드포인트 명세 |
| [ui-screens.md](ui-screens.md) | 화면별 UI 구성 및 동작 |
### 1.3 구현 현황
| Phase | 범위 | 상태 |
|-------|------|------|
| **Phase 1** | 순차결재, 기안/상신/승인/반려/회수 | ✅ 완료 |
| **Phase 2** | 보류/해제, 전결, 참조 열람 추적, 복사 재기안 | ✅ 완료 |
| **Phase 3** | 병렬결재, 위임(대결), 알림 | 미착수 |
| **Phase 4** | ERP 연동, 결재 통계, 관리자 설정 | 미착수 |
---
## 2. 아키텍처
### 2.1 기술 스택
| 계층 | 기술 | 설명 |
|------|------|------|
| 뷰 | Blade + HTMX + Alpine.js | 동적 UI, 부분 렌더링 |
| API | Laravel Controller + Service | JSON API (내부용) |
| 모델 | Eloquent ORM | Multi-tenant 스코프 |
| DB | MySQL 8.0 | API 프로젝트에서 마이그레이션 관리 |
### 2.2 프로젝트 분리
```
API (/home/aweso/sam/api)
├── database/migrations/ ← 모든 결재 테이블 마이그레이션
MNG (/home/aweso/sam/mng)
├── app/Models/Approvals/ ← 모델 (Approval, ApprovalStep, ApprovalForm, ApprovalLine, ApprovalDelegation)
├── app/Services/ ← ApprovalService (비즈니스 로직)
├── app/Http/Controllers/ ← ApprovalController (웹), ApprovalApiController (API)
├── resources/views/approvals/ ← Blade 뷰
└── routes/ ← 웹 라우트 + API 라우트
```
### 2.3 핵심 클래스
```
ApprovalService
├── 목록 조회: getMyDrafts(), getPendingForMe(), getCompletedByMe(), getReferencesForMe()
├── CRUD: createApproval(), updateApproval(), deleteApproval(), getApproval()
├── 워크플로우: submit(), approve(), reject(), cancel(), hold(), releaseHold(), preDecide(), copyForRedraft()
├── 참조: markAsRead()
└── 유틸: getBadgeCounts(), getApprovalLines(), getApprovalForms(), saveApprovalSteps()
```
---
## 3. 데이터베이스
### 3.1 테이블 관계
```
approval_forms (결재 양식)
│ 1:N
approvals (결재 문서)
│ 1:N │ N:1 (self)
▼ ▼
approval_steps (결재 단계) approvals (parent_doc_id → 원본 문서)
approval_lines (결재선 템플릿) ← approvals.line_id 참조
approval_delegations (위임 설정) ← Phase 3 준비
```
### 3.2 approvals (결재 문서)
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `id` | BIGINT PK | |
| `tenant_id` | BIGINT | 테넌트 격리 |
| `document_number` | VARCHAR | `APR-YYMMDD-001` 형식 |
| `form_id` | BIGINT FK | 양식 |
| `line_id` | BIGINT FK NULL | 결재선 템플릿 |
| `title` | VARCHAR(200) | 제목 |
| `content` | JSON | 양식 필드 데이터 |
| `body` | TEXT NULL | 본문 |
| `status` | VARCHAR(20) | 문서 상태 (6가지) |
| `is_urgent` | BOOLEAN | 긴급 여부 |
| `drafter_id` | BIGINT FK | 기안자 |
| `department_id` | BIGINT FK NULL | 기안 부서 |
| `current_step` | INT | 현재 결재 단계 번호 |
| `drafted_at` | TIMESTAMP NULL | 상신 일시 |
| `completed_at` | TIMESTAMP NULL | 완료 일시 |
| `recall_reason` | TEXT NULL | 회수 사유 |
| `parent_doc_id` | BIGINT FK NULL | 재기안 원본 문서 |
| `attachments` | JSON NULL | 첨부파일 |
### 3.3 approval_steps (결재 단계)
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `id` | BIGINT PK | |
| `approval_id` | BIGINT FK | 결재 문서 |
| `step_order` | INT | 순서 (1, 2, 3...) |
| `step_type` | VARCHAR | `approval`, `agreement`, `reference` |
| `parallel_group` | INT NULL | 병렬 그룹 (Phase 3) |
| `approver_id` | BIGINT FK | 결재자 |
| `acted_by` | BIGINT FK NULL | 실제 처리자 (대결 시) |
| `approver_name` | VARCHAR | 결재자명 스냅샷 |
| `approver_department` | VARCHAR | 부서 스냅샷 |
| `approver_position` | VARCHAR | 직급 스냅샷 |
| `status` | VARCHAR(20) | 단계 상태 (5가지) |
| `approval_type` | VARCHAR(20) | `normal`, `pre_decided`, `delegated` |
| `comment` | TEXT NULL | 결재 의견 |
| `acted_at` | TIMESTAMP NULL | 처리 일시 |
| `is_read` | BOOLEAN | 참조 열람 여부 |
| `read_at` | TIMESTAMP NULL | 열람 일시 |
### 3.4 approval_delegations (위임 설정, Phase 3)
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `id` | BIGINT PK | |
| `tenant_id` | BIGINT FK | |
| `delegator_id` | BIGINT FK | 위임자 |
| `delegate_id` | BIGINT FK | 대리인 |
| `start_date` | DATE | 위임 시작일 |
| `end_date` | DATE | 위임 종료일 |
| `form_ids` | JSON NULL | 대상 양식 (NULL=전체) |
| `notify_delegator` | BOOLEAN | 대결 시 보고 여부 |
| `is_active` | BOOLEAN | 활성 여부 |
| `reason` | VARCHAR(200) | 위임 사유 |
---
## 4. 상태 관리
### 4.1 문서 상태 (6가지)
| 상태 | 코드 | 라벨 | 색상 | 설명 |
|------|------|------|------|------|
| 임시저장 | `draft` | 임시저장 | gray | 작성 중, 미상신 |
| 진행 | `pending` | 진행 | blue | 결재선 순환 중 |
| 완료 | `approved` | 완료 | green | 최종 승인 |
| 반려 | `rejected` | 반려 | red | 결재자가 반려 |
| 회수 | `cancelled` | 회수 | yellow | 기안자가 회수 |
| 보류 | `on_hold` | 보류 | amber | 결재자가 보류 |
### 4.2 단계 상태 (5가지)
| 상태 | 코드 | 라벨 | 아이콘 | 설명 |
|------|------|------|--------|------|
| 대기 | `pending` | 대기 | 숫자 | 차례 아직 아님 |
| 승인 | `approved` | 승인 | ✓ (녹색) | 승인 완료 |
| 반려 | `rejected` | 반려 | ✗ (적색) | 반려 |
| 건너뜀 | `skipped` | 건너뜀 | — (회색) | 전결/회수로 소멸 |
| 보류 | `on_hold` | 보류 | ⏸ (노란) | 보류 중 |
### 4.3 결재 유형 (approval_type)
| 유형 | 코드 | 아이콘 | 설명 |
|------|------|--------|------|
| 일반결재 | `normal` | ✓ | 기본 승인 |
| 전결 | `pre_decided` | ⚡ (남색) | 이후 단계 모두 건너뛰고 즉시 완료 |
| 대결 | `delegated` | — | 대리인이 처리 (Phase 3) |
### 4.4 참여자 역할 (step_type)
| 역할 | 코드 | 의사결정 | 설명 |
|------|------|---------|------|
| 결재 | `approval` | ✅ 있음 | 승인/반려/보류/전결 가능 |
| 합의 | `agreement` | ✅ 있음 | 타부서 동의 (승인/반려 가능) |
| 참조 | `reference` | ❌ 없음 | 열람만 가능, 열람 추적 |
### 4.5 상태 전이 다이어그램
```
┌─────────────────────────────┐
│ │
┌────────┐ submit() │ ┌─────────┐ │
│ draft │────────────→│ │ pending │ │
└────────┘ │ └────┬────┘ │
▲ │ │ │
│ │ ┌────┼─────────┬───────┐ │
│ (수정 후 재상신) │ │ │ │ │ │
│ │ │ approve() reject() hold()│
│ │ │ │ │ │ │
│ │ │ ▼ ▼ ▼ │
│ │ │ 다음 step rejected on_hold│
│ │ │ 또는 │ │ │
│ │ │ approved │ releaseHold()
│ │ │ │ │ │ │
│ │ │ │ │ │ │
│ │ └────┼────────┼───────┘ │
│ │ │ │ │
│ │ preDecide() │ │
│ │ → approved │ │
│ │ │ │ cancel() │
│ │ │ │ │ │
│ │ ▼ │ ▼ │
│ │ ┌─────────┐ │ ┌──────────┐
│ │ │approved │ │ │cancelled │
│ │ └─────────┘ │ └──────────┘
│ │ │ │ │
│ │ │ │ │
│ │ copyForRedraft() │
│ │ │ │ │
└───────────────────┼───────┴────────┘ │
(새 draft 생성) │ │
│ copyForRedraft() │
│◀──────────────────────┘
└─────────────────────────────┘
```
---
## 5. 권한 매트릭스
### 5.1 누가 무엇을 할 수 있는가
| 액션 | 대상자 | 조건 |
|------|--------|------|
| **기안 작성** | 모든 사용자 | — |
| **수정** | 기안자 | `draft` 또는 `rejected` |
| **삭제** | 기안자 | `draft`만 |
| **상신** | 기안자 | `draft` 또는 `rejected`, 결재선 1명 이상 |
| **승인** | 현재 결재자 | `pending`, 자신이 현재 차례 |
| **반려** | 현재 결재자 | `pending`, 사유 필수 |
| **보류** | 현재 결재자 | `pending`, 사유 필수 |
| **보류 해제** | 보류한 결재자 | `on_hold`, 자신이 보류한 건 |
| **전결** | 현재 결재자 | `pending`, 이후 모든 단계 건너뜀 |
| **회수** | 기안자 | `pending` 또는 `on_hold`, 첫 결재자 미처리 |
| **복사 재기안** | 기안자 | `approved`, `rejected`, `cancelled` |
| **참조 열람** | 참조자 | `reference` step 보유 |
### 5.2 회수 가능 조건 상세
```
회수(cancel) 가능 여부 판단:
1. 문서 상태가 pending 또는 on_hold인가? → 아니면 불가
2. 요청자가 기안자(drafter_id)인가? → 아니면 불가
3. 첫 번째 결재자(approval/agreement)의 상태가 pending 또는 on_hold인가?
→ 이미 approved/rejected이면 불가 (첫 결재자가 이미 처리)
```
---
## 6. 메뉴 구조
```
결재관리
├── 기안함 /approval-mgmt/drafts ← 내가 기안한 문서
├── 결재 대기함 /approval-mgmt/pending ← 내가 결재해야 할 문서
├── 처리 완료함 /approval-mgmt/completed ← 내가 결재한 문서
└── 참조함 /approval-mgmt/references ← 참조 문서 (열람 추적)
```
### 추가 페이지
| URL | 설명 |
|-----|------|
| `/approval-mgmt/create` | 기안 작성 |
| `/approval-mgmt/{id}` | 상세 조회 |
| `/approval-mgmt/{id}/edit` | 기안 수정 |
---
## 7. 관련 문서
- [결재관리 워크플로우 상세](workflows.md) — 각 동작의 상세 흐름
- [API 명세](api-reference.md) — 엔드포인트 목록 및 요청/응답 예시
- [UI 화면 구성](ui-screens.md) — 화면별 UI 요소 및 동작
- [기획서 원본](../../plans/approval-management-system-plan.md) — Phase 1~4 전체 기획
---
**최종 업데이트**: 2026-02-28

View File

@@ -0,0 +1,594 @@
# 결재관리 API 명세
> **작성일**: 2026-02-28
> **상태**: Phase 2 구현 완료
> **Base URL**: `/api/admin/approvals`
> **미들웨어**: `web`, `auth`, `hq.member`
> **관련**: [README.md](README.md) | [워크플로우](workflows.md) | [UI 화면](ui-screens.md)
---
## 1. 개요
모든 API는 JSON 응답을 반환한다. 인증은 세션 기반이며, CSRF 토큰이 필요하다.
### 1.1 공통 응답 형식
**성공:**
```json
{
"success": true,
"message": "처리 메시지",
"data": { ... }
}
```
**실패 (400):**
```json
{
"success": false,
"message": "에러 메시지"
}
```
### 1.2 공통 헤더
```
Content-Type: application/json
Accept: application/json
X-CSRF-TOKEN: {csrf_token}
```
---
## 2. 목록 조회 API
### 2.1 기안함
내가 기안한 문서 목록을 조회한다.
```
GET /api/admin/approvals/drafts
```
**Query Parameters:**
| 파라미터 | 타입 | 설명 |
|---------|------|------|
| `search` | string | 제목/문서번호 검색 |
| `status` | string | 상태 필터 (`draft`, `pending`, `approved`, `rejected`, `cancelled`, `on_hold`) |
| `is_urgent` | boolean | 긴급 문서만 |
| `date_from` | date | 시작일 (YYYY-MM-DD) |
| `date_to` | date | 종료일 (YYYY-MM-DD) |
| `per_page` | int | 페이지당 건수 (기본 15) |
| `page` | int | 페이지 번호 |
**응답:** Laravel 페이지네이션 형식
```json
{
"data": [
{
"id": 1,
"document_number": "APR-260228-001",
"title": "휴가 신청",
"status": "pending",
"is_urgent": false,
"form": { "id": 1, "name": "휴가신청서" },
"steps": [...],
"created_at": "2026-02-28T10:00:00",
"drafted_at": "2026-02-28T10:05:00"
}
],
"current_page": 1,
"last_page": 3,
"per_page": 15,
"total": 42
}
```
---
### 2.2 결재 대기함
내가 현재 결재해야 할 문서 목록을 조회한다.
```
GET /api/admin/approvals/pending
```
**Query Parameters:**
| 파라미터 | 타입 | 설명 |
|---------|------|------|
| `search` | string | 제목/문서번호 검색 |
| `is_urgent` | boolean | 긴급 문서만 |
| `date_from` | date | 시작일 |
| `date_to` | date | 종료일 |
| `per_page` | int | 페이지당 건수 |
> 현재 사용자가 결재 차례인 문서만 표시된다. 이미 승인/반려한 문서는 표시되지 않는다.
---
### 2.3 처리 완료함
내가 승인 또는 반려한 문서 목록을 조회한다.
```
GET /api/admin/approvals/completed
```
**Query Parameters:**
| 파라미터 | 타입 | 설명 |
|---------|------|------|
| `search` | string | 제목/문서번호 검색 |
| `status` | string | 상태 필터 |
| `date_from` | date | 시작일 |
| `date_to` | date | 종료일 |
| `per_page` | int | 페이지당 건수 |
---
### 2.4 참조함
내가 참조자로 지정된 문서 목록을 조회한다.
```
GET /api/admin/approvals/references
```
**Query Parameters:**
| 파라미터 | 타입 | 설명 |
|---------|------|------|
| `search` | string | 제목/문서번호 검색 |
| `is_read` | string | 열람 상태 필터 (`true`=열람완료, `false`=미열람) |
| `date_from` | date | 시작일 |
| `date_to` | date | 종료일 |
| `per_page` | int | 페이지당 건수 |
---
## 3. CRUD API
### 3.1 상세 조회
```
GET /api/admin/approvals/{id}
```
**응답:**
```json
{
"success": true,
"data": {
"id": 1,
"tenant_id": 1,
"document_number": "APR-260228-001",
"form_id": 1,
"line_id": null,
"title": "휴가 신청",
"content": {},
"body": "2월 27일~28일 연차 사용 신청합니다.",
"status": "pending",
"is_urgent": false,
"drafter_id": 10,
"department_id": 3,
"current_step": 2,
"drafted_at": "2026-02-28T10:05:00",
"completed_at": null,
"recall_reason": null,
"parent_doc_id": null,
"form": { "id": 1, "name": "휴가신청서" },
"drafter": { "id": 10, "name": "홍길동" },
"line": null,
"steps": [
{
"id": 1,
"step_order": 1,
"step_type": "approval",
"approver_id": 20,
"approver_name": "김과장",
"approver_department": "경영지원팀",
"approver_position": "과장",
"status": "approved",
"approval_type": "normal",
"comment": "승인합니다.",
"acted_at": "2026-02-28T11:00:00",
"is_read": false,
"read_at": null
},
{
"id": 2,
"step_order": 2,
"step_type": "approval",
"approver_id": 30,
"approver_name": "박부장",
"approver_department": "경영지원팀",
"approver_position": "부장",
"status": "pending",
"approval_type": "normal",
"comment": null,
"acted_at": null,
"is_read": false,
"read_at": null
}
]
}
}
```
---
### 3.2 생성 (임시저장)
```
POST /api/admin/approvals
```
**Request Body:**
```json
{
"form_id": 1,
"title": "휴가 신청",
"body": "2월 27일~28일 연차 사용",
"is_urgent": false,
"steps": [
{ "user_id": 20, "step_type": "approval" },
{ "user_id": 30, "step_type": "approval" },
{ "user_id": 40, "step_type": "reference" }
]
}
```
**Validation:**
| 필드 | 규칙 |
|------|------|
| `form_id` | required, exists:approval_forms,id |
| `title` | required, string, max:200 |
| `body` | nullable, string |
| `is_urgent` | boolean |
| `steps` | nullable, array |
| `steps.*.user_id` | required_with:steps, exists:users,id |
| `steps.*.step_type` | required_with:steps, in:approval,agreement,reference |
**응답 (201):**
```json
{
"success": true,
"message": "결재 문서가 저장되었습니다.",
"data": { ... }
}
```
---
### 3.3 수정
```
PUT /api/admin/approvals/{id}
```
> `draft` 또는 `rejected` 상태에서만 수정 가능
**Request Body:** (생성과 동일, 모든 필드 선택)
**Validation:**
| 필드 | 규칙 |
|------|------|
| `title` | sometimes, string, max:200 |
| `body` | nullable, string |
| `is_urgent` | boolean |
| `steps` | nullable, array |
---
### 3.4 삭제
```
DELETE /api/admin/approvals/{id}
```
> `draft` 상태에서만 삭제 가능
**응답:**
```json
{
"success": true,
"message": "결재 문서가 삭제되었습니다."
}
```
---
## 4. 워크플로우 API
### 4.1 상신
```
POST /api/admin/approvals/{id}/submit
```
> 기안자가 `draft`/`rejected` 문서를 결재 요청한다.
**Request Body:** 없음
**응답:** `{ "success": true, "message": "결재가 상신되었습니다.", "data": {...} }`
---
### 4.2 승인
```
POST /api/admin/approvals/{id}/approve
```
> 현재 결재자가 승인한다.
**Request Body:**
```json
{
"comment": "승인합니다." // 선택
}
```
**응답:** `{ "success": true, "message": "승인되었습니다.", "data": {...} }`
---
### 4.3 반려
```
POST /api/admin/approvals/{id}/reject
```
> 현재 결재자가 반려한다. 사유 필수.
**Request Body:**
```json
{
"comment": "예산 초과로 반려합니다." // 필수
}
```
**Validation:** `comment` — required, string, max:1000
**응답:** `{ "success": true, "message": "반려되었습니다.", "data": {...} }`
---
### 4.4 회수
```
POST /api/admin/approvals/{id}/cancel
```
> 기안자가 `pending`/`on_hold` 문서를 회수한다. 첫 결재자 미처리 시에만 가능.
**Request Body:**
```json
{
"recall_reason": "내용 수정 필요" // 선택
}
```
**응답:** `{ "success": true, "message": "결재가 회수되었습니다.", "data": {...} }`
---
### 4.5 보류
```
POST /api/admin/approvals/{id}/hold
```
> 현재 결재자가 결재를 보류한다. 사유 필수.
**Request Body:**
```json
{
"comment": "추가 자료 검토 필요" // 필수
}
```
**Validation:** `comment` — required, string, max:1000
**응답:** `{ "success": true, "message": "보류되었습니다.", "data": {...} }`
---
### 4.6 보류 해제
```
POST /api/admin/approvals/{id}/release-hold
```
> 보류한 결재자가 보류를 해제한다.
**Request Body:** 없음
**응답:** `{ "success": true, "message": "보류가 해제되었습니다.", "data": {...} }`
---
### 4.7 전결
```
POST /api/admin/approvals/{id}/pre-decide
```
> 현재 결재자가 이후 모든 결재를 건너뛰고 최종 승인한다.
**Request Body:**
```json
{
"comment": "전결 처리합니다." // 선택
}
```
**응답:** `{ "success": true, "message": "전결 처리되었습니다.", "data": {...} }`
---
### 4.8 복사 재기안
```
POST /api/admin/approvals/{id}/copy
```
> 기안자가 `approved`/`rejected`/`cancelled` 문서를 복사하여 새 draft를 생성한다.
**Request Body:** 없음
**응답:**
```json
{
"success": true,
"message": "문서가 복사되었습니다.",
"data": {
"id": 15,
"document_number": "APR-260228-003",
"parent_doc_id": 1,
"status": "draft",
...
}
}
```
> 응답의 `data.id`를 사용하여 `/approval-mgmt/{id}/edit`로 이동한다.
---
### 4.9 참조 열람 추적
```
POST /api/admin/approvals/{id}/mark-read
```
> 참조자가 문서를 열람했음을 기록한다.
**Request Body:** 없음
**응답:** `{ "success": true, "message": "열람 처리되었습니다." }`
---
## 5. 유틸리티 API
### 5.1 결재선 템플릿 목록
```
GET /api/admin/approvals/lines
```
**응답:**
```json
{
"success": true,
"data": [
{ "id": 1, "name": "일반 결재선", "steps": [...] }
]
}
```
---
### 5.2 양식 목록
```
GET /api/admin/approvals/forms
```
**응답:**
```json
{
"success": true,
"data": [
{ "id": 1, "name": "휴가신청서", "is_active": true }
]
}
```
---
### 5.3 미처리 건수 (뱃지)
```
GET /api/admin/approvals/badge-counts
```
**응답:**
```json
{
"success": true,
"data": {
"pending": 3,
"draft": 1,
"reference_unread": 5
}
}
```
| 필드 | 설명 |
|------|------|
| `pending` | 내가 결재해야 할 문서 수 |
| `draft` | 내 임시저장 문서 수 |
| `reference_unread` | 미열람 참조 문서 수 |
---
## 6. 라우트 전체 목록
| Method | Path | 컨트롤러 메서드 | 이름 | 설명 |
|--------|------|---------------|------|------|
| GET | `/drafts` | `drafts` | `drafts` | 기안함 |
| GET | `/pending` | `pending` | `pending` | 결재 대기함 |
| GET | `/completed` | `completed` | `completed` | 처리 완료함 |
| GET | `/references` | `references` | `references` | 참조함 |
| GET | `/lines` | `lines` | `lines` | 결재선 템플릿 |
| GET | `/forms` | `forms` | `forms` | 양식 목록 |
| GET | `/badge-counts` | `badgeCounts` | `badge-counts` | 뱃지 건수 |
| POST | `/` | `store` | `store` | 생성 |
| GET | `/{id}` | `show` | `show` | 상세 |
| PUT | `/{id}` | `update` | `update` | 수정 |
| DELETE | `/{id}` | `destroy` | `destroy` | 삭제 |
| POST | `/{id}/submit` | `submit` | `submit` | 상신 |
| POST | `/{id}/approve` | `approve` | `approve` | 승인 |
| POST | `/{id}/reject` | `reject` | `reject` | 반려 |
| POST | `/{id}/cancel` | `cancel` | `cancel` | 회수 |
| POST | `/{id}/hold` | `hold` | `hold` | 보류 |
| POST | `/{id}/release-hold` | `releaseHold` | `release-hold` | 보류 해제 |
| POST | `/{id}/pre-decide` | `preDecide` | `pre-decide` | 전결 |
| POST | `/{id}/copy` | `copyForRedraft` | `copy` | 복사 재기안 |
| POST | `/{id}/mark-read` | `markAsRead` | `mark-read` | 열람 추적 |
---
## 관련 문서
- [README.md](README.md) — 시스템 전체 개요
- [워크플로우 상세](workflows.md) — 각 동작의 상세 흐름
- [UI 화면 구성](ui-screens.md) — 화면별 동작
---
**최종 업데이트**: 2026-02-28

View File

@@ -0,0 +1,381 @@
# 결재관리 UI 화면 구성
> **작성일**: 2026-02-28
> **상태**: Phase 2 구현 완료
> **기술**: Blade + HTMX + Alpine.js + Tailwind CSS
> **관련**: [README.md](README.md) | [워크플로우](workflows.md) | [API 명세](api-reference.md)
---
## 1. 개요
결재관리 화면은 MNG(관리자 웹)에서 Blade 템플릿으로 구현되며, API 호출은 `fetch()`를 사용한다.
### 1.1 파일 구조
```
resources/views/approvals/
├── drafts.blade.php ← 기안함 (목록)
├── pending.blade.php ← 결재 대기함 (목록)
├── completed.blade.php ← 처리 완료함 (목록)
├── references.blade.php ← 참조함 (목록)
├── create.blade.php ← 기안 작성
├── edit.blade.php ← 기안 수정
├── show.blade.php ← 상세 조회 + 결재 처리
└── partials/
├── _status-badge.blade.php ← 상태 뱃지 컴포넌트
└── _step-progress.blade.php ← 결재 단계 진행 표시
```
---
## 2. 목록 화면
### 2.1 기안함 (`/approval-mgmt/drafts`)
내가 기안한 모든 문서를 표시한다.
**UI 구성:**
```
┌──────────────────────────────────────────────────────────┐
│ 기안함 [+ 새 기안] │
├──────────────────────────────────────────────────────────┤
│ [검색] [상태 필터 ▼] [긴급만 □] [날짜 범위] │
├──────────────────────────────────────────────────────────┤
│ 문서번호 │ 제목 │ 양식 │ 상태 │ 기안일 │
│ APR-260228-001│ 휴가 신청 │ 휴가서 │ 🟢완료 │ 02-28 │
│ APR-260228-002│ 출장 보고 │ 출장서 │ 🔵진행 │ 02-28 │
│ APR-260227-001│ 경비 청구 │ 경비서 │ ⬜임시 │ 02-27 │
├──────────────────────────────────────────────────────────┤
│ [◀ 이전] 1 / 3 [다음 ▶] │
└──────────────────────────────────────────────────────────┘
```
**상태 필터:** 전체, 임시저장, 진행, 완료, 반려, 회수, 보류
---
### 2.2 결재 대기함 (`/approval-mgmt/pending`)
내가 현재 결재해야 할 문서를 표시한다.
**UI 구성:**
```
┌──────────────────────────────────────────────────────────┐
│ 결재 대기함 [뱃지: 3건] │
├──────────────────────────────────────────────────────────┤
│ 문서번호 │ 제목 │ 기안자 │ 양식 │ 상신일 │
│ 🔴 APR-260..│ 긴급 승인 │ 홍길동 │ 구매서 │ 02-28 │
│ APR-260..│ 휴가 신청 │ 김영희 │ 휴가서 │ 02-27 │
└──────────────────────────────────────────────────────────┘
```
> 긴급 문서는 🔴 아이콘과 함께 상단에 표시
---
### 2.3 참조함 (`/approval-mgmt/references`)
내가 참조자로 지정된 문서를 표시한다.
**UI 구성:**
```
┌──────────────────────────────────────────────────────────┐
│ 참조함 │
├──────────────────────────────────────────────────────────┤
│ [전체] [미열람 (5)] [열람완료] │
├──────────────────────────────────────────────────────────┤
│ 문서번호 │ 제목 │ 기안자 │ 상태 │ 열람 │
│ APR-260228-001│ 회의록 │ 박부장 │ 🟢완료 │ ❌미열람│
│ APR-260227-003│ 인사발령 │ 이팀장 │ 🔵진행 │ ✅열람 │
└──────────────────────────────────────────────────────────┘
```
**열람 추적:**
- 문서 클릭 시 `mark-read` API가 자동 호출된다
- 미열람/열람완료 탭으로 필터링 가능
- 미열람 건수가 뱃지로 표시된다
---
## 3. 상세 화면 (`/approval-mgmt/{id}`)
### 3.1 전체 레이아웃
```
┌──────────────────────────────────────────────────────────┐
│ 결재 상세 [수정] [목록으로] │
│ APR-260228-001 │
├──────────────────────────────────────────────────────────┤
│ │
│ 상태: [🔵 진행] [🔴 긴급] │
│ 양식: 휴가신청서 기안자: 홍길동 │
│ 기안일: 2026-02-28 10:05 완료일: - │
│ 원본 문서: APR-260225-003 (재기안 시 표시) │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 회수 사유 (cancelled 상태에서만) │ │
│ │ 내용 수정이 필요하여 회수합니다. │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ 제목: 2월 연차 사용 신청 │
│ 본문: 2월 27일~28일 연차 사용합니다... │
│ │
├──────────────────────────────────────────────────────────┤
│ │
│ 결재 진행 │
│ ┌────────────────────────────────────────────────┐ │
│ │ [결재 단계 프로그레스 바] │ │
│ │ ✓김과장(승인) → ●박부장(대기) → ③이사(대기) │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ 결재 의견 │
│ ┌────────────────────────────────────────────────┐ │
│ │ ✓ 김과장 2026-02-28 11:00 │ │
│ │ 승인합니다. │ │
│ └────────────────────────────────────────────────┘ │
│ │
├──────────────────────────────────────────────────────────┤
│ │
│ 결재 처리 (현재 결재자에게만 표시) │
│ [결재 의견 textarea] │
│ [승인] [반려] [보류] [전결] │
│ │
├──────────────────────────────────────────────────────────┤
│ 보류 해제 (on_hold + 보류한 본인에게만) │
│ [보류 해제] │
├──────────────────────────────────────────────────────────┤
│ 회수 (기안자 + pending/on_hold) │
│ [회수 사유 textarea] │
│ [결재 회수] │
├──────────────────────────────────────────────────────────┤
│ 복사 재기안 (기안자 + approved/rejected/cancelled) │
│ [복사하여 재기안] │
└──────────────────────────────────────────────────────────┘
```
### 3.2 조건부 섹션 표시
| 섹션 | 표시 조건 |
|------|----------|
| **수정 버튼** | 기안자 + `draft`/`rejected` |
| **회수 사유** | `cancelled` + `recall_reason` 존재 |
| **원본 문서 링크** | `parent_doc_id` 존재 (재기안 문서) |
| **결재 처리** | `pending` + 현재 결재자 |
| **보류 해제** | `on_hold` + 보류한 본인 |
| **회수** | 기안자 + `pending`/`on_hold` |
| **복사 재기안** | 기안자 + `approved`/`rejected`/`cancelled` |
---
## 4. 파셜 컴포넌트
### 4.1 상태 뱃지 (`_status-badge.blade.php`)
문서 상태를 색상 뱃지로 표시한다.
| 상태 | 라벨 | 스타일 |
|------|------|--------|
| `draft` | 임시저장 | `bg-gray-100 text-gray-700` |
| `pending` | 진행 | `bg-blue-100 text-blue-700` |
| `approved` | 완료 | `bg-green-100 text-green-700` |
| `rejected` | 반려 | `bg-red-100 text-red-700` |
| `cancelled` | 회수 | `bg-yellow-100 text-yellow-700` |
| `on_hold` | 보류 | `bg-amber-100 text-amber-700` |
---
### 4.2 결재 단계 프로그레스 (`_step-progress.blade.php`)
결재선의 각 단계를 가로 프로그레스 바로 표시한다.
**단계 아이콘:**
| 상태 | 아이콘 | 배경색 | 텍스트색 |
|------|--------|--------|---------|
| `approved` (normal) | ✓ | `bg-green-500` | white |
| `approved` (pre_decided) | ⚡ | `bg-indigo-500` | white |
| `rejected` | ✗ | `bg-red-500` | white |
| `on_hold` | ⏸ | `bg-amber-400` | white |
| `skipped` | — | `bg-gray-300` | gray |
| `pending` (현재 차례) | 번호 | `bg-blue-500` | white |
| `pending` (대기) | 번호 | `bg-gray-200` | gray |
**레이아웃:**
```
┌──────────────────────────────────────────────────────┐
│ │
│ ✓ ──── ⚡ ──── — ──── — ──── ● ──── 3 │
│ 김과장 박부장 이사장 팀장 최대리 참조자 │
│ 경영팀 경영팀 대표실 개발팀 개발팀 인사팀 │
│ (승인) (전결) (건너뜀)(건너뜀)(대기) (참조) │
│ │
└──────────────────────────────────────────────────────┘
```
**특수 표시:**
- **전결** step: ⚡ 아이콘 + "전결" 라벨 (남색)
- **보류** step: ⏸ 아이콘 + "보류" 라벨 (노란색)
- **건너뜀** step: 이름에 취소선 (line-through)
- **참조** step: 별도 구분 없이 동일 프로그레스 바에 표시
- **연결선**: 단계 사이 가로선 (`border-t-2`)
---
## 5. 결재 처리 인터랙션
### 5.1 승인
```
[승인 버튼 클릭]
→ confirm("승인하시겠습니까?")
→ POST /api/admin/approvals/{id}/approve
body: { comment: "의견 텍스트" }
→ 성공 시: 토스트("승인되었습니다") + 페이지 리로드
```
### 5.2 반려
```
[반려 버튼 클릭]
→ comment 빈 값 체크 → 경고 토스트("반려 시 사유를 입력해주세요")
→ confirm("반려하시겠습니까?")
→ POST /api/admin/approvals/{id}/reject
body: { comment: "사유" }
→ 성공 시: 토스트("반려되었습니다") + 페이지 리로드
```
### 5.3 보류
```
[보류 버튼 클릭]
→ comment 빈 값 체크 → 경고 토스트("보류 사유를 입력해주세요")
→ confirm("이 결재를 보류하시겠습니까?")
→ POST /api/admin/approvals/{id}/hold
body: { comment: "사유" }
→ 성공 시: 토스트("보류되었습니다") + 페이지 리로드
```
### 5.4 전결
```
[전결 버튼 클릭]
→ confirm("전결 처리하시겠습니까?\n이후 모든 결재를 건너뛰고 문서를 최종 승인합니다.")
→ POST /api/admin/approvals/{id}/pre-decide
body: { comment: "의견(선택)" }
→ 성공 시: 토스트("전결 처리되었습니다") + 페이지 리로드
```
### 5.5 보류 해제
```
[보류 해제 버튼 클릭]
→ confirm("보류를 해제하시겠습니까?")
→ POST /api/admin/approvals/{id}/release-hold
→ 성공 시: 토스트("보류가 해제되었습니다") + 페이지 리로드
```
### 5.6 회수
```
[결재 회수 버튼 클릭]
→ confirm("결재를 회수하시겠습니까? 이 작업은 되돌릴 수 없습니다.")
→ POST /api/admin/approvals/{id}/cancel
body: { recall_reason: "사유(선택)" }
→ 성공 시: 토스트("결재가 회수되었습니다") + 페이지 리로드
```
### 5.7 복사 재기안
```
[복사하여 재기안 버튼 클릭]
→ confirm("이 문서를 복사하여 새 결재를 작성하시겠습니까?")
→ POST /api/admin/approvals/{id}/copy
→ 성공 시: 토스트("문서가 복사되었습니다")
→ /approval-mgmt/{newId}/edit로 이동
```
---
## 6. 결재 의견 표시
상세 페이지에서 결재 의견이 있는 step을 카드 형태로 표시한다.
```
┌──────────────────────────────────────┐
│ ✓ 김과장 2026-02-28 11:00 │
│ 승인합니다. │
├──────────────────────────────────────┤
│ ⚡ 박부장 (전결) 2026-02-28 14:00 │
│ 전결 처리합니다. │
├──────────────────────────────────────┤
│ ⏸ 이사장 (보류) 2026-02-28 15:00 │
│ 추가 자료 검토 필요 │
├──────────────────────────────────────┤
│ ✗ 팀장 2026-02-28 16:00 │
│ 예산 초과로 반려합니다. │
└──────────────────────────────────────┘
```
**아이콘 색상:**
- ✓ 승인: 녹색 (`bg-green-100 text-green-600`)
- ⚡ 전결: 남색 (`bg-indigo-100 text-indigo-600`)
- ⏸ 보류: 노란색 (`bg-amber-100 text-amber-600`)
- ✗ 반려: 적색 (`bg-red-100 text-red-600`)
---
## 7. 참조함 열람 추적 UI
### 7.1 탭 필터
```
[전체] [미열람 (5)] [열람완료]
```
- 탭 클릭 시 `is_read` 파라미터로 API 재호출
- 미열람 탭에 건수 뱃지 표시
### 7.2 열람 상태 표시
| 상태 | 표시 |
|------|------|
| 미열람 | `bg-red-100 text-red-700` "미열람" |
| 열람완료 | `bg-green-100 text-green-700` "열람완료" |
### 7.3 자동 열람 처리
문서 행 클릭 시:
1. `mark-read` API 호출 (비동기)
2. 상세 페이지로 이동
---
## 8. 버튼 스타일 가이드
| 버튼 | 색상 | Tailwind 클래스 |
|------|------|----------------|
| 승인 | 녹색 | `bg-green-600 hover:bg-green-700` |
| 반려 | 적색 | `bg-red-600 hover:bg-red-700` |
| 보류 | 노란색 | `bg-amber-500 hover:bg-amber-600` |
| 전결 | 남색 | `bg-indigo-600 hover:bg-indigo-700` |
| 보류 해제 | 노란색 | `bg-amber-500 hover:bg-amber-600` |
| 회수 | 노란색 | `bg-yellow-500 hover:bg-yellow-600` |
| 복사 재기안 | 회색 | `bg-gray-600 hover:bg-gray-700` |
| 수정 | 회색 | `bg-gray-600 hover:bg-gray-700` |
---
## 관련 문서
- [README.md](README.md) — 시스템 전체 개요
- [워크플로우 상세](workflows.md) — 각 동작의 상세 흐름
- [API 명세](api-reference.md) — 엔드포인트별 요청/응답
---
**최종 업데이트**: 2026-02-28

View File

@@ -0,0 +1,565 @@
# 결재관리 워크플로우 상세
> **작성일**: 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