From 3a62a2a6e6f57ec947db81e8635fc90c7cff63b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Fri, 6 Feb 2026 21:01:24 +0900 Subject: [PATCH] =?UTF-8?q?feat:=EC=9D=B8=ED=84=B0=EB=B7=B0=20=EC=8B=9C?= =?UTF-8?q?=EB=82=98=EB=A6=AC=EC=98=A4=20=EB=A7=88=EC=9D=B4=EA=B7=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98/=EB=AA=A8=EB=8D=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- app/Models/Interview/InterviewAnswer.php | 40 +++++++++++++ app/Models/Interview/InterviewCategory.php | 44 ++++++++++++++ app/Models/Interview/InterviewQuestion.php | 44 ++++++++++++++ app/Models/Interview/InterviewSession.php | 57 +++++++++++++++++++ app/Models/Interview/InterviewTemplate.php | 45 +++++++++++++++ ...0000_create_interview_categories_table.php | 33 +++++++++++ ...00001_create_interview_templates_table.php | 35 ++++++++++++ ...00002_create_interview_questions_table.php | 37 ++++++++++++ ...100003_create_interview_sessions_table.php | 41 +++++++++++++ ..._100004_create_interview_answers_table.php | 32 +++++++++++ 10 files changed, 408 insertions(+) create mode 100644 app/Models/Interview/InterviewAnswer.php create mode 100644 app/Models/Interview/InterviewCategory.php create mode 100644 app/Models/Interview/InterviewQuestion.php create mode 100644 app/Models/Interview/InterviewSession.php create mode 100644 app/Models/Interview/InterviewTemplate.php create mode 100644 database/migrations/2026_02_06_100000_create_interview_categories_table.php create mode 100644 database/migrations/2026_02_06_100001_create_interview_templates_table.php create mode 100644 database/migrations/2026_02_06_100002_create_interview_questions_table.php create mode 100644 database/migrations/2026_02_06_100003_create_interview_sessions_table.php create mode 100644 database/migrations/2026_02_06_100004_create_interview_answers_table.php diff --git a/app/Models/Interview/InterviewAnswer.php b/app/Models/Interview/InterviewAnswer.php new file mode 100644 index 0000000..cd48438 --- /dev/null +++ b/app/Models/Interview/InterviewAnswer.php @@ -0,0 +1,40 @@ + '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'); + } +} diff --git a/app/Models/Interview/InterviewCategory.php b/app/Models/Interview/InterviewCategory.php new file mode 100644 index 0000000..c647cd0 --- /dev/null +++ b/app/Models/Interview/InterviewCategory.php @@ -0,0 +1,44 @@ + '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'); + } +} diff --git a/app/Models/Interview/InterviewQuestion.php b/app/Models/Interview/InterviewQuestion.php new file mode 100644 index 0000000..7ae519e --- /dev/null +++ b/app/Models/Interview/InterviewQuestion.php @@ -0,0 +1,44 @@ + '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'); + } +} diff --git a/app/Models/Interview/InterviewSession.php b/app/Models/Interview/InterviewSession.php new file mode 100644 index 0000000..63e94ad --- /dev/null +++ b/app/Models/Interview/InterviewSession.php @@ -0,0 +1,57 @@ + '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'); + } +} diff --git a/app/Models/Interview/InterviewTemplate.php b/app/Models/Interview/InterviewTemplate.php new file mode 100644 index 0000000..787baf4 --- /dev/null +++ b/app/Models/Interview/InterviewTemplate.php @@ -0,0 +1,45 @@ + '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'); + } +} diff --git a/database/migrations/2026_02_06_100000_create_interview_categories_table.php b/database/migrations/2026_02_06_100000_create_interview_categories_table.php new file mode 100644 index 0000000..43ce572 --- /dev/null +++ b/database/migrations/2026_02_06_100000_create_interview_categories_table.php @@ -0,0 +1,33 @@ +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'); + } +}; diff --git a/database/migrations/2026_02_06_100001_create_interview_templates_table.php b/database/migrations/2026_02_06_100001_create_interview_templates_table.php new file mode 100644 index 0000000..c1d8e5a --- /dev/null +++ b/database/migrations/2026_02_06_100001_create_interview_templates_table.php @@ -0,0 +1,35 @@ +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'); + } +}; diff --git a/database/migrations/2026_02_06_100002_create_interview_questions_table.php b/database/migrations/2026_02_06_100002_create_interview_questions_table.php new file mode 100644 index 0000000..9ce06cf --- /dev/null +++ b/database/migrations/2026_02_06_100002_create_interview_questions_table.php @@ -0,0 +1,37 @@ +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'); + } +}; diff --git a/database/migrations/2026_02_06_100003_create_interview_sessions_table.php b/database/migrations/2026_02_06_100003_create_interview_sessions_table.php new file mode 100644 index 0000000..636fc84 --- /dev/null +++ b/database/migrations/2026_02_06_100003_create_interview_sessions_table.php @@ -0,0 +1,41 @@ +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'); + } +}; diff --git a/database/migrations/2026_02_06_100004_create_interview_answers_table.php b/database/migrations/2026_02_06_100004_create_interview_answers_table.php new file mode 100644 index 0000000..b262ab8 --- /dev/null +++ b/database/migrations/2026_02_06_100004_create_interview_answers_table.php @@ -0,0 +1,32 @@ +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'); + } +};