diff --git a/INDEX.md b/INDEX.md index 6f825f0..b56fc5d 100644 --- a/INDEX.md +++ b/INDEX.md @@ -19,7 +19,7 @@ | 품목관리 | `rules/item-policy.md` | 품목 정책 | | 단가관리 | `rules/pricing-policy.md` | 원가/판매가, 리비전 | | 견적관리 | `features/quotes/README.md` | 견적 시스템, BOM 계산 | -| 급여관리 API | `plans/payroll-api-implementation-plan.md` | MNG→API 급여관리 이식 계획 | +| 급여관리 API | `frontend/api-specs/payroll-api.md` | 급여관리 API 전체 명세 (18개 엔드포인트) | | 결재관리 | `dev/dev_plans/approval-system-unification-plan.md` | MNG→API 결재 통합 계획 | | 운영 배포 | `dev/dev_plans/production-deployment-plan.md` | 배포 계획 | | 서버 운영 | `dev/deploys/ops-manual/README.md` | 서버 운영 매뉴얼 | @@ -210,6 +210,7 @@ DB 도메인별: |------|------| | [approval-api.md](frontend/api-specs/approval-api.md) | 결재관리 API 전체 명세 (28개 엔드포인트) | | [document-api-integration.md](frontend/api-specs/document-api-integration.md) | 문서 API 연동 명세 | +| [payroll-api.md](frontend/api-specs/payroll-api.md) | 급여관리 API 전체 명세 (18개 엔드포인트) | --- diff --git a/frontend/api-specs/payroll-api.md b/frontend/api-specs/payroll-api.md new file mode 100644 index 0000000..d44cb0a --- /dev/null +++ b/frontend/api-specs/payroll-api.md @@ -0,0 +1,1117 @@ +# 급여관리 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