381 lines
11 KiB
Markdown
381 lines
11 KiB
Markdown
# 결재 시스템 v2 사용 가이드
|
|
|
|
## 개요
|
|
|
|
결재 시스템 v2는 기존 순차결재(escalation) 외에 다양한 결재 방식을 지원합니다.
|
|
|
|
| 결재 유형 | 코드 | 설명 |
|
|
|-----------|------|------|
|
|
| 순차결재 (기본) | `escalation` | 결재선 순서대로 한 명씩 처리 |
|
|
| 전결 (자기결재) | `self` | 상신자 본인이 직접 승인 (결재자 불필요) |
|
|
| 합의결재 | `consensus` | 같은 단계에 여러 결재자 → 전원 승인 필요 |
|
|
| 후결 | `post` | 먼저 실행 후 나중에 결재 (결재 전 상태에서도 업무 진행) |
|
|
|
|
추가 기능:
|
|
- **대결 위임**: 부재 시 다른 사용자에게 결재 위임
|
|
- **통보 단계**: 결재선에 통보만 하는 단계 (자동 승인 처리)
|
|
- **긴급도**: `normal` / `urgent` / `critical`
|
|
- **혼합형 결재선**: 한 결재선에 결재/합의/통보 단계를 자유롭게 조합
|
|
|
|
---
|
|
|
|
## DB 스키마 변경사항
|
|
|
|
### 마이그레이션 적용
|
|
|
|
```bash
|
|
# 개발 DB에 마이그레이션 적용
|
|
psql -h 39.117.244.52 -p 11132 -U postgres -d plm -f db/migrations/1051_approval_system_v2.sql
|
|
psql -h 39.117.244.52 -p 11132 -U postgres -d plm -f db/migrations/1052_rename_proxy_id_to_id.sql
|
|
```
|
|
|
|
### 변경된 테이블
|
|
|
|
#### approval_requests (추가 컬럼)
|
|
|
|
| 컬럼 | 타입 | 기본값 | 설명 |
|
|
|------|------|--------|------|
|
|
| approval_type | VARCHAR(20) | 'escalation' | self/escalation/consensus/post |
|
|
| is_post_approved | BOOLEAN | FALSE | 후결 처리 완료 여부 |
|
|
| post_approved_at | TIMESTAMPTZ | NULL | 후결 처리 시각 |
|
|
| urgency | VARCHAR(20) | 'normal' | normal/urgent/critical |
|
|
|
|
#### approval_lines (추가 컬럼)
|
|
|
|
| 컬럼 | 타입 | 기본값 | 설명 |
|
|
|------|------|--------|------|
|
|
| step_type | VARCHAR(20) | 'approval' | approval/consensus/notification |
|
|
| proxy_for | VARCHAR(50) | NULL | 대결 시 원래 결재자 ID |
|
|
| proxy_reason | TEXT | NULL | 대결 사유 |
|
|
| is_required | BOOLEAN | TRUE | 필수 결재 여부 |
|
|
|
|
#### approval_proxy_settings (신규)
|
|
|
|
| 컬럼 | 타입 | 설명 |
|
|
|------|------|------|
|
|
| id | SERIAL PK | |
|
|
| company_code | VARCHAR(20) NOT NULL | |
|
|
| original_user_id | VARCHAR(50) | 원래 결재자 |
|
|
| proxy_user_id | VARCHAR(50) | 대결자 |
|
|
| start_date | DATE | 위임 시작일 |
|
|
| end_date | DATE | 위임 종료일 |
|
|
| reason | TEXT | 위임 사유 |
|
|
| is_active | CHAR(1) | 'Y'/'N' |
|
|
|
|
---
|
|
|
|
## API 엔드포인트
|
|
|
|
모든 API는 `/api/approval` 접두사 + JWT 인증 필수.
|
|
|
|
### 결재 요청 (Requests)
|
|
|
|
| Method | Endpoint | 설명 |
|
|
|--------|----------|------|
|
|
| GET | `/requests` | 목록 조회 |
|
|
| GET | `/requests/:id` | 상세 조회 (lines 포함) |
|
|
| POST | `/requests` | 결재 요청 생성 |
|
|
| POST | `/requests/:id/cancel` | 결재 회수 |
|
|
| POST | `/requests/:id/post-approve` | 후결 처리 |
|
|
|
|
#### 결재 요청 생성 Body
|
|
|
|
```typescript
|
|
{
|
|
title: string;
|
|
target_table: string;
|
|
target_record_id: string;
|
|
approval_type?: "self" | "escalation" | "consensus" | "post"; // 기본: escalation
|
|
urgency?: "normal" | "urgent" | "critical"; // 기본: normal
|
|
definition_id?: number;
|
|
target_record_data?: Record<string, any>;
|
|
approvers: Array<{
|
|
approver_id: string;
|
|
step_order: number;
|
|
step_type?: "approval" | "consensus" | "notification"; // 기본: approval
|
|
}>;
|
|
}
|
|
```
|
|
|
|
#### 결재 유형별 요청 예시
|
|
|
|
**전결 (self)**: 결재자 없이 본인 즉시 승인
|
|
|
|
```typescript
|
|
await createApprovalRequest({
|
|
title: "긴급 출장비 전결",
|
|
target_table: "expense",
|
|
target_record_id: "123",
|
|
approval_type: "self",
|
|
approvers: [],
|
|
});
|
|
```
|
|
|
|
**합의결재 (consensus)**: 같은 step_order에 여러 결재자
|
|
|
|
```typescript
|
|
await createApprovalRequest({
|
|
title: "프로젝트 예산안 합의",
|
|
target_table: "budget",
|
|
target_record_id: "456",
|
|
approval_type: "consensus",
|
|
approvers: [
|
|
{ approver_id: "user1", step_order: 1, step_type: "consensus" },
|
|
{ approver_id: "user2", step_order: 1, step_type: "consensus" },
|
|
{ approver_id: "user3", step_order: 1, step_type: "consensus" },
|
|
],
|
|
});
|
|
```
|
|
|
|
**혼합형 결재선**: 결재 → 합의 → 통보 조합
|
|
|
|
```typescript
|
|
await createApprovalRequest({
|
|
title: "신규 채용 승인",
|
|
target_table: "recruitment",
|
|
target_record_id: "789",
|
|
approval_type: "escalation",
|
|
approvers: [
|
|
{ approver_id: "teamLead", step_order: 1, step_type: "approval" },
|
|
{ approver_id: "hrManager", step_order: 2, step_type: "consensus" },
|
|
{ approver_id: "cfo", step_order: 2, step_type: "consensus" },
|
|
{ approver_id: "ceo", step_order: 3, step_type: "approval" },
|
|
{ approver_id: "secretary", step_order: 4, step_type: "notification" },
|
|
],
|
|
});
|
|
```
|
|
|
|
**후결 (post)**: 먼저 실행 후 나중에 결재
|
|
|
|
```typescript
|
|
await createApprovalRequest({
|
|
title: "긴급 자재 발주",
|
|
target_table: "purchase_order",
|
|
target_record_id: "101",
|
|
approval_type: "post",
|
|
urgency: "urgent",
|
|
approvers: [
|
|
{ approver_id: "manager", step_order: 1, step_type: "approval" },
|
|
],
|
|
});
|
|
```
|
|
|
|
### 결재 처리 (Lines)
|
|
|
|
| Method | Endpoint | 설명 |
|
|
|--------|----------|------|
|
|
| GET | `/my-pending` | 내 결재 대기 목록 |
|
|
| POST | `/lines/:lineId/process` | 승인/반려 처리 |
|
|
|
|
#### 승인/반려 Body
|
|
|
|
```typescript
|
|
{
|
|
action: "approved" | "rejected";
|
|
comment?: string;
|
|
proxy_reason?: string; // 대결 시 사유
|
|
}
|
|
```
|
|
|
|
대결 처리: 원래 결재자가 아닌 사용자가 처리하면 자동으로 대결 설정 확인 후 `proxy_for`, `proxy_reason` 기록.
|
|
|
|
### 대결 위임 설정 (Proxy Settings)
|
|
|
|
| Method | Endpoint | 설명 |
|
|
|--------|----------|------|
|
|
| GET | `/proxy-settings` | 위임 목록 |
|
|
| POST | `/proxy-settings` | 위임 생성 |
|
|
| PUT | `/proxy-settings/:id` | 위임 수정 |
|
|
| DELETE | `/proxy-settings/:id` | 위임 삭제 |
|
|
| GET | `/proxy-settings/check/:userId` | 활성 대결자 확인 |
|
|
|
|
#### 대결 생성 Body
|
|
|
|
```typescript
|
|
{
|
|
original_user_id: string;
|
|
proxy_user_id: string;
|
|
start_date: string; // "2026-03-10"
|
|
end_date: string; // "2026-03-20"
|
|
reason?: string;
|
|
is_active?: "Y" | "N";
|
|
}
|
|
```
|
|
|
|
### 템플릿 (Templates)
|
|
|
|
| Method | Endpoint | 설명 |
|
|
|--------|----------|------|
|
|
| GET | `/templates` | 템플릿 목록 |
|
|
| GET | `/templates/:id` | 템플릿 상세 (steps 포함) |
|
|
| POST | `/templates` | 템플릿 생성 |
|
|
| PUT | `/templates/:id` | 템플릿 수정 |
|
|
| DELETE | `/templates/:id` | 템플릿 삭제 |
|
|
|
|
---
|
|
|
|
## 프론트엔드 화면
|
|
|
|
### 1. 결재 요청 모달 (`ApprovalRequestModal`)
|
|
|
|
경로: `frontend/components/approval/ApprovalRequestModal.tsx`
|
|
|
|
- 결재 유형 선택: 상신결재 / 전결 / 합의결재 / 후결
|
|
- 템플릿 불러오기: 등록된 템플릿에서 결재선 자동 세팅
|
|
- 전결 시 결재자 섹션 숨김 + "본인이 직접 승인합니다" 안내
|
|
- 합의결재 시 결재자 레이블 "합의 결재자"로 변경
|
|
- 후결 시 안내 배너 표시
|
|
- 혼합형 step_type 뱃지 표시 (결재/합의/통보)
|
|
|
|
### 2. 결재함 (`/admin/approvalBox`)
|
|
|
|
경로: `frontend/app/(main)/admin/approvalBox/page.tsx`
|
|
|
|
탭 구성:
|
|
- **수신함**: 내가 결재할 건 목록
|
|
- **상신함**: 내가 요청한 건 목록
|
|
- **대결 설정**: 대결 위임 CRUD
|
|
|
|
대결 설정 기능:
|
|
- 위임자/대결자 사용자 검색 (디바운스 300ms)
|
|
- 시작일/종료일 설정
|
|
- 활성/비활성 토글
|
|
- 기간 중복 체크 (서버 측)
|
|
- 등록/수정/삭제 모달
|
|
|
|
### 3. 결재 템플릿 관리 (`/admin/approvalTemplate`)
|
|
|
|
경로: `frontend/app/(main)/admin/approvalTemplate/page.tsx`
|
|
|
|
- 템플릿 목록/검색
|
|
- 등록/수정 Dialog
|
|
- 단계별 결재 유형 설정 (결재/합의/통보)
|
|
- 합의 단계: "합의자 추가" 버튼으로 같은 step_order에 복수 결재자
|
|
- 결재자 사용자 검색
|
|
|
|
### 4. 결재 단계 컴포넌트 (`v2-approval-step`)
|
|
|
|
경로: `frontend/lib/registry/components/v2-approval-step/`
|
|
|
|
화면 디자이너에서 사용하는 결재 단계 시각화 컴포넌트:
|
|
- 가로형/세로형 스테퍼
|
|
- step_order 기준 그룹핑 (합의결재 시 가로 나열)
|
|
- step_type 아이콘: 결재(CheckCircle), 합의(Users), 통보(Bell)
|
|
- 상태별 색상: 승인(success), 반려(destructive), 대기(warning)
|
|
- 대결/후결/전결 뱃지
|
|
- 긴급도 표시 (urgent: 주황 dot, critical: 빨강 배경)
|
|
|
|
---
|
|
|
|
## API 클라이언트 사용법
|
|
|
|
```typescript
|
|
import {
|
|
// 결재 요청
|
|
createApprovalRequest,
|
|
getApprovalRequests,
|
|
getApprovalRequest,
|
|
cancelApprovalRequest,
|
|
postApproveRequest,
|
|
|
|
// 대결 위임
|
|
getProxySettings,
|
|
createProxySetting,
|
|
updateProxySetting,
|
|
deleteProxySetting,
|
|
checkActiveProxy,
|
|
|
|
// 템플릿 단계
|
|
getTemplateSteps,
|
|
createTemplateStep,
|
|
updateTemplateStep,
|
|
deleteTemplateStep,
|
|
|
|
// 타입
|
|
type ApprovalProxySetting,
|
|
type CreateApprovalRequestInput,
|
|
type ApprovalLineTemplateStep,
|
|
} from "@/lib/api/approval";
|
|
```
|
|
|
|
---
|
|
|
|
## 핵심 로직 설명
|
|
|
|
### 동시성 보호 (FOR UPDATE)
|
|
|
|
결재 처리(`processApproval`)에서 동시 승인/반려 방지:
|
|
|
|
```sql
|
|
SELECT * FROM approval_lines WHERE line_id = $1 FOR UPDATE
|
|
SELECT * FROM approval_requests WHERE request_id = $1 FOR UPDATE
|
|
```
|
|
|
|
### 대결 자동 감지
|
|
|
|
결재자가 아닌 사용자가 결재 처리하면:
|
|
1. `approval_proxy_settings`에서 활성 대결 설정 확인
|
|
2. 대결 설정이 있으면 → `proxy_for`, `proxy_reason` 자동 기록
|
|
3. 없으면 → 403 에러
|
|
|
|
### 통보 단계 자동 처리
|
|
|
|
`step_type = 'notification'`인 단계가 활성화되면:
|
|
1. 해당 단계의 모든 결재자를 자동 `approved` 처리
|
|
2. `comment = '자동 통보 처리'` 기록
|
|
3. `activateNextStep()` 재귀 호출로 다음 단계 진행
|
|
|
|
### 합의결재 단계 완료 판정
|
|
|
|
같은 `step_order`의 모든 결재자가 `approved`여야 다음 단계로:
|
|
|
|
```sql
|
|
SELECT COUNT(*) FROM approval_lines
|
|
WHERE request_id = $1 AND step_order = $2
|
|
AND status NOT IN ('approved', 'skipped')
|
|
```
|
|
|
|
하나라도 `rejected`면 전체 결재 반려.
|
|
|
|
---
|
|
|
|
## 메뉴 등록
|
|
|
|
결재 관련 화면을 메뉴에 등록하려면:
|
|
|
|
| 화면 | URL | 메뉴명 예시 |
|
|
|------|-----|-------------|
|
|
| 결재함 | `/admin/approvalBox` | 결재함 |
|
|
| 결재 템플릿 관리 | `/admin/approvalTemplate` | 결재 템플릿 |
|
|
| 결재 유형 관리 | `/admin/approvalMng` | 결재 유형 (기존) |
|
|
|
|
---
|
|
|
|
## 파일 구조
|
|
|
|
```
|
|
backend-node/src/
|
|
├── controllers/
|
|
│ ├── approvalController.ts # 결재 유형/템플릿/요청/라인 처리
|
|
│ └── approvalProxyController.ts # 대결 위임 CRUD
|
|
└── routes/
|
|
└── approvalRoutes.ts # 라우트 등록
|
|
|
|
frontend/
|
|
├── app/(main)/admin/
|
|
│ ├── approvalBox/page.tsx # 결재함 (수신/상신/대결)
|
|
│ ├── approvalTemplate/page.tsx # 템플릿 관리
|
|
│ └── approvalMng/page.tsx # 결재 유형 관리 (기존)
|
|
├── components/approval/
|
|
│ └── ApprovalRequestModal.tsx # 결재 요청 모달
|
|
└── lib/
|
|
├── api/approval.ts # API 클라이언트
|
|
└── registry/components/v2-approval-step/
|
|
├── ApprovalStepComponent.tsx # 결재 단계 시각화
|
|
└── types.ts # 확장 타입
|
|
|
|
db/migrations/
|
|
├── 1051_approval_system_v2.sql # v2 스키마 확장
|
|
└── 1052_rename_proxy_id_to_id.sql # PK 컬럼명 통일
|
|
```
|