16 KiB
경조사비 서비스 이관 기획서
작성일: 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)
{
"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 |
응답:
{
"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
응답:
{
"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 등록/수정 요청
{
"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
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 자동 계산
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 트레이트 호환)
// 추가 마이그레이션 필요
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로 재현:
- 날짜 (경조사일자, 지출일자)
- 거래처명 (필수), 내역, 구분 (축의/부조)
- 부조금 섹션 (체크박스 토글) — 지출방법, 금액
- 선물 섹션 (체크박스 토글) — 종류, 금액
- 총금액 (읽기 전용, 자동 계산)
- 비고
5.4 Server Actions (actions.ts)
// 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,SoftDeletestrait 적용 - 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