diff --git a/plans/welfare-section-plan.md b/plans/welfare-section-plan.md new file mode 100644 index 0000000..94541ed --- /dev/null +++ b/plans/welfare-section-plan.md @@ -0,0 +1,1021 @@ +# 복리후생비 현황 섹션 개발 계획 + +> **작성일**: 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[]; + 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 = { + 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 { + 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(`welfare/detail?${params.toString()}`); +} +``` + +#### 2.3 Transformer 추가 + +**파일**: `react/src/lib/api/dashboard/transformers.ts` + +```typescript +const CATEGORY_COLORS: Record = { + 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 { + 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 스킬로 생성되었습니다.* \ No newline at end of file