diff --git a/CLAUDE.md b/CLAUDE.md index 609b4e2c..07cc8080 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -84,6 +84,49 @@ ## 프로젝트 기술 스택 - **React 페이지**: React 18 + Babel (브라우저 트랜스파일링) - **Database**: MySQL 8 +--- + +## 데이터베이스 아키텍처 (필수 규칙) + +> **경고: MNG 프로젝트에서는 마이그레이션 파일을 생성하지 않습니다!** + +### 핵심 원칙 + +| 작업 | 올바른 위치 | MNG에서 | +|------|------------|---------| +| 마이그레이션 생성 | `/home/aweso/sam/api/database/migrations/` | ❌ 금지 | +| 마이그레이션 실행 | `docker exec sam-api-1 php artisan migrate` | ❌ 금지 | +| 테이블 생성/수정 | API 프로젝트에서만 | ❌ 금지 | + +### MNG database 폴더 상태 + +``` +/home/aweso/sam/mng/database/ +├── migrations/ ← 비어있음 (파일 생성 금지!) +├── seeders/ ← MNG 전용 시더만 허용 (예: MngMenuSeeder) +└── factories/ ← 사용 안 함 +``` + +### MNG에서 허용되는 것 + +- ✅ 컨트롤러, 뷰, 라우트 작성 +- ✅ 모델 작성 (API의 테이블 사용) +- ✅ MNG 전용 시더 (MngMenuSeeder 등) + +### MNG에서 금지되는 것 + +- ❌ `database/migrations/` 에 파일 생성 +- ❌ `docker exec sam-mng-1 php artisan migrate` 실행 +- ❌ 테이블 구조 변경 관련 작업 + +### 새 테이블이 필요할 때 + +1. API 프로젝트에서 마이그레이션 생성 +2. `docker exec sam-api-1 php artisan migrate` 실행 +3. MNG에서 해당 테이블의 모델만 작성 + +--- + ## HTMX 네비게이션 규칙 ### HX-Redirect가 필요한 페이지 diff --git a/app/Http/Controllers/Sales/SalesManagerController.php b/app/Http/Controllers/Sales/SalesManagerController.php new file mode 100644 index 00000000..5ee1c506 --- /dev/null +++ b/app/Http/Controllers/Sales/SalesManagerController.php @@ -0,0 +1,155 @@ +header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('sales.managers.index')); + } + + $query = SalesManager::query()->active(); + + // 검색 + if ($search = $request->get('search')) { + $query->where(function ($q) use ($search) { + $q->where('name', 'like', "%{$search}%") + ->orWhere('member_id', 'like', "%{$search}%") + ->orWhere('email', 'like', "%{$search}%") + ->orWhere('phone', 'like', "%{$search}%"); + }); + } + + // 역할 필터 + if ($role = $request->get('role')) { + $query->where('role', $role); + } + + $managers = $query->orderBy('name')->paginate(20); + + // 통계 + $stats = [ + 'total' => SalesManager::active()->count(), + 'operators' => SalesManager::active()->where('role', 'operator')->count(), + 'sales_admins' => SalesManager::active()->where('role', 'sales_admin')->count(), + 'managers' => SalesManager::active()->where('role', 'manager')->count(), + ]; + + return view('sales.managers.index', compact('managers', 'stats')); + } + + /** + * 등록 폼 + */ + public function create(): View + { + $parents = SalesManager::active() + ->whereIn('role', ['operator', 'sales_admin']) + ->orderBy('name') + ->get(); + + return view('sales.managers.create', compact('parents')); + } + + /** + * 등록 처리 + */ + public function store(Request $request) + { + $validated = $request->validate([ + 'member_id' => 'required|string|max:50|unique:sales_managers,member_id', + 'password' => 'required|string|min:4', + 'name' => 'required|string|max:100', + 'phone' => 'nullable|string|max:20', + 'email' => 'nullable|email|max:100', + 'parent_id' => 'nullable|exists:sales_managers,id', + 'role' => 'required|in:operator,sales_admin,manager', + 'remarks' => 'nullable|string', + ]); + + SalesManager::create($validated); + + return redirect()->route('sales.managers.index') + ->with('success', '담당자가 등록되었습니다.'); + } + + /** + * 상세 페이지 + */ + public function show(int $id): View + { + $manager = SalesManager::with(['parent', 'children', 'registeredProspects', 'records']) + ->findOrFail($id); + + return view('sales.managers.show', compact('manager')); + } + + /** + * 수정 폼 + */ + public function edit(int $id): View + { + $manager = SalesManager::findOrFail($id); + $parents = SalesManager::active() + ->whereIn('role', ['operator', 'sales_admin']) + ->where('id', '!=', $id) + ->orderBy('name') + ->get(); + + return view('sales.managers.edit', compact('manager', 'parents')); + } + + /** + * 수정 처리 + */ + public function update(Request $request, int $id) + { + $manager = SalesManager::findOrFail($id); + + $validated = $request->validate([ + 'name' => 'required|string|max:100', + 'phone' => 'nullable|string|max:20', + 'email' => 'nullable|email|max:100', + 'parent_id' => 'nullable|exists:sales_managers,id', + 'role' => 'required|in:operator,sales_admin,manager', + 'remarks' => 'nullable|string', + 'password' => 'nullable|string|min:4', + ]); + + // 비밀번호가 비어있으면 제외 + if (empty($validated['password'])) { + unset($validated['password']); + } + + $manager->update($validated); + + return redirect()->route('sales.managers.index') + ->with('success', '담당자 정보가 수정되었습니다.'); + } + + /** + * 삭제 처리 (비활성화) + */ + public function destroy(int $id) + { + $manager = SalesManager::findOrFail($id); + $manager->update(['is_active' => false]); + + return redirect()->route('sales.managers.index') + ->with('success', '담당자가 비활성화되었습니다.'); + } +} diff --git a/app/Http/Controllers/Sales/SalesProspectController.php b/app/Http/Controllers/Sales/SalesProspectController.php new file mode 100644 index 00000000..d602103c --- /dev/null +++ b/app/Http/Controllers/Sales/SalesProspectController.php @@ -0,0 +1,170 @@ +header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('sales.prospects.index')); + } + + $query = SalesProspect::with(['manager', 'salesManager', 'products']); + + // 검색 + if ($search = $request->get('search')) { + $query->where(function ($q) use ($search) { + $q->where('company_name', 'like', "%{$search}%") + ->orWhere('business_no', 'like', "%{$search}%") + ->orWhere('representative', 'like', "%{$search}%") + ->orWhere('contact_phone', 'like', "%{$search}%"); + }); + } + + // 상태 필터 + if ($status = $request->get('status')) { + $query->where('status', $status); + } + + // 담당자 필터 + if ($managerId = $request->get('manager_id')) { + $query->where(function ($q) use ($managerId) { + $q->where('manager_id', $managerId) + ->orWhere('sales_manager_id', $managerId); + }); + } + + $prospects = $query->orderByDesc('created_at')->paginate(20); + + // 통계 + $stats = [ + 'total' => SalesProspect::count(), + 'lead' => SalesProspect::where('status', 'lead')->count(), + 'prospect' => SalesProspect::where('status', 'prospect')->count(), + 'negotiation' => SalesProspect::where('status', 'negotiation')->count(), + 'contracted' => SalesProspect::where('status', 'contracted')->count(), + 'total_contract' => SalesProspectProduct::sum('contract_amount'), + 'total_commission' => SalesProspectProduct::sum('commission_amount'), + ]; + + $managers = SalesManager::active()->orderBy('name')->get(); + + return view('sales.prospects.index', compact('prospects', 'stats', 'managers')); + } + + /** + * 등록 폼 + */ + public function create(): View + { + $managers = SalesManager::active()->orderBy('name')->get(); + + return view('sales.prospects.create', compact('managers')); + } + + /** + * 등록 처리 + */ + public function store(Request $request) + { + $validated = $request->validate([ + 'manager_id' => 'required|exists:sales_managers,id', + 'sales_manager_id' => 'nullable|exists:sales_managers,id', + 'company_name' => 'required|string|max:200', + 'representative' => 'nullable|string|max:100', + 'business_no' => 'nullable|string|max:20', + 'contact_phone' => 'nullable|string|max:20', + 'email' => 'nullable|email|max:100', + 'address' => 'nullable|string|max:500', + 'status' => 'required|in:lead,prospect,negotiation,contracted,lost', + ]); + + $prospect = SalesProspect::create($validated); + + return redirect()->route('sales.prospects.show', $prospect->id) + ->with('success', '가망고객이 등록되었습니다.'); + } + + /** + * 상세 페이지 + */ + public function show(int $id): View + { + $prospect = SalesProspect::with([ + 'manager', + 'salesManager', + 'products', + 'scenarios', + 'consultations.manager' + ])->findOrFail($id); + + $managers = SalesManager::active() + ->whereIn('role', ['sales_admin', 'manager']) + ->orderBy('name') + ->get(); + + return view('sales.prospects.show', compact('prospect', 'managers')); + } + + /** + * 수정 폼 + */ + public function edit(int $id): View + { + $prospect = SalesProspect::findOrFail($id); + $managers = SalesManager::active()->orderBy('name')->get(); + + return view('sales.prospects.edit', compact('prospect', 'managers')); + } + + /** + * 수정 처리 + */ + public function update(Request $request, int $id) + { + $prospect = SalesProspect::findOrFail($id); + + $validated = $request->validate([ + 'sales_manager_id' => 'nullable|exists:sales_managers,id', + 'company_name' => 'required|string|max:200', + 'representative' => 'nullable|string|max:100', + 'business_no' => 'nullable|string|max:20', + 'contact_phone' => 'nullable|string|max:20', + 'email' => 'nullable|email|max:100', + 'address' => 'nullable|string|max:500', + 'status' => 'required|in:lead,prospect,negotiation,contracted,lost', + ]); + + $prospect->update($validated); + + return redirect()->route('sales.prospects.show', $prospect->id) + ->with('success', '가망고객 정보가 수정되었습니다.'); + } + + /** + * 삭제 처리 + */ + public function destroy(int $id) + { + $prospect = SalesProspect::findOrFail($id); + $prospect->delete(); + + return redirect()->route('sales.prospects.index') + ->with('success', '가망고객이 삭제되었습니다.'); + } +} diff --git a/app/Http/Controllers/Sales/SalesRecordController.php b/app/Http/Controllers/Sales/SalesRecordController.php new file mode 100644 index 00000000..cc83d8e9 --- /dev/null +++ b/app/Http/Controllers/Sales/SalesRecordController.php @@ -0,0 +1,166 @@ +header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('sales.records.index')); + } + + $query = SalesRecord::with(['manager', 'prospect']); + + // 검색 + if ($search = $request->get('search')) { + $query->where(function ($q) use ($search) { + $q->where('description', 'like', "%{$search}%") + ->orWhere('record_type', 'like', "%{$search}%") + ->orWhereHas('prospect', function ($q2) use ($search) { + $q2->where('company_name', 'like', "%{$search}%"); + }); + }); + } + + // 상태 필터 + if ($status = $request->get('status')) { + $query->where('status', $status); + } + + // 담당자 필터 + if ($managerId = $request->get('manager_id')) { + $query->where('manager_id', $managerId); + } + + // 기간 필터 + if ($startDate = $request->get('start_date')) { + $query->where('record_date', '>=', $startDate); + } + if ($endDate = $request->get('end_date')) { + $query->where('record_date', '<=', $endDate); + } + + $records = $query->orderByDesc('record_date')->paginate(20); + + // 통계 + $stats = [ + 'total_count' => SalesRecord::count(), + 'pending_count' => SalesRecord::pending()->count(), + 'approved_count' => SalesRecord::approved()->count(), + 'total_amount' => SalesRecord::sum('amount'), + 'total_commission' => SalesRecord::sum('commission'), + 'pending_amount' => SalesRecord::pending()->sum('amount'), + 'approved_amount' => SalesRecord::approved()->sum('amount'), + ]; + + $managers = SalesManager::active()->orderBy('name')->get(); + + return view('sales.records.index', compact('records', 'stats', 'managers')); + } + + /** + * 등록 폼 + */ + public function create(): View + { + $managers = SalesManager::active()->orderBy('name')->get(); + $prospects = SalesProspect::orderBy('company_name')->get(); + + return view('sales.records.create', compact('managers', 'prospects')); + } + + /** + * 등록 처리 + */ + public function store(Request $request) + { + $validated = $request->validate([ + 'manager_id' => 'required|exists:sales_managers,id', + 'prospect_id' => 'nullable|exists:sales_prospects,id', + 'record_date' => 'required|date', + 'record_type' => 'required|string|max:50', + 'amount' => 'required|numeric|min:0', + 'commission' => 'nullable|numeric|min:0', + 'description' => 'nullable|string', + 'status' => 'required|in:pending,approved,rejected,paid', + ]); + + SalesRecord::create($validated); + + return redirect()->route('sales.records.index') + ->with('success', '실적이 등록되었습니다.'); + } + + /** + * 상세 페이지 + */ + public function show(int $id): View + { + $record = SalesRecord::with(['manager', 'prospect'])->findOrFail($id); + + return view('sales.records.show', compact('record')); + } + + /** + * 수정 폼 + */ + public function edit(int $id): View + { + $record = SalesRecord::findOrFail($id); + $managers = SalesManager::active()->orderBy('name')->get(); + $prospects = SalesProspect::orderBy('company_name')->get(); + + return view('sales.records.edit', compact('record', 'managers', 'prospects')); + } + + /** + * 수정 처리 + */ + public function update(Request $request, int $id) + { + $record = SalesRecord::findOrFail($id); + + $validated = $request->validate([ + 'manager_id' => 'required|exists:sales_managers,id', + 'prospect_id' => 'nullable|exists:sales_prospects,id', + 'record_date' => 'required|date', + 'record_type' => 'required|string|max:50', + 'amount' => 'required|numeric|min:0', + 'commission' => 'nullable|numeric|min:0', + 'description' => 'nullable|string', + 'status' => 'required|in:pending,approved,rejected,paid', + ]); + + $record->update($validated); + + return redirect()->route('sales.records.index') + ->with('success', '실적이 수정되었습니다.'); + } + + /** + * 삭제 처리 + */ + public function destroy(int $id) + { + $record = SalesRecord::findOrFail($id); + $record->delete(); + + return redirect()->route('sales.records.index') + ->with('success', '실적이 삭제되었습니다.'); + } +} diff --git a/app/Models/Sales/SalesManager.php b/app/Models/Sales/SalesManager.php new file mode 100644 index 00000000..4512b0e0 --- /dev/null +++ b/app/Models/Sales/SalesManager.php @@ -0,0 +1,120 @@ + 'boolean', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + ]; + + /** + * 상위 관리자 + */ + public function parent(): BelongsTo + { + return $this->belongsTo(SalesManager::class, 'parent_id'); + } + + /** + * 하위 관리자 목록 + */ + public function children(): HasMany + { + return $this->hasMany(SalesManager::class, 'parent_id'); + } + + /** + * 등록한 가망고객 + */ + public function registeredProspects(): HasMany + { + return $this->hasMany(SalesProspect::class, 'manager_id'); + } + + /** + * 담당 가망고객 + */ + public function assignedProspects(): HasMany + { + return $this->hasMany(SalesProspect::class, 'sales_manager_id'); + } + + /** + * 영업 실적 + */ + public function records(): HasMany + { + return $this->hasMany(SalesRecord::class, 'manager_id'); + } + + /** + * 상담 기록 + */ + public function consultations(): HasMany + { + return $this->hasMany(SalesProspectConsultation::class, 'manager_id'); + } + + /** + * 역할 라벨 + */ + public function getRoleLabelAttribute(): string + { + return match ($this->role) { + 'operator' => '운영자', + 'sales_admin' => '영업관리', + 'manager' => '매니저', + default => $this->role, + }; + } + + /** + * 역할별 색상 클래스 + */ + public function getRoleColorAttribute(): string + { + return match ($this->role) { + 'operator' => 'bg-purple-100 text-purple-800', + 'sales_admin' => 'bg-blue-100 text-blue-800', + 'manager' => 'bg-green-100 text-green-800', + default => 'bg-gray-100 text-gray-800', + }; + } + + /** + * 활성 관리자만 조회 + */ + public function scopeActive($query) + { + return $query->where('is_active', true); + } +} diff --git a/app/Models/Sales/SalesProspect.php b/app/Models/Sales/SalesProspect.php new file mode 100644 index 00000000..77b62036 --- /dev/null +++ b/app/Models/Sales/SalesProspect.php @@ -0,0 +1,139 @@ + 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + ]; + + /** + * 등록한 영업 담당자 + */ + public function manager(): BelongsTo + { + return $this->belongsTo(SalesManager::class, 'manager_id'); + } + + /** + * 담당 매니저 + */ + public function salesManager(): BelongsTo + { + return $this->belongsTo(SalesManager::class, 'sales_manager_id'); + } + + /** + * 계약 상품 목록 + */ + public function products(): HasMany + { + return $this->hasMany(SalesProspectProduct::class, 'prospect_id'); + } + + /** + * 시나리오 체크리스트 + */ + public function scenarios(): HasMany + { + return $this->hasMany(SalesProspectScenario::class, 'prospect_id'); + } + + /** + * 상담 기록 + */ + public function consultations(): HasMany + { + return $this->hasMany(SalesProspectConsultation::class, 'prospect_id'); + } + + /** + * 영업 실적 + */ + public function records(): HasMany + { + return $this->hasMany(SalesRecord::class, 'prospect_id'); + } + + /** + * 상태 라벨 + */ + public function getStatusLabelAttribute(): string + { + return match ($this->status) { + 'lead' => '리드', + 'prospect' => '가망', + 'negotiation' => '협상중', + 'contracted' => '계약완료', + 'lost' => '실패', + default => $this->status, + }; + } + + /** + * 상태별 색상 클래스 + */ + public function getStatusColorAttribute(): string + { + return match ($this->status) { + 'lead' => 'bg-gray-100 text-gray-800', + 'prospect' => 'bg-blue-100 text-blue-800', + 'negotiation' => 'bg-yellow-100 text-yellow-800', + 'contracted' => 'bg-green-100 text-green-800', + 'lost' => 'bg-red-100 text-red-800', + default => 'bg-gray-100 text-gray-800', + }; + } + + /** + * 사업자번호 포맷팅 (XXX-XX-XXXXX) + */ + public function getFormattedBusinessNoAttribute(): string + { + $bizNo = preg_replace('/[^0-9]/', '', $this->business_no); + if (strlen($bizNo) === 10) { + return substr($bizNo, 0, 3) . '-' . substr($bizNo, 3, 2) . '-' . substr($bizNo, 5); + } + return $this->business_no ?? ''; + } + + /** + * 총 계약금액 + */ + public function getTotalContractAmountAttribute(): float + { + return $this->products()->sum('contract_amount'); + } + + /** + * 총 수수료 + */ + public function getTotalCommissionAttribute(): float + { + return $this->products()->sum('commission_amount'); + } +} diff --git a/app/Models/Sales/SalesProspectConsultation.php b/app/Models/Sales/SalesProspectConsultation.php new file mode 100644 index 00000000..639cbde9 --- /dev/null +++ b/app/Models/Sales/SalesProspectConsultation.php @@ -0,0 +1,91 @@ + 'integer', + 'attachment_paths' => 'array', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + ]; + + /** + * 가망고객 + */ + public function prospect(): BelongsTo + { + return $this->belongsTo(SalesProspect::class, 'prospect_id'); + } + + /** + * 작성자 + */ + public function manager(): BelongsTo + { + return $this->belongsTo(SalesManager::class, 'manager_id'); + } + + /** + * 상담 유형 라벨 + */ + public function getConsultationTypeLabelAttribute(): string + { + return match ($this->consultation_type) { + 'text' => '텍스트', + 'audio' => '음성', + 'file' => '파일', + default => $this->consultation_type, + }; + } + + /** + * 상담 유형별 아이콘 클래스 + */ + public function getConsultationTypeIconAttribute(): string + { + return match ($this->consultation_type) { + 'text' => 'fa-comment', + 'audio' => 'fa-microphone', + 'file' => 'fa-paperclip', + default => 'fa-question', + }; + } + + /** + * 첨부파일 개수 + */ + public function getAttachmentCountAttribute(): int + { + return is_array($this->attachment_paths) ? count($this->attachment_paths) : 0; + } + + /** + * 음성 파일 존재 여부 + */ + public function getHasAudioAttribute(): bool + { + return !empty($this->audio_file_path); + } +} diff --git a/app/Models/Sales/SalesProspectProduct.php b/app/Models/Sales/SalesProspectProduct.php new file mode 100644 index 00000000..2743c182 --- /dev/null +++ b/app/Models/Sales/SalesProspectProduct.php @@ -0,0 +1,97 @@ + 'decimal:2', + 'subscription_fee' => 'decimal:2', + 'commission_rate' => 'decimal:2', + 'commission_amount' => 'decimal:2', + 'payout_rate' => 'decimal:2', + 'payout_amount' => 'decimal:2', + 'contract_date' => 'date', + 'join_approved' => 'boolean', + 'payment_approved' => 'boolean', + 'sub_models' => 'array', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + ]; + + /** + * 가망고객 + */ + public function prospect(): BelongsTo + { + return $this->belongsTo(SalesProspect::class, 'prospect_id'); + } + + /** + * 수수료 자동 계산 + */ + public function calculateCommission(): void + { + $this->commission_amount = $this->contract_amount * ($this->commission_rate / 100); + } + + /** + * 지급금액 계산 + */ + public function calculatePayout(): void + { + $this->payout_amount = $this->contract_amount * ($this->payout_rate / 100); + } + + /** + * 승인 상태 라벨 + */ + public function getApprovalStatusAttribute(): string + { + if ($this->payment_approved) { + return '결제승인'; + } + if ($this->join_approved) { + return '가입승인'; + } + return '대기중'; + } + + /** + * 승인 상태별 색상 + */ + public function getApprovalColorAttribute(): string + { + if ($this->payment_approved) { + return 'bg-green-100 text-green-800'; + } + if ($this->join_approved) { + return 'bg-blue-100 text-blue-800'; + } + return 'bg-gray-100 text-gray-800'; + } +} diff --git a/app/Models/Sales/SalesProspectScenario.php b/app/Models/Sales/SalesProspectScenario.php new file mode 100644 index 00000000..e0e7a2df --- /dev/null +++ b/app/Models/Sales/SalesProspectScenario.php @@ -0,0 +1,68 @@ + 'integer', + 'checkpoint_index' => 'integer', + 'is_checked' => 'boolean', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + /** + * 가망고객 + */ + public function prospect(): BelongsTo + { + return $this->belongsTo(SalesProspect::class, 'prospect_id'); + } + + /** + * 시나리오 유형 라벨 + */ + public function getScenarioTypeLabelAttribute(): string + { + return match ($this->scenario_type) { + 'sales' => '영업 시나리오', + 'manager' => '매니저 시나리오', + default => $this->scenario_type, + }; + } + + /** + * 특정 가망고객의 시나리오 진행률 계산 + */ + public static function getProgressRate(int $prospectId, string $scenarioType): float + { + $total = self::where('prospect_id', $prospectId) + ->where('scenario_type', $scenarioType) + ->count(); + + if ($total === 0) { + return 0; + } + + $checked = self::where('prospect_id', $prospectId) + ->where('scenario_type', $scenarioType) + ->where('is_checked', true) + ->count(); + + return round(($checked / $total) * 100, 1); + } +} diff --git a/app/Models/Sales/SalesRecord.php b/app/Models/Sales/SalesRecord.php new file mode 100644 index 00000000..f3a406c5 --- /dev/null +++ b/app/Models/Sales/SalesRecord.php @@ -0,0 +1,117 @@ + 'date', + 'amount' => 'decimal:2', + 'commission' => 'decimal:2', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + ]; + + /** + * 담당자 + */ + public function manager(): BelongsTo + { + return $this->belongsTo(SalesManager::class, 'manager_id'); + } + + /** + * 가망고객 + */ + public function prospect(): BelongsTo + { + return $this->belongsTo(SalesProspect::class, 'prospect_id'); + } + + /** + * 상태 라벨 + */ + public function getStatusLabelAttribute(): string + { + return match ($this->status) { + 'pending' => '대기', + 'approved' => '승인', + 'rejected' => '반려', + 'paid' => '지급완료', + default => $this->status, + }; + } + + /** + * 상태별 색상 클래스 + */ + public function getStatusColorAttribute(): string + { + return match ($this->status) { + 'pending' => 'bg-yellow-100 text-yellow-800', + 'approved' => 'bg-blue-100 text-blue-800', + 'rejected' => 'bg-red-100 text-red-800', + 'paid' => 'bg-green-100 text-green-800', + default => 'bg-gray-100 text-gray-800', + }; + } + + /** + * 특정 기간의 실적 합계 + */ + public static function getSummary(int $managerId, ?string $startDate = null, ?string $endDate = null): array + { + $query = self::where('manager_id', $managerId); + + if ($startDate) { + $query->where('record_date', '>=', $startDate); + } + if ($endDate) { + $query->where('record_date', '<=', $endDate); + } + + return [ + 'total_amount' => $query->sum('amount'), + 'total_commission' => $query->sum('commission'), + 'record_count' => $query->count(), + 'approved_amount' => (clone $query)->where('status', 'approved')->sum('amount'), + 'paid_amount' => (clone $query)->where('status', 'paid')->sum('amount'), + ]; + } + + /** + * 대기 상태만 조회 + */ + public function scopePending($query) + { + return $query->where('status', 'pending'); + } + + /** + * 승인 상태만 조회 + */ + public function scopeApproved($query) + { + return $query->where('status', 'approved'); + } +} diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php deleted file mode 100644 index 05fb5d9e..00000000 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ /dev/null @@ -1,49 +0,0 @@ -id(); - $table->string('name'); - $table->string('email')->unique(); - $table->timestamp('email_verified_at')->nullable(); - $table->string('password'); - $table->rememberToken(); - $table->timestamps(); - }); - - Schema::create('password_reset_tokens', function (Blueprint $table) { - $table->string('email')->primary(); - $table->string('token'); - $table->timestamp('created_at')->nullable(); - }); - - Schema::create('sessions', function (Blueprint $table) { - $table->string('id')->primary(); - $table->foreignId('user_id')->nullable()->index(); - $table->string('ip_address', 45)->nullable(); - $table->text('user_agent')->nullable(); - $table->longText('payload'); - $table->integer('last_activity')->index(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('users'); - Schema::dropIfExists('password_reset_tokens'); - Schema::dropIfExists('sessions'); - } -}; diff --git a/database/migrations/0001_01_01_000001_create_cache_table.php b/database/migrations/0001_01_01_000001_create_cache_table.php deleted file mode 100644 index b9c106be..00000000 --- a/database/migrations/0001_01_01_000001_create_cache_table.php +++ /dev/null @@ -1,35 +0,0 @@ -string('key')->primary(); - $table->mediumText('value'); - $table->integer('expiration'); - }); - - Schema::create('cache_locks', function (Blueprint $table) { - $table->string('key')->primary(); - $table->string('owner'); - $table->integer('expiration'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('cache'); - Schema::dropIfExists('cache_locks'); - } -}; diff --git a/database/migrations/0001_01_01_000002_create_jobs_table.php b/database/migrations/0001_01_01_000002_create_jobs_table.php deleted file mode 100644 index 425e7058..00000000 --- a/database/migrations/0001_01_01_000002_create_jobs_table.php +++ /dev/null @@ -1,57 +0,0 @@ -id(); - $table->string('queue')->index(); - $table->longText('payload'); - $table->unsignedTinyInteger('attempts'); - $table->unsignedInteger('reserved_at')->nullable(); - $table->unsignedInteger('available_at'); - $table->unsignedInteger('created_at'); - }); - - Schema::create('job_batches', function (Blueprint $table) { - $table->string('id')->primary(); - $table->string('name'); - $table->integer('total_jobs'); - $table->integer('pending_jobs'); - $table->integer('failed_jobs'); - $table->longText('failed_job_ids'); - $table->mediumText('options')->nullable(); - $table->integer('cancelled_at')->nullable(); - $table->integer('created_at'); - $table->integer('finished_at')->nullable(); - }); - - Schema::create('failed_jobs', function (Blueprint $table) { - $table->id(); - $table->string('uuid')->unique(); - $table->text('connection'); - $table->text('queue'); - $table->longText('payload'); - $table->longText('exception'); - $table->timestamp('failed_at')->useCurrent(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('jobs'); - Schema::dropIfExists('job_batches'); - Schema::dropIfExists('failed_jobs'); - } -}; diff --git a/database/migrations/2025_12_16_224112_create_sales_scenario_checklists_table.php b/database/migrations/2025_12_16_224112_create_sales_scenario_checklists_table.php deleted file mode 100644 index 76e71604..00000000 --- a/database/migrations/2025_12_16_224112_create_sales_scenario_checklists_table.php +++ /dev/null @@ -1,36 +0,0 @@ -id(); - $table->foreignId('tenant_id')->constrained()->cascadeOnDelete(); - $table->foreignId('user_id')->constrained()->cascadeOnDelete(); - $table->unsignedTinyInteger('step_id')->comment('단계 ID (1-6)'); - $table->unsignedTinyInteger('checkpoint_index')->comment('체크포인트 인덱스'); - $table->boolean('is_checked')->default(true); - $table->timestamps(); - - // 복합 유니크 키: 사용자별, 단계별, 체크포인트별 하나의 레코드 - $table->unique(['tenant_id', 'user_id', 'step_id', 'checkpoint_index'], 'sales_scenario_unique'); - $table->index(['tenant_id', 'user_id']); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('sales_scenario_checklists'); - } -}; diff --git a/database/migrations/2025_12_24_000001_add_simulator_fields_to_items_table.php b/database/migrations/2025_12_24_000001_add_simulator_fields_to_items_table.php deleted file mode 100644 index 8fd234b6..00000000 --- a/database/migrations/2025_12_24_000001_add_simulator_fields_to_items_table.php +++ /dev/null @@ -1,42 +0,0 @@ -string('process_type', 20)->nullable()->after('category_id') - ->comment('공정유형: screen, bending, electric, steel, assembly'); - - // 품목분류: 원단, 패널, 도장, 표면처리, 가이드레일, 케이스, 모터, 제어반 등 - $table->string('item_category', 50)->nullable()->after('process_type') - ->comment('품목분류: 원단, 패널, 도장, 가이드레일, 모터 등'); - - // 인덱스 추가 - $table->index('process_type', 'idx_items_process_type'); - $table->index('item_category', 'idx_items_item_category'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('items', function (Blueprint $table) { - $table->dropIndex('idx_items_process_type'); - $table->dropIndex('idx_items_item_category'); - $table->dropColumn(['process_type', 'item_category']); - }); - } -}; diff --git a/database/migrations/2025_12_24_000002_create_category_groups_table.php b/database/migrations/2025_12_24_000002_create_category_groups_table.php deleted file mode 100644 index 49394d9b..00000000 --- a/database/migrations/2025_12_24_000002_create_category_groups_table.php +++ /dev/null @@ -1,53 +0,0 @@ -id(); - $table->foreignId('tenant_id')->constrained()->cascadeOnDelete(); - - // 코드: area_based, weight_based, quantity_based - $table->string('code', 50)->comment('코드: area_based, weight_based, quantity_based'); - - // 이름: 면적기반, 중량기반, 수량기반 - $table->string('name', 100)->comment('이름: 면적기반, 중량기반, 수량기반'); - - // 곱셈 변수: M(면적), K(중량), null(수량) - $table->string('multiplier_variable', 20)->nullable() - ->comment('곱셈 변수: M(면적), K(중량), null(수량기반)'); - - // 소속 카테고리 목록 (JSON 배열) - $table->json('categories')->nullable() - ->comment('소속 카테고리 목록: ["원단", "패널", "도장"]'); - - $table->text('description')->nullable(); - $table->unsignedInteger('sort_order')->default(0); - $table->boolean('is_active')->default(true); - - $table->timestamps(); - - // 인덱스 - $table->index('tenant_id', 'idx_category_groups_tenant'); - $table->unique(['tenant_id', 'code'], 'uq_category_groups_tenant_code'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('category_groups'); - } -}; diff --git a/database/migrations/2026_01_22_084412_create_barobill_members_table.php b/database/migrations/2026_01_22_084412_create_barobill_members_table.php deleted file mode 100644 index d35a391a..00000000 --- a/database/migrations/2026_01_22_084412_create_barobill_members_table.php +++ /dev/null @@ -1,44 +0,0 @@ -id(); - $table->foreignId('tenant_id')->constrained()->cascadeOnDelete(); - $table->string('biz_no', 20)->comment('사업자번호'); - $table->string('corp_name', 100)->comment('상호명'); - $table->string('ceo_name', 50)->comment('대표자명'); - $table->string('addr', 255)->nullable()->comment('주소'); - $table->string('biz_type', 50)->nullable()->comment('업태'); - $table->string('biz_class', 50)->nullable()->comment('종목'); - $table->string('barobill_id', 50)->comment('바로빌 아이디'); - $table->string('barobill_pwd', 255)->comment('바로빌 비밀번호 (해시)'); - $table->string('manager_name', 50)->nullable()->comment('담당자명'); - $table->string('manager_email', 100)->nullable()->comment('담당자 이메일'); - $table->string('manager_hp', 20)->nullable()->comment('담당자 전화번호'); - $table->enum('status', ['active', 'inactive', 'pending'])->default('active')->comment('상태'); - $table->timestamps(); - $table->softDeletes(); - - $table->unique(['tenant_id', 'biz_no'], 'unique_tenant_biz_no'); - $table->index('status'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('barobill_members'); - } -}; diff --git a/database/migrations/2026_01_22_091500_create_barobill_configs_table.php b/database/migrations/2026_01_22_091500_create_barobill_configs_table.php deleted file mode 100644 index 7d0c0b5b..00000000 --- a/database/migrations/2026_01_22_091500_create_barobill_configs_table.php +++ /dev/null @@ -1,41 +0,0 @@ -id(); - $table->string('name', 50)->comment('설정 이름 (예: 테스트서버, 운영서버)'); - $table->enum('environment', ['test', 'production'])->comment('환경 (test/production)'); - $table->string('cert_key', 100)->comment('CERTKEY (인증키)'); - $table->string('corp_num', 20)->nullable()->comment('파트너 사업자번호'); - $table->string('base_url', 255)->comment('API 서버 URL'); - $table->text('description')->nullable()->comment('설명'); - $table->boolean('is_active')->default(false)->comment('활성화 여부 (환경당 1개만 활성화)'); - $table->timestamps(); - $table->softDeletes(); - - // 환경별로 하나만 활성화 가능하도록 부분 유니크 인덱스 (is_active=true인 것 중에서) - $table->index(['environment', 'is_active']); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('barobill_configs'); - } -}; diff --git a/database/migrations/2026_01_22_170000_add_service_options_to_barobill_settings.php b/database/migrations/2026_01_22_170000_add_service_options_to_barobill_settings.php deleted file mode 100644 index cc20120f..00000000 --- a/database/migrations/2026_01_22_170000_add_service_options_to_barobill_settings.php +++ /dev/null @@ -1,32 +0,0 @@ -boolean('use_tax_invoice')->default(false)->after('auto_issue')->comment('전자세금계산서 이용'); - $table->boolean('use_bank_account')->default(false)->after('use_tax_invoice')->comment('계좌조회 이용'); - $table->boolean('use_card_usage')->default(false)->after('use_bank_account')->comment('카드사용내역 이용'); - $table->boolean('use_hometax')->default(false)->after('use_card_usage')->comment('홈텍스매입/매출 이용'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('barobill_settings', function (Blueprint $table) { - $table->dropColumn(['use_tax_invoice', 'use_bank_account', 'use_card_usage', 'use_hometax']); - }); - } -}; diff --git a/database/migrations/2026_01_22_192637_create_coocon_configs_table.php b/database/migrations/2026_01_22_192637_create_coocon_configs_table.php deleted file mode 100644 index 8fa30504..00000000 --- a/database/migrations/2026_01_22_192637_create_coocon_configs_table.php +++ /dev/null @@ -1,34 +0,0 @@ -id(); - $table->string('name', 100)->comment('설정 이름'); - $table->enum('environment', ['test', 'production'])->default('test')->comment('환경'); - $table->string('api_key', 100)->comment('API 키'); - $table->string('base_url', 255)->comment('API 기본 URL'); - $table->text('description')->nullable()->comment('설명'); - $table->boolean('is_active')->default(false)->comment('활성화 여부'); - $table->timestamps(); - $table->softDeletes(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('coocon_configs'); - } -}; diff --git a/database/migrations/2026_01_22_201143_create_credit_inquiries_table.php b/database/migrations/2026_01_22_201143_create_credit_inquiries_table.php deleted file mode 100644 index d9e78347..00000000 --- a/database/migrations/2026_01_22_201143_create_credit_inquiries_table.php +++ /dev/null @@ -1,57 +0,0 @@ -id(); - $table->string('inquiry_key', 32)->unique()->comment('조회 고유 키'); - $table->string('company_key', 20)->index()->comment('사업자번호/법인번호'); - $table->string('company_name')->nullable()->comment('업체명'); - $table->unsignedBigInteger('user_id')->nullable()->comment('조회자 ID'); - $table->timestamp('inquired_at')->comment('조회 일시'); - - // 요약 정보 (빠른 조회용) - $table->unsignedInteger('short_term_overdue_cnt')->default(0)->comment('단기연체정보 건수'); - $table->unsignedInteger('negative_info_kci_cnt')->default(0)->comment('신용도판단정보(한국신용정보원) 건수'); - $table->unsignedInteger('negative_info_pb_cnt')->default(0)->comment('공공정보 건수'); - $table->unsignedInteger('negative_info_cb_cnt')->default(0)->comment('신용도판단정보(신용정보사) 건수'); - $table->unsignedInteger('suspension_info_cnt')->default(0)->comment('당좌거래정지정보 건수'); - $table->unsignedInteger('workout_cnt')->default(0)->comment('법정관리/워크아웃정보 건수'); - - // API 응답 원본 데이터 (JSON) - $table->json('raw_summary')->nullable()->comment('OA12 신용요약정보 원본'); - $table->json('raw_short_term_overdue')->nullable()->comment('OA13 단기연체정보 원본'); - $table->json('raw_negative_info_kci')->nullable()->comment('OA14 신용도판단정보(한국신용정보원) 원본'); - $table->json('raw_negative_info_cb')->nullable()->comment('OA15 신용도판단정보(신용정보사) 원본'); - $table->json('raw_suspension_info')->nullable()->comment('OA16 당좌거래정지정보 원본'); - $table->json('raw_workout_info')->nullable()->comment('OA17 법정관리/워크아웃정보 원본'); - - // 상태 - $table->enum('status', ['success', 'partial', 'failed'])->default('success')->comment('조회 상태'); - $table->text('error_message')->nullable()->comment('에러 메시지'); - - $table->timestamps(); - - // 인덱스 - $table->index(['company_key', 'inquired_at']); - $table->index('inquired_at'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('credit_inquiries'); - } -}; diff --git a/database/migrations/2026_01_22_203001_add_company_info_to_credit_inquiries_table.php b/database/migrations/2026_01_22_203001_add_company_info_to_credit_inquiries_table.php deleted file mode 100644 index 2844e114..00000000 --- a/database/migrations/2026_01_22_203001_add_company_info_to_credit_inquiries_table.php +++ /dev/null @@ -1,55 +0,0 @@ -string('ceo_name')->nullable()->after('company_name')->comment('대표자명'); - $table->string('company_address')->nullable()->after('ceo_name')->comment('회사 주소'); - $table->string('business_type')->nullable()->after('company_address')->comment('업종'); - $table->string('business_item')->nullable()->after('business_type')->comment('업태'); - $table->date('establishment_date')->nullable()->after('business_item')->comment('설립일'); - - // 국세청 사업자등록 상태 - $table->string('nts_status', 20)->nullable()->after('establishment_date')->comment('국세청 상태 (영업/휴업/폐업)'); - $table->string('nts_status_code', 2)->nullable()->after('nts_status')->comment('국세청 상태코드 (01/02/03)'); - $table->string('nts_tax_type', 50)->nullable()->after('nts_status_code')->comment('과세유형'); - $table->date('nts_closure_date')->nullable()->after('nts_tax_type')->comment('폐업일'); - - // API 원본 데이터 - $table->json('raw_company_info')->nullable()->after('raw_workout_info')->comment('OA08 기업기본정보 원본'); - $table->json('raw_nts_status')->nullable()->after('raw_company_info')->comment('국세청 상태조회 원본'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('credit_inquiries', function (Blueprint $table) { - $table->dropColumn([ - 'ceo_name', - 'company_address', - 'business_type', - 'business_item', - 'establishment_date', - 'nts_status', - 'nts_status_code', - 'nts_tax_type', - 'nts_closure_date', - 'raw_company_info', - 'raw_nts_status', - ]); - }); - } -}; diff --git a/database/migrations/2026_01_23_130000_create_barobill_bank_transactions_table.php b/database/migrations/2026_01_23_130000_create_barobill_bank_transactions_table.php deleted file mode 100644 index 1079a552..00000000 --- a/database/migrations/2026_01_23_130000_create_barobill_bank_transactions_table.php +++ /dev/null @@ -1,53 +0,0 @@ -id(); - $table->foreignId('tenant_id')->constrained()->onDelete('cascade'); - $table->string('bank_account_num', 50)->comment('계좌번호'); - $table->string('bank_code', 10)->nullable()->comment('은행코드'); - $table->string('bank_name', 50)->nullable()->comment('은행명'); - $table->string('trans_date', 8)->comment('거래일 (YYYYMMDD)'); - $table->string('trans_time', 6)->nullable()->comment('거래시간 (HHMMSS)'); - $table->string('trans_dt', 20)->comment('거래일시 원본 (YYYYMMDDHHMMSS)'); - $table->decimal('deposit', 18, 2)->default(0)->comment('입금액'); - $table->decimal('withdraw', 18, 2)->default(0)->comment('출금액'); - $table->decimal('balance', 18, 2)->default(0)->comment('잔액'); - $table->string('summary', 255)->nullable()->comment('적요'); - $table->string('cast', 100)->nullable()->comment('상대방'); - $table->string('memo', 255)->nullable()->comment('메모'); - $table->string('trans_office', 100)->nullable()->comment('거래점'); - $table->string('account_code', 50)->nullable()->comment('계정과목 코드'); - $table->string('account_name', 100)->nullable()->comment('계정과목 명'); - $table->timestamps(); - - // 복합 유니크 인덱스: 같은 거래는 중복 저장 방지 - $table->unique( - ['tenant_id', 'bank_account_num', 'trans_dt', 'deposit', 'withdraw', 'balance'], - 'barobill_bank_trans_unique' - ); - - // 조회용 인덱스 - $table->index(['tenant_id', 'trans_date'], 'bb_trans_tenant_date_idx'); - $table->index(['tenant_id', 'bank_account_num', 'trans_date'], 'bb_trans_tenant_acct_date_idx'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('barobill_bank_transactions'); - } -}; diff --git a/database/migrations/2026_01_23_140000_create_account_codes_table.php b/database/migrations/2026_01_23_140000_create_account_codes_table.php deleted file mode 100644 index 683e2933..00000000 --- a/database/migrations/2026_01_23_140000_create_account_codes_table.php +++ /dev/null @@ -1,37 +0,0 @@ -id(); - $table->foreignId('tenant_id')->constrained()->onDelete('cascade'); - $table->string('code', 10)->comment('계정과목 코드'); - $table->string('name', 100)->comment('계정과목 명'); - $table->string('category', 50)->nullable()->comment('분류 (자산/부채/자본/수익/비용)'); - $table->integer('sort_order')->default(0)->comment('정렬순서'); - $table->boolean('is_active')->default(true)->comment('사용여부'); - $table->timestamps(); - - // 테넌트별 계정과목 코드 유니크 - $table->unique(['tenant_id', 'code'], 'account_codes_tenant_code_unique'); - $table->index(['tenant_id', 'is_active'], 'account_codes_tenant_active_idx'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('account_codes'); - } -}; diff --git a/database/migrations/2026_01_23_150000_create_barobill_card_transactions_table.php b/database/migrations/2026_01_23_150000_create_barobill_card_transactions_table.php deleted file mode 100644 index 4993d4ac..00000000 --- a/database/migrations/2026_01_23_150000_create_barobill_card_transactions_table.php +++ /dev/null @@ -1,61 +0,0 @@ -id(); - $table->foreignId('tenant_id')->constrained()->onDelete('cascade'); - $table->string('card_num', 50)->comment('카드번호'); - $table->string('card_company', 10)->nullable()->comment('카드사 코드'); - $table->string('card_company_name', 50)->nullable()->comment('카드사명'); - $table->string('use_dt', 20)->comment('사용일시 원본 (YYYYMMDDHHMMSS)'); - $table->string('use_date', 8)->comment('사용일 (YYYYMMDD)'); - $table->string('use_time', 6)->nullable()->comment('사용시간 (HHMMSS)'); - $table->string('approval_num', 50)->nullable()->comment('승인번호'); - $table->string('approval_type', 10)->nullable()->comment('승인유형 (1=승인, 2=취소)'); - $table->decimal('approval_amount', 18, 2)->default(0)->comment('승인금액'); - $table->decimal('tax', 18, 2)->default(0)->comment('부가세'); - $table->decimal('service_charge', 18, 2)->default(0)->comment('봉사료'); - $table->string('payment_plan', 10)->nullable()->comment('할부개월수'); - $table->string('currency_code', 10)->default('KRW')->comment('통화코드'); - $table->string('merchant_name', 255)->nullable()->comment('가맹점명'); - $table->string('merchant_biz_num', 20)->nullable()->comment('가맹점 사업자번호'); - $table->string('merchant_addr', 255)->nullable()->comment('가맹점 주소'); - $table->string('merchant_ceo', 100)->nullable()->comment('가맹점 대표자'); - $table->string('merchant_biz_type', 100)->nullable()->comment('가맹점 업종'); - $table->string('merchant_tel', 50)->nullable()->comment('가맹점 전화번호'); - $table->string('memo', 255)->nullable()->comment('메모'); - $table->string('use_key', 100)->nullable()->comment('사용키'); - $table->string('account_code', 50)->nullable()->comment('계정과목 코드'); - $table->string('account_name', 100)->nullable()->comment('계정과목 명'); - $table->timestamps(); - - // 복합 유니크 인덱스: 같은 거래는 중복 저장 방지 - $table->unique( - ['tenant_id', 'card_num', 'use_dt', 'approval_num', 'approval_amount'], - 'bb_card_trans_unique' - ); - - // 조회용 인덱스 - $table->index(['tenant_id', 'use_date'], 'bb_card_trans_tenant_date_idx'); - $table->index(['tenant_id', 'card_num', 'use_date'], 'bb_card_trans_tenant_card_date_idx'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('barobill_card_transactions'); - } -}; diff --git a/database/migrations/2026_01_23_160000_create_barobill_card_transaction_splits_table.php b/database/migrations/2026_01_23_160000_create_barobill_card_transaction_splits_table.php deleted file mode 100644 index d1829e0c..00000000 --- a/database/migrations/2026_01_23_160000_create_barobill_card_transaction_splits_table.php +++ /dev/null @@ -1,52 +0,0 @@ -id(); - $table->foreignId('tenant_id')->constrained()->onDelete('cascade'); - - // 원본 거래 고유키 (바로빌에서 가져온 원본 데이터 식별) - $table->string('original_unique_key', 200)->comment('원본 거래 고유키 (cardNum|useDt|approvalNum|amount)'); - - // 분개 정보 - $table->decimal('split_amount', 18, 2)->comment('분개 금액'); - $table->string('account_code', 50)->nullable()->comment('계정과목 코드'); - $table->string('account_name', 100)->nullable()->comment('계정과목명'); - $table->string('memo', 255)->nullable()->comment('분개 메모'); - $table->integer('sort_order')->default(0)->comment('정렬 순서'); - - // 원본 거래 정보 (조회 편의를 위해 저장) - $table->string('card_num', 50)->comment('카드번호'); - $table->string('use_dt', 20)->comment('사용일시 (YYYYMMDDHHMMSS)'); - $table->string('use_date', 8)->comment('사용일 (YYYYMMDD)'); - $table->string('approval_num', 50)->nullable()->comment('승인번호'); - $table->decimal('original_amount', 18, 2)->comment('원본 거래 총액'); - $table->string('merchant_name', 255)->nullable()->comment('가맹점명'); - - $table->timestamps(); - - // 인덱스 - $table->index(['tenant_id', 'original_unique_key'], 'bb_card_split_tenant_key_idx'); - $table->index(['tenant_id', 'use_date'], 'bb_card_split_tenant_date_idx'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('barobill_card_transaction_splits'); - } -}; diff --git a/database/migrations/2026_01_23_180000_add_editable_fields_to_barobill_card_transactions_table.php b/database/migrations/2026_01_23_180000_add_editable_fields_to_barobill_card_transactions_table.php deleted file mode 100644 index 52623721..00000000 --- a/database/migrations/2026_01_23_180000_add_editable_fields_to_barobill_card_transactions_table.php +++ /dev/null @@ -1,34 +0,0 @@ -string('deduction_type', 20)->nullable()->after('account_name')->comment('공제유형: deductible/non_deductible'); - // 증빙/판매자상호 (사용자 수정용) - $table->string('evidence_name', 255)->nullable()->after('deduction_type')->comment('증빙/판매자상호 (수정용)'); - // 내역 (사용자 수정용) - $table->string('description', 500)->nullable()->after('evidence_name')->comment('내역 (수정용)'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('barobill_card_transactions', function (Blueprint $table) { - $table->dropColumn(['deduction_type', 'evidence_name', 'description']); - }); - } -}; diff --git a/database/migrations/2026_01_23_190000_add_evidence_fields_to_barobill_card_transaction_splits_table.php b/database/migrations/2026_01_23_190000_add_evidence_fields_to_barobill_card_transaction_splits_table.php deleted file mode 100644 index 977efe72..00000000 --- a/database/migrations/2026_01_23_190000_add_evidence_fields_to_barobill_card_transaction_splits_table.php +++ /dev/null @@ -1,30 +0,0 @@ -string('evidence_name', 255)->nullable()->after('account_name')->comment('증빙/판매자상호'); - $table->string('description', 500)->nullable()->after('evidence_name')->comment('내역'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('barobill_card_transaction_splits', function (Blueprint $table) { - $table->dropColumn(['evidence_name', 'description']); - }); - } -}; diff --git a/database/migrations/2026_01_23_191000_add_deduction_type_to_barobill_card_transaction_splits_table.php b/database/migrations/2026_01_23_191000_add_deduction_type_to_barobill_card_transaction_splits_table.php deleted file mode 100644 index 45ace617..00000000 --- a/database/migrations/2026_01_23_191000_add_deduction_type_to_barobill_card_transaction_splits_table.php +++ /dev/null @@ -1,29 +0,0 @@ -string('deduction_type', 20)->nullable()->after('account_name')->comment('공제유형: deductible/non_deductible'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('barobill_card_transaction_splits', function (Blueprint $table) { - $table->dropColumn('deduction_type'); - }); - } -}; diff --git a/database/seeders/MngMenuSeeder.php b/database/seeders/MngMenuSeeder.php index c34699f3..6d92cb8d 100644 --- a/database/seeders/MngMenuSeeder.php +++ b/database/seeders/MngMenuSeeder.php @@ -639,6 +639,46 @@ protected function seedMainMenus(): void 'options' => ['route_name' => 'credit.inquiry.index', 'section' => 'main'], ]); + // ======================================== + // 영업관리 그룹 + // ======================================== + $salesGroup = $this->createMenu([ + 'name' => '영업관리', + 'url' => '#', + 'icon' => 'briefcase', + 'sort_order' => $sortOrder++, + 'options' => [ + 'section' => 'main', + 'meta' => ['group_id' => 'sales-group'], + ], + ]); + + $salesSubOrder = 0; + $this->createMenu([ + 'parent_id' => $salesGroup->id, + 'name' => '영업담당자 관리', + 'url' => '/sales/managers', + 'icon' => 'users', + 'sort_order' => $salesSubOrder++, + 'options' => ['route_name' => 'sales.managers.index', 'section' => 'main'], + ]); + $this->createMenu([ + 'parent_id' => $salesGroup->id, + 'name' => '가망고객 관리', + 'url' => '/sales/prospects', + 'icon' => 'user-group', + 'sort_order' => $salesSubOrder++, + 'options' => ['route_name' => 'sales.prospects.index', 'section' => 'main'], + ]); + $this->createMenu([ + 'parent_id' => $salesGroup->id, + 'name' => '영업실적 관리', + 'url' => '/sales/records', + 'icon' => 'chart-bar', + 'sort_order' => $salesSubOrder++, + 'options' => ['route_name' => 'sales.records.index', 'section' => 'main'], + ]); + // ======================================== // 시스템 그룹 // ======================================== diff --git a/resources/views/sales/managers/create.blade.php b/resources/views/sales/managers/create.blade.php new file mode 100644 index 00000000..6476a7ba --- /dev/null +++ b/resources/views/sales/managers/create.blade.php @@ -0,0 +1,153 @@ +@extends('layouts.app') + +@section('title', '영업담당자 등록') + +@section('content') +
영업 담당자 및 매니저를 관리합니다
+| 이름 | +아이디 | +역할 | +연락처 | +상위관리자 | +등록일 | +관리 | +
|---|---|---|---|---|---|---|
|
+ {{ $manager->name }}
+ @if($manager->email)
+ {{ $manager->email }}
+ @endif
+ |
+ + {{ $manager->member_id }} + | ++ + {{ $manager->role_label }} + + | ++ {{ $manager->phone ?? '-' }} + | ++ {{ $manager->parent?->name ?? '-' }} + | ++ {{ $manager->created_at->format('Y-m-d') }} + | ++ 상세 + 수정 + + | +
| + 등록된 담당자가 없습니다. + | +||||||
{{ $manager->remarks }}
+| 이름 | +역할 | +연락처 | +
|---|---|---|
| + + {{ $child->name }} + + | ++ + {{ $child->role_label }} + + | +{{ $child->phone ?? '-' }} | +
영업 가망고객을 관리합니다
+| 업체정보 | +상태 | +담당자 | +연락처 | +계약금액 | +등록일 | +관리 | +
|---|---|---|---|---|---|---|
|
+ {{ $prospect->company_name }}
+ @if($prospect->business_no)
+ {{ $prospect->formatted_business_no }}
+ @endif
+ @if($prospect->representative)
+ {{ $prospect->representative }}
+ @endif
+ |
+ + + {{ $prospect->status_label }} + + | +
+ {{ $prospect->manager?->name ?? '-' }}
+ @if($prospect->salesManager && $prospect->salesManager->id !== $prospect->manager?->id)
+ 담당: {{ $prospect->salesManager->name }}
+ @endif
+ |
+ + {{ $prospect->contact_phone ?? '-' }} + | +
+ @if($prospect->products->count() > 0)
+ {{ number_format($prospect->total_contract_amount) }}원
+ 수수료: {{ number_format($prospect->total_commission) }}원
+ @else
+ -
+ @endif
+ |
+ + {{ $prospect->created_at->format('Y-m-d') }} + | ++ 상세 + 수정 + + | +
| + 등록된 가망고객이 없습니다. + | +||||||
+ + {{ $prospect->status_label }} + + @if($prospect->business_no) + {{ $prospect->formatted_business_no }} + @endif +
+| 상품명 | +계약금액 | +수수료 | +승인상태 | +계약일 | +
|---|---|---|---|---|
| {{ $product->product_name }} | +{{ number_format($product->contract_amount) }}원 | +{{ number_format($product->commission_amount) }}원 | ++ + {{ $product->approval_status }} + + | +{{ $product->contract_date?->format('Y-m-d') ?? '-' }} | +
등록된 계약 상품이 없습니다.
+ @endif +{{ $consultation->log_text }}
+등록된 상담 기록이 없습니다.
+ @endif +영업 실적을 관리합니다
+| 실적일 | +담당자 | +유형 | +가망고객 | +금액 | +수수료 | +상태 | +관리 | +
|---|---|---|---|---|---|---|---|
| + {{ $record->record_date->format('Y-m-d') }} + | +
+ {{ $record->manager?->name ?? '-' }}
+ |
+ + {{ $record->record_type }} + | ++ @if($record->prospect) + + {{ $record->prospect->company_name }} + + @else + - + @endif + | ++ {{ number_format($record->amount) }}원 + | ++ {{ number_format($record->commission) }}원 + | ++ + {{ $record->status_label }} + + | ++ 상세 + 수정 + + | +
| + 등록된 실적이 없습니다. + | +|||||||
{{ $record->description }}
+