feat: [interview] 카테고리 계층 구조(대분류/중분류) 지원
- InterviewCategory 모델에 parent/children 관계 추가 - Service: getTree, getProjectTree 루트+children eager loading - Service: createCategory에 parent_id 지원 - Service: cloneMaster 2단계 계층 복제 - Controller: storeCategory validation에 parent_id 추가 - UI: CategorySidebar/DomainSidebar 트리 뷰 렌더링 - UI: findCategory 헬퍼로 트리 내 카테고리 검색
This commit is contained in:
@@ -21,7 +21,9 @@ class InterviewScenarioService
|
||||
|
||||
public function getCategories()
|
||||
{
|
||||
return InterviewCategory::orderBy('sort_order')
|
||||
return InterviewCategory::whereNull('parent_id')
|
||||
->with(['children' => fn ($q) => $q->orderBy('sort_order')->orderBy('id')])
|
||||
->orderBy('sort_order')
|
||||
->orderBy('id')
|
||||
->get();
|
||||
}
|
||||
@@ -29,10 +31,19 @@ public function getCategories()
|
||||
public function createCategory(array $data): InterviewCategory
|
||||
{
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
$maxSort = InterviewCategory::max('sort_order') ?? 0;
|
||||
$parentId = $data['parent_id'] ?? null;
|
||||
|
||||
$query = InterviewCategory::query();
|
||||
if ($parentId) {
|
||||
$query->where('parent_id', $parentId);
|
||||
} else {
|
||||
$query->whereNull('parent_id');
|
||||
}
|
||||
$maxSort = $query->max('sort_order') ?? 0;
|
||||
|
||||
return InterviewCategory::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'parent_id' => $parentId,
|
||||
'name' => $data['name'],
|
||||
'description' => $data['description'] ?? null,
|
||||
'sort_order' => $maxSort + 1,
|
||||
@@ -209,14 +220,15 @@ public function bulkImport(int $categoryId, array $templates): array
|
||||
|
||||
public function getTree()
|
||||
{
|
||||
return InterviewCategory::with([
|
||||
'templates' => function ($q) {
|
||||
$q->orderBy('sort_order')->orderBy('id');
|
||||
$q->with(['questions' => function ($q2) {
|
||||
$q2->orderBy('sort_order')->orderBy('id');
|
||||
}]);
|
||||
},
|
||||
])
|
||||
return InterviewCategory::whereNull('parent_id')
|
||||
->with([
|
||||
'children' => fn ($q) => $q->orderBy('sort_order')->orderBy('id')
|
||||
->with(['templates' => fn ($q2) => $q2->orderBy('sort_order')->orderBy('id')
|
||||
->with(['questions' => fn ($q3) => $q3->orderBy('sort_order')->orderBy('id')]),
|
||||
]),
|
||||
'templates' => fn ($q) => $q->orderBy('sort_order')->orderBy('id')
|
||||
->with(['questions' => fn ($q2) => $q2->orderBy('sort_order')->orderBy('id')]),
|
||||
])
|
||||
->orderBy('sort_order')
|
||||
->orderBy('id')
|
||||
->get();
|
||||
@@ -431,58 +443,84 @@ private function cloneMasterQuestionsToProject(InterviewProject $project): void
|
||||
$tenantId = $project->tenant_id;
|
||||
$userId = auth()->id();
|
||||
|
||||
// interview_project_id = NULL이고 domain이 있는 마스터 카테고리
|
||||
$masterCategories = InterviewCategory::withoutGlobalScopes()
|
||||
// 루트 마스터 카테고리 (parent_id=null, project=null)
|
||||
$masterRoots = InterviewCategory::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereNull('interview_project_id')
|
||||
->whereNotNull('domain')
|
||||
->with(['templates.questions'])
|
||||
->whereNull('parent_id')
|
||||
->with(['children.templates.questions', 'templates.questions'])
|
||||
->orderBy('sort_order')
|
||||
->get();
|
||||
|
||||
foreach ($masterCategories as $masterCat) {
|
||||
$newCat = InterviewCategory::create([
|
||||
foreach ($masterRoots as $masterRoot) {
|
||||
// 루트 카테고리 복제
|
||||
$newRoot = InterviewCategory::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'interview_project_id' => $project->id,
|
||||
'name' => $masterCat->name,
|
||||
'description' => $masterCat->description,
|
||||
'domain' => $masterCat->domain,
|
||||
'sort_order' => $masterCat->sort_order,
|
||||
'parent_id' => null,
|
||||
'name' => $masterRoot->name,
|
||||
'description' => $masterRoot->description,
|
||||
'domain' => $masterRoot->domain,
|
||||
'sort_order' => $masterRoot->sort_order,
|
||||
'is_active' => true,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
|
||||
foreach ($masterCat->templates as $masterTpl) {
|
||||
$newTpl = InterviewTemplate::create([
|
||||
// 루트의 직접 templates 복제
|
||||
$this->cloneTemplates($masterRoot->templates, $newRoot->id, $tenantId, $userId);
|
||||
|
||||
// 자식 카테고리 복제
|
||||
foreach ($masterRoot->children as $masterChild) {
|
||||
$newChild = InterviewCategory::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'interview_category_id' => $newCat->id,
|
||||
'name' => $masterTpl->name,
|
||||
'description' => $masterTpl->description,
|
||||
'sort_order' => $masterTpl->sort_order,
|
||||
'interview_project_id' => $project->id,
|
||||
'parent_id' => $newRoot->id,
|
||||
'name' => $masterChild->name,
|
||||
'description' => $masterChild->description,
|
||||
'domain' => $masterChild->domain,
|
||||
'sort_order' => $masterChild->sort_order,
|
||||
'is_active' => true,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
|
||||
foreach ($masterTpl->questions as $masterQ) {
|
||||
InterviewQuestion::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'interview_template_id' => $newTpl->id,
|
||||
'question_text' => $masterQ->question_text,
|
||||
'question_type' => $masterQ->question_type,
|
||||
'options' => $masterQ->options,
|
||||
'ai_hint' => $masterQ->ai_hint,
|
||||
'expected_format' => $masterQ->expected_format,
|
||||
'depends_on' => $masterQ->depends_on,
|
||||
'domain' => $masterQ->domain,
|
||||
'is_required' => $masterQ->is_required,
|
||||
'sort_order' => $masterQ->sort_order,
|
||||
'is_active' => true,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
}
|
||||
$this->cloneTemplates($masterChild->templates, $newChild->id, $tenantId, $userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function cloneTemplates($templates, int $categoryId, int $tenantId, ?int $userId): void
|
||||
{
|
||||
foreach ($templates as $masterTpl) {
|
||||
$newTpl = InterviewTemplate::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'interview_category_id' => $categoryId,
|
||||
'name' => $masterTpl->name,
|
||||
'description' => $masterTpl->description,
|
||||
'sort_order' => $masterTpl->sort_order,
|
||||
'is_active' => true,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
|
||||
foreach ($masterTpl->questions as $masterQ) {
|
||||
InterviewQuestion::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'interview_template_id' => $newTpl->id,
|
||||
'question_text' => $masterQ->question_text,
|
||||
'question_type' => $masterQ->question_type,
|
||||
'options' => $masterQ->options,
|
||||
'ai_hint' => $masterQ->ai_hint,
|
||||
'expected_format' => $masterQ->expected_format,
|
||||
'depends_on' => $masterQ->depends_on,
|
||||
'domain' => $masterQ->domain,
|
||||
'is_required' => $masterQ->is_required,
|
||||
'sort_order' => $masterQ->sort_order,
|
||||
'is_active' => true,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -493,13 +531,14 @@ private function cloneMasterQuestionsToProject(InterviewProject $project): void
|
||||
public function getProjectTree(int $projectId)
|
||||
{
|
||||
return InterviewCategory::where('interview_project_id', $projectId)
|
||||
->whereNull('parent_id')
|
||||
->with([
|
||||
'templates' => function ($q) {
|
||||
$q->orderBy('sort_order')->orderBy('id');
|
||||
$q->with(['questions' => function ($q2) {
|
||||
$q2->orderBy('sort_order')->orderBy('id');
|
||||
}]);
|
||||
},
|
||||
'children' => fn ($q) => $q->orderBy('sort_order')->orderBy('id')
|
||||
->with(['templates' => fn ($q2) => $q2->orderBy('sort_order')->orderBy('id')
|
||||
->with(['questions' => fn ($q3) => $q3->orderBy('sort_order')->orderBy('id')]),
|
||||
]),
|
||||
'templates' => fn ($q) => $q->orderBy('sort_order')->orderBy('id')
|
||||
->with(['questions' => fn ($q2) => $q2->orderBy('sort_order')->orderBy('id')]),
|
||||
])
|
||||
->orderBy('sort_order')
|
||||
->orderBy('id')
|
||||
|
||||
Reference in New Issue
Block a user