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

30 KiB
Raw Blame History

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() 메서드:

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() 메서드 (롤백):

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. 마이그레이션 실행 결과

실행 명령:

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 추가 시:

// 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)

수정 내용:

// 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() 메서드:

// 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() 메서드:

// 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() 메서드:

// 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가 수정되어도 이 파일들은 로컬 정의를 사용하여 불일치 발생

수정 내용: 각 파일에서 다음 블록을 제거:

/**
 * @OA\Schema(
 *   schema="ApiResponse",
 *   ...
 * )
 * @OA\Schema(
 *   schema="ErrorResponse",
 *   ...
 * )
 */

결과:

  • 모든 API가 CommonComponents의 글로벌 스키마 사용
  • 문서 일관성 확보

4. ClientGroup 자동 복원 기능 구현

비즈니스 요구사항:

  • 사용자가 삭제된 그룹 코드로 다시 생성 시도 시 자동으로 복원
  • UX 개선: "중복 코드" 에러 대신 자연스러운 재생성

ClientGroupService.php - store() 메서드:

// 삭제된 레코드 확인
$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() 메서드:

// 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):

// 고객 그룹 관련
'duplicate_code' => '중복된 그룹 코드입니다.',
'has_clients' => '해당 고객 그룹에 속한 고객이 있어 삭제할 수 없습니다.',
'code_exists_in_deleted' => '삭제된 데이터에 동일한 코드가 존재합니다. 먼저 해당 코드를 완전히 삭제하거나 다른 코드를 사용하세요.',

Swagger 문서 업데이트 (ClientGroupApi.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 스키마 (응답용):
@OA\Property(property="client_group_id", type="integer", nullable=true, example=1, description="고객 그룹 ID")
  1. ClientCreateRequest 스키마 (생성 요청용):
@OA\Property(property="client_group_id", type="integer", nullable=true, example=1, description="고객 그룹 ID")
  1. ClientUpdateRequest 스키마 (수정 요청용):
@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 재생성 결과:

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 시스템 활용

// withTrashed() - 삭제된 데이터 포함 조회
$existing = ClientGroup::withTrashed()
    ->where('group_code', 'VIP')
    ->first();

// restore() - 삭제 취소
$existing->restore();

// forceDelete() - 물리적 삭제
$existing->forceDelete();

// trashed() - 삭제 여부 확인
if ($existing->trashed()) { ... }

allOf 패턴 활용

// 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) - 활성/비활성 상태 토글

검증 규칙:

- 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 패턴:

- 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 구현

특화된 엔드포인트:

- 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: 기준일 (선택, 미지정 시 오늘)

일괄 조회 요청:

{
  "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[])

특수 스키마:

// 단일 항목 가격 조회 결과
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 라우트:

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 라우트:

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 재생성 및 검증

# 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 기능으로 즉시 테스트 가능

완료된 향후 작업 (오전 작업 기준):

  • 가격 관리 API 엔드포인트 추가 (CRUD)
  • Swagger 문서 작성 (가격 관련 API)
  • 고객 그룹 관리 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 테이블:

- 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 핵심 로직

가격 조회 우선순위:

1순위: 고객 그룹별 매출단가 (client_group_id 있음)
2순위: 기본 매출단가 (client_group_id = NULL)
3순위: NULL (경고 발생)

// ❌ 제거: 매입단가는 견적에서 사용하지 않음 (순수 참고용)

주요 메서드:

  • getItemPrice() → 단일 항목 가격 조회
  • getBulkItemPrices() → 여러 항목 일괄 조회
  • upsertPrice() → 가격 등록/수정
  • listPrices() → 가격 이력 조회 (페이지네이션)
  • deletePrice() → 가격 삭제 (Soft Delete)

4. EstimateService 통합

견적 생성 프로세스:

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순위)

마이그레이션 실행 결과:

✅ 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)

향후 작업:

  • PricingService 구현
  • EstimateService 통합
  • 가격 관리 API 엔드포인트 추가 (CRUD)
  • Swagger 문서 작성 (가격 관련 API)
  • 고객 그룹 관리 API 엔드포인트 추가
  • Frontend 가격 관리 화면 구현
  • 가격 일괄 업로드 기능 추가

Git 커밋 준비:

  • 다음 커밋 예정: feat: 고객그룹별 차등 가격 시스템 구축