2025-09-24 21:49:39 +09:00
|
|
|
|
# SAM API 저장소 작업 현황
|
2025-09-19 22:12:09 +09:00
|
|
|
|
|
2025-10-14 22:26:15 +09:00
|
|
|
|
## 2025-10-14 (화) - UpdateLogicalRelationships 명령 개선
|
|
|
|
|
|
|
|
|
|
|
|
### 주요 작업
|
|
|
|
|
|
- **use 문 파싱 추가**: 짧은 클래스명을 완전한 클래스명(FQCN)으로 변환
|
|
|
|
|
|
- **self/static 처리**: 자기 참조 관계 정상 처리
|
|
|
|
|
|
- **Polymorphic 관계 지원**: morphTo, morphMany, morphOne 관계 추가
|
|
|
|
|
|
- **에러 처리 개선**: 클래스 존재 확인 및 안전한 처리
|
|
|
|
|
|
|
|
|
|
|
|
### 수정된 파일:
|
|
|
|
|
|
- `app/Console/Commands/UpdateLogicalRelationships.php` - 관계 추출 로직 전면 개선
|
|
|
|
|
|
- `LOGICAL_RELATIONSHIPS.md` - 자동 생성된 모델 관계 문서
|
|
|
|
|
|
|
|
|
|
|
|
### 작업 내용:
|
|
|
|
|
|
|
|
|
|
|
|
#### 1. 문제 상황
|
|
|
|
|
|
|
|
|
|
|
|
**마이그레이션 실행 시 에러:**
|
|
|
|
|
|
```
|
|
|
|
|
|
Class "BoardSetting::class" not found
|
|
|
|
|
|
위치: UpdateLogicalRelationships.php:203
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**근본 원인:**
|
|
|
|
|
|
- 정규식으로 모델명 추출 시 `::class` 부분까지 포함됨
|
|
|
|
|
|
- 짧은 클래스명만 추출되어 완전한 클래스명으로 변환 불가
|
|
|
|
|
|
- use 문을 파싱하지 않아 네임스페이스 해석 불가
|
|
|
|
|
|
|
|
|
|
|
|
#### 2. use 문 파싱 구현
|
|
|
|
|
|
|
|
|
|
|
|
**새로운 메서드 추가:**
|
|
|
|
|
|
```php
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 파일 내용에서 use 문들을 추출
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function extractUseStatements(string $content): array
|
|
|
|
|
|
{
|
|
|
|
|
|
$useStatements = [];
|
|
|
|
|
|
|
|
|
|
|
|
// use Full\Namespace\ClassName; 파싱
|
|
|
|
|
|
if (preg_match_all('/use\s+([^;]+);/', $content, $matches)) {
|
|
|
|
|
|
foreach ($matches[1] as $useStatement) {
|
|
|
|
|
|
// as 별칭 처리
|
|
|
|
|
|
if (strpos($useStatement, ' as ') !== false) {
|
|
|
|
|
|
[$fullClass, $alias] = array_map('trim', explode(' as ', $useStatement));
|
|
|
|
|
|
$useStatements[$alias] = $fullClass;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$fullClass = trim($useStatement);
|
|
|
|
|
|
$shortName = class_basename($fullClass);
|
|
|
|
|
|
$useStatements[$shortName] = $fullClass;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $useStatements;
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**결과:**
|
|
|
|
|
|
```php
|
|
|
|
|
|
// use App\Models\Commons\BoardSetting;
|
|
|
|
|
|
// → ['BoardSetting' => 'App\Models\Commons\BoardSetting']
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 3. namespace 추출 구현
|
|
|
|
|
|
|
|
|
|
|
|
```php
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 파일 내용에서 namespace 추출
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function extractNamespace(string $content): ?string
|
|
|
|
|
|
{
|
|
|
|
|
|
if (preg_match('/namespace\s+([^;]+);/', $content, $matches)) {
|
|
|
|
|
|
return trim($matches[1]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 4. 클래스명 해석 로직 구현
|
|
|
|
|
|
|
|
|
|
|
|
```php
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 짧은 클래스명을 완전한 클래스명으로 변환
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function resolveClassName(
|
|
|
|
|
|
string $className,
|
|
|
|
|
|
array $useStatements,
|
|
|
|
|
|
?string $currentNamespace,
|
|
|
|
|
|
string $currentClassName
|
|
|
|
|
|
): string {
|
|
|
|
|
|
// 1. 이미 완전한 클래스명인 경우
|
|
|
|
|
|
if (strpos($className, '\\') !== false) {
|
|
|
|
|
|
return ltrim($className, '\\');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. self/static 처리 - 현재 클래스로 대체
|
|
|
|
|
|
if ($className === 'self' || $className === 'static') {
|
|
|
|
|
|
return $currentClassName;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. use 문에서 찾기
|
|
|
|
|
|
if (isset($useStatements[$className])) {
|
|
|
|
|
|
return $useStatements[$className];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 같은 namespace에 있다고 가정
|
|
|
|
|
|
if ($currentNamespace) {
|
|
|
|
|
|
return $currentNamespace . '\\' . $className;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 5. 그 외의 경우 그대로 반환
|
|
|
|
|
|
return $className;
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**해석 순서:**
|
|
|
|
|
|
1. 이미 FQCN인지 확인 (백슬래시 포함)
|
|
|
|
|
|
2. self/static → 현재 클래스로 대체
|
|
|
|
|
|
3. use 문에서 매핑 찾기
|
|
|
|
|
|
4. 같은 namespace로 추정
|
|
|
|
|
|
5. 실패 시 그대로 반환
|
|
|
|
|
|
|
|
|
|
|
|
#### 5. Polymorphic 관계 지원
|
|
|
|
|
|
|
|
|
|
|
|
**새로운 관계 타입 추가:**
|
|
|
|
|
|
```php
|
|
|
|
|
|
$patterns = [
|
|
|
|
|
|
'belongsTo' => '/...',
|
|
|
|
|
|
'hasMany' => '/...',
|
|
|
|
|
|
'hasOne' => '/...',
|
|
|
|
|
|
'belongsToMany' => '/...',
|
|
|
|
|
|
'morphTo' => '/public\s+function\s+(\w+)\s*\([^)]*\)\s*[^{]*{\s*return\s+\$this->morphTo\s*\(/',
|
|
|
|
|
|
'morphMany' => '/public\s+function\s+(\w+)\s*\([^)]*\)\s*[^{]*{\s*return\s+\$this->morphMany\s*\(\s*([^,\)]+)/',
|
|
|
|
|
|
'morphOne' => '/public\s+function\s+(\w+)\s*\([^)]*\)\s*[^{]*{\s*return\s+\$this->morphOne\s*\(\s*([^,\)]+)/',
|
|
|
|
|
|
];
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**morphTo 특별 처리:**
|
|
|
|
|
|
```php
|
|
|
|
|
|
// morphTo는 관련 모델이 없으므로 특별 표시
|
|
|
|
|
|
if ($type === 'morphTo') {
|
|
|
|
|
|
$relationships[] = [
|
|
|
|
|
|
'method' => $match[1],
|
|
|
|
|
|
'type' => $type,
|
|
|
|
|
|
'related_model' => '(Polymorphic)',
|
|
|
|
|
|
'foreign_key' => null,
|
|
|
|
|
|
'local_key' => null
|
|
|
|
|
|
];
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 6. 에러 처리 개선
|
|
|
|
|
|
|
|
|
|
|
|
**클래스 존재 확인:**
|
|
|
|
|
|
```php
|
|
|
|
|
|
// Polymorphic 관계는 특별 표시
|
|
|
|
|
|
if ($rel['related_model'] === '(Polymorphic)') {
|
|
|
|
|
|
$content .= "- **{$rel['method']}()**: {$rel['type']} → `(Polymorphic)`\n";
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 클래스가 존재하는지 확인
|
|
|
|
|
|
if (!class_exists($rel['related_model'])) {
|
|
|
|
|
|
$this->warn("모델 클래스가 존재하지 않음: {$rel['related_model']}");
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**try-catch로 안전하게 처리:**
|
|
|
|
|
|
```php
|
|
|
|
|
|
try {
|
|
|
|
|
|
$relatedTable = (new $rel['related_model'])->getTable();
|
|
|
|
|
|
$content .= "- **{$rel['method']}()**: {$rel['type']} → `{$relatedTable}`";
|
|
|
|
|
|
// ...
|
|
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
|
|
$this->warn("관계 처리 실패: {$rel['method']} - " . $e->getMessage());
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 7. 실행 결과
|
|
|
|
|
|
|
|
|
|
|
|
**이전 (에러 발생):**
|
|
|
|
|
|
```
|
|
|
|
|
|
Class "BoardSetting::class" not found
|
|
|
|
|
|
모델 클래스가 존재하지 않음: BoardSetting (68개 경고)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**현재 (정상 실행):**
|
|
|
|
|
|
```
|
|
|
|
|
|
🔄 논리적 관계 문서 업데이트 시작...
|
|
|
|
|
|
📄 문서 업데이트: /Users/hskwon/Works/@KD_SAM/SAM/api/LOGICAL_RELATIONSHIPS.md
|
|
|
|
|
|
✅ 논리적 관계 문서 업데이트 완료!
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 8. 생성된 문서 예시
|
|
|
|
|
|
|
|
|
|
|
|
**LOGICAL_RELATIONSHIPS.md:**
|
|
|
|
|
|
```markdown
|
|
|
|
|
|
### files
|
|
|
|
|
|
**모델**: `App\Models\Commons\File`
|
|
|
|
|
|
|
|
|
|
|
|
- **uploader()**: belongsTo → `users`
|
|
|
|
|
|
- **fileable()**: morphTo → `(Polymorphic)`
|
|
|
|
|
|
|
|
|
|
|
|
### products
|
|
|
|
|
|
**모델**: `App\Models\Products\Product`
|
|
|
|
|
|
|
|
|
|
|
|
- **category()**: belongsTo → `categories`
|
|
|
|
|
|
- **componentLines()**: hasMany → `product_components`
|
|
|
|
|
|
- **children()**: belongsToMany → `products`
|
|
|
|
|
|
- **files()**: morphMany → `files`
|
|
|
|
|
|
|
|
|
|
|
|
### departments
|
|
|
|
|
|
**모델**: `App\Models\Tenants\Department`
|
|
|
|
|
|
|
|
|
|
|
|
- **parent()**: belongsTo → `departments`
|
|
|
|
|
|
- **children()**: hasMany → `departments`
|
|
|
|
|
|
- **permissionOverrides()**: morphMany → `permission_overrides`
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 9. 주요 개선 사항
|
|
|
|
|
|
|
|
|
|
|
|
✅ **완전한 클래스명 해석:**
|
|
|
|
|
|
- `BoardSetting` → `App\Models\Commons\BoardSetting`
|
|
|
|
|
|
- `Post` → `App\Models\Boards\Post`
|
|
|
|
|
|
- `User` → `App\Models\Members\User`
|
|
|
|
|
|
|
|
|
|
|
|
✅ **자기 참조 관계 처리:**
|
|
|
|
|
|
- `self` → 현재 클래스의 FQCN
|
|
|
|
|
|
- `Category::self` → `App\Models\Commons\Category`
|
|
|
|
|
|
|
|
|
|
|
|
✅ **Polymorphic 관계 지원:**
|
|
|
|
|
|
- `morphTo` → `(Polymorphic)` 표시
|
|
|
|
|
|
- `morphMany`, `morphOne` → 관련 모델 정상 해석
|
|
|
|
|
|
|
|
|
|
|
|
✅ **에러 없는 실행:**
|
|
|
|
|
|
- 클래스 존재 확인
|
|
|
|
|
|
- 안전한 인스턴스 생성
|
|
|
|
|
|
- 명확한 경고 메시지
|
|
|
|
|
|
|
|
|
|
|
|
#### 10. 코드 품질
|
|
|
|
|
|
|
|
|
|
|
|
**SAM API Development Rules 준수:**
|
|
|
|
|
|
- ✅ 명확한 메서드 분리 (단일 책임)
|
|
|
|
|
|
- ✅ 주석으로 의도 명시
|
|
|
|
|
|
- ✅ 에러 처리 철저
|
|
|
|
|
|
- ✅ 검증 가능한 결과
|
|
|
|
|
|
|
|
|
|
|
|
**Laravel 컨벤션:**
|
|
|
|
|
|
- ✅ Artisan Command 표준 패턴
|
|
|
|
|
|
- ✅ ReflectionClass 안전한 사용
|
|
|
|
|
|
- ✅ File Facade 활용
|
|
|
|
|
|
|
|
|
|
|
|
### 향후 개선 가능 사항:
|
|
|
|
|
|
|
|
|
|
|
|
- [ ] 관계 FK 자동 추출 (현재는 null)
|
|
|
|
|
|
- [ ] 피벗 테이블 정보 추가 (belongsToMany)
|
|
|
|
|
|
- [ ] 관계 조건 추출 (where 절 등)
|
|
|
|
|
|
- [ ] 성능 최적화 (캐싱)
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2025-10-14 20:49:51 +09:00
|
|
|
|
## 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-14 09:10:52 +09:00
|
|
|
|
## 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 22:06:42 +09:00
|
|
|
|
## 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 21:52:34 +09:00
|
|
|
|
## 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: 고객그룹별 차등 가격 시스템 구축`
|
|
|
|
|
|
|
|
|
|
|
|
---
|