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:
2025-11-21 09:18:19 +09:00
parent 8400352562
commit 661c5adbe6
8 changed files with 239 additions and 1 deletions

View File

@@ -226,3 +226,88 @@ ### 개발 프로세스:
### Git 커밋:
- `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: 테넌트 선택 기능 구현"

View 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
View 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');
}
}

View 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);
}
});
}
}

View File

@@ -2,4 +2,5 @@
return [
App\Providers\AppServiceProvider::class,
App\Providers\ViewServiceProvider::class,
];

View File

@@ -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">

View 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>

View File

@@ -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');