info('🔄 논리적 관계 문서 업데이트 시작...'); $relationships = $this->extractModelRelationships(); $this->updateLogicalDocument($relationships); $this->info('✅ 논리적 관계 문서 업데이트 완료!'); } private function extractModelRelationships(): array { $relationships = []; $modelPath = app_path('Models'); // 모든 모델 파일 스캔 $modelFiles = File::allFiles($modelPath); foreach ($modelFiles as $file) { if ($file->getExtension() !== 'php') { continue; } $className = $this->getClassNameFromFile($file); if (! $className || ! class_exists($className)) { continue; } try { $reflection = new ReflectionClass($className); // 모델이 Eloquent Model인지 확인 if (! $reflection->isSubclassOf(\Illuminate\Database\Eloquent\Model::class)) { continue; } // Abstract 클래스 건너뛰기 if ($reflection->isAbstract()) { continue; } // 테이블 이름 직접 추출 $tableName = $this->getTableNameFromModel($className, $reflection); if (! $tableName) { continue; } $relationships[$tableName] = [ 'model' => $className, 'relationships' => $this->getModelRelationshipsFromFile($file, $className), ]; } catch (\Exception $e) { $this->warn("모델 분석 실패: {$className} - ".$e->getMessage()); continue; } } return $relationships; } private function getTableNameFromModel(string $className, ReflectionClass $reflection): ?string { // 클래스명에서 테이블명 추정 $modelName = class_basename($className); $tableName = strtolower(preg_replace('/(?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' => $fullyQualifiedClass, 'foreign_key' => null, 'local_key' => null, ]; } } } 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 = []; $methods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC); foreach ($methods as $method) { if ($method->getDeclaringClass()->getName() !== $reflection->getName()) { continue; } try { // 관계 메서드 호출해서 타입 확인 $result = $method->invoke($model); if ($this->isRelationshipMethod($result)) { $relationships[] = [ 'method' => $method->getName(), 'type' => $this->getRelationshipType($result), 'related_model' => get_class($result->getRelated()), 'foreign_key' => $this->getForeignKey($result), 'local_key' => $this->getLocalKey($result), ]; } } catch (\Exception $e) { // 관계 메서드가 아니거나 호출 실패 시 건너뛰기 continue; } } return $relationships; } private function isRelationshipMethod($result): bool { return $result instanceof \Illuminate\Database\Eloquent\Relations\Relation; } private function getRelationshipType($relation): string { $className = get_class($relation); return class_basename($className); } private function getForeignKey($relation): ?string { return method_exists($relation, 'getForeignKeyName') ? $relation->getForeignKeyName() : null; } private function getLocalKey($relation): ?string { return method_exists($relation, 'getLocalKeyName') ? $relation->getLocalKeyName() : null; } private function getClassNameFromFile($file): ?string { $content = File::get($file->getRealPath()); if (! preg_match('/namespace\s+([^;]+);/', $content, $namespaceMatches)) { return null; } if (! preg_match('/class\s+(\w+)/', $content, $classMatches)) { return null; } return $namespaceMatches[1].'\\'.$classMatches[1]; } private function updateLogicalDocument(array $relationships): void { $documentPath = base_path('LOGICAL_RELATIONSHIPS.md'); $timestamp = now()->format('Y-m-d H:i:s'); $content = "# 논리적 데이터베이스 관계 문서\n\n"; $content .= "> **자동 생성**: {$timestamp}\n"; $content .= "> **소스**: Eloquent 모델 관계 분석\n\n"; $content .= "## 📊 모델별 관계 현황\n\n"; foreach ($relationships as $tableName => $info) { if (empty($info['relationships'])) { continue; } $content .= "### {$tableName}\n"; $content .= "**모델**: `{$info['model']}`\n\n"; foreach ($info['relationships'] as $rel) { try { // 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; } $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"; } File::put($documentPath, $content); $this->info("📄 문서 업데이트: {$documentPath}"); } }