docs: [finance] 경조사비 서비스 이관 기획서 추가

- MNG 경조사비 → API+React 이관 기획
- API 엔드포인트 7개, React 페이지 구성 설계
- INDEX.md에 문서 등록
This commit is contained in:
김보곤
2026-03-19 12:59:46 +09:00
parent c88b9c0d14
commit 2e3ee9fc92
2 changed files with 549 additions and 0 deletions

View File

@@ -42,6 +42,7 @@
| QMS 점검표 | `dev/dev_plans/qms-checklist-template-plan.md` | 점검표 템플릿 관리 기능 |
| 부적합관리 | `dev/dev_plans/nonconforming-management-plan.md` | 자재관리 부적합관리 기획 (불량내역/처리방법/자재비용) |
| 계정별원장·손익계산서 | `dev/dev_plans/account-ledger-income-statement-plan.md` | 계정별원장 + 손익계산서 신규 메뉴 기획 (더존 참고) |
| 경조사비 서비스 이관 | `dev/dev_plans/condolence-expense-service-plan.md` | MNG 경조사비 → API+React 서비스 이관 기획 |
| 서버 운영 | `dev/deploys/ops-manual/README.md` | 서버 운영 매뉴얼 |
| 서버 접근/백업 | `system/server-access-management.md` | 계정, 권한, 백업, 리플리케이션 |
| 이관 작업 | `system/migration-status.md` | MNG→API+React 이관 현황, 우선순위, 로드맵 |

View File

@@ -0,0 +1,548 @@
# 경조사비 서비스 이관 기획서
> **작성일**: 2026-03-19
> **상태**: 기획 확정
> **대상**: MNG → API + React (서비스 이관)
> **위치**: 서비스 > 회계관리 > 경조사비
---
## 1. 개요
### 1.1 목적
MNG 백오피스의 경조사비관리 기능을 서비스(API + React)로 이관한다.
기존 MNG의 HTMX + Alpine.js 기반 UI를 REST API + React 구조로 재구현하며,
멀티테넌트 정책을 강화하고 사용자(테넌트) 직접 사용이 가능하도록 한다.
### 1.2 배경
- MNG `회계/세무관리 > 경조사비관리` 메뉴로 기존 구현 완료 (2026-03-06)
- DB 테이블 `condolence_expenses`는 API 프로젝트에서 마이그레이션 관리 중
- MNG는 `scopeForTenant()` 수동 스코프 사용 → API는 `BelongsToTenant` 글로벌 스코프로 전환
- MNG Controller에 비즈니스 로직이 직접 작성됨 → API는 Service-First 패턴 적용
### 1.3 이관 범위
| 구분 | MNG (현재) | API + React (이관 후) |
|------|-----------|---------------------|
| **백엔드** | Controller 직접 로직 | Service + Controller + FormRequest |
| **프론트** | Blade + Alpine.js | React (Next.js) |
| **인증** | 세션 기반 | Bearer 토큰 + `BelongsToTenant` |
| **데이터** | `scopeForTenant()` 수동 | 글로벌 스코프 자동 격리 |
| **API 문서** | 없음 | Swagger |
---
## 2. 현재 MNG 기능 분석
### 2.1 DB 테이블 (`condolence_expenses`)
> **마이그레이션**: `api/database/migrations/2026_03_06_220000_create_condolence_expenses_table.php`
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `id` | BIGINT PK | 고유 ID |
| `tenant_id` | BIGINT (FK, INDEX) | 테넌트 ID |
| `event_date` | DATE | 경조사 발생일 |
| `expense_date` | DATE | 지출일 |
| `partner_name` | VARCHAR(100) | 거래처명/대상자 |
| `description` | VARCHAR(200) | 내역 |
| `category` | VARCHAR(20) | `congratulation`(축의) / `condolence`(부조) |
| `has_cash` | BOOLEAN | 부조금 여부 |
| `cash_method` | VARCHAR(30) | `cash` / `transfer` / `card` |
| `cash_amount` | INTEGER | 부조금액 |
| `has_gift` | BOOLEAN | 선물 여부 |
| `gift_type` | VARCHAR(50) | 선물 종류 |
| `gift_amount` | INTEGER | 선물 금액 |
| `total_amount` | INTEGER | 총금액 (부조금 + 선물) |
| `options` | JSON | 확장 속성 |
| `memo` | TEXT | 비고 |
| `created_by` | BIGINT | 등록자 |
| `created_at` / `updated_at` | TIMESTAMP | 타임스탬프 |
| `deleted_at` | TIMESTAMP | 소프트 삭제 |
**인덱스**: `(tenant_id, event_date)`, `(tenant_id, category)`
### 2.2 MNG 기능 목록
| 기능 | MNG 메서드 | 설명 |
|------|-----------|------|
| 목록 조회 | `list()` | 연도/구분/검색 필터 + 통계 |
| 등록 | `store()` | 경조사비 신규 등록 |
| 수정 | `update()` | 기존 항목 수정 |
| 삭제 | `destroy()` | 소프트 삭제 |
| CSV 내보내기 | JS `exportCsv()` | 프론트엔드에서 직접 생성 |
### 2.3 통계 응답 (`stats`)
```json
{
"totalCount": 12,
"totalAmount": 1250000,
"cashTotal": 750000,
"giftTotal": 500000,
"congratulationCount": 7,
"condolenceCount": 5
}
```
---
## 3. API 설계
### 3.1 엔드포인트
> **라우트 파일**: `api/routes/api/v1/finance.php`
> **기본 경로**: `/api/v1/condolence-expenses`
| Method | Path | 설명 | 비고 |
|--------|------|------|------|
| GET | `/` | 목록 조회 (페이지네이션) | 필터: year, category, search |
| POST | `/` | 신규 등록 | |
| GET | `/summary` | 통계 조회 | 연도/구분별 집계 |
| GET | `/{id}` | 상세 조회 | |
| PUT | `/{id}` | 수정 | |
| DELETE | `/{id}` | 삭제 (소프트) | |
| GET | `/export` | CSV/Excel 내보내기 | 서버사이드 생성 |
### 3.2 목록 조회 요청/응답
**요청 파라미터**:
| 파라미터 | 타입 | 필수 | 설명 | 기본값 |
|---------|------|:---:|------|--------|
| `year` | integer | N | 연도 필터 | 당해연도 |
| `category` | string | N | `congratulation` / `condolence` | 전체 |
| `search` | string | N | 거래처명/내역/비고 통합 검색 | - |
| `page` | integer | N | 페이지 번호 | 1 |
| `per_page` | integer | N | 페이지 크기 | 50 |
| `sort_by` | string | N | 정렬 기준 | `event_date` |
| `sort_order` | string | N | `asc` / `desc` | `desc` |
**응답**:
```json
{
"success": true,
"data": [
{
"id": 1,
"event_date": "2026-03-15",
"expense_date": "2026-03-16",
"partner_name": "ABC 회사",
"description": "김과장 결혼축의금",
"category": "congratulation",
"category_label": "축의",
"has_cash": true,
"cash_method": "transfer",
"cash_method_label": "계좌이체",
"cash_amount": 50000,
"has_gift": true,
"gift_type": "화환",
"gift_amount": 30000,
"total_amount": 80000,
"memo": "사원 본인 결혼",
"created_by_name": "홍길동",
"created_at": "2026-03-16T10:30:00"
}
],
"meta": { "current_page": 1, "last_page": 1, "per_page": 50, "total": 12 }
}
```
### 3.3 통계 조회 (`/summary`)
**요청**: `?year=2026&category=all`
**응답**:
```json
{
"success": true,
"data": {
"total_count": 12,
"total_amount": 1250000,
"cash_total": 750000,
"gift_total": 500000,
"congratulation_count": 7,
"condolence_count": 5,
"congratulation_amount": 800000,
"condolence_amount": 450000
}
}
```
### 3.4 등록/수정 요청
```json
{
"event_date": "2026-03-15",
"expense_date": "2026-03-16",
"partner_name": "ABC 회사",
"description": "김과장 결혼축의금",
"category": "congratulation",
"has_cash": true,
"cash_method": "transfer",
"cash_amount": 50000,
"has_gift": true,
"gift_type": "화환",
"gift_amount": 30000,
"memo": "사원 본인 결혼"
}
```
> `total_amount`는 서버에서 자동 계산 (클라이언트 전송 불필요)
### 3.5 검증 규칙 (FormRequest)
| 필드 | 규칙 |
|------|------|
| `partner_name` | 필수, string, max:100 |
| `category` | 필수, in:congratulation,condolence |
| `event_date` | 선택, date |
| `expense_date` | 선택, date |
| `description` | 선택, string, max:200 |
| `has_cash` | 선택, boolean |
| `cash_method` | `has_cash`=true일 때 필수, in:cash,transfer,card |
| `cash_amount` | `has_cash`=true일 때 필수, integer, min:0 |
| `has_gift` | 선택, boolean |
| `gift_type` | `has_gift`=true일 때 선택, string, max:50 |
| `gift_amount` | `has_gift`=true일 때 필수, integer, min:0 |
| `memo` | 선택, string |
---
## 4. API 구현 상세
### 4.1 파일 구조
```
api/
├── app/
│ ├── Http/
│ │ ├── Controllers/Api/V1/CondolenceExpenseController.php
│ │ └── Requests/V1/CondolenceExpense/
│ │ ├── StoreCondolenceExpenseRequest.php
│ │ └── UpdateCondolenceExpenseRequest.php
│ ├── Models/Tenants/CondolenceExpense.php
│ ├── Services/CondolenceExpenseService.php
│ └── Swagger/v1/CondolenceExpenseApi.php
└── routes/api/v1/finance.php (라우트 추가)
```
### 4.2 Model
```php
class CondolenceExpense extends Model
{
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
// 카테고리 상수
const CATEGORY_CONGRATULATION = 'congratulation';
const CATEGORY_CONDOLENCE = 'condolence';
// 지출방법 상수
const CASH_METHOD_CASH = 'cash';
const CASH_METHOD_TRANSFER = 'transfer';
const CASH_METHOD_CARD = 'card';
protected $casts = [
'event_date' => 'date',
'expense_date' => 'date',
'has_cash' => 'boolean',
'has_gift' => 'boolean',
'cash_amount' => 'integer',
'gift_amount' => 'integer',
'total_amount' => 'integer',
'options' => 'array',
];
// Accessor: category_label
// Accessor: cash_method_label
// Scope: scopeByCategory(), scopeInYear()
// Relation: creator() → User (created_by)
}
```
### 4.3 Service 메서드
| 메서드 | 설명 |
|--------|------|
| `index(array $params)` | 페이지네이션 + 필터 (year, category, search, sort) |
| `store(array $data)` | 등록 (total_amount 자동 계산, created_by 설정) |
| `show(int $id)` | 상세 조회 |
| `update(int $id, array $data)` | 수정 (total_amount 재계산) |
| `destroy(int $id)` | 소프트 삭제 |
| `summary(array $params)` | 통계 집계 (year, category 필터) |
| `export(array $params)` | CSV/Excel 데이터 생성 |
### 4.4 total_amount 자동 계산
```php
private function calculateTotal(array $data): int
{
$cash = ($data['has_cash'] ?? false) ? ($data['cash_amount'] ?? 0) : 0;
$gift = ($data['has_gift'] ?? false) ? ($data['gift_amount'] ?? 0) : 0;
return $cash + $gift;
}
```
### 4.5 마이그레이션 추가 필요 여부
> **기존 테이블**: `condolence_expenses` (이미 존재)
> **추가 마이그레이션**: `updated_by`, `deleted_by` 컬럼 추가 (Auditable 트레이트 호환)
```php
// 추가 마이그레이션 필요
Schema::table('condolence_expenses', function (Blueprint $table) {
$table->unsignedBigInteger('updated_by')->nullable()->after('created_by');
$table->unsignedBigInteger('deleted_by')->nullable()->after('updated_by');
});
```
---
## 5. React 구현 상세
### 5.1 메뉴 위치
```
회계관리 (accounting)
├── 입금관리
├── 출금관리
├── 카드거래조회
├── 은행거래조회
├── ...
├── 경조사비 ← 신규 (NEW)
├── 상품권
└── ...
```
**React 라우트**: `/accounting/condolence-expenses`
### 5.2 파일 구조
```
react/src/
├── app/[locale]/(protected)/accounting/
│ └── condolence-expenses/
│ └── page.tsx (진입점: mode 분기)
├── components/accounting/
│ └── CondolenceExpenseManagement/
│ ├── index.tsx (메인 컴포넌트)
│ ├── CondolenceExpenseList.tsx (목록 + 필터 + 통계)
│ ├── CondolenceExpenseForm.tsx (등록/수정 모달)
│ ├── actions.ts (Server Actions)
│ └── types.ts (타입 정의)
```
### 5.3 페이지 구성
#### 통계 카드 (상단)
| 카드 | 값 | 스타일 |
|------|------|--------|
| 총 건수 | `total_count` | - |
| 총 금액 | `total_amount` | 통화 포맷 |
| 부조금 합계 | `cash_total` | 통화 포맷 |
| 선물 합계 | `gift_total` | 통화 포맷 |
| 축의/부조 | `congratulation_count` / `condolence_count` | 건수 |
#### 필터 (통계 카드 하단)
| 필터 | 타입 | 옵션 |
|------|------|------|
| 연도 | Select | 당해 ~ 5년 전 |
| 구분 | Select | 전체 / 축의 / 부조 |
| 검색 | Input | 거래처명, 내역, 비고 (debounce 300ms) |
#### 테이블
| No | 컬럼 | 정렬 |
|----|------|------|
| 1 | 경조사일자 | 좌 |
| 2 | 지출일자 | 좌 |
| 3 | 거래처명 | 좌 |
| 4 | 내역 | 좌 |
| 5 | 구분 | 중앙 (배지: 축의=빨강, 부조=회색) |
| 6 | 부조금 여부 | 중앙 |
| 7 | 지출방법 | 좌 |
| 8 | 부조금액 | 우 (통화) |
| 9 | 선물 여부 | 중앙 |
| 10 | 선물종류 | 좌 |
| 11 | 선물금액 | 우 (통화) |
| 12 | 총금액 | 우 (통화, 굵게) |
| 13 | 비고 | 좌 |
**하단 합계 행**: 부조금액 합계, 선물금액 합계, 총금액 합계
#### 등록/수정 모달
MNG 모달 구조를 React Dialog로 재현:
1. 날짜 (경조사일자, 지출일자)
2. 거래처명 (필수), 내역, 구분 (축의/부조)
3. 부조금 섹션 (체크박스 토글) — 지출방법, 금액
4. 선물 섹션 (체크박스 토글) — 종류, 금액
5. 총금액 (읽기 전용, 자동 계산)
6. 비고
### 5.4 Server Actions (`actions.ts`)
```typescript
// buildApiUrl 필수 사용
import { buildApiUrl } from '@/lib/api/query-params';
export async function getCondolenceExpenses(params) {
// GET /api/v1/condolence-expenses
}
export async function getCondolenceExpenseSummary(params) {
// GET /api/v1/condolence-expenses/summary
}
export async function createCondolenceExpense(data) {
// POST /api/v1/condolence-expenses
}
export async function updateCondolenceExpense(id, data) {
// PUT /api/v1/condolence-expenses/{id}
}
export async function deleteCondolenceExpense(id) {
// DELETE /api/v1/condolence-expenses/{id}
}
```
---
## 6. 인증 및 권한
### 6.1 API 인증
- 기본 미들웨어: `auth.apikey` (API Key + Bearer 토큰)
- `BelongsToTenant` 글로벌 스코프로 자동 데이터 격리
### 6.2 MNG → API 호출 시
> 경조사비 API는 Bearer 토큰 **필수** (화이트리스트 등록 불필요)
> MNG에서 경조사비를 계속 사용하려면 FormulaApiService 패턴으로 API 호출 필요
### 6.3 React 인증
- `authenticatedFetch` 사용 (HttpOnly 쿠키 기반)
- 직접 `fetch` 금지
---
## 7. 구현 순서
### Phase 1: API 구현
| 순서 | 작업 | 파일 |
|------|------|------|
| 1 | 마이그레이션 (updated_by, deleted_by 추가) | `database/migrations/` |
| 2 | Model 생성 | `app/Models/Tenants/CondolenceExpense.php` |
| 3 | Service 생성 | `app/Services/CondolenceExpenseService.php` |
| 4 | FormRequest 생성 | `app/Http/Requests/V1/CondolenceExpense/` |
| 5 | Controller 생성 | `app/Http/Controllers/Api/V1/CondolenceExpenseController.php` |
| 6 | Route 등록 | `routes/api/v1/finance.php` |
| 7 | Swagger 작성 | `app/Swagger/v1/CondolenceExpenseApi.php` |
### Phase 2: React 구현
| 순서 | 작업 | 파일 |
|------|------|------|
| 1 | 타입 정의 | `types.ts` |
| 2 | Server Actions | `actions.ts` |
| 3 | 목록 컴포넌트 (통계 + 필터 + 테이블) | `CondolenceExpenseList.tsx` |
| 4 | 등록/수정 모달 | `CondolenceExpenseForm.tsx` |
| 5 | 페이지 진입점 | `page.tsx` |
| 6 | 메뉴 등록 | DB (tinker) |
### Phase 3: 연동 테스트
| 순서 | 작업 |
|------|------|
| 1 | API Swagger UI 테스트 |
| 2 | React ↔ API 연동 확인 |
| 3 | 멀티테넌트 데이터 격리 검증 |
| 4 | 기존 MNG 데이터 React에서 조회 확인 |
---
## 8. MNG 기존 코드 처리
> **MNG 경조사비 코드는 삭제하지 않는다.**
| 항목 | 처리 |
|------|------|
| MNG Controller | 유지 (관리자 전용 조회 가능) |
| MNG Model | 유지 |
| MNG Blade View | 유지 |
| MNG Route | 유지 |
이관 완료 후 MNG 메뉴에서 경조사비를 숨기고, 서비스(React)로 안내한다.
---
## 9. 기존 패턴 참고
| 참고 기능 | 위치 | 참고 사항 |
|----------|------|----------|
| BankAccount | `api/app/Services/BankAccountService.php` | CRUD + toggle + setPrimary 패턴 |
| ExpenseAccount | `api/app/Models/Tenants/ExpenseAccount.php` | 복리후생비 카테고리 구분 |
| Deposit/Withdrawal | `api/app/Models/Tenants/Deposit.php` | 입출금 CRUD 패턴 |
| 급여관리 | `docs/features/finance/payroll.md` | 상태 워크플로우, 전표 연동 |
---
## 10. 체크리스트
### API 구현 체크리스트
- [ ] Model: `BelongsToTenant`, `Auditable`, `SoftDeletes` trait 적용
- [ ] Model: `'options' => 'array'` cast + `getOption()`/`setOption()` 헬퍼
- [ ] Model: 카테고리/지출방법 상수 정의
- [ ] Service: `$this->tenantId()`, `$this->apiUserId()` 사용
- [ ] Service: `total_amount` 자동 계산 로직
- [ ] Controller: `ApiResponse::handle()` 사용
- [ ] FormRequest: 검증 규칙 분리 (Store/Update)
- [ ] Route: `finance.php`에 등록
- [ ] Swagger: 엔드포인트 문서 작성
- [ ] 마이그레이션: `updated_by`, `deleted_by` 컬럼 추가
### React 구현 체크리스트
- [ ] `'use client'` 선언
- [ ] `buildApiUrl()` 사용 (직접 URL 조립 금지)
- [ ] `authenticatedFetch` / Server Actions 사용
- [ ] 통계 카드 + 필터 + 테이블 + 합계행
- [ ] 등록/수정 모달 (Dialog)
- [ ] 금액 포맷팅 (천 단위 구분)
- [ ] debounce 검색 (300ms)
- [ ] 메뉴 등록 (DB tinker)
### 인증 체크리스트
- [ ] API: Bearer 토큰 필수 (화이트리스트 불필요)
- [ ] React: `authenticatedFetch` 사용
- [ ] 멀티테넌트 데이터 격리 검증
---
## 관련 문서
| 문서 | 경로 |
|------|------|
| API 개발 규칙 | `dev/standards/api-rules.md` |
| options 컬럼 정책 | `standards/options-column-policy.md` |
| 이관 현황 | `system/migration-status.md` |
| 급여관리 | `features/finance/payroll.md` |
| 보안 정책 | `system/security-policy.md` |
| 계정별원장 기획 | `dev/dev_plans/account-ledger-income-statement-plan.md` |
---
**최종 업데이트**: 2026-03-19