2026-02-06 21:01:35 +09:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Services\Sales;
|
|
|
|
|
|
|
|
|
|
use App\Models\Interview\InterviewAnswer;
|
|
|
|
|
use App\Models\Interview\InterviewCategory;
|
|
|
|
|
use App\Models\Interview\InterviewQuestion;
|
|
|
|
|
use App\Models\Interview\InterviewSession;
|
|
|
|
|
use App\Models\Interview\InterviewTemplate;
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
|
|
|
|
|
class InterviewScenarioService
|
|
|
|
|
{
|
|
|
|
|
// ============================================================
|
|
|
|
|
// 카테고리 CRUD
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
public function getCategories()
|
|
|
|
|
{
|
|
|
|
|
return InterviewCategory::orderBy('sort_order')
|
|
|
|
|
->orderBy('id')
|
|
|
|
|
->get();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function createCategory(array $data): InterviewCategory
|
|
|
|
|
{
|
|
|
|
|
$tenantId = session('selected_tenant_id', 1);
|
|
|
|
|
$maxSort = InterviewCategory::max('sort_order') ?? 0;
|
|
|
|
|
|
|
|
|
|
return InterviewCategory::create([
|
|
|
|
|
'tenant_id' => $tenantId,
|
|
|
|
|
'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',
|
|
|
|
|
'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();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 21:42:14 +09:00
|
|
|
// ============================================================
|
|
|
|
|
// 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,
|
|
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 21:01:35 +09:00
|
|
|
// ============================================================
|
|
|
|
|
// 전체 트리 조회
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
}]);
|
|
|
|
|
},
|
|
|
|
|
])
|
|
|
|
|
->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();
|
|
|
|
|
}
|
|
|
|
|
}
|