Files
sam-manage/app/Services/Sales/SalesManagerService.php
pro c6f509c78c feat:영업담당자 User 모듈 통합 및 승인 시스템 구현
- SalesManagerController: User 시스템 기반으로 재구현
- SalesManagerService: 영업담당자 CRUD, 승인/반려 로직
- SalesManagerDocument: 멀티파일 업로드 모델
- User 모델에 parent, approval 관계 및 메서드 추가
- SalesRoleSeeder: 영업 역할 시더 (sales_operator, sales_admin, sales_manager)
- 뷰 파일 전면 수정 (역할 체크박스, 멀티파일 업로드, 승인/반려 UI)
- 라우트 추가 (approve, reject, documents)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 20:06:51 +09:00

345 lines
11 KiB
PHP

<?php
namespace App\Services\Sales;
use App\Models\DepartmentUser;
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 function createSalesManager(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. 부서 할당
if (!empty($data['department_ids'])) {
$this->syncDepartments($user, $tenantId, $data['department_ids']);
}
// 5. 첨부 서류 저장
if (!empty($documents)) {
$this->uploadDocuments($user, $tenantId, $documents);
}
return $user;
});
}
/**
* 영업담당자 수정
*/
public function updateSalesManager(User $user, array $data, array $documents = []): User
{
return DB::transaction(function () use ($user, $data, $documents) {
$tenantId = session('selected_tenant_id', 1);
// 1. 기본 정보 업데이트
$updateData = [
'name' => $data['name'],
'email' => $data['email'],
'phone' => $data['phone'] ?? null,
'parent_id' => $data['parent_id'] ?? 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 (isset($data['department_ids'])) {
$this->syncDepartments($user, $tenantId, $data['department_ids']);
}
// 4. 새 첨부 서류 저장
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;
}
/**
* 역할 동기화
*/
public function syncRoles(User $user, int $tenantId, array $roleIds): void
{
// 기존 역할 삭제
UserRole::withTrashed()
->where('user_id', $user->id)
->where('tenant_id', $tenantId)
->forceDelete();
// 새 역할 추가
foreach ($roleIds as $roleId) {
UserRole::create([
'user_id' => $user->id,
'tenant_id' => $tenantId,
'role_id' => $roleId,
'assigned_at' => now(),
]);
}
}
/**
* 부서 동기화
*/
public function syncDepartments(User $user, int $tenantId, array $departmentIds): void
{
// 기존 부서 삭제
DepartmentUser::withTrashed()
->where('user_id', $user->id)
->where('tenant_id', $tenantId)
->forceDelete();
// 새 부서 추가
foreach ($departmentIds as $index => $departmentId) {
DepartmentUser::create([
'user_id' => $user->id,
'tenant_id' => $tenantId,
'department_id' => $departmentId,
'is_primary' => $index === 0,
'joined_at' => now(),
'created_by' => auth()->id(),
]);
}
}
/**
* 서류 업로드
*/
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-managers/{$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 getSalesManagers(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', ['sales_operator', 'sales_admin', 'sales_manager']);
});
})
->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']);
}
return $query->orderBy('name');
}
/**
* 상위 관리자 목록 (운영자, 영업관리)
*/
public function getParentCandidates(?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', ['sales_operator', 'sales_admin']);
});
});
if ($excludeId) {
$query->where('id', '!=', $excludeId);
}
return $query->orderBy('name')->get();
}
/**
* 통계 조회
*/
public function getStats(): 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', ['sales_operator', 'sales_admin', 'sales_manager']);
});
});
return [
'total' => (clone $baseQuery)->count(),
'pending' => (clone $baseQuery)->where('approval_status', 'pending')->count(),
'approved' => (clone $baseQuery)->where('approval_status', 'approved')->count(),
'operators' => (clone $baseQuery)
->whereHas('userRoles.role', fn($q) => $q->where('name', 'sales_operator'))
->count(),
'sales_admins' => (clone $baseQuery)
->whereHas('userRoles.role', fn($q) => $q->where('name', 'sales_admin'))
->count(),
'managers' => (clone $baseQuery)
->whereHas('userRoles.role', fn($q) => $q->where('name', 'sales_manager'))
->count(),
];
}
}