diff --git a/app/Http/Controllers/Sales/SalesDashboardController.php b/app/Http/Controllers/Sales/SalesDashboardController.php index fe077e3d..0db5e311 100644 --- a/app/Http/Controllers/Sales/SalesDashboardController.php +++ b/app/Http/Controllers/Sales/SalesDashboardController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Sales; use App\Http\Controllers\Controller; +use App\Models\Sales\SalesCommission; use App\Models\Sales\SalesPartner; use App\Models\Sales\SalesTenantManagement; use App\Models\Sales\TenantProspect; @@ -64,56 +65,107 @@ private function getDashboardData(Request $request): array $endDate = now()->endOfMonth()->format('Y-m-d'); } - // 통계 데이터 (임시 데이터 - 추후 실제 데이터로 교체) + $currentUserId = auth()->id(); + $childrenIds = auth()->user()->children()->pluck('id')->toArray(); + $partnerIds = array_merge([$currentUserId], $childrenIds); + + // 현재 사용자의 영업파트너 정보 조회 + $partner = SalesPartner::where('user_id', $currentUserId)->first(); + $partnerId = $partner?->id; + + // 나와 관련된 모든 수당 조회 (영업파트너로서 + 매니저로서) + $myCommissionsAsPartner = $partnerId + ? SalesCommission::forPartner($partnerId)->get() + : collect(); + $myCommissionsAsManager = SalesCommission::forManager($currentUserId)->get(); + + // 판매자(영업파트너) 수당 계산 + $partnerCommissionTotal = $myCommissionsAsPartner->sum('partner_commission'); + $partnerCommissionPaid = $myCommissionsAsPartner->where('status', SalesCommission::STATUS_PAID)->sum('partner_commission'); + $partnerCommissionPending = $myCommissionsAsPartner->where('status', SalesCommission::STATUS_PENDING)->sum('partner_commission'); + $partnerCommissionApproved = $myCommissionsAsPartner->where('status', SalesCommission::STATUS_APPROVED)->sum('partner_commission'); + + // 매니저 수당 계산 + $managerCommissionTotal = $myCommissionsAsManager->sum('manager_commission'); + $managerCommissionPaid = $myCommissionsAsManager->where('status', SalesCommission::STATUS_PAID)->sum('manager_commission'); + $managerCommissionPending = $myCommissionsAsManager->where('status', SalesCommission::STATUS_PENDING)->sum('manager_commission'); + $managerCommissionApproved = $myCommissionsAsManager->where('status', SalesCommission::STATUS_APPROVED)->sum('manager_commission'); + + // 총 수당 계산 (중복 제거: 동일 commission에서 partner + manager인 경우) + $allCommissionIds = $myCommissionsAsPartner->pluck('id')->merge($myCommissionsAsManager->pluck('id'))->unique(); + $totalContracts = $allCommissionIds->count(); + + // 통계 데이터 (실제 데이터) + $totalMembershipFee = $myCommissionsAsPartner->sum('payment_amount') + $myCommissionsAsManager->sum('payment_amount'); + $totalCommission = $partnerCommissionTotal + $managerCommissionTotal; + $paidCommission = $partnerCommissionPaid + $managerCommissionPaid; + $commissionRate = $totalCommission > 0 ? round(($paidCommission / $totalCommission) * 100, 1) : 0; + $stats = [ - 'total_membership_fee' => 0, // 총 가입비 - 'total_commission' => 0, // 총 수당 - 'commission_rate' => 0, // 지급 승인 완료 비율 - 'total_contracts' => 0, // 전체 건수 - 'pending_membership_approval' => 0, // 가입 승인 대기 - 'pending_payment_approval' => 0, // 지급 승인 대기 + 'total_membership_fee' => $totalMembershipFee, // 총 가입비 + 'total_commission' => $totalCommission, // 총 수당 + 'commission_rate' => $commissionRate, // 지급 완료 비율 + 'total_contracts' => $totalContracts, // 전체 건수 + 'pending_membership_approval' => $myCommissionsAsPartner->where('status', SalesCommission::STATUS_PENDING)->count() + + $myCommissionsAsManager->where('status', SalesCommission::STATUS_PENDING)->count(), + 'pending_payment_approval' => $myCommissionsAsPartner->where('status', SalesCommission::STATUS_APPROVED)->count() + + $myCommissionsAsManager->where('status', SalesCommission::STATUS_APPROVED)->count(), ]; - // 역할별 수당 상세 + // 역할별 수당 상세 (실제 데이터) $commissionByRole = [ [ 'name' => '판매자', 'rate' => 20, - 'amount' => 0, + 'amount' => $partnerCommissionTotal, + 'paid' => $partnerCommissionPaid, + 'pending' => $partnerCommissionPending, + 'approved' => $partnerCommissionApproved, 'color' => 'green', ], [ 'name' => '관리자', 'rate' => 5, - 'amount' => 0, + 'amount' => $managerCommissionTotal, + 'paid' => $managerCommissionPaid, + 'pending' => $managerCommissionPending, + 'approved' => $managerCommissionApproved, 'color' => 'blue', ], [ - 'name' => '매뉴제작 협업수당', - 'rate' => null, // 별도 - 'amount' => null, // 운영팀 산정 - 'color' => 'red', + 'name' => '협업지원금', + 'rate' => null, // 메뉴당 2,000원 + 'amount' => null, // 가입비 완납 시 계산 + 'color' => 'purple', ], ]; - // 총 가입비 대비 수당 - $totalCommissionRatio = 0; + // 총 가입비 대비 수당 비율 + $totalCommissionRatio = $totalMembershipFee > 0 ? round(($totalCommission / $totalMembershipFee) * 100, 1) : 0; - // 수익 및 테넌트 관리 통계 (임시 데이터 - 추후 실제 데이터로 교체) - $tenantStats = [ - 'total_tenants' => 0, // 관리 테넌트 - 'total_membership_revenue' => 0, // 총 가입비 실적 - 'total_commission_accumulated' => 0, // 누적 가입비 수당 - 'confirmed_commission' => 0, // 확정 가입비 수당 - ]; - - // 테넌트 목록 (가망고객에서 전환된 테넌트만) - // 전환된 가망고객의 tenant_id 목록 조회 - $convertedTenantIds = TenantProspect::whereNotNull('tenant_id') + // 1) 내가 등록한 가망고객에서 전환된 tenant_id (20% 수당) + $registeredTenantIds = TenantProspect::whereNotNull('tenant_id') ->where('status', TenantProspect::STATUS_CONVERTED) + ->whereIn('registered_by', $partnerIds) ->pluck('tenant_id') ->toArray(); + // 2) 내가 매니저로 지정된 tenant_id (5% 수당) + $managedTenantIds = SalesTenantManagement::where('manager_user_id', $currentUserId) + ->pluck('tenant_id') + ->toArray(); + + // 두 목록 합치기 (중복 제거) + $convertedTenantIds = array_unique(array_merge($registeredTenantIds, $managedTenantIds)); + + // 수익 및 테넌트 관리 통계 (실제 데이터) + $tenantStats = [ + 'total_tenants' => count($convertedTenantIds), // 관리 테넌트 + 'total_membership_revenue' => $totalMembershipFee, // 총 가입비 실적 + 'total_commission_accumulated' => $totalCommission, // 누적 수당 + 'confirmed_commission' => $paidCommission, // 확정(지급완료) 수당 + ]; + // 전환된 테넌트만 조회 (최신순, 페이지네이션) $tenants = Tenant::whereIn('id', $convertedTenantIds) ->orderBy('created_at', 'desc') @@ -198,12 +250,26 @@ public function assignManager(int $tenantId, Request $request): JsonResponse */ public function refreshTenantList(Request $request): View { - // 전환된 가망고객의 tenant_id 목록 조회 - $convertedTenantIds = TenantProspect::whereNotNull('tenant_id') + // 테넌트 목록 (나와 연결된 계약만) + $currentUserId = auth()->id(); + $childrenIds = auth()->user()->children()->pluck('id')->toArray(); + $partnerIds = array_merge([$currentUserId], $childrenIds); + + // 1) 내가 등록한 가망고객에서 전환된 tenant_id (20% 수당) + $registeredTenantIds = TenantProspect::whereNotNull('tenant_id') ->where('status', TenantProspect::STATUS_CONVERTED) + ->whereIn('registered_by', $partnerIds) ->pluck('tenant_id') ->toArray(); + // 2) 내가 매니저로 지정된 tenant_id (5% 수당) + $managedTenantIds = SalesTenantManagement::where('manager_user_id', $currentUserId) + ->pluck('tenant_id') + ->toArray(); + + // 두 목록 합치기 (중복 제거) + $convertedTenantIds = array_unique(array_merge($registeredTenantIds, $managedTenantIds)); + // 전환된 테넌트만 조회 (최신순, 페이지네이션) $tenants = Tenant::whereIn('id', $convertedTenantIds) ->orderBy('created_at', 'desc') diff --git a/app/Http/Controllers/TenantController.php b/app/Http/Controllers/TenantController.php index 82c5ddc9..b437a240 100644 --- a/app/Http/Controllers/TenantController.php +++ b/app/Http/Controllers/TenantController.php @@ -4,6 +4,7 @@ use App\Services\TenantService; use Illuminate\Http\Request; +use Illuminate\Http\Response; use Illuminate\View\View; class TenantController extends Controller @@ -15,8 +16,13 @@ public function __construct( /** * 테넌트 목록 (Blade 화면) */ - public function index(Request $request): View + public function index(Request $request): View|Response { + // HTMX 요청 시 전체 페이지 리로드 (스크립트 실행 필요) + if ($request->header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('tenants.index')); + } + return view('tenants.index'); } diff --git a/claudedocs/수당지급.md b/claudedocs/수당지급.md new file mode 100644 index 00000000..92339fa5 --- /dev/null +++ b/claudedocs/수당지급.md @@ -0,0 +1,372 @@ +# 수당 지급 시스템 + +> SAM 프로젝트 영업파트너 수당 지급 시스템 기술 문서 +> +> 최종 수정: 2026-01-30 + +--- + +## 1. 개요 + +### 1.1 목적 +이 문서는 SAM 영업관리 시스템의 **수당 계산 및 지급 프로세스**를 정의합니다. + +### 1.2 수당 유형 + +| 수당 유형 | 수당률/금액 | 대상 | 기준 | +|-----------|-------------|------|------| +| **판매자 수당** | 20% | 가망고객 등록자 | 가입비의 50% 기준 | +| **매니저 수당** | 5% | 지정된 매니저 | 가입비의 50% 기준 | +| **협업지원금** | 메뉴당 2,000원 | 2단계 상위 파트너 | 가입비 완납 시 | + +--- + +## 2. 수당 계산 로직 + +### 2.1 기본 공식 + +``` +기준 금액 = 총 가입비 ÷ 2 (50%) + +판매자 수당 = 기준 금액 × 20% +매니저 수당 = 기준 금액 × 5% +``` + +### 2.2 계산 예시 + +``` +총 가입비: 10,000,000원 +기준 금액: 5,000,000원 (50%) + +판매자 수당: 5,000,000 × 20% = 1,000,000원 +매니저 수당: 5,000,000 × 5% = 250,000원 +``` + +### 2.3 입금 구분별 수당 + +| 입금 구분 | 코드 | 설명 | +|-----------|------|------| +| **계약금** | `deposit` | 계약 시 선입금 | +| **잔금** | `balance` | 계약 후 잔여금 | + +각 입금 시점마다 별도의 수당이 생성됩니다. + +--- + +## 3. 협업지원금 + +### 3.1 도입 배경 + +**다단계 판매법 준수**: 다단계 판매법에서는 2단계 이상의 수당 지급이 금지되어 있습니다. +이를 준수하면서도 상위 파트너의 기여를 인정하기 위해 "수당"이 아닌 "지원금" 형태로 지급합니다. + +### 3.2 지급 대상 + +계약 체결자(판매자) 기준 **2단계 상위 파트너** (할아버지 파트너) + +``` +할아버지 파트너 ← 협업지원금 수령 + │ + ↓ (유치) +아버지 파트너 + │ + ↓ (유치) +손자 파트너 ← 테넌트 계약 체결 (판매자 수당 20%) + │ + ↓ +테넌트 계약 +``` + +### 3.3 산출 기준 + +| 항목 | 내용 | +|------|------| +| **산출 공식** | 테넌트 메뉴 개수 × 2,000원 | +| **지급 시점** | 가입비 완납 시 | +| **지급 대상** | 계약자의 parent의 parent (2단계 상위) | + +### 3.4 계산 예시 + +``` +[상황] +- 손자 파트너가 테넌트 A와 계약 체결 +- 테넌트 A에 메뉴 50개 생성 +- 가입비 1,000만원 완납 + +[수당/지원금 지급] +손자 파트너 (판매자): 500만원 × 20% = 100만원 +매니저 (지정된 경우): 500만원 × 5% = 25만원 +할아버지 파트너: 50개 × 2,000원 = 10만원 (협업지원금) +``` + +### 3.5 지급 조건 + +1. 계약자(손자)의 parent_id가 존재해야 함 (아버지 파트너) +2. 아버지 파트너의 parent_id가 존재해야 함 (할아버지 파트너) +3. 가입비가 **완납**되어야 함 +4. 테넌트에 메뉴가 생성되어 있어야 함 + +> **주의**: 1단계 상위(아버지)는 협업지원금 대상이 아님. +> 직접 유치한 파트너의 계약에 대해서는 별도 수당 정책 없음 (다단계법 준수). + +--- + +## 4. 수당 지급 프로세스 + +### 3.1 상태 흐름 + +``` +┌─────────┐ ┌──────────┐ ┌─────────┐ ┌───────────┐ +│ 입금 │ ──▶ │ 대기 │ ──▶ │ 승인 │ ──▶ │ 지급완료 │ +│ 등록 │ │ pending │ │ approved│ │ paid │ +└─────────┘ └──────────┘ └─────────┘ └───────────┘ + │ + ▼ + ┌──────────┐ + │ 취소 │ + │cancelled │ + └──────────┘ +``` + +### 3.2 상태별 설명 + +| 상태 | 코드 | 설명 | +|------|------|------| +| **대기** | `pending` | 입금 등록 후 승인 대기 중 | +| **승인** | `approved` | 본사 승인 완료, 지급 예정 | +| **지급완료** | `paid` | 실제 지급 완료 | +| **취소** | `cancelled` | 취소됨 (대기/승인 상태에서만 가능) | + +### 3.3 지급예정일 계산 + +```php +// 입금일 익월 10일 +$scheduledPaymentDate = $paymentDate->addMonth()->day(10); +``` + +**예시:** +- 1월 15일 입금 → 2월 10일 지급예정 +- 1월 31일 입금 → 2월 10일 지급예정 + +--- + +## 4. 데이터베이스 구조 + +### 4.1 sales_commissions 테이블 + +```sql +CREATE TABLE sales_commissions ( + id BIGINT UNSIGNED PRIMARY KEY, + tenant_id BIGINT UNSIGNED NOT NULL, + management_id BIGINT UNSIGNED NOT NULL, + + -- 입금 정보 + payment_type ENUM('deposit', 'balance') NOT NULL, + payment_amount DECIMAL(15,2) NOT NULL, + payment_date DATE NOT NULL, + + -- 수당 계산 + base_amount DECIMAL(15,2) NOT NULL, -- 기준 금액 (가입비의 50%) + partner_rate DECIMAL(5,2) DEFAULT 20.00, -- 판매자 수당률 + manager_rate DECIMAL(5,2) DEFAULT 5.00, -- 매니저 수당률 + partner_commission DECIMAL(15,2) NOT NULL, -- 판매자 수당액 + manager_commission DECIMAL(15,2) NOT NULL, -- 매니저 수당액 + + -- 지급 정보 + scheduled_payment_date DATE NOT NULL, -- 지급예정일 (익월 10일) + actual_payment_date DATE NULL, -- 실제 지급일 + status ENUM('pending', 'approved', 'paid', 'cancelled'), + + -- 담당자 + partner_id BIGINT UNSIGNED NOT NULL, -- 영업파트너 ID + manager_user_id BIGINT UNSIGNED NULL, -- 매니저 사용자 ID + + -- 승인 정보 + approved_by BIGINT UNSIGNED NULL, + approved_at TIMESTAMP NULL, + + -- 기타 + bank_reference VARCHAR(100) NULL, -- 이체 참조번호 + notes TEXT NULL, + + created_at TIMESTAMP, + updated_at TIMESTAMP, + deleted_at TIMESTAMP NULL +); +``` + +### 4.2 sales_commission_details 테이블 (상품별 상세) + +```sql +CREATE TABLE sales_commission_details ( + id BIGINT UNSIGNED PRIMARY KEY, + commission_id BIGINT UNSIGNED NOT NULL, + contract_product_id BIGINT UNSIGNED NOT NULL, + + registration_fee DECIMAL(15,2) NOT NULL, -- 상품 가입비 + base_amount DECIMAL(15,2) NOT NULL, -- 기준 금액 + partner_rate DECIMAL(5,2) NOT NULL, -- 상품별 판매자 수당률 + manager_rate DECIMAL(5,2) NOT NULL, -- 상품별 매니저 수당률 + partner_commission DECIMAL(15,2) NOT NULL, -- 판매자 수당액 + manager_commission DECIMAL(15,2) NOT NULL, -- 매니저 수당액 + + created_at TIMESTAMP, + updated_at TIMESTAMP +); +``` + +--- + +## 5. 서비스 클래스 + +### 5.1 SalesCommissionService + +경로: `app/Services/SalesCommissionService.php` + +#### 주요 메서드 + +| 메서드 | 설명 | +|--------|------| +| `createCommission()` | 입금 등록 시 수당 생성 | +| `approve()` | 수당 승인 처리 | +| `markAsPaid()` | 지급완료 처리 | +| `bulkApprove()` | 일괄 승인 | +| `bulkMarkAsPaid()` | 일괄 지급완료 | +| `cancel()` | 취소 처리 | +| `getPartnerCommissionSummary()` | 영업파트너 수당 요약 | +| `getManagerCommissionSummary()` | 매니저 수당 요약 | + +#### 수당 생성 예시 + +```php +$commission = $this->commissionService->createCommission( + managementId: $management->id, + paymentType: 'deposit', // 계약금 + paymentAmount: 5000000, // 500만원 + paymentDate: '2026-01-30' +); +``` + +### 5.2 수당 요약 조회 + +```php +// 영업파트너 요약 +$summary = $this->commissionService->getPartnerCommissionSummary($partnerId); +// [ +// 'scheduled_this_month' => 1000000, // 이번 달 지급예정 +// 'total_received' => 5000000, // 누적 수령 +// 'pending_amount' => 500000, // 대기중 +// 'contracts_this_month' => 3, // 이번 달 계약 건수 +// ] + +// 매니저 요약 +$summary = $this->commissionService->getManagerCommissionSummary($managerUserId); +``` + +--- + +## 6. 대시보드 통계 + +### 6.1 영업파트너 대시보드 + +경로: `/sales/salesmanagement/dashboard` + +#### 표시 항목 + +| 항목 | 설명 | +|------|------| +| 총 가입비 | 나와 관련된 계약의 총 입금액 | +| 총 수당 | 판매자 수당 + 매니저 수당 합계 | +| 지급 완료 비율 | (지급완료 수당 / 총 수당) × 100 | +| 전체 건수 | 관련 계약 건수 | +| 승인 대기 | pending 상태 건수 | +| 지급 대기 | approved 상태 건수 | + +#### 역할별 수당 표시 + +``` +┌─────────────────────────────────────────────┐ +│ 판매자 수당 (20%) │ +│ ├─ 총액: 1,000,000원 │ +│ ├─ 지급완료: 500,000원 │ +│ ├─ 승인완료: 300,000원 │ +│ └─ 대기중: 200,000원 │ +├─────────────────────────────────────────────┤ +│ 매니저 수당 (5%) │ +│ ├─ 총액: 250,000원 │ +│ ├─ 지급완료: 100,000원 │ +│ ├─ 승인완료: 100,000원 │ +│ └─ 대기중: 50,000원 │ +└─────────────────────────────────────────────┘ +``` + +### 6.2 내 계약 현황 조회 범위 + +대시보드에 표시되는 계약: +1. **내가 등록한 가망고객** → 전환된 테넌트 (판매자 수당 20%) +2. **내 하위 파트너가 등록한 가망고객** → 전환된 테넌트 +3. **내가 매니저로 지정된 계약** (매니저 수당 5%) + +```php +// 1) 내가 등록한 가망고객에서 전환된 tenant_id +$registeredTenantIds = TenantProspect::whereIn('registered_by', $partnerIds) + ->where('status', 'converted') + ->pluck('tenant_id'); + +// 2) 내가 매니저로 지정된 tenant_id +$managedTenantIds = SalesTenantManagement::where('manager_user_id', $currentUserId) + ->pluck('tenant_id'); +``` + +--- + +## 7. API 엔드포인트 + +### 7.1 수당 정산 관리 + +| Method | Endpoint | 설명 | +|--------|----------|------| +| GET | `/sales/commissions` | 정산 목록 조회 | +| GET | `/sales/commissions/{id}` | 정산 상세 조회 | +| POST | `/sales/commissions` | 입금 등록 (수당 생성) | +| POST | `/sales/commissions/{id}/approve` | 승인 처리 | +| POST | `/sales/commissions/{id}/paid` | 지급완료 처리 | +| POST | `/sales/commissions/{id}/cancel` | 취소 처리 | +| POST | `/sales/commissions/bulk-approve` | 일괄 승인 | +| POST | `/sales/commissions/bulk-paid` | 일괄 지급완료 | + +--- + +## 8. 관련 파일 + +### 모델 +``` +app/Models/Sales/SalesCommission.php # 수당 정산 모델 +app/Models/Sales/SalesCommissionDetail.php # 수당 상세 내역 +app/Models/Sales/SalesPartner.php # 영업파트너 (누적 수당 저장) +app/Models/Sales/SalesTenantManagement.php # 테넌트별 영업 관리 +``` + +### 서비스 +``` +app/Services/SalesCommissionService.php # 수당 정산 서비스 +``` + +### 컨트롤러 +``` +app/Http/Controllers/Sales/SalesCommissionController.php # 수당 정산 관리 +app/Http/Controllers/Sales/SalesDashboardController.php # 대시보드 +``` + +--- + +## 9. 변경 이력 + +| 날짜 | 변경 내용 | 작성자 | +|------|----------|--------| +| 2026-01-30 | 최초 작성 | Claude | + +--- + +> **참고:** 이 문서는 수당 관련 기능 개발 시 기준 문서로 사용됩니다. +> 수당 정책 변경 시 반드시 이 문서를 먼저 업데이트하세요. diff --git a/claudedocs/영업파트너구조.md b/claudedocs/영업파트너구조.md index 4944ae71..954953a3 100644 --- a/claudedocs/영업파트너구조.md +++ b/claudedocs/영업파트너구조.md @@ -2,7 +2,7 @@ # 영업파트너 구조 설계서 > SAM 프로젝트 영업관리 시스템의 핵심 구조 문서 > -> 최종 수정: 2026-01-27 +> 최종 수정: 2026-01-30 --- @@ -96,38 +96,47 @@ ### 3.2 역할 위임 시나리오 ## 4. 수당/수익 구조 +> **상세 내용:** [수당지급.md](./수당지급.md) 참조 + ### 4.1 수당 유형 -| 수당 유형 | 기준 | 지급 대상 | -|-----------|------|-----------| -| **영업 수당** | 본인이 체결한 계약 | 계약 체결자 (sales 역할) | -| **관리 수당** | 하위 파트너 실적 | manager 역할 보유자 | -| **유치 수당** | 신규 파트너 유치 | recruiter 역할 보유자 | +| 수당 유형 | 수당률/금액 | 지급 대상 | 설명 | +|-----------|-------------|-----------|------| +| **판매자 수당** | 20% | 가망고객 등록자 | 가입비의 50% × 20% | +| **매니저 수당** | 5% | 지정된 매니저 | 가입비의 50% × 5% | +| **협업지원금** | 메뉴당 2,000원 | 2단계 상위 파트너 | 가입비 완납 시 지급 | ### 4.2 수당 계산 원칙 ``` -1. 영업 수당: 본인 계약 × 영업 수당률 -2. 관리 수당: 하위 N단계 실적 × 단계별 관리 수당률 -3. 유치 수당: 유치한 파트너 가입비 × 유치 수당률 +기준 금액 = 총 가입비의 50% + +1. 판매자 수당: 기준 금액 × 20% (가망고객 등록자) +2. 매니저 수당: 기준 금액 × 5% (매니저로 지정된 파트너) ``` -### 4.3 계층별 수당 흐름 예시 +### 4.3 수당 흐름 예시 ``` -고객 계약 100만원 발생 (계약자: 박지민) +고객 계약 (가입비 1,000만원) + └─ 기준 금액: 500만원 (가입비의 50%) -박지민 (레벨3, sales) - → 영업 수당 20% = 20만원 +김철수 (가망고객 등록자, 판매자) + → 판매자 수당: 500만원 × 20% = 100만원 -이영희 (레벨2, 박지민의 상위) - → 관리 수당 5% = 5만원 (sales 역할이지만 상위로서) - -김철수 (레벨1, 이영희의 상위) - → 관리 수당 2% = 2만원 (레벨 차이에 따른 감소) +이영희 (김철수가 지정한 매니저) + → 매니저 수당: 500만원 × 5% = 25만원 ``` -> **주의:** 실제 수당률은 정책에 따라 변경될 수 있음 +### 4.4 수당 지급 프로세스 + +``` +1. 입금 등록 → SalesCommission 생성 (status: pending) +2. 본사 승인 → status: approved +3. 지급 완료 → status: paid + 누적 수당 업데이트 +``` + +> **참고:** 자세한 수당 시스템 구현 내용은 [수당지급.md](./수당지급.md) 참조 --- @@ -193,14 +202,18 @@ ### 6.1 완료된 기능 - [x] 역할 위임 기능 (상위 → 하위) - [x] 역할 부여/제거 기능 - [x] 추천인(유치자) 관리 +- [x] **수당 자동 계산 (판매자 20%, 매니저 5%)** +- [x] **수당 정산 시스템 (SalesCommission)** +- [x] **수당 승인/지급 프로세스** +- [x] **대시보드 통계 (실적, 수당 현황)** +- [x] **가망고객 등록/관리** +- [x] **테넌트 전환 프로세스** ### 6.2 구현 예정 기능 -- [ ] 계층별 수당 자동 계산 - [ ] 조직도 시각화 (트리 뷰) -- [ ] 하위 파트너 실적 대시보드 - [ ] 유치 실적 관리 -- [ ] 수당 지급 내역 관리 +- [ ] 성과 분석 리포트 --- @@ -255,18 +268,46 @@ ## 8. 용어 정리 ## 9. 관련 파일 경로 ### MNG 프로젝트 + +#### 모델 ``` -app/Models/User.php # 사용자 모델 (영업파트너) +app/Models/User.php # 사용자 모델 (영업파트너, parent_id) +app/Models/Sales/SalesPartner.php # 영업파트너 정보 app/Models/Sales/SalesManagerDocument.php # 첨부 서류 모델 +app/Models/Sales/SalesCommission.php # 수당 정산 모델 +app/Models/Sales/SalesCommissionDetail.php # 수당 상세 내역 +app/Models/Sales/SalesTenantManagement.php # 테넌트별 영업 관리 +app/Models/Sales/TenantProspect.php # 가망고객 모델 +``` + +#### 서비스 +``` +app/Services/SalesCommissionService.php # 수당 정산 서비스 app/Services/Sales/SalesManagerService.php # 영업파트너 서비스 -app/Http/Controllers/Sales/SalesManagerController.php -resources/views/sales/managers/ # 뷰 파일들 +``` + +#### 컨트롤러 +``` +app/Http/Controllers/Sales/SalesManagerController.php # 영업파트너 관리 +app/Http/Controllers/Sales/SalesDashboardController.php # 대시보드 +app/Http/Controllers/Sales/SalesProspectController.php # 가망고객 관리 +app/Http/Controllers/Sales/SalesCommissionController.php # 수당 정산 +``` + +#### 뷰 +``` +resources/views/sales/managers/ # 영업파트너 관리 +resources/views/sales/dashboard/ # 대시보드 +resources/views/sales/prospects/ # 가망고객 관리 +resources/views/sales/commissions/ # 수당 정산 ``` ### API 프로젝트 ``` database/migrations/2026_01_27_200000_add_sales_manager_fields_to_users_table.php database/migrations/2026_01_27_200100_create_sales_manager_documents_table.php +database/migrations/..._create_sales_commissions_table.php +database/migrations/..._create_tenant_prospects_table.php ``` --- @@ -277,6 +318,9 @@ ## 10. 변경 이력 |------|----------|--------| | 2026-01-27 | 최초 작성 | Claude | | 2026-01-27 | 역할 위임/부여/제거 기능 구현 완료 | Claude | +| 2026-01-30 | 수당 구조 업데이트 (판매자 20%, 매니저 5%) | Claude | +| 2026-01-30 | 수당 시스템 구현 완료 반영 | Claude | +| 2026-01-30 | 관련 파일 경로 업데이트 | Claude | --- diff --git a/resources/views/finance/corporate-cards.blade.php b/resources/views/finance/corporate-cards.blade.php index 5fba401e..d62ba9b9 100644 --- a/resources/views/finance/corporate-cards.blade.php +++ b/resources/views/finance/corporate-cards.blade.php @@ -50,61 +50,8 @@ const XCircle = createIcon('x-circle'); function CorporateCardsManagement() { - // 카드 목록 데이터 - const [cards, setCards] = useState([ - { - id: 1, - cardName: '업무용 법인카드', - cardCompany: '삼성카드', - cardNumber: '9410-1234-5678-9012', - cardType: 'credit', - paymentDay: 15, - creditLimit: 10000000, - currentUsage: 3500000, - holder: '김대표', - status: 'active', - memo: '일반 업무용' - }, - { - id: 2, - cardName: '마케팅 법인카드', - cardCompany: '현대카드', - cardNumber: '5412-9876-5432-1098', - cardType: 'credit', - paymentDay: 25, - creditLimit: 5000000, - currentUsage: 2100000, - holder: '박마케팅', - status: 'active', - memo: '마케팅/광고비 전용' - }, - { - id: 3, - cardName: '개발팀 체크카드', - cardCompany: '국민카드', - cardNumber: '4532-1111-2222-3333', - cardType: 'debit', - paymentDay: 0, - creditLimit: 0, - currentUsage: 850000, - holder: '이개발', - status: 'active', - memo: '개발 장비/소프트웨어 구매' - }, - { - id: 4, - cardName: '예비 법인카드', - cardCompany: '신한카드', - cardNumber: '9876-5555-4444-3333', - cardType: 'credit', - paymentDay: 10, - creditLimit: 3000000, - currentUsage: 0, - holder: '최관리', - status: 'inactive', - memo: '비상용' - } - ]); + // 카드 목록 데이터 (빈 배열로 시작 - 실제 데이터는 서버 연동 후 로드) + const [cards, setCards] = useState([]); const [searchTerm, setSearchTerm] = useState(''); const [filterStatus, setFilterStatus] = useState('all'); @@ -120,7 +67,10 @@ function CorporateCardsManagement() { cardType: 'credit', paymentDay: 15, creditLimit: '', - holder: '', + cardHolderName: '', + actualUser: '', + expiryDate: '', + cvc: '', status: 'active', memo: '' }; @@ -158,7 +108,8 @@ function CorporateCardsManagement() { const filteredCards = cards.filter(card => { const matchesSearch = card.cardName.toLowerCase().includes(searchTerm.toLowerCase()) || card.cardCompany.toLowerCase().includes(searchTerm.toLowerCase()) || - card.holder.toLowerCase().includes(searchTerm.toLowerCase()); + card.actualUser.toLowerCase().includes(searchTerm.toLowerCase()) || + (card.cardHolderName && card.cardHolderName.toLowerCase().includes(searchTerm.toLowerCase())); const matchesStatus = filterStatus === 'all' || card.status === filterStatus; return matchesSearch && matchesStatus; }); @@ -181,7 +132,10 @@ function CorporateCardsManagement() { cardType: card.cardType, paymentDay: card.paymentDay, creditLimit: card.creditLimit, - holder: card.holder, + cardHolderName: card.cardHolderName || '', + actualUser: card.actualUser || '', + expiryDate: card.expiryDate || '', + cvc: card.cvc || '', status: card.status, memo: card.memo }); @@ -190,7 +144,7 @@ function CorporateCardsManagement() { // 카드 저장 const handleSaveCard = () => { - if (!formData.cardName || !formData.cardNumber || !formData.holder) { + if (!formData.cardName || !formData.cardNumber || !formData.cardHolderName || !formData.actualUser) { alert('필수 항목을 입력해주세요.'); return; } @@ -215,9 +169,22 @@ function CorporateCardsManagement() { setEditingCard(null); }; - // 카드 삭제 - const handleDeleteCard = (id) => { - if (confirm('정말 삭제하시겠습니까?')) { + // 카드 비활성화 (소프트 삭제) + const handleDeactivateCard = (id) => { + if (confirm('카드를 비활성화하시겠습니까?\n(목록에서 숨겨지지만 데이터는 유지됩니다)')) { + setCards(prev => prev.map(card => + card.id === id ? { ...card, status: 'inactive' } : card + )); + if (showModal) { + setShowModal(false); + setEditingCard(null); + } + } + }; + + // 카드 영구삭제 + const handlePermanentDeleteCard = (id) => { + if (confirm('⚠️ 카드를 영구 삭제하시겠습니까?\n\n이 작업은 되돌릴 수 없습니다.')) { setCards(prev => prev.filter(card => card.id !== id)); if (showModal) { setShowModal(false); @@ -312,7 +279,7 @@ className="flex items-center gap-2 px-4 py-2 bg-violet-600 hover:bg-violet-700 t setSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-violet-500 focus:border-violet-500" @@ -380,8 +347,16 @@ className={`bg-white rounded-xl border-2 p-6 cursor-pointer transition-all hover
-

사용자

-

{card.holder}

+

이용자명

+

{card.cardHolderName || '-'}

+
+
+

실사용자

+

{card.actualUser}

+
+
+

유효기간(년도/월)

+

{card.expiryDate || '-'}

카드종류

@@ -487,28 +462,60 @@ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus
-
- - setFormData(prev => ({ ...prev, cardNumber: e.target.value }))} - placeholder="1234-5678-9012-3456" - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-violet-500 font-mono" - /> -
-
- + setFormData(prev => ({ ...prev, holder: e.target.value }))} - placeholder="카드 사용자명" + value={formData.cardNumber} + onChange={(e) => setFormData(prev => ({ ...prev, cardNumber: e.target.value }))} + placeholder="1234-5678-9012-3456" + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-violet-500 font-mono" + /> +
+
+ + setFormData(prev => ({ ...prev, cardHolderName: e.target.value }))} + placeholder="카드 명의자 (법인명)" className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-violet-500" />
+
+ +
+
+ + { + let val = e.target.value.replace(/[^\d]/g, ''); + if (val.length > 4) val = val.slice(0, 4); + if (val.length >= 2) val = val.slice(0, 2) + '/' + val.slice(2); + setFormData(prev => ({ ...prev, expiryDate: val })); + }} + placeholder="YY/MM" + maxLength={5} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-violet-500 font-mono text-center" + /> +
+
+ + { + const val = e.target.value.replace(/[^\d]/g, '').slice(0, 3); + setFormData(prev => ({ ...prev, cvc: val })); + }} + placeholder="3자리" + maxLength={3} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-violet-500 font-mono text-center" + /> +
setFormData(prev => ({ ...prev, actualUser: e.target.value }))} + placeholder="실제 카드 사용자명" + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-violet-500" + /> +
+ {formData.cardType === 'credit' && (
@@ -563,12 +581,22 @@ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus
{modalMode === 'edit' && ( - + <> + + + )}