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