테넌트 목록/모달 UI 개선

- 저장소 사용량 표시 추가 (테이블 + 모달)
- 이메일/전화번호 컬럼 병합 (연락처)
- 전화번호 하이픈 포맷 적용
- 생성일 yymmdd 형식 변경 및 ID 뒤로 이동
- 테이블 헤더 가운데 정렬
- 액션 컬럼을 관리(colspan)로 변경
This commit is contained in:
2025-12-01 14:57:53 +09:00
parent a2477837d0
commit c8ddbfd130
3 changed files with 191 additions and 39 deletions

View File

@@ -44,9 +44,13 @@ class Tenant extends Model
protected $casts = [
'max_users' => 'integer',
'storage_limit' => 'integer',
'storage_used' => 'integer',
'trial_ends_at' => 'datetime',
'expires_at' => 'datetime',
'last_paid_at' => 'datetime',
'storage_warning_sent_at' => 'datetime',
'storage_grace_period_until' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
@@ -143,4 +147,110 @@ public function getBillingTypeLabelAttribute(): ?string
default => $this->billing_tp_code,
};
}
/**
* 저장소 사용률 (%) - Blade 뷰에서 사용
*/
public function getStorageUsagePercentAttribute(): float
{
$limit = $this->storage_limit ?? 10737418240; // 기본 10GB
if ($limit <= 0) {
return 0;
}
return round(($this->storage_used ?? 0) / $limit * 100, 1);
}
/**
* 저장소 사용량 포맷 (예: "2.5 GB / 10 GB")
*/
public function getStorageUsageFormattedAttribute(): string
{
$used = $this->formatBytes($this->storage_used ?? 0);
$limit = $this->formatBytes($this->storage_limit ?? 10737418240);
return "{$used} / {$limit}";
}
/**
* 저장소 사용량만 포맷 (예: "2.5 GB")
*/
public function getStorageUsedFormattedAttribute(): string
{
return $this->formatBytes($this->storage_used ?? 0);
}
/**
* 저장소 한도만 포맷 (예: "10 GB")
*/
public function getStorageLimitFormattedAttribute(): string
{
return $this->formatBytes($this->storage_limit ?? 10737418240);
}
/**
* 저장소 상태 배지 색상 (Blade 뷰에서 사용)
*/
public function getStorageBadgeColorAttribute(): string
{
$percent = $this->storage_usage_percent;
return match (true) {
$percent >= 90 => 'error',
$percent >= 70 => 'warning',
default => 'success',
};
}
/**
* 바이트를 읽기 쉬운 형식으로 변환
*/
protected function formatBytes(int $bytes): string
{
if ($bytes <= 0) {
return '0 B';
}
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$factor = floor(log($bytes, 1024));
$factor = min($factor, count($units) - 1);
return round($bytes / pow(1024, $factor), 1).' '.$units[$factor];
}
/**
* 전화번호 포맷 (하이픈 추가)
*/
public function getPhoneFormattedAttribute(): ?string
{
if (! $this->phone) {
return null;
}
// 숫자만 추출
$numbers = preg_replace('/[^0-9]/', '', $this->phone);
// 휴대폰 (010, 011, 016, 017, 018, 019)
if (preg_match('/^(01[0-9])(\d{3,4})(\d{4})$/', $numbers, $matches)) {
return $matches[1].'-'.$matches[2].'-'.$matches[3];
}
// 서울 (02)
if (preg_match('/^(02)(\d{3,4})(\d{4})$/', $numbers, $matches)) {
return $matches[1].'-'.$matches[2].'-'.$matches[3];
}
// 지역번호 (031, 032, ...)
if (preg_match('/^(0\d{2})(\d{3,4})(\d{4})$/', $numbers, $matches)) {
return $matches[1].'-'.$matches[2].'-'.$matches[3];
}
// 대표번호 (1588, 1544, ...)
if (preg_match('/^(1\d{3})(\d{4})$/', $numbers, $matches)) {
return $matches[1].'-'.$matches[2];
}
// 포맷 불가 시 원본 반환
return $this->phone;
}
}

View File

@@ -93,6 +93,29 @@ class="px-3 py-1.5 text-sm font-medium text-white bg-green-600 rounded-lg hover:
</tr>
</tbody>
</table>
{{-- 저장소 사용량 --}}
<div class="mt-4 pt-4 border-t border-gray-200">
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-medium text-gray-700">저장소 사용량</span>
<span class="text-sm text-gray-500">{{ $tenant->storage_usage_formatted }}</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-3">
<div class="h-3 rounded-full transition-all duration-300
{{ $tenant->storage_badge_color === 'error' ? 'bg-red-500' : '' }}
{{ $tenant->storage_badge_color === 'warning' ? 'bg-yellow-500' : '' }}
{{ $tenant->storage_badge_color === 'success' ? 'bg-green-500' : '' }}"
style="width: {{ min($tenant->storage_usage_percent, 100) }}%"></div>
</div>
<div class="flex items-center justify-between mt-1">
<span class="text-xs text-gray-500">{{ $tenant->storage_usage_percent }}% 사용</span>
@if($tenant->storage_usage_percent >= 90)
<span class="text-xs text-red-600 font-medium">용량 부족 경고</span>
@elseif($tenant->storage_usage_percent >= 70)
<span class="text-xs text-yellow-600 font-medium">용량 주의</span>
@endif
</div>
</div>
</div>
</div>
</div>

View File

@@ -2,19 +2,19 @@
<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-sm font-semibold text-gray-700 uppercase tracking-wider">ID</th>
<th class="px-3 py-2 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">회사명</th>
<th class="px-3 py-2 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">코드</th>
<th class="px-3 py-2 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">상태</th>
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider">ID</th>
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider">생성일</th>
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider">회사명</th>
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider">코드</th>
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider">상태</th>
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider">유형</th>
<th class="px-3 py-2 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">이메일</th>
<th class="px-3 py-2 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">전화번호</th>
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider">연락처</th>
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider">사용자</th>
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider">부서</th>
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider">메뉴</th>
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider">역할</th>
<th class="px-3 py-2 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">생성일</th>
<th class="px-6 py-3 text-right text-sm font-semibold text-gray-700 uppercase tracking-wider">액션</th>
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider">사용량</th>
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider" colspan="2">관리</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@@ -25,6 +25,9 @@
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{{ $tenant->id }}
</td>
<td class="px-3 py-2 whitespace-nowrap text-sm text-gray-500">
{{ $tenant->created_at?->format('ymd') ?? '-' }}
</td>
<td class="px-3 py-2 whitespace-nowrap">
<div class="text-sm font-medium text-gray-900 cursor-pointer hover:text-blue-600"
data-context-menu="tenant"
@@ -61,11 +64,11 @@
{{ $typeLabels[$type] ?? $type }}
</span>
</td>
<td class="px-3 py-2 whitespace-nowrap text-sm text-gray-500">
{{ $tenant->email ?? '-' }}
</td>
<td class="px-3 py-2 whitespace-nowrap text-sm text-gray-500">
{{ $tenant->phone ?? '-' }}
<td class="px-3 py-2 whitespace-nowrap">
<div class="text-sm text-gray-900">{{ $tenant->email ?? '-' }}</div>
@if($tenant->phone)
<div class="text-xs text-gray-500">{{ $tenant->phone_formatted }}</div>
@endif
</td>
<td class="px-3 py-2 whitespace-nowrap text-sm text-center text-gray-900">
{{ $tenant->users_count ?? 0 }}
@@ -79,39 +82,55 @@
<td class="px-3 py-2 whitespace-nowrap text-sm text-center text-gray-900">
{{ $tenant->roles_count ?? 0 }}
</td>
<td class="px-3 py-2 whitespace-nowrap text-sm text-gray-500">
{{ $tenant->created_at?->format('Y-m-d') ?? '-' }}
<td class="px-3 py-2 whitespace-nowrap">
<div class="flex flex-col items-center">
<div class="w-24 bg-gray-200 rounded-full h-2 mb-1">
<div class="h-2 rounded-full
{{ $tenant->storage_badge_color === 'error' ? 'bg-red-500' : '' }}
{{ $tenant->storage_badge_color === 'warning' ? 'bg-yellow-500' : '' }}
{{ $tenant->storage_badge_color === 'success' ? 'bg-green-500' : '' }}"
style="width: {{ min($tenant->storage_usage_percent, 100) }}%"></div>
</div>
<span class="text-xs text-gray-500">{{ $tenant->storage_used_formatted }}</span>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium" onclick="event.stopPropagation()">
@if($tenant->deleted_at)
<!-- 삭제된 항목 - 복원은 일반관리자도 가능, 영구삭제는 슈퍼관리자만 -->
<button onclick="confirmRestore({{ $tenant->id }}, '{{ $tenant->company_name }}')"
class="text-green-600 hover:text-green-900 mr-3">
복원
</button>
@if(auth()->user()?->is_super_admin)
<button onclick="confirmForceDelete({{ $tenant->id }}, '{{ $tenant->company_name }}')"
class="text-red-600 hover:text-red-900">
영구삭제
</button>
@endif
@if($tenant->deleted_at)
{{-- 삭제된 항목 --}}
<td class="px-2 py-2 whitespace-nowrap text-center text-sm font-medium" onclick="event.stopPropagation()">
<button onclick="confirmRestore({{ $tenant->id }}, '{{ $tenant->company_name }}')"
class="text-green-600 hover:text-green-900">
복원
</button>
</td>
<td class="px-2 py-2 whitespace-nowrap text-center text-sm font-medium" onclick="event.stopPropagation()">
@if(auth()->user()?->is_super_admin)
<button onclick="confirmForceDelete({{ $tenant->id }}, '{{ $tenant->company_name }}')"
class="text-red-600 hover:text-red-900">
영구삭제
</button>
@else
<!-- 활성 항목 -->
<a href="{{ route('tenants.edit', $tenant->id) }}"
onclick="event.stopPropagation()"
class="text-blue-600 hover:text-blue-900 mr-3">
수정
</a>
<button onclick="confirmDelete({{ $tenant->id }}, '{{ $tenant->company_name }}')"
class="text-red-600 hover:text-red-900">
삭제
</button>
<span class="text-gray-400">-</span>
@endif
</td>
@else
{{-- 활성 항목 --}}
<td class="px-2 py-2 whitespace-nowrap text-center text-sm font-medium" onclick="event.stopPropagation()">
<a href="{{ route('tenants.edit', $tenant->id) }}"
class="text-blue-600 hover:text-blue-900">
수정
</a>
</td>
<td class="px-2 py-2 whitespace-nowrap text-center text-sm font-medium" onclick="event.stopPropagation()">
<button onclick="confirmDelete({{ $tenant->id }}, '{{ $tenant->company_name }}')"
class="text-red-600 hover:text-red-900">
삭제
</button>
</td>
@endif
</tr>
@empty
<tr>
<td colspan="13" class="px-6 py-12 text-center text-gray-500">
<td colspan="14" class="px-6 py-12 text-center text-gray-500">
등록된 테넌트가 없습니다.
</td>
</tr>