feat: 테넌트 선택 기능 구현
- Tenant 모델 생성 (SoftDeletes, active scope) - TenantController 전환 컨트롤러 - ViewServiceProvider로 모든 뷰에 테넌트 데이터 자동 제공 - partials/tenant-selector.blade.php UI 컴포넌트 기능: - 드롭다운으로 테넌트 전환 (전체 보기 포함) - Session 기반 선택 상태 저장 - 우측에 현재 테넌트 정보 표시 - 모든 페이지에서 @include로 사용 가능 이슈 해결: - is_active 컬럼 없음 → tenant_st_code + deleted_at 사용 - 삭제되지 않은 모든 테넌트 표시 (7개)
This commit is contained in:
@@ -225,4 +225,89 @@ ### 개발 프로세스:
|
||||
3. Phase 3: 컨텐츠 구성 (다음 단계)
|
||||
|
||||
### Git 커밋:
|
||||
- ✅ `9367f61` "feat: 좌측 사이드바 레이아웃 및 메뉴 시스템 구현"
|
||||
- ✅ `9367f61` "feat: 좌측 사이드바 레이아웃 및 메뉴 시스템 구현"
|
||||
|
||||
---
|
||||
|
||||
## 2025-11-21 (목) - 테넌트 선택 기능 구현
|
||||
|
||||
### 주요 작업
|
||||
- admin 패널의 테넌트 전환 기능을 MNG로 이식
|
||||
- Plain Laravel (Livewire 없이) 방식으로 구현
|
||||
- ViewServiceProvider를 통한 전역 테넌트 데이터 제공
|
||||
|
||||
### 추가된 파일:
|
||||
- `app/Models/Tenant.php` - 테넌트 모델 (SoftDeletes, active scope)
|
||||
- `app/Http/Controllers/TenantController.php` - 테넌트 전환 컨트롤러
|
||||
- `app/Providers/ViewServiceProvider.php` - 모든 뷰에 테넌트 목록 자동 제공
|
||||
- `resources/views/partials/tenant-selector.blade.php` - 테넌트 선택 UI 컴포넌트
|
||||
|
||||
### 수정된 파일:
|
||||
- `routes/web.php` - 테넌트 전환 라우트 추가 (`POST /tenant/switch`)
|
||||
- `bootstrap/providers.php` - ViewServiceProvider 등록
|
||||
- `resources/views/dashboard/index.blade.php` - 테넌트 선택기 포함
|
||||
|
||||
### 기능 상세:
|
||||
|
||||
**테넌트 선택기 구조:**
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ 좌측 우측 │
|
||||
│ [아이콘] 테넌트 선택: [드롭다운] [상태 표시] │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**UI 컴포넌트:**
|
||||
- Welcome Card 위에 배치 (동일한 깊이)
|
||||
- 좌측: 건물 아이콘 + "테넌트 선택" 라벨 + 셀렉트박스
|
||||
- 우측: 현재 테넌트 정보 표시
|
||||
- 특정 테넌트 선택: "○○ 데이터만 표시 중" (primary 색상 뱃지)
|
||||
- 전체 보기: "전체 테넌트 데이터 표시 중" (회색 텍스트)
|
||||
|
||||
**동작 방식:**
|
||||
1. 드롭다운 변경 시 자동 Form Submit (`onchange`)
|
||||
2. `POST /tenant/switch` 라우트로 전송
|
||||
3. Session에 `selected_tenant_id` 저장
|
||||
4. 이전 페이지로 리다이렉트 (`redirect()->back()`)
|
||||
|
||||
**ViewServiceProvider 역할:**
|
||||
- 모든 인증된 뷰에서 `$tenants` 변수 자동 사용 가능
|
||||
- View Composer 패턴 적용 (`View::composer('*', ...)`)
|
||||
- 활성 테넌트만 회사명 순 정렬
|
||||
|
||||
**Tenant 모델:**
|
||||
- SoftDeletes trait 사용
|
||||
- `active()` scope: 삭제되지 않은 테넌트만 조회
|
||||
- `tenant_st_code` 컬럼 사용 (trial, none 등의 상태값)
|
||||
|
||||
### DB 스키마 분석:
|
||||
- **테이블**: `tenants` (29개 컬럼)
|
||||
- **주요 컬럼**:
|
||||
- `id`, `company_name`, `code` (회사 기본 정보)
|
||||
- `tenant_st_code` (상태: trial, none 등)
|
||||
- `deleted_at` (SoftDelete)
|
||||
- `storage_limit`, `storage_used` (용량 관리)
|
||||
|
||||
### 이슈 해결:
|
||||
- **문제**: `is_active` 컬럼 없음 (SQLSTATE[42S22])
|
||||
- **원인**: admin 패널과 DB 스키마 차이
|
||||
- **해결**: `tenant_st_code` 사용 → 삭제되지 않은 모든 테넌트 표시
|
||||
|
||||
### 테스트 결과:
|
||||
- ✅ 테넌트 모델 조회 성공 (7개 활성 테넛트)
|
||||
- ✅ ViewServiceProvider 등록 완료
|
||||
- ✅ 테넌트 선택기 UI 렌더링
|
||||
- ⏳ 브라우저 테스트 대기 중
|
||||
|
||||
### 빌드 결과:
|
||||
- **CSS**: 25.58 KB (gzip: 5.61 KB)
|
||||
- **파일**: `public/build/assets/app-CQyaIaRP.css`
|
||||
|
||||
### 기술적 결정:
|
||||
- **Livewire 대신 Plain Laravel**: 심플함, 페이지 새로고침 방식
|
||||
- **ViewServiceProvider**: 전역 데이터 제공 패턴
|
||||
- **Session 기반**: `selected_tenant_id` 세션 저장
|
||||
- **모든 페이지 공통**: `@include('partials.tenant-selector')` 사용
|
||||
|
||||
### Git 커밋:
|
||||
- ✅ 예정: "feat: 테넌트 선택 기능 구현"
|
||||
24
app/Http/Controllers/TenantController.php
Normal file
24
app/Http/Controllers/TenantController.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TenantController extends Controller
|
||||
{
|
||||
/**
|
||||
* 테넌트 전환
|
||||
*/
|
||||
public function switch(Request $request)
|
||||
{
|
||||
$tenantId = $request->input('tenant_id');
|
||||
|
||||
if ($tenantId === 'all') {
|
||||
$request->session()->forget('selected_tenant_id');
|
||||
} else {
|
||||
$request->session()->put('selected_tenant_id', $tenantId);
|
||||
}
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
}
|
||||
27
app/Models/Tenant.php
Normal file
27
app/Models/Tenant.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Tenant extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'tenants';
|
||||
|
||||
protected $fillable = [
|
||||
'company_name',
|
||||
'code',
|
||||
'tenant_st_code',
|
||||
];
|
||||
|
||||
/**
|
||||
* 활성 테넌트만 조회 (삭제되지 않은 모든 테넌트)
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->whereNull('deleted_at');
|
||||
}
|
||||
}
|
||||
35
app/Providers/ViewServiceProvider.php
Normal file
35
app/Providers/ViewServiceProvider.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\Tenant;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class ViewServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
// 모든 뷰에 테넌트 목록 공유
|
||||
View::composer('*', function ($view) {
|
||||
if (auth()->check()) {
|
||||
$tenants = Tenant::active()
|
||||
->orderBy('company_name')
|
||||
->get(['id', 'company_name', 'code']);
|
||||
|
||||
$view->with('tenants', $tenants);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,4 +2,5 @@
|
||||
|
||||
return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
App\Providers\ViewServiceProvider::class,
|
||||
];
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
@section('page-title', '대시보드')
|
||||
|
||||
@section('content')
|
||||
<!-- Tenant Selector -->
|
||||
@include('partials.tenant-selector')
|
||||
|
||||
<!-- Welcome Card -->
|
||||
<div class="bg-white rounded-lg shadow overflow-hidden">
|
||||
<div class="p-6">
|
||||
|
||||
59
resources/views/partials/tenant-selector.blade.php
Normal file
59
resources/views/partials/tenant-selector.blade.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<!-- Tenant Selector Card -->
|
||||
<div class="bg-white rounded-lg shadow overflow-hidden">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<!-- 좌측: 테넌트 선택 -->
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
<label for="tenant-select" class="text-sm font-medium text-gray-700">테넌트 선택:</label>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('tenant.switch') }}" method="POST" id="tenant-switch-form">
|
||||
@csrf
|
||||
<select
|
||||
name="tenant_id"
|
||||
id="tenant-select"
|
||||
onchange="document.getElementById('tenant-switch-form').submit()"
|
||||
class="border-gray-300 rounded-lg text-sm focus:ring-primary focus:border-primary min-w-[200px]"
|
||||
>
|
||||
<option value="all" {{ session('selected_tenant_id') === null ? 'selected' : '' }}>
|
||||
전체 보기
|
||||
</option>
|
||||
@if($tenants->isNotEmpty())
|
||||
<option disabled>─────────</option>
|
||||
@endif
|
||||
@foreach($tenants as $tenant)
|
||||
<option value="{{ $tenant->id }}" {{ session('selected_tenant_id') == $tenant->id ? 'selected' : '' }}>
|
||||
{{ $tenant->company_name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 우측: 현재 테넌트 정보 -->
|
||||
<div class="flex items-center gap-2">
|
||||
@if(session('selected_tenant_id'))
|
||||
@php
|
||||
$currentTenant = $tenants->firstWhere('id', session('selected_tenant_id'));
|
||||
@endphp
|
||||
@if($currentTenant)
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-primary/10 text-primary">
|
||||
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
{{ $currentTenant->company_name }} 데이터만 표시 중
|
||||
</span>
|
||||
@endif
|
||||
@else
|
||||
<span class="text-xs text-gray-500">
|
||||
전체 테넌트 데이터 표시 중
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Auth\LoginController;
|
||||
use App\Http\Controllers\TenantController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
/*
|
||||
@@ -23,6 +24,9 @@
|
||||
Route::middleware('auth')->group(function () {
|
||||
Route::post('/logout', [LoginController::class, 'logout'])->name('logout');
|
||||
|
||||
// 테넌트 전환
|
||||
Route::post('/tenant/switch', [TenantController::class, 'switch'])->name('tenant.switch');
|
||||
|
||||
// 대시보드 (임시)
|
||||
Route::get('/dashboard', function () {
|
||||
return view('dashboard.index');
|
||||
|
||||
Reference in New Issue
Block a user