diff --git a/CURRENT_WORKS.md b/CURRENT_WORKS.md index 3f15904..4e7a4a0 100644 --- a/CURRENT_WORKS.md +++ b/CURRENT_WORKS.md @@ -1,5 +1,270 @@ # SAM API 저장소 작업 현황 +## 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 (화) - role 컬럼 타입 변경 (ENUM → VARCHAR) ### 주요 작업 diff --git a/LOGICAL_RELATIONSHIPS.md b/LOGICAL_RELATIONSHIPS.md index ba955b8..def9283 100644 --- a/LOGICAL_RELATIONSHIPS.md +++ b/LOGICAL_RELATIONSHIPS.md @@ -1,184 +1,353 @@ # 논리적 데이터베이스 관계 문서 -> FK 제약조건 제거 후 개발자 참조용 논리적 관계 명세서 -> -> **생성일**: 2025-09-24 -> **마이그레이션 batch**: 17까지 적용됨 +> **자동 생성**: 2025-10-14 22:24:19 +> **소스**: Eloquent 모델 관계 분석 -## 🎯 목적 +## 📊 모델별 관계 현황 -물리적 FK 제약조건을 제거하여 성능과 관리 편의성을 확보하면서도, -개발자들이 비즈니스 로직에서 참조해야 할 **논리적 관계**를 명시합니다. +### boards +**모델**: `App\Models\Boards\Board` -## 📋 FK 제거 현황 +- **customFields()**: hasMany → `board_settings` +- **posts()**: hasMany → `posts` -### ✅ 제거된 FK 제약조건 (4개) +### board_comments +**모델**: `App\Models\Boards\BoardComment` -| 테이블 | 컬럼 | 참조 테이블 | 참조 컬럼 | 제거 단계 | 효과 | -|--------|------|-------------|-----------|----------|------| -| `classifications` | `tenant_id` | `tenants` | `id` | Phase 1 | 분류 코드 관리 유연성 | -| `departments` | `parent_id` | `departments` | `id` | Phase 1 | 부서 구조 변경 유연성 | -| `estimates` | `model_set_id` | `categories` | `id` | Phase 2 | 견적 성능 향상 | -| `estimate_items` | `estimate_id` | `estimates` | `id` | Phase 2 | 견적 아이템 성능 향상 | +- **post()**: belongsTo → `posts` +- **user()**: belongsTo → `users` +- **parent()**: belongsTo → `board_comments` +- **children()**: hasMany → `board_comments` -### 🔒 유지된 중요 FK 제약조건 (40+개) +### board_settings +**모델**: `App\Models\Boards\BoardSetting` -**멀티테넌트 보안 FK** (필수 유지): -- 모든 `tenant_id → tenants.id` 관계 유지 -- 사용자 권한 관련 FK 모두 유지 +- **board()**: belongsTo → `boards` -**핵심 비즈니스 FK** (필수 유지): -- `products.category_id → categories.id` -- 권한 관리 시스템 (users, roles, permissions) -- 주문 관리 시스템 (orders, order_items, clients) +### posts +**모델**: `App\Models\Boards\Post` -## 🧩 논리적 관계 명세 +- **board()**: belongsTo → `boards` +- **comments()**: hasMany → `board_comments` +- **files()**: morphMany → `files` -### 1. 분류 관리 (Classifications) -``` -classifications (분류 코드) -├── tenant_id → tenants.id (논리적 - 멀티테넌트) -└── [Eloquent Model에서 BelongsToTenant 트레잇으로 관리] -``` +### post_custom_field_values +**모델**: `App\Models\Boards\PostCustomFieldValue` -**개발 시 주의사항:** -- Service 레이어에서 테넌트 격리 검증 필수 -- 분류 삭제 시 사용 중인 참조 확인 필요 +- **post()**: belongsTo → `posts` +- **field()**: belongsTo → `board_settings` -### 2. 부서 관리 (Departments) -``` -departments (부서) -├── parent_id → departments.id (논리적 - 계층 구조) -└── [자기 참조 관계, Soft Delete 적용] -``` +### categorys +**모델**: `App\Models\Commons\Category` -**개발 시 주의사항:** -- 부서 삭제 시 하위 부서 존재 여부 확인 -- 순환 참조 방지 로직 필요 -- 사용자 배치 여부 확인 후 삭제 +- **parent()**: belongsTo → `categories` +- **children()**: hasMany → `categories` +- **products()**: hasMany → `products` +- **categoryFields()**: hasMany → `category_fields` -### 3. 견적 시스템 (Estimates) -``` -estimates (견적) -├── model_set_id → categories.id (논리적 - 스냅샷 특성) -├── tenant_id → tenants.id (물리적 FK 유지 - 보안) -└── [견적은 생성 시점 데이터 보존] +### category_fields +**모델**: `App\Models\Commons\CategoryField` -estimate_items (견적 아이템) -├── estimate_id → estimates.id (논리적 - 성능 최적화) -├── tenant_id → tenants.id (물리적 FK 유지 - 보안) -└── [대량 데이터 처리 성능 우선] -``` +- **category()**: belongsTo → `categories` -**개발 시 주의사항:** -- 견적 데이터는 스냅샷 특성상 참조 무결성보다 성능 우선 -- 카테고리 변경이 기존 견적에 영향주지 않도록 주의 -- 대량 견적 아이템 처리 시 batch 작업 권장 +### category_logs +**모델**: `App\Models\Commons\CategoryLog` -### 4. BOM 시스템 (Product Components) -``` -product_components (제품 구성요소) -├── parent_product_id → products.id (물리적 FK 없음 - 이미 유연한 구조) -├── ref_type: 'MATERIAL' | 'PRODUCT' (참조 타입) -├── ref_id → materials.id | products.id (논리적 - ref_type에 따라) -└── [통합 참조 구조로 설계됨] -``` +- **category()**: belongsTo → `categories` -**개발 시 주의사항:** -- ref_type 값에 따라 ref_id 해석 필요 -- 자재/제품 삭제 시 BOM 영향도 분석 필수 -- Service 레이어에서 참조 무결성 검증 구현 +### category_templates +**모델**: `App\Models\Commons\CategoryTemplate` -## 📈 성능 최적화 효과 +- **category()**: belongsTo → `categories` -### 제거된 FK의 성능 영향 +### files +**모델**: `App\Models\Commons\File` -1. **Classifications**: 분류 코드 조회 성능 향상 -2. **Departments**: 부서 구조 변경 시 락킹 감소 -3. **Estimates**: 견적 생성/수정 처리량 증가 -4. **Estimate Items**: 대량 아이템 처리 성능 향상 +- **uploader()**: belongsTo → `users` +- **fileable()**: morphTo → `(Polymorphic)` -### 유지된 인덱스 +### menus +**모델**: `App\Models\Commons\Menu` -모든 제거된 FK에 대해 성능 인덱스는 유지하여 조회 성능 보장: +- **parent()**: belongsTo → `menus` +- **children()**: hasMany → `menus` -```sql --- 유지된 성능 인덱스들 -CREATE INDEX idx_classifications_tenant_id ON classifications (tenant_id); -CREATE INDEX idx_departments_parent_id ON departments (parent_id); -CREATE INDEX idx_estimates_tenant_model_set ON estimates (tenant_id, model_set_id); -CREATE INDEX idx_estimate_items_tenant_estimate ON estimate_items (tenant_id, estimate_id); -CREATE INDEX idx_components_ref_type_id ON product_components (ref_type, ref_id); -``` +### tags +**모델**: `App\Models\Commons\Tag` -## 🛡️ 개발 가이드라인 +- **tenant()**: belongsTo → `tenants` -### Service 레이어 구현 필수사항 +### bom_templates +**모델**: `App\Models\Design\BomTemplate` -1. **데이터 무결성 검증** - ```php - // 예시: 부서 삭제 전 검증 - public function deleteDepartment($id) { - if ($this->hasChildDepartments($id)) { - throw new ValidationException('하위 부서가 존재합니다.'); - } - if ($this->hasAssignedUsers($id)) { - throw new ValidationException('배정된 사용자가 존재합니다.'); - } - // 삭제 진행 - } - ``` +- **modelVersion()**: belongsTo → `model_versions` +- **items()**: hasMany → `bom_template_items` -2. **Eloquent 관계 적극 활용** - ```php - // 논리적 관계는 Model에서 정의 - class Department extends Model { - public function parent() { - return $this->belongsTo(Department::class, 'parent_id'); - } +### bom_template_items +**모델**: `App\Models\Design\BomTemplateItem` - public function children() { - return $this->hasMany(Department::class, 'parent_id'); - } - } - ``` +- **template()**: belongsTo → `bom_templates` -3. **배치 작업 시 주의사항** - - 대량 데이터 변경 시 관련 테이블 영향도 고려 - - Transaction 사용으로 일관성 보장 - - 참조 무결성 검증 로직 포함 +### design_models +**모델**: `App\Models\Design\DesignModel` -## 🚨 주의사항 +- **versions()**: hasMany → `model_versions` -### 절대 하지 말아야 할 것 +### model_versions +**모델**: `App\Models\Design\ModelVersion` -❌ **직접 SQL로 참조 데이터 삭제** -❌ **Service 레이어 무결성 검증 생략** -❌ **대량 작업 시 관련 테이블 확인 누락** +- **model()**: belongsTo → `models` +- **bomTemplates()**: hasMany → `bom_templates` -### 권장사항 +### estimates +**모델**: `App\Models\Estimate\Estimate` -✅ **Eloquent ORM 관계 메서드 사용** -✅ **Service 레이어에서 비즈니스 규칙 검증** -✅ **Soft Delete 활용으로 데이터 보호** -✅ **단위 테스트로 무결성 검증** +- **modelSet()**: belongsTo → `categories` +- **items()**: hasMany → `estimate_items` -## 🔄 복구 방법 +### estimate_items +**모델**: `App\Models\Estimate\EstimateItem` -필요시 물리적 FK 제약조건 복구 가능: +- **estimate()**: belongsTo → `estimates` -```bash -# 전체 롤백 -php artisan migrate:rollback --step=4 +### main_requests +**모델**: `App\Models\MainRequest` -# 특정 단계만 롤백 -php artisan migrate:rollback --step=1 -``` +- **flows()**: hasMany → `main_request_flows` -**참고**: FK 복구 시 데이터 무결성 위반으로 실패할 수 있으므로, -복구 전 데이터 정합성 점검 필수. +### main_request_flows +**모델**: `App\Models\MainRequestFlow` ---- +- **mainRequest()**: belongsTo → `main_requests` +- **flowable()**: morphTo → `(Polymorphic)` + +### materials +**모델**: `App\Models\Materials\Material` + +- **receipts()**: hasMany → `material_receipts` +- **lots()**: hasMany → `lots` +- **files()**: morphMany → `files` + +### material_inspections +**모델**: `App\Models\Materials\MaterialInspection` + +- **receipt()**: belongsTo → `material_receipts` +- **items()**: hasMany → `material_inspection_items` + +### material_inspection_items +**모델**: `App\Models\Materials\MaterialInspectionItem` + +- **inspection()**: belongsTo → `material_inspections` + +### material_receipts +**모델**: `App\Models\Materials\MaterialReceipt` + +- **material()**: belongsTo → `materials` +- **inspections()**: hasMany → `material_inspections` + +### users +**모델**: `App\Models\Members\User` + +- **userTenants()**: hasMany → `user_tenants` +- **userRoles()**: hasMany → `user_roles` +- **userTenant()**: hasOne → `user_tenants` +- **userTenantById()**: hasOne → `user_tenants` +- **tenantsMembership()**: belongsToMany → `tenants` +- **files()**: morphMany → `files` + +### user_menu_permissions +**모델**: `App\Models\Members\UserMenuPermission` + +- **user()**: belongsTo → `users` +- **menu()**: belongsTo → `menus` + +### user_roles +**모델**: `App\Models\Members\UserRole` + +- **user()**: belongsTo → `users` +- **tenant()**: belongsTo → `tenants` +- **role()**: belongsTo → `roles` + +### user_tenants +**모델**: `App\Models\Members\UserTenant` + +- **user()**: belongsTo → `users` +- **tenant()**: belongsTo → `tenants` + +### clients +**모델**: `App\Models\Orders\Client` + +- **clientGroup()**: belongsTo → `client_groups` +- **orders()**: hasMany → `orders` + +### client_groups +**모델**: `App\Models\Orders\ClientGroup` + +- **clients()**: hasMany → `clients` + +### orders +**모델**: `App\Models\Orders\Order` + +- **items()**: hasMany → `order_items` +- **histories()**: hasMany → `order_histories` +- **versions()**: hasMany → `order_versions` + +### order_historys +**모델**: `App\Models\Orders\OrderHistory` + +- **order()**: belongsTo → `orders` + +### order_items +**모델**: `App\Models\Orders\OrderItem` + +- **order()**: belongsTo → `orders` +- **components()**: hasMany → `order_item_components` + +### order_item_components +**모델**: `App\Models\Orders\OrderItemComponent` + +- **orderItem()**: belongsTo → `order_items` + +### order_versions +**모델**: `App\Models\Orders\OrderVersion` + +- **order()**: belongsTo → `orders` + +### permissions +**모델**: `App\Models\Permissions\Permission` + +- **tenant()**: belongsTo → `tenants` + +### permission_overrides +**모델**: `App\Models\Permissions\PermissionOverride` + +- **permission()**: belongsTo → `permissions` + +### roles +**모델**: `App\Models\Permissions\Role` + +- **tenant()**: belongsTo → `tenants` +- **menuPermissions()**: hasMany → `role_menu_permissions` +- **userRoles()**: hasMany → `user_roles` + +### role_menu_permissions +**모델**: `App\Models\Permissions\RoleMenuPermission` + +- **role()**: belongsTo → `roles` +- **menu()**: belongsTo → `menus` + +### common_codes +**모델**: `App\Models\Products\CommonCode` + +- **parent()**: belongsTo → `common_codes` +- **children()**: hasMany → `common_codes` + +### parts +**모델**: `App\Models\Products\Part` + +- **category()**: belongsTo → `common_codes` +- **partType()**: belongsTo → `common_codes` + +### price_historys +**모델**: `App\Models\Products\PriceHistory` + +- **clientGroup()**: belongsTo → `client_groups` + +### products +**모델**: `App\Models\Products\Product` + +- **category()**: belongsTo → `categories` +- **componentLines()**: hasMany → `product_components` +- **parentLines()**: hasMany → `product_components` +- **children()**: belongsToMany → `products` +- **parents()**: belongsToMany → `products` +- **files()**: morphMany → `files` + +### product_components +**모델**: `App\Models\Products\ProductComponent` + +- **parentProduct()**: belongsTo → `products` +- **product()**: belongsTo → `products` +- **material()**: belongsTo → `materials` + +### lots +**모델**: `App\Models\Qualitys\Lot` + +- **material()**: belongsTo → `materials` +- **sales()**: hasMany → `lot_sales` + +### lot_sales +**모델**: `App\Models\Qualitys\LotSale` + +- **lot()**: belongsTo → `lots` + +### departments +**모델**: `App\Models\Tenants\Department` + +- **parent()**: belongsTo → `departments` +- **children()**: hasMany → `departments` +- **departmentUsers()**: hasMany → `department_user` +- **users()**: belongsToMany → `users` +- **permissionOverrides()**: morphMany → `permission_overrides` + +### payments +**모델**: `App\Models\Tenants\Payment` + +- **subscription()**: belongsTo → `subscriptions` + +### department_users +**모델**: `App\Models\Tenants\Pivots\DepartmentUser` + +- **department()**: belongsTo → `departments` +- **user()**: belongsTo → `users` + +### plans +**모델**: `App\Models\Tenants\Plan` + +- **subscriptions()**: hasMany → `subscriptions` + +### setting_field_defs +**모델**: `App\Models\Tenants\SettingFieldDef` + +- **tenantSettings()**: hasMany → `tenant_field_settings` + +### subscriptions +**모델**: `App\Models\Tenants\Subscription` + +- **tenant()**: belongsTo → `tenants` +- **plan()**: belongsTo → `plans` +- **payments()**: hasMany → `payments` + +### tenants +**모델**: `App\Models\Tenants\Tenant` + +- **plan()**: belongsTo → `plans` +- **subscription()**: belongsTo → `subscriptions` +- **userTenants()**: hasMany → `user_tenants` +- **roles()**: hasMany → `roles` +- **userRoles()**: hasMany → `user_roles` +- **users()**: belongsToMany → `users` +- **files()**: morphMany → `files` + +### tenant_field_settings +**모델**: `App\Models\Tenants\TenantFieldSetting` + +- **fieldDef()**: belongsTo → `setting_field_defs` +- **optionGroup()**: belongsTo → `tenant_option_groups` + +### tenant_option_groups +**모델**: `App\Models\Tenants\TenantOptionGroup` + +- **values()**: hasMany → `tenant_option_values` + +### tenant_option_values +**모델**: `App\Models\Tenants\TenantOptionValue` + +- **group()**: belongsTo → `tenant_option_groups` + +### tenant_user_profiles +**모델**: `App\Models\Tenants\TenantUserProfile` + +- **user()**: belongsTo → `users` +- **department()**: belongsTo → `departments` -**문서 관리**: 이 문서는 데이터베이스 스키마 변경 시 함께 업데이트해야 합니다. -**최종 업데이트**: 2025-09-24 (Phase 1~3 FK 제거 완료) \ No newline at end of file diff --git a/app/Console/Commands/UpdateLogicalRelationships.php b/app/Console/Commands/UpdateLogicalRelationships.php index 8fd1a2a..440b560 100644 --- a/app/Console/Commands/UpdateLogicalRelationships.php +++ b/app/Console/Commands/UpdateLogicalRelationships.php @@ -85,21 +85,48 @@ private function getModelRelationshipsFromFile($file, string $className): array $content = File::get($file->getRealPath()); $relationships = []; + // use 문 추출 + $useStatements = $this->extractUseStatements($content); + + // 현재 파일의 namespace 추출 + $currentNamespace = $this->extractNamespace($content); + // 관계 메서드 패턴 검출 $patterns = [ 'belongsTo' => '/public\s+function\s+(\w+)\s*\([^)]*\)\s*[^{]*{\s*return\s+\$this->belongsTo\s*\(\s*([^,\)]+)/', 'hasMany' => '/public\s+function\s+(\w+)\s*\([^)]*\)\s*[^{]*{\s*return\s+\$this->hasMany\s*\(\s*([^,\)]+)/', 'hasOne' => '/public\s+function\s+(\w+)\s*\([^)]*\)\s*[^{]*{\s*return\s+\$this->hasOne\s*\(\s*([^,\)]+)/', 'belongsToMany' => '/public\s+function\s+(\w+)\s*\([^)]*\)\s*[^{]*{\s*return\s+\$this->belongsToMany\s*\(\s*([^,\)]+)/', + '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*([^,\)]+)/', ]; foreach ($patterns as $type => $pattern) { if (preg_match_all($pattern, $content, $matches, PREG_SET_ORDER)) { foreach ($matches as $match) { + // morphTo는 관련 모델이 없으므로 특별 처리 + if ($type === 'morphTo') { + $relationships[] = [ + 'method' => $match[1], + 'type' => $type, + 'related_model' => '(Polymorphic)', + 'foreign_key' => null, + 'local_key' => null + ]; + continue; + } + + // ::class 제거 및 따옴표 제거 + $relatedModel = str_replace('::class', '', trim($match[2], '"\'')); + + // 완전한 클래스명으로 변환 + $fullyQualifiedClass = $this->resolveClassName($relatedModel, $useStatements, $currentNamespace, $className); + $relationships[] = [ 'method' => $match[1], 'type' => $type, - 'related_model' => trim($match[2], '"\''), + 'related_model' => $fullyQualifiedClass, 'foreign_key' => null, 'local_key' => null ]; @@ -110,6 +137,72 @@ private function getModelRelationshipsFromFile($file, string $className): array return $relationships; } + /** + * 파일 내용에서 use 문들을 추출 + */ + private function extractUseStatements(string $content): array + { + $useStatements = []; + + // use 문 패턴: use Full\Namespace\ClassName; 또는 use Full\Namespace\ClassName as Alias; + 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; + } + + /** + * 파일 내용에서 namespace 추출 + */ + private function extractNamespace(string $content): ?string + { + if (preg_match('/namespace\s+([^;]+);/', $content, $matches)) { + return trim($matches[1]); + } + + return null; + } + + /** + * 짧은 클래스명을 완전한 클래스명으로 변환 + */ + private function resolveClassName(string $className, array $useStatements, ?string $currentNamespace, string $currentClassName): string + { + // 이미 완전한 클래스명인 경우 (백슬래시 포함) + if (strpos($className, '\\') !== false) { + return ltrim($className, '\\'); + } + + // self/static 처리 - 현재 클래스로 대체 + if ($className === 'self' || $className === 'static') { + return $currentClassName; + } + + // use 문에서 찾기 + if (isset($useStatements[$className])) { + return $useStatements[$className]; + } + + // 같은 namespace에 있다고 가정 + if ($currentNamespace) { + return $currentNamespace . '\\' . $className; + } + + // 그 외의 경우 그대로 반환 + return $className; + } + private function getModelRelationships(ReflectionClass $reflection, $model): array { $relationships = []; @@ -200,14 +293,31 @@ private function updateLogicalDocument(array $relationships): void $content .= "**모델**: `{$info['model']}`\n\n"; foreach ($info['relationships'] as $rel) { - $relatedTable = (new $rel['related_model'])->getTable(); - $content .= "- **{$rel['method']}()**: {$rel['type']} → `{$relatedTable}`"; + try { + // Polymorphic 관계는 특별 표시 + if ($rel['related_model'] === '(Polymorphic)') { + $content .= "- **{$rel['method']}()**: {$rel['type']} → `(Polymorphic)`\n"; + continue; + } - if ($rel['foreign_key']) { - $content .= " (FK: `{$rel['foreign_key']}`)"; + // 관련 모델 클래스가 존재하는지 확인 + if (!class_exists($rel['related_model'])) { + $this->warn("모델 클래스가 존재하지 않음: {$rel['related_model']}"); + continue; + } + + $relatedTable = (new $rel['related_model'])->getTable(); + $content .= "- **{$rel['method']}()**: {$rel['type']} → `{$relatedTable}`"; + + if ($rel['foreign_key']) { + $content .= " (FK: `{$rel['foreign_key']}`)"; + } + + $content .= "\n"; + } catch (\Exception $e) { + $this->warn("관계 처리 실패: {$rel['method']} - " . $e->getMessage()); + continue; } - - $content .= "\n"; } $content .= "\n";