Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
2026-01-30 13:51:49 +09:00
29 changed files with 1514 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('ai_configs', function (Blueprint $table) {
$table->id();
$table->string('name', 50)->comment('설정 이름 (Gemini, Claude 등)');
$table->string('provider', 30)->comment('제공자 (gemini, claude, openai)');
$table->string('api_key', 255)->comment('API 키');
$table->string('model', 100)->comment('모델명 (gemini-2.0-flash 등)');
$table->string('base_url', 255)->nullable()->comment('API Base URL');
$table->text('description')->nullable()->comment('설명');
$table->boolean('is_active')->default(false)->comment('활성화 여부 (provider당 1개만 활성화)');
$table->json('options')->nullable()->comment('추가 옵션 (JSON)');
$table->timestamps();
$table->softDeletes();
$table->index('provider');
$table->index(['provider', 'is_active']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('ai_configs');
}
};

View File

@@ -0,0 +1,46 @@
<?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('barobill_subscriptions', function (Blueprint $table) {
$table->id();
$table->foreignId('member_id')->comment('바로빌 회원사 ID');
$table->enum('service_type', ['bank_account', 'card', 'hometax'])
->comment('서비스 유형: bank_account=계좌조회, card=카드내역, hometax=홈텍스');
$table->unsignedInteger('monthly_fee')->default(0)->comment('월정액 금액 (원)');
$table->date('started_at')->comment('구독 시작일');
$table->date('ended_at')->nullable()->comment('구독 종료일 (null=진행중)');
$table->boolean('is_active')->default(true)->comment('활성 상태');
$table->text('memo')->nullable()->comment('메모');
$table->timestamps();
$table->softDeletes();
// 인덱스
$table->index(['member_id', 'service_type']);
$table->index(['is_active', 'service_type']);
// 외래키
$table->foreign('member_id')
->references('id')
->on('barobill_members')
->onDelete('cascade');
});
}
public function down(): void
{
Schema::dropIfExists('barobill_subscriptions');
}
};

View File

@@ -0,0 +1,52 @@
<?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('barobill_billing_records', function (Blueprint $table) {
$table->id();
$table->foreignId('member_id')->comment('바로빌 회원사 ID');
$table->string('billing_month', 7)->comment('과금 월 (YYYY-MM)');
$table->enum('service_type', ['tax_invoice', 'bank_account', 'card', 'hometax'])
->comment('서비스 유형');
$table->enum('billing_type', ['subscription', 'usage'])
->comment('과금 유형: subscription=월정액, usage=건별');
$table->unsignedInteger('quantity')->default(1)->comment('수량 (월정액=1, 건별=사용건수)');
$table->unsignedInteger('unit_price')->default(0)->comment('단가 (원)');
$table->unsignedInteger('total_amount')->default(0)->comment('총액 (원)');
$table->date('billed_at')->comment('과금일');
$table->text('description')->nullable()->comment('설명');
$table->timestamps();
// 인덱스
$table->index(['member_id', 'billing_month']);
$table->index(['billing_month', 'service_type']);
$table->index(['billing_month', 'billing_type']);
$table->index('billed_at');
// 중복 방지 (같은 월, 같은 서비스, 같은 과금유형)
$table->unique(['member_id', 'billing_month', 'service_type', 'billing_type'], 'billing_unique');
// 외래키
$table->foreign('member_id')
->references('id')
->on('barobill_members')
->onDelete('cascade');
});
}
public function down(): void
{
Schema::dropIfExists('barobill_billing_records');
}
};

View File

@@ -0,0 +1,53 @@
<?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('barobill_monthly_summaries', function (Blueprint $table) {
$table->id();
$table->foreignId('member_id')->comment('바로빌 회원사 ID');
$table->string('billing_month', 7)->comment('과금 월 (YYYY-MM)');
// 월정액 항목별
$table->unsignedInteger('bank_account_fee')->default(0)->comment('계좌조회 월정액');
$table->unsignedInteger('card_fee')->default(0)->comment('카드내역 월정액');
$table->unsignedInteger('hometax_fee')->default(0)->comment('홈텍스 월정액');
$table->unsignedInteger('subscription_total')->default(0)->comment('월정액 합계');
// 건별 사용량
$table->unsignedInteger('tax_invoice_count')->default(0)->comment('세금계산서 발행 건수');
$table->unsignedInteger('tax_invoice_amount')->default(0)->comment('세금계산서 과금액');
$table->unsignedInteger('usage_total')->default(0)->comment('건별 사용 합계');
// 총합계
$table->unsignedInteger('grand_total')->default(0)->comment('총합계');
$table->timestamps();
// 인덱스
$table->unique(['member_id', 'billing_month']);
$table->index('billing_month');
// 외래키
$table->foreign('member_id')
->references('id')
->on('barobill_members')
->onDelete('cascade');
});
}
public function down(): void
{
Schema::dropIfExists('barobill_monthly_summaries');
}
};

View File

@@ -0,0 +1,46 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('barobill_pricing_policies', function (Blueprint $table) {
$table->id();
$table->string('service_type', 50)->comment('서비스 유형: card, tax_invoice, bank_account');
$table->string('name', 100)->comment('정책명');
$table->string('description')->nullable()->comment('설명');
// 기본 무료 제공량
$table->integer('free_quota')->default(0)->comment('무료 기본 제공량');
$table->string('free_quota_unit', 20)->default('개')->comment('무료 제공 단위 (장, 건, 개 등)');
// 추가 과금 설정
$table->integer('additional_unit')->default(1)->comment('추가 과금 단위 (1, 50 등)');
$table->string('additional_unit_label', 20)->default('개')->comment('추가 단위 라벨');
$table->integer('additional_price')->default(0)->comment('추가 과금 금액 (원)');
$table->boolean('is_active')->default(true)->comment('활성화 여부');
$table->integer('sort_order')->default(0)->comment('정렬 순서');
$table->timestamps();
$table->unique('service_type');
$table->index('is_active');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('barobill_pricing_policies');
}
};

View File

@@ -0,0 +1,52 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
// 영업담당자 계층 구조 (상위 관리자)
$table->unsignedBigInteger('parent_id')->nullable()->after('is_super_admin');
$table->foreign('parent_id')->references('id')->on('users')->onDelete('set null');
// 승인 상태: pending(대기), approved(승인), rejected(반려)
$table->string('approval_status', 20)->default('approved')->after('parent_id');
// 승인 관련 정보
$table->unsignedBigInteger('approved_by')->nullable()->after('approval_status');
$table->timestamp('approved_at')->nullable()->after('approved_by');
$table->text('rejection_reason')->nullable()->after('approved_at');
// 인덱스
$table->index('parent_id');
$table->index('approval_status');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropForeign(['parent_id']);
$table->dropIndex(['parent_id']);
$table->dropIndex(['approval_status']);
$table->dropColumn([
'parent_id',
'approval_status',
'approved_by',
'approved_at',
'rejection_reason',
]);
});
}
};

View File

@@ -0,0 +1,53 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('sales_manager_documents', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id');
$table->unsignedBigInteger('user_id');
// 파일 정보
$table->string('file_path', 500);
$table->string('original_name', 255);
$table->string('stored_name', 255);
$table->string('mime_type', 100)->nullable();
$table->unsignedBigInteger('file_size')->default(0);
// 문서 타입: id_card(신분증), business_license(사업자등록증), contract(계약서), other(기타)
$table->string('document_type', 50)->default('other');
$table->string('description', 500)->nullable();
// 메타 정보
$table->unsignedBigInteger('uploaded_by')->nullable();
$table->timestamps();
$table->softDeletes();
$table->unsignedBigInteger('deleted_by')->nullable();
// 외래키 및 인덱스
$table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('cascade');
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->foreign('uploaded_by')->references('id')->on('users')->onDelete('set null');
$table->index(['tenant_id', 'user_id']);
$table->index('document_type');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('sales_manager_documents');
}
};

View File

@@ -0,0 +1,75 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* 영업파트너 영업권(명함등록) 테이블
*
* 영업 프로세스:
* 1. 영업파트너가 명함 등록 (status = 'active')
* 2. 2개월간 영업권 유효 (expires_at)
* 3. 계약 성사 시 테넌트 전환 (status = 'converted', tenant_id 연결)
* 4. 미성사/만료 시 (status = 'expired')
* 5. 만료 후 1개월 쿨다운 (cooldown_ends_at) 이후 재등록 가능
*/
return new class extends Migration
{
public function up(): void
{
Schema::create('tenant_prospects', function (Blueprint $table) {
$table->id();
// 회사 정보
$table->string('business_number', 20)->index()->comment('사업자번호 (중복체크 키)');
$table->string('company_name', 100)->comment('회사명');
$table->string('ceo_name', 50)->nullable()->comment('대표자명');
$table->string('contact_phone', 20)->nullable()->comment('연락처');
$table->string('contact_email', 100)->nullable()->comment('이메일');
$table->string('address', 500)->nullable()->comment('주소');
// 영업파트너 정보
$table->foreignId('registered_by')
->constrained('users')
->cascadeOnDelete()
->comment('등록한 영업파트너 ID');
// 명함 이미지
$table->string('business_card_path', 500)->nullable()->comment('명함 이미지 경로');
// 영업권 상태
$table->string('status', 20)->default('active')->index()->comment('active, expired, converted');
$table->timestamp('registered_at')->useCurrent()->comment('등록일');
$table->timestamp('expires_at')->comment('만료일 (등록일 + 2개월)');
$table->timestamp('cooldown_ends_at')->comment('쿨다운 종료일 (만료일 + 1개월)');
// 테넌트 전환 정보
$table->foreignId('tenant_id')
->nullable()
->constrained('tenants')
->nullOnDelete()
->comment('전환된 테넌트 ID');
$table->timestamp('converted_at')->nullable()->comment('테넌트 전환일');
$table->foreignId('converted_by')
->nullable()
->constrained('users')
->nullOnDelete()
->comment('전환 처리자 ID');
// 메모
$table->text('memo')->nullable()->comment('메모');
$table->timestamps();
$table->softDeletes();
// 복합 인덱스: 사업자번호 + 상태 (중복 체크용)
$table->index(['business_number', 'status']);
});
}
public function down(): void
{
Schema::dropIfExists('tenant_prospects');
}
};

View File

@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('sales_prospects', function (Blueprint $table) {
$table->string('business_card_image', 500)->nullable()->after('address')
->comment('명함 이미지 파일 경로');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('sales_prospects', function (Blueprint $table) {
$table->dropColumn('business_card_image');
});
}
};

View File

@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('sales_prospects', function (Blueprint $table) {
$table->string('id_card_image', 500)->nullable()->after('business_card_image')
->comment('신분증 사본 이미지 파일 경로');
$table->string('bankbook_image', 500)->nullable()->after('id_card_image')
->comment('통장 사본 이미지 파일 경로');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('sales_prospects', function (Blueprint $table) {
$table->dropColumn(['id_card_image', 'bankbook_image']);
});
}
};

View File

@@ -0,0 +1,23 @@
<?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::table('tenant_prospects', function (Blueprint $table) {
$table->string('id_card_path')->nullable()->after('business_card_path')->comment('신분증 사본 경로');
$table->string('bankbook_path')->nullable()->after('id_card_path')->comment('통장 사본 경로');
});
}
public function down(): void
{
Schema::table('tenant_prospects', function (Blueprint $table) {
$table->dropColumn(['id_card_path', 'bankbook_path']);
});
}
};

View File

@@ -0,0 +1,26 @@
<?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::table('credit_inquiries', function (Blueprint $table) {
$table->unsignedBigInteger('tenant_id')->nullable()->after('id');
$table->index('tenant_id');
$table->index(['tenant_id', 'inquired_at']);
});
}
public function down(): void
{
Schema::table('credit_inquiries', function (Blueprint $table) {
$table->dropIndex(['tenant_id', 'inquired_at']);
$table->dropIndex(['tenant_id']);
$table->dropColumn('tenant_id');
});
}
};

View File

@@ -0,0 +1,48 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('sales_scenario_checklists', function (Blueprint $table) {
// 기존 유니크 키 삭제
$table->dropUnique('sales_scenario_unique');
// checkpoint_index를 nullable로 변경
$table->unsignedTinyInteger('checkpoint_index')->nullable()->change();
// 새 유니크 키 생성 (checkpoint_id 기반)
$table->unique(
['tenant_id', 'scenario_type', 'step_id', 'checkpoint_id'],
'sales_scenario_checkpoint_unique'
);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('sales_scenario_checklists', function (Blueprint $table) {
// 새 유니크 키 삭제
$table->dropUnique('sales_scenario_checkpoint_unique');
// checkpoint_index를 NOT NULL로 복원
$table->unsignedTinyInteger('checkpoint_index')->nullable(false)->change();
// 기존 유니크 키 복원
$table->unique(
['tenant_id', 'user_id', 'step_id', 'checkpoint_index'],
'sales_scenario_unique'
);
});
}
};

View File

@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('sales_consultations', function (Blueprint $table) {
$table->string('gcs_uri', 500)->nullable()->after('duration')
->comment('Google Cloud Storage URI (본사 연구용 백업)');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('sales_consultations', function (Blueprint $table) {
$table->dropColumn('gcs_uri');
});
}
};

View File

@@ -0,0 +1,57 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
* 영업 파트너(영업 담당자) 테이블
*/
public function up(): void
{
Schema::create('sales_partners', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id')->comment('연결된 사용자 ID');
$table->string('partner_code', 20)->unique()->comment('파트너 고유 코드');
$table->enum('partner_type', ['individual', 'corporate'])->default('individual')->comment('파트너 유형');
// 수수료 정보
$table->decimal('commission_rate', 5, 2)->default(20.00)->comment('기본 수수료율 (%)');
$table->decimal('manager_commission_rate', 5, 2)->default(5.00)->comment('관리자 수수료율 (%)');
// 계좌 정보
$table->string('bank_name', 50)->nullable()->comment('은행명');
$table->string('account_number', 50)->nullable()->comment('계좌번호');
$table->string('account_holder', 50)->nullable()->comment('예금주');
// 상태 관리
$table->enum('status', ['pending', 'active', 'inactive', 'suspended'])->default('pending')->comment('상태');
$table->timestamp('approved_at')->nullable()->comment('승인 일시');
$table->unsignedBigInteger('approved_by')->nullable()->comment('승인자 ID');
// 실적 통계 (캐시용)
$table->unsignedInteger('total_contracts')->default(0)->comment('총 계약 건수');
$table->decimal('total_commission', 15, 2)->default(0)->comment('총 누적 수당');
$table->text('notes')->nullable()->comment('메모');
$table->timestamps();
$table->softDeletes();
// 인덱스
$table->index('user_id');
$table->index('status');
$table->index('partner_type');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('sales_partners');
}
};

View File

@@ -0,0 +1,75 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
* 테넌트별 영업 관리 테이블 (tenants 외래키 연결)
*/
public function up(): void
{
Schema::create('sales_tenant_managements', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id')->unique()->comment('테넌트 ID (1:1 관계)');
$table->unsignedBigInteger('sales_partner_id')->nullable()->comment('영업 담당자 ID');
$table->unsignedBigInteger('manager_user_id')->nullable()->comment('관리 매니저 사용자 ID');
// 시나리오 진행 상태
$table->unsignedTinyInteger('sales_scenario_step')->default(1)->comment('영업 시나리오 현재 단계 (1-6)');
$table->unsignedTinyInteger('manager_scenario_step')->default(1)->comment('매니저 시나리오 현재 단계 (1-6)');
// 영업 상태
$table->enum('status', [
'prospect', // 잠재 고객
'approach', // 접근 중
'negotiation', // 협상 중
'contracted', // 계약 완료
'onboarding', // 온보딩 중
'active', // 활성 고객
'churned', // 이탈
])->default('prospect')->comment('영업 상태');
// 계약 정보
$table->timestamp('first_contact_at')->nullable()->comment('최초 접촉일');
$table->timestamp('contracted_at')->nullable()->comment('계약 체결일');
$table->timestamp('onboarding_completed_at')->nullable()->comment('온보딩 완료일');
// 가입비 정보
$table->decimal('membership_fee', 12, 2)->nullable()->comment('가입비');
$table->timestamp('membership_paid_at')->nullable()->comment('가입비 입금일');
$table->enum('membership_status', ['pending', 'partial', 'paid', 'refunded'])->nullable()->comment('가입비 상태');
// 수당 정보
$table->decimal('sales_commission', 12, 2)->nullable()->comment('영업 수당');
$table->decimal('manager_commission', 12, 2)->nullable()->comment('관리 수당');
$table->timestamp('commission_paid_at')->nullable()->comment('수당 지급일');
$table->enum('commission_status', ['pending', 'approved', 'paid'])->nullable()->comment('수당 상태');
// 진행률 캐시
$table->unsignedTinyInteger('sales_progress')->default(0)->comment('영업 시나리오 진행률 (%)');
$table->unsignedTinyInteger('manager_progress')->default(0)->comment('매니저 시나리오 진행률 (%)');
$table->text('notes')->nullable()->comment('메모');
$table->timestamps();
$table->softDeletes();
// 인덱스
$table->index('sales_partner_id');
$table->index('manager_user_id');
$table->index('status');
$table->index('contracted_at');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('sales_tenant_managements');
}
};

View File

@@ -0,0 +1,45 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
* 영업 시나리오 체크리스트 진행 상태 테이블
*/
public function up(): void
{
Schema::create('sales_scenario_checklists', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
$table->enum('scenario_type', ['sales', 'manager'])->comment('시나리오 유형');
$table->unsignedTinyInteger('step_id')->comment('단계 ID (1-6)');
$table->string('checkpoint_id', 50)->comment('체크포인트 ID');
$table->boolean('is_checked')->default(false)->comment('체크 여부');
$table->timestamp('checked_at')->nullable()->comment('체크 일시');
$table->unsignedBigInteger('checked_by')->nullable()->comment('체크한 사용자 ID');
$table->text('memo')->nullable()->comment('메모');
$table->timestamps();
// 복합 유니크 키 (테넌트 + 시나리오타입 + 단계 + 체크포인트)
$table->unique(['tenant_id', 'scenario_type', 'step_id', 'checkpoint_id'], 'unique_checklist_item');
// 인덱스
$table->index(['tenant_id', 'scenario_type']);
$table->index('checked_by');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('sales_scenario_checklists');
}
};

View File

@@ -0,0 +1,56 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
* 영업 상담 기록 테이블 (텍스트, 음성, 첨부파일)
*/
public function up(): void
{
Schema::create('sales_consultations', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
$table->enum('scenario_type', ['sales', 'manager'])->comment('시나리오 유형');
$table->unsignedTinyInteger('step_id')->nullable()->comment('관련 단계 ID (1-6)');
// 상담 유형 및 내용
$table->enum('consultation_type', ['text', 'audio', 'file'])->comment('상담 유형');
$table->text('content')->nullable()->comment('텍스트 내용');
// 파일 정보 (음성/첨부파일)
$table->string('file_path', 500)->nullable()->comment('파일 경로');
$table->string('file_name', 255)->nullable()->comment('원본 파일명');
$table->unsignedInteger('file_size')->nullable()->comment('파일 크기 (bytes)');
$table->string('file_type', 100)->nullable()->comment('MIME 타입');
// 음성 관련
$table->text('transcript')->nullable()->comment('음성 변환 텍스트 (STT)');
$table->unsignedInteger('duration')->nullable()->comment('녹음 길이 (초)');
// 작성자
$table->unsignedBigInteger('created_by')->comment('작성자 ID');
$table->timestamps();
$table->softDeletes();
// 인덱스
$table->index(['tenant_id', 'scenario_type']);
$table->index('consultation_type');
$table->index('step_id');
$table->index('created_by');
$table->index('created_at');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('sales_consultations');
}
};

View File

@@ -0,0 +1,66 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
* sales_scenario_checklists 테이블에 필요한 컬럼 추가
*/
public function up(): void
{
Schema::table('sales_scenario_checklists', function (Blueprint $table) {
// scenario_type 컬럼 추가 (sales/manager 구분)
if (!Schema::hasColumn('sales_scenario_checklists', 'scenario_type')) {
$table->enum('scenario_type', ['sales', 'manager'])->default('sales')->after('tenant_id')->comment('시나리오 유형');
}
// checkpoint_id 컬럼 추가 (문자열 ID)
if (!Schema::hasColumn('sales_scenario_checklists', 'checkpoint_id')) {
$table->string('checkpoint_id', 50)->nullable()->after('step_id')->comment('체크포인트 ID');
}
// checked_at 컬럼 추가
if (!Schema::hasColumn('sales_scenario_checklists', 'checked_at')) {
$table->timestamp('checked_at')->nullable()->after('is_checked')->comment('체크 일시');
}
// checked_by 컬럼 추가 (user_id를 대체)
if (!Schema::hasColumn('sales_scenario_checklists', 'checked_by')) {
$table->unsignedBigInteger('checked_by')->nullable()->after('checked_at')->comment('체크한 사용자 ID');
}
// memo 컬럼 추가
if (!Schema::hasColumn('sales_scenario_checklists', 'memo')) {
$table->text('memo')->nullable()->after('checked_by')->comment('메모');
}
});
// 인덱스 추가 (존재 여부 확인)
$indexExists = DB::select("SHOW INDEX FROM sales_scenario_checklists WHERE Key_name = 'sales_scenario_checklists_tenant_id_scenario_type_index'");
if (empty($indexExists)) {
Schema::table('sales_scenario_checklists', function (Blueprint $table) {
$table->index(['tenant_id', 'scenario_type']);
});
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('sales_scenario_checklists', function (Blueprint $table) {
$columns = ['scenario_type', 'checkpoint_id', 'checked_at', 'checked_by', 'memo'];
foreach ($columns as $column) {
if (Schema::hasColumn('sales_scenario_checklists', $column)) {
$table->dropColumn($column);
}
}
});
}
};

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('sales_tenant_managements', function (Blueprint $table) {
// 본사 진행 상태
$table->string('hq_status', 20)->default('pending')->after('manager_progress')
->comment('본사 진행 상태: pending, review, planning, coding, dev_test, dev_done, int_test, handover');
// 수당 지급 상태
$table->string('incentive_status', 20)->default('pending')->after('hq_status')
->comment('수당 지급 상태: pending, eligible, paid');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('sales_tenant_managements', function (Blueprint $table) {
$table->dropColumn(['hq_status', 'incentive_status']);
});
}
};

View File

@@ -0,0 +1,74 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// 상품 카테고리 테이블
Schema::create('sales_product_categories', function (Blueprint $table) {
$table->id();
$table->string('code', 50)->unique()->comment('카테고리 코드 (manufacturing, construction)');
$table->string('name', 100)->comment('카테고리명 (제조 업체, 공사 업체)');
$table->text('description')->nullable()->comment('설명');
$table->string('base_storage', 20)->default('100GB')->comment('기본 제공 용량');
$table->integer('display_order')->default(0)->comment('표시 순서');
$table->boolean('is_active')->default(true);
$table->timestamps();
$table->softDeletes();
});
// 상품 테이블
Schema::create('sales_products', function (Blueprint $table) {
$table->id();
$table->foreignId('category_id')->constrained('sales_product_categories')->comment('카테고리 FK');
$table->string('code', 50)->comment('상품 코드');
$table->string('name', 100)->comment('상품명');
$table->text('description')->nullable()->comment('프로그램 타입/설명');
$table->decimal('development_fee', 15, 2)->default(0)->comment('개발비 (가입비)');
$table->decimal('subscription_fee', 15, 2)->default(0)->comment('월 구독료');
$table->decimal('commission_rate', 5, 2)->default(25.00)->comment('수당 비율 (%)');
$table->boolean('allow_flexible_pricing')->default(true)->comment('재량권 허용');
$table->boolean('is_required')->default(false)->comment('필수 선택 여부');
$table->integer('display_order')->default(0)->comment('표시 순서');
$table->boolean('is_active')->default(true);
$table->timestamps();
$table->softDeletes();
$table->unique(['category_id', 'code'], 'uk_category_code');
});
// 계약별 선택 상품 테이블
Schema::create('sales_contract_products', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained('tenants')->comment('테넌트 FK');
$table->foreignId('management_id')->constrained('sales_tenant_managements')->comment('영업관리 FK');
$table->foreignId('category_id')->constrained('sales_product_categories')->comment('선택한 카테고리');
$table->foreignId('product_id')->constrained('sales_products')->comment('선택한 상품');
$table->decimal('development_fee', 15, 2)->nullable()->comment('적용된 개발비 (협상 가능)');
$table->decimal('subscription_fee', 15, 2)->nullable()->comment('적용된 구독료');
$table->decimal('discount_rate', 5, 2)->default(0)->comment('할인율 (%)');
$table->text('notes')->nullable()->comment('비고');
$table->foreignId('created_by')->nullable()->constrained('users')->comment('등록자');
$table->timestamps();
$table->index(['tenant_id', 'category_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('sales_contract_products');
Schema::dropIfExists('sales_products');
Schema::dropIfExists('sales_product_categories');
}
};

View File

@@ -0,0 +1,47 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('sales_products', function (Blueprint $table) {
$table->decimal('partner_commission_rate', 5, 2)->default(20.00)->after('subscription_fee')->comment('영업파트너 수당율(%)');
$table->decimal('manager_commission_rate', 5, 2)->default(5.00)->after('partner_commission_rate')->comment('매니저 수당율(%)');
});
// 기존 데이터 업데이트: commission_rate 25% → partner 20%, manager 5%
DB::table('sales_products')->update([
'partner_commission_rate' => 20.00,
'manager_commission_rate' => 5.00,
]);
Schema::table('sales_products', function (Blueprint $table) {
$table->dropColumn('commission_rate');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('sales_products', function (Blueprint $table) {
$table->decimal('commission_rate', 5, 2)->default(25.00)->after('subscription_fee')->comment('수당율(%)');
});
// 롤백 시 partner + manager 합산으로 복원
DB::statement('UPDATE sales_products SET commission_rate = partner_commission_rate + manager_commission_rate');
Schema::table('sales_products', function (Blueprint $table) {
$table->dropColumn(['partner_commission_rate', 'manager_commission_rate']);
});
}
};

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('sales_products', function (Blueprint $table) {
$table->decimal('registration_fee', 15, 2)->default(0)->after('development_fee')->comment('가입비 (할인된 가격)');
});
// 기존 데이터: 가입비 = 개발비 × 25%
DB::statement('UPDATE sales_products SET registration_fee = development_fee * 0.25');
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('sales_products', function (Blueprint $table) {
$table->dropColumn('registration_fee');
});
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('sales_contract_products', function (Blueprint $table) {
$table->renameColumn('development_fee', 'registration_fee');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('sales_contract_products', function (Blueprint $table) {
$table->renameColumn('registration_fee', 'development_fee');
});
}
};

View File

@@ -0,0 +1,72 @@
<?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('sales_commissions', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
$table->unsignedBigInteger('management_id')->comment('영업 관리 ID (sales_tenant_managements)');
// 입금 정보
$table->enum('payment_type', ['deposit', 'balance'])->comment('입금 구분 (deposit:계약금, balance:잔금)');
$table->decimal('payment_amount', 14, 2)->comment('입금액');
$table->date('payment_date')->comment('입금일');
// 수당 계산 기준
$table->decimal('base_amount', 14, 2)->comment('수당 계산 기준액 (가입비의 50%)');
$table->decimal('partner_rate', 5, 2)->default(20.00)->comment('영업파트너 수당률 (%)');
$table->decimal('manager_rate', 5, 2)->default(5.00)->comment('매니저 수당률 (%)');
$table->decimal('partner_commission', 14, 2)->comment('영업파트너 수당액');
$table->decimal('manager_commission', 14, 2)->comment('매니저 수당액');
// 지급 정보
$table->date('scheduled_payment_date')->comment('지급예정일 (익월 10일)');
$table->enum('status', ['pending', 'approved', 'paid', 'cancelled'])
->default('pending')
->comment('상태 (pending:대기, approved:승인, paid:지급완료, cancelled:취소)');
$table->date('actual_payment_date')->nullable()->comment('실제 지급일');
// 대상자 정보
$table->unsignedBigInteger('partner_id')->comment('영업파트너 ID (sales_partners)');
$table->unsignedBigInteger('manager_user_id')->nullable()->comment('매니저 사용자 ID');
// 부가 정보
$table->text('notes')->nullable()->comment('메모');
$table->string('bank_reference', 100)->nullable()->comment('이체 참조번호');
// 승인 정보
$table->unsignedBigInteger('approved_by')->nullable()->comment('승인자 ID');
$table->timestamp('approved_at')->nullable()->comment('승인일시');
$table->timestamps();
$table->softDeletes();
// 인덱스
$table->index('tenant_id');
$table->index('management_id');
$table->index('partner_id');
$table->index('manager_user_id');
$table->index('payment_type');
$table->index('payment_date');
$table->index('scheduled_payment_date');
$table->index('status');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('sales_commissions');
}
};

View File

@@ -0,0 +1,47 @@
<?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('sales_commission_details', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('commission_id')->comment('수수료 정산 ID');
$table->unsignedBigInteger('contract_product_id')->comment('계약 상품 ID (sales_contract_products)');
// 상품별 수당 계산
$table->decimal('registration_fee', 14, 2)->comment('상품 가입비');
$table->decimal('base_amount', 14, 2)->comment('수당 계산 기준액 (가입비의 50%)');
$table->decimal('partner_rate', 5, 2)->comment('영업파트너 수당률 (%)');
$table->decimal('manager_rate', 5, 2)->comment('매니저 수당률 (%)');
$table->decimal('partner_commission', 14, 2)->comment('영업파트너 수당액');
$table->decimal('manager_commission', 14, 2)->comment('매니저 수당액');
$table->timestamps();
// 인덱스 및 외래키
$table->index('commission_id');
$table->index('contract_product_id');
$table->foreign('commission_id')
->references('id')
->on('sales_commissions')
->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('sales_commission_details');
}
};

View File

@@ -0,0 +1,53 @@
<?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::table('sales_tenant_managements', function (Blueprint $table) {
// 계약금 정보
$table->decimal('deposit_amount', 14, 2)->nullable()->after('membership_status')->comment('계약금');
$table->date('deposit_paid_date')->nullable()->after('deposit_amount')->comment('계약금 입금일');
$table->enum('deposit_status', ['pending', 'paid'])
->default('pending')
->after('deposit_paid_date')
->comment('계약금 상태 (pending:대기, paid:입금완료)');
// 잔금 정보
$table->decimal('balance_amount', 14, 2)->nullable()->after('deposit_status')->comment('잔금');
$table->date('balance_paid_date')->nullable()->after('balance_amount')->comment('잔금 입금일');
$table->enum('balance_status', ['pending', 'paid'])
->default('pending')
->after('balance_paid_date')
->comment('잔금 상태 (pending:대기, paid:입금완료)');
// 총 가입비 (계약 상품 합계, 캐시용)
$table->decimal('total_registration_fee', 14, 2)->nullable()->after('balance_status')->comment('총 가입비');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('sales_tenant_managements', function (Blueprint $table) {
$table->dropColumn([
'deposit_amount',
'deposit_paid_date',
'deposit_status',
'balance_amount',
'balance_paid_date',
'balance_status',
'total_registration_fee',
]);
});
}
};

View File

@@ -0,0 +1,217 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
/**
* 영업 상품 초기 데이터 시더
*
* 개발비: 원래 가격 (예: 80,000,000원)
* 가입비: 개발비의 25% (예: 20,000,000원) - 할인된 가격으로 표시
*/
class SalesProductSeeder extends Seeder
{
public function run(): void
{
DB::transaction(function () {
// 기존 데이터 삭제
DB::table('sales_contract_products')->delete();
DB::table('sales_products')->delete();
DB::table('sales_product_categories')->delete();
// 카테고리 생성
$categories = [
[
'code' => 'MANUFACTURER',
'name' => '제조 업체',
'description' => '제조업 전용 SAM 솔루션 상품군',
'base_storage' => '100GB',
'display_order' => 1,
],
[
'code' => 'CONSTRUCTION',
'name' => '공사 업체',
'description' => '공사/시공업 전용 SAM 솔루션 상품군',
'base_storage' => '100GB',
'display_order' => 2,
],
];
$categoryIds = [];
foreach ($categories as $category) {
$categoryIds[$category['code']] = DB::table('sales_product_categories')->insertGetId([
...$category,
'is_active' => true,
'created_at' => now(),
'updated_at' => now(),
]);
}
// 제조업체 상품 (8개)
$manufacturerProducts = [
[
'code' => 'MFG_BASIC',
'name' => '기본 / PC + 모바일 사용 겸용',
'description' => '일정관리+근태+재고+견적+발주+생산공정 1개+출고+회계+신용조회+대표자 화면 (보고서+전자결제 음성알림)',
'development_fee' => 80000000,
'subscription_fee' => 500000,
'partner_commission_rate' => 20,
'manager_commission_rate' => 5,
'allow_flexible_pricing' => true,
'is_required' => true,
'display_order' => 1,
],
[
'code' => 'MFG_QUALITY',
'name' => '품질관리',
'description' => '로트관리 + 사진등록기능 + 설비 관리(QR)',
'development_fee' => 80000000,
'subscription_fee' => 500000,
'partner_commission_rate' => 20,
'manager_commission_rate' => 5,
'allow_flexible_pricing' => true,
'is_required' => false,
'display_order' => 2,
],
[
'code' => 'MFG_PROCESS_ADD',
'name' => '생산 공정 1개 추가시',
'description' => '별도의 작업지시서',
'development_fee' => 20000000,
'subscription_fee' => 100000,
'partner_commission_rate' => 20,
'manager_commission_rate' => 5,
'allow_flexible_pricing' => true,
'is_required' => false,
'display_order' => 3,
],
[
'code' => 'MFG_AI',
'name' => 'SAM 봇 / AI기능',
'description' => '음성녹음-->텍스트 변환 (회의록+업무일지) + 프로그램 내 검색 기능',
'development_fee' => 20000000,
'subscription_fee' => 100000,
'partner_commission_rate' => 20,
'manager_commission_rate' => 5,
'allow_flexible_pricing' => true,
'is_required' => false,
'display_order' => 4,
],
[
'code' => 'MFG_PHOTO',
'name' => '사진등록기능',
'description' => '현장 또는 원하는 포인트에 촬영 --> 프로그램에 바로 등록 (맞춤형)',
'development_fee' => 10000000,
'subscription_fee' => 100000,
'partner_commission_rate' => 20,
'manager_commission_rate' => 5,
'allow_flexible_pricing' => true,
'is_required' => false,
'display_order' => 5,
],
[
'code' => 'MFG_INVOICE_CARD',
'name' => '계산서 + 카드 관리',
'description' => '계산서 발행 (월 100건 기준 / 초과시 추가 5만원) + 법인카드 (접대비+복리후생비+가지급금 관리)',
'development_fee' => 10000000,
'subscription_fee' => 100000,
'partner_commission_rate' => 20,
'manager_commission_rate' => 5,
'allow_flexible_pricing' => true,
'is_required' => false,
'display_order' => 6,
],
[
'code' => 'MFG_EQUIPMENT',
'name' => '설비 관리 (QR)',
'description' => '관리 프로그램',
'development_fee' => 10000000,
'subscription_fee' => 50000,
'partner_commission_rate' => 20,
'manager_commission_rate' => 5,
'allow_flexible_pricing' => true,
'is_required' => false,
'display_order' => 7,
],
[
'code' => 'MFG_RND',
'name' => '기업부설연구소',
'description' => '관리 프로그램',
'development_fee' => 10000000,
'subscription_fee' => 50000,
'partner_commission_rate' => 20,
'manager_commission_rate' => 5,
'allow_flexible_pricing' => true,
'is_required' => false,
'display_order' => 8,
],
];
foreach ($manufacturerProducts as $product) {
DB::table('sales_products')->insert([
'category_id' => $categoryIds['MANUFACTURER'],
...$product,
'registration_fee' => $product['development_fee'] * 0.25,
'is_active' => true,
'created_at' => now(),
'updated_at' => now(),
]);
}
// 공사업체 상품 (3개)
$constructionProducts = [
[
'code' => 'CON_BASIC',
'name' => '기본 / PC + 모바일 사용 겸용',
'description' => '일정관리+근태+견적+발주+공사관리+기성+회계+신용조회+대표자 화면 (보고서+전자결제 음성알림)',
'development_fee' => 80000000,
'subscription_fee' => 500000,
'partner_commission_rate' => 20,
'manager_commission_rate' => 5,
'allow_flexible_pricing' => true,
'is_required' => true,
'display_order' => 1,
],
[
'code' => 'CON_AI',
'name' => 'SAM 봇 / AI기능',
'description' => '음성녹음-->텍스트 변환 (회의록+업무일지) + 프로그램 내 검색 기능',
'development_fee' => 20000000,
'subscription_fee' => 100000,
'partner_commission_rate' => 20,
'manager_commission_rate' => 5,
'allow_flexible_pricing' => true,
'is_required' => false,
'display_order' => 2,
],
[
'code' => 'CON_PHOTO',
'name' => '사진등록기능',
'description' => '현장 또는 원하는 포인트에 촬영 --> 프로그램에 바로 등록 (맞춤형)',
'development_fee' => 10000000,
'subscription_fee' => 100000,
'partner_commission_rate' => 20,
'manager_commission_rate' => 5,
'allow_flexible_pricing' => true,
'is_required' => false,
'display_order' => 3,
],
];
foreach ($constructionProducts as $product) {
DB::table('sales_products')->insert([
'category_id' => $categoryIds['CONSTRUCTION'],
...$product,
'registration_fee' => $product['development_fee'] * 0.25,
'is_active' => true,
'created_at' => now(),
'updated_at' => now(),
]);
}
});
$this->command->info('✅ 영업 상품 초기 데이터 생성 완료 (2개 카테고리, 11개 상품)');
}
}

9
fix_git_sync.sh Normal file
View File

@@ -0,0 +1,9 @@
#!/bin/bash
echo "Stopping file mode tracking..."
git config core.filemode false
echo "Resetting unintentional changes..."
git checkout .
echo "Pulling remote changes with rebase..."
git pull --rebase origin develop
echo "Pushing synchronized changes..."
git push