Files
sam-docs/plans/react-mock-to-api-migration-plan.md

989 lines
44 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 현황 분석
**목업 데이터 사용 페이지**: 80개+ 파일에서 목업 데이터 사용 중
```
┌─────────────────────────────────────────────────────────────────┐
│ 🎯 연동 목표 │
├─────────────────────────────────────────────────────────────────┤
│ - React 목업 데이터 → 실제 API 호출로 전환 │
│ - 단가관리(pricing-management) 패턴을 표준으로 적용 │
│ - Phase A~B (API 완료 기능) 우선 연동 │
└─────────────────────────────────────────────────────────────────┘
```
### 1.4 2025-12-24 전수 조사 결과 요약
> MCP Serena를 활용한 Mock 데이터 사용 현황 전수 조사 결과
**🔍 발견된 주요 패턴:**
1. **리스트 API 연동 완료, 상세/CRUD Mock**: 리스트는 API 호출하지만 상세 조회, 등록, 수정, 삭제는 Mock
2. **actions.ts 존재하지만 미연동**: Server Actions 파일은 있으나 UI에서 호출하지 않음 (console.log 사용)
3. **MOCK_XXX 패턴**: 상수 배열로 Mock 데이터 정의 (예: `MOCK_POPUPS`, `mockEmployees`)
4. **generateMockData 함수**: 동적 Mock 데이터 생성 함수 사용
5. **console.log CRUD**: 저장/삭제 시 console.log만 출력하고 실제 API 호출 없음
**📊 현황 요약:**
| 분류 | 개수 | 상태 |
|------|------|------|
| 리스트 API 완료 | 12개 | Phase A-B |
| 상세 페이지 Mock | 10개+ | 연동 필요 |
| CRUD Mock (console.log) | 25개+ | 연동 필요 |
| 전체 Mock | 30개+ | Phase C-H |
| API 미존재 | 7개 | Phase I |
**⚠️ 우선 연동 대상 (actions.ts 존재):**
- A-1 악성채권: `BadDebtDetail.tsx``actions.ts` 연동 *(다른 세션에서 진행)*
- A-2 팝업: `[id]/page.tsx`, `[id]/edit/page.tsx``actions.ts` 연동
- B-1~B-6: 상세 페이지 및 CRUD → `actions.ts` 연동
### 1.5 작업 진행 정책
> **단위 작업 → 검수 → 승인 → 문서 업데이트 → 커밋** 순서로 진행
```
┌─────────────────────────────────────────────────────────────────┐
│ 📋 작업 흐름 (페이지 단위) │
├─────────────────────────────────────────────────────────────────┤
│ 1⃣ 작업 시작: 대상 페이지 Mock → API 연동 작업 │
│ 2⃣ 작업 완료: 코드 수정 완료 후 사용자에게 검수 요청 │
│ 3⃣ 검수: 사용자가 기능 확인 (브라우저 테스트) │
│ 4⃣ [승인] 문서 업데이트: 이 문서의 상태 갱신 │
│ 5⃣ [승인] 커밋: Git 커밋 생성 │
│ 6⃣ 다음 페이지로 이동 │
└─────────────────────────────────────────────────────────────────┘
```
**⚠️ 중요 규칙:**
- 각 단계에서 `[승인]` 표시된 작업은 **사용자 승인 후** 진행
- 한 번에 하나의 페이지만 작업 (병렬 작업 금지)
- 검수 실패 시 수정 후 재검수
- 커밋 메시지: `feat: [Phase]-[번호] [페이지명] Mock → API 연동`
**📌 현재 진행 순서:**
1. ~~A-1 악성채권~~ *(완료 - 상세+CRUD+메모 API 연동)*
2. ~~A-2 팝업 관리~~ *(완료 - 상세+CRUD API 연동)*
3. A-3 결제 내역 (조회 전용, 완료)
4. A-4 구독 관리 (조회 전용, 완료)
5. A-5 알림 설정 (완료)
6. B-1 매출 관리
7. B-2 매입 관리
8. B-3 입금 관리
9. B-4 출금 관리
10. B-5 거래처 관리
11. B-6 어음 관리
### 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. 우선순위별 연동 대상
> **상태 범례**:
> - ✅ 완료: API 연동 완료 (Server Actions 호출)
> - 🔄 Mock: Mock 데이터 또는 console.log 사용
> - ⚠️ API 필요: 백엔드 API 개발 선행 필요
> - ⏭️ 건너뜀: API 미존재 또는 해당 없음
> - 📦 actions.ts: Server Actions 파일 존재 (UI 연동 필요)
### 2.1 Phase A: API 완료 기능 (즉시 연동 가능)
> 이미 API가 구현되어 있어 React 연동만 필요
> **⚠️ 2025-12-24 전수 조사 결과**: 리스트 API 연동 완료했으나 상세/CRUD 작업에서 Mock 사용 확인
| # | 페이지 | React 경로 | 리스트 | 상세 조회 | 등록 | 수정 | 삭제 | 비고 |
|---|--------|-----------|--------|----------|------|------|------|------|
| A-1 | 악성채권 관리 | `/accounting/bad-debt-collection` | ✅ | ✅ | ✅ | ✅ | ✅ | 상세+CRUD+메모 API 연동 완료 (2025-12-24) |
| A-2 | 팝업 관리 | `/settings/popup-management` | ✅ | ✅ | ✅ | ✅ | ✅ | 상세+CRUD API 연동 완료 (2025-12-24) |
| A-3 | 결제 내역 | `/settings/payment-history` | ✅ | ⏭️ | ⏭️ | ⏭️ | ⏭️ | 조회 전용 |
| A-4 | 구독 관리 | `/settings/subscription` | ✅ | ⏭️ | ⏭️ | ⏭️ | ⏭️ | 조회 전용 |
| A-5 | 알림 설정 | `/settings/notifications` | ✅ | ⏭️ | ⏭️ | ✅ | ⏭️ | 저장 기능 완료 |
### 2.2 Phase B: 핵심 업무 기능
> **⚠️ 2025-12-24 전수 조사 결과**: actions.ts 존재하나 UI에서 console.log로 Mock 처리
| # | 페이지 | React 경로 | 리스트 | 상세 조회 | 등록 | 수정 | 삭제 | 비고 |
|---|--------|-----------|--------|----------|------|------|------|------|
| B-1 | 매출 관리 | `/accounting/sales` | ✅ | ✅ | ✅ | ✅ | ✅ | API 연동 완료, alert→toast 변환 |
| B-2 | 매입 관리 | `/accounting/purchase` | ✅ | ✅ | ✅ | ✅ | ✅ | API 연동 완료, alert→toast 변환 |
| B-3 | 입금 관리 | `/accounting/deposit` | ✅ | ✅ | ✅ | ✅ | ✅ | API 연동 완료, alert→toast 변환 |
| B-4 | 출금 관리 | `/accounting/withdrawal` | ✅ | ✅ | ✅ | ✅ | ✅ | API 연동 완료, alert→toast 변환 |
| B-5 | 거래처 관리 | `/accounting/vendor` | ✅ | ✅ | ✅ | ✅ | ✅ | API 연동 완료, console.error 제거 |
| B-6 | 어음 관리 | `/accounting/bills` | ✅ | ⏭️ 모달 | ✅ | ✅ | ✅ | API 연동 완료 |
### 2.3 Phase C: 인사/근태
> **2025-12-24 분석 결과**: API 존재, 전체 Mock 사용 중
| # | 페이지 | React 경로 | 리스트 | 상세 조회 | 등록 | 수정 | 삭제 | 비고 |
|---|--------|-----------|--------|----------|------|------|------|------|
| C-1 | 직원 관리 | `/hr/employee-management` | ✅ | ✅ | ✅ | ✅ | ✅ | **2025-12-24 연동 완료** - actions.ts, utils.ts 생성, 전체 CRUD API 연동 |
| C-2 | 근태 관리 | `/hr/attendance` | ✅ | ⏭️ | ✅ | ⏭️ | ⏭️ | **2025-12-24 연동 완료** - actions.ts 생성, check-in/check-out/today API 연동 |
| C-3 | 휴가 관리 | `/hr/vacation-management` | ✅ | ⏭️ | ✅ | ⏭️ | ⏭️ | **2025-12-24 연동 완료** - actions.ts 생성, 신청현황 탭 API 연동 (목록/승인/반려/신청), 사용현황/부여현황 탭 API 연동 |
| C-4 | 부서 관리 | `/hr/departments` | ✅ | ⏭️ | ✅ | ✅ | ✅ | **2025-12-24 연동 완료** - actions.ts 생성, 트리 조회/생성/수정/삭제 API 연동 |
### 2.4 Phase D: 설정/시스템 관리
> **2025-12-24 분석 결과**: 대부분 Mock, console.log로 저장 처리
| # | 페이지 | React 경로 | 리스트 | 상세 조회 | 등록 | 수정 | 삭제 | 비고 |
|---|--------|-----------|--------|----------|------|------|------|------|
| D-1 | 회사 정보 관리 | `/settings/company-info` | ✅ | ⏭️ | ⏭️ | ✅ | ⏭️ | **2025-12-24 연동 완료** - actions.ts 생성, API 조회/수정 연동 |
| D-2 | 계정 관리 | `/settings/accounts` | ✅ | ✅ | ✅ | ✅ | ✅ | **2025-12-24 연동 완료** - actions.ts 생성, CRUD API 연동 |
| D-3 | 근무 일정 관리 | `/settings/work-schedule` | ✅ | ⏭️ | ⏭️ | ✅ | ⏭️ | **2025-12-24 연동 완료** - actions.ts 생성, 조회/수정 API 연동 |
| D-4 | 근태 설정 관리 | `/settings/attendance-settings` | ✅ | ⏭️ | ⏭️ | ✅ | ⏭️ | **2025-12-24 연동 완료** - actions.ts 생성, GPS/허용반경 API 연동 |
### 2.5 Phase E: 인사/급여
> **2025-12-24 분석 결과**: 전체 Mock, CRUD 모두 console.log
| # | 페이지 | React 경로 | 리스트 | 상세 조회 | 등록 | 수정 | 삭제 | 비고 |
|---|--------|-----------|--------|----------|------|------|------|------|
| E-1 | 법인카드 관리 | `/hr/card-management` | 🔄 Mock | 🔄 Mock | 🔄 | 🔄 | 🔄 | `new/[id]/page.tsx` - console.log |
| E-2 | 급여 관리 | `/hr/salary` | 🔄 Mock | ⏭️ | ⏭️ | ⏭️ | ⏭️ | `index.tsx:89,158` mockData |
### 2.6 Phase F: 결재 시스템
> **2025-12-24 분석 결과**: 전체 Mock, 삭제 console.log
| # | 페이지 | React 경로 | 리스트 | 상세 조회 | 등록 | 수정 | 삭제 | 비고 |
|---|--------|-----------|--------|----------|------|------|------|------|
| F-1 | 기안함 | `/approval/draft-box` | 🔄 Mock | ⏭️ | ⏭️ | ⏭️ | 🔄 | `index.tsx:183,383,433` console.log('삭제') |
| F-2 | 참조함 | `/approval/reference-box` | 🔄 Mock | ⏭️ | ⏭️ | ⏭️ | ⏭️ | `index.tsx:115` mockData |
| F-3 | 결재함 | `/approval/approval-box` | 🔄 Mock | ⏭️ | ⏭️ | ⏭️ | ⏭️ | `index.tsx:123` mockData |
| F-4 | 비용견적서 양식 | `/approval/create/expense-estimate` | ⏭️ | ⏭️ | 🔄 | ⏭️ | ⏭️ | `ExpenseEstimateForm.tsx:20` mockData |
### 2.7 Phase G: 생산 관리
> **2025-12-24 분석 결과**: 전체 Mock (mockData.ts 파일 사용)
| # | 페이지 | React 경로 | 리스트 | 상세 조회 | 등록 | 수정 | 삭제 | 비고 |
|---|--------|-----------|--------|----------|------|------|------|------|
| G-1 | 작업지시 관리 | `/production/work-orders` | 🔄 Mock | 🔄 Mock | 🔄 | 🔄 | 🔄 | `mockData.ts` 전체 사용 |
| G-2 | 작업실적 관리 | `/production/work-results` | 🔄 Mock | ⏭️ | 🔄 | 🔄 | ⏭️ | `mockData.ts` 전체 사용 |
| G-3 | 생산 대시보드 | `/production/dashboard` | 🔄 Mock | ⏭️ | ⏭️ | ⏭️ | ⏭️ | `mockData.ts` 전체 사용 |
| G-4 | 작업자 화면 | `/production/worker-screen` | 🔄 Mock | ⏭️ | ⏭️ | ⏭️ | ⏭️ | 전체 Mock |
| G-5 | 품질 검사 | `/quality/inspection` | 🔄 Mock | 🔄 Mock | 🔄 | 🔄 | 🔄 | `mockData.ts` 전체 사용 |
### 2.8 Phase H: 자재/출하 관리
> **2025-12-24 분석 결과**: 전체 Mock (mockData.ts 파일 사용)
| # | 페이지 | React 경로 | 리스트 | 상세 조회 | 등록 | 수정 | 삭제 | 비고 |
|---|--------|-----------|--------|----------|------|------|------|------|
| H-1 | 입고 관리 | `/material/receiving` | 🔄 Mock | 🔄 Mock | 🔄 | 🔄 | 🔄 | `mockData.ts` 전체 사용 |
| H-2 | 재고 현황 | `/material/stock-status` | 🔄 Mock | 🔄 Mock | ⏭️ | ⏭️ | ⏭️ | `mockData.ts`, `StockStatusDetail.tsx:38` |
| H-3 | 출하 관리 | `/outbound/shipment` | 🔄 Mock | 🔄 Mock | 🔄 | 🔄 | 🔄 | `mockData.ts`, `ShipmentDetail.tsx:48` |
### 2.9 Phase I: API 신규 개발 필요 (후순위)
> 현재 API가 존재하지 않아 백엔드 개발 선행 필요
| # | 페이지 | React 경로 | 필요 API | 상태 |
|---|--------|-----------|---------|------|
| I-1 | 미지급비용 관리 | `/accounting/expected-expense` | `GET/POST /v1/expected-expenses` | ⚠️ API 개발 필요 |
| I-2 | 거래처 원장 | `/accounting/vendor-ledger` | `GET /v1/vendor-ledger` | ⚠️ API 개발 필요 |
| I-3 | 카드 거래 조회 | `/accounting/card-transaction` | `GET /v1/card-transactions` | ⚠️ API 개발 필요 |
| I-4 | 은행 거래 조회 | `/accounting/bank-transaction` | `GET /v1/bank-transactions` | ⚠️ API 개발 필요 |
| I-5 | 채권 현황 | `/accounting/receivables-status` | `GET /v1/receivables` | ⚠️ API 개발 필요 |
| I-6 | 일일 보고서 | `/accounting/daily-report` | `GET /v1/daily-reports` | ⚠️ API 개발 필요 |
| I-7 | 종합 분석 보고서 | `/reports/comprehensive-analysis` | `GET /v1/reports/comprehensive` | ⚠️ API 개발 필요 |
| I-8 | 휴가 정책 관리 | `/settings/leave-policy` | `GET/PUT /v1/settings/leave-policy` | ⚠️ API 개발 필요 |
### 2.10 상세 페이지 Mock 현황
> 리스트 페이지 API 연동 완료 후 상세 페이지 연동 필요
| # | 컴포넌트 | 파일 위치 | Mock 사용 라인 | 부모 기능 |
|---|----------|----------|---------------|----------|
| S-1 | SalesDetail | `SalesManagement/SalesDetail.tsx` | L56-63 (MOCK_VENDORS), L77-110 (fetchSalesDetail) | B-1 매출 관리 |
| S-2 | PurchaseDetail | `PurchaseManagement/PurchaseDetail.tsx` | L49-56 (Mock 계좌/거래처), L65-126 (Mock 데이터) | B-2 매입 관리 |
| S-3 | DepositDetail | `DepositManagement/DepositDetail.tsx` | L52 (Mock 데이터 로드) | B-3 입금 관리 |
| S-4 | WithdrawalDetail | `WithdrawalManagement/WithdrawalDetail.tsx` | L59 (Mock 데이터 로드) | B-4 출금 관리 |
| S-5 | VendorDetail | `VendorManagement/VendorDetail.tsx` | L61 (Mock 데이터 가져오기) | B-5 거래처 관리 |
| S-6 | BadDebtDetail | `BadDebtCollection/BadDebtDetail.tsx` | L49-57 (Mock 담당자/데이터) | A-1 악성채권 관리 |
| S-7 | StockStatusDetail | `StockStatus/StockStatusDetail.tsx` | L38 (Mock 상세 정보) | H-2 재고 현황 |
| S-8 | ShipmentDetail | `ShipmentManagement/ShipmentDetail.tsx` | L48 (Mock 데이터) | H-3 출하 관리 |
| S-9 | ReceivingDetail | `ReceivingManagement/ReceivingDetail.tsx` | Mock 전체 | H-1 입고 관리 |
| S-10 | WorkOrderDetail | `WorkOrders/WorkOrderDetail.tsx` | Mock 전체 | G-1 작업지시 관리 |
---
## 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 패턴
```typescript
// 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 패턴
```typescript
// 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 연동 패턴
```typescript
// 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 상태 매핑 예시
```typescript
// 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`):
```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`):
```typescript
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` | 그대로 | - |
**상태 매핑**:
```typescript
// 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`):
```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`):
```typescript
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`):
```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`):
```typescript
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`):
```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`):
```typescript
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`):
```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 공통 변환 함수 템플릿
```typescript
// 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) |
### 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 |
| 2025-12-24 | 커밋 유실로 인한 API 연동 복원 (A-2~A-5, B-3~B-5: `actions.ts` 재생성, `page.tsx` 서버 컴포넌트 전환, `index.tsx` mock 제거) | Claude |
| 2025-12-24 | Mock 데이터 사용 파일 전수 조사 (MCP Serena 활용), 신규 Phase 추가 (D~H, S) | Claude |
| 2025-12-24 | B-1 매출관리, B-2 매입관리 상태 수정 (Mock 사용중으로 재분류) | Claude |
| 2025-12-24 | Phase E (설정/시스템), F (인사/급여), G (결재), H (API 개발 필요), S (상세 페이지) 신규 추가 | Claude |
| 2025-12-24 | **문서 전면 수정**: 리스트/상세 분리 상태 표기, 상세 페이지 Mock 현황 섹션 추가, Phase 재구성 (A~I), 게시판 제외 | Claude |
| 2025-12-24 | **전수 조사 및 문서 업데이트**: MCP Serena로 80개+ 파일 Mock 패턴 분석, Phase A~H CRUD별 상태 테이블 추가, 전수 조사 결과 요약(1.4) 추가, console.log 패턴 및 actions.ts 미연동 현황 문서화 | Claude |
| 2025-12-24 | **작업 진행 정책 추가 (1.5)**: 페이지 단위 작업 → 검수 → [승인] 문서 업데이트 → [승인] 커밋 순서 정의, A-1 다른 세션 처리, A-2부터 순차 진행 | Claude |
| 2025-12-24 | **A-1 악성채권 관리 완전 연동**: 상세 페이지(`[id]/page.tsx`, `[id]/edit/page.tsx`) 서버 컴포넌트 전환, `BadDebtDetail.tsx` CRUD API 연동, 메모 API 연동(`addBadDebtMemo`, `deleteBadDebtMemo`) | Claude |
| 2025-12-24 | **A-2 팝업 관리 API 연동**: `[id]/page.tsx` getPopupById+deletePopup 연동, `[id]/edit/page.tsx` getPopupById 연동, `PopupForm.tsx` createPopup+updatePopup 연동, 로딩/에러 상태 처리 | Claude |
| 2025-12-24 | **C-1 직원 관리 API 연동**: `actions.ts` 생성 (CRUD+통계+일괄삭제), `utils.ts` 생성 (API↔Frontend 변환), `index.tsx` Mock 제거+API 연동, `[id]/page.tsx` 상세 API 연동, `[id]/edit/page.tsx` 수정 API 연동, `new/page.tsx` 등록 API 연동 | Claude |
| 2025-12-24 | **C-2 근태 관리 API 연동**: `actions.ts` 생성 (checkIn/checkOut/getTodayAttendance), `GoogleMap.tsx` userLocation 콜백 추가, `page.tsx` Mock console.log 제거+API 연동, 처리중 상태 및 버튼 텍스트 추가 | Claude |
| 2025-12-24 | **C-3 휴가 관리 API 연동**: `actions.ts` 생성 (getLeaves/createLeave/approveLeave/rejectLeave 등), `index.tsx` 신청현황 탭 Mock 제거+API 연동, 일괄승인/반려 API 연동 | Claude |
| 2025-12-24 | **C-4 부서 관리 API 연동**: `actions.ts` 생성 (getDepartmentTree/createDepartment/updateDepartment/deleteDepartment/deleteDepartmentsMany), `index.tsx` Mock 제거+API 연동 | Claude |
| 2025-12-24 | **`.env.local` 환경변수 수정**: `API_URL=https://api.sam.kr/api` 추가 - Server Actions 서버사이드 API URL | Claude |
| 2025-12-24 | **D-2 계정 관리(계좌) API 연동**: `actions.ts` 생성 (CRUD+toggle+setPrimary+일괄삭제), `types.ts` API 필드 추가 (id:number, bankName, isPrimary, assignedUserId), `index.tsx` generateMockData 제거+API 연동, `AccountDetail.tsx` console.log 제거+API 연동 | Claude |
| 2025-12-24 | **D-3 근무 일정 관리 API 연동**: `actions.ts` 생성 (getWorkSetting/updateWorkSetting), `index.tsx` console.log 제거+API 연동, 로딩/저장 상태 추가, HH:mm↔HH:mm:ss 시간 변환 | Claude |
| 2025-12-24 | **D-4 근태 설정 관리 API 연동**: `actions.ts` 생성 (getAttendanceSetting/updateAttendanceSetting), `index.tsx` console.log 제거+API 연동, 로딩/저장 상태 추가 (API 지원 필드: useGps, allowedRadius만 연동) | Claude |
| 2025-12-24 | **B-6 어음 관리 중복번호 검증 추가**: `StoreBillRequest.php` tenant scope unique 검증 규칙 추가 (`Rule::unique`), `lang/ko/error.php` 에러 메시지 추가 (`bill.duplicate_number`), SAM 표준 패턴 적용 (`app('tenant_id')`) | 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`