feat:고객관리 상세/수정 모달창 구현

- TenantProspectController에 modalShow, modalEdit 메서드 추가
- prospects 라우트에 modal-show, modal-edit 엔드포인트 추가
- index.blade.php에 모달 컨테이너 및 JavaScript 추가
- partials/show-modal.blade.php, edit-modal.blade.php 신규 생성

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
pro
2026-01-29 22:20:32 +09:00
parent bfc0ee3006
commit 32bb5795d1
5 changed files with 389 additions and 2 deletions

View File

@@ -220,6 +220,27 @@ public function checkBusinessNumber(Request $request)
return response()->json($result);
}
/**
* 모달용 상세 정보
*/
public function modalShow(int $id): View
{
$prospect = TenantProspect::with(['registeredBy', 'tenant', 'convertedBy'])
->findOrFail($id);
return view('sales.prospects.partials.show-modal', compact('prospect'));
}
/**
* 모달용 수정 폼
*/
public function modalEdit(int $id): View
{
$prospect = TenantProspect::findOrFail($id);
return view('sales.prospects.partials.edit-modal', compact('prospect'));
}
/**
* 첨부 이미지 삭제 (AJAX)
*/

View File

@@ -114,9 +114,9 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
@endif
</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>
<button type="button" onclick="openProspectShowModal({{ $prospect->id }})" class="text-blue-600 hover:text-blue-900 mr-3">상세</button>
@if(!$prospect->isConverted())
<a href="{{ route('sales.prospects.edit', $prospect->id) }}" class="text-indigo-600 hover:text-indigo-900 mr-3">수정</a>
<button type="button" onclick="openProspectEditModal({{ $prospect->id }})" class="text-indigo-600 hover:text-indigo-900 mr-3">수정</button>
@if($prospect->isActive())
<form action="{{ route('sales.prospects.convert', $prospect->id) }}" method="POST" class="inline"
onsubmit="return confirm('계약 처리하시겠습니까?')">
@@ -152,4 +152,109 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
@endif
</div>
</div>
<!-- 고객 모달 -->
<div id="prospectModal" class="hidden fixed inset-0 z-50 overflow-y-auto">
<!-- 배경 오버레이 -->
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity"></div>
<!-- 모달 컨텐츠 wrapper -->
<div class="flex min-h-full items-center justify-center p-4">
<div id="prospectModalContent" class="relative bg-white rounded-xl shadow-2xl w-full max-w-3xl">
<!-- AJAX로 로드되는 내용 -->
<div class="flex items-center justify-center p-12">
<svg class="animate-spin h-8 w-8 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
// 전역 함수 등록
window.openProspectShowModal = function(id) {
const modal = document.getElementById('prospectModal');
const content = document.getElementById('prospectModalContent');
modal.classList.remove('hidden');
document.body.style.overflow = 'hidden';
// 로딩 표시
content.innerHTML = `
<div class="flex items-center justify-center p-12">
<svg class="animate-spin h-8 w-8 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
`;
// AJAX로 내용 로드
fetch(`/sales/prospects/${id}/modal-show`)
.then(response => response.text())
.then(html => {
content.innerHTML = html;
})
.catch(error => {
console.error('Error:', error);
content.innerHTML = '<div class="p-6 text-center text-red-500">내용을 불러올 수 없습니다.</div>';
});
};
window.openProspectEditModal = function(id) {
const modal = document.getElementById('prospectModal');
const content = document.getElementById('prospectModalContent');
modal.classList.remove('hidden');
document.body.style.overflow = 'hidden';
// 로딩 표시
content.innerHTML = `
<div class="flex items-center justify-center p-12">
<svg class="animate-spin h-8 w-8 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
`;
// AJAX로 내용 로드
fetch(`/sales/prospects/${id}/modal-edit`)
.then(response => response.text())
.then(html => {
content.innerHTML = html;
})
.catch(error => {
console.error('Error:', error);
content.innerHTML = '<div class="p-6 text-center text-red-500">내용을 불러올 수 없습니다.</div>';
});
};
window.closeProspectModal = function() {
document.getElementById('prospectModal').classList.add('hidden');
document.body.style.overflow = '';
};
// ESC 키로 모달 닫기
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
const modal = document.getElementById('prospectModal');
if (!modal.classList.contains('hidden')) {
window.closeProspectModal();
}
}
});
// 이벤트 델리게이션 (닫기 버튼)
document.addEventListener('click', function(e) {
const closeBtn = e.target.closest('[data-close-modal]');
if (closeBtn) {
e.preventDefault();
window.closeProspectModal();
}
});
</script>
@endpush

View File

@@ -0,0 +1,109 @@
{{-- 고객 수정 모달 내용 --}}
<div class="p-6 max-h-[80vh] overflow-y-auto">
<!-- 헤더 -->
<div class="flex justify-between items-start mb-6">
<div>
<h2 class="text-xl font-bold text-gray-800">고객 정보 수정</h2>
<p class="text-sm text-gray-500 mt-1">{{ $prospect->company_name }} ({{ $prospect->business_number }})</p>
</div>
<button type="button" data-close-modal class="text-gray-400 hover:text-gray-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- -->
<form action="{{ route('sales.prospects.update', $prospect->id) }}" method="POST" enctype="multipart/form-data">
@csrf
@method('PUT')
<!-- 사업자번호 (수정 불가) -->
<div class="mb-4">
<label class="block text-xs font-medium text-gray-700 mb-1">사업자번호</label>
<input type="text" value="{{ $prospect->business_number }}" disabled
class="w-full px-3 py-2 border border-gray-300 rounded-lg bg-gray-100 text-gray-500 text-sm">
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">회사명 <span class="text-red-500">*</span></label>
<input type="text" name="company_name" value="{{ $prospect->company_name }}" required
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm">
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">대표자명</label>
<input type="text" name="ceo_name" value="{{ $prospect->ceo_name }}"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm">
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">연락처</label>
<input type="text" name="contact_phone" value="{{ $prospect->contact_phone }}"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm">
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">이메일</label>
<input type="email" name="contact_email" value="{{ $prospect->contact_email }}"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm">
</div>
</div>
<div class="mb-4">
<label class="block text-xs font-medium text-gray-700 mb-1">주소</label>
<input type="text" name="address" value="{{ $prospect->address }}"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm">
</div>
<div class="mb-4">
<label class="block text-xs font-medium text-gray-700 mb-1">명함 이미지</label>
@if($prospect->hasBusinessCard())
<div class="mb-2 p-2 bg-gray-50 rounded-lg flex items-center gap-3" id="prospect_card_preview">
<img src="{{ $prospect->business_card_url }}" alt="현재 명함" class="h-16 rounded">
<span class="text-xs text-gray-500"> 이미지 업로드 교체됨</span>
</div>
@endif
<input type="file" name="business_card" accept="image/*"
class="w-full text-sm text-gray-500 file:mr-2 file:py-1.5 file:px-3 file:rounded-lg file:border-0 file:text-sm file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100">
</div>
<div class="mb-4">
<label class="block text-xs font-medium text-gray-700 mb-1">메모</label>
<textarea name="memo" rows="2"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm">{{ $prospect->memo }}</textarea>
</div>
<!-- 영업권 상태 정보 -->
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3 mb-4">
<h3 class="text-xs font-medium text-gray-800 mb-2">영업권 상태</h3>
<div class="grid grid-cols-2 gap-2 text-xs">
<div class="text-gray-500">상태</div>
<div>
<span class="px-2 py-0.5 text-xs font-medium rounded-full {{ $prospect->status_color }}">
{{ $prospect->status_label }}
</span>
</div>
<div class="text-gray-500">등록일</div>
<div class="font-medium">{{ $prospect->registered_at->format('Y-m-d') }}</div>
<div class="text-gray-500">만료일</div>
<div class="font-medium">{{ $prospect->expires_at->format('Y-m-d') }}</div>
<div class="text-gray-500">등록자</div>
<div class="font-medium">{{ $prospect->registeredBy?->name ?? '-' }}</div>
</div>
</div>
<!-- 푸터 버튼 -->
<div class="flex justify-end gap-3">
<button type="button" data-close-modal
class="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition text-sm">
취소
</button>
<button type="submit"
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition text-sm">
수정
</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,150 @@
{{-- 고객 상세 모달 내용 --}}
<div class="p-6 max-h-[80vh] overflow-y-auto">
<!-- 헤더 -->
<div class="flex justify-between items-start mb-6">
<div>
<h2 class="text-xl font-bold text-gray-800">{{ $prospect->company_name }}</h2>
<p class="text-sm text-gray-500 mt-1">
<span class="px-2 py-1 text-xs font-medium rounded-full {{ $prospect->status_color }}">
{{ $prospect->status_label }}
</span>
<span class="ml-2">{{ $prospect->business_number }}</span>
</p>
</div>
<button type="button" data-close-modal class="text-gray-400 hover:text-gray-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<!-- 회사 정보 -->
<div class="bg-gray-50 rounded-lg p-4">
<h3 class="text-sm font-semibold text-gray-800 mb-3">회사 정보</h3>
<dl class="space-y-2 text-sm">
<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->ceo_name ?? '-' }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500">연락처</dt>
<dd class="font-medium text-gray-900">{{ $prospect->contact_phone ?? '-' }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500">이메일</dt>
<dd class="font-medium text-gray-900">{{ $prospect->contact_email ?? '-' }}</dd>
</div>
@if($prospect->address)
<div class="flex justify-between">
<dt class="text-gray-500">주소</dt>
<dd class="font-medium text-gray-900 text-right text-xs">{{ $prospect->address }}</dd>
</div>
@endif
</dl>
</div>
<!-- 영업권 정보 -->
<div class="bg-gray-50 rounded-lg p-4">
<h3 class="text-sm font-semibold text-gray-800 mb-3">영업권 정보</h3>
<dl class="space-y-2 text-sm">
<div class="flex justify-between">
<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->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') }}
@if($prospect->isActive())
<span class="text-xs">(D-{{ $prospect->remaining_days }})</span>
@endif
</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') }}</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>
@endif
</dl>
</div>
</div>
<!-- 첨부 이미지 -->
@if($prospect->hasBusinessCard())
<div class="mt-4 bg-gray-50 rounded-lg p-4">
<h3 class="text-sm font-semibold text-gray-800 mb-3">명함 이미지</h3>
<a href="{{ $prospect->business_card_url }}" target="_blank" class="block">
<img src="{{ $prospect->business_card_url }}" alt="명함 이미지" class="max-h-32 rounded-lg shadow hover:shadow-lg transition">
</a>
</div>
@endif
<!-- 메모 -->
@if($prospect->memo)
<div class="mt-4 bg-gray-50 rounded-lg p-4">
<h3 class="text-sm font-semibold text-gray-800 mb-2">메모</h3>
<p class="text-sm text-gray-700 whitespace-pre-line">{{ $prospect->memo }}</p>
</div>
@endif
<!-- 상태별 안내 -->
<div class="mt-4">
@if($prospect->isActive())
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3">
<p class="text-sm text-blue-700">
<strong>영업권 유효:</strong> {{ $prospect->expires_at->format('Y-m-d') }}까지 (D-{{ $prospect->remaining_days }})
</p>
</div>
@elseif($prospect->isConverted())
<div class="bg-green-50 border border-green-200 rounded-lg p-3">
<p class="text-sm text-green-700">
<strong>계약 완료:</strong> {{ $prospect->converted_at?->format('Y-m-d') }} 계약되었습니다.
</p>
</div>
@elseif($prospect->isInCooldown())
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
<p class="text-sm text-yellow-700">
<strong>재등록 대기:</strong> {{ $prospect->cooldown_ends_at->format('Y-m-d') }} 이후 재등록 가능
</p>
</div>
@else
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3">
<p class="text-sm text-gray-700">
<strong>영업권 만료:</strong> 재등록이 가능합니다.
</p>
</div>
@endif
</div>
<!-- 푸터 버튼 -->
<div class="mt-6 flex justify-end gap-3">
<button type="button" data-close-modal
class="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition text-sm">
닫기
</button>
@if(!$prospect->isConverted())
<button type="button" onclick="window.openProspectEditModal({{ $prospect->id }})"
class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition text-sm">
수정
</button>
@endif
</div>
</div>

View File

@@ -805,6 +805,8 @@
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::delete('prospects/{id}/attachment', [\App\Http\Controllers\Sales\TenantProspectController::class, 'deleteAttachment'])->name('prospects.delete-attachment');
Route::get('prospects/{id}/modal-show', [\App\Http\Controllers\Sales\TenantProspectController::class, 'modalShow'])->name('prospects.modal-show');
Route::get('prospects/{id}/modal-edit', [\App\Http\Controllers\Sales\TenantProspectController::class, 'modalEdit'])->name('prospects.modal-edit');
// 영업 시나리오 관리
Route::prefix('scenarios')->name('scenarios.')->group(function () {