diff --git a/database/migrations/2026_01_27_100000_create_ai_configs_table.php b/database/migrations/2026_01_27_100000_create_ai_configs_table.php new file mode 100644 index 0000000..e4ddce6 --- /dev/null +++ b/database/migrations/2026_01_27_100000_create_ai_configs_table.php @@ -0,0 +1,39 @@ +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'); + } +}; diff --git a/database/migrations/2026_01_27_150000_create_barobill_subscriptions_table.php b/database/migrations/2026_01_27_150000_create_barobill_subscriptions_table.php new file mode 100644 index 0000000..ebcd126 --- /dev/null +++ b/database/migrations/2026_01_27_150000_create_barobill_subscriptions_table.php @@ -0,0 +1,46 @@ +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'); + } +}; diff --git a/database/migrations/2026_01_27_150100_create_barobill_billing_records_table.php b/database/migrations/2026_01_27_150100_create_barobill_billing_records_table.php new file mode 100644 index 0000000..1023eec --- /dev/null +++ b/database/migrations/2026_01_27_150100_create_barobill_billing_records_table.php @@ -0,0 +1,52 @@ +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'); + } +}; diff --git a/database/migrations/2026_01_27_150200_create_barobill_monthly_summaries_table.php b/database/migrations/2026_01_27_150200_create_barobill_monthly_summaries_table.php new file mode 100644 index 0000000..00b2b63 --- /dev/null +++ b/database/migrations/2026_01_27_150200_create_barobill_monthly_summaries_table.php @@ -0,0 +1,53 @@ +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'); + } +}; diff --git a/database/migrations/2026_01_27_160000_create_barobill_pricing_policies_table.php b/database/migrations/2026_01_27_160000_create_barobill_pricing_policies_table.php new file mode 100644 index 0000000..e744058 --- /dev/null +++ b/database/migrations/2026_01_27_160000_create_barobill_pricing_policies_table.php @@ -0,0 +1,46 @@ +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'); + } +}; diff --git a/database/migrations/2026_01_27_200000_add_sales_manager_fields_to_users_table.php b/database/migrations/2026_01_27_200000_add_sales_manager_fields_to_users_table.php new file mode 100644 index 0000000..7a92744 --- /dev/null +++ b/database/migrations/2026_01_27_200000_add_sales_manager_fields_to_users_table.php @@ -0,0 +1,52 @@ +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', + ]); + }); + } +}; diff --git a/database/migrations/2026_01_27_200100_create_sales_manager_documents_table.php b/database/migrations/2026_01_27_200100_create_sales_manager_documents_table.php new file mode 100644 index 0000000..38426ed --- /dev/null +++ b/database/migrations/2026_01_27_200100_create_sales_manager_documents_table.php @@ -0,0 +1,53 @@ +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'); + } +}; diff --git a/database/migrations/2026_01_27_221000_create_tenant_prospects_table.php b/database/migrations/2026_01_27_221000_create_tenant_prospects_table.php new file mode 100644 index 0000000..3b7a7de --- /dev/null +++ b/database/migrations/2026_01_27_221000_create_tenant_prospects_table.php @@ -0,0 +1,75 @@ +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'); + } +}; diff --git a/database/migrations/2026_01_27_234500_add_business_card_image_to_sales_prospects_table.php b/database/migrations/2026_01_27_234500_add_business_card_image_to_sales_prospects_table.php new file mode 100644 index 0000000..2007daf --- /dev/null +++ b/database/migrations/2026_01_27_234500_add_business_card_image_to_sales_prospects_table.php @@ -0,0 +1,29 @@ +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'); + }); + } +}; diff --git a/database/migrations/2026_01_27_235000_add_id_card_and_bankbook_image_to_sales_prospects_table.php b/database/migrations/2026_01_27_235000_add_id_card_and_bankbook_image_to_sales_prospects_table.php new file mode 100644 index 0000000..87981ce --- /dev/null +++ b/database/migrations/2026_01_27_235000_add_id_card_and_bankbook_image_to_sales_prospects_table.php @@ -0,0 +1,31 @@ +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']); + }); + } +}; diff --git a/database/migrations/2026_01_28_090000_add_attachments_to_tenant_prospects_table.php b/database/migrations/2026_01_28_090000_add_attachments_to_tenant_prospects_table.php new file mode 100644 index 0000000..be7f9ec --- /dev/null +++ b/database/migrations/2026_01_28_090000_add_attachments_to_tenant_prospects_table.php @@ -0,0 +1,23 @@ +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']); + }); + } +}; diff --git a/database/migrations/2026_01_28_163000_add_tenant_id_to_credit_inquiries_table.php b/database/migrations/2026_01_28_163000_add_tenant_id_to_credit_inquiries_table.php new file mode 100644 index 0000000..52ec02f --- /dev/null +++ b/database/migrations/2026_01_28_163000_add_tenant_id_to_credit_inquiries_table.php @@ -0,0 +1,26 @@ +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'); + }); + } +}; diff --git a/database/migrations/2026_01_29_090000_fix_sales_scenario_checklists_unique_key.php b/database/migrations/2026_01_29_090000_fix_sales_scenario_checklists_unique_key.php new file mode 100644 index 0000000..fee45fd --- /dev/null +++ b/database/migrations/2026_01_29_090000_fix_sales_scenario_checklists_unique_key.php @@ -0,0 +1,48 @@ +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' + ); + }); + } +}; diff --git a/database/migrations/2026_01_29_093000_add_gcs_uri_to_sales_consultations.php b/database/migrations/2026_01_29_093000_add_gcs_uri_to_sales_consultations.php new file mode 100644 index 0000000..8ce6499 --- /dev/null +++ b/database/migrations/2026_01_29_093000_add_gcs_uri_to_sales_consultations.php @@ -0,0 +1,29 @@ +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'); + }); + } +}; diff --git a/database/migrations/2026_01_29_100000_create_sales_partners_table.php b/database/migrations/2026_01_29_100000_create_sales_partners_table.php new file mode 100644 index 0000000..6cd3018 --- /dev/null +++ b/database/migrations/2026_01_29_100000_create_sales_partners_table.php @@ -0,0 +1,57 @@ +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'); + } +}; diff --git a/database/migrations/2026_01_29_100100_create_sales_tenant_managements_table.php b/database/migrations/2026_01_29_100100_create_sales_tenant_managements_table.php new file mode 100644 index 0000000..320d387 --- /dev/null +++ b/database/migrations/2026_01_29_100100_create_sales_tenant_managements_table.php @@ -0,0 +1,75 @@ +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'); + } +}; diff --git a/database/migrations/2026_01_29_100200_create_sales_scenario_checklists_table.php b/database/migrations/2026_01_29_100200_create_sales_scenario_checklists_table.php new file mode 100644 index 0000000..61d215e --- /dev/null +++ b/database/migrations/2026_01_29_100200_create_sales_scenario_checklists_table.php @@ -0,0 +1,45 @@ +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'); + } +}; diff --git a/database/migrations/2026_01_29_100300_create_sales_consultations_table.php b/database/migrations/2026_01_29_100300_create_sales_consultations_table.php new file mode 100644 index 0000000..360f3fe --- /dev/null +++ b/database/migrations/2026_01_29_100300_create_sales_consultations_table.php @@ -0,0 +1,56 @@ +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'); + } +}; diff --git a/database/migrations/2026_01_29_100400_add_columns_to_sales_scenario_checklists_table.php b/database/migrations/2026_01_29_100400_add_columns_to_sales_scenario_checklists_table.php new file mode 100644 index 0000000..5df33e1 --- /dev/null +++ b/database/migrations/2026_01_29_100400_add_columns_to_sales_scenario_checklists_table.php @@ -0,0 +1,66 @@ +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); + } + } + }); + } +}; diff --git a/database/migrations/2026_01_29_120000_add_hq_status_to_sales_tenant_managements.php b/database/migrations/2026_01_29_120000_add_hq_status_to_sales_tenant_managements.php new file mode 100644 index 0000000..cb76679 --- /dev/null +++ b/database/migrations/2026_01_29_120000_add_hq_status_to_sales_tenant_managements.php @@ -0,0 +1,34 @@ +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']); + }); + } +}; diff --git a/database/migrations/2026_01_29_150000_create_sales_products_tables.php b/database/migrations/2026_01_29_150000_create_sales_products_tables.php new file mode 100644 index 0000000..43061bc --- /dev/null +++ b/database/migrations/2026_01_29_150000_create_sales_products_tables.php @@ -0,0 +1,74 @@ +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'); + } +}; diff --git a/database/migrations/2026_01_29_161626_add_partner_manager_commission_to_sales_products_table.php b/database/migrations/2026_01_29_161626_add_partner_manager_commission_to_sales_products_table.php new file mode 100644 index 0000000..2d4603c --- /dev/null +++ b/database/migrations/2026_01_29_161626_add_partner_manager_commission_to_sales_products_table.php @@ -0,0 +1,47 @@ +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']); + }); + } +}; diff --git a/database/migrations/2026_01_29_162847_add_registration_fee_to_sales_products_table.php b/database/migrations/2026_01_29_162847_add_registration_fee_to_sales_products_table.php new file mode 100644 index 0000000..4a6aa96 --- /dev/null +++ b/database/migrations/2026_01_29_162847_add_registration_fee_to_sales_products_table.php @@ -0,0 +1,32 @@ +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'); + }); + } +}; diff --git a/database/migrations/2026_01_29_163733_rename_development_fee_to_registration_fee_in_sales_contract_products.php b/database/migrations/2026_01_29_163733_rename_development_fee_to_registration_fee_in_sales_contract_products.php new file mode 100644 index 0000000..d6914a8 --- /dev/null +++ b/database/migrations/2026_01_29_163733_rename_development_fee_to_registration_fee_in_sales_contract_products.php @@ -0,0 +1,28 @@ +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'); + }); + } +}; diff --git a/database/migrations/2026_01_29_170000_create_sales_commissions_table.php b/database/migrations/2026_01_29_170000_create_sales_commissions_table.php new file mode 100644 index 0000000..1fad60a --- /dev/null +++ b/database/migrations/2026_01_29_170000_create_sales_commissions_table.php @@ -0,0 +1,72 @@ +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'); + } +}; diff --git a/database/migrations/2026_01_29_170100_create_sales_commission_details_table.php b/database/migrations/2026_01_29_170100_create_sales_commission_details_table.php new file mode 100644 index 0000000..9b60ba2 --- /dev/null +++ b/database/migrations/2026_01_29_170100_create_sales_commission_details_table.php @@ -0,0 +1,47 @@ +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'); + } +}; diff --git a/database/migrations/2026_01_29_170200_add_payment_columns_to_sales_tenant_managements.php b/database/migrations/2026_01_29_170200_add_payment_columns_to_sales_tenant_managements.php new file mode 100644 index 0000000..8a53699 --- /dev/null +++ b/database/migrations/2026_01_29_170200_add_payment_columns_to_sales_tenant_managements.php @@ -0,0 +1,53 @@ +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', + ]); + }); + } +}; diff --git a/database/seeders/SalesProductSeeder.php b/database/seeders/SalesProductSeeder.php new file mode 100644 index 0000000..3e77387 --- /dev/null +++ b/database/seeders/SalesProductSeeder.php @@ -0,0 +1,217 @@ +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개 상품)'); + } +} diff --git a/fix_git_sync.sh b/fix_git_sync.sh new file mode 100644 index 0000000..65883ed --- /dev/null +++ b/fix_git_sync.sh @@ -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