# 경조사비 서비스 이관 기획서

> **작성일**: 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
