Files
sam-docs/frontend/api-specs/payroll-api.md
김보곤 e83954eddd docs: [payroll] 급여관리 API 프론트엔드 연동 명세서 추가
- 18개 엔드포인트 전체 명세 (CRUD, 상태변경, 일괄생성, 미리보기 등)
- 4대보험/근로소득세 계산 엔진 설명
- 상태 흐름도 (draft → confirmed → paid)
- 프론트엔드 구현 가이드 및 UI 와이어프레임
- INDEX.md에 문서 등록
2026-03-11 19:30:00 +09:00

1118 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 급여관리 API 명세
> **작성일**: 2026-03-11
> **상태**: 개발 완료 (API 배포됨)
> **Base URL**: `api.codebridge-x.com` (운영) / `api.dev.codebridge-x.com` (개발)
---
## 1. 개요
월별 급여를 등록/확정/지급하고, 4대보험과 근로소득세를 자동 계산하는 급여관리 API이다.
### 1.1 인증
모든 요청에 다음 헤더가 필요하다:
```
X-API-KEY: {api_key}
Authorization: Bearer {token}
```
### 1.2 공통 응답 형식
```json
{
"success": true,
"message": "조회 성공",
"data": { ... }
}
```
에러 시:
```json
{
"success": false,
"message": "에러 메시지"
}
```
### 1.3 엔드포인트 요약
| # | Method | Path | 설명 |
|---|--------|------|------|
| 1 | GET | `/api/v1/payrolls` | 급여 목록 (페이지네이션) |
| 2 | POST | `/api/v1/payrolls` | 급여 등록 |
| 3 | GET | `/api/v1/payrolls/summary` | 월간 요약 통계 |
| 4 | POST | `/api/v1/payrolls/calculate` | 급여 일괄 계산 (draft 재계산) |
| 5 | POST | `/api/v1/payrolls/calculate-preview` | 계산 미리보기 (저장 안 함) |
| 6 | POST | `/api/v1/payrolls/bulk-confirm` | 일괄 확정 |
| 7 | POST | `/api/v1/payrolls/bulk-generate` | 재직사원 일괄 생성 |
| 8 | POST | `/api/v1/payrolls/copy-from-previous` | 전월 급여 복사 |
| 9 | GET | `/api/v1/payrolls/{id}` | 급여 상세 |
| 10 | PUT | `/api/v1/payrolls/{id}` | 급여 수정 |
| 11 | DELETE | `/api/v1/payrolls/{id}` | 급여 삭제 |
| 12 | POST | `/api/v1/payrolls/{id}/confirm` | 확정 |
| 13 | POST | `/api/v1/payrolls/{id}/unconfirm` | 확정 취소 |
| 14 | POST | `/api/v1/payrolls/{id}/pay` | 지급 처리 |
| 15 | POST | `/api/v1/payrolls/{id}/unpay` | 지급 취소 (슈퍼관리자) |
| 16 | GET | `/api/v1/payrolls/{id}/payslip` | 급여명세서 조회 |
| 17 | GET | `/api/v1/payrolls/settings` | 급여 설정 조회 |
| 18 | PUT | `/api/v1/payrolls/settings` | 급여 설정 수정 |
---
## 2. 상태 흐름도
```
┌────────────────┐
│ draft (작성중) │
└───┬──────┬─────┘
│ ▲
│ confirm │ unconfirm
▼ │
┌────────────────┐
│confirmed (확정) │
└───┬──────┬─────┘
│ ▲
│ pay │ unpay*
▼ │
┌────────────────┐
│ paid (지급완료)│
└────────────────┘
* unpay는 슈퍼관리자 전용 (paid → draft 초기화)
```
### 2.1 상태값
| 상태 | 값 | 설명 | UI 배지 색상 |
|------|---|------|-------------|
| 작성중 | `draft` | 수정/삭제/확정 가능 | gray |
| 확정 | `confirmed` | 확정취소/지급 가능 | blue |
| 지급완료 | `paid` | 상세보기만 가능 | green |
### 2.2 상태별 가능한 작업
| 상태 | 일반 사용자 | 슈퍼관리자 |
|------|-----------|-----------|
| `draft` | 수정, 삭제, 확정, 일괄계산 | 동일 |
| `confirmed` | 확정취소, 지급처리 | + **수정** |
| `paid` | 상세보기만 | + **수정**, **지급취소** |
---
## 3. 급여 계산 엔진
### 3.1 과세표준
```
과세표준 = 총지급액(gross_salary) - 식대(bonus)
```
- `bonus` 필드는 **식대(비과세)** 항목이다
- 4대보험과 세금은 과세표준 기준으로 계산한다
### 3.2 4대보험 자동 계산
| 항목 | 계산식 | 기본 요율 | 비고 |
|------|--------|----------|------|
| 건강보험 | `과세표준 × 3.545%` | 3.545% | 10원 단위 절삭 |
| 장기요양보험 | `건강보험료 × 0.9082%` | 0.9082% | 건강보험료 기준, 10원 단위 절삭 |
| 국민연금 | `과세표준 × 4.5%` | 4.5% | 상한 590만 / 하한 37만 적용 |
| 고용보험 | `과세표준 × 0.9%` | 0.9% | 10원 단위 절삭 |
> **모든 보험료는 10원 단위 절삭** (floor to nearest 10)
### 3.3 근로소득세
- **770천원 미만**: 0원
- **770~10,000천원**: 간이세액표(DB) 조회 (연도, 월급여 천원단위, 가족수)
- **10,000천원 초과**: 공식 계산 (소득세법 시행령 별표2)
### 3.4 지방소득세
```
지방소득세 = 근로소득세 × 10% (10원 단위 절삭)
```
### 3.5 가족수 (공제대상가족수)
- 기본: 본인 1인
- TenantUserProfile의 `json_extra.dependents` 배열에서 `is_dependent: true`인 수만큼 추가
- 범위: 최소 1, 최대 11
### 3.6 요율 설정
요율은 `payroll_settings` 테이블에서 테넌트별로 관리한다. 기본값은 위 표와 동일하며, 설정 API(17, 18번)를 통해 변경 가능하다.
---
## 4. 데이터 모델
### 4.1 Payroll 객체
```json
{
"id": 1,
"tenant_id": 1,
"user_id": 5,
"pay_year": 2026,
"pay_month": 3,
"base_salary": "3500000",
"overtime_pay": "0",
"bonus": "200000",
"allowances": [
{"name": "직책수당", "amount": 300000},
{"name": "교통비", "amount": 100000}
],
"gross_salary": "4100000",
"income_tax": "117750",
"resident_tax": "11770",
"health_insurance": "138220",
"long_term_care": "1250",
"pension": "175500",
"employment_insurance": "35100",
"deductions": [
{"name": "대출상환", "amount": 500000}
],
"options": null,
"total_deductions": "979590",
"net_salary": "3120410",
"status": "draft",
"confirmed_at": null,
"confirmed_by": null,
"paid_at": null,
"withdrawal_id": null,
"note": null,
"created_by": 1,
"updated_by": 1,
"created_at": "2026-03-11T10:00:00.000000Z",
"updated_at": "2026-03-11T10:00:00.000000Z",
"user": {
"id": 5,
"name": "홍길동",
"email": "hong@example.com"
},
"creator": {
"id": 1,
"name": "관리자"
}
}
```
### 4.2 필드 설명
**지급 항목:**
| 필드 | 타입 | 설명 |
|------|------|------|
| `base_salary` | decimal(0) | 기본급 |
| `overtime_pay` | decimal(0) | 고정연장근로수당 |
| `bonus` | decimal(0) | 식대 (비과세) |
| `allowances` | json | 추가 수당 `[{name, amount}]` |
| `gross_salary` | decimal(0) | 총지급액 (자동 계산) |
**법정 공제 항목 (자동 계산):**
| 필드 | 타입 | 설명 |
|------|------|------|
| `income_tax` | decimal(0) | 근로소득세 |
| `resident_tax` | decimal(0) | 지방소득세 |
| `health_insurance` | decimal(0) | 건강보험료 |
| `long_term_care` | decimal(0) | 장기요양보험료 |
| `pension` | decimal(0) | 국민연금 |
| `employment_insurance` | decimal(0) | 고용보험료 |
**기타 공제:**
| 필드 | 타입 | 설명 |
|------|------|------|
| `deductions` | json | 기타공제 `[{name, amount}]` |
**결과:**
| 필드 | 타입 | 설명 |
|------|------|------|
| `total_deductions` | decimal(0) | 총공제액 (법정 + 기타) |
| `net_salary` | decimal(0) | 실수령액 = gross - total_deductions |
**상태:**
| 필드 | 타입 | 설명 |
|------|------|------|
| `status` | string | `draft` / `confirmed` / `paid` |
| `confirmed_at` | datetime | 확정 일시 |
| `confirmed_by` | int | 확정자 user_id |
| `paid_at` | datetime | 지급 일시 |
| `withdrawal_id` | int | 연결된 출금 ID |
---
## 5. API 상세
### 5.1 급여 목록 조회
```
GET /api/v1/payrolls
```
**Query Parameters:**
| 파라미터 | 타입 | 필수 | 설명 | 예시 |
|---------|------|------|------|------|
| `year` | int | - | 귀속연도 | `2026` |
| `month` | int | - | 귀속월 | `3` |
| `user_id` | int | - | 사원 ID | `5` |
| `status` | string | - | 상태 필터 | `draft` |
| `department_id` | int | - | 부서 필터 | `2` |
| `search` | string | - | 사원명 검색 | `홍길동` |
| `sort_by` | string | - | 정렬 기준 (기본: `pay_year`) | `net_salary` |
| `sort_dir` | string | - | 정렬 방향 (기본: `desc`) | `asc` |
| `per_page` | int | - | 페이지당 건수 (기본: 20) | `50` |
| `page` | int | - | 페이지 번호 | `1` |
**응답 (200):**
```json
{
"success": true,
"message": "조회 성공",
"data": {
"current_page": 1,
"data": [
{
"id": 1,
"user_id": 5,
"pay_year": 2026,
"pay_month": 3,
"base_salary": "3500000",
"gross_salary": "4100000",
"total_deductions": "979590",
"net_salary": "3120410",
"status": "draft",
"user": {"id": 5, "name": "홍길동", "email": "hong@example.com"},
"creator": {"id": 1, "name": "관리자"}
}
],
"per_page": 20,
"total": 15,
"last_page": 1
}
}
```
**프론트엔드 참고:**
- 연월을 선택하는 UI를 제공하면 `year` + `month` 필터 사용
- `sort_by=period` 전달 시 `pay_year` + `pay_month` 복합 정렬
---
### 5.2 급여 등록
```
POST /api/v1/payrolls
```
**Request Body:**
```json
{
"user_id": 5,
"pay_year": 2026,
"pay_month": 3,
"base_salary": 3500000,
"overtime_pay": 0,
"bonus": 200000,
"allowances": [
{"name": "직책수당", "amount": 300000}
],
"deductions": [
{"name": "대출상환", "amount": 500000}
],
"family_count": 3,
"deduction_overrides": {
"income_tax": 100000,
"pension": 170000
},
"note": "메모"
}
```
**필드 규칙:**
| 필드 | 타입 | 필수 | 설명 |
|------|------|------|------|
| `user_id` | int | ✅ | 급여 대상 사원 ID |
| `pay_year` | int | ✅ | 귀속연도 (2000~2100) |
| `pay_month` | int | ✅ | 귀속월 (1~12) |
| `base_salary` | numeric | ✅ | 기본급 (0 이상) |
| `overtime_pay` | numeric | - | 고정연장근로수당 |
| `bonus` | numeric | - | 식대 (비과세) |
| `allowances` | array | - | 추가수당 `[{name, amount}]` |
| `deductions` | array | - | 기타공제 `[{name, amount}]` |
| `family_count` | int | - | 공제대상가족수 (1~11, 미전달 시 자동) |
| `deduction_overrides` | object | - | 법정공제 수동 입력 (아래 참고) |
| `note` | string | - | 메모 (최대 1000자) |
**`deduction_overrides` 필드:**
자동 계산된 법정 공제 항목을 수동으로 덮어쓸 때 사용한다. 지정한 항목만 덮어쓰고, 나머지는 자동 계산값을 유지한다.
```json
{
"income_tax": 100000,
"resident_tax": 10000,
"health_insurance": 140000,
"long_term_care": 1300,
"pension": 170000,
"employment_insurance": 35000
}
```
**응답 (201):**
```json
{
"success": true,
"message": "등록 성공",
"data": { /* Payroll 객체 (4.1 참고) */ }
}
```
**에러:**
| 상황 | 응답 코드 | 메시지 |
|------|----------|--------|
| 동일 연월+사원 중복 | 400 | 해당 연월에 이미 급여가 등록되어 있습니다. |
| 검증 실패 | 422 | 요청 데이터 검증에 실패했습니다. |
---
### 5.3 월간 요약 통계
```
GET /api/v1/payrolls/summary?year=2026&month=3
```
**Query Parameters:**
| 파라미터 | 타입 | 필수 | 기본값 |
|---------|------|------|--------|
| `year` | int | - | 현재 연도 |
| `month` | int | - | 현재 월 |
**응답 (200):**
```json
{
"success": true,
"message": "조회 성공",
"data": {
"year": 2026,
"month": 3,
"total_count": 15,
"draft_count": 3,
"confirmed_count": 7,
"paid_count": 5,
"total_gross": 62500000,
"total_deductions": 15800000,
"total_net": 46700000
}
}
```
**프론트엔드 참고:**
- 대시보드 카드에 `total_count`, `total_gross`, `total_net` 표시
- 상태별 카운트로 진행 상태 게이지 표현 가능
---
### 5.4 급여 일괄 계산
기존 `draft` 상태 급여의 공제 항목을 재계산한다.
```
POST /api/v1/payrolls/calculate
```
**Request Body:**
```json
{
"year": 2026,
"month": 3,
"user_ids": [5, 8, 12]
}
```
| 필드 | 타입 | 필수 | 설명 |
|------|------|------|------|
| `year` | int | ✅ | 대상 연도 |
| `month` | int | ✅ | 대상 월 |
| `user_ids` | array | - | 특정 사원만 지정 (미전달 시 전체) |
**응답 (200):**
```json
{
"success": true,
"message": "급여가 일괄 계산되었습니다.",
"data": [
{ /* 재계산된 Payroll 객체들 */ }
]
}
```
> **주의:** `confirmed`/`paid` 상태 급여는 재계산하지 않는다 (draft만 대상).
---
### 5.5 계산 미리보기
급여 데이터를 저장하지 않고 계산 결과만 미리 확인한다. 급여 등록/수정 폼에서 실시간 미리보기용.
```
POST /api/v1/payrolls/calculate-preview
```
**Request Body:**
```json
{
"user_id": 5,
"base_salary": 3500000,
"overtime_pay": 0,
"bonus": 200000,
"allowances": [
{"name": "직책수당", "amount": 300000}
],
"deductions": [
{"name": "대출상환", "amount": 500000}
]
}
```
**응답 (200):**
```json
{
"success": true,
"message": "계산 완료",
"data": {
"gross_salary": 4100000,
"taxable_base": 3900000,
"income_tax": 117750,
"resident_tax": 11770,
"health_insurance": 138220,
"long_term_care": 1250,
"pension": 175500,
"employment_insurance": 35100,
"total_deductions": 979590,
"net_salary": 3120410,
"family_count": 3
}
}
```
**프론트엔드 참고:**
- `user_id`를 전달하면 해당 사원의 가족수를 자동 반영
- `user_id` 미전달 시 가족수 1로 계산
- 폼 입력값이 변경될 때마다 호출하여 실시간 계산 결과를 표시 (debounce 300ms 권장)
---
### 5.6 일괄 확정
해당 월의 모든 `draft` 급여를 한 번에 `confirmed`로 변경한다.
```
POST /api/v1/payrolls/bulk-confirm
```
**Request Body:**
```json
{
"year": 2026,
"month": 3
}
```
**응답 (200):**
```json
{
"success": true,
"message": "급여가 일괄 확정되었습니다.",
"data": {
"count": 12
}
}
```
---
### 5.7 재직사원 일괄 생성
해당 연월에 재직 중인(employee_status=active) 모든 사원의 급여를 자동 생성한다.
```
POST /api/v1/payrolls/bulk-generate
```
**Request Body:**
```json
{
"year": 2026,
"month": 3
}
```
**응답 (200):**
```json
{
"success": true,
"message": "급여가 일괄 생성되었습니다.",
"data": {
"created": 12,
"skipped": 3
}
}
```
**동작 설명:**
- 사원별 연봉 정보(`json_extra.salary_info.annual_salary`)에서 `÷12`로 월 기본급 산출
- 이미 존재하는 사원은 `skipped` 처리 (중복 생성하지 않음)
- 연봉 정보가 없는 사원도 기본급 0으로 생성됨
- 모든 급여는 `draft` 상태로 생성됨
**프론트엔드 참고:**
- 매월 초에 "일괄 생성" 버튼을 눌러 해당 월 급여를 초기화
- `created`/`skipped` 결과를 토스트 메시지로 표시
---
### 5.8 전월 급여 복사
전월 급여 데이터를 현재 월로 복사한다. 매월 동일한 급여 구조를 유지할 때 사용.
```
POST /api/v1/payrolls/copy-from-previous
```
**Request Body:**
```json
{
"year": 2026,
"month": 3
}
```
**응답 (200):**
```json
{
"success": true,
"message": "전월 급여가 복사되었습니다.",
"data": {
"created": 15,
"skipped": 0
}
}
```
**동작 설명:**
- 지급/공제 항목을 전월 그대로 복사 (`base_salary`, `allowances`, `deductions` 등)
- 상태는 `draft`로 초기화
- 이미 존재하는 사원은 `skipped` 처리
- 1월 요청 시 전년 12월 데이터 참조
**에러:**
| 상황 | 응답 코드 | 메시지 |
|------|----------|--------|
| 전월 데이터 없음 | 400 | 전월 급여 데이터가 없습니다. |
---
### 5.9 급여 상세 조회
```
GET /api/v1/payrolls/{id}
```
**응답 (200):**
```json
{
"success": true,
"message": "조회 성공",
"data": {
"id": 1,
"user": {"id": 5, "name": "홍길동", "email": "hong@example.com"},
"confirmer": {"id": 1, "name": "관리자"},
"withdrawal": null,
"creator": {"id": 1, "name": "관리자"},
/* ... 전체 Payroll 필드 */
}
}
```
---
### 5.10 급여 수정
```
PUT /api/v1/payrolls/{id}
```
**Request Body:**
```json
{
"base_salary": 3600000,
"overtime_pay": 100000,
"bonus": 200000,
"allowances": [
{"name": "직책수당", "amount": 300000}
],
"deductions": [
{"name": "대출상환", "amount": 500000}
],
"deduction_overrides": {
"pension": 175000
},
"_is_super_admin": false,
"note": "기본급 인상 반영"
}
```
**필드 규칙:**
- `_is_super_admin: true` 전달 시 `confirmed`/`paid` 상태에서도 수정 가능
- `deduction_overrides`로 법정 공제 항목을 수동 변경 가능
- 전달하지 않은 필드는 기존값 유지
**에러:**
| 상황 | 응답 코드 | 메시지 |
|------|----------|--------|
| draft 아닌 상태에서 수정 | 400 | 작성중 상태의 급여만 수정할 수 있습니다. |
| 연월/사원 변경 시 중복 | 400 | 해당 연월에 이미 급여가 등록되어 있습니다. |
---
### 5.11 급여 삭제
```
DELETE /api/v1/payrolls/{id}
```
**응답 (200):**
```json
{
"success": true,
"message": "삭제 성공",
"data": null
}
```
> `draft` 상태에서만 삭제 가능. Soft delete 처리.
---
### 5.12 확정
```
POST /api/v1/payrolls/{id}/confirm
```
**Request Body:** 없음
**응답 (200):**
```json
{
"success": true,
"message": "급여가 확정되었습니다.",
"data": {
"id": 1,
"status": "confirmed",
"confirmed_at": "2026-03-11T14:30:00.000000Z",
"confirmer": {"id": 1, "name": "관리자"}
}
}
```
---
### 5.13 확정 취소
```
POST /api/v1/payrolls/{id}/unconfirm
```
**Request Body:** 없음
**응답 (200):**
```json
{
"success": true,
"message": "급여 확정이 취소되었습니다.",
"data": {
"id": 1,
"status": "draft",
"confirmed_at": null,
"confirmed_by": null
}
}
```
> `confirmed` 상태에서만 가능. `draft`로 되돌린다.
---
### 5.14 지급 처리
```
POST /api/v1/payrolls/{id}/pay
```
**Request Body:**
```json
{
"withdrawal_id": 42
}
```
| 필드 | 타입 | 필수 | 설명 |
|------|------|------|------|
| `withdrawal_id` | int | - | 연결할 출금 내역 ID |
**응답 (200):**
```json
{
"success": true,
"message": "급여가 지급 처리되었습니다.",
"data": {
"id": 1,
"status": "paid",
"paid_at": "2026-03-25T09:00:00.000000Z",
"withdrawal": { "id": 42, "..." : "..." }
}
}
```
---
### 5.15 지급 취소
```
POST /api/v1/payrolls/{id}/unpay
```
**Request Body:** 없음
> **슈퍼관리자 전용.** `paid` → `draft`로 초기화. 확정/지급 이력 모두 제거.
**응답 (200):**
```json
{
"success": true,
"message": "급여 지급이 취소되었습니다.",
"data": {
"id": 1,
"status": "draft",
"confirmed_at": null,
"paid_at": null,
"withdrawal_id": null
}
}
```
---
### 5.16 급여명세서 조회
```
GET /api/v1/payrolls/{id}/payslip
```
**응답 (200):**
```json
{
"success": true,
"message": "조회 성공",
"data": {
"payroll": { /* 전체 Payroll 객체 */ },
"period": "2026년 03월",
"employee": {
"id": 5,
"name": "홍길동",
"email": "hong@example.com"
},
"earnings": {
"base_salary": 3500000,
"overtime_pay": 0,
"bonus": 200000,
"allowances": [
{"name": "직책수당", "amount": 300000},
{"name": "교통비", "amount": 100000}
],
"allowances_total": 400000,
"gross_total": 4100000
},
"deductions": {
"income_tax": 117750,
"resident_tax": 11770,
"health_insurance": 138220,
"long_term_care": 1250,
"pension": 175500,
"employment_insurance": 35100,
"other_deductions": [
{"name": "대출상환", "amount": 500000}
],
"other_total": 500000,
"total": 979590
},
"net_salary": 3120410,
"status": "confirmed",
"status_label": "확정",
"paid_at": null
}
}
```
**프론트엔드 참고:**
- `earnings` 구조로 지급 항목 테이블 구성
- `deductions` 구조로 공제 항목 테이블 구성
- `period`, `employee`, `net_salary`로 명세서 헤더 구성
- 인쇄용 레이아웃은 A4 세로 기준 권장
---
### 5.17 급여 설정 조회
```
GET /api/v1/payrolls/settings
```
**응답 (200):**
```json
{
"success": true,
"message": "조회 성공",
"data": {
"id": 1,
"tenant_id": 1,
"income_tax_rate": "0.00",
"resident_tax_rate": "10.00",
"health_insurance_rate": "3.545",
"long_term_care_rate": "0.9082",
"pension_rate": "4.500",
"employment_insurance_rate": "0.900",
"pension_max_salary": "5900000.00",
"pension_min_salary": "370000.00",
"pay_day": 25,
"auto_calculate": false,
"allowance_types": [
{"code": "meal", "name": "식대", "is_taxable": false},
{"code": "transport", "name": "교통비", "is_taxable": false},
{"code": "position", "name": "직책수당", "is_taxable": true},
{"code": "skill", "name": "기술수당", "is_taxable": true},
{"code": "family", "name": "가족수당", "is_taxable": true},
{"code": "housing", "name": "주거수당", "is_taxable": true}
],
"deduction_types": [
{"code": "loan", "name": "대출상환"},
{"code": "union", "name": "조합비"},
{"code": "savings", "name": "저축"},
{"code": "etc", "name": "기타공제"}
]
}
}
```
**프론트엔드 참고:**
- `allowance_types`로 수당 입력 폼의 드롭다운 구성
- `deduction_types`로 공제 입력 폼의 드롭다운 구성
- `pay_day`를 급여 지급일 표시에 활용
---
### 5.18 급여 설정 수정
```
PUT /api/v1/payrolls/settings
```
**Request Body:**
```json
{
"health_insurance_rate": 3.545,
"pension_rate": 4.5,
"pay_day": 25,
"allowance_types": [
{"code": "meal", "name": "식대", "is_taxable": false},
{"code": "transport", "name": "교통비", "is_taxable": false}
]
}
```
> 전달한 필드만 업데이트. 미전달 필드는 기존값 유지.
---
## 6. 에러 코드 정리
| 에러 키 | 메시지 | 발생 상황 |
|---------|--------|----------|
| `error.payroll.not_found` | 급여 정보를 찾을 수 없습니다. | 존재하지 않는 ID |
| `error.payroll.already_exists` | 해당 연월에 이미 급여가 등록되어 있습니다. | 동일 사원+연월 중복 |
| `error.payroll.not_editable` | 작성중 상태의 급여만 수정할 수 있습니다. | draft 외 수정 시도 |
| `error.payroll.not_deletable` | 작성중 상태의 급여만 삭제할 수 있습니다. | draft 외 삭제 시도 |
| `error.payroll.not_confirmable` | 작성중 상태의 급여만 확정할 수 있습니다. | draft 외 확정 시도 |
| `error.payroll.not_unconfirmable` | 확정된 급여만 확정 취소할 수 있습니다. | confirmed 외 확정취소 |
| `error.payroll.not_payable` | 확정된 급여만 지급 처리할 수 있습니다. | confirmed 외 지급 |
| `error.payroll.not_unpayable` | 지급완료된 급여만 지급 취소할 수 있습니다. | paid 외 지급취소 |
| `error.payroll.no_previous_month` | 전월 급여 데이터가 없습니다. | 전월 복사 시 데이터 없음 |
| `error.payroll.invalid_withdrawal` | 유효하지 않은 출금 내역입니다. | 존재하지 않는 withdrawal_id |
---
## 7. 프론트엔드 구현 가이드
### 7.1 추천 화면 구성
```
┌─────────────────────────────────────────────────────┐
│ 급여관리 [2026년 03월 ▼] │
├─────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐│
│ │ 총 인원 │ │ 총 지급액 │ │ 총 공제액 │ │ 실수령액 ││
│ │ 15명 │ │ 62,500천원│ │ 15,800천원│ │46,700천원││
│ └──────────┘ └──────────┘ └──────────┘ └─────────┘│
│ │
│ [일괄생성] [전월복사] [일괄계산] [일괄확정] [+ 등록] │
│ │
│ ┌───┬──────┬──────┬──────┬──────┬──────┬────┬────┐│
│ │ # │ 사원 │ 기본급│총지급액│총공제액│실수령액│상태 │ 작업││
│ ├───┼──────┼──────┼──────┼──────┼──────┼────┼────┤│
│ │ 1 │홍길동│ 350만│ 410만│ 98만 │ 312만│작성│ ⋮ ││
│ │ 2 │김철수│ 300만│ 350만│ 85만 │ 265만│확정│ ⋮ ││
│ └───┴──────┴──────┴──────┴──────┴──────┴────┴────┘│
└─────────────────────────────────────────────────────┘
```
### 7.2 급여 등록/수정 폼
```
┌─────────────────────────────────────────────────────┐
│ 급여 등록 │
├─────────────────────────────────────────────────────┤
│ 사원: [홍길동 ▼] 연도: [2026] 월: [3 ▼] │
│ │
│ ── 지급 항목 ──────────────────────────────────── │
│ 기본급: [ 3,500,000 ] │
│ 연장근로수당: [ 0 ] │
│ 식대(비과세): [ 200,000 ] │
│ 수당: │
│ 직책수당 [ 300,000 ] [삭제] │
│ 교통비 [ 100,000 ] [삭제] │
│ [+ 수당 추가] │
│ ────────────────────────── 총 지급액: 4,100,000 │
│ │
│ ── 공제 항목 (자동 계산) ──────────────────────── │
│ 근로소득세: [ 117,750 ] ← 자동 (수정 가능) │
│ 지방소득세: [ 11,770 ] ← 자동 (수정 가능) │
│ 건강보험: [ 138,220 ] ← 자동 (수정 가능) │
│ 장기요양보험: [ 1,250 ] ← 자동 (수정 가능) │
│ 국민연금: [ 175,500 ] ← 자동 (수정 가능) │
│ 고용보험: [ 35,100 ] ← 자동 (수정 가능) │
│ 기타 공제: │
│ 대출상환 [ 500,000 ] [삭제] │
│ [+ 기타 공제 추가] │
│ ────────────────────────── 총 공제액: 979,590 │
│ │
│ ═════════════════════════ 실수령액: 3,120,410 │
│ │
│ 메모: [ ] │
│ │
│ [취소] [미리보기] [저장]│
└─────────────────────────────────────────────────────┘
```
**구현 포인트:**
- 지급 항목 변경 시 `calculate-preview` API 호출하여 공제 항목 자동 갱신
- 법정 공제 필드는 기본 readonly + "수정" 토글로 수동 입력 허용
- 수동 입력된 공제 항목은 `deduction_overrides`로 전달
- `allowance_types`, `deduction_types`는 설정 API에서 조회하여 드롭다운 제공
### 7.3 급여명세서 (인쇄용)
`payslip` API 응답의 `earnings`/`deductions` 구조를 활용:
```
┌─────────────────────────────────────────────┐
│ 급 여 명 세 서 │
│ │
│ 사원명: 홍길동 귀속기간: 2026년 03월 │
│ │
│ ┌──── 지급 내역 ────┬── 공제 내역 ────┐ │
│ │ 기본급 3,500,000│ 소득세 117,750│ │
│ │ 식대 200,000│ 지방소득세 11,770│ │
│ │ 직책수당 300,000│ 건강보험 138,220│ │
│ │ 교통비 100,000│ 장기요양 1,250│ │
│ │ │ 국민연금 175,500│ │
│ │ │ 고용보험 35,100│ │
│ │ │ 대출상환 500,000│ │
│ ├───────────────────┼─────────────────┤ │
│ │ 지급합계 4,100,000│ 공제합계 979,590│ │
│ └───────────────────┴─────────────────┘ │
│ │
│ 실수령액: 3,120,410원 │
└─────────────────────────────────────────────┘
```
### 7.4 월간 워크플로우
```
1. 월초 → [일괄생성] 또는 [전월복사] 실행
2. 개별 급여 데이터 확인/수정
3. [일괄계산] 실행 (공제 항목 최신 요율로 재계산)
4. 데이터 확인 완료 → [일괄확정]
5. 급여 지급일 → 개별 [지급처리] (출금과 연결)
6. 급여명세서 조회/인쇄
```
### 7.5 금액 표시 규칙
- 모든 금액은 **원(KRW)** 단위 정수
- 천 단위 콤마 필수: `3,500,000`
- 음수 금액(환급): 빨간색 + `-` 부호
---
## 관련 문서
- [급여관리 기능 상세](../../features/finance/payroll.md) — 전표 변환, 권한, 멀티테넌트
- [DB 스키마 — 인사](../../system/database/hr.md)
- [결재관리 API 명세](approval-api.md)
---
**최종 업데이트**: 2026-03-11