diff --git a/INDEX.md b/INDEX.md index 7e71b9c..6f825f0 100644 --- a/INDEX.md +++ b/INDEX.md @@ -19,6 +19,7 @@ | 품목관리 | `rules/item-policy.md` | 품목 정책 | | 단가관리 | `rules/pricing-policy.md` | 원가/판매가, 리비전 | | 견적관리 | `features/quotes/README.md` | 견적 시스템, BOM 계산 | +| 급여관리 API | `plans/payroll-api-implementation-plan.md` | MNG→API 급여관리 이식 계획 | | 결재관리 | `dev/dev_plans/approval-system-unification-plan.md` | MNG→API 결재 통합 계획 | | 운영 배포 | `dev/dev_plans/production-deployment-plan.md` | 배포 계획 | | 서버 운영 | `dev/deploys/ops-manual/README.md` | 서버 운영 매뉴얼 | diff --git a/plans/payroll-api-implementation-plan.md b/plans/payroll-api-implementation-plan.md new file mode 100644 index 0000000..93c3afd --- /dev/null +++ b/plans/payroll-api-implementation-plan.md @@ -0,0 +1,339 @@ +# 급여관리 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