copy()->addMonths(TenantProspect::VALIDITY_MONTHS); $cooldownEndsAt = $expiresAt->copy()->addMonths(TenantProspect::COOLDOWN_MONTHS); // 명함 이미지 저장 (파일 업로드 또는 Base64) $businessCardPath = null; if ($businessCard) { $businessCardPath = $this->uploadBusinessCard($businessCard, $data['registered_by']); } elseif ($businessCardBase64) { $businessCardPath = $this->saveBase64Image($businessCardBase64, $data['registered_by']); } return TenantProspect::create([ 'business_number' => $data['business_number'], 'company_name' => $data['company_name'], 'ceo_name' => $data['ceo_name'] ?? null, 'contact_phone' => $data['contact_phone'] ?? null, 'contact_email' => $data['contact_email'] ?? null, 'address' => $data['address'] ?? null, 'registered_by' => $data['registered_by'], 'business_card_path' => $businessCardPath, 'status' => TenantProspect::STATUS_ACTIVE, 'registered_at' => $now, 'expires_at' => $expiresAt, 'cooldown_ends_at' => $cooldownEndsAt, 'memo' => $data['memo'] ?? null, ]); }); } /** * 영업권 정보 수정 */ public function update( TenantProspect $prospect, array $data, ?UploadedFile $businessCard = null, ?UploadedFile $idCard = null, ?UploadedFile $bankbook = null ): TenantProspect { return DB::transaction(function () use ($prospect, $data, $businessCard, $idCard, $bankbook) { $updateData = [ 'company_name' => $data['company_name'], 'ceo_name' => $data['ceo_name'] ?? null, 'contact_phone' => $data['contact_phone'] ?? null, 'contact_email' => $data['contact_email'] ?? null, 'address' => $data['address'] ?? null, 'memo' => $data['memo'] ?? null, ]; // 명함 이미지 교체 if ($businessCard) { if ($prospect->business_card_path) { Storage::disk('tenant')->delete($prospect->business_card_path); } $updateData['business_card_path'] = $this->uploadAttachment($businessCard, $prospect->registered_by); } // 신분증 이미지 교체 if ($idCard) { if ($prospect->id_card_path) { Storage::disk('tenant')->delete($prospect->id_card_path); } $updateData['id_card_path'] = $this->uploadAttachment($idCard, $prospect->registered_by); } // 통장사본 이미지 교체 if ($bankbook) { if ($prospect->bankbook_path) { Storage::disk('tenant')->delete($prospect->bankbook_path); } $updateData['bankbook_path'] = $this->uploadAttachment($bankbook, $prospect->registered_by); } $prospect->update($updateData); return $prospect->fresh(); }); } /** * 테넌트로 전환 */ public function convertToTenant(TenantProspect $prospect, int $convertedBy): Tenant { return DB::transaction(function () use ($prospect, $convertedBy) { // 고유 테넌트 코드 생성 (T + 타임스탬프 + 랜덤) $tenantCode = 'T' . now()->format('ymd') . strtoupper(substr(uniqid(), -4)); // 테넌트 생성 $tenant = Tenant::create([ 'company_name' => $prospect->company_name, 'code' => $tenantCode, 'business_num' => $prospect->business_number, 'ceo_name' => $prospect->ceo_name, 'phone' => $prospect->contact_phone, 'email' => $prospect->contact_email, 'address' => $prospect->address, 'tenant_st_code' => 'trial', 'tenant_type' => 'STD', // STD = Standard (일반 고객) ]); // 전환한 사용자를 테넌트에 연결 (user_tenants) $tenant->users()->attach($convertedBy, [ 'is_active' => true, 'is_default' => false, 'joined_at' => now(), ]); // 영업권 상태 업데이트 $prospect->update([ 'status' => TenantProspect::STATUS_CONVERTED, 'tenant_id' => $tenant->id, 'converted_at' => now(), 'converted_by' => $convertedBy, ]); return $tenant; }); } /** * 영업권 만료 처리 (배치용) */ public function expireOldProspects(): int { return TenantProspect::where('status', TenantProspect::STATUS_ACTIVE) ->where('expires_at', '<=', now()) ->update(['status' => TenantProspect::STATUS_EXPIRED]); } /** * 사업자번호로 등록 가능 여부 확인 */ public function canRegister(string $businessNumber, ?int $excludeId = null): array { $query = TenantProspect::where('business_number', $businessNumber); if ($excludeId) { $query->where('id', '!=', $excludeId); } // 이미 전환된 경우 $converted = (clone $query)->where('status', TenantProspect::STATUS_CONVERTED)->first(); if ($converted) { return [ 'can_register' => false, 'reason' => '이미 테넌트로 전환된 회사입니다.', 'prospect' => $converted, ]; } // 유효한 영업권이 있는 경우 $active = (clone $query)->active()->first(); if ($active) { return [ 'can_register' => false, 'reason' => "이미 {$active->registeredBy->name}님이 영업권을 보유 중입니다. (만료: {$active->expires_at->format('Y-m-d')})", 'prospect' => $active, ]; } // 재등록 대기 중인 경우 $inCooldown = (clone $query) ->where('status', TenantProspect::STATUS_EXPIRED) ->where('cooldown_ends_at', '>', now()) ->first(); if ($inCooldown) { return [ 'can_register' => false, 'reason' => "재등록 대기 기간 중입니다. (등록 가능: {$inCooldown->cooldown_ends_at->format('Y-m-d')})", 'prospect' => $inCooldown, ]; } return [ 'can_register' => true, 'reason' => null, 'prospect' => null, ]; } /** * 목록 조회 */ public function getProspects(array $filters = []) { $query = TenantProspect::with(['registeredBy', 'tenant']); // 검색 if (!empty($filters['search'])) { $search = $filters['search']; $query->where(function ($q) use ($search) { $q->where('company_name', 'like', "%{$search}%") ->orWhere('business_number', 'like', "%{$search}%") ->orWhere('ceo_name', 'like', "%{$search}%") ->orWhere('contact_phone', 'like', "%{$search}%"); }); } // 상태 필터 if (!empty($filters['status'])) { if ($filters['status'] === 'active') { $query->active(); } elseif ($filters['status'] === 'expired') { $query->where('status', TenantProspect::STATUS_EXPIRED); } elseif ($filters['status'] === 'converted') { $query->converted(); } } // 특정 영업파트너 if (!empty($filters['registered_by'])) { $query->byPartner($filters['registered_by']); } return $query->orderBy('created_at', 'desc'); } /** * 통계 조회 */ public function getStats(?int $partnerId = null): array { $baseQuery = TenantProspect::query(); if ($partnerId) { $baseQuery->byPartner($partnerId); } return [ 'total' => (clone $baseQuery)->count(), 'active' => (clone $baseQuery)->active()->count(), 'expired' => (clone $baseQuery)->where('status', TenantProspect::STATUS_EXPIRED)->count(), 'converted' => (clone $baseQuery)->converted()->count(), ]; } /** * 첨부파일 업로드 (명함, 신분증, 통장사본 등) */ private function uploadAttachment(UploadedFile $file, int $userId): string { $storedName = Str::uuid() . '.' . $file->getClientOriginalExtension(); $filePath = "prospects/{$userId}/{$storedName}"; Storage::disk('tenant')->put($filePath, file_get_contents($file)); return $filePath; } /** * 명함 이미지 업로드 (register에서 사용) */ private function uploadBusinessCard(UploadedFile $file, int $userId): string { return $this->uploadAttachment($file, $userId); } /** * Base64 이미지 저장 */ private function saveBase64Image(string $base64Data, int $userId): ?string { // data:image/jpeg;base64,... 형식에서 데이터 추출 if (preg_match('/^data:image\/(\w+);base64,/', $base64Data, $matches)) { $extension = $matches[1]; $base64Data = preg_replace('/^data:image\/\w+;base64,/', '', $base64Data); } else { $extension = 'jpg'; } $imageData = base64_decode($base64Data); if ($imageData === false) { return null; } $storedName = Str::uuid() . '.' . $extension; $filePath = "prospects/{$userId}/{$storedName}"; Storage::disk('tenant')->put($filePath, $imageData); return $filePath; } /** * 명함 이미지 삭제 */ public function deleteBusinessCard(TenantProspect $prospect): bool { return $this->deleteAttachment($prospect, 'business_card'); } /** * 첨부파일 삭제 */ public function deleteAttachment(TenantProspect $prospect, string $type): bool { $pathField = $type . '_path'; if (!$prospect->$pathField) { return false; } if (Storage::disk('tenant')->exists($prospect->$pathField)) { Storage::disk('tenant')->delete($prospect->$pathField); } $prospect->update([$pathField => null]); return true; } }