Merge remote-tracking branch 'origin/develop' into develop
This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
372
claudedocs/수당지급.md
Normal file
372
claudedocs/수당지급.md
Normal file
@@ -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 |
|
||||
|
||||
---
|
||||
|
||||
> **참고:** 이 문서는 수당 관련 기능 개발 시 기준 문서로 사용됩니다.
|
||||
> 수당 정책 변경 시 반드시 이 문서를 먼저 업데이트하세요.
|
||||
@@ -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 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
<Search className="w-5 h-5 text-gray-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="카드명, 카드사, 사용자 검색..."
|
||||
placeholder="카드명, 카드사, 이용자명, 실사용자 검색..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => 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
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mb-4 text-sm">
|
||||
<div>
|
||||
<p className="text-gray-500">사용자</p>
|
||||
<p className="font-medium text-gray-900">{card.holder}</p>
|
||||
<p className="text-gray-500">이용자명</p>
|
||||
<p className="font-medium text-gray-900">{card.cardHolderName || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-500">실사용자</p>
|
||||
<p className="font-medium text-gray-900">{card.actualUser}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-500">유효기간(년도/월)</p>
|
||||
<p className="font-medium text-gray-900">{card.expiryDate || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-500">카드종류</p>
|
||||
@@ -487,28 +462,60 @@ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">카드번호 *</label>
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">사용자 *</label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">카드번호 *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.holder}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">이용자명 *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.cardHolderName}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">유효기간(년도/월)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.expiryDate}
|
||||
onChange={(e) => {
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">CVC</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.cvc}
|
||||
onChange={(e) => {
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">상태</label>
|
||||
<select
|
||||
@@ -522,6 +529,17 @@ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">실사용자 *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.actualUser}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{formData.cardType === 'credit' && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
@@ -563,12 +581,22 @@ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus
|
||||
|
||||
<div className="flex gap-3 mt-6">
|
||||
{modalMode === 'edit' && (
|
||||
<button
|
||||
onClick={() => handleDeleteCard(editingCard.id)}
|
||||
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg flex items-center gap-2"
|
||||
>
|
||||
<span>🗑️</span> 삭제
|
||||
</button>
|
||||
<>
|
||||
<button
|
||||
onClick={() => handleDeactivateCard(editingCard.id)}
|
||||
className="px-4 py-2 bg-gray-500 hover:bg-gray-600 text-white rounded-lg flex items-center gap-2"
|
||||
title="비활성화 (데이터 유지)"
|
||||
>
|
||||
<span>🚫</span> 비활성화
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handlePermanentDeleteCard(editingCard.id)}
|
||||
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg flex items-center gap-2"
|
||||
title="영구삭제 (복구불가)"
|
||||
>
|
||||
<span>🗑️</span> 영구삭제
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
onClick={() => { setShowModal(false); setEditingCard(null); }}
|
||||
|
||||
96
서버작업이력.md
96
서버작업이력.md
@@ -423,3 +423,99 @@ ## 참고: Docker vs 서버 경로 차이
|
||||
| 공유 스토리지 | `/var/www/shared-storage/` | `/home/webservice/shared-storage/` |
|
||||
|
||||
서버에 새로운 설정을 추가할 때는 경로 차이를 반드시 확인해야 합니다.
|
||||
|
||||
---
|
||||
|
||||
### 14. 영업관리 - 대시보드 메뉴 정렬 순서 수정
|
||||
|
||||
**문제**: 대시보드 메뉴가 "영업담당자 관리" 아래에 표시됨 (가장 위에 있어야 함)
|
||||
|
||||
**작업 내용** (tinker 사용):
|
||||
```bash
|
||||
cd /home/webservice/mng
|
||||
php artisan tinker --execute='
|
||||
use App\Models\Commons\Menu;
|
||||
|
||||
// 대시보드를 sort_order -1로 설정 (가장 위)
|
||||
Menu::where("tenant_id", 1)->where("parent_id", 15385)->where("url", "/sales/salesmanagement/dashboard")->update(["sort_order" => -1]);
|
||||
|
||||
// 결과 확인
|
||||
$children = Menu::where("tenant_id", 1)->where("parent_id", 15385)->orderBy("sort_order")->get();
|
||||
foreach ($children as $c) {
|
||||
echo $c->name . " | sort:" . $c->sort_order . PHP_EOL;
|
||||
}
|
||||
'
|
||||
```
|
||||
|
||||
**결과**:
|
||||
```
|
||||
대시보드 | sort:-1
|
||||
영업담당자 관리 | sort:0
|
||||
가망고객 관리 | sort:1
|
||||
영업실적 관리 | sort:2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 15. 영업관리 - 상품관리 메뉴 추가
|
||||
|
||||
**문제**: 로컬에는 있는 "영업관리 - 상품관리" 메뉴가 서버에 없음
|
||||
|
||||
**작업 내용** (tinker 사용):
|
||||
```bash
|
||||
cd /home/webservice/mng
|
||||
php artisan tinker --execute='
|
||||
use App\Models\Commons\Menu;
|
||||
|
||||
$parentId = 15385;
|
||||
$m = Menu::create([
|
||||
"tenant_id" => 1,
|
||||
"parent_id" => $parentId,
|
||||
"name" => "상품관리",
|
||||
"url" => "/sales/products",
|
||||
"is_active" => true,
|
||||
"sort_order" => 4,
|
||||
"hidden" => false,
|
||||
"icon" => "cube"
|
||||
]);
|
||||
echo "Created: " . $m->id;
|
||||
'
|
||||
```
|
||||
|
||||
**결과**: Menu ID 15400 생성
|
||||
|
||||
---
|
||||
|
||||
### 16. 상품 데이터 추가 (카테고리 + 상품)
|
||||
|
||||
**문제**: 서버에 상품 데이터가 없음 (로컬에만 존재)
|
||||
|
||||
**작업 내용** (tinker 사용):
|
||||
```bash
|
||||
cd /home/webservice/mng
|
||||
php artisan tinker --execute='
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
// 카테고리 추가
|
||||
$categories = [
|
||||
["id" => 5, "code" => "MANUFACTURER", "name" => "제조 업체", ...],
|
||||
["id" => 6, "code" => "CONSTRUCTION", "name" => "공사 업체", ...],
|
||||
];
|
||||
foreach ($categories as $cat) {
|
||||
DB::table("sales_product_categories")->insert($cat);
|
||||
}
|
||||
|
||||
// 상품 추가 (11개)
|
||||
$products = [
|
||||
// 제조업 상품 8개 (MFG_BASIC, MFG_QUALITY, MFG_PROCESS_ADD, MFG_AI, MFG_PHOTO, MFG_INVOICE_CARD, MFG_EQUIPMENT, MFG_RND)
|
||||
// 공사업 상품 3개 (CON_BASIC, CON_AI, CON_PHOTO)
|
||||
];
|
||||
foreach ($products as $prod) {
|
||||
DB::table("sales_products")->insert($prod);
|
||||
}
|
||||
'
|
||||
```
|
||||
|
||||
**결과**:
|
||||
- 카테고리 2개 생성 (제조 업체, 공사 업체)
|
||||
- 상품 11개 생성 (제조업 8개, 공사업 3개)
|
||||
|
||||
Reference in New Issue
Block a user