feat:영업권(명함등록) 시스템 구현

- TenantProspect 모델, 서비스, 컨트롤러 추가
- 명함 등록 시 2개월 영업권 부여
- 만료 후 1개월 쿨다운 기간 적용
- 테넌트 전환 기능 구현
- 사업자번호 중복 체크 API 추가
- 명함 이미지 업로드 지원

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
pro
2026-01-27 22:39:42 +09:00
parent 45a4956e72
commit e04cbcf1e0
9 changed files with 1027 additions and 268 deletions

View File

@@ -0,0 +1,212 @@
<?php
namespace App\Http\Controllers\Sales;
use App\Http\Controllers\Controller;
use App\Models\Sales\TenantProspect;
use App\Services\Sales\TenantProspectService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\View\View;
/**
* 영업권(명함등록) 관리 컨트롤러
*/
class TenantProspectController extends Controller
{
public function __construct(
private TenantProspectService $service
) {}
/**
* 목록 페이지
*/
public function index(Request $request): View|Response
{
if ($request->header('HX-Request')) {
return response('', 200)->header('HX-Redirect', route('sales.prospects.index'));
}
$filters = [
'search' => $request->get('search'),
'status' => $request->get('status'),
'registered_by' => $request->get('registered_by'),
];
$prospects = $this->service->getProspects($filters)->paginate(20);
$stats = $this->service->getStats();
return view('sales.prospects.index', compact('prospects', 'stats'));
}
/**
* 등록 폼
*/
public function create(Request $request): View|Response
{
// HTMX 요청이면 JavaScript 로드를 위해 전체 페이지로 리다이렉트
if ($request->header('HX-Request')) {
return response('', 200)->header('HX-Redirect', route('sales.prospects.create'));
}
return view('sales.prospects.create');
}
/**
* 등록 처리
*/
public function store(Request $request)
{
$validated = $request->validate([
'business_number' => 'required|string|max:20',
'company_name' => 'required|string|max:100',
'ceo_name' => 'nullable|string|max:50',
'contact_phone' => 'nullable|string|max:20',
'contact_email' => 'nullable|email|max:100',
'address' => 'nullable|string|max:500',
'business_card' => 'nullable|image|max:5120',
'memo' => 'nullable|string|max:1000',
]);
// 등록 가능 여부 확인
$checkResult = $this->service->canRegister($validated['business_number']);
if (!$checkResult['can_register']) {
return redirect()->back()
->withInput()
->with('error', $checkResult['reason']);
}
// 등록자는 현재 로그인 사용자
$validated['registered_by'] = auth()->id();
$this->service->register(
$validated,
$request->file('business_card')
);
return redirect()->route('sales.prospects.index')
->with('success', '명함이 등록되었습니다. 2개월간 영업권이 유효합니다.');
}
/**
* 상세 페이지
*/
public function show(int $id): View
{
$prospect = TenantProspect::with(['registeredBy', 'tenant', 'convertedBy'])
->findOrFail($id);
return view('sales.prospects.show', compact('prospect'));
}
/**
* 수정 폼
*/
public function edit(int $id): View
{
$prospect = TenantProspect::findOrFail($id);
// 이미 전환된 경우 수정 불가
if ($prospect->isConverted()) {
return redirect()->route('sales.prospects.show', $id)
->with('error', '이미 테넌트로 전환된 영업권은 수정할 수 없습니다.');
}
return view('sales.prospects.edit', compact('prospect'));
}
/**
* 수정 처리
*/
public function update(Request $request, int $id)
{
$prospect = TenantProspect::findOrFail($id);
// 이미 전환된 경우 수정 불가
if ($prospect->isConverted()) {
return redirect()->route('sales.prospects.show', $id)
->with('error', '이미 테넌트로 전환된 영업권은 수정할 수 없습니다.');
}
$validated = $request->validate([
'company_name' => 'required|string|max:100',
'ceo_name' => 'nullable|string|max:50',
'contact_phone' => 'nullable|string|max:20',
'contact_email' => 'nullable|email|max:100',
'address' => 'nullable|string|max:500',
'business_card' => 'nullable|image|max:5120',
'memo' => 'nullable|string|max:1000',
]);
$this->service->update(
$prospect,
$validated,
$request->file('business_card')
);
return redirect()->route('sales.prospects.show', $id)
->with('success', '영업권 정보가 수정되었습니다.');
}
/**
* 삭제 처리
*/
public function destroy(int $id)
{
$prospect = TenantProspect::findOrFail($id);
// 이미 전환된 경우 삭제 불가
if ($prospect->isConverted()) {
return redirect()->route('sales.prospects.index')
->with('error', '이미 테넌트로 전환된 영업권은 삭제할 수 없습니다.');
}
// 본인 또는 관리자만 삭제 가능
if ($prospect->registered_by !== auth()->id()) {
// TODO: 관리자 권한 체크 추가
}
$prospect->delete();
return redirect()->route('sales.prospects.index')
->with('success', '영업권이 삭제되었습니다.');
}
/**
* 테넌트 전환
*/
public function convert(int $id)
{
$prospect = TenantProspect::findOrFail($id);
// 이미 전환된 경우
if ($prospect->isConverted()) {
return redirect()->route('sales.prospects.show', $id)
->with('error', '이미 테넌트로 전환되었습니다.');
}
// 만료된 경우
if ($prospect->isExpired()) {
return redirect()->route('sales.prospects.show', $id)
->with('error', '만료된 영업권은 전환할 수 없습니다.');
}
$tenant = $this->service->convertToTenant($prospect, auth()->id());
return redirect()->route('sales.prospects.show', $id)
->with('success', "테넌트로 전환되었습니다. (테넌트 ID: {$tenant->id})");
}
/**
* 사업자번호 중복 체크 (AJAX)
*/
public function checkBusinessNumber(Request $request)
{
$businessNumber = $request->get('business_number');
$excludeId = $request->get('exclude_id');
$result = $this->service->canRegister($businessNumber, $excludeId);
return response()->json($result);
}
}

View File

@@ -0,0 +1,225 @@
<?php
namespace App\Models\Sales;
use App\Models\Tenants\Tenant;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Storage;
/**
* 영업파트너 영업권(명함등록) 모델
*/
class TenantProspect extends Model
{
use SoftDeletes;
protected $table = 'tenant_prospects';
public const STATUS_ACTIVE = 'active'; // 영업권 유효
public const STATUS_EXPIRED = 'expired'; // 영업권 만료
public const STATUS_CONVERTED = 'converted'; // 테넌트 전환 완료
public const VALIDITY_MONTHS = 2; // 영업권 유효기간 (개월)
public const COOLDOWN_MONTHS = 1; // 쿨다운 기간 (개월)
protected $fillable = [
'business_number',
'company_name',
'ceo_name',
'contact_phone',
'contact_email',
'address',
'registered_by',
'business_card_path',
'status',
'registered_at',
'expires_at',
'cooldown_ends_at',
'tenant_id',
'converted_at',
'converted_by',
'memo',
];
protected $casts = [
'registered_at' => 'datetime',
'expires_at' => 'datetime',
'cooldown_ends_at' => 'datetime',
'converted_at' => 'datetime',
];
/**
* 등록한 영업파트너
*/
public function registeredBy(): BelongsTo
{
return $this->belongsTo(User::class, 'registered_by');
}
/**
* 전환된 테넌트
*/
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class, 'tenant_id');
}
/**
* 전환 처리자
*/
public function convertedBy(): BelongsTo
{
return $this->belongsTo(User::class, 'converted_by');
}
/**
* 영업권 유효 여부
*/
public function isActive(): bool
{
return $this->status === self::STATUS_ACTIVE && now()->lt($this->expires_at);
}
/**
* 영업권 만료 여부
*/
public function isExpired(): bool
{
return $this->status === self::STATUS_EXPIRED || now()->gte($this->expires_at);
}
/**
* 테넌트 전환 완료 여부
*/
public function isConverted(): bool
{
return $this->status === self::STATUS_CONVERTED;
}
/**
* 쿨다운 중 여부
*/
public function isInCooldown(): bool
{
return $this->isExpired() && now()->lt($this->cooldown_ends_at);
}
/**
* 재등록 가능 여부
*/
public function canReRegister(): bool
{
return $this->isExpired() && now()->gte($this->cooldown_ends_at);
}
/**
* 상태 라벨
*/
public function getStatusLabelAttribute(): string
{
if ($this->isConverted()) {
return '계약완료';
}
if ($this->isActive()) {
return '영업중';
}
if ($this->isInCooldown()) {
return '쿨다운';
}
return '만료';
}
/**
* 상태 색상 (Tailwind CSS)
*/
public function getStatusColorAttribute(): string
{
if ($this->isConverted()) {
return 'bg-green-100 text-green-800';
}
if ($this->isActive()) {
return 'bg-blue-100 text-blue-800';
}
if ($this->isInCooldown()) {
return 'bg-yellow-100 text-yellow-800';
}
return 'bg-gray-100 text-gray-800';
}
/**
* 남은 일수
*/
public function getRemainingDaysAttribute(): int
{
if (!$this->isActive()) {
return 0;
}
return max(0, now()->diffInDays($this->expires_at, false));
}
/**
* 명함 이미지 URL
*/
public function getBusinessCardUrlAttribute(): ?string
{
if (!$this->business_card_path) {
return null;
}
return Storage::disk('tenant')->url($this->business_card_path);
}
/**
* 명함 이미지 존재 여부
*/
public function hasBusinessCard(): bool
{
return $this->business_card_path && Storage::disk('tenant')->exists($this->business_card_path);
}
/**
* 스코프: 유효한 영업권
*/
public function scopeActive($query)
{
return $query->where('status', self::STATUS_ACTIVE)
->where('expires_at', '>', now());
}
/**
* 스코프: 만료된 영업권
*/
public function scopeExpired($query)
{
return $query->where(function ($q) {
$q->where('status', self::STATUS_EXPIRED)
->orWhere('expires_at', '<=', now());
});
}
/**
* 스코프: 전환 완료
*/
public function scopeConverted($query)
{
return $query->where('status', self::STATUS_CONVERTED);
}
/**
* 스코프: 특정 영업파트너의 영업권
*/
public function scopeByPartner($query, int $userId)
{
return $query->where('registered_by', $userId);
}
}

View File

@@ -40,6 +40,14 @@ class Tenant extends Model
'admin_memo',
// 삭제 정보
'deleted_by',
// 영업파트너 영업권 관련
'registered_by',
'business_card_path',
'prospect_status',
'prospect_registered_at',
'prospect_expires_at',
'cooldown_ends_at',
'converted_at',
];
protected $casts = [

View File

@@ -0,0 +1,253 @@
<?php
namespace App\Services\Sales;
use App\Models\Sales\TenantProspect;
use App\Models\Tenants\Tenant;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class TenantProspectService
{
/**
* 명함 등록 (영업권 확보)
*/
public function register(array $data, ?UploadedFile $businessCard = null): TenantProspect
{
return DB::transaction(function () use ($data, $businessCard) {
$now = now();
$expiresAt = $now->copy()->addMonths(TenantProspect::VALIDITY_MONTHS);
$cooldownEndsAt = $expiresAt->copy()->addMonths(TenantProspect::COOLDOWN_MONTHS);
// 명함 이미지 저장
$businessCardPath = null;
if ($businessCard) {
$businessCardPath = $this->uploadBusinessCard($businessCard, $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): TenantProspect
{
return DB::transaction(function () use ($prospect, $data, $businessCard) {
$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->uploadBusinessCard($businessCard, $prospect->registered_by);
}
$prospect->update($updateData);
return $prospect->fresh();
});
}
/**
* 테넌트로 전환
*/
public function convertToTenant(TenantProspect $prospect, int $convertedBy): Tenant
{
return DB::transaction(function () use ($prospect, $convertedBy) {
// 테넌트 생성
$tenant = Tenant::create([
'company_name' => $prospect->company_name,
'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' => 'customer',
'created_by' => $convertedBy,
]);
// 영업권 상태 업데이트
$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 uploadBusinessCard(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;
}
/**
* 명함 이미지 삭제
*/
public function deleteBusinessCard(TenantProspect $prospect): bool
{
if ($prospect->business_card_path && Storage::disk('tenant')->exists($prospect->business_card_path)) {
Storage::disk('tenant')->delete($prospect->business_card_path);
$prospect->update(['business_card_path' => null]);
return true;
}
return false;
}
}

View File

@@ -1,6 +1,6 @@
@extends('layouts.app')
@section('title', '가망고객 등록')
@section('title', '명함 등록')
@section('content')
<div class="max-w-2xl mx-auto">
@@ -12,16 +12,42 @@
</svg>
목록으로
</a>
<h1 class="text-2xl font-bold text-gray-800">가망고객 등록</h1>
<h1 class="text-2xl font-bold text-gray-800">명함 등록</h1>
<p class="text-sm text-gray-500 mt-1">명함 등록 2개월간 영업권이 부여됩니다</p>
</div>
<!-- 알림 메시지 -->
@if(session('error'))
<div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700">
{{ session('error') }}
</div>
@endif
<!-- -->
<form action="{{ route('sales.prospects.store') }}" method="POST" class="bg-white rounded-lg shadow-sm p-6 space-y-6">
<form action="{{ route('sales.prospects.store') }}" method="POST" enctype="multipart/form-data" class="bg-white rounded-lg shadow-sm p-6 space-y-6">
@csrf
<!-- 사업자번호 (중복 체크) -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">사업자번호 <span class="text-red-500">*</span></label>
<div class="flex gap-2">
<input type="text" name="business_number" id="business_number" value="{{ old('business_number') }}" required
placeholder="000-00-00000"
class="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 @error('business_number') border-red-500 @enderror">
<button type="button" id="checkBusinessNumber"
class="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg transition">
중복확인
</button>
</div>
<p id="businessNumberResult" class="mt-1 text-sm"></p>
@error('business_number')
<p class="mt-1 text-sm text-red-500">{{ $message }}</p>
@enderror
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">업체 <span class="text-red-500">*</span></label>
<label class="block text-sm font-medium text-gray-700 mb-2">회사 <span class="text-red-500">*</span></label>
<input type="text" name="company_name" value="{{ old('company_name') }}" required
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 @error('company_name') border-red-500 @enderror">
@error('company_name')
@@ -30,43 +56,24 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">사업자번호</label>
<input type="text" name="business_no" value="{{ old('business_no') }}" placeholder="000-00-00000"
<label class="block text-sm font-medium text-gray-700 mb-2">대표자명</label>
<input type="text" name="ceo_name" value="{{ old('ceo_name') }}"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">대표자명</label>
<input type="text" name="representative" value="{{ old('representative') }}"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">연락처</label>
<input type="text" name="contact_phone" value="{{ old('contact_phone') }}"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">이메일</label>
<input type="email" name="email" value="{{ old('email') }}"
<input type="email" name="contact_email" value="{{ old('contact_email') }}"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">상태 <span class="text-red-500">*</span></label>
<select name="status" required
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="lead" {{ old('status') === 'lead' ? 'selected' : '' }}>리드</option>
<option value="prospect" {{ old('status') === 'prospect' ? 'selected' : '' }}>가망</option>
<option value="negotiation" {{ old('status') === 'negotiation' ? 'selected' : '' }}>협상중</option>
<option value="contracted" {{ old('status') === 'contracted' ? 'selected' : '' }}>계약완료</option>
</select>
</div>
</div>
<div>
@@ -75,35 +82,27 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">영업 담당자 <span class="text-red-500">*</span></label>
<select name="manager_id" required
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 @error('manager_id') border-red-500 @enderror">
<option value="">선택하세요</option>
@foreach($managers as $manager)
<option value="{{ $manager->id }}" {{ old('manager_id') == $manager->id ? 'selected' : '' }}>
{{ $manager->name }} ({{ $manager->role_label }})
</option>
@endforeach
</select>
@error('manager_id')
<p class="mt-1 text-sm text-red-500">{{ $message }}</p>
@enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">명함 이미지</label>
<input type="file" name="business_card" accept="image/*"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<p class="mt-1 text-xs text-gray-500">JPG, PNG 형식 (최대 5MB)</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">담당 매니저</label>
<select name="sales_manager_id"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">선택 안함</option>
@foreach($managers as $manager)
<option value="{{ $manager->id }}" {{ old('sales_manager_id') == $manager->id ? 'selected' : '' }}>
{{ $manager->name }} ({{ $manager->role_label }})
</option>
@endforeach
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">메모</label>
<textarea name="memo" rows="3"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">{{ old('memo') }}</textarea>
</div>
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h3 class="font-medium text-blue-800 mb-2">영업권 안내</h3>
<ul class="text-sm text-blue-700 space-y-1">
<li>등록일로부터 <strong>2개월간</strong> 영업권이 유효합니다.</li>
<li>유효기간 테넌트로 전환 영업 실적으로 인정됩니다.</li>
<li>만료 <strong>1개월간</strong> 쿨다운 기간이 적용됩니다.</li>
<li>쿨다운 이후 다른 영업파트너가 등록할 있습니다.</li>
</ul>
</div>
<div class="flex justify-end gap-3 pt-4 border-t">
@@ -118,4 +117,42 @@ class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
</div>
</form>
</div>
@push('scripts')
<script>
document.getElementById('checkBusinessNumber').addEventListener('click', function() {
const businessNumber = document.getElementById('business_number').value;
const resultEl = document.getElementById('businessNumberResult');
if (!businessNumber) {
resultEl.textContent = '사업자번호를 입력해주세요.';
resultEl.className = 'mt-1 text-sm text-red-500';
return;
}
fetch('{{ route("sales.prospects.check-business-number") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: JSON.stringify({ business_number: businessNumber })
})
.then(response => response.json())
.then(data => {
if (data.can_register) {
resultEl.textContent = '등록 가능한 사업자번호입니다.';
resultEl.className = 'mt-1 text-sm text-green-500';
} else {
resultEl.textContent = data.reason;
resultEl.className = 'mt-1 text-sm text-red-500';
}
})
.catch(error => {
resultEl.textContent = '확인 중 오류가 발생했습니다.';
resultEl.className = 'mt-1 text-sm text-red-500';
});
});
</script>
@endpush
@endsection

View File

@@ -1,29 +1,44 @@
@extends('layouts.app')
@section('title', '가망고객 수정')
@section('title', '영업권 수정')
@section('content')
<div class="max-w-2xl mx-auto">
<!-- 페이지 헤더 -->
<div class="mb-6">
<a href="{{ route('sales.prospects.index') }}" class="text-gray-500 hover:text-gray-700 text-sm mb-2 inline-flex items-center">
<a href="{{ route('sales.prospects.show', $prospect->id) }}" class="text-gray-500 hover:text-gray-700 text-sm mb-2 inline-flex items-center">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
목록으로
상세로 돌아가기
</a>
<h1 class="text-2xl font-bold text-gray-800">가망고객 수정</h1>
<p class="text-sm text-gray-500 mt-1">{{ $prospect->company_name }}</p>
<h1 class="text-2xl font-bold text-gray-800">영업권 수정</h1>
<p class="text-sm text-gray-500 mt-1">{{ $prospect->company_name }} ({{ $prospect->business_number }})</p>
</div>
<!-- 알림 메시지 -->
@if(session('error'))
<div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700">
{{ session('error') }}
</div>
@endif
<!-- -->
<form action="{{ route('sales.prospects.update', $prospect->id) }}" method="POST" class="bg-white rounded-lg shadow-sm p-6 space-y-6">
<form action="{{ route('sales.prospects.update', $prospect->id) }}" method="POST" enctype="multipart/form-data" class="bg-white rounded-lg shadow-sm p-6 space-y-6">
@csrf
@method('PUT')
<!-- 사업자번호 (수정 불가) -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">사업자번호</label>
<input type="text" value="{{ $prospect->business_number }}" disabled
class="w-full px-4 py-2 border border-gray-300 rounded-lg bg-gray-100 text-gray-500">
<p class="mt-1 text-xs text-gray-500">사업자번호는 수정할 없습니다</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">업체 <span class="text-red-500">*</span></label>
<label class="block text-sm font-medium text-gray-700 mb-2">회사 <span class="text-red-500">*</span></label>
<input type="text" name="company_name" value="{{ old('company_name', $prospect->company_name) }}" required
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 @error('company_name') border-red-500 @enderror">
@error('company_name')
@@ -32,44 +47,24 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">사업자번호</label>
<input type="text" name="business_no" value="{{ old('business_no', $prospect->business_no) }}" placeholder="000-00-00000"
<label class="block text-sm font-medium text-gray-700 mb-2">대표자명</label>
<input type="text" name="ceo_name" value="{{ old('ceo_name', $prospect->ceo_name) }}"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">대표자명</label>
<input type="text" name="representative" value="{{ old('representative', $prospect->representative) }}"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">연락처</label>
<input type="text" name="contact_phone" value="{{ old('contact_phone', $prospect->contact_phone) }}"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">이메일</label>
<input type="email" name="email" value="{{ old('email', $prospect->email) }}"
<input type="email" name="contact_email" value="{{ old('contact_email', $prospect->contact_email) }}"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">상태 <span class="text-red-500">*</span></label>
<select name="status" required
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="lead" {{ old('status', $prospect->status) === 'lead' ? 'selected' : '' }}>리드</option>
<option value="prospect" {{ old('status', $prospect->status) === 'prospect' ? 'selected' : '' }}>가망</option>
<option value="negotiation" {{ old('status', $prospect->status) === 'negotiation' ? 'selected' : '' }}>협상중</option>
<option value="contracted" {{ old('status', $prospect->status) === 'contracted' ? 'selected' : '' }}>계약완료</option>
<option value="lost" {{ old('status', $prospect->status) === 'lost' ? 'selected' : '' }}>실패</option>
</select>
</div>
</div>
<div>
@@ -79,20 +74,45 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">담당 매니저</label>
<select name="sales_manager_id"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">선택 안함</option>
@foreach($managers as $manager)
<option value="{{ $manager->id }}" {{ old('sales_manager_id', $prospect->sales_manager_id) == $manager->id ? 'selected' : '' }}>
{{ $manager->name }} ({{ $manager->role_label }})
</option>
@endforeach
</select>
<label class="block text-sm font-medium text-gray-700 mb-2">명함 이미지</label>
@if($prospect->hasBusinessCard())
<div class="mb-2 p-2 bg-gray-50 rounded-lg">
<img src="{{ $prospect->business_card_url }}" alt="현재 명함" class="max-h-32 rounded">
<p class="text-xs text-gray-500 mt-1"> 이미지를 업로드하면 기존 이미지가 교체됩니다</p>
</div>
@endif
<input type="file" name="business_card" accept="image/*"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<p class="mt-1 text-xs text-gray-500">JPG, PNG 형식 (최대 5MB)</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">메모</label>
<textarea name="memo" rows="3"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">{{ old('memo', $prospect->memo) }}</textarea>
</div>
<!-- 영업권 상태 정보 -->
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
<h3 class="font-medium text-gray-800 mb-2">영업권 상태</h3>
<dl class="grid grid-cols-2 gap-2 text-sm">
<dt class="text-gray-500">상태</dt>
<dd class="font-medium">
<span class="px-2 py-1 text-xs font-medium rounded-full {{ $prospect->status_color }}">
{{ $prospect->status_label }}
</span>
</dd>
<dt class="text-gray-500">등록일</dt>
<dd class="font-medium">{{ $prospect->registered_at->format('Y-m-d') }}</dd>
<dt class="text-gray-500">만료일</dt>
<dd class="font-medium">{{ $prospect->expires_at->format('Y-m-d') }}</dd>
<dt class="text-gray-500">등록자</dt>
<dd class="font-medium">{{ $prospect->registeredBy?->name ?? '-' }}</dd>
</dl>
</div>
<div class="flex justify-end gap-3 pt-4 border-t">
<a href="{{ route('sales.prospects.index') }}"
<a href="{{ route('sales.prospects.show', $prospect->id) }}"
class="px-6 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition">
취소
</a>

View File

@@ -1,53 +1,41 @@
@extends('layouts.app')
@section('title', '가망고객 관리')
@section('title', '명함등록 (영업권)')
@section('content')
<div class="flex flex-col h-full">
<!-- 페이지 헤더 -->
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-6 flex-shrink-0">
<div>
<h1 class="text-2xl font-bold text-gray-800">가망고객 관리</h1>
<p class="text-sm text-gray-500 mt-1">영업 가망고객을 관리합니다</p>
<h1 class="text-2xl font-bold text-gray-800">명함등록 (영업권)</h1>
<p class="text-sm text-gray-500 mt-1">명함을 등록하여 2개월간 영업권을 확보하세요</p>
</div>
<a href="{{ route('sales.prospects.create') }}"
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition text-center w-full sm:w-auto flex items-center justify-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
가망고객 등록
명함 등록
</a>
</div>
<!-- 통계 카드 -->
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-4 mb-6 flex-shrink-0">
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6 flex-shrink-0">
<div class="bg-white rounded-lg shadow-sm p-4">
<div class="text-sm text-gray-500">전체</div>
<div class="text-xl font-bold text-gray-800">{{ number_format($stats['total']) }}</div>
</div>
<div class="bg-gray-50 rounded-lg shadow-sm p-4">
<div class="text-sm text-gray-600">리드</div>
<div class="text-xl font-bold text-gray-800">{{ number_format($stats['lead']) }}</div>
</div>
<div class="bg-blue-50 rounded-lg shadow-sm p-4">
<div class="text-sm text-blue-600">가망</div>
<div class="text-xl font-bold text-blue-800">{{ number_format($stats['prospect']) }}</div>
<div class="text-sm text-blue-600">영업중</div>
<div class="text-xl font-bold text-blue-800">{{ number_format($stats['active']) }}</div>
</div>
<div class="bg-yellow-50 rounded-lg shadow-sm p-4">
<div class="text-sm text-yellow-600">협상중</div>
<div class="text-xl font-bold text-yellow-800">{{ number_format($stats['negotiation']) }}</div>
<div class="bg-gray-50 rounded-lg shadow-sm p-4">
<div class="text-sm text-gray-600">만료</div>
<div class="text-xl font-bold text-gray-800">{{ number_format($stats['expired']) }}</div>
</div>
<div class="bg-green-50 rounded-lg shadow-sm p-4">
<div class="text-sm text-green-600">계약완료</div>
<div class="text-xl font-bold text-green-800">{{ number_format($stats['contracted']) }}</div>
</div>
<div class="bg-purple-50 rounded-lg shadow-sm p-4">
<div class="text-sm text-purple-600"> 계약금액</div>
<div class="text-xl font-bold text-purple-800">{{ number_format($stats['total_contract']) }}</div>
</div>
<div class="bg-indigo-50 rounded-lg shadow-sm p-4">
<div class="text-sm text-indigo-600"> 수수료</div>
<div class="text-xl font-bold text-indigo-800">{{ number_format($stats['total_commission']) }}</div>
<div class="text-xl font-bold text-green-800">{{ number_format($stats['converted']) }}</div>
</div>
</div>
@@ -58,27 +46,15 @@ class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition
<input type="text"
name="search"
value="{{ request('search') }}"
placeholder="업체명, 사업자번호, 대표자로 검색..."
placeholder="회사명, 사업자번호, 대표자, 연락처로 검색..."
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="w-full sm:w-40">
<select name="status" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">전체 상태</option>
<option value="lead" {{ request('status') === 'lead' ? 'selected' : '' }}>리드</option>
<option value="prospect" {{ request('status') === 'prospect' ? 'selected' : '' }}>가망</option>
<option value="negotiation" {{ request('status') === 'negotiation' ? 'selected' : '' }}>협상중</option>
<option value="contracted" {{ request('status') === 'contracted' ? 'selected' : '' }}>계약완료</option>
<option value="lost" {{ request('status') === 'lost' ? 'selected' : '' }}>실패</option>
</select>
</div>
<div class="w-full sm:w-40">
<select name="manager_id" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">전체 담당자</option>
@foreach($managers as $manager)
<option value="{{ $manager->id }}" {{ request('manager_id') == $manager->id ? 'selected' : '' }}>
{{ $manager->name }}
</option>
@endforeach
<option value="active" {{ request('status') === 'active' ? 'selected' : '' }}>영업중</option>
<option value="expired" {{ request('status') === 'expired' ? 'selected' : '' }}>만료</option>
<option value="converted" {{ request('status') === 'converted' ? 'selected' : '' }}>계약완료</option>
</select>
</div>
<button type="submit" class="bg-gray-600 hover:bg-gray-700 text-white px-6 py-2 rounded-lg transition w-full sm:w-auto">
@@ -93,12 +69,11 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">업체정보</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">회사정보</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">상태</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">담당</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">등록</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">연락처</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">계약금액</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">등록일</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">유효기간</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">관리</th>
</tr>
</thead>
@@ -107,53 +82,61 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
<tr class="hover:bg-gray-50">
<td class="px-6 py-4">
<div class="font-medium text-gray-900">{{ $prospect->company_name }}</div>
@if($prospect->business_no)
<div class="text-sm text-gray-500">{{ $prospect->formatted_business_no }}</div>
@endif
@if($prospect->representative)
<div class="text-sm text-gray-500">{{ $prospect->representative }}</div>
<div class="text-sm text-gray-500">{{ $prospect->business_number }}</div>
@if($prospect->ceo_name)
<div class="text-sm text-gray-500">{{ $prospect->ceo_name }}</div>
@endif
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 py-1 text-xs font-medium rounded-full {{ $prospect->status_color }}">
{{ $prospect->status_label }}
</span>
@if($prospect->isActive() && $prospect->remaining_days <= 14)
<div class="text-xs text-red-500 mt-1">D-{{ $prospect->remaining_days }}</div>
@endif
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900">{{ $prospect->manager?->name ?? '-' }}</div>
@if($prospect->salesManager && $prospect->salesManager->id !== $prospect->manager?->id)
<div class="text-xs text-gray-500">담당: {{ $prospect->salesManager->name }}</div>
@endif
<div class="text-sm text-gray-900">{{ $prospect->registeredBy?->name ?? '-' }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ $prospect->contact_phone ?? '-' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm">
@if($prospect->products->count() > 0)
<div class="font-medium text-gray-900">{{ number_format($prospect->total_contract_amount) }}</div>
<div class="text-xs text-gray-500">수수료: {{ number_format($prospect->total_commission) }}</div>
<td class="px-6 py-4 whitespace-nowrap text-sm">
@if($prospect->isConverted())
<div class="text-green-600">{{ $prospect->converted_at?->format('Y-m-d') }} 전환</div>
@elseif($prospect->isActive())
<div class="text-blue-600">{{ $prospect->expires_at->format('Y-m-d') }} 까지</div>
@else
<span class="text-gray-400">-</span>
<div class="text-gray-500">{{ $prospect->expires_at->format('Y-m-d') }} 만료</div>
@if($prospect->isInCooldown())
<div class="text-xs text-yellow-600">쿨다운: {{ $prospect->cooldown_ends_at->format('Y-m-d') }}</div>
@endif
@endif
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ $prospect->created_at->format('Y-m-d') }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<a href="{{ route('sales.prospects.show', $prospect->id) }}" class="text-blue-600 hover:text-blue-900 mr-3">상세</a>
@if(!$prospect->isConverted())
<a href="{{ route('sales.prospects.edit', $prospect->id) }}" class="text-indigo-600 hover:text-indigo-900 mr-3">수정</a>
@if($prospect->isActive())
<form action="{{ route('sales.prospects.convert', $prospect->id) }}" method="POST" class="inline"
onsubmit="return confirm('테넌트로 전환하시겠습니까?')">
@csrf
<button type="submit" class="text-green-600 hover:text-green-900 mr-3">전환</button>
</form>
@endif
<form action="{{ route('sales.prospects.destroy', $prospect->id) }}" method="POST" class="inline"
onsubmit="return confirm('정말 삭제하시겠습니까?')">
@csrf
@method('DELETE')
<button type="submit" class="text-red-600 hover:text-red-900">삭제</button>
</form>
@endif
</td>
</tr>
@empty
<tr>
<td colspan="7" class="px-6 py-12 text-center text-gray-500">
등록된 가망고객 없습니다.
<td colspan="6" class="px-6 py-12 text-center text-gray-500">
등록된 명함 없습니다.
</td>
</tr>
@endforelse

View File

@@ -1,9 +1,9 @@
@extends('layouts.app')
@section('title', '가망고객 상세')
@section('title', '영업권 상세')
@section('content')
<div class="max-w-5xl mx-auto">
<div class="max-w-4xl mx-auto">
<!-- 페이지 헤더 -->
<div class="mb-6 flex justify-between items-start">
<div>
@@ -18,25 +18,57 @@
<span class="px-2 py-1 text-xs font-medium rounded-full {{ $prospect->status_color }}">
{{ $prospect->status_label }}
</span>
@if($prospect->business_no)
<span class="ml-2">{{ $prospect->formatted_business_no }}</span>
@endif
<span class="ml-2">{{ $prospect->business_number }}</span>
</p>
</div>
<a href="{{ route('sales.prospects.edit', $prospect->id) }}"
class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg transition">
수정
</a>
<div class="flex gap-2">
@if(!$prospect->isConverted())
<a href="{{ route('sales.prospects.edit', $prospect->id) }}"
class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg transition">
수정
</a>
@if($prospect->isActive())
<form action="{{ route('sales.prospects.convert', $prospect->id) }}" method="POST"
onsubmit="return confirm('테넌트로 전환하시겠습니까?\n\n전환 후에는 수정/삭제가 불가능합니다.')">
@csrf
<button type="submit"
class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg transition">
테넌트 전환
</button>
</form>
@endif
@endif
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- 알림 메시지 -->
@if(session('success'))
<div class="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg text-green-700">
{{ session('success') }}
</div>
@endif
@if(session('error'))
<div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700">
{{ session('error') }}
</div>
@endif
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- 기본 정보 -->
<div class="bg-white rounded-lg shadow-sm p-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">기본 정보</h2>
<h2 class="text-lg font-semibold text-gray-800 mb-4">회사 정보</h2>
<dl class="space-y-3">
<div class="flex justify-between">
<dt class="text-gray-500">사업자번호</dt>
<dd class="font-medium text-gray-900">{{ $prospect->business_number }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500">회사명</dt>
<dd class="font-medium text-gray-900">{{ $prospect->company_name }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500">대표자</dt>
<dd class="font-medium text-gray-900">{{ $prospect->representative ?? '-' }}</dd>
<dd class="font-medium text-gray-900">{{ $prospect->ceo_name ?? '-' }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500">연락처</dt>
@@ -44,7 +76,7 @@ class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg transit
</div>
<div class="flex justify-between">
<dt class="text-gray-500">이메일</dt>
<dd class="font-medium text-gray-900">{{ $prospect->email ?? '-' }}</dd>
<dd class="font-medium text-gray-900">{{ $prospect->contact_email ?? '-' }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500">주소</dt>
@@ -53,120 +85,107 @@ class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg transit
</dl>
</div>
<!-- 담당자 정보 -->
<!-- 영업권 정보 -->
<div class="bg-white rounded-lg shadow-sm p-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">담당자 정보</h2>
<h2 class="text-lg font-semibold text-gray-800 mb-4">영업권 정보</h2>
<dl class="space-y-3">
<div class="flex justify-between">
<dt class="text-gray-500">영업 담당</dt>
<dd class="font-medium text-gray-900">
@if($prospect->manager)
<a href="{{ route('sales.managers.show', $prospect->manager->id) }}" class="text-blue-600 hover:underline">
{{ $prospect->manager->name }}
</a>
@else
-
@endif
</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500">담당 매니저</dt>
<dd class="font-medium text-gray-900">
@if($prospect->salesManager)
<a href="{{ route('sales.managers.show', $prospect->salesManager->id) }}" class="text-blue-600 hover:underline">
{{ $prospect->salesManager->name }}
</a>
@else
-
@endif
</dd>
<dt class="text-gray-500">등록</dt>
<dd class="font-medium text-gray-900">{{ $prospect->registeredBy?->name ?? '-' }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500">등록일</dt>
<dd class="font-medium text-gray-900">{{ $prospect->created_at->format('Y-m-d H:i') }}</dd>
<dd class="font-medium text-gray-900">{{ $prospect->registered_at->format('Y-m-d H:i') }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500">만료일</dt>
<dd class="font-medium {{ $prospect->isActive() ? 'text-blue-600' : 'text-gray-500' }}">
{{ $prospect->expires_at->format('Y-m-d H:i') }}
@if($prospect->isActive())
<span class="text-sm">(D-{{ $prospect->remaining_days }})</span>
@endif
</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500">쿨다운 종료일</dt>
<dd class="font-medium text-gray-900">{{ $prospect->cooldown_ends_at->format('Y-m-d H:i') }}</dd>
</div>
@if($prospect->isConverted())
<div class="flex justify-between">
<dt class="text-gray-500">전환일</dt>
<dd class="font-medium text-green-600">{{ $prospect->converted_at?->format('Y-m-d H:i') }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500">전환 처리자</dt>
<dd class="font-medium text-gray-900">{{ $prospect->convertedBy?->name ?? '-' }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500">전환된 테넌트</dt>
<dd class="font-medium text-blue-600">
@if($prospect->tenant)
<a href="{{ route('tenants.edit', $prospect->tenant_id) }}" class="hover:underline">
{{ $prospect->tenant->company_name }} (ID: {{ $prospect->tenant_id }})
</a>
@else
ID: {{ $prospect->tenant_id }}
@endif
</dd>
</div>
@endif
</dl>
</div>
<!-- 통계 -->
<div class="bg-white rounded-lg shadow-sm p-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">계약 현황</h2>
<div class="space-y-4">
<div class="bg-blue-50 rounded-lg p-4 text-center">
<div class="text-2xl font-bold text-blue-800">{{ number_format($prospect->total_contract_amount) }}</div>
<div class="text-sm text-blue-600"> 계약금액</div>
</div>
<div class="bg-green-50 rounded-lg p-4 text-center">
<div class="text-2xl font-bold text-green-800">{{ number_format($prospect->total_commission) }}</div>
<div class="text-sm text-green-600"> 수수료</div>
</div>
<div class="bg-gray-50 rounded-lg p-4 text-center">
<div class="text-2xl font-bold text-gray-800">{{ $prospect->products->count() }}</div>
<div class="text-sm text-gray-600">계약 상품</div>
</div>
</div>
</div>
</div>
<!-- 계약 상품 목록 -->
<!-- 명함 이미지 -->
@if($prospect->hasBusinessCard())
<div class="mt-6 bg-white rounded-lg shadow-sm p-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">계약 상품</h2>
@if($prospect->products->isNotEmpty())
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">상품명</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">계약금액</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">수수료</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">승인상태</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">계약일</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
@foreach($prospect->products as $product)
<tr>
<td class="px-4 py-3 font-medium text-gray-900">{{ $product->product_name }}</td>
<td class="px-4 py-3 text-right text-gray-900">{{ number_format($product->contract_amount) }}</td>
<td class="px-4 py-3 text-right text-gray-900">{{ number_format($product->commission_amount) }}</td>
<td class="px-4 py-3 text-center">
<span class="px-2 py-1 text-xs font-medium rounded-full {{ $product->approval_color }}">
{{ $product->approval_status }}
</span>
</td>
<td class="px-4 py-3 text-gray-500">{{ $product->contract_date?->format('Y-m-d') ?? '-' }}</td>
</tr>
@endforeach
</tbody>
</table>
<h2 class="text-lg font-semibold text-gray-800 mb-4">명함 이미지</h2>
<div class="flex justify-center">
<img src="{{ $prospect->business_card_url }}" alt="명함 이미지" class="max-w-md rounded-lg shadow">
</div>
@else
<p class="text-center text-gray-500 py-8">등록된 계약 상품이 없습니다.</p>
@endif
</div>
@endif
<!-- 상담 기록 -->
<!-- 메모 -->
@if($prospect->memo)
<div class="mt-6 bg-white rounded-lg shadow-sm p-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">상담 기록</h2>
@if($prospect->consultations->isNotEmpty())
<div class="space-y-4">
@foreach($prospect->consultations->take(10) as $consultation)
<div class="border-l-4 border-blue-400 pl-4 py-2">
<div class="flex justify-between items-start">
<div>
<span class="text-sm font-medium text-gray-900">{{ $consultation->manager?->name ?? '알 수 없음' }}</span>
<span class="text-xs text-gray-500 ml-2">{{ $consultation->created_at->format('Y-m-d H:i') }}</span>
</div>
<span class="px-2 py-1 text-xs bg-gray-100 text-gray-600 rounded">
{{ $consultation->consultation_type_label }}
</span>
</div>
<p class="mt-1 text-gray-700 whitespace-pre-line">{{ $consultation->log_text }}</p>
</div>
@endforeach
<h2 class="text-lg font-semibold text-gray-800 mb-4">메모</h2>
<p class="text-gray-700 whitespace-pre-line">{{ $prospect->memo }}</p>
</div>
@endif
<!-- 상태별 안내 -->
<div class="mt-6">
@if($prospect->isActive())
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h3 class="font-medium text-blue-800 mb-2">영업권 유효</h3>
<p class="text-sm text-blue-700">
{{ $prospect->expires_at->format('Y-m-d') }}까지 영업권이 유효합니다.
남은 기간: <strong>{{ $prospect->remaining_days }}</strong>
</p>
</div>
@elseif($prospect->isInCooldown())
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<h3 class="font-medium text-yellow-800 mb-2">쿨다운 기간</h3>
<p class="text-sm text-yellow-700">
영업권이 만료되었습니다.
{{ $prospect->cooldown_ends_at->format('Y-m-d') }} 이후 다른 영업파트너가 재등록할 있습니다.
</p>
</div>
@elseif($prospect->isConverted())
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
<h3 class="font-medium text-green-800 mb-2">테넌트 전환 완료</h3>
<p class="text-sm text-green-700">
{{ $prospect->converted_at?->format('Y-m-d') }} 테넌트로 전환되었습니다.
</p>
</div>
@else
<p class="text-center text-gray-500 py-8">등록된 상담 기록이 없습니다.</p>
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
<h3 class="font-medium text-gray-800 mb-2">영업권 만료</h3>
<p class="text-sm text-gray-700">
영업권이 만료되었습니다. 쿨다운 기간이 종료되어 재등록이 가능합니다.
</p>
</div>
@endif
</div>
</div>

View File

@@ -802,8 +802,10 @@
Route::get('managers/{id}/documents/{documentId}/download', [\App\Http\Controllers\Sales\SalesManagerController::class, 'downloadDocument'])->name('managers.documents.download');
Route::delete('managers/{id}/documents/{documentId}', [\App\Http\Controllers\Sales\SalesManagerController::class, 'deleteDocument'])->name('managers.documents.delete');
// 가망고객 관리
Route::resource('prospects', \App\Http\Controllers\Sales\SalesProspectController::class);
// 명함등록 (영업권) 관리
Route::resource('prospects', \App\Http\Controllers\Sales\TenantProspectController::class);
Route::post('prospects/{id}/convert', [\App\Http\Controllers\Sales\TenantProspectController::class, 'convert'])->name('prospects.convert');
Route::post('prospects/check-business-number', [\App\Http\Controllers\Sales\TenantProspectController::class, 'checkBusinessNumber'])->name('prospects.check-business-number');
// 영업 실적 관리
Route::resource('records', \App\Http\Controllers\Sales\SalesRecordController::class);