- soft delete된 레코드가 있을 때 새 레코드 생성 대신 복원 - withTrashed()로 기존 레코드 확인 후 처리 - 불필요한 역할만 선별적으로 삭제 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
592 lines
18 KiB
PHP
592 lines
18 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Sales;
|
|
|
|
use App\Models\Department;
|
|
use App\Models\DepartmentUser;
|
|
use App\Models\Role;
|
|
use App\Models\Sales\SalesManagerDocument;
|
|
use App\Models\User;
|
|
use App\Models\UserRole;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
|
|
class SalesManagerService
|
|
{
|
|
/**
|
|
* 영업파트너 역할 이름 목록
|
|
*/
|
|
public const SALES_ROLES = ['sales', 'manager', 'recruiter'];
|
|
|
|
/**
|
|
* 영업파트너 생성
|
|
*/
|
|
public function createSalesPartner(array $data, array $documents = []): User
|
|
{
|
|
return DB::transaction(function () use ($data, $documents) {
|
|
$tenantId = session('selected_tenant_id', 1);
|
|
|
|
// 1. 사용자 생성
|
|
$user = User::create([
|
|
'user_id' => $data['user_id'] ?? null,
|
|
'name' => $data['name'],
|
|
'email' => $data['email'],
|
|
'phone' => $data['phone'] ?? null,
|
|
'password' => Hash::make($data['password']),
|
|
'is_active' => false, // 승인 전까지 비활성
|
|
'parent_id' => $data['parent_id'] ?? null, // 추천인(유치자)
|
|
'approval_status' => 'pending', // 승인 대기 상태
|
|
'must_change_password' => true,
|
|
]);
|
|
|
|
// 2. 테넌트 연결
|
|
$user->tenants()->attach($tenantId, [
|
|
'is_active' => true,
|
|
'is_default' => true,
|
|
'joined_at' => now(),
|
|
]);
|
|
|
|
// 3. 역할 할당
|
|
if (!empty($data['role_ids'])) {
|
|
$this->syncRoles($user, $tenantId, $data['role_ids']);
|
|
}
|
|
|
|
// 4. 영업팀 부서 자동 할당
|
|
$this->assignSalesDepartment($user, $tenantId);
|
|
|
|
// 5. 첨부 서류 저장
|
|
if (!empty($documents)) {
|
|
$this->uploadDocuments($user, $tenantId, $documents);
|
|
}
|
|
|
|
return $user;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 영업파트너 수정
|
|
*/
|
|
public function updateSalesPartner(User $user, array $data, array $documents = []): User
|
|
{
|
|
return DB::transaction(function () use ($user, $data, $documents) {
|
|
$tenantId = session('selected_tenant_id', 1);
|
|
|
|
// 1. 기본 정보 업데이트 (parent_id는 변경 불가)
|
|
$updateData = [
|
|
'name' => $data['name'],
|
|
'email' => $data['email'],
|
|
'phone' => $data['phone'] ?? null,
|
|
];
|
|
|
|
// 비밀번호 변경 시에만 업데이트
|
|
if (!empty($data['password'])) {
|
|
$updateData['password'] = Hash::make($data['password']);
|
|
}
|
|
|
|
$user->update($updateData);
|
|
|
|
// 2. 역할 동기화
|
|
if (isset($data['role_ids'])) {
|
|
$this->syncRoles($user, $tenantId, $data['role_ids']);
|
|
}
|
|
|
|
// 3. 새 첨부 서류 저장
|
|
if (!empty($documents)) {
|
|
$this->uploadDocuments($user, $tenantId, $documents);
|
|
}
|
|
|
|
return $user->fresh();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 영업파트너 승인
|
|
*/
|
|
public function approve(User $user, int $approverId): User
|
|
{
|
|
$user->update([
|
|
'approval_status' => 'approved',
|
|
'approved_by' => $approverId,
|
|
'approved_at' => now(),
|
|
'is_active' => true,
|
|
'rejection_reason' => null,
|
|
]);
|
|
|
|
return $user;
|
|
}
|
|
|
|
/**
|
|
* 영업파트너 반려
|
|
*/
|
|
public function reject(User $user, int $approverId, string $reason): User
|
|
{
|
|
$user->update([
|
|
'approval_status' => 'rejected',
|
|
'approved_by' => $approverId,
|
|
'approved_at' => now(),
|
|
'rejection_reason' => $reason,
|
|
'is_active' => false,
|
|
]);
|
|
|
|
return $user;
|
|
}
|
|
|
|
/**
|
|
* 역할 위임
|
|
*
|
|
* @param User $fromUser 역할을 넘기는 파트너
|
|
* @param User $toUser 역할을 받는 파트너
|
|
* @param string $roleName 위임할 역할 (manager, recruiter 등)
|
|
*/
|
|
public function delegateRole(User $fromUser, User $toUser, string $roleName): bool
|
|
{
|
|
$tenantId = session('selected_tenant_id', 1);
|
|
|
|
return DB::transaction(function () use ($fromUser, $toUser, $roleName, $tenantId) {
|
|
// 역할 조회
|
|
$role = Role::where('tenant_id', $tenantId)
|
|
->where('name', $roleName)
|
|
->first();
|
|
|
|
if (!$role) {
|
|
throw new \InvalidArgumentException("역할을 찾을 수 없습니다: {$roleName}");
|
|
}
|
|
|
|
// fromUser가 해당 역할을 가지고 있는지 확인
|
|
$hasRole = UserRole::where('user_id', $fromUser->id)
|
|
->where('tenant_id', $tenantId)
|
|
->where('role_id', $role->id)
|
|
->exists();
|
|
|
|
if (!$hasRole) {
|
|
throw new \InvalidArgumentException("{$fromUser->name}님이 {$roleName} 역할을 보유하고 있지 않습니다.");
|
|
}
|
|
|
|
// fromUser에서 역할 제거
|
|
UserRole::where('user_id', $fromUser->id)
|
|
->where('tenant_id', $tenantId)
|
|
->where('role_id', $role->id)
|
|
->delete();
|
|
|
|
// toUser에게 역할 추가 (이미 있으면 무시)
|
|
UserRole::firstOrCreate([
|
|
'user_id' => $toUser->id,
|
|
'tenant_id' => $tenantId,
|
|
'role_id' => $role->id,
|
|
], [
|
|
'assigned_at' => now(),
|
|
]);
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 역할 부여
|
|
*/
|
|
public function assignRole(User $user, string $roleName): bool
|
|
{
|
|
$tenantId = session('selected_tenant_id', 1);
|
|
|
|
$role = Role::where('tenant_id', $tenantId)
|
|
->where('name', $roleName)
|
|
->first();
|
|
|
|
if (!$role) {
|
|
return false;
|
|
}
|
|
|
|
UserRole::firstOrCreate([
|
|
'user_id' => $user->id,
|
|
'tenant_id' => $tenantId,
|
|
'role_id' => $role->id,
|
|
], [
|
|
'assigned_at' => now(),
|
|
]);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 역할 제거
|
|
*/
|
|
public function removeRole(User $user, string $roleName): bool
|
|
{
|
|
$tenantId = session('selected_tenant_id', 1);
|
|
|
|
$role = Role::where('tenant_id', $tenantId)
|
|
->where('name', $roleName)
|
|
->first();
|
|
|
|
if (!$role) {
|
|
return false;
|
|
}
|
|
|
|
UserRole::where('user_id', $user->id)
|
|
->where('tenant_id', $tenantId)
|
|
->where('role_id', $role->id)
|
|
->delete();
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 영업팀 부서 자동 할당
|
|
*/
|
|
private function assignSalesDepartment(User $user, int $tenantId): void
|
|
{
|
|
// "영업팀" 부서를 찾거나 생성
|
|
$salesDepartment = Department::firstOrCreate(
|
|
[
|
|
'tenant_id' => $tenantId,
|
|
'name' => '영업팀',
|
|
],
|
|
[
|
|
'code' => 'SALES',
|
|
'description' => '영업파트너 부서',
|
|
'is_active' => true,
|
|
'sort_order' => 100,
|
|
'created_by' => auth()->id(),
|
|
]
|
|
);
|
|
|
|
// 사용자-부서 연결 (이미 있으면 무시)
|
|
DepartmentUser::firstOrCreate(
|
|
[
|
|
'tenant_id' => $tenantId,
|
|
'department_id' => $salesDepartment->id,
|
|
'user_id' => $user->id,
|
|
],
|
|
[
|
|
'is_primary' => true,
|
|
'joined_at' => now(),
|
|
'created_by' => auth()->id(),
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 역할 동기화
|
|
*/
|
|
public function syncRoles(User $user, int $tenantId, array $roleIds): void
|
|
{
|
|
// 영업 관련 역할만 처리 (다른 역할은 유지)
|
|
$salesRoleIds = Role::where('tenant_id', $tenantId)
|
|
->whereIn('name', self::SALES_ROLES)
|
|
->pluck('id')
|
|
->toArray();
|
|
|
|
// 제거할 역할 soft delete (새 역할 목록에 없는 것들)
|
|
$roleIdsToRemove = array_diff($salesRoleIds, $roleIds);
|
|
if (!empty($roleIdsToRemove)) {
|
|
UserRole::where('user_id', $user->id)
|
|
->where('tenant_id', $tenantId)
|
|
->whereIn('role_id', $roleIdsToRemove)
|
|
->delete();
|
|
}
|
|
|
|
// 새 역할 추가 (이미 있으면 복원, 없으면 생성)
|
|
foreach ($roleIds as $roleId) {
|
|
// soft delete된 레코드 포함하여 검색
|
|
$existingRole = UserRole::withTrashed()
|
|
->where('user_id', $user->id)
|
|
->where('tenant_id', $tenantId)
|
|
->where('role_id', $roleId)
|
|
->first();
|
|
|
|
if ($existingRole) {
|
|
// 이미 존재하면 복원 (soft delete된 경우)
|
|
if ($existingRole->trashed()) {
|
|
$existingRole->restore();
|
|
}
|
|
} else {
|
|
// 새로 생성
|
|
UserRole::create([
|
|
'user_id' => $user->id,
|
|
'tenant_id' => $tenantId,
|
|
'role_id' => $roleId,
|
|
'assigned_at' => now(),
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 서류 업로드
|
|
*/
|
|
public function uploadDocuments(User $user, int $tenantId, array $documents): array
|
|
{
|
|
$uploaded = [];
|
|
|
|
foreach ($documents as $doc) {
|
|
if (!isset($doc['file']) || !$doc['file'] instanceof UploadedFile) {
|
|
continue;
|
|
}
|
|
|
|
$file = $doc['file'];
|
|
$documentType = $doc['document_type'] ?? 'other';
|
|
$description = $doc['description'] ?? null;
|
|
|
|
// 파일 저장
|
|
$storedName = Str::uuid() . '.' . $file->getClientOriginalExtension();
|
|
$filePath = "sales-partners/{$user->id}/{$storedName}";
|
|
|
|
Storage::disk('tenant')->put($filePath, file_get_contents($file));
|
|
|
|
// DB 저장
|
|
$document = SalesManagerDocument::create([
|
|
'tenant_id' => $tenantId,
|
|
'user_id' => $user->id,
|
|
'file_path' => $filePath,
|
|
'original_name' => $file->getClientOriginalName(),
|
|
'stored_name' => $storedName,
|
|
'mime_type' => $file->getMimeType(),
|
|
'file_size' => $file->getSize(),
|
|
'document_type' => $documentType,
|
|
'description' => $description,
|
|
'uploaded_by' => auth()->id(),
|
|
]);
|
|
|
|
$uploaded[] = $document;
|
|
}
|
|
|
|
return $uploaded;
|
|
}
|
|
|
|
/**
|
|
* 서류 삭제
|
|
*/
|
|
public function deleteDocument(SalesManagerDocument $document): bool
|
|
{
|
|
// 물리 파일 삭제
|
|
if ($document->existsInStorage()) {
|
|
Storage::disk('tenant')->delete($document->file_path);
|
|
}
|
|
|
|
// DB 삭제
|
|
$document->deleted_by = auth()->id();
|
|
$document->save();
|
|
|
|
return $document->delete();
|
|
}
|
|
|
|
/**
|
|
* 영업파트너 목록 조회
|
|
*/
|
|
public function getSalesPartners(array $filters = [])
|
|
{
|
|
$tenantId = session('selected_tenant_id', 1);
|
|
|
|
// 영업 관련 역할을 가진 사용자 조회
|
|
$query = User::query()
|
|
->whereHas('userRoles', function ($q) use ($tenantId) {
|
|
$q->where('tenant_id', $tenantId)
|
|
->whereHas('role', function ($rq) {
|
|
$rq->whereIn('name', self::SALES_ROLES);
|
|
});
|
|
})
|
|
->with(['parent', 'userRoles.role', 'salesDocuments']);
|
|
|
|
// 검색
|
|
if (!empty($filters['search'])) {
|
|
$search = $filters['search'];
|
|
$query->where(function ($q) use ($search) {
|
|
$q->where('name', 'like', "%{$search}%")
|
|
->orWhere('user_id', 'like', "%{$search}%")
|
|
->orWhere('email', 'like', "%{$search}%")
|
|
->orWhere('phone', 'like', "%{$search}%");
|
|
});
|
|
}
|
|
|
|
// 역할 필터
|
|
if (!empty($filters['role'])) {
|
|
$roleName = $filters['role'];
|
|
$query->whereHas('userRoles', function ($q) use ($tenantId, $roleName) {
|
|
$q->where('tenant_id', $tenantId)
|
|
->whereHas('role', function ($rq) use ($roleName) {
|
|
$rq->where('name', $roleName);
|
|
});
|
|
});
|
|
}
|
|
|
|
// 승인 상태 필터
|
|
if (!empty($filters['approval_status'])) {
|
|
$query->where('approval_status', $filters['approval_status']);
|
|
}
|
|
|
|
// 유치자(추천인) 필터 - 현재 로그인한 사용자가 유치한 파트너만
|
|
if (!empty($filters['parent_id'])) {
|
|
$query->where('parent_id', $filters['parent_id']);
|
|
}
|
|
|
|
return $query->orderBy('name');
|
|
}
|
|
|
|
/**
|
|
* 추천인(유치자) 후보 목록
|
|
* 승인된 모든 영업파트너가 추천인이 될 수 있음
|
|
*/
|
|
public function getRecommenderCandidates(?int $excludeId = null)
|
|
{
|
|
$tenantId = session('selected_tenant_id', 1);
|
|
|
|
$query = User::query()
|
|
->where('is_active', true)
|
|
->where('approval_status', 'approved')
|
|
->whereHas('userRoles', function ($q) use ($tenantId) {
|
|
$q->where('tenant_id', $tenantId)
|
|
->whereHas('role', function ($rq) {
|
|
$rq->whereIn('name', self::SALES_ROLES);
|
|
});
|
|
});
|
|
|
|
if ($excludeId) {
|
|
$query->where('id', '!=', $excludeId);
|
|
}
|
|
|
|
return $query->orderBy('name')->get();
|
|
}
|
|
|
|
/**
|
|
* 역할 위임 가능한 하위 파트너 목록
|
|
*/
|
|
public function getDelegationCandidates(User $user)
|
|
{
|
|
return User::query()
|
|
->where('parent_id', $user->id)
|
|
->where('is_active', true)
|
|
->where('approval_status', 'approved')
|
|
->orderBy('name')
|
|
->get();
|
|
}
|
|
|
|
/**
|
|
* 영업 역할 목록 조회
|
|
*/
|
|
public function getSalesRoles()
|
|
{
|
|
$tenantId = session('selected_tenant_id', 1);
|
|
|
|
return Role::where('tenant_id', $tenantId)
|
|
->whereIn('name', self::SALES_ROLES)
|
|
->get();
|
|
}
|
|
|
|
/**
|
|
* 통계 조회
|
|
*/
|
|
public function getStats(?int $parentId = null): array
|
|
{
|
|
$tenantId = session('selected_tenant_id', 1);
|
|
|
|
$baseQuery = User::query()
|
|
->whereHas('userRoles', function ($q) use ($tenantId) {
|
|
$q->where('tenant_id', $tenantId)
|
|
->whereHas('role', function ($rq) {
|
|
$rq->whereIn('name', self::SALES_ROLES);
|
|
});
|
|
});
|
|
|
|
// 유치자 기준 필터
|
|
if ($parentId) {
|
|
$baseQuery->where('parent_id', $parentId);
|
|
}
|
|
|
|
return [
|
|
'total' => (clone $baseQuery)->count(),
|
|
'pending' => (clone $baseQuery)->where('approval_status', 'pending')->count(),
|
|
'approved' => (clone $baseQuery)->where('approval_status', 'approved')->count(),
|
|
'sales' => (clone $baseQuery)
|
|
->whereHas('userRoles.role', fn($q) => $q->where('name', 'sales'))
|
|
->count(),
|
|
'manager' => (clone $baseQuery)
|
|
->whereHas('userRoles.role', fn($q) => $q->where('name', 'manager'))
|
|
->count(),
|
|
'recruiter' => (clone $baseQuery)
|
|
->whereHas('userRoles.role', fn($q) => $q->where('name', 'recruiter'))
|
|
->count(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 승인 대기 통계 조회 (본사 관리자용)
|
|
*/
|
|
public function getApprovalStats(): array
|
|
{
|
|
$tenantId = session('selected_tenant_id', 1);
|
|
|
|
$baseQuery = User::query()
|
|
->whereHas('userRoles', function ($q) use ($tenantId) {
|
|
$q->where('tenant_id', $tenantId)
|
|
->whereHas('role', function ($rq) {
|
|
$rq->whereIn('name', self::SALES_ROLES);
|
|
});
|
|
});
|
|
|
|
return [
|
|
'pending' => (clone $baseQuery)->where('approval_status', 'pending')->count(),
|
|
'approved_today' => (clone $baseQuery)
|
|
->where('approval_status', 'approved')
|
|
->whereDate('approved_at', today())
|
|
->count(),
|
|
'rejected_today' => (clone $baseQuery)
|
|
->where('approval_status', 'rejected')
|
|
->whereDate('approved_at', today())
|
|
->count(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 파트너의 계층 레벨 계산
|
|
*/
|
|
public function getPartnerLevel(User $user): int
|
|
{
|
|
$level = 1;
|
|
$current = $user;
|
|
|
|
while ($current->parent_id !== null) {
|
|
$level++;
|
|
$current = $current->parent;
|
|
|
|
// 무한 루프 방지
|
|
if ($level > 100) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $level;
|
|
}
|
|
|
|
/**
|
|
* 하위 파트너 전체 목록 (재귀)
|
|
*/
|
|
public function getAllDescendants(User $user, int $maxDepth = 10): array
|
|
{
|
|
$descendants = [];
|
|
$this->collectDescendants($user, $descendants, 1, $maxDepth);
|
|
return $descendants;
|
|
}
|
|
|
|
private function collectDescendants(User $user, array &$descendants, int $currentDepth, int $maxDepth): void
|
|
{
|
|
if ($currentDepth > $maxDepth) {
|
|
return;
|
|
}
|
|
|
|
$children = User::where('parent_id', $user->id)
|
|
->where('approval_status', 'approved')
|
|
->get();
|
|
|
|
foreach ($children as $child) {
|
|
$descendants[] = [
|
|
'user' => $child,
|
|
'level' => $currentDepth,
|
|
];
|
|
$this->collectDescendants($child, $descendants, $currentDepth + 1, $maxDepth);
|
|
}
|
|
}
|
|
}
|