docs: 견적 산출 API 개발 계획 문서 추가

- 견적 산출 API 개발 계획 문서 신규 작성 (quote-calculation-api-plan.md)
- MNG FormulaEvaluatorService 핵심 로직 상세 명세 포함
- 10단계 BOM 계산 프로세스, CategoryGroup 단가 계산 방식 등
- React 견적등록 화면 연동을 위한 API 명세 정의
- 페이지네이션 정책 문서 업데이트 (Laravel 기본 응답 구조 반영)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-30 22:55:09 +09:00
parent b1362ee9cb
commit 3ba4f87566
2 changed files with 702 additions and 158 deletions

View File

@@ -0,0 +1,620 @@
# 견적 산출 API 개발 계획
> **작성일**: 2025-12-30
> **목적**: 견적 산출 API 개발 및 React 견적등록 화면 연동
> **참조 로직**: `mng/app/Services/Quote/FormulaEvaluatorService.php` (코드 복사/재구현)
> **상태**: 🔄 진행중 (Serena ID: quote-calc-api-state)
---
## 📍 현재 진행 상태
| 항목 | 내용 |
|------|------|
| **마지막 완료 작업** | 분석 및 계획 수립 |
| **다음 작업** | Phase 1.1 API 계산 로직 구현 |
| **진행률** | 0/12 (0%) |
| **마지막 업데이트** | 2025-12-30 20:00 |
---
## 0. 로컬 개발 환경
### 도메인 구성
| 서비스 | 도메인 | 설명 |
|--------|--------|------|
| React (프론트엔드) | `http://dev.sam.kr` | 사용자 화면 |
| API (백엔드) | `http://api.sam.kr` | REST API 서버 |
| MNG (운영관리자) | `http://mng.sam.kr` | 관리자 패널 |
### 테스트 대상 테넌트
| 항목 | 값 | 비고 |
|------|-----|------|
| **Tenant ID** | 287 | 프론트_테스트회사 |
| **테스트 User ID** | 33 | 홍킬동 (hhhhhh@example.com) |
---
## 1. 작업 규칙
### 1.0 아키텍처 원칙 (필수)
> **React는 오직 `api.sam.kr` (api 프로젝트)만 호출한다**
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ react/ │ ───► │ api/ │ │ mng/ │
│ dev.sam.kr │ │ api.sam.kr │ │ mng.sam.kr │
│ (프론트엔드) │ │ (REST API) │ │ (관리자패널) │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
│ ✅ 호출 허용 │ │
└────────────────────┘ │
❌ 절대 호출 금지 ─────────────────────────┘
```
**규칙:**
- React에서 mng API 직접 호출 **절대 금지**
- 필요한 API가 api 프로젝트에 없으면 **api에 새로 개발**
- mng의 모델/로직은 **참조만** (코드 복사 또는 재구현)
- **MNG와 API는 동일한 DB 사용** (데이터 복제 불필요)
### 1.1 작업 진행 정책
> **단위 작업 → 검수 → 승인 → 문서 업데이트 → 커밋** 순서로 진행
```
┌─────────────────────────────────────────────────────────────────┐
│ 📋 작업 흐름 (페이지 단위) │
├─────────────────────────────────────────────────────────────────┤
│ 1⃣ 작업 시작: 대상 기능 구현 │
│ 2⃣ 작업 완료: 코드 수정 완료 후 사용자에게 검수 요청 │
│ 3⃣ 검수: 사용자가 기능 확인 (브라우저 테스트) │
│ 4⃣ [승인] 문서 업데이트: 이 문서의 상태 갱신 │
│ 5⃣ [승인] 커밋: Git 커밋 생성 │
│ 6⃣ 다음 작업으로 이동 │
└─────────────────────────────────────────────────────────────────┘
```
**⚠️ 중요 규칙:**
- 각 단계에서 `[승인]` 표시된 작업은 **사용자 승인 후** 진행
---
## 2. 개요
### 2.1 배경
MNG 시뮬레이터(`mng.sam.kr/quote-formulas/simulator`)의 견적 산출 로직이 정상 작동함을 확인함.
이 로직을 **API 프로젝트에 재구현**하여 React 견적등록 화면(`dev.sam.kr/sales/quote-management/new`)에서 사용.
**현재 상태:**
- MNG: `FormulaEvaluatorService` - DB 기반 정확한 계산 (**참조용**)
- API: `QuoteCalculationService` - 존재하지만 로직 미완성
- React: `handleAutoCalculate()` - 토스트 메시지만 표시 (API 미연동)
**목표:**
```
React 입력 → API 계산 → 결과 반환 → React 표시
```
### 2.2 기준 원칙
```
┌─────────────────────────────────────────────────────────────────┐
│ 🎯 핵심 원칙 │
├─────────────────────────────────────────────────────────────────┤
│ 1. MNG FormulaEvaluatorService 로직을 API에 재구현 │
│ 2. DB 기반 동적 계산 (하드코딩 금지) │
│ 3. 단가는 DB에서 조회 (localStorage 사용 금지) │
│ 4. 카테고리별 계산 방식도 DB에서 (CategoryGroup 활용) │
│ 5. MNG 직접 호출 절대 금지 │
└─────────────────────────────────────────────────────────────────┘
```
### 1.3 변경 승인 정책
| 분류 | 예시 | 승인 |
|------|------|------|
| ✅ 즉시 가능 | React UI 연동, 파라미터 추가 | 불필요 |
| ⚠️ 컨펌 필요 | API 계산 로직 변경, 새 엔드포인트 | **필수** |
| 🔴 금지 | DB 스키마 변경, 기존 API 삭제 | 별도 협의 |
### 1.4 준수 규칙
- `docs/quickstart/quick-start.md` - 빠른 시작 가이드
- `docs/standards/quality-checklist.md` - 품질 체크리스트
- `docs/guides/swagger-guide.md` - Swagger 문서 가이드
- `api/CLAUDE.md` - API 개발 규칙
---
## 2. 대상 범위
### 2.1 Phase 1: API 계산 로직 구현
| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 1.1 | QuoteCalculationService MNG 로직 동기화 | ⏳ | FormulaEvaluatorService 기반 |
| 1.2 | 입력 변수 처리 (W0, H0, GT, MP, CT 등) | ⏳ | React QuoteItem 매핑 |
| 1.3 | 수식 평가 로직 구현 | ⏳ | evaluate, evaluateRange |
| 1.4 | 품목 가격 계산 로직 구현 | ⏳ | area_based, weight_based 구분 |
### 2.2 Phase 2: API 엔드포인트 정비
| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 2.1 | POST /api/v1/quotes/calculate 엔드포인트 | ⏳ | 기존 존재, 로직 수정 |
| 2.2 | QuoteCalculateRequest 유효성 검증 | ⏳ | FormRequest 수정 |
| 2.3 | Swagger 문서 업데이트 | ⏳ | 요청/응답 스키마 |
### 2.3 Phase 3: React 연동
| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 3.1 | handleAutoCalculate API 호출 구현 | ⏳ | quoteApi.calculate() 사용 |
| 3.2 | 계산 결과 UI 표시 | ⏳ | 품목별 단가/금액 |
| 3.3 | 에러 처리 및 로딩 상태 | ⏳ | UX 개선 |
| 3.4 | 계산 결과로 QuoteItem 자동 생성 | ⏳ | items 배열 업데이트 |
---
## 3. MNG 핵심 로직 상세 (API 재구현 기준)
> **중요**: 이 섹션은 API에서 재구현해야 할 MNG 로직의 완전한 명세입니다.
> 새 세션에서 이 문서만 보고 작업할 수 있도록 상세히 기술합니다.
### 3.1 핵심 서비스 구조
```
┌──────────────────────────────────────────────────────────────────────────┐
│ FormulaEvaluatorService 핵심 메서드 │
├──────────────────────────────────────────────────────────────────────────┤
│ 📥 입력 처리 │
│ ├── validateFormula() - 수식 문법 검증 │
│ └── resetVariables() - 변수 초기화 │
│ │
│ 🔢 수식 평가 │
│ ├── evaluate() - 단일 수식 평가 (변수 치환 + 함수 처리) │
│ ├── evaluateRange() - 범위 조건별 수식 평가 │
│ └── evaluateMapping() - 매핑값 기반 수식 평가 │
│ │
│ 📊 BOM 기반 계산 (핵심) │
│ ├── calculateBomWithDebug() - 10단계 디버그 포함 전체 계산 │
│ ├── expandBomWithFormulas() - BOM 트리 전개 │
│ └── calculateCategoryPrice() - 카테고리별 단가 계산 │
│ │
│ 💰 가격 조회 │
│ ├── getItemPrice() - 품목 단가 조회 (Price 모델 → Fallback) │
│ └── getItemCategory() - 품목 카테고리 조회 │
└──────────────────────────────────────────────────────────────────────────┘
```
### 3.2 사용 DB 테이블
| 테이블명 | 용도 | 주요 컬럼 |
|---------|------|----------|
| `items` | 품목 마스터 | code, name, item_type, item_category, process_type, bom(JSON), unit |
| `prices` | 단가 정보 | tenant_id, item_code, sales_price |
| `category_groups` | 카테고리별 계산 방식 | code, categories(JSON), multiplier_variable |
| `quote_formulas` | 수식 정의 | variable, formula, type, output_type |
| `quote_formula_ranges` | 범위별 조건 | formula_id, condition_variable, min, max, result_value |
| `quote_formula_mappings` | 매핑 정의 | formula_id, source_variable, source_value, result_value |
### 3.3 입력 변수 (React → API)
| 변수명 | 의미 | 타입 | 예시 |
|--------|------|------|------|
| `W0` | 오픈사이즈 가로 (mm) | number | 3000 |
| `H0` | 오픈사이즈 세로 (mm) | number | 2500 |
| `QTY` | 수량 | number | 1 |
| `PC` | 제품 카테고리 | string | "SCREEN", "STEEL" |
| `GT` | 가이드레일 설치유형 | string | "wall", "ceiling" |
| `MP` | 모터 전원 | string | "single", "three" |
| `CT` | 연동제어기 | string | "basic", "advanced" |
| `WS` | 마구리 날개치수 | number | 50 |
| `INSP` | 검사비 | number | 50000 |
### 3.4 계산 변수 (자동 산출)
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 변수 계산 로직 (제품 카테고리별 마진값) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ if (PC === 'STEEL') { │
│ marginW = 110; // 철재 마진 │
│ marginH = 350; │
│ K = M × 25; // 철재 중량 │
│ } else { │
│ marginW = 140; // 스크린 기본 마진 │
│ marginH = 350; │
│ K = M × 2 + (W0 / 1000) × 14.17; // 스크린 중량 │
│ } │
│ │
│ W1 = W0 + marginW; // 마진 포함 폭 │
│ H1 = H0 + marginH; // 마진 포함 높이 │
│ M = (W1 × H1) / 1,000,000; // 면적 (㎡) │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
### 3.5 CategoryGroup 단가 계산 방식
```php
// category_groups 테이블 구조
// code: 'area_based' | 'weight_based' | 'quantity_based'
// multiplier_variable: 'M' (면적) | 'K' (중량) | null (수량)
// categories: JSON 배열 ['원단', '패널', '도장'] 등
// 단가 계산 로직
if (multiplier_variable === 'M') {
// 면적 기반: 기본단가 × M (면적 ㎡)
final_price = base_price × M;
note = "면적단가 (xxx원/㎡ × x.xx㎡)";
} else if (multiplier_variable === 'K') {
// 중량 기반: 기본단가 × K (중량 kg)
final_price = base_price × K;
note = "중량단가 (xxx원/kg × x.xxkg)";
} else {
// 수량 기반: 기본단가 × 수량
final_price = base_price × quantity;
note = "수량단가";
}
```
### 3.6 10단계 BOM 계산 프로세스
```
┌─────────────────────────────────────────────────────────────────────────┐
│ calculateBomWithDebug() 10단계 프로세스 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Step 1. 입력값 수집 │
│ W0, H0, QTY, PC, GT, MP, CT, WS, INSP + 완제품코드 │
│ │
│ Step 2. 완제품 선택 │
│ items 테이블에서 FG(완제품) 조회 │
│ → code, name, item_category, bom(JSON) 확인 │
│ │
│ Step 3. 변수 계산 │
│ W1, H1, M, K 계산 (3.4 참조) │
│ │
│ Step 4. BOM 전개 │
│ 완제품의 bom JSON → 자식 품목 목록 생성 │
│ 재귀적으로 반제품(SF, PT) 하위 BOM 포함 │
│ │
│ Step 5. 단가 출처 결정 │
│ 각 품목의 item_category → CategoryGroup 매칭 │
│ → multiplier_variable 결정 (M/K/null) │
│ │
│ Step 6. 수량 수식 평가 │
│ BOM의 quantityFormula 평가 (예: "M", "W0/1000", "1") │
│ │
│ Step 7. 금액 계산 │
│ 면적/중량 기반: final_price = base_price × M|K │
│ 수량 기반: total_price = quantity × unit_price │
│ │
│ Step 8. 공정별 그룹화 │
│ items.process_type으로 그룹화 │
│ screen, bending, steel, electric, assembly, other │
│ │
│ Step 9. 소계 계산 │
│ 공정별 subtotal 합산 │
│ │
│ Step 10. 최종 합계 │
│ grand_total = sum(all item total_price) │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
### 3.7 수식 평가 함수 (evaluate)
```php
// 지원 함수 목록
$supportedFunctions = ['SUM', 'ROUND', 'CEIL', 'FLOOR', 'ABS', 'MIN', 'MAX', 'IF', 'AND', 'OR', 'NOT'];
// 평가 과정
1. substituteVariables() - 변수명 값으로 치환
: "W0 + 140" "3000 + 140"
2. processFunctions() - 함수 처리
- ROUND(value, decimals) round(value, decimals)
- SUM(a, b, c) a + b + c
- IF(condition, true_val, false_val) 조건 평가 결과 반환
- MIN/MAX(a, b, ...) 최소/최대값
- ABS/CEIL/FLOOR(value) 수학 함수
3. calculateExpression() - 최종 계산
- 안전한 수식 평가 (숫자, 연산자, 괄호만 허용)
- eval() 사용 (프로덕션에서는 expression-language 라이브러리 권장)
```
### 3.8 단가 조회 우선순위
```php
function getItemPrice(string $itemCode): float
{
// 1차: prices 테이블에서 판매단가 조회
$price = Price::getSalesPriceByItemCode($tenantId, $itemCode);
if ($price > 0) return $price;
// 2차 Fallback: items.attributes.salesPrice에서 조회
$item = DB::table('items')
->where('tenant_id', $tenantId)
->where('code', $itemCode)
->first();
if ($item && $item->attributes) {
$attributes = json_decode($item->attributes, true);
return (float) ($attributes['salesPrice'] ?? 0);
}
return 0;
}
```
### 3.9 BOM JSON 구조
```json
// items.bom 필드 (완제품/반제품)
[
{
"child_item_id": 123, // 또는
"item_code": "PT-001", // childItemCode
"quantity": 1, // 또는
"quantityFormula": "M" // 수식 (면적 기반)
},
{
"childItemCode": "RM-002",
"quantityFormula": "W0/1000", // 폭 기반 수량
"quantity": 1
}
]
```
### 3.10 API 응답 구조 (목표)
```json
{
"success": true,
"data": {
"finished_goods": {
"code": "FG-SCR-001",
"name": "스크린셔터 3000x2500",
"item_category": "SCREEN"
},
"variables": {
"W0": 3000, "H0": 2500,
"W1": 3140, "H1": 2850,
"M": 8.949, "K": 60.598
},
"items": [
{
"item_code": "RM-FABRIC-01",
"item_name": "스크린 원단",
"item_category": "원단",
"quantity": 8.949,
"unit_price": 213465,
"total_price": 1910203,
"calculation_note": "면적단가 (213,465원/㎡ × 8.949㎡)",
"category_group": "area_based"
}
],
"grouped_items": {
"screen": {
"name": "스크린 공정",
"items": [...],
"subtotal": 1910203
}
},
"subtotals": {
"screen": { "count": 3, "subtotal": 1910203 },
"electric": { "count": 2, "subtotal": 500000 }
},
"grand_total": 2410203
}
}
```
---
## 4. 현재 시스템 분석
### 4.1 MNG FormulaEvaluatorService 핵심 기능
```php
// 1. 수식 검증
validateFormula(string $formula): array
// 2. 수식 평가 (변수 치환 + 함수 처리)
evaluate(string $formula, array $variables): mixed
// 3. 범위 기반 수식 평가 (조건에 따른 결과)
evaluateRange(QuoteFormula $formula, array $variables): mixed
// 4. 전체 수식 실행 (카테고리별 순서대로)
executeAll(Collection $formulasByCategory, array $inputVariables): array
// 5. BOM 기반 계산 (디버깅 포함)
calculateBomWithDebug(string $fgCode, array $inputVars, int $tenantId): array
// 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
```
### 3.2 React QuoteItem 인터페이스
```typescript
interface QuoteItem {
id: string;
floor: string; // 층수
code: string; // 부호
productCategory: string; // PC (제품 카테고리)
productName: string; // 제품명
openWidth: string; // W0 (오픈사이즈 가로)
openHeight: string; // H0 (오픈사이즈 세로)
guideRailType: string; // GT (가이드레일 설치유형)
motorPower: string; // MP (모터 전원)
controller: string; // CT (연동제어기)
quantity: number; // QTY (수량)
wingSize: string; // WS (마구리 날개치수)
inspectionFee: number; // INSP (검사비)
unitPrice?: number; // 단가 (계산 결과)
totalAmount?: number; // 합계 (계산 결과)
}
```
### 3.3 API 요청/응답 구조 (목표)
**요청:**
```json
{
"items": [
{
"product_code": "SCR-001",
"W0": 3000,
"H0": 2500,
"QTY": 1,
"GT": "벽면형",
"MP": "220V",
"CT": "단독",
"WS": 50,
"INSP": 50000
}
]
}
```
**응답:**
```json
{
"success": true,
"data": {
"items": [
{
"product_code": "SCR-001",
"inputs": { "W0": 3000, "H0": 2500, ... },
"outputs": {
"W1": 3140,
"H1": 2850,
"M": 8.95,
"K": 0
},
"bom_items": [
{
"item_code": "SF-SCR-F01",
"item_name": "스크린 원단",
"quantity": 8.95,
"unit_price": 213465,
"total_price": 1910486
}
],
"costs": {
"material_cost": 1910486,
"labor_cost": 0,
"install_cost": 50000,
"subtotal": 1960486
}
}
],
"summary": {
"total_material": 1910486,
"total_labor": 0,
"total_install": 50000,
"grand_total": 1960486
}
}
}
```
---
## 4. 상세 작업 내용
### 4.1 Phase 1: API 계산 로직 구현
#### 1.1 QuoteCalculationService MNG 로직 동기화
**현재 상태:**
- `api/app/Services/Quote/QuoteCalculationService.php` 존재
- MNG `FormulaEvaluatorService`와 동기화 안됨
**목표 상태:**
- MNG 로직을 API로 완전 이전
- DB 기반 수식/단가 조회
- CategoryGroup 활용한 계산 방식 결정
**수정 사항:**
- [ ] ✅ FormulaEvaluatorService 메서드 이전
- [ ] ✅ DB 연결 (quote_formulas, items, category_groups 테이블)
- [ ] ⚠️ 계산 로직 구현 (컨펌 필요)
---
## 5. 컨펌 대기 목록
| # | 항목 | 변경 내용 | 영향 범위 | 상태 |
|---|------|----------|----------|------|
| 1 | API 계산 로직 | MNG FormulaEvaluatorService 기반 구현 | api | ⚠️ 컨펌 필요 |
| 2 | DB 연결 방식 | MNG DB 직접 조회 vs API DB 복제 | api, database | ⚠️ 컨펌 필요 |
---
## 6. 변경 이력
| 날짜 | 항목 | 변경 내용 | 파일 | 승인 |
|------|------|----------|------|------|
| 2025-12-30 | 초안 | 계획 문서 작성 | - | - |
| 2025-12-30 | MNG 로직 상세 | 섹션 3 추가: API 재구현을 위한 MNG 핵심 로직 상세 명세 | docs/plans/quote-calculation-api-plan.md | - |
---
## 7. 참고 문서
- **MNG 시뮬레이터**: `mng/resources/views/quote-formulas/simulator.blade.php`
- **MNG 계산 로직**: `mng/app/Services/Quote/FormulaEvaluatorService.php`
- **API 컨트롤러**: `api/app/Http/Controllers/Api/V1/QuoteController.php`
- **React 컴포넌트**: `react/src/components/quotes/QuoteRegistration.tsx`
- **기존 시뮬레이터 계획**: `docs/plans/simulator-ui-enhancement-plan.md`
---
## 8. 세션 및 메모리 관리 정책
### 8.1 세션 시작 시 (Load Strategy)
```javascript
read_memory("quote-calc-api-state") // 1. 상태 파악
read_memory("quote-calc-api-snapshot") // 2. 사고 흐름 복구
```
### 8.2 Serena 메모리 구조
- `quote-calc-api-state`: { phase, progress, next_step, last_decision }
- `quote-calc-api-snapshot`: 현재까지의 논의 및 코드 변경점 요약
---
## 9. 검증 결과
> 작업 완료 후 이 섹션에 검증 결과 추가
### 9.1 테스트 케이스
| 입력값 | 예상 결과 | 실제 결과 | 상태 |
|--------|----------|----------|------|
| W0=3000, H0=2500, QTY=1 | MNG와 동일 | - | ⏳ |
| W0=1200, H0=2400, QTY=10 | MNG와 동일 | - | ⏳ |
### 9.2 성공 기준 달성 현황
| 기준 | 달성 | 비고 |
|------|------|------|
| API 계산 로직 MNG와 동일 | ⏳ | FormulaEvaluatorService 기반 |
| React에서 API 호출 성공 | ⏳ | handleAutoCalculate 연동 |
| 계산 결과 UI 표시 | ⏳ | 품목/단가/금액 표시 |
---
*이 문서는 /sc:plan 스킬로 생성되었습니다.*

View File

@@ -1,29 +1,40 @@
# SAM API 페이지네이션 정책 # SAM API 페이지네이션 정책
**작성일**: 2025-12-09 **작성일**: 2025-12-09
**수정일**: 2025-12-30
**적용 범위**: 모든 목록 조회 API **적용 범위**: 모든 목록 조회 API
--- ---
## 1. 응답 구조 ## 1. 응답 구조
### 1.1 표준 페이지네이션 응답 ### 1.1 표준 페이지네이션 응답 (Laravel 기본)
```json ```json
{ {
"success": true, "success": true,
"message": "조회되었습니다.", "message": "조회되었습니다.",
"data": [ "data": {
{ "id": 1, "name": "항목1", ... },
{ "id": 2, "name": "항목2", ... }
],
"pagination": {
"current_page": 1, "current_page": 1,
"per_page": 20, "data": [
"total": 100, { "id": 1, "name": "항목1" },
"last_page": 5, { "id": 2, "name": "항목2" }
],
"first_page_url": "http://sam.kr/api/v1/items?page=1",
"from": 1, "from": 1,
"to": 20 "last_page": 5,
"last_page_url": "http://sam.kr/api/v1/items?page=5",
"links": [
{ "url": null, "label": "&laquo; Previous", "active": false },
{ "url": "http://sam.kr/api/v1/items?page=1", "label": "1", "active": true },
{ "url": "http://sam.kr/api/v1/items?page=2", "label": "Next &raquo;", "active": false }
],
"next_page_url": "http://sam.kr/api/v1/items?page=2",
"path": "http://sam.kr/api/v1/items",
"per_page": 20,
"prev_page_url": null,
"to": 20,
"total": 100
} }
} }
``` ```
@@ -34,19 +45,25 @@
|----|------|------| |----|------|------|
| `success` | boolean | 요청 성공 여부 | | `success` | boolean | 요청 성공 여부 |
| `message` | string | 응답 메시지 (i18n 키) | | `message` | string | 응답 메시지 (i18n 키) |
| `data` | array | **데이터 배열** (바로 접근 가능) | | `data` | object | **페이지네이션 객체** (Laravel 기본) |
| `pagination` | object | **페이지네이션 정보** |
### 1.3 pagination 객체 필드 ### 1.3 data 객체 필드
| 필드 | 타입 | 설명 | | 필드 | 타입 | 설명 |
|------|------|------| |------|------|------|
| `current_page` | int | 현재 페이지 번호 (1부터 시작) | | `current_page` | int | 현재 페이지 번호 (1부터 시작) |
| `data` | array | **실제 데이터 배열** |
| `per_page` | int | 페이지당 항목 수 | | `per_page` | int | 페이지당 항목 수 |
| `total` | int | 전체 항목 수 | | `total` | int | 전체 항목 수 |
| `last_page` | int | 마지막 페이지 번호 | | `last_page` | int | 마지막 페이지 번호 |
| `from` | int\|null | 현재 페이지 첫 번째 항목 번호 | | `from` | int\|null | 현재 페이지 첫 번째 항목 번호 |
| `to` | int\|null | 현재 페이지 마지막 항목 번호 | | `to` | int\|null | 현재 페이지 마지막 항목 번호 |
| `first_page_url` | string | 첫 페이지 URL |
| `last_page_url` | string | 마지막 페이지 URL |
| `next_page_url` | string\|null | 다음 페이지 URL |
| `prev_page_url` | string\|null | 이전 페이지 URL |
| `path` | string | 기본 경로 |
| `links` | array | 페이지 링크 배열 |
--- ---
@@ -73,21 +90,11 @@ GET /api/v1/items?page=2&size=50&search=스크린
### 3.1 Controller 구현 ### 3.1 Controller 구현
```php ```php
public function index(Request $request) public function index(IndexRequest $request)
{ {
$items = $this->service->getItems($request->all()); $items = $this->service->getItems($request->validated());
return ApiResponse::success([ return ApiResponse::success($items, __('message.fetched'));
'data' => $items->items(),
'pagination' => [
'current_page' => $items->currentPage(),
'per_page' => $items->perPage(),
'total' => $items->total(),
'last_page' => $items->lastPage(),
'from' => $items->firstItem(),
'to' => $items->lastItem(),
],
], __('message.fetched'));
} }
``` ```
@@ -105,65 +112,7 @@ public function getItems(array $params): LengthAwarePaginator
} }
``` ```
### 3.3 Helper 함수 (권장) ### 3.3 최대 페이지 크기 제한
`ApiResponse` 클래스에 페이지네이션 헬퍼 추가:
```php
// app/Helpers/ApiResponse.php
public static function paginated(LengthAwarePaginator $paginator, string $message = null): JsonResponse
{
return self::success([
'data' => $paginator->items(),
'pagination' => [
'current_page' => $paginator->currentPage(),
'per_page' => $paginator->perPage(),
'total' => $paginator->total(),
'last_page' => $paginator->lastPage(),
'from' => $paginator->firstItem(),
'to' => $paginator->lastItem(),
],
], $message ?? __('message.fetched'));
}
```
**사용 예시:**
```php
public function index(Request $request)
{
$items = $this->service->getItems($request->all());
return ApiResponse::paginated($items);
}
```
---
## 4. 설계 원칙
### 4.1 data는 항상 배열
```json
// ✅ 올바른 형식 - data가 바로 배열
{
"data": [...]
}
// ❌ 잘못된 형식 - data.data 중첩
{
"data": {
"data": [...]
}
}
```
### 4.2 pagination 분리
- 페이지네이션 정보는 `pagination` 객체로 명확히 분리
- Laravel 기본 형식(first_page_url, links 등) 대신 필수 정보만 포함
- 응답 경량화 및 프론트엔드 접근 편의성 확보
### 4.3 최대 페이지 크기 제한
```php ```php
$size = min($params['size'] ?? 20, 100); // 최대 100개 $size = min($params['size'] ?? 20, 100); // 최대 100개
@@ -175,27 +124,37 @@ $size = min($params['size'] ?? 20, 100); // 최대 100개
--- ---
## 5. 프론트엔드 사용 가이드 ## 4. 프론트엔드 사용 가이드
### 5.1 TypeScript 타입 정의 ### 4.1 TypeScript 타입 정의
```typescript ```typescript
interface PaginatedResponse<T> { interface PaginatedResponse<T> {
success: boolean; success: boolean;
message: string; message: string;
data: T[]; data: {
pagination: {
current_page: number; current_page: number;
per_page: number; data: T[];
total: number; first_page_url: string;
last_page: number;
from: number | null; from: number | null;
last_page: number;
last_page_url: string;
links: Array<{
url: string | null;
label: string;
active: boolean;
}>;
next_page_url: string | null;
path: string;
per_page: number;
prev_page_url: string | null;
to: number | null; to: number | null;
total: number;
}; };
} }
``` ```
### 5.2 React 사용 예시 ### 4.2 React 사용 예시
```typescript ```typescript
const fetchItems = async (page: number, size: number) => { const fetchItems = async (page: number, size: number) => {
@@ -203,84 +162,49 @@ const fetchItems = async (page: number, size: number) => {
params: { page, size } params: { page, size }
}); });
// 데이터 직접 접근 // 데이터 접근
const items = response.data.data; const items = response.data.data.data;
// 페이지네이션 정보 // 페이지네이션 정보
const { current_page, total, last_page } = response.data.pagination; const { current_page, total, last_page } = response.data.data;
return { items, pagination: response.data.pagination }; return { items, pagination: response.data.data };
}; };
``` ```
--- ### 4.3 간편 접근 패턴
## 6. 기존 API 마이그레이션 ```typescript
// 구조 분해로 간결하게 사용
### 6.1 변경 전 (Laravel 기본) const { data: pagination } = response.data;
const items = pagination.data;
```json const { current_page, total, last_page, per_page } = pagination;
{
"success": true,
"data": {
"current_page": 1,
"data": [...],
"first_page_url": "http://...",
"from": 1,
"last_page": 5,
"links": [...],
"next_page_url": "http://...",
"path": "http://...",
"per_page": 20,
"prev_page_url": null,
"to": 20,
"total": 100
}
}
``` ```
### 6.2 변경 후 (SAM 표준) ---
```json ## 5. 설계 원칙
{
"success": true,
"message": "조회되었습니다.",
"data": [...],
"pagination": {
"current_page": 1,
"per_page": 20,
"total": 100,
"last_page": 5,
"from": 1,
"to": 20
}
}
```
### 6.3 Breaking Changes ### 5.1 Laravel 기본 형식 유지
| 항목 | 변경 전 | 변경 후 | - 프레임워크 기본 동작 활용
|------|---------|---------| - 추가 변환 로직 불필요
| 데이터 접근 | `response.data.data` | `response.data` | - 생태계 호환성 유지
| 페이지 정보 | `response.data.current_page` | `response.pagination.current_page` |
| URL 정보 | 포함 (first_page_url 등) | 제거 | ### 5.2 일관성 우선
| links 배열 | 포함 | 제거 |
- 기존 API와 동일한 형식 유지
- 프론트엔드 코드 재사용
- 학습 비용 최소화
### 5.3 최대 페이지 크기 제한
- 서버 부하 방지
- 응답 시간 최적화
- DoS 공격 방어
--- ---
## 7. 적용 API 목록 ## 6. 관련 문서
| API | 상태 | 비고 | - [API 개발 규칙](./api-rules.md)
|-----|------|------|
| `GET /api/v1/items` | 🔧 예정 | Items API 통합 작업 시 적용 |
| `GET /api/v1/products` | ⏳ 대기 | |
| `GET /api/v1/materials` | ⏳ 대기 | |
| `GET /api/v1/pricing` | ⏳ 대기 | |
| `GET /api/v1/employees` | ⏳ 대기 | |
---
## 8. 관련 문서
- [API 개발 규칙](./api-rules.md)
- [Items API 통합 계획](../plans/items-api-unified-plan.md)