Files
sam-docs/plans/react-mock-to-api-migration-plan.md
hskwon 9b665c0d5a docs: 플로우 테스트 파일 이동 및 계획 문서 추가
- api에서 플로우 테스트 JSON 파일들 이동
- 더미 데이터 시딩 계획 추가
- 견적 자동 계산 개발 계획 추가
- 기존 계획 문서 업데이트
2025-12-24 08:54:52 +09:00

29 KiB

React 목업 데이터 → API 연동 마이그레이션 계획

작성일: 2025-12-23 목적: React 프론트엔드의 목업 데이터를 실제 API와 연동 참고 문서: react-api-integration-plan.md, erp-api-development-plan-d1.0-changes.md 참조 구현: 단가관리 (/sales/pricing-management)


1. 개요

1.1 현황 분석

목업 데이터 사용 페이지: 66개 파일에서 목업 데이터 사용 중

┌─────────────────────────────────────────────────────────────────┐
│  🎯 연동 목표                                                    │
├─────────────────────────────────────────────────────────────────┤
│  - React 목업 데이터 → 실제 API 호출로 전환                       │
│  - 단가관리(pricing-management) 패턴을 표준으로 적용              │
│  - Phase 5~8 API 완료 기능 우선 연동                             │
└─────────────────────────────────────────────────────────────────┘

1.2 단가관리 연동 패턴 (표준 참조)

┌─────────────────────────────────────────────────────────────────┐
│  📂 파일 구조                                                    │
├─────────────────────────────────────────────────────────────────┤
│  react/src/                                                     │
│  ├── app/[locale]/(protected)/sales/pricing-management/         │
│  │   └── page.tsx           ← 서버 컴포넌트 (API 호출)           │
│  │                                                              │
│  └── components/pricing/                                        │
│      ├── types.ts           ← 타입 정의                          │
│      ├── actions.ts         ← Server Actions (CRUD)             │
│      ├── PricingListClient.tsx  ← 클라이언트 컴포넌트            │
│      └── index.ts           ← Export                            │
└─────────────────────────────────────────────────────────────────┘

데이터 흐름:

page.tsx (서버)
  ↓ API 호출 (fetch)
  ↓ 데이터 변환 (API → Frontend)
  ↓ initialData prop 전달
ListClient.tsx (클라이언트)
  ↓ useState로 데이터 관리
  ↓ UI 렌더링

1.3 핵심 패턴 요약

구분 파일 역할
타입 types.ts 프론트엔드 인터페이스 정의
서버 페이지 page.tsx API 호출, 데이터 병합, 초기 데이터 전달
서버 액션 actions.ts CRUD 작업, 데이터 변환 함수
클라이언트 *Client.tsx UI 렌더링, 사용자 상호작용

2. 우선순위별 연동 대상

2.1 Phase A: API 완료 기능 (즉시 연동 가능)

이미 API가 구현되어 있어 React 연동만 필요

# 페이지 React 경로 API 엔드포인트 상태
A-1 악성채권 관리 /accounting/bad-debt-collection GET/POST /v1/bad-debts 완료
A-2 팝업 관리 /settings/popup-management GET/POST /v1/popups 완료
A-3 결제 내역 /settings/payment-history GET /v1/payments 완료
A-4 구독 관리 /settings/subscription GET /v1/subscriptions 완료
A-5 알림 설정 /settings/notifications GET/PUT /v1/settings/notifications 완료
A-6 거래처 원장 /accounting/vendor-ledger GET /v1/vendor-ledger ⏭️ API 미존재

2.2 Phase B: 핵심 업무 기능

# 페이지 React 경로 API 엔드포인트 상태
B-1 매출 관리 /accounting/sales GET/POST /v1/sales 완료 (기존 연동)
B-2 매입 관리 /accounting/purchase GET/POST /v1/purchases 완료 (기존 연동)
B-3 입금 관리 /accounting/deposit GET/POST /v1/deposits 완료
B-4 출금 관리 /accounting/withdrawal GET/POST /v1/withdrawals 완료
B-5 거래처 관리 /accounting/vendor GET/POST /v1/clients 완료
B-6 어음 관리 /accounting/bills GET/POST /v1/bills 완료

2.3 Phase C: 인사/근태

# 페이지 React 경로 API 엔드포인트 상태
C-1 직원 관리 /hr/employees GET/POST /v1/employees 대기
C-2 근태 관리 /hr/attendance GET/POST /v1/attendances 대기
C-3 휴가 관리 /hr/vacation GET/POST /v1/vacations 대기
C-4 부서 관리 /hr/departments GET/POST /v1/departments 대기

2.4 Phase D: 게시판/고객센터 (후순위)

# 페이지 React 경로 API 엔드포인트 상태
D-1 게시판 관리 /settings/boards GET/POST /v1/boards ⏭️ 후순위
D-2 공지사항 /customer-center/notices GET/POST /v1/notices ⏭️ 후순위
D-3 문의 관리 /customer-center/inquiries GET/POST /v1/inquiries ⏭️ 후순위
D-4 FAQ 관리 /customer-center/faq GET/POST /v1/faqs ⏭️ 후순위

3. 연동 작업 가이드

3.1 표준 연동 절차

Step 1: 현재 상태 분석
├── 목업 데이터 구조 확인 (types.ts)
├── 클라이언트 컴포넌트 props 확인 (*Client.tsx)
└── API 스펙 확인 (Swagger: sam.kr/api-docs)

Step 2: 타입 정의 정비
├── API 응답 타입 추가 (xxxApiData)
├── 변환 함수 타입 정의
└── 기존 프론트엔드 타입 유지

Step 3: 서버 컴포넌트 수정 (page.tsx)
├── API 호출 함수 구현
├── 데이터 변환 함수 구현
└── initialData prop 전달

Step 4: 서버 액션 구현 (actions.ts)
├── CRUD 함수 구현 (create, update, delete)
├── transformApiToFrontend() 함수
└── transformFrontendToApi() 함수

Step 5: 클라이언트 컴포넌트 연동
├── Server Actions import
├── 핸들러 함수에서 Server Actions 호출
└── 로딩/에러 상태 처리

3.2 체크리스트

□ API 엔드포인트 확인 (Swagger)
□ 응답 데이터 구조 확인
□ 타입 정의 (API 응답 타입 + 프론트엔드 타입)
□ 변환 함수 구현 (API ↔ Frontend)
□ 서버 컴포넌트에서 API 호출
□ 클라이언트 컴포넌트에 initialData 전달
□ Server Actions 구현 (CRUD)
□ 에러 처리 (try-catch, 사용자 알림)
□ 로딩 상태 처리
□ 브라우저 테스트

4. 상세 구현 가이드

4.1 page.tsx 패턴

// app/[locale]/(protected)/[feature]/page.tsx

import { cookies } from 'next/headers';
import { FeatureListClient } from '@/components/feature';

// API 응답 타입
interface ApiData {
  id: number;
  // ... API 필드
}

// API 헤더 생성
async function getApiHeaders(): Promise<HeadersInit> {
  const cookieStore = await cookies();
  const token = cookieStore.get('access_token')?.value;

  return {
    'Accept': 'application/json',
    'Authorization': token ? `Bearer ${token}` : '',
    'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '',
  };
}

// API 호출
async function getList(): Promise<ApiData[]> {
  try {
    const headers = await getApiHeaders();
    const response = await fetch(
      `${process.env.NEXT_PUBLIC_API_URL}/api/v1/feature`,
      { method: 'GET', headers, cache: 'no-store' }
    );

    if (!response.ok) return [];
    const result = await response.json();
    return result.success ? result.data.data : [];
  } catch (error) {
    console.error('[FeaturePage] Fetch error:', error);
    return [];
  }
}

// 데이터 변환
function transformToFrontend(apiData: ApiData[]): FeatureListItem[] {
  return apiData.map(item => ({
    id: String(item.id),
    // ... 필드 매핑
  }));
}

// 페이지 컴포넌트
export default async function FeaturePage() {
  const apiData = await getList();
  const data = transformToFrontend(apiData);

  return <FeatureListClient initialData={data} />;
}

4.2 actions.ts 패턴

// components/feature/actions.ts

'use server';

import { cookies } from 'next/headers';
import type { FeatureData } from './types';

async function getApiHeaders(): Promise<HeadersInit> {
  const cookieStore = await cookies();
  const token = cookieStore.get('access_token')?.value;

  return {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Authorization': token ? `Bearer ${token}` : '',
    'X-API-KEY': process.env.API_KEY || '',
  };
}

// API → Frontend 변환
function transformApiToFrontend(apiData: ApiData): FeatureData {
  return {
    id: String(apiData.id),
    // ... 필드 매핑
  };
}

// Frontend → API 변환
function transformFrontendToApi(data: FeatureData): Record<string, unknown> {
  return {
    // ... 필드 매핑 (snake_case)
  };
}

// 등록
export async function createFeature(
  data: FeatureData
): Promise<{ success: boolean; data?: FeatureData; error?: string }> {
  try {
    const headers = await getApiHeaders();
    const apiData = transformFrontendToApi(data);

    const response = await fetch(
      `${process.env.NEXT_PUBLIC_API_URL}/api/v1/feature`,
      {
        method: 'POST',
        headers,
        body: JSON.stringify(apiData),
      }
    );

    const result = await response.json();

    if (!response.ok || !result.success) {
      return { success: false, error: result.message };
    }

    return { success: true, data: transformApiToFrontend(result.data) };
  } catch (error) {
    return { success: false, error: '서버 오류가 발생했습니다.' };
  }
}

// 수정
export async function updateFeature(
  id: string,
  data: FeatureData
): Promise<{ success: boolean; data?: FeatureData; error?: string }> {
  // ... 유사 패턴
}

// 삭제
export async function deleteFeature(
  id: string
): Promise<{ success: boolean; error?: string }> {
  // ... 유사 패턴
}

4.3 ListClient.tsx 연동 패턴

// components/feature/FeatureListClient.tsx

'use client';

import { useState } from 'react';
import { createFeature, updateFeature, deleteFeature } from './actions';
import { toast } from 'sonner';

interface FeatureListClientProps {
  initialData: FeatureListItem[];
}

export function FeatureListClient({ initialData }: FeatureListClientProps) {
  const [data, setData] = useState<FeatureListItem[]>(initialData);
  const [isLoading, setIsLoading] = useState(false);

  const handleCreate = async (formData: FeatureData) => {
    setIsLoading(true);
    const result = await createFeature(formData);

    if (result.success && result.data) {
      setData(prev => [...prev, result.data!]);
      toast.success('등록되었습니다.');
    } else {
      toast.error(result.error || '등록에 실패했습니다.');
    }
    setIsLoading(false);
  };

  const handleUpdate = async (id: string, formData: FeatureData) => {
    setIsLoading(true);
    const result = await updateFeature(id, formData);

    if (result.success && result.data) {
      setData(prev => prev.map(item =>
        item.id === id ? result.data! : item
      ));
      toast.success('수정되었습니다.');
    } else {
      toast.error(result.error || '수정에 실패했습니다.');
    }
    setIsLoading(false);
  };

  const handleDelete = async (id: string) => {
    setIsLoading(true);
    const result = await deleteFeature(id);

    if (result.success) {
      setData(prev => prev.filter(item => item.id !== id));
      toast.success('삭제되었습니다.');
    } else {
      toast.error(result.error || '삭제에 실패했습니다.');
    }
    setIsLoading(false);
  };

  // ... 나머지 UI 렌더링
}

5. 필드 매핑 규칙

5.1 네이밍 컨벤션

API (snake_case) Frontend (camelCase)
created_at createdAt
updated_at updatedAt
item_type itemType
purchase_price purchasePrice
sales_price salesPrice
is_active isActive

5.2 타입 변환

API 타입 Frontend 타입 변환
number (id) string String(id)
string (decimal) number parseFloat(value)
string (date) string 그대로 또는 포맷팅
null undefined value ?? undefined

5.3 상태 매핑 예시

// API 상태 → Frontend 상태
function mapStatus(apiStatus: string): FrontendStatus {
  switch (apiStatus) {
    case 'draft': return 'draft';
    case 'active': return 'active';
    case 'finalized': return 'finalized';
    default: return 'draft';
  }
}

6. Phase B 상세 필드 매핑

이 섹션은 신규 세션에서 바로 개발 가능하도록 상세 스펙을 포함합니다.

6.1 매출 관리 (Sales)

API FormRequest 필드 (api/app/Http/Requests/V1/Sale/StoreSaleRequest.php):

'sale_date' => ['required', 'date'],
'client_id' => ['required', 'integer', 'exists:clients,id'],
'supply_amount' => ['required', 'numeric', 'min:0'],
'tax_amount' => ['required', 'numeric', 'min:0'],
'total_amount' => ['required', 'numeric', 'min:0'],
'description' => ['nullable', 'string', 'max:1000'],
'deposit_id' => ['nullable', 'integer', 'exists:deposits,id'],

React 인터페이스 (react/src/components/accounting/SalesManagement/types.ts):

interface SalesRecord {
  id: string;
  salesNo: string;           // 매출번호
  salesDate: string;         // 매출일
  vendorId: string;          // 거래처 ID
  vendorName: string;        // 거래처명
  salesType: SalesType;      // 매출 유형
  accountSubject: string;    // 계정과목
  items: SalesItem[];        // 품목 목록
  totalSupplyAmount: number; // 공급가액 합계
  totalVat: number;          // 부가세 합계
  totalAmount: number;       // 총 금액
  receivedAmount: number;    // 입금액
  outstandingAmount: number; // 미수금액
  taxInvoiceIssued: boolean; // 세금계산서 발행 여부
  transactionStatementIssued: boolean; // 거래명세서 발행 여부
  note: string;              // 비고
  status: SalesStatus;       // 상태
  createdAt: string;
  updatedAt: string;
}

필드 매핑 테이블:

API 필드 (snake_case) React 필드 (camelCase) 타입 변환 비고
id id String(id) -
sale_number salesNo 그대로 시스템 자동 생성
sale_date salesDate 그대로 YYYY-MM-DD
client_id vendorId String(client_id) FK
client.name vendorName 그대로 관계 조회
supply_amount totalSupplyAmount parseFloat() -
tax_amount totalVat parseFloat() -
total_amount totalAmount parseFloat() -
description note ?? '' -
status status 매핑 필요 API: draft/confirmed/invoiced
deposit_id receivedAmount 관계 조회 deposit.amount
created_at createdAt 그대로 -
updated_at updatedAt 그대로 -

상태 매핑:

// API → React
const SALES_STATUS_MAP = {
  'draft': 'outstanding',      // 미수
  'confirmed': 'monthlyClose', // 당월마감
  'invoiced': 'agreed',        // 합의
};

6.2 매입 관리 (Purchases)

API FormRequest 필드 (api/app/Http/Requests/V1/Purchase/StorePurchaseRequest.php):

'purchase_date' => ['required', 'date'],
'client_id' => ['required', 'integer', 'exists:clients,id'],
'supply_amount' => ['required', 'numeric', 'min:0'],
'tax_amount' => ['required', 'numeric', 'min:0'],
'total_amount' => ['required', 'numeric', 'min:0'],
'description' => ['nullable', 'string', 'max:1000'],
'withdrawal_id' => ['nullable', 'integer', 'exists:withdrawals,id'],

React 인터페이스 (react/src/components/accounting/PurchaseManagement/types.ts):

interface PurchaseRecord {
  id: string;
  purchaseNo: string;       // 매입번호
  purchaseDate: string;     // 매입일자
  vendorId: string;         // 거래처 ID
  vendorName: string;       // 거래처명
  supplyAmount: number;     // 공급가액
  vat: number;              // 부가세
  totalAmount: number;      // 합계금액
  purchaseType: PurchaseType; // 매입유형
  evidenceType: EvidenceType; // 증빙유형
  status: PurchaseStatus;   // 상태
  items: PurchaseItem[];    // 품목 정보
  taxInvoiceReceived: boolean; // 세금계산서 수취 여부
  createdAt: string;
  updatedAt: string;
}

필드 매핑 테이블:

API 필드 (snake_case) React 필드 (camelCase) 타입 변환 비고
id id String(id) -
purchase_number purchaseNo 그대로 시스템 자동 생성
purchase_date purchaseDate 그대로 YYYY-MM-DD
client_id vendorId String(client_id) FK
client.name vendorName 그대로 관계 조회
supply_amount supplyAmount parseFloat() -
tax_amount vat parseFloat() -
total_amount totalAmount parseFloat() -
description note ?? '' 품목 적요용
status status 그대로 pending/completed/cancelled
withdrawal_id - 관계 조회 출금 연결
created_at createdAt 그대로 -
updated_at updatedAt 그대로 -

매입유형 (purchaseType) 옵션:

  • raw_material: 원재료매입
  • subsidiary_material: 부재료매입
  • product: 상품매입
  • outsourcing: 외주가공비
  • consumables: 소모품비
  • 기타 15종 (types.ts 참조)

6.3 입금 관리 (Deposits)

API FormRequest 필드 (api/app/Http/Requests/V1/Deposit/StoreDepositRequest.php):

'deposit_date' => ['required', 'date'],
'client_id' => ['nullable', 'integer', 'exists:clients,id'],
'client_name' => ['nullable', 'string', 'max:100'],
'bank_account_id' => ['nullable', 'integer', 'exists:bank_accounts,id'],
'amount' => ['required', 'numeric', 'min:0'],
'payment_method' => ['required', 'string', 'in:cash,transfer,card,check'],
'account_code' => ['nullable', 'string', 'max:20'],
'description' => ['nullable', 'string', 'max:1000'],
'reference_type' => ['nullable', 'string', 'max:50'],
'reference_id' => ['nullable', 'integer'],

React 인터페이스 (react/src/components/accounting/DepositManagement/types.ts):

interface DepositRecord {
  id: string;
  depositDate: string;        // 입금일
  depositAmount: number;      // 입금액
  accountName: string;        // 입금계좌명
  depositorName: string;      // 입금자명
  note: string;               // 적요
  depositType: DepositType;   // 입금유형
  vendorId: string;           // 거래처 ID
  vendorName: string;         // 거래처명
  status: DepositStatus;      // 상태
  createdAt: string;
  updatedAt: string;
}

필드 매핑 테이블:

API 필드 (snake_case) React 필드 (camelCase) 타입 변환 비고
id id String(id) -
deposit_date depositDate 그대로 YYYY-MM-DD
amount depositAmount parseFloat() -
bank_account.name accountName 그대로 관계 조회
client_name depositorName 그대로 입금자명
description note ?? '' 적요
account_code depositType 매핑 필요 유형 코드
client_id vendorId String(client_id) FK
client.name vendorName 그대로 관계 조회
payment_method - 참조 cash/transfer/card/check
created_at createdAt 그대로 -
updated_at updatedAt 그대로 -

입금유형 (depositType) 옵션:

  • salesRevenue: 매출대금
  • advance: 선수금
  • suspense: 가수금
  • rentalIncome: 임대수익
  • interestIncome: 이자수익
  • 기타 6종 (types.ts 참조)

입금상태 (status) 옵션:

  • inputWaiting: 입력대기
  • requesting: 신청중
  • rejected: 반려
  • pending: 보류
  • incomplete: 미완
  • error: 오류
  • confirmed: 확정완료

6.4 출금 관리 (Withdrawals)

API FormRequest 필드 (api/app/Http/Requests/V1/Withdrawal/StoreWithdrawalRequest.php):

'withdrawal_date' => ['required', 'date'],
'client_id' => ['nullable', 'integer', 'exists:clients,id'],
'client_name' => ['nullable', 'string', 'max:100'],
'bank_account_id' => ['nullable', 'integer', 'exists:bank_accounts,id'],
'amount' => ['required', 'numeric', 'min:0'],
'payment_method' => ['required', 'string', 'in:cash,transfer,card,check'],
'account_code' => ['nullable', 'string', 'max:20'],
'description' => ['nullable', 'string', 'max:1000'],
'reference_type' => ['nullable', 'string', 'max:50'],
'reference_id' => ['nullable', 'integer'],

React 인터페이스 (react/src/components/accounting/WithdrawalManagement/types.ts):

interface WithdrawalRecord {
  id: string;
  withdrawalDate: string;       // 출금일
  withdrawalAmount: number;     // 출금액
  accountName: string;          // 출금계좌명
  recipientName: string;        // 수취인명
  note: string;                 // 적요
  withdrawalType: WithdrawalType; // 출금유형
  vendorId: string;             // 거래처 ID
  vendorName: string;           // 거래처명
  createdAt: string;
  updatedAt: string;
}

필드 매핑 테이블:

API 필드 (snake_case) React 필드 (camelCase) 타입 변환 비고
id id String(id) -
withdrawal_date withdrawalDate 그대로 YYYY-MM-DD
amount withdrawalAmount parseFloat() -
bank_account.name accountName 그대로 관계 조회
client_name recipientName 그대로 수취인명
description note ?? '' 적요
account_code withdrawalType 매핑 필요 유형 코드
client_id vendorId String(client_id) FK
client.name vendorName 그대로 관계 조회
payment_method - 참조 cash/transfer/card/check
created_at createdAt 그대로 -
updated_at updatedAt 그대로 -

출금유형 (withdrawalType) 옵션:

  • purchasePayment: 매입대금
  • advance: 선급금
  • suspense: 가지급금
  • rent: 임대료
  • salary: 급여
  • 기타 11종 (types.ts 참조)

6.5 거래처 관리 (Clients)

API FormRequest 필드 (api/app/Http/Requests/Client/ClientStoreRequest.php):

'client_group_id' => 'nullable|integer',
'client_code' => 'nullable|string|max:50',
'name' => 'required|string|max:100',
'client_type' => ['nullable', Rule::exists('common_codes', 'code')...],
'contact_person' => 'nullable|string|max:100',
'phone' => 'nullable|string|max:20',
'mobile' => 'nullable|string|max:20',
'fax' => 'nullable|string|max:20',
'email' => 'nullable|email|max:100',
'address' => 'nullable|string|max:255',
'manager_name' => 'nullable|string|max:50',
'manager_tel' => 'nullable|string|max:20',
'system_manager' => 'nullable|string|max:50',
'account_id' => 'nullable|string|max:50',
'account_password' => 'nullable|string|max:255',
'purchase_payment_day' => 'nullable|string|max:20',
'sales_payment_day' => 'nullable|string|max:20',
'business_no' => 'nullable|string|max:20',
'business_type' => 'nullable|string|max:50',
'business_item' => 'nullable|string|max:100',
'tax_agreement' => 'nullable|boolean',
'tax_amount' => 'nullable|numeric|min:0',
'tax_start_date' => 'nullable|date',
'tax_end_date' => 'nullable|date',
'bad_debt' => 'nullable|boolean',
'bad_debt_amount' => 'nullable|numeric|min:0',
'bad_debt_receive_date' => 'nullable|date',
'bad_debt_end_date' => 'nullable|date',
'bad_debt_progress' => ['nullable', Rule::exists('common_codes', 'code')...],
'memo' => 'nullable|string',
'is_active' => 'nullable|boolean',

필드 매핑 테이블 (주요 필드):

API 필드 (snake_case) React 필드 (camelCase) 타입 변환 비고
id id String(id) -
client_code clientCode 그대로 자동 생성
name name 그대로 거래처명
client_type clientType common_codes 참조 -
contact_person contactPerson 그대로 담당자
phone phone 그대로 전화번호
mobile mobile 그대로 휴대폰
email email 그대로 이메일
address address 그대로 주소
business_no businessNo 그대로 사업자번호
business_type businessType 그대로 업종
business_item businessItem 그대로 업태
bad_debt badDebt Boolean 악성채권 여부
bad_debt_amount badDebtAmount parseFloat() 악성채권 금액
is_active isActive Boolean 활성 상태
created_at createdAt 그대로 -
updated_at updatedAt 그대로 -

6.6 공통 변환 함수 템플릿

// utils/apiTransform.ts

/** snake_case를 camelCase로 변환 */
export function snakeToCamel(str: string): string {
  return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
}

/** camelCase를 snake_case로 변환 */
export function camelToSnake(str: string): string {
  return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
}

/** 객체 전체 키 변환 */
export function transformKeys<T>(obj: Record<string, any>, transformer: (key: string) => string): T {
  const result: Record<string, any> = {};
  for (const [key, value] of Object.entries(obj)) {
    const newKey = transformer(key);
    result[newKey] = value !== null ? value : undefined;
  }
  return result as T;
}

/** API → Frontend 공통 변환 */
export function transformApiToFrontend<T>(apiData: Record<string, any>): T {
  return transformKeys<T>(apiData, snakeToCamel);
}

/** Frontend → API 공통 변환 */
export function transformFrontendToApi(data: Record<string, any>): Record<string, any> {
  return transformKeys(data, camelToSnake);
}

7. 작업 일정

Phase A: API 완료 기능 (1주)

일차 작업 내용 상태
Day 1 A-1 악성채권 관리 연동 완료 (2025-12-23)
Day 2 A-2 팝업 관리 연동 완료 (2025-12-23)
Day 3 A-3 결제 내역 연동 완료 (2025-12-23)
Day 4 A-4 구독 관리 연동 완료 (2025-12-23)
Day 5 A-5 알림 설정 연동 완료 (2025-12-23)
Day 6 A-6 거래처 원장 - API 미존재로 건너뜀 ⏭️ 건너뜀

Phase B: 핵심 업무 기능 (2주)

일차 작업 내용
Day 1-2 B-1 매출 관리 연동
Day 3-4 B-2 매입 관리 연동
Day 5-6 B-3 입금 관리, B-4 출금 관리 연동
Day 7-8 B-5 거래처 관리 연동
Day 9-10 B-6 어음 관리 연동 + 통합 테스트

8. 변경 이력

날짜 내용 작성자
2025-12-23 문서 초안 작성 Claude
2025-12-23 Phase B 상세 필드 매핑 추가 (6.1~6.6) Claude
2025-12-23 A-1 악성채권 관리 API 연동 완료 (actions.ts, page.tsx, index.tsx) Claude
2025-12-23 A-2 팝업 관리 API 연동 완료 (actions.ts, page.tsx, PopupList.tsx, [id]/page.tsx, [id]/edit/page.tsx) Claude
2025-12-23 A-3 결제 내역 API 연동 완료 (types.ts, actions.ts, page.tsx, PaymentHistoryClient.tsx, index.ts) Claude
2025-12-23 A-4 구독 관리 API 연동 완료 (types.ts, utils.ts, actions.ts, page.tsx, SubscriptionClient.tsx, index.ts) Claude
2025-12-23 A-5 알림 설정 API 연동 완료 (types.ts, actions.ts, page.tsx, NotificationSettingsClient.tsx, index.ts) Claude
2025-12-23 A-6 거래처 원장 - API 미존재 확인, Phase A 완료 Claude
2025-12-23 B-1 매출 관리 - 기존 API 연동 확인 (/api/proxy/sales 사용) Claude
2025-12-23 B-2 매입 관리 - 기존 API 연동 확인 (/api/proxy/purchases 사용) Claude
2025-12-23 B-3 입금 관리 API 연동 완료 (types.ts: API 타입 추가, index.tsx: Mock → API 호출 전환) Claude
2025-12-23 B-4 출금 관리 API 연동 완료 (types.ts: API 타입 추가, index.tsx: Mock → API 호출 전환) Claude
2025-12-23 B-5 거래처 관리 API 연동 완료 (types.ts: API 타입 추가, actions.ts: Server Actions, page.tsx: 서버 컴포넌트, VendorManagementClient.tsx: 클라이언트 컴포넌트) Claude

9. 참고 문서

  • API 스펙: http://sam.kr/api-docs
  • 기존 연동 계획: react-api-integration-plan.md
  • API 개발 계획: erp-api-development-plan-d1.0-changes.md
  • 표준 구현 참조: /sales/pricing-management