diff --git a/INDEX.md b/INDEX.md index 4ac44b1..0db6d78 100644 --- a/INDEX.md +++ b/INDEX.md @@ -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 이관 현황, 우선순위, 로드맵 | diff --git a/dev/dev_plans/condolence-expense-service-plan.md b/dev/dev_plans/condolence-expense-service-plan.md new file mode 100644 index 0000000..8719b96 --- /dev/null +++ b/dev/dev_plans/condolence-expense-service-plan.md @@ -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