823 lines
29 KiB
Markdown
823 lines
29 KiB
Markdown
# 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 패턴
|
|
|
|
```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) |
|
|
| 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`
|