Files
sam-docs/dev/dev_plans/archive/welfare-section-plan.md
권혁성 db63fcff85 refactor: [docs] 팀별 폴더 구조 재편 (공유/개발/프론트/기획)
- 개발팀 전용 폴더 dev/ 생성 (standards, guides, quickstart, changes, deploys, data, history, dev_plans 이동)
- 프론트엔드 전용 폴더 frontend/ 생성 (api/ → frontend/api-specs/)
- 기획팀 폴더 requests/ 생성
- plans/ → dev/dev_plans/ 이름 변경
- README.md 신규 (사람용 안내), INDEX.md 재작성 (Claude Code용)
- resources.md 신규 (노션 링크용, assets/brochure 이관 예정)
- CURRENT_WORKS.md 삭제, TODO.md → dev/ 이동
- 전체 참조 경로 업데이트

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 16:46:03 +09:00

1021 lines
35 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 복리후생비 현황 섹션 개발 계획
> **작성일**: 2026-01-22
> **목적**: CEO 대시보드 복리후생비 현황 섹션 완성 (4개 카드 + 모달 API 연동)
> **기준 문서**: `api/app/Swagger/v1/WelfareApi.php`
> **상태**: 🔄 진행중 (Serena ID: welfare-section-state)
---
## 📍 현재 진행 상태
| 항목 | 내용 |
|------|------|
| **마지막 완료 작업** | Phase 2 - 프론트엔드 연동 완료 |
| **다음 작업** | 검증 및 테스트 |
| **진행률** | 6/6 (100%) |
| **마지막 업데이트** | 2026-01-22 |
---
## 1. 개요
### 1.1 배경
CEO 대시보드의 복리후생비 현황 섹션은 4개의 카드로 구성됩니다:
1. **당해년도 복리후생비 한도** - 연간 총 한도
2. **{분기} 복리후생비 총 한도** - 분기별 한도
3. **{분기} 복리후생비 잔여한도** - 분기별 남은 한도
4. **{분기} 복리후생비 사용금액** - 분기별 사용 금액
현재 상태:
- ✅ 섹션 UI 컴포넌트: 완료 (`WelfareSection.tsx`)
- ✅ 카드 데이터 API: 완료 (`/api/v1/welfare/summary`)
- ✅ 프론트엔드 Hook: 완료 (`useWelfare()`)
- ⚠️ 모달 상세 데이터: Mock 사용 중 (API 연동 필요)
### 1.2 기준 원칙
```
┌─────────────────────────────────────────────────────────────────┐
│ 🎯 핵심 원칙 │
├─────────────────────────────────────────────────────────────────┤
│ 1. API-First: 백엔드 API 완성 후 프론트엔드 연동 │
│ 2. 기존 패턴 준수: WelfareService 확장 │
│ 3. Mock 데이터 구조 유지: 기존 모달 설정 형식 호환 │
└─────────────────────────────────────────────────────────────────┘
```
### 1.3 변경 승인 정책
| 분류 | 예시 | 승인 |
|------|------|------|
| ✅ 즉시 가능 | 모달 설정 Mock → API 변환, Transformer 추가 | 불필요 |
| ⚠️ 컨펌 필요 | 새 API 엔드포인트 추가, 서비스 메서드 추가 | **필수** |
| 🔴 금지 | expense_accounts 테이블 구조 변경 | 별도 협의 |
### 1.4 준수 규칙
- `docs/quickstart/quick-start.md` - 빠른 시작 가이드
- `docs/standards/quality-checklist.md` - 품질 체크리스트
- `api/CLAUDE.md` - SAM API Development Rules
---
## 2. 대상 범위
### 2.1 Phase 1: API 개발 (Backend)
| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 1.1 | 모달 상세 데이터 API 개발 | ✅ | `/api/v1/welfare/detail` |
| 1.2 | Swagger 문서 업데이트 | ✅ | WelfareApi.php |
### 2.2 Phase 2: 프론트엔드 연동
| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 2.1 | 타입 정의 추가 | ✅ | WelfareDetailApiResponse (types.ts) |
| 2.2 | API 함수 추가 | ✅ | useWelfareDetail hook (useCEODashboard.ts) |
| 2.3 | Transformer 추가 | ✅ | transformWelfareDetailResponse (transformers.ts) |
| 2.4 | 모달 설정 동적 생성 | ✅ | CEODashboard.tsx 연동 + fallback |
---
## 3. 작업 절차
### 3.1 단계별 절차
```
Step 1: API 개발 (Backend)
├── WelfareService에 getDetail() 메서드 추가
├── WelfareController에 detail() 액션 추가
├── routes/api.php에 라우트 등록
└── Swagger 문서 작성
Step 2: 프론트엔드 연동
├── types.ts에 WelfareDetailApiResponse 추가
├── useCEODashboard.ts에 fetchWelfareDetail 추가
├── transformers.ts에 transformWelfareDetailResponse 추가
└── welfareConfigs.ts를 API 응답 기반으로 수정
```
---
## 4. 핵심 참조 코드 (인라인)
### 4.1 DetailModalConfig 타입 정의
**파일**: `react/src/components/business/CEODashboard/types.ts` (라인 414-426)
```typescript
// 상세 모달 전체 설정 타입
export interface DetailModalConfig {
title: string;
summaryCards: SummaryCardData[];
barChart?: BarChartConfig;
pieChart?: PieChartConfig;
horizontalBarChart?: HorizontalBarChartConfig;
comparisonSection?: ComparisonSectionConfig;
referenceTable?: ReferenceTableConfig;
referenceTables?: ReferenceTableConfig[];
calculationCards?: CalculationCardsConfig;
quarterlyTable?: QuarterlyTableConfig;
table?: TableConfig;
}
```
### 4.2 관련 서브 타입 정의
```typescript
// 요약 카드 타입 (라인 249-255)
export interface SummaryCardData {
label: string;
value: string | number;
isComparison?: boolean;
isPositive?: boolean;
unit?: string;
}
// 막대 차트 설정 타입 (라인 265-271)
export interface BarChartConfig {
title: string;
data: BarChartDataItem[];
dataKey: string;
xAxisKey: string;
color?: string;
}
// 도넛 차트 설정 타입 (라인 282-285)
export interface PieChartConfig {
title: string;
data: PieChartDataItem[];
}
// 도넛 차트 데이터 아이템 (라인 274-279)
export interface PieChartDataItem {
name: string;
value: number;
percentage: number;
color: string;
}
// 테이블 설정 타입 (라인 332-342)
export interface TableConfig {
title: string;
columns: TableColumnConfig[];
data: Record<string, unknown>[];
filters?: TableFilterConfig[];
showTotal?: boolean;
totalLabel?: string;
totalValue?: string | number;
totalColumnKey?: string;
footerSummary?: FooterSummaryItem[];
}
// 계산 카드 섹션 설정 타입 (라인 391-395)
export interface CalculationCardsConfig {
title: string;
subtitle?: string;
cards: CalculationCardItem[];
}
// 계산 카드 아이템 타입 (라인 383-388)
export interface CalculationCardItem {
label: string;
value: number;
unit?: string;
operator?: '+' | '=' | '-' | '×';
}
// 분기별 테이블 설정 타입 (라인 408-411)
export interface QuarterlyTableConfig {
title: string;
rows: QuarterlyTableRow[];
}
// 분기별 테이블 행 타입 (라인 398-405)
export interface QuarterlyTableRow {
label: string;
q1?: number | string;
q2?: number | string;
q3?: number | string;
q4?: number | string;
total?: number | string;
}
```
### 4.3 현재 Mock 데이터 구조 (welfareConfigs.ts 전체)
**파일**: `react/src/components/business/CEODashboard/modalConfigs/welfareConfigs.ts`
```typescript
import type { DetailModalConfig } from '../types';
export function getWelfareModalConfig(calculationType: 'fixed' | 'ratio'): DetailModalConfig {
// 계산 방식에 따른 조건부 calculationCards 생성
const calculationCards = calculationType === 'fixed'
? {
// 직원당 정액 금액/월 방식
title: '복리후생비 계산',
subtitle: '직원당 정액 금액/월 200,000원',
cards: [
{ label: '직원 수', value: 20, unit: '명' },
{ label: '연간 직원당 월급 금액', value: 2400000, unit: '원', operator: '×' as const },
{ label: '당해년도 복리후생비 총 한도', value: 48000000, unit: '원', operator: '=' as const },
],
}
: {
// 연봉 총액 비율 방식
title: '복리후생비 계산',
subtitle: '연봉 총액 기준 비율 20.5%',
cards: [
{ label: '연봉 총액', value: 1000000000, unit: '원' },
{ label: '비율', value: 20.5, unit: '%', operator: '×' as const },
{ label: '당해년도 복리후생비 총 한도', value: 205000000, unit: '원', operator: '=' as const },
],
};
return {
title: '복리후생비 상세',
// 1. 요약 카드 (8개)
summaryCards: [
// 1행: 당해년도 기준
{ label: '당해년도 복리후생비 계정', value: 3123000, unit: '원' },
{ label: '당해년도 복리후생비 한도', value: 600000, unit: '원' },
{ label: '당해년도 복리후생비 사용', value: 6000000, unit: '원' },
{ label: '당해년도 잔여한도', value: 0, unit: '원' },
// 2행: 분기 기준
{ label: '1사분기 복리후생비 총 한도', value: 3123000, unit: '원' },
{ label: '1사분기 복리후생비 잔여한도', value: 6000000, unit: '원' },
{ label: '1사분기 복리후생비 사용금액', value: 6000000, unit: '원' },
{ label: '1사분기 복리후생비 초과 금액', value: 6000000, unit: '원' },
],
// 2. 월별 사용 추이 (막대 차트)
barChart: {
title: '월별 복리후생비 사용 추이',
data: [
{ name: '1월', value: 1500000 },
{ name: '2월', value: 1800000 },
{ name: '3월', value: 2200000 },
{ name: '4월', value: 1900000 },
{ name: '5월', value: 2100000 },
{ name: '6월', value: 1700000 },
],
dataKey: 'value',
xAxisKey: 'name',
color: '#60A5FA',
},
// 3. 항목별 사용 비율 (도넛 차트)
pieChart: {
title: '항목별 사용 비율',
data: [
{ name: '식비', value: 55000000, percentage: 55, color: '#FBBF24' },
{ name: '건강검진', value: 25000000, percentage: 5, color: '#60A5FA' },
{ name: '경조사비', value: 10000000, percentage: 10, color: '#F87171' },
{ name: '기타', value: 10000000, percentage: 30, color: '#34D399' },
],
},
// 4. 일별 사용 내역 (테이블)
table: {
title: '일별 복리후생비 사용 내역',
columns: [
{ key: 'no', label: 'No.', align: 'center' },
{ key: 'cardName', label: '카드명', align: 'left' },
{ key: 'user', label: '사용자', align: 'center' },
{ key: 'date', label: '사용일자', align: 'center', format: 'date' },
{ key: 'store', label: '가맹점명', align: 'left' },
{ key: 'amount', label: '사용금액', align: 'right', format: 'currency' },
{ key: 'usageType', label: '사용항목', align: 'center' },
],
data: [
{ cardName: '카드명', user: '홍길동', date: '2025-12-12 12:12', store: '가맹점명', amount: 1000000, usageType: '식비' },
{ cardName: '카드명', user: '홍길동', date: '2025-12-12 12:12', store: '가맹점명', amount: 1200000, usageType: '건강검진' },
{ cardName: '카드명', user: '홍길동', date: '2025-12-12 12:12', store: '가맹점명', amount: 1500000, usageType: '경조사비' },
{ cardName: '카드명', user: '홍길동', date: '2025-12-12 12:12', store: '가맹점명', amount: 1300000, usageType: '기타' },
{ cardName: '카드명', user: '홍길동', date: '2025-12-12 12:12', store: '가맹점명', amount: 6000000, usageType: '식비' },
],
filters: [
{
key: 'usageType',
options: [
{ value: 'all', label: '전체' },
{ value: '식비', label: '식비' },
{ value: '건강검진', label: '건강검진' },
{ value: '경조사비', label: '경조사비' },
{ value: '기타', label: '기타' },
],
defaultValue: 'all',
},
{
key: 'sortOrder',
options: [
{ value: 'latest', label: '최신순' },
{ value: 'oldest', label: '등록순' },
{ value: 'amountDesc', label: '금액 높은순' },
{ value: 'amountAsc', label: '금액 낮은순' },
],
defaultValue: 'latest',
},
],
showTotal: true,
totalLabel: '합계',
totalValue: 11000000,
totalColumnKey: 'amount',
},
// 5. 복리후생비 계산 (조건부 - calculationType에 따라)
calculationCards,
// 6. 분기별 현황 테이블
quarterlyTable: {
title: '복리후생비 현황',
rows: [
{ label: '한도금액', q1: 12000000, q2: 12000000, q3: 12000000, q4: 12000000, total: 48000000 },
{ label: '이월금액', q1: 0, q2: '', q3: '', q4: '', total: '' },
{ label: '사용금액', q1: 1000000, q2: '', q3: '', q4: '', total: '' },
{ label: '잔여한도', q1: 11000000, q2: '', q3: '', q4: '', total: '' },
{ label: '초과금액', q1: '', q2: '', q3: '', q4: '', total: '' },
],
},
};
}
```
### 4.4 expense_accounts 테이블 스키마
**파일**: `api/database/migrations/2026_01_21_103734_create_expense_accounts_table.php`
```sql
CREATE TABLE expense_accounts (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '테넌트 ID',
-- 비용 유형
account_type VARCHAR(50) NOT NULL COMMENT '계정 유형: welfare, entertainment, etc.',
sub_type VARCHAR(50) NULL COMMENT '세부 유형: meal, gift, etc.',
-- 비용 정보
expense_date DATE NOT NULL COMMENT '지출일',
amount DECIMAL(15,2) DEFAULT 0 COMMENT '금액',
description VARCHAR(500) NULL COMMENT '비용 내역',
receipt_no VARCHAR(100) NULL COMMENT '증빙번호',
-- 거래처 정보
vendor_id BIGINT UNSIGNED NULL COMMENT '거래처 ID',
vendor_name VARCHAR(200) NULL COMMENT '거래처명 (직접 입력)',
-- 카드/결제 정보
payment_method VARCHAR(50) NULL COMMENT '결제수단: card, cash, transfer',
card_no VARCHAR(50) NULL COMMENT '카드 마지막 4자리',
-- 감사 컬럼
created_by BIGINT UNSIGNED NULL COMMENT '등록자',
updated_by BIGINT UNSIGNED NULL COMMENT '수정자',
deleted_by BIGINT UNSIGNED NULL COMMENT '삭제자',
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL,
deleted_at TIMESTAMP NULL,
-- 인덱스
INDEX idx_tenant_type_date (tenant_id, account_type, expense_date),
INDEX idx_tenant_date (tenant_id, expense_date),
-- 외래키
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
FOREIGN KEY (vendor_id) REFERENCES clients(id) ON DELETE SET NULL
);
```
**account_type 값**:
- `welfare` - 복리후생비
- `entertainment` - 접대비
**sub_type 값** (welfare의 경우):
- `meal` - 식비
- `health_check` - 건강검진
- `congratulation` - 경조사비
- `other` - 기타
---
## 5. API → 모달 설정 변환 매핑
### 5.1 API 응답 스키마 (제안)
```typescript
// 백엔드 API 응답: GET /api/v1/welfare/detail
interface WelfareDetailApiResponse {
// 요약 카드 데이터
summary: {
annual_account: number; // 당해년도 복리후생비 계정
annual_limit: number; // 당해년도 복리후생비 한도
annual_used: number; // 당해년도 복리후생비 사용
annual_remaining: number; // 당해년도 잔여한도
quarterly_limit: number; // 분기 복리후생비 총 한도
quarterly_remaining: number; // 분기 복리후생비 잔여한도
quarterly_used: number; // 분기 복리후생비 사용금액
quarterly_exceeded: number; // 분기 복리후생비 초과 금액
};
// 월별 사용 추이
monthly_usage: {
month: number; // 1-12
amount: number;
}[];
// 항목별 분포
category_distribution: {
category: string; // meal, health_check, congratulation, other
label: string; // 식비, 건강검진, 경조사비, 기타
amount: number;
ratio: number; // 백분율 (0-100)
}[];
// 일별 사용 내역
transactions: {
id: number;
card_name: string;
user_name: string;
expense_date: string; // YYYY-MM-DD HH:mm
vendor_name: string;
amount: number;
sub_type: string;
sub_type_label: string;
}[];
// 계산 정보
calculation: {
type: 'fixed' | 'ratio';
employee_count: number;
monthly_amount?: number; // fixed 방식
total_salary?: number; // ratio 방식
ratio?: number; // ratio 방식 (%)
annual_limit: number;
};
// 분기별 현황
quarterly: {
quarter: number; // 1-4
limit: number;
carryover: number;
used: number;
remaining: number;
exceeded: number;
}[];
}
```
### 5.2 변환 매핑 테이블
| API 필드 | DetailModalConfig 필드 | 변환 로직 |
|----------|----------------------|----------|
| `summary.annual_account` | `summaryCards[0].value` | 직접 매핑 |
| `summary.annual_limit` | `summaryCards[1].value` | 직접 매핑 |
| `summary.annual_used` | `summaryCards[2].value` | 직접 매핑 |
| `summary.annual_remaining` | `summaryCards[3].value` | 직접 매핑 |
| `summary.quarterly_limit` | `summaryCards[4].value` | 라벨에 분기 동적 삽입 |
| `summary.quarterly_remaining` | `summaryCards[5].value` | 라벨에 분기 동적 삽입 |
| `summary.quarterly_used` | `summaryCards[6].value` | 라벨에 분기 동적 삽입 |
| `summary.quarterly_exceeded` | `summaryCards[7].value` | 라벨에 분기 동적 삽입 |
| `monthly_usage[]` | `barChart.data[]` | `{ name: '${month}월', value: amount }` |
| `category_distribution[]` | `pieChart.data[]` | 색상 매핑 추가 필요 |
| `transactions[]` | `table.data[]` | 필드명 camelCase 변환 |
| `calculation` | `calculationCards` | type에 따라 분기 |
| `quarterly[]` | `quarterlyTable.rows[]` | 행/열 피벗 변환 |
### 5.3 색상 매핑 (카테고리별)
```typescript
const CATEGORY_COLORS: Record<string, string> = {
meal: '#FBBF24', // 식비 - 노란색
health_check: '#60A5FA', // 건강검진 - 파란색
congratulation: '#F87171', // 경조사비 - 빨간색
other: '#34D399', // 기타 - 초록색
};
```
---
## 6. 상세 작업 내용
### 6.1 Phase 1: API 개발
#### 1.1 WelfareService 확장
**파일**: `api/app/Services/WelfareService.php`
**추가할 메서드**:
```php
/**
* 복리후생비 상세 정보 조회 (모달용)
*/
public function getDetail(
?string $calculationType = 'fixed',
?int $fixedAmountPerMonth = 200000,
?float $ratio = 0.05,
?int $year = null,
?int $quarter = null
): array {
// 1. 요약 데이터 조회
// 2. 월별 사용 추이 조회
// 3. 항목별 분포 조회
// 4. 일별 사용 내역 조회
// 5. 계산 정보 생성
// 6. 분기별 현황 조회
}
```
**필요한 쿼리**:
```php
// 월별 사용 추이
DB::table('expense_accounts')
->select(DB::raw('MONTH(expense_date) as month'), DB::raw('SUM(amount) as amount'))
->where('tenant_id', $tenantId)
->where('account_type', 'welfare')
->whereYear('expense_date', $year)
->whereNull('deleted_at')
->groupBy(DB::raw('MONTH(expense_date)'))
->orderBy('month')
->get();
// 항목별 분포
DB::table('expense_accounts')
->select('sub_type', DB::raw('SUM(amount) as amount'))
->where('tenant_id', $tenantId)
->where('account_type', 'welfare')
->whereBetween('expense_date', [$startDate, $endDate])
->whereNull('deleted_at')
->groupBy('sub_type')
->get();
// 일별 사용 내역
DB::table('expense_accounts')
->select('id', 'card_no', 'created_by', 'expense_date', 'vendor_name', 'amount', 'sub_type')
->where('tenant_id', $tenantId)
->where('account_type', 'welfare')
->whereBetween('expense_date', [$startDate, $endDate])
->whereNull('deleted_at')
->orderByDesc('expense_date')
->get();
```
#### 1.2 WelfareController 확장
**파일**: `api/app/Http/Controllers/Api/V1/WelfareController.php`
**추가할 메서드**:
```php
/**
* 복리후생비 상세 조회 (모달용)
*/
public function detail(Request $request): JsonResponse
{
$calculationType = $request->query('calculation_type', 'fixed');
$fixedAmountPerMonth = $request->query('fixed_amount_per_month')
? (int) $request->query('fixed_amount_per_month')
: 200000;
$ratio = $request->query('ratio')
? (float) $request->query('ratio')
: 0.05;
$year = $request->query('year') ? (int) $request->query('year') : null;
$quarter = $request->query('quarter') ? (int) $request->query('quarter') : null;
return ApiResponse::handle(function () use ($calculationType, $fixedAmountPerMonth, $ratio, $year, $quarter) {
return $this->welfareService->getDetail(
$calculationType,
$fixedAmountPerMonth,
$ratio,
$year,
$quarter
);
}, __('message.fetched'));
}
```
#### 1.3 라우트 등록
**파일**: `api/routes/api.php`
```php
Route::prefix('welfare')->group(function () {
Route::get('/summary', [WelfareController::class, 'summary']);
Route::get('/detail', [WelfareController::class, 'detail']); // 추가
});
```
### 6.2 Phase 2: 프론트엔드 연동
#### 2.1 타입 정의 추가
**파일**: `react/src/lib/api/dashboard/types.ts`
```typescript
// Welfare Detail API 응답 타입
export interface WelfareDetailApiResponse {
summary: {
annual_account: number;
annual_limit: number;
annual_used: number;
annual_remaining: number;
quarterly_limit: number;
quarterly_remaining: number;
quarterly_used: number;
quarterly_exceeded: number;
};
monthly_usage: {
month: number;
amount: number;
}[];
category_distribution: {
category: string;
label: string;
amount: number;
ratio: number;
}[];
transactions: {
id: number;
card_name: string;
user_name: string;
expense_date: string;
vendor_name: string;
amount: number;
sub_type: string;
sub_type_label: string;
}[];
calculation: {
type: 'fixed' | 'ratio';
employee_count: number;
monthly_amount?: number;
total_salary?: number;
ratio?: number;
annual_limit: number;
};
quarterly: {
quarter: number;
limit: number;
carryover: number;
used: number;
remaining: number;
exceeded: number;
}[];
}
```
#### 2.2 API 함수 추가
**파일**: `react/src/hooks/useCEODashboard.ts`
```typescript
export async function fetchWelfareDetail(
options: {
calculationType?: 'fixed' | 'ratio';
fixedAmountPerMonth?: number;
ratio?: number;
year?: number;
quarter?: number;
}
): Promise<WelfareDetailApiResponse> {
const params = new URLSearchParams();
if (options.calculationType) params.append('calculation_type', options.calculationType);
if (options.fixedAmountPerMonth) params.append('fixed_amount_per_month', options.fixedAmountPerMonth.toString());
if (options.ratio) params.append('ratio', options.ratio.toString());
if (options.year) params.append('year', options.year.toString());
if (options.quarter) params.append('quarter', options.quarter.toString());
return fetchApi<WelfareDetailApiResponse>(`welfare/detail?${params.toString()}`);
}
```
#### 2.3 Transformer 추가
**파일**: `react/src/lib/api/dashboard/transformers.ts`
```typescript
const CATEGORY_COLORS: Record<string, string> = {
meal: '#FBBF24',
health_check: '#60A5FA',
congratulation: '#F87171',
other: '#34D399',
};
export function transformWelfareDetailToModalConfig(
api: WelfareDetailApiResponse,
quarter: number
): DetailModalConfig {
const quarterLabel = `${quarter}사분기`;
return {
title: '복리후생비 상세',
summaryCards: [
{ label: '당해년도 복리후생비 계정', value: api.summary.annual_account, unit: '원' },
{ label: '당해년도 복리후생비 한도', value: api.summary.annual_limit, unit: '원' },
{ label: '당해년도 복리후생비 사용', value: api.summary.annual_used, unit: '원' },
{ label: '당해년도 잔여한도', value: api.summary.annual_remaining, unit: '원' },
{ label: `${quarterLabel} 복리후생비 총 한도`, value: api.summary.quarterly_limit, unit: '원' },
{ label: `${quarterLabel} 복리후생비 잔여한도`, value: api.summary.quarterly_remaining, unit: '원' },
{ label: `${quarterLabel} 복리후생비 사용금액`, value: api.summary.quarterly_used, unit: '원' },
{ label: `${quarterLabel} 복리후생비 초과 금액`, value: api.summary.quarterly_exceeded, unit: '원' },
],
barChart: {
title: '월별 복리후생비 사용 추이',
data: api.monthly_usage.map(m => ({ name: `${m.month}월`, value: m.amount })),
dataKey: 'value',
xAxisKey: 'name',
color: '#60A5FA',
},
pieChart: {
title: '항목별 사용 비율',
data: api.category_distribution.map(c => ({
name: c.label,
value: c.amount,
percentage: c.ratio,
color: CATEGORY_COLORS[c.category] || '#9CA3AF',
})),
},
table: {
title: '일별 복리후생비 사용 내역',
columns: [
{ key: 'no', label: 'No.', align: 'center' },
{ key: 'cardName', label: '카드명', align: 'left' },
{ key: 'user', label: '사용자', align: 'center' },
{ key: 'date', label: '사용일자', align: 'center', format: 'date' },
{ key: 'store', label: '가맹점명', align: 'left' },
{ key: 'amount', label: '사용금액', align: 'right', format: 'currency' },
{ key: 'usageType', label: '사용항목', align: 'center' },
],
data: api.transactions.map((t, i) => ({
no: i + 1,
cardName: t.card_name,
user: t.user_name,
date: t.expense_date,
store: t.vendor_name,
amount: t.amount,
usageType: t.sub_type_label,
})),
filters: [
{
key: 'usageType',
options: [
{ value: 'all', label: '전체' },
{ value: '식비', label: '식비' },
{ value: '건강검진', label: '건강검진' },
{ value: '경조사비', label: '경조사비' },
{ value: '기타', label: '기타' },
],
defaultValue: 'all',
},
{
key: 'sortOrder',
options: [
{ value: 'latest', label: '최신순' },
{ value: 'oldest', label: '등록순' },
{ value: 'amountDesc', label: '금액 높은순' },
{ value: 'amountAsc', label: '금액 낮은순' },
],
defaultValue: 'latest',
},
],
showTotal: true,
totalLabel: '합계',
totalValue: api.transactions.reduce((sum, t) => sum + t.amount, 0),
totalColumnKey: 'amount',
},
calculationCards: api.calculation.type === 'fixed'
? {
title: '복리후생비 계산',
subtitle: `직원당 정액 금액/월 ${(api.calculation.monthly_amount || 0).toLocaleString()}원`,
cards: [
{ label: '직원 수', value: api.calculation.employee_count, unit: '명' },
{ label: '연간 직원당 월급 금액', value: (api.calculation.monthly_amount || 0) * 12, unit: '원', operator: '×' },
{ label: '당해년도 복리후생비 총 한도', value: api.calculation.annual_limit, unit: '원', operator: '=' },
],
}
: {
title: '복리후생비 계산',
subtitle: `연봉 총액 기준 비율 ${api.calculation.ratio}%`,
cards: [
{ label: '연봉 총액', value: api.calculation.total_salary || 0, unit: '원' },
{ label: '비율', value: api.calculation.ratio || 0, unit: '%', operator: '×' },
{ label: '당해년도 복리후생비 총 한도', value: api.calculation.annual_limit, unit: '원', operator: '=' },
],
},
quarterlyTable: {
title: '복리후생비 현황',
rows: [
{
label: '한도금액',
q1: api.quarterly.find(q => q.quarter === 1)?.limit || '',
q2: api.quarterly.find(q => q.quarter === 2)?.limit || '',
q3: api.quarterly.find(q => q.quarter === 3)?.limit || '',
q4: api.quarterly.find(q => q.quarter === 4)?.limit || '',
total: api.quarterly.reduce((sum, q) => sum + q.limit, 0),
},
{
label: '이월금액',
q1: api.quarterly.find(q => q.quarter === 1)?.carryover || '',
q2: api.quarterly.find(q => q.quarter === 2)?.carryover || '',
q3: api.quarterly.find(q => q.quarter === 3)?.carryover || '',
q4: api.quarterly.find(q => q.quarter === 4)?.carryover || '',
total: '',
},
{
label: '사용금액',
q1: api.quarterly.find(q => q.quarter === 1)?.used || '',
q2: api.quarterly.find(q => q.quarter === 2)?.used || '',
q3: api.quarterly.find(q => q.quarter === 3)?.used || '',
q4: api.quarterly.find(q => q.quarter === 4)?.used || '',
total: api.quarterly.reduce((sum, q) => sum + q.used, 0),
},
{
label: '잔여한도',
q1: api.quarterly.find(q => q.quarter === 1)?.remaining || '',
q2: api.quarterly.find(q => q.quarter === 2)?.remaining || '',
q3: api.quarterly.find(q => q.quarter === 3)?.remaining || '',
q4: api.quarterly.find(q => q.quarter === 4)?.remaining || '',
total: '',
},
{
label: '초과금액',
q1: api.quarterly.find(q => q.quarter === 1)?.exceeded || '',
q2: api.quarterly.find(q => q.quarter === 2)?.exceeded || '',
q3: api.quarterly.find(q => q.quarter === 3)?.exceeded || '',
q4: api.quarterly.find(q => q.quarter === 4)?.exceeded || '',
total: '',
},
],
},
};
}
```
#### 2.4 모달 설정 동적 생성
**파일**: `react/src/components/business/CEODashboard/modalConfigs/welfareConfigs.ts`
```typescript
import type { DetailModalConfig } from '../types';
import { fetchWelfareDetail, transformWelfareDetailToModalConfig } from '@/lib/api/dashboard';
// 기존 Mock 함수 (fallback용)
export function getWelfareModalConfigMock(calculationType: 'fixed' | 'ratio'): DetailModalConfig {
// ... 기존 Mock 코드 유지
}
// 새로운 API 기반 함수
export async function getWelfareModalConfigFromApi(
options: {
calculationType: 'fixed' | 'ratio';
fixedAmountPerMonth?: number;
ratio?: number;
year?: number;
quarter?: number;
}
): Promise<DetailModalConfig> {
try {
const apiData = await fetchWelfareDetail(options);
return transformWelfareDetailToModalConfig(apiData, options.quarter || getCurrentQuarter());
} catch (error) {
console.error('[Welfare] Failed to fetch detail, using mock data:', error);
return getWelfareModalConfigMock(options.calculationType);
}
}
function getCurrentQuarter(): number {
return Math.ceil((new Date().getMonth() + 1) / 3);
}
```
---
## 7. 컨펌 대기 목록
| # | 항목 | 변경 내용 | 영향 범위 | 상태 |
|---|------|----------|----------|------|
| 1 | 새 API 엔드포인트 | `GET /api/v1/welfare/detail` 추가 | api | ✅ 완료 |
| 2 | WelfareService 확장 | getDetail() 메서드 추가 | api | ✅ 완료 |
---
## 8. 변경 이력
| 날짜 | 항목 | 변경 내용 | 파일 | 승인 |
|------|------|----------|------|------|
| 2026-01-22 | - | 문서 초안 작성 | - | - |
| 2026-01-22 | - | 자기완결성 보완 (타입, 스키마, 매핑 추가) | - | - |
| 2026-01-22 | Phase 1.1 | getDetail() 메서드 추가 | WelfareService.php | ✅ |
| 2026-01-22 | Phase 1.1 | detail() 액션 추가 | WelfareController.php | ✅ |
| 2026-01-22 | Phase 1.1 | /welfare/detail 라우트 추가 | routes/api.php | ✅ |
| 2026-01-22 | Phase 1.2 | Swagger 스키마 및 엔드포인트 추가 | WelfareApi.php | ✅ |
| 2026-01-22 | Phase 2.1 | WelfareDetailApiResponse 타입 추가 | types.ts | ✅ |
| 2026-01-22 | Phase 2.2 | useWelfareDetail hook 추가 | useCEODashboard.ts | ✅ |
| 2026-01-22 | Phase 2.3 | transformWelfareDetailResponse 추가 | transformers.ts | ✅ |
| 2026-01-22 | Phase 2.4 | 모달 설정 API 연동 + fallback | CEODashboard.tsx, welfareConfigs.ts | ✅ |
---
## 9. 참고 문서
- **빠른 시작**: `docs/quickstart/quick-start.md`
- **품질 체크리스트**: `docs/standards/quality-checklist.md`
- **API 규칙**: `api/CLAUDE.md` (SAM API Development Rules)
- **Swagger 가이드**: `docs/guides/swagger-guide.md`
---
## 10. 세션 및 메모리 관리 정책 (Serena Optimized)
### 10.1 세션 시작 시 (Load Strategy)
```javascript
// 순차적 로드
read_memory("welfare-section-state") // 1. 상태 파악
read_memory("welfare-section-snapshot") // 2. 사고 흐름 복구
```
### 10.2 작업 중 관리 (Context Defense)
| 컨텍스트 잔량 | Action | 내용 |
|--------------|--------|------|
| **30% 이하** | 🛠 **Snapshot** | `write_memory("welfare-section-snapshot", "코드변경+논의요약")` |
| **20% 이하** | 🧹 **Context Purge** | `write_memory("welfare-section-active-symbols", "주요 수정 파일/함수")` |
| **10% 이하** | 🛑 **Stop & Save** | 최종 상태 저장 후 세션 교체 권고 |
### 10.3 Serena 메모리 구조
- `welfare-section-state`: { phase, progress, next_step, last_decision }
- `welfare-section-snapshot`: 현재까지의 논의 및 코드 변경점 요약
- `welfare-section-active-symbols`: 현재 수정 중인 파일/심볼 리스트
---
## 11. 검증 결과
> 작업 완료 후 이 섹션에 검증 결과 추가
### 11.1 테스트 케이스
| 입력값 | 예상 결과 | 실제 결과 | 상태 |
|--------|----------|----------|------|
| 모달 클릭 (fixed) | 정액 방식 계산 표시 | - | ⏳ |
| 모달 클릭 (ratio) | 비율 방식 계산 표시 | - | ⏳ |
| 분기 변경 (Q1→Q2) | 해당 분기 데이터 표시 | - | ⏳ |
### 11.2 성공 기준 달성 현황
| 기준 | 달성 | 비고 |
|------|------|------|
| 4개 카드 데이터 실시간 반영 | ✅ | API 연동 완료 상태 |
| 모달 상세 데이터 API 연동 | ✅ | Backend API + Frontend hook 완료 |
| Mock 데이터 제거 | ✅ | API 우선, Mock fallback 유지 |
---
## 12. 자기완결성 점검 결과
### 12.1 체크리스트 검증
| # | 검증 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 1 | 작업 목적이 명확한가? | ✅ | 모달 데이터 API 연동 |
| 2 | 성공 기준이 정의되어 있는가? | ✅ | 11.2 참조 |
| 3 | 작업 범위가 구체적인가? | ✅ | 2. 대상 범위 참조 |
| 4 | 의존성이 명시되어 있는가? | ✅ | Phase 1 → Phase 2 순서 |
| 5 | 참고 파일 경로가 정확한가? | ✅ | 4. 핵심 참조 코드 인라인 |
| 6 | 단계별 절차가 실행 가능한가? | ✅ | 6. 상세 작업 내용 |
| 7 | 검증 방법이 명시되어 있는가? | ✅ | 11.1 테스트 케이스 |
| 8 | 모호한 표현이 없는가? | ✅ | 구체적 코드 스니펫 포함 |
### 12.2 새 세션 시뮬레이션 테스트
| 질문 | 답변 가능 | 참조 섹션 |
|------|:--------:|----------|
| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 |
| Q2. 어디서부터 시작해야 하는가? | ✅ | 3.1 단계별 절차 |
| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 6. 상세 작업 내용 |
| Q4. DetailModalConfig 구조는? | ✅ | 4.1, 4.2 타입 정의 |
| Q5. Mock 데이터 구조는? | ✅ | 4.3 현재 Mock 데이터 |
| Q6. DB 테이블 스키마는? | ✅ | 4.4 expense_accounts |
| Q7. API → 모달 변환 방법은? | ✅ | 5. 변환 매핑 |
| Q8. 작업 완료 확인 방법은? | ✅ | 11. 검증 결과 |
| Q9. 막혔을 때 참고 문서는? | ✅ | 9. 참고 문서 |
**결과**: 9/9 통과 → ✅ 자기완결성 확보
### 12.3 보완 이력
| 날짜 | 항목 | 원본 | 보완 내용 |
|------|------|------|----------|
| 2026-01-22 | DetailModalConfig | 없음 | 타입 정의 전체 인라인 |
| 2026-01-22 | Mock 데이터 | 없음 | welfareConfigs.ts 전체 인라인 |
| 2026-01-22 | DB 스키마 | 없음 | expense_accounts 테이블 구조 |
| 2026-01-22 | 변환 매핑 | 없음 | API → 모달 매핑 테이블 및 코드 |
---
*이 문서는 /sc:plan 스킬로 생성되었습니다.*