# 경조사비 서비스 이관 기획서 > **작성일**: 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