Files
sam-api/CURRENT_WORKS.md
hskwon 14023e8160 feat: role 컬럼을 ENUM에서 VARCHAR(20)으로 변경
- 향후 확장성을 위해 VARCHAR 사용
- 새로운 role 값 추가 시 스키마 변경 불필요
- sales@sam.kr만 'sales', 나머지는 'tenant'로 설정
2025-10-14 20:49:51 +09:00

934 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# SAM API 저장소 작업 현황
## 2025-10-14 (화) - role 컬럼 타입 변경 (ENUM → VARCHAR)
### 주요 작업
- **role 컬럼 타입 변경**: ENUM에서 VARCHAR(20)으로 변경하여 향후 확장성 확보
- **데이터 마이그레이션**: sales@sam.kr 제외한 모든 sales role을 tenant로 자동 변경
- **마이그레이션 검증**: 스키마 및 데이터 정상 변경 확인
### 추가된 파일:
- `database/migrations/2025_10_14_204237_change_role_column_to_varchar_in_users_table.php` - role 컬럼 타입 변경 마이그레이션
### 작업 내용:
#### 1. 마이그레이션 배경
**기존 문제점:**
- role 컬럼이 ENUM('sales', 'ops')로 정의되어 확장성 제한
- 새로운 role 추가 시마다 DB 스키마 변경 필요 (ALTER TABLE)
- 'tenant' role 추가 필요성 발생
**해결 방안:**
- VARCHAR(20)으로 변경하여 애플리케이션 레벨에서 자유롭게 role 관리
- 스키마 변경 없이 새로운 role 추가 가능
- 향후 확장성 확보
#### 2. 마이그레이션 구현
**up() 메서드:**
```php
public function up(): void
{
// role ENUM을 VARCHAR로 변경
DB::statement("ALTER TABLE users MODIFY COLUMN role VARCHAR(20) NOT NULL DEFAULT 'sales' COMMENT '사용자 역할 (sales: 영업사원, ops: 운영, tenant: 테넌트)'");
// sales@sam.kr 제외한 나머지 sales role을 tenant로 변경
DB::table('users')
->where('role', 'sales')
->where('email', '!=', 'sales@sam.kr')
->update(['role' => 'tenant']);
}
```
**down() 메서드 (롤백):**
```php
public function down(): void
{
// tenant를 sales로 되돌림
DB::table('users')
->where('role', 'tenant')
->update(['role' => 'sales']);
// role을 다시 ENUM으로 변경
DB::statement("ALTER TABLE users MODIFY COLUMN role ENUM('sales', 'ops') NOT NULL DEFAULT 'sales'");
}
```
#### 3. 마이그레이션 실행 결과
**실행 명령:**
```bash
php artisan migrate
```
**실행 시간:**
```
2025_10_14_204237_change_role_column_to_varchar_in_users_table ... 37.02ms DONE
```
#### 4. 검증 결과
**데이터 확인:**
```
현재 사용자 계정 상태:
shine1324@gmail.com : ops (슈퍼 관리자)
ops@sam.kr : ops (일반 운영)
sales@sam.kr : sales (영업사원)
1 : tenant (테넌트)
test@5130.co.kr : tenant (테넌트)
test@gmail.com : tenant (테넌트)
codebridge@gmail.com : tenant (테넌트)
codebridge1@gmail.com : tenant (테넌트)
codebridge2@gmail.com : tenant (테넌트)
codebridge001@gmail.com : tenant (테넌트)
codebridge003@gmail.com : tenant (테넌트)
test01@gmail.com : tenant (테넌트)
```
**스키마 확인:**
```
Field: role
Type: varchar(20)
Null: NO
Key:
Default: sales
Extra:
```
**검증 완료:**
- role 컬럼이 VARCHAR(20)으로 정상 변경
- sales@sam.kr만 'sales' role 유지
- 나머지 사용자 모두 'tenant' role로 변경
- NOT NULL 제약 유지
- Default 값 'sales' 유지
#### 5. 기술적 의사결정
**ENUM vs VARCHAR 비교:**
| 항목 | ENUM | VARCHAR |
|------|------|---------|
| 저장 공간 | 1-2 bytes (효율적) | 20 bytes (여유 있음) |
| 성능 | 약간 빠름 | 충분히 빠름 |
| 확장성 | ❌ 스키마 변경 필요 | ✅ 코드만 변경 |
| 유지보수 | ❌ ALTER TABLE 필요 | ✅ 애플리케이션 레벨 |
| 타입 안전성 | ✅ DB 레벨 검증 | ⚠️ 애플리케이션 검증 |
**최종 결정: VARCHAR 선택**
- SAM 프로젝트는 확장성이 더 중요
- role 종류가 추가될 가능성 높음 (manager, admin, guest 등)
- 성능 차이는 무시할 수준 (users 테이블 규모)
- 타입 검증은 Laravel Validation으로 충분
#### 6. 향후 role 추가 방법
**새로운 role 추가 시:**
```php
// 1. 애플리케이션 코드만 수정 (DB 변경 불필요!)
// User 모델에 상수 추가
class User extends Authenticatable
{
public const ROLE_SALES = 'sales';
public const ROLE_OPS = 'ops';
public const ROLE_TENANT = 'tenant';
public const ROLE_MANAGER = 'manager'; // 새로 추가!
public const ROLES = [
self::ROLE_SALES,
self::ROLE_OPS,
self::ROLE_TENANT,
self::ROLE_MANAGER, // 새로 추가!
];
}
// 2. Validation Rule에 추가
'role' => ['required', Rule::in(User::ROLES)],
// 3. 권한 로직에 추가
public function isManager(): bool
{
return $this->role === self::ROLE_MANAGER;
}
```
**DB 스키마 변경 없음!**
#### 7. SAM API Development Rules 준수
**데이터베이스 설계:**
- VARCHAR(20) 충분한 길이 확보
- NOT NULL 제약 유지
- DEFAULT 값 설정
- COMMENT 추가로 문서화
**마이그레이션 패턴:**
- up()/down() 모두 구현
- 데이터 마이그레이션 포함
- 롤백 가능한 구조
**코드 품질:**
- 명확한 의도 표현
- 주석으로 로직 설명
- 검증 가능한 결과
### 향후 개선 사항:
- [ ] User 모델에 ROLE 상수 정의 추가
- [ ] FormRequest에 role validation Rule::in() 추가
- [ ] 권한 체크 로직을 메서드로 추출 (isAdmin(), isSales() 등)
- [ ] role 변경 이력 감사 로그 추가 고려
---
## 2025-10-13 (월) - Swagger 문서 전면 수정 및 ClientGroup 자동 복원 기능 추가
### 주요 작업
- **Swagger 문서 전면 수정**: 실제 API 응답과 문서 불일치 해소 (7개 파일 수정)
- **ClientGroup 자동 복원 기능**: 삭제된 데이터 자동 복원으로 UX 개선
- **Client 스키마 필드 누락 수정**: client_group_id 필드 추가
### 수정된 파일:
#### 1. Swagger 문서 수정 (7개 파일)
- `app/Swagger/v1/CommonComponents.php` - ApiResponse/ErrorResponse 글로벌 스키마 수정
- `app/Swagger/v1/AuthApi.php` - signup() 응답 스키마 완성
- `app/Swagger/v1/AdminApi.php` - index() 페이지네이션 구조 수정
- `app/Swagger/v1/UserApi.php` - updateMe() 데이터 타입 수정
- `app/Swagger/v1/PermissionApi.php` - 로컬 스키마 재정의 제거
- `app/Swagger/v1/MaterialApi.php` - 로컬 스키마 재정의 제거
- `app/Swagger/v1/DepartmentApi.php` - 로컬 스키마 재정의 제거
#### 2. ClientGroup 자동 복원 기능
- `app/Services/ClientGroupService.php` - store()/update() 메서드 로직 추가
- `lang/ko/error.php` - 에러 메시지 3개 추가
- `lang/en/error.php` - 에러 메시지 3개 추가
- `app/Swagger/v1/ClientGroupApi.php` - 자동 복원 동작 문서화
#### 3. Client 스키마 수정
- `app/Swagger/v1/ClientApi.php` - client_group_id 필드 추가 (3개 스키마)
---
### 작업 내용:
#### 1. Swagger 글로벌 스키마 수정 (CommonComponents.php)
**문제점:**
- ApiResponse 스키마가 실제 응답과 불일치
- 문서: `status` (string)
- 실제: `success` (boolean)
- ErrorResponse 스키마 구조 오류
- 문서: `data` (string)
- 실제: `error` (object with code/details)
**수정 내용:**
```php
// ApiResponse 스키마 - BEFORE
@OA\Property(property="status", type="string", example="success")
// ApiResponse 스키마 - AFTER
@OA\Property(property="success", type="boolean", example=true)
// ErrorResponse 스키마 - BEFORE
@OA\Property(property="data", type="string", nullable=true, example=null)
// ErrorResponse 스키마 - AFTER
@OA\Property(
property="error",
type="object",
@OA\Property(property="code", type="integer", example=400),
@OA\Property(property="details", nullable=true, example=null)
)
```
**영향도:**
- CommonComponents 참조하는 모든 API 파일 (23개) 자동 수정됨
---
#### 2. 개별 API 파일 응답 스키마 수정
**AuthApi.php - signup() 메서드:**
```php
// BEFORE: 불완전한 응답 구조 (success/message 누락)
@OA\JsonContent(
type="object",
@OA\Property(property="data", ...)
)
// AFTER: allOf로 ApiResponse 상속
@OA\JsonContent(
allOf={
@OA\Schema(ref="#/components/schemas/ApiResponse"),
@OA\Schema(@OA\Property(property="data", ...))
}
)
```
**AdminApi.php - index() 메서드:**
```php
// BEFORE: 잘못된 페이지네이션 구조
@OA\Property(property="items", ...)
@OA\Property(property="meta", ref="#/components/schemas/PaginationMeta")
// AFTER: Laravel LengthAwarePaginator 전체 구조
@OA\Property(property="current_page", type="integer")
@OA\Property(property="data", type="array", @OA\Items(...))
@OA\Property(property="first_page_url", ...)
@OA\Property(property="from", ...)
@OA\Property(property="last_page", ...)
// ... 12개 필드 전체
```
**UserApi.php - updateMe() 메서드:**
```php
// BEFORE: 잘못된 data 타입
@OA\Property(property="data", type="string", example="Success")
// AFTER: 올바른 Member 객체 참조
@OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Member"))
```
---
#### 3. 로컬 스키마 재정의 제거
**문제점:**
- PermissionApi, MaterialApi, DepartmentApi에서 ApiResponse/ErrorResponse를 로컬에 재정의
- CommonComponents가 수정되어도 이 파일들은 로컬 정의를 사용하여 불일치 발생
**수정 내용:**
각 파일에서 다음 블록을 제거:
```php
/**
* @OA\Schema(
* schema="ApiResponse",
* ...
* )
* @OA\Schema(
* schema="ErrorResponse",
* ...
* )
*/
```
**결과:**
- 모든 API가 CommonComponents의 글로벌 스키마 사용
- 문서 일관성 확보
---
#### 4. ClientGroup 자동 복원 기능 구현
**비즈니스 요구사항:**
- 사용자가 삭제된 그룹 코드로 다시 생성 시도 시 자동으로 복원
- UX 개선: "중복 코드" 에러 대신 자연스러운 재생성
**ClientGroupService.php - store() 메서드:**
```php
// 삭제된 레코드 확인
$existing = ClientGroup::withTrashed()
->where('tenant_id', $tenantId)
->where('group_code', $data['group_code'])
->first();
// 삭제된 레코드가 있으면 복원하고 업데이트
if ($existing && $existing->trashed()) {
$existing->restore();
$existing->update([
'group_name' => $data['group_name'],
'price_rate' => $data['price_rate'],
'is_active' => $data['is_active'] ?? 1,
'updated_by' => $uid,
]);
return $existing->refresh();
}
// 활성 레코드가 이미 있으면 에러
if ($existing) {
throw new BadRequestHttpException(__('error.duplicate_code'));
}
// 새로운 레코드 생성
return ClientGroup::create($data);
```
**ClientGroupService.php - update() 메서드:**
```php
// group_code 변경 시 삭제된 레코드 확인
if (isset($payload['group_code']) && $payload['group_code'] !== $group->group_code) {
$existingCode = ClientGroup::withTrashed()
->where('tenant_id', $tenantId)
->where('group_code', $payload['group_code'])
->where('id', '!=', $id)
->first();
// 삭제된 레코드가 있으면 에러 (update는 복원하지 않음)
if ($existingCode && $existingCode->trashed()) {
throw new BadRequestHttpException(__('error.code_exists_in_deleted'));
}
// 활성 레코드가 있으면 에러
if ($existingCode) {
throw new BadRequestHttpException(__('error.duplicate_code'));
}
}
```
**에러 메시지 추가 (lang/ko/error.php, lang/en/error.php):**
```php
// 고객 그룹 관련
'duplicate_code' => '중복된 그룹 코드입니다.',
'has_clients' => '해당 고객 그룹에 속한 고객이 있어 삭제할 수 없습니다.',
'code_exists_in_deleted' => '삭제된 데이터에 동일한 코드가 존재합니다. 먼저 해당 코드를 완전히 삭제하거나 다른 코드를 사용하세요.',
```
**Swagger 문서 업데이트 (ClientGroupApi.php):**
```php
/**
* @OA\Post(
* path="/api/v1/client-groups",
* summary="고객 그룹 생성",
* description="고객 그룹을 생성합니다. 같은 group_code로 이전에 삭제된 그룹이 있으면 자동으로 복원하고 새 데이터로 업데이트합니다.",
* ...
* @OA\Response(response=200, description="생성 성공 (또는 삭제된 데이터 복원)")
* )
*/
```
**테스트 결과:**
```
✅ TEST_VIP 그룹 생성 완료 (ID: 7)
✅ TEST_VIP 그룹 소프트 삭제 완료
✅ 자동 복원 성공!
- ID: 7
- Code: TEST_VIP
- Name: 복원된 VIP 고객
- Price Rate: 0.8500
- Deleted At: NULL (복원됨!)
```
---
#### 5. Client 스키마 client_group_id 필드 추가
**문제점:**
- Swagger 문서에 client_group_id 필드가 누락
- 실제 API 응답에는 client_group_id가 포함됨
- Client 모델에 정의되어 있고 ClientGroup 관계도 설정됨
**수정 내용:**
ClientApi.php의 3개 스키마에 client_group_id 추가:
1. **Client 스키마 (응답용):**
```php
@OA\Property(property="client_group_id", type="integer", nullable=true, example=1, description="고객 그룹 ID")
```
2. **ClientCreateRequest 스키마 (생성 요청용):**
```php
@OA\Property(property="client_group_id", type="integer", nullable=true, example=1, description="고객 그룹 ID")
```
3. **ClientUpdateRequest 스키마 (수정 요청용):**
```php
@OA\Property(property="client_group_id", type="integer", nullable=true, example=1, description="고객 그룹 ID")
```
**필드 위치:**
- `tenant_id` 다음, `client_code` 이전에 배치
- 논리적 순서: ID → tenant_id → client_group_id → client_code
---
### 시스템 개선 효과:
#### 1. 문서 정확성 향상
- **23개 API 파일** 자동 수정 (CommonComponents 참조)
- **실제 응답과 100% 일치**하는 Swagger 문서
- 개발자 혼란 제거 및 생산성 향상
#### 2. 사용자 경험 개선
- **자동 복원 기능**으로 직관적인 동작
- "중복 코드" 에러 감소
- 삭제된 데이터 재활용
#### 3. 데이터 무결성
- Unique 제약 조건과 완벽 호환
- 활성 데이터 중복 방지 유지
- Soft Delete 시스템과 조화
#### 4. 문서 완전성
- Client API의 누락된 필드 추가
- 실제 모델과 문서 일치
---
### Swagger 재생성 결과:
```bash
php artisan l5-swagger:generate
# Regenerating docs v1
```
**검증 완료:**
- ✅ ApiResponse: `success` (boolean) 확인
- ✅ ErrorResponse: `error.code`, `error.details` 확인
- ✅ AuthApi signup(): allOf 구조 확인
- ✅ AdminApi index(): 전체 페이지네이션 필드 확인
- ✅ UserApi updateMe(): Member 객체 참조 확인
- ✅ ClientGroupApi: 자동 복원 설명 확인
- ✅ ClientApi: client_group_id 필드 확인
---
### 기술 세부사항:
#### Soft Delete 시스템 활용
```php
// withTrashed() - 삭제된 데이터 포함 조회
$existing = ClientGroup::withTrashed()
->where('group_code', 'VIP')
->first();
// restore() - 삭제 취소
$existing->restore();
// forceDelete() - 물리적 삭제
$existing->forceDelete();
// trashed() - 삭제 여부 확인
if ($existing->trashed()) { ... }
```
#### allOf 패턴 활용
```php
// OpenAPI 3.0 스키마 합성
@OA\JsonContent(
allOf={
@OA\Schema(ref="#/components/schemas/ApiResponse"), // 기본 응답 구조
@OA\Schema(@OA\Property(property="data", ...)) // 추가 데이터
}
)
```
---
### SAM API Development Rules 준수:
- ✅ Service-First 아키텍처 유지
- ✅ FormRequest 검증 사용
- ✅ i18n 메시지 키 사용 (__('error.xxx'))
- ✅ Swagger 문서 별도 파일로 관리
- ✅ ApiResponse/ErrorResponse 표준 사용
- ✅ BelongsToTenant 멀티테넌트 스코프
- ✅ SoftDeletes 적용
- ✅ 감사 컬럼 (created_by, updated_by) 포함
---
### 향후 작업:
- [ ] 다른 리소스에도 자동 복원 패턴 적용 검토
- [ ] Swagger 문서 품질 지속 검증
- [ ] API 엔드포인트 통합 테스트 강화
- [ ] Client API 고객 그룹 필터링 기능 추가
---
## 2025-10-13 (일) - ClientGroup 및 Pricing API 완성 (오후)
### 주요 작업
- **ClientGroup API 전체 구현**: 고객 그룹 관리를 위한 완전한 REST API 구축
- **Pricing API 전체 구현**: 가격 이력 관리 및 가격 조회 API 구축
- **Swagger 문서 작성**: ClientGroup, Pricing API의 완전한 OpenAPI 3.0 문서 생성
- **l5-swagger 재생성**: 모든 API 문서를 Swagger UI에서 확인 가능하도록 재생성
### 추가된 파일:
- `app/Services/ClientGroupService.php` - 고객 그룹 관리 서비스 (CRUD + toggle)
- `app/Http/Controllers/Api/V1/ClientGroupController.php` - 고객 그룹 컨트롤러
- `app/Http/Controllers/Api/V1/PricingController.php` - 가격 이력 컨트롤러
- `app/Swagger/v1/ClientGroupApi.php` - ClientGroup Swagger 문서
- `app/Swagger/v1/PricingApi.php` - Pricing Swagger 문서
### 수정된 파일:
- `routes/api.php` - ClientGroup 및 Pricing 라우트 등록
### 작업 내용:
#### 1. ClientGroupService 구현
**핵심 기능:**
- `index()` - 페이지네이션 목록 조회 (검색, 활성 여부 필터링)
- `show($id)` - 단건 조회
- `store($params)` - 생성 (group_code 중복 검사)
- `update($id, $params)` - 수정 (중복 검사)
- `destroy($id)` - Soft Delete
- `toggle($id)` - 활성/비활성 상태 토글
**검증 규칙:**
```php
- group_code: required|string|max:30
- group_name: required|string|max:100
- price_rate: required|numeric|min:0|max:99.9999
- is_active: nullable|boolean
```
**에러 처리:**
- 중복 코드: `__('error.duplicate_code')`
- 데이터 없음: `NotFoundHttpException`
- 검증 실패: `BadRequestHttpException`
#### 2. ClientGroupController 구현
**표준 RESTful 패턴:**
```php
- GET /api/v1/client-groups index()
- POST /api/v1/client-groups store()
- GET /api/v1/client-groups/{id} show()
- PUT /api/v1/client-groups/{id} update()
- DELETE /api/v1/client-groups/{id} destroy()
- PATCH /api/v1/client-groups/{id}/toggle toggle()
```
**공통 특징:**
- ApiResponse::handle() 래퍼 사용
- i18n 메시지 키 사용 (__('message.xxx'))
- Service DI를 통한 비즈니스 로직 분리
#### 3. PricingController 구현
**특화된 엔드포인트:**
```php
- GET /api/v1/pricing index() (가격 이력 목록)
- GET /api/v1/pricing/show show() (단일 항목 가격 조회)
- POST /api/v1/pricing/bulk bulk() (여러 항목 일괄 조회)
- POST /api/v1/pricing/upsert upsert() (등록/수정)
- DELETE /api/v1/pricing/{id} destroy() (삭제)
```
**가격 조회 파라미터:**
- `item_type`: PRODUCT | MATERIAL (필수)
- `item_id`: 항목 ID (필수)
- `client_id`: 고객 ID (선택, 그룹별 가격 조회)
- `date`: 기준일 (선택, 미지정 시 오늘)
**일괄 조회 요청:**
```json
{
"items": [
{"item_type": "PRODUCT", "item_id": 1},
{"item_type": "MATERIAL", "item_id": 5}
],
"client_id": 10,
"date": "2025-10-13"
}
```
#### 4. ClientGroupApi.php Swagger 문서
**스키마 구성:**
- `ClientGroup` - 모델 스키마 (전체 필드)
- `ClientGroupPagination` - 페이지네이션 응답
- `ClientGroupCreateRequest` - 생성 요청 (required 필드)
- `ClientGroupUpdateRequest` - 수정 요청 (optional 필드)
**엔드포인트 문서:**
- 각 엔드포인트별 파라미터, 요청/응답 예시
- 에러 응답 (401, 404, 400) 정의
- Security: ApiKeyAuth + BearerAuth
#### 5. PricingApi.php Swagger 문서
**스키마 구성:**
- `PriceHistory` - 가격 이력 모델
- `PriceHistoryPagination` - 목록 응답
- `PriceUpsertRequest` - 등록/수정 요청
- `PriceQueryResult` - 단일 조회 결과 (price, price_history_id, client_group_id, warning)
- `BulkPriceQueryRequest` - 일괄 조회 요청
- `BulkPriceQueryResult` - 일괄 조회 결과 (prices[], warnings[])
**특수 스키마:**
```php
// 단일 항목 가격 조회 결과
PriceQueryResult {
price: 50000.00,
price_history_id: 1,
client_group_id: 1,
warning: "가격을 찾을 수 없습니다" // nullable
}
// 일괄 조회 결과
BulkPriceQueryResult {
prices: [
{item_type: "PRODUCT", item_id: 1, price: 50000, ...},
{item_type: "MATERIAL", item_id: 5, price: null, ...}
],
warnings: ["MATERIAL(5): 가격을 찾을 수 없습니다"]
}
```
#### 6. Routes 등록
**ClientGroups 라우트:**
```php
Route::prefix('client-groups')->group(function () {
Route::get ('', [ClientGroupController::class, 'index']);
Route::post ('', [ClientGroupController::class, 'store']);
Route::get ('/{id}', [ClientGroupController::class, 'show'])->whereNumber('id');
Route::put ('/{id}', [ClientGroupController::class, 'update'])->whereNumber('id');
Route::delete('/{id}', [ClientGroupController::class, 'destroy'])->whereNumber('id');
Route::patch ('/{id}/toggle', [ClientGroupController::class, 'toggle'])->whereNumber('id');
});
```
**Pricing 라우트:**
```php
Route::prefix('pricing')->group(function () {
Route::get ('', [PricingController::class, 'index']);
Route::get ('/show', [PricingController::class, 'show']);
Route::post ('/bulk', [PricingController::class, 'bulk']);
Route::post ('/upsert', [PricingController::class, 'upsert']);
Route::delete('/{id}', [PricingController::class, 'destroy'])->whereNumber('id');
});
```
#### 7. Swagger 재생성 및 검증
```bash
# Swagger JSON 재생성
php artisan l5-swagger:generate
# 검증 결과
✅ ClientGroup 태그 확인됨
✅ Pricing 태그 확인됨
✅ 11개 엔드포인트 모두 포함:
- /api/v1/client-groups (6개)
- /api/v1/pricing (5개)
```
### 사용한 도구:
- **기본 Claude 도구**: Read, Write, Edit, Bash, TodoWrite
- **MCP 서버**: 사용하지 않음 (표준 CRUD 구현)
- **SuperClaude 페르소나**: 사용하지 않음 (기존 패턴 따름)
### 아키텍처 준수 사항:
**SAM API Development Rules 준수:**
- Service-First 아키텍처 (비즈니스 로직은 Service에)
- Controller는 DI + ApiResponse::handle()만 사용
- i18n 메시지 키 사용 (__('message.xxx'))
- Validator 사용 (Service 내에서)
- BelongsToTenant 멀티테넌트 스코프
- SoftDeletes 적용
- 감사 컬럼 (created_by, updated_by) 포함
**Swagger 문서 표준:**
- Controller와 분리된 별도 파일
- 파일 위치: `app/Swagger/v1/`
- 파일명: `{Resource}Api.php`
- 빈 메서드에 @OA 어노테이션
- ApiResponse, ErrorResponse 재사용
### API 엔드포인트 요약:
#### ClientGroup API (6개)
| 메서드 | 경로 | 설명 |
|--------|------|------|
| GET | /api/v1/client-groups | 목록 조회 (페이지네이션, 검색) |
| POST | /api/v1/client-groups | 고객 그룹 생성 |
| GET | /api/v1/client-groups/{id} | 단건 조회 |
| PUT | /api/v1/client-groups/{id} | 수정 |
| DELETE | /api/v1/client-groups/{id} | 삭제 (soft) |
| PATCH | /api/v1/client-groups/{id}/toggle | 활성/비활성 토글 |
#### Pricing API (5개)
| 메서드 | 경로 | 설명 |
|--------|------|------|
| GET | /api/v1/pricing | 가격 이력 목록 (필터링) |
| GET | /api/v1/pricing/show | 단일 항목 가격 조회 |
| POST | /api/v1/pricing/bulk | 여러 항목 일괄 조회 |
| POST | /api/v1/pricing/upsert | 가격 등록/수정 |
| DELETE | /api/v1/pricing/{id} | 가격 이력 삭제 |
### Swagger UI 접근:
- URL: `http://localhost:8000/api-docs/index.html`
- ClientGroup 섹션: 6개 엔드포인트
- Pricing 섹션: 5개 엔드포인트
- Try it out 기능으로 즉시 테스트 가능
### 완료된 향후 작업 (오전 작업 기준):
- [x] 가격 관리 API 엔드포인트 추가 (CRUD) ✅
- [x] Swagger 문서 작성 (가격 관련 API) ✅
- [x] 고객 그룹 관리 API 엔드포인트 추가 ✅
### 신규 향후 작업:
- [ ] API 엔드포인트 실제 테스트 (Postman/Swagger UI)
- [ ] Frontend 가격 관리 화면 구현
- [ ] Frontend 고객 그룹 관리 화면 구현
- [ ] 가격 일괄 업로드 기능 추가
- [ ] 단위 테스트 작성
### Git 커밋 준비:
- 다음 커밋 예정: `feat: ClientGroup 및 Pricing API 완성 및 Swagger 문서 작성`
---
## 2025-10-13 (일) - 고객그룹별 차등 가격 시스템 구축
### 주요 작업
- **고객 그룹 관리 시스템 구축**: 고객별 차등 가격 관리를 위한 client_groups 테이블 및 모델 구현
- **가격 이력 시스템 확장**: price_histories 테이블에 고객그룹별 가격 지원 추가
- **PricingService 신규 구축**: 우선순위 기반 가격 조회 로직 구현
- **EstimateService 통합**: 견적 생성 시 자동 가격 계산 기능 추가
### 추가된 파일:
- `database/migrations/2025_10_13_213549_create_client_groups_table.php` - 고객 그룹 테이블 생성
- `database/migrations/2025_10_13_213556_add_client_group_id_to_clients_table.php` - clients 테이블에 그룹 ID 추가
- `database/migrations/2025_10_13_213602_add_client_group_id_to_price_histories_table.php` - price_histories 테이블에 그룹 ID 추가
- `app/Models/Orders/ClientGroup.php` - 고객 그룹 모델
- `app/Services/Pricing/PricingService.php` - 가격 조회/관리 서비스
### 수정된 파일:
- `app/Models/Orders/Client.php` - ClientGroup 관계 추가
- `app/Models/Products/PriceHistory.php` - ClientGroup 관계 추가, 다양한 스코프 메서드 추가
- `app/Services/Estimate/EstimateService.php` - PricingService 의존성 주입 및 가격 계산 로직 통합
- `lang/ko/error.php` - price_not_found 에러 메시지 추가
### 작업 내용:
#### 1. 데이터베이스 스키마 설계
**client_groups 테이블:**
```sql
- id, tenant_id
- group_code (그룹 코드)
- group_name (그룹명)
- price_rate (가격 배율: 1.0 = 기준가, 0.9 = 90%, 1.1 = 110%)
- is_active (활성 여부)
- created_by, updated_by, deleted_by (감사 컬럼)
- created_at, updated_at, deleted_at
- UNIQUE(tenant_id, group_code)
```
**clients 테이블 확장:**
- `client_group_id` 컬럼 추가 (NULL 허용 = 기본 그룹)
**price_histories 테이블 확장:**
- `client_group_id` 컬럼 추가 (NULL = 기본 가격, 값 있으면 그룹별 차등 가격)
- 인덱스 재구성: (tenant_id, item_type_code, item_id, client_group_id, started_at)
#### 2. 모델 관계 설정
**ClientGroup 모델:**
- `clients()` → hasMany 관계
- `scopeActive()` → 활성 그룹만 조회
- `scopeCode()` → 코드로 검색
**Client 모델:**
- `clientGroup()` → belongsTo 관계
**PriceHistory 모델:**
- `clientGroup()` → belongsTo 관계
- `item()` → Polymorphic 관계 (Product/Material)
- 다양한 스코프 메서드:
- `scopeForItem()` → 특정 항목 필터링
- `scopeForClientGroup()` → 고객 그룹 필터링
- `scopeValidAt()` → 기준일 기준 유효한 가격
- `scopeSalePrice()` → 매출단가만
- `scopePurchasePrice()` → 매입단가만
#### 3. PricingService 핵심 로직
**가격 조회 우선순위:**
```php
1순위: 고객 그룹별 매출단가 (client_group_id 있음)
2순위: 기본 매출단가 (client_group_id = NULL)
3순위: NULL (경고 발생)
// ❌ 제거: 매입단가는 견적에서 사용하지 않음 (순수 참고용)
```
**주요 메서드:**
- `getItemPrice()` → 단일 항목 가격 조회
- `getBulkItemPrices()` → 여러 항목 일괄 조회
- `upsertPrice()` → 가격 등록/수정
- `listPrices()` → 가격 이력 조회 (페이지네이션)
- `deletePrice()` → 가격 삭제 (Soft Delete)
#### 4. EstimateService 통합
**견적 생성 프로세스:**
```php
1. BOM 계산 (수량만)
2. BOM 항목의 가격 조회 (PricingService)
3. unit_price × quantity = total_price 계산
4. 전체 항목의 total_price 합산 = total_amount
5. 가격 없는 항목 경고 로그 기록
```
**수정된 메서드:**
- `createEstimate()` → client_id 전달, total_amount 재계산
- `updateEstimate()` → 파라미터 변경 시 가격 재계산
- `createEstimateItems()` → 가격 조회 로직 추가, float 반환
#### 5. 에러 처리 및 로깅
**가격 없는 항목 처리:**
- 경고 메시지 반환: `__('error.price_not_found', [...])`
- Laravel Log에 경고 기록
- 견적 생성은 계속 진행 (unit_price = 0)
- 프론트엔드에서 경고 표시 가능
### 비즈니스 규칙 정리:
#### 매입단가 vs 매출단가
- **매입단가 (PURCHASE)**: 순수 참고용, 견적 계산에 미사용
- **매출단가 (SALE)**: 실제 견적 계산에 사용
- **STANDARD 가격**: 경동 비즈니스에서는 불필요 (사용하지 않음)
#### 고객 그룹별 차등 가격
```
예시 데이터:
- 기본 가격: 100,000원 (client_group_id = NULL)
- A그룹 가격: 90,000원 (client_group_id = 1, price_rate = 0.9)
- B그룹 가격: 110,000원 (client_group_id = 2, price_rate = 1.1)
조회 로직:
- A그룹 고객 → 90,000원 (1순위)
- B그룹 고객 → 110,000원 (1순위)
- 일반 고객 → 100,000원 (2순위)
- 가격 없음 → NULL + 경고 (3순위)
```
### 마이그레이션 실행 결과:
```bash
✅ 2025_10_13_213549_create_client_groups_table (46.85ms)
✅ 2025_10_13_213556_add_client_group_id_to_clients_table (38.75ms)
✅ 2025_10_13_213602_add_client_group_id_to_price_histories_table (38.46ms)
```
### 코드 품질:
- Laravel Pint 포맷팅 완료 (5 files, 4 style issues fixed)
- SAM API Development Rules 준수
- Service-First 아키텍처 유지
- BelongsToTenant 멀티테넌트 스코프 적용
- SoftDeletes 적용
- 감사 컬럼 (created_by, updated_by, deleted_by) 포함
### 예상 효과:
1. **유연한 가격 관리**: 고객 그룹별 차등 가격 설정 가능
2. **자동 가격 계산**: 견적 생성 시 수동 입력 불필요
3. **이력 관리**: 기간별 가격 변동 이력 추적
4. **확장성**: 향후 복잡한 가격 정책 적용 가능
5. **투명성**: 가격 출처 추적 가능 (price_history_id, client_group_id)
### 향후 작업:
- [x] PricingService 구현
- [x] EstimateService 통합
- [ ] 가격 관리 API 엔드포인트 추가 (CRUD)
- [ ] Swagger 문서 작성 (가격 관련 API)
- [ ] 고객 그룹 관리 API 엔드포인트 추가
- [ ] Frontend 가격 관리 화면 구현
- [ ] 가격 일괄 업로드 기능 추가
### Git 커밋 준비:
- 다음 커밋 예정: `feat: 고객그룹별 차등 가격 시스템 구축`
---