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

35 KiB
Raw Permalink Blame History

복리후생비 현황 섹션 개발 계획

작성일: 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)

// 상세 모달 전체 설정 타입
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 관련 서브 타입 정의

// 요약 카드 타입 (라인 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

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

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 응답 스키마 (제안)

// 백엔드 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 색상 매핑 (카테고리별)

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

추가할 메서드:

/**
 * 복리후생비 상세 정보 조회 (모달용)
 */
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. 분기별 현황 조회
}

필요한 쿼리:

// 월별 사용 추이
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

추가할 메서드:

/**
 * 복리후생비 상세 조회 (모달용)
 */
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

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

// 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

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

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

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)

// 순차적 로드
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 스킬로 생성되었습니다.