# 복리후생비 현황 섹션 개발 계획 > **작성일**: 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 스킬로 생성되었습니다.*