- 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 헬퍼로 트리 내 카테고리 검색
733 lines
26 KiB
PHP
733 lines
26 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Sales;
|
|
|
|
use App\Models\Interview\InterviewAnswer;
|
|
use App\Models\Interview\InterviewAttachment;
|
|
use App\Models\Interview\InterviewCategory;
|
|
use App\Models\Interview\InterviewKnowledge;
|
|
use App\Models\Interview\InterviewProject;
|
|
use App\Models\Interview\InterviewQuestion;
|
|
use App\Models\Interview\InterviewSession;
|
|
use App\Models\Interview\InterviewTemplate;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
class InterviewScenarioService
|
|
{
|
|
// ============================================================
|
|
// 카테고리 CRUD
|
|
// ============================================================
|
|
|
|
public function getCategories()
|
|
{
|
|
return InterviewCategory::whereNull('parent_id')
|
|
->with(['children' => fn ($q) => $q->orderBy('sort_order')->orderBy('id')])
|
|
->orderBy('sort_order')
|
|
->orderBy('id')
|
|
->get();
|
|
}
|
|
|
|
public function createCategory(array $data): InterviewCategory
|
|
{
|
|
$tenantId = session('selected_tenant_id', 1);
|
|
$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,
|
|
'is_active' => true,
|
|
'created_by' => auth()->id(),
|
|
'updated_by' => auth()->id(),
|
|
]);
|
|
}
|
|
|
|
public function updateCategory(int $id, array $data): InterviewCategory
|
|
{
|
|
$category = InterviewCategory::findOrFail($id);
|
|
$category->update([
|
|
'name' => $data['name'],
|
|
'description' => $data['description'] ?? null,
|
|
'updated_by' => auth()->id(),
|
|
]);
|
|
|
|
return $category->fresh();
|
|
}
|
|
|
|
public function deleteCategory(int $id): void
|
|
{
|
|
$category = InterviewCategory::findOrFail($id);
|
|
$category->update(['deleted_by' => auth()->id()]);
|
|
$category->delete();
|
|
}
|
|
|
|
// ============================================================
|
|
// 템플릿(항목) CRUD
|
|
// ============================================================
|
|
|
|
public function createTemplate(array $data): InterviewTemplate
|
|
{
|
|
$tenantId = session('selected_tenant_id', 1);
|
|
$maxSort = InterviewTemplate::where('interview_category_id', $data['interview_category_id'])
|
|
->max('sort_order') ?? 0;
|
|
|
|
return InterviewTemplate::create([
|
|
'tenant_id' => $tenantId,
|
|
'interview_category_id' => $data['interview_category_id'],
|
|
'name' => $data['name'],
|
|
'description' => $data['description'] ?? null,
|
|
'sort_order' => $maxSort + 1,
|
|
'is_active' => true,
|
|
'created_by' => auth()->id(),
|
|
'updated_by' => auth()->id(),
|
|
]);
|
|
}
|
|
|
|
public function updateTemplate(int $id, array $data): InterviewTemplate
|
|
{
|
|
$template = InterviewTemplate::findOrFail($id);
|
|
$template->update([
|
|
'name' => $data['name'],
|
|
'description' => $data['description'] ?? null,
|
|
'updated_by' => auth()->id(),
|
|
]);
|
|
|
|
return $template->fresh();
|
|
}
|
|
|
|
public function deleteTemplate(int $id): void
|
|
{
|
|
$template = InterviewTemplate::findOrFail($id);
|
|
$template->update(['deleted_by' => auth()->id()]);
|
|
$template->delete();
|
|
}
|
|
|
|
// ============================================================
|
|
// 질문 CRUD
|
|
// ============================================================
|
|
|
|
public function createQuestion(array $data): InterviewQuestion
|
|
{
|
|
$tenantId = session('selected_tenant_id', 1);
|
|
$maxSort = InterviewQuestion::where('interview_template_id', $data['interview_template_id'])
|
|
->max('sort_order') ?? 0;
|
|
|
|
return InterviewQuestion::create([
|
|
'tenant_id' => $tenantId,
|
|
'interview_template_id' => $data['interview_template_id'],
|
|
'question_text' => $data['question_text'],
|
|
'question_type' => $data['question_type'] ?? 'checkbox',
|
|
'options' => $data['options'] ?? null,
|
|
'ai_hint' => $data['ai_hint'] ?? null,
|
|
'expected_format' => $data['expected_format'] ?? null,
|
|
'depends_on' => $data['depends_on'] ?? null,
|
|
'domain' => $data['domain'] ?? null,
|
|
'is_required' => $data['is_required'] ?? false,
|
|
'sort_order' => $maxSort + 1,
|
|
'is_active' => true,
|
|
'created_by' => auth()->id(),
|
|
'updated_by' => auth()->id(),
|
|
]);
|
|
}
|
|
|
|
public function updateQuestion(int $id, array $data): InterviewQuestion
|
|
{
|
|
$question = InterviewQuestion::findOrFail($id);
|
|
$question->update([
|
|
'question_text' => $data['question_text'],
|
|
'question_type' => $data['question_type'] ?? $question->question_type,
|
|
'is_required' => $data['is_required'] ?? $question->is_required,
|
|
'updated_by' => auth()->id(),
|
|
]);
|
|
|
|
return $question->fresh();
|
|
}
|
|
|
|
public function deleteQuestion(int $id): void
|
|
{
|
|
$question = InterviewQuestion::findOrFail($id);
|
|
$question->update(['deleted_by' => auth()->id()]);
|
|
$question->delete();
|
|
}
|
|
|
|
// ============================================================
|
|
// MD 파일 일괄 가져오기
|
|
// ============================================================
|
|
|
|
public function bulkImport(int $categoryId, array $templates): array
|
|
{
|
|
return DB::transaction(function () use ($categoryId, $templates) {
|
|
$tenantId = session('selected_tenant_id', 1);
|
|
$userId = auth()->id();
|
|
$maxTemplateSort = InterviewTemplate::where('interview_category_id', $categoryId)
|
|
->max('sort_order') ?? 0;
|
|
|
|
$createdTemplates = 0;
|
|
$createdQuestions = 0;
|
|
|
|
foreach ($templates as $tpl) {
|
|
$maxTemplateSort++;
|
|
$template = InterviewTemplate::create([
|
|
'tenant_id' => $tenantId,
|
|
'interview_category_id' => $categoryId,
|
|
'name' => $tpl['name'],
|
|
'sort_order' => $maxTemplateSort,
|
|
'is_active' => true,
|
|
'created_by' => $userId,
|
|
'updated_by' => $userId,
|
|
]);
|
|
$createdTemplates++;
|
|
|
|
$questionSort = 0;
|
|
foreach ($tpl['questions'] as $questionText) {
|
|
$questionSort++;
|
|
InterviewQuestion::create([
|
|
'tenant_id' => $tenantId,
|
|
'interview_template_id' => $template->id,
|
|
'question_text' => $questionText,
|
|
'question_type' => 'checkbox',
|
|
'is_required' => false,
|
|
'sort_order' => $questionSort,
|
|
'is_active' => true,
|
|
'created_by' => $userId,
|
|
'updated_by' => $userId,
|
|
]);
|
|
$createdQuestions++;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'templates_created' => $createdTemplates,
|
|
'questions_created' => $createdQuestions,
|
|
];
|
|
});
|
|
}
|
|
|
|
// ============================================================
|
|
// 전체 트리 조회
|
|
// ============================================================
|
|
|
|
public function getTree()
|
|
{
|
|
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();
|
|
}
|
|
|
|
// ============================================================
|
|
// 세션 관리
|
|
// ============================================================
|
|
|
|
public function getSessions(array $filters = [])
|
|
{
|
|
$query = InterviewSession::with(['category', 'interviewer'])
|
|
->orderByDesc('interview_date')
|
|
->orderByDesc('id');
|
|
|
|
if (! empty($filters['status'])) {
|
|
$query->where('status', $filters['status']);
|
|
}
|
|
|
|
if (! empty($filters['category_id'])) {
|
|
$query->where('interview_category_id', $filters['category_id']);
|
|
}
|
|
|
|
return $query->paginate(20);
|
|
}
|
|
|
|
public function startSession(array $data): InterviewSession
|
|
{
|
|
return DB::transaction(function () use ($data) {
|
|
$tenantId = session('selected_tenant_id', 1);
|
|
$categoryId = $data['interview_category_id'];
|
|
|
|
// 카테고리의 모든 활성 템플릿과 질문 가져오기
|
|
$templates = InterviewTemplate::where('interview_category_id', $categoryId)
|
|
->where('is_active', true)
|
|
->with(['questions' => function ($q) {
|
|
$q->where('is_active', true)->orderBy('sort_order');
|
|
}])
|
|
->orderBy('sort_order')
|
|
->get();
|
|
|
|
$totalQuestions = $templates->sum(fn ($t) => $t->questions->count());
|
|
|
|
// 세션 생성
|
|
$session = InterviewSession::create([
|
|
'tenant_id' => $tenantId,
|
|
'interview_category_id' => $categoryId,
|
|
'interviewer_id' => auth()->id(),
|
|
'interviewee_name' => $data['interviewee_name'] ?? null,
|
|
'interviewee_company' => $data['interviewee_company'] ?? null,
|
|
'interview_date' => $data['interview_date'] ?? now()->toDateString(),
|
|
'status' => 'in_progress',
|
|
'total_questions' => $totalQuestions,
|
|
'answered_questions' => 0,
|
|
'memo' => $data['memo'] ?? null,
|
|
'created_by' => auth()->id(),
|
|
'updated_by' => auth()->id(),
|
|
]);
|
|
|
|
// 모든 질문에 대해 빈 답변 레코드 생성
|
|
foreach ($templates as $template) {
|
|
foreach ($template->questions as $question) {
|
|
InterviewAnswer::create([
|
|
'tenant_id' => $tenantId,
|
|
'interview_session_id' => $session->id,
|
|
'interview_question_id' => $question->id,
|
|
'interview_template_id' => $template->id,
|
|
'is_checked' => false,
|
|
]);
|
|
}
|
|
}
|
|
|
|
return $session;
|
|
});
|
|
}
|
|
|
|
public function getSessionDetail(int $id)
|
|
{
|
|
return InterviewSession::with([
|
|
'category',
|
|
'interviewer',
|
|
'answers' => function ($q) {
|
|
$q->with(['question', 'template']);
|
|
},
|
|
])->findOrFail($id);
|
|
}
|
|
|
|
public function toggleAnswer(array $data): InterviewAnswer
|
|
{
|
|
$answer = InterviewAnswer::where('interview_session_id', $data['session_id'])
|
|
->where('interview_question_id', $data['question_id'])
|
|
->firstOrFail();
|
|
|
|
$answer->update([
|
|
'is_checked' => ! $answer->is_checked,
|
|
'answer_text' => $data['answer_text'] ?? $answer->answer_text,
|
|
'memo' => $data['memo'] ?? $answer->memo,
|
|
]);
|
|
|
|
// answered_questions 갱신
|
|
$session = InterviewSession::findOrFail($data['session_id']);
|
|
$answeredCount = InterviewAnswer::where('interview_session_id', $session->id)
|
|
->where('is_checked', true)
|
|
->count();
|
|
$session->update([
|
|
'answered_questions' => $answeredCount,
|
|
'updated_by' => auth()->id(),
|
|
]);
|
|
|
|
return $answer->fresh();
|
|
}
|
|
|
|
public function completeSession(int $id): InterviewSession
|
|
{
|
|
$session = InterviewSession::findOrFail($id);
|
|
|
|
$answeredCount = InterviewAnswer::where('interview_session_id', $session->id)
|
|
->where('is_checked', true)
|
|
->count();
|
|
|
|
$session->update([
|
|
'status' => 'completed',
|
|
'answered_questions' => $answeredCount,
|
|
'completed_at' => now(),
|
|
'updated_by' => auth()->id(),
|
|
]);
|
|
|
|
return $session->fresh();
|
|
}
|
|
|
|
// ============================================================
|
|
// 프로젝트 CRUD
|
|
// ============================================================
|
|
|
|
public function getProjects(array $filters = [])
|
|
{
|
|
$query = InterviewProject::with('creator')
|
|
->orderByDesc('id');
|
|
|
|
if (! empty($filters['status'])) {
|
|
$query->where('status', $filters['status']);
|
|
}
|
|
|
|
if (! empty($filters['search'])) {
|
|
$search = $filters['search'];
|
|
$query->where(function ($q) use ($search) {
|
|
$q->where('company_name', 'like', "%{$search}%")
|
|
->orWhere('company_type', 'like', "%{$search}%");
|
|
});
|
|
}
|
|
|
|
return $query->paginate(20);
|
|
}
|
|
|
|
public function getProject(int $id): InterviewProject
|
|
{
|
|
return InterviewProject::with([
|
|
'categories.templates.questions',
|
|
'attachments',
|
|
'sessions',
|
|
])->findOrFail($id);
|
|
}
|
|
|
|
public function createProject(array $data): InterviewProject
|
|
{
|
|
$tenantId = session('selected_tenant_id', 1);
|
|
|
|
return DB::transaction(function () use ($data, $tenantId) {
|
|
$project = InterviewProject::create([
|
|
'tenant_id' => $tenantId,
|
|
'company_name' => $data['company_name'],
|
|
'company_type' => $data['company_type'] ?? null,
|
|
'contact_person' => $data['contact_person'] ?? null,
|
|
'contact_info' => $data['contact_info'] ?? null,
|
|
'status' => 'draft',
|
|
'product_categories' => $data['product_categories'] ?? null,
|
|
'progress_percent' => 0,
|
|
'created_by' => auth()->id(),
|
|
'updated_by' => auth()->id(),
|
|
]);
|
|
|
|
// 마스터 질문 데이터에서 8개 도메인 카테고리 복제
|
|
$this->cloneMasterQuestionsToProject($project);
|
|
|
|
return $project->fresh();
|
|
});
|
|
}
|
|
|
|
public function updateProject(int $id, array $data): InterviewProject
|
|
{
|
|
$project = InterviewProject::findOrFail($id);
|
|
$project->update([
|
|
...$data,
|
|
'updated_by' => auth()->id(),
|
|
]);
|
|
|
|
return $project->fresh();
|
|
}
|
|
|
|
public function deleteProject(int $id): void
|
|
{
|
|
$project = InterviewProject::findOrFail($id);
|
|
$project->update(['deleted_by' => auth()->id()]);
|
|
$project->delete();
|
|
}
|
|
|
|
/**
|
|
* 마스터 질문 데이터를 프로젝트에 복제
|
|
*/
|
|
private function cloneMasterQuestionsToProject(InterviewProject $project): void
|
|
{
|
|
$tenantId = $project->tenant_id;
|
|
$userId = auth()->id();
|
|
|
|
// 루트 마스터 카테고리 (parent_id=null, project=null)
|
|
$masterRoots = InterviewCategory::withoutGlobalScopes()
|
|
->where('tenant_id', $tenantId)
|
|
->whereNull('interview_project_id')
|
|
->whereNull('parent_id')
|
|
->with(['children.templates.questions', 'templates.questions'])
|
|
->orderBy('sort_order')
|
|
->get();
|
|
|
|
foreach ($masterRoots as $masterRoot) {
|
|
// 루트 카테고리 복제
|
|
$newRoot = InterviewCategory::create([
|
|
'tenant_id' => $tenantId,
|
|
'interview_project_id' => $project->id,
|
|
'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,
|
|
]);
|
|
|
|
// 루트의 직접 templates 복제
|
|
$this->cloneTemplates($masterRoot->templates, $newRoot->id, $tenantId, $userId);
|
|
|
|
// 자식 카테고리 복제
|
|
foreach ($masterRoot->children as $masterChild) {
|
|
$newChild = InterviewCategory::create([
|
|
'tenant_id' => $tenantId,
|
|
'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,
|
|
]);
|
|
|
|
$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,
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 프로젝트별 트리 조회
|
|
*/
|
|
public function getProjectTree(int $projectId)
|
|
{
|
|
return InterviewCategory::where('interview_project_id', $projectId)
|
|
->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();
|
|
}
|
|
|
|
/**
|
|
* 프로젝트 진행률 갱신
|
|
*/
|
|
public function updateProjectProgress(int $projectId): InterviewProject
|
|
{
|
|
$project = InterviewProject::findOrFail($projectId);
|
|
|
|
// 프로젝트 연결 카테고리의 모든 질문 수
|
|
$totalQuestions = InterviewQuestion::whereHas('template.category', function ($q) use ($projectId) {
|
|
$q->where('interview_project_id', $projectId);
|
|
})->where('is_active', true)->count();
|
|
|
|
if ($totalQuestions === 0) {
|
|
$project->update(['progress_percent' => 0, 'updated_by' => auth()->id()]);
|
|
|
|
return $project->fresh();
|
|
}
|
|
|
|
// 프로젝트 세션의 답변 완료 수
|
|
$answeredQuestions = InterviewAnswer::whereHas('session', function ($q) use ($projectId) {
|
|
$q->where('interview_project_id', $projectId);
|
|
})->where('is_checked', true)->count();
|
|
|
|
$progress = min(100, (int) round(($answeredQuestions / $totalQuestions) * 100));
|
|
$project->update(['progress_percent' => $progress, 'updated_by' => auth()->id()]);
|
|
|
|
return $project->fresh();
|
|
}
|
|
|
|
// ============================================================
|
|
// 첨부파일 관리
|
|
// ============================================================
|
|
|
|
public function getAttachments(int $projectId)
|
|
{
|
|
return InterviewAttachment::where('interview_project_id', $projectId)
|
|
->with('creator')
|
|
->orderByDesc('id')
|
|
->get();
|
|
}
|
|
|
|
public function uploadAttachment(int $projectId, array $data, $file): InterviewAttachment
|
|
{
|
|
$tenantId = session('selected_tenant_id', 1);
|
|
$path = $file->store("interviews/{$projectId}", 'local');
|
|
|
|
return InterviewAttachment::create([
|
|
'tenant_id' => $tenantId,
|
|
'interview_project_id' => $projectId,
|
|
'file_type' => $data['file_type'] ?? 'other',
|
|
'file_name' => $file->getClientOriginalName(),
|
|
'file_path' => $path,
|
|
'file_size' => $file->getSize(),
|
|
'mime_type' => $file->getMimeType(),
|
|
'ai_analysis_status' => 'pending',
|
|
'description' => $data['description'] ?? null,
|
|
'created_by' => auth()->id(),
|
|
]);
|
|
}
|
|
|
|
public function deleteAttachment(int $id): void
|
|
{
|
|
$attachment = InterviewAttachment::findOrFail($id);
|
|
|
|
if (Storage::disk('local')->exists($attachment->file_path)) {
|
|
Storage::disk('local')->delete($attachment->file_path);
|
|
}
|
|
|
|
$attachment->delete();
|
|
}
|
|
|
|
// ============================================================
|
|
// 지식 관리
|
|
// ============================================================
|
|
|
|
public function getKnowledge(int $projectId, array $filters = [])
|
|
{
|
|
$query = InterviewKnowledge::where('interview_project_id', $projectId)
|
|
->with('creator')
|
|
->orderByDesc('id');
|
|
|
|
if (! empty($filters['domain'])) {
|
|
$query->where('domain', $filters['domain']);
|
|
}
|
|
|
|
if (! empty($filters['is_verified'])) {
|
|
$query->where('is_verified', $filters['is_verified'] === 'true');
|
|
}
|
|
|
|
if (! empty($filters['min_confidence'])) {
|
|
$query->where('confidence', '>=', (float) $filters['min_confidence']);
|
|
}
|
|
|
|
return $query->get();
|
|
}
|
|
|
|
public function createKnowledge(int $projectId, array $data): InterviewKnowledge
|
|
{
|
|
$tenantId = session('selected_tenant_id', 1);
|
|
|
|
return InterviewKnowledge::create([
|
|
'tenant_id' => $tenantId,
|
|
'interview_project_id' => $projectId,
|
|
'domain' => $data['domain'],
|
|
'knowledge_type' => $data['knowledge_type'],
|
|
'title' => $data['title'],
|
|
'content' => $data['content'],
|
|
'source_type' => $data['source_type'] ?? 'manual',
|
|
'source_id' => $data['source_id'] ?? null,
|
|
'confidence' => $data['confidence'] ?? 1.00,
|
|
'is_verified' => $data['source_type'] === 'manual',
|
|
'verified_by' => $data['source_type'] === 'manual' ? auth()->id() : null,
|
|
'verified_at' => $data['source_type'] === 'manual' ? now() : null,
|
|
'created_by' => auth()->id(),
|
|
]);
|
|
}
|
|
|
|
public function updateKnowledge(int $id, array $data): InterviewKnowledge
|
|
{
|
|
$knowledge = InterviewKnowledge::findOrFail($id);
|
|
$knowledge->update($data);
|
|
|
|
return $knowledge->fresh();
|
|
}
|
|
|
|
public function verifyKnowledge(int $id): InterviewKnowledge
|
|
{
|
|
$knowledge = InterviewKnowledge::findOrFail($id);
|
|
$knowledge->update([
|
|
'is_verified' => ! $knowledge->is_verified,
|
|
'verified_by' => $knowledge->is_verified ? null : auth()->id(),
|
|
'verified_at' => $knowledge->is_verified ? null : now(),
|
|
]);
|
|
|
|
return $knowledge->fresh();
|
|
}
|
|
|
|
public function deleteKnowledge(int $id): void
|
|
{
|
|
$knowledge = InterviewKnowledge::findOrFail($id);
|
|
$knowledge->delete();
|
|
}
|
|
|
|
// ============================================================
|
|
// 답변 저장 (구조화 답변 포함)
|
|
// ============================================================
|
|
|
|
public function saveAnswer(array $data): InterviewAnswer
|
|
{
|
|
$answer = InterviewAnswer::where('interview_session_id', $data['session_id'])
|
|
->where('interview_question_id', $data['question_id'])
|
|
->firstOrFail();
|
|
|
|
$updateData = [
|
|
'is_checked' => $data['is_checked'] ?? $answer->is_checked,
|
|
];
|
|
|
|
if (isset($data['answer_text'])) {
|
|
$updateData['answer_text'] = $data['answer_text'];
|
|
}
|
|
if (isset($data['answer_data'])) {
|
|
$updateData['answer_data'] = $data['answer_data'];
|
|
}
|
|
if (isset($data['attachments'])) {
|
|
$updateData['attachments'] = $data['attachments'];
|
|
}
|
|
if (isset($data['memo'])) {
|
|
$updateData['memo'] = $data['memo'];
|
|
}
|
|
|
|
$answer->update($updateData);
|
|
|
|
// answered_questions 갱신
|
|
$session = InterviewSession::findOrFail($data['session_id']);
|
|
$answeredCount = InterviewAnswer::where('interview_session_id', $session->id)
|
|
->where('is_checked', true)
|
|
->count();
|
|
$session->update([
|
|
'answered_questions' => $answeredCount,
|
|
'updated_by' => auth()->id(),
|
|
]);
|
|
|
|
return $answer->fresh();
|
|
}
|
|
}
|