feat:인터뷰 시나리오 마이그레이션/모델 추가

- interview_categories, interview_templates, interview_questions 테이블 생성
- interview_sessions, interview_answers 테이블 생성
- InterviewCategory, InterviewTemplate, InterviewQuestion 모델 추가
- InterviewSession, InterviewAnswer 모델 추가
- 멀티테넌트(tenant_id) 지원, 감사 로깅(Auditable) 적용

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
김보곤
2026-02-06 21:01:24 +09:00
parent bdf6bcc480
commit 3a62a2a6e6
10 changed files with 408 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Models\Interview;
use App\Traits\BelongsToTenant;
use Illuminate\Database\Eloquent\Model;
class InterviewAnswer extends Model
{
use BelongsToTenant;
protected $fillable = [
'tenant_id',
'interview_session_id',
'interview_question_id',
'interview_template_id',
'is_checked',
'answer_text',
'memo',
];
protected $casts = [
'is_checked' => 'boolean',
];
public function session()
{
return $this->belongsTo(InterviewSession::class, 'interview_session_id');
}
public function question()
{
return $this->belongsTo(InterviewQuestion::class, 'interview_question_id');
}
public function template()
{
return $this->belongsTo(InterviewTemplate::class, 'interview_template_id');
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Models\Interview;
use App\Traits\Auditable;
use App\Traits\BelongsToTenant;
use App\Traits\ModelTrait;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class InterviewCategory extends Model
{
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
protected $fillable = [
'tenant_id',
'name',
'description',
'sort_order',
'is_active',
'created_by',
'updated_by',
'deleted_by',
];
protected $casts = [
'is_active' => 'boolean',
'sort_order' => 'integer',
];
protected $hidden = [
'deleted_at',
];
public function templates()
{
return $this->hasMany(InterviewTemplate::class, 'interview_category_id');
}
public function sessions()
{
return $this->hasMany(InterviewSession::class, 'interview_category_id');
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Models\Interview;
use App\Traits\Auditable;
use App\Traits\BelongsToTenant;
use App\Traits\ModelTrait;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class InterviewQuestion extends Model
{
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
protected $fillable = [
'tenant_id',
'interview_template_id',
'question_text',
'question_type',
'options',
'is_required',
'sort_order',
'is_active',
'created_by',
'updated_by',
'deleted_by',
];
protected $casts = [
'options' => 'array',
'is_required' => 'boolean',
'is_active' => 'boolean',
'sort_order' => 'integer',
];
protected $hidden = [
'deleted_at',
];
public function template()
{
return $this->belongsTo(InterviewTemplate::class, 'interview_template_id');
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Models\Interview;
use App\Traits\Auditable;
use App\Traits\BelongsToTenant;
use App\Traits\ModelTrait;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class InterviewSession extends Model
{
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
protected $fillable = [
'tenant_id',
'interview_category_id',
'interviewer_id',
'interviewee_name',
'interviewee_company',
'interview_date',
'status',
'total_questions',
'answered_questions',
'memo',
'completed_at',
'created_by',
'updated_by',
'deleted_by',
];
protected $casts = [
'interview_date' => 'date',
'completed_at' => 'datetime',
'total_questions' => 'integer',
'answered_questions' => 'integer',
];
protected $hidden = [
'deleted_at',
];
public function category()
{
return $this->belongsTo(InterviewCategory::class, 'interview_category_id');
}
public function interviewer()
{
return $this->belongsTo(\App\Models\User::class, 'interviewer_id');
}
public function answers()
{
return $this->hasMany(InterviewAnswer::class, 'interview_session_id');
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Models\Interview;
use App\Traits\Auditable;
use App\Traits\BelongsToTenant;
use App\Traits\ModelTrait;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class InterviewTemplate extends Model
{
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
protected $fillable = [
'tenant_id',
'interview_category_id',
'name',
'description',
'sort_order',
'is_active',
'created_by',
'updated_by',
'deleted_by',
];
protected $casts = [
'is_active' => 'boolean',
'sort_order' => 'integer',
];
protected $hidden = [
'deleted_at',
];
public function category()
{
return $this->belongsTo(InterviewCategory::class, 'interview_category_id');
}
public function questions()
{
return $this->hasMany(InterviewQuestion::class, 'interview_template_id');
}
}

View File

@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('interview_categories', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
$table->string('name', 100)->comment('카테고리명 (예: 제조-방화셔터)');
$table->text('description')->nullable()->comment('설명');
$table->unsignedInteger('sort_order')->default(0)->comment('정렬순서');
$table->boolean('is_active')->default(true)->comment('활성여부');
$table->unsignedBigInteger('created_by')->nullable()->comment('등록자');
$table->unsignedBigInteger('updated_by')->nullable()->comment('수정자');
$table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자');
$table->timestamps();
$table->softDeletes();
$table->index('tenant_id', 'idx_interview_categories_tenant');
$table->index('sort_order', 'idx_interview_categories_sort');
});
}
public function down(): void
{
Schema::dropIfExists('interview_categories');
}
};

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('interview_templates', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
$table->unsignedBigInteger('interview_category_id')->comment('카테고리 ID');
$table->string('name', 200)->comment('항목명 (예: 견적서 제작)');
$table->text('description')->nullable()->comment('설명');
$table->unsignedInteger('sort_order')->default(0)->comment('정렬순서');
$table->boolean('is_active')->default(true)->comment('활성여부');
$table->unsignedBigInteger('created_by')->nullable()->comment('등록자');
$table->unsignedBigInteger('updated_by')->nullable()->comment('수정자');
$table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자');
$table->timestamps();
$table->softDeletes();
$table->index('tenant_id', 'idx_interview_templates_tenant');
$table->index('interview_category_id', 'idx_interview_templates_category');
$table->index('sort_order', 'idx_interview_templates_sort');
});
}
public function down(): void
{
Schema::dropIfExists('interview_templates');
}
};

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('interview_questions', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
$table->unsignedBigInteger('interview_template_id')->comment('템플릿(항목) ID');
$table->string('question_text', 500)->comment('질문 내용');
$table->string('question_type', 20)->default('checkbox')->comment('질문유형: checkbox/text');
$table->json('options')->nullable()->comment('확장용 옵션');
$table->boolean('is_required')->default(false)->comment('필수여부');
$table->unsignedInteger('sort_order')->default(0)->comment('정렬순서');
$table->boolean('is_active')->default(true)->comment('활성여부');
$table->unsignedBigInteger('created_by')->nullable()->comment('등록자');
$table->unsignedBigInteger('updated_by')->nullable()->comment('수정자');
$table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자');
$table->timestamps();
$table->softDeletes();
$table->index('tenant_id', 'idx_interview_questions_tenant');
$table->index('interview_template_id', 'idx_interview_questions_template');
$table->index('sort_order', 'idx_interview_questions_sort');
});
}
public function down(): void
{
Schema::dropIfExists('interview_questions');
}
};

View File

@@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('interview_sessions', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
$table->unsignedBigInteger('interview_category_id')->comment('사용한 카테고리 ID');
$table->unsignedBigInteger('interviewer_id')->comment('면담자(매니저) ID');
$table->string('interviewee_name', 100)->nullable()->comment('면담 상대 이름');
$table->string('interviewee_company', 200)->nullable()->comment('면담 상대 회사');
$table->date('interview_date')->comment('면담 일자');
$table->string('status', 20)->default('in_progress')->comment('상태: in_progress/completed');
$table->unsignedInteger('total_questions')->default(0)->comment('총 질문 수');
$table->unsignedInteger('answered_questions')->default(0)->comment('답변 완료 수');
$table->text('memo')->nullable()->comment('메모');
$table->timestamp('completed_at')->nullable()->comment('완료 일시');
$table->unsignedBigInteger('created_by')->nullable()->comment('등록자');
$table->unsignedBigInteger('updated_by')->nullable()->comment('수정자');
$table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자');
$table->timestamps();
$table->softDeletes();
$table->index(['tenant_id', 'interviewer_id'], 'idx_interview_sessions_tenant_interviewer');
$table->index('interview_category_id', 'idx_interview_sessions_category');
$table->index('interview_date', 'idx_interview_sessions_date');
$table->index('status', 'idx_interview_sessions_status');
});
}
public function down(): void
{
Schema::dropIfExists('interview_sessions');
}
};

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('interview_answers', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
$table->unsignedBigInteger('interview_session_id')->comment('세션 ID');
$table->unsignedBigInteger('interview_question_id')->comment('질문 ID');
$table->unsignedBigInteger('interview_template_id')->comment('템플릿 ID (비정규화)');
$table->boolean('is_checked')->default(false)->comment('체크 여부');
$table->text('answer_text')->nullable()->comment('텍스트 답변');
$table->text('memo')->nullable()->comment('개별 메모');
$table->timestamps();
$table->unique(['interview_session_id', 'interview_question_id'], 'uniq_session_question');
$table->index('tenant_id', 'idx_interview_answers_tenant');
$table->index('interview_template_id', 'idx_interview_answers_template');
});
}
public function down(): void
{
Schema::dropIfExists('interview_answers');
}
};