# 결재 시스템 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; 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 컬럼명 통일 ```