Files
sam-docs/plans/payroll-api-implementation-plan.md
김보곤 593bef9e5d docs: [payroll] 급여관리 API 구현 기획서 작성
- MNG 급여관리 시스템 → API 이식 3단계 계획 수립
- Phase 1: 핵심 계산 엔진 (소득세, 4대보험, 공제 오버라이드)
- Phase 2: 상태 관리 + 일괄 처리 (unconfirm, unpay, bulkGenerate)
- Phase 3: 문서 생성 (PDF 명세서, 전표 변환, 엑셀 내보내기)
- INDEX.md에 문서 등록
2026-03-11 18:04:10 +09:00

340 lines
13 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
> **상태**: 계획 수립
> **참조**: MNG 급여관리 시스템 (`mng/app/Services/HR/PayrollService.php`)
---
## 1. 개요
### 1.1 목적
MNG에서 운영 중인 급여관리 시스템의 핵심 비즈니스 로직을 API 서버에 구현한다. React 프론트엔드에서 급여 관리 기능을 사용할 수 있도록 완전한 REST API를 제공한다.
### 1.2 배경
- MNG 급여관리: 완성도 100% (CRUD, 자동계산, 일괄생성, PDF 명세서, 전표변환)
- API 급여관리: 완성도 ~50% (기본 CRUD만 구현, 핵심 계산 로직 누락)
- React에서 급여관리 화면을 구현하려면 API에 동일한 비즈니스 로직이 필요하다
### 1.3 원칙
- MNG의 검증된 로직을 API 컨벤션에 맞게 이식한다
- API 프로젝트의 Service-First 아키텍처, i18n, FormRequest 패턴을 준수한다
- 기존 `payrolls` 테이블 스키마를 그대로 사용한다 (추가 마이그레이션 최소화)
---
## 2. 현황 분석 (GAP)
### 2.1 기능 비교
| 기능 | MNG | API | GAP |
|------|:---:|:---:|-----|
| 급여 CRUD | ✅ | ✅ | - |
| 급여 설정 CRUD | ✅ | ✅ | - |
| 목록 조회 (필터/페이지네이션) | ✅ | ✅ | - |
| 월별 통계 | ✅ | ✅ | - |
| 확정 (`confirm`) | ✅ | ✅ | - |
| 지급 처리 (`pay`) | ✅ | ✅ | - |
| 일괄 확정 (`bulkConfirm`) | ✅ | ✅ | - |
| **소득세 자동 계산** | ✅ | ❌ | 간이세액표 기반 계산 로직 전체 누락 |
| **4대보험 자동 계산** | ✅ | ⚠️ | 설정값만 존재, `calculateAmounts()` 미구현 |
| **공제 오버라이드** | ✅ | ❌ | 수동 공제 수정 후 재계산 미지원 |
| **확정 취소 (`unconfirm`)** | ✅ | ❌ | 상태 복구 불가 |
| **지급 취소 (`unpay`)** | ✅ | ❌ | 슈퍼관리자 기능 누락 |
| **일괄 생성 (`bulkGenerate`)** | ✅ | ❌ | 재직사원 기반 신규 생성 미구현 |
| **전월 복사 (`copyFromPrevious`)** | ✅ | ❌ | 이전 월 데이터 복사 미구현 |
| **급여명세서 PDF 생성** | ✅ | ❌ | 데이터 조회만 가능, PDF 미생성 |
| **급여명세서 이메일 발송** | ✅ | ❌ | 이메일 발송 미구현 |
| **전표 자동 생성** | ✅ | ❌ | `generateJournalEntry()` 미구현 |
| **엑셀 내보내기** | ✅ | ❌ | export 미구현 |
| **공제대상가족수 자동 산출** | ✅ | ❌ | 피부양자 기반 가족수 미산출 |
### 2.2 API 기존 코드 현황
| 파일 | 상태 | 비고 |
|------|------|------|
| `Controllers/Api/V1/PayrollController.php` | 기본 CRUD 구현 | 누락 엔드포인트 추가 필요 |
| `Services/PayrollService.php` | 기본 CRUD + 제한적 계산 | 핵심 로직 이식 필요 |
| `Models/Tenants/Payroll.php` | 모델 정의 완료 | 상태 헬퍼 메서드 보강 필요 |
| `Models/Tenants/PayrollSetting.php` | 설정 모델 완료 | - |
| `Requests/V1/Payroll/` | FormRequest 5개 존재 | 추가 Request 필요 |
| `routes/api/v1/finance.php` | 기본 라우트 정의 | 누락 엔드포인트 추가 |
---
## 3. 구현 범위
### Phase 1: 핵심 계산 엔진 (필수)
> **목표**: 급여 자동 계산이 동작하도록 핵심 비즈니스 로직을 이식한다.
| # | 작업 | 참조 (MNG) | 대상 파일 (API) |
|---|------|-----------|----------------|
| 1-1 | `calculateAmounts()` 메서드 구현 | `PayrollService:529-590` | `Services/PayrollService.php` |
| 1-2 | `calculateIncomeTax()` 소득세 계산 | `PayrollService:592-670` | `Services/PayrollService.php` |
| 1-3 | 4대보험 개별 계산 메서드 | `PayrollService:672-720` | `Services/PayrollService.php` |
| 1-4 | `applyDeductionOverrides()` 공제 수동 수정 | `PayrollService:722-760` | `Services/PayrollService.php` |
| 1-5 | `resolveFamilyCount()` 가족수 산출 | `PayrollService:762-800` | `Services/PayrollService.php` |
| 1-6 | `IncomeTaxBracket` 모델 생성 | `Models/HR/IncomeTaxBracket.php` | `Models/Tenants/IncomeTaxBracket.php` |
| 1-7 | `income_tax_brackets` 마이그레이션 실행 확인 | 이미 존재 확인 필요 | `database/migrations/` |
| 1-8 | `store()`/`update()` 에서 자동 계산 적용 | `PayrollService:150-250` | `Services/PayrollService.php` |
**계산 흐름**:
```
입력: base_salary, overtime_pay, bonus, allowances, deductions
├─ Step 1: 총 지급액 = base_salary + overtime_pay + bonus + Σ(allowances)
├─ Step 2: 과세표준 = 총 지급액 - bonus (비과세)
├─ Step 3: 4대보험 = 과세표준 × 요율 (PayrollSetting 참조)
│ ├─ 건강보험 = 과세표준 × 3.545%
│ ├─ 장기요양 = 건강보험 × 0.9082%
│ ├─ 국민연금 = clamp(min, max, 과세표준) × 4.5%
│ └─ 고용보험 = 과세표준 × 0.9%
├─ Step 4: 근로소득세 = 간이세액표 조회 (가족수 반영)
│ ├─ < 770천원: 0원
│ ├─ 770~10,000천원: DB 간이세액표
│ └─ > 10,000천원: 소득세법 시행령 별표2 공식
├─ Step 5: 지방소득세 = 근로소득세 × 10%
├─ Step 6: 총 공제액 = 4대보험 + 세금 + Σ(deductions)
└─ Step 7: 실수령액 = 총 지급액 - 총 공제액
※ 모든 금액: 10원 단위 절삭 (floor)
```
---
### Phase 2: 상태 관리 + 일괄 처리
| # | 작업 | 참조 (MNG) | 비고 |
|---|------|-----------|------|
| 2-1 | `unconfirm()` 확정 취소 | `PayrollService:340-360` | confirmed → draft |
| 2-2 | `unpay()` 지급 취소 | `PayrollService:380-400` | paid → draft (슈퍼관리자) |
| 2-3 | `bulkGenerate()` 재직사원 일괄 생성 | `PayrollService:442-521` | Employee 연봉 기반 |
| 2-4 | `copyFromPreviousMonth()` 전월 복사 | `PayrollService:402-440` | soft-delete 처리 포함 |
| 2-5 | Payroll 모델에 상태 헬퍼 메서드 추가 | `Models/HR/Payroll.php` | `isEditable()`, `isConfirmable()` 등 |
**일괄 생성 로직**:
```
bulkGenerate(year, month)
├─ 1. PayrollSetting 조회
├─ 2. 활성 재직사원 전체 조회
├─ 3. 각 사원별:
│ ├─ 이미 존재 → skip
│ ├─ soft-deleted 존재 → forceDelete 후 재생성
│ ├─ 기본급 = 연봉 / 12
│ ├─ calculateAmounts() 호출
│ └─ Payroll 생성 (status: draft)
└─ 4. 결과: {created: N, skipped: M}
```
---
### Phase 3: 문서 생성 + 내보내기
| # | 작업 | 참조 (MNG) | 비고 |
|---|------|-----------|------|
| 3-1 | `sendPayslip()` 급여명세서 PDF + 이메일 | `PayrollService:820-920` | DomPDF + Pretendard |
| 3-2 | `generateJournalEntry()` 전표 자동 생성 | `PayrollController:900-1088` | 분개 구조 동일 |
| 3-3 | `export()` 엑셀 내보내기 | `PayrollService:100-140` | 동적 열 포함 |
| 3-4 | 급여명세서 Blade 뷰 생성 | `emails/payslip.blade.php` | PDF 폰트 정책 준수 |
| 3-5 | PayslipMail Mailable 생성 | `Mail/PayslipMail.php` | |
---
## 4. API 엔드포인트 설계
### 4.1 추가 엔드포인트
기존 라우트(`routes/api/v1/finance.php`)에 추가할 엔드포인트:
| Method | URI | 설명 | Phase |
|--------|-----|------|:-----:|
| POST | `/v1/payrolls/{id}/unconfirm` | 확정 취소 | 2 |
| POST | `/v1/payrolls/{id}/unpay` | 지급 취소 (슈퍼관리자) | 2 |
| POST | `/v1/payrolls/bulk-generate` | 재직사원 일괄 생성 | 2 |
| POST | `/v1/payrolls/copy-from-previous` | 전월 복사 | 2 |
| POST | `/v1/payrolls/{id}/send-payslip` | 급여명세서 이메일 발송 | 3 |
| POST | `/v1/payrolls/generate-journal-entry` | 전표 자동 생성 | 3 |
| GET | `/v1/payrolls/export` | 엑셀 내보내기 | 3 |
### 4.2 기존 엔드포인트 수정
| URI | 변경 내용 | Phase |
|-----|----------|:-----:|
| `POST /v1/payrolls` | `calculateAmounts()` 자동 적용 | 1 |
| `PUT /v1/payrolls/{id}` | 공제 오버라이드 지원 | 1 |
| `POST /v1/payrolls/calculate` | 소득세 포함 전체 계산으로 개선 | 1 |
### 4.3 요청/응답 예시
**급여 등록 요청** (`POST /v1/payrolls`):
```json
{
"user_id": 15,
"pay_year": 2026,
"pay_month": 3,
"base_salary": 3500000,
"overtime_pay": 500000,
"bonus": 200000,
"allowances": [
{"name": "교통비", "amount": 100000}
],
"deductions": [
{"name": "대출상환", "amount": 300000}
],
"deduction_overrides": {
"pension": 180000,
"health_insurance": null
}
}
```
**자동 계산 응답** (`POST /v1/payrolls/calculate`):
```json
{
"success": true,
"data": {
"gross_salary": 4300000,
"taxable_base": 4100000,
"pension": 184500,
"health_insurance": 145345,
"long_term_care": 13200,
"employment_insurance": 36900,
"income_tax": 78340,
"resident_tax": 7830,
"total_deductions": 766115,
"net_salary": 3533885,
"family_count": 2
}
}
```
---
## 5. 데이터베이스
### 5.1 기존 테이블 (변경 불필요)
- `payrolls` — 이미 모든 필드 존재 (options JSON 컬럼 포함)
- `payroll_settings` — 설정 테이블 완비
### 5.2 확인 필요
| 테이블 | 상태 | 조치 |
|--------|------|------|
| `income_tax_brackets` | 마이그레이션 존재 확인 필요 | 없으면 생성 + 2024 간이세액표 시딩 |
| `payrolls.long_term_care` | 2026-02-27 추가 완료 | - |
| `payrolls.options` | 2026-03-10 추가 완료 | - |
### 5.3 간이세액표 시딩
`income_tax_brackets` 테이블에 2024년 국세청 간이세액표 데이터가 필요하다.
- 770천원 ~ 10,000천원 구간
- 가족수 1~11명별 세액
- MNG에 이미 시더 존재 → API로 이관
---
## 6. 추가 생성 파일
### 6.1 Phase 1
| 파일 | 설명 |
|------|------|
| `app/Models/Tenants/IncomeTaxBracket.php` | 간이세액표 모델 |
| `app/Http/Requests/V1/Payroll/BulkGenerateRequest.php` | 일괄 생성 요청 |
| `app/Http/Requests/V1/Payroll/CopyFromPreviousRequest.php` | 전월 복사 요청 |
### 6.2 Phase 3
| 파일 | 설명 |
|------|------|
| `app/Mail/PayslipMail.php` | 급여명세서 Mailable |
| `resources/views/emails/payslip.blade.php` | 급여명세서 PDF 뷰 |
| `resources/views/emails/payslip-notification.blade.php` | 이메일 본문 |
| `app/Exports/PayrollExport.php` | 엑셀 내보내기 |
---
## 7. 주의사항
### 7.1 필수 준수
- ✅ 마이그레이션은 API 프로젝트에서만 생성 (CLAUDE.md 규칙)
- ✅ PDF 생성 시 Pretendard 폰트 + `ensureKoreanFont()` 적용 (폰트 정책)
- ✅ 모든 응답 메시지는 i18n 키 사용 (`__('message.xxx')`)
-`ApiResponse::handle()` 패턴 사용
- ✅ FormRequest로 입력 검증
### 7.2 MNG 코드 이식 시 변환 규칙
| MNG 패턴 | API 패턴 |
|----------|---------|
| `auth()->id()` | `$this->apiUserId()` |
| `session('tenant_id')` | `$this->tenantId()` |
| 직접 JSON 응답 | `ApiResponse::success()` / `ApiResponse::handle()` |
| 하드코딩 한글 메시지 | `__('message.payroll.xxx')` |
| HTMX 부분 렌더링 | JSON 응답 전용 |
| `Payroll::query()` | `Payroll::query()->forTenant($this->tenantId())` |
### 7.3 Salary 모델과의 관계
- `Payroll` = 상세 급여 관리 (세금/보험 자동 계산, MNG 연동)
- `Salary` = React용 간소화 급여 현황 (별도 유지)
- 두 모델은 독립적으로 운영하며, 추후 통합 여부 검토
---
## 8. 작업 순서 (권장)
```
Phase 1 (핵심 계산) ─────────────────────────────────────
1-6. IncomeTaxBracket 모델 생성
1-7. 간이세액표 마이그레이션/시딩 확인
1-1. calculateAmounts() 구현
1-2. calculateIncomeTax() 구현
1-3. 4대보험 계산 메서드 구현
1-4. applyDeductionOverrides() 구현
1-5. resolveFamilyCount() 구현
1-8. store()/update()에 자동 계산 적용
─── 테스트: 급여 등록 → 자동 계산 검증 ───
Phase 2 (상태 + 일괄) ──────────────────────────────────
2-5. Payroll 모델 상태 헬퍼 추가
2-1. unconfirm() 구현 + 라우트
2-2. unpay() 구현 + 라우트
2-3. bulkGenerate() 구현 + 라우트
2-4. copyFromPreviousMonth() 구현 + 라우트
─── 테스트: 상태 전이, 일괄 생성 검증 ───
Phase 3 (문서 + 내보내기) ──────────────────────────────
3-4. 급여명세서 Blade 뷰 생성
3-1. sendPayslip() PDF + 이메일 구현
3-2. generateJournalEntry() 전표 생성 구현
3-3. export() 엑셀 내보내기 구현
─── 테스트: PDF 생성, 이메일 발송, 전표 검증 ───
```
---
## 관련 문서
- [급여관리 기능 문서](../features/finance/payroll.md) — MNG 급여관리 상세
- [API 개발 규칙](../dev/standards/api-rules.md) — Service-First, FormRequest 패턴
- [DB 스키마 — 인사](../system/database/hr.md) — payrolls 테이블 구조
- [PDF 폰트 정책](../dev/standards/pdf-font-policy.md) — DomPDF 한글 폰트
- [options 컬럼 정책](../dev/standards/options-column-policy.md) — JSON 확장 필드
---
**최종 업데이트**: 2026-03-11