504 lines
16 KiB
Markdown
504 lines
16 KiB
Markdown
|
|
# MNG 레이아웃 패턴 가이드
|
||
|
|
|
||
|
|
**작성일:** 2025-01-24
|
||
|
|
**목적:** MNG 프로젝트의 표준 페이지 레이아웃 패턴 문서화
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📋 목차
|
||
|
|
|
||
|
|
1. [기본 레이아웃 구조](#1-기본-레이아웃-구조)
|
||
|
|
2. [Tenant Selector 패턴](#2-tenant-selector-패턴)
|
||
|
|
3. [페이지별 적용 가이드](#3-페이지별-적용-가이드)
|
||
|
|
4. [컨텐츠 영역 구조](#4-컨텐츠-영역-구조)
|
||
|
|
5. [체크리스트](#5-체크리스트)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 1. 기본 레이아웃 구조
|
||
|
|
|
||
|
|
### 1.1 전체 구조
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────────────────────────────────────────────────┐
|
||
|
|
│ Header (상단) │
|
||
|
|
│ - 로고, 사용자 정보, 알림 등 │
|
||
|
|
├──────────┬──────────────────────────────────────────┤
|
||
|
|
│ │ │
|
||
|
|
│ │ <!-- Tenant Selector (공통) --> │
|
||
|
|
│ │ ┌─────────────────────────────────┐ │
|
||
|
|
│ │ │ 테넌트 선택 드롭다운 │ │
|
||
|
|
│ Sidebar │ │ [전체보기] [A회사] [B회사] │ │
|
||
|
|
│ │ └─────────────────────────────────┘ │
|
||
|
|
│ (좌측 │ │
|
||
|
|
│ 메뉴) │ <!-- 페이지 헤더 --> │
|
||
|
|
│ │ 페이지 제목 [+ 버튼] │
|
||
|
|
│ │ │
|
||
|
|
│ │ <!-- 필터 영역 --> │
|
||
|
|
│ │ [검색] [필터1] [필터2] [검색버튼] │
|
||
|
|
│ │ │
|
||
|
|
│ │ <!-- 테이블/컨텐츠 영역 --> │
|
||
|
|
│ │ ┌─────────────────────────────────┐ │
|
||
|
|
│ │ │ 데이터 테이블 또는 컨텐츠 │ │
|
||
|
|
│ │ │ │ │
|
||
|
|
│ │ │ (HTMX 동적 로딩 영역) │ │
|
||
|
|
│ │ └─────────────────────────────────┘ │
|
||
|
|
│ │ │
|
||
|
|
└──────────┴──────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
### 1.2 레이아웃 파일 구조
|
||
|
|
|
||
|
|
```
|
||
|
|
resources/views/
|
||
|
|
├── layouts/
|
||
|
|
│ └── app.blade.php # 메인 레이아웃
|
||
|
|
├── partials/
|
||
|
|
│ ├── sidebar.blade.php # 좌측 메뉴
|
||
|
|
│ ├── header.blade.php # 상단 헤더
|
||
|
|
│ ├── tenant-selector.blade.php # 테넌트 선택기 (공통)
|
||
|
|
│ └── pagination.blade.php # 페이지네이션
|
||
|
|
└── [feature]/
|
||
|
|
├── index.blade.php # 목록 페이지
|
||
|
|
├── create.blade.php # 생성 페이지
|
||
|
|
├── edit.blade.php # 수정 페이지
|
||
|
|
└── partials/
|
||
|
|
└── table.blade.php # HTMX 응답용 테이블
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 2. Tenant Selector 패턴
|
||
|
|
|
||
|
|
### 2.1 역할과 목적
|
||
|
|
|
||
|
|
**Tenant Selector는 모든 데이터 관리 페이지 상단에 위치하는 공통 컴포넌트입니다.**
|
||
|
|
|
||
|
|
- **목적**: 사용자가 특정 테넌트의 데이터만 필터링하여 볼 수 있도록 함
|
||
|
|
- **위치**: `@section('content')` 직후, 페이지 컨텐츠 최상단
|
||
|
|
- **예외**: 테넌트 관리 페이지 (`tenants/index.blade.php`)는 제외
|
||
|
|
|
||
|
|
### 2.2 Tenant Selector 구조
|
||
|
|
|
||
|
|
**파일**: `resources/views/partials/tenant-selector.blade.php`
|
||
|
|
|
||
|
|
```blade
|
||
|
|
<!-- 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>...</svg>
|
||
|
|
<label>테넌트 선택:</label>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<form action="{{ route('tenant.switch') }}" method="POST">
|
||
|
|
@csrf
|
||
|
|
<select name="tenant_id" onchange="this.form.submit()">
|
||
|
|
<option value="all">전체 보기</option>
|
||
|
|
@foreach($globalTenants as $tenant)
|
||
|
|
<option value="{{ $tenant->id }}">
|
||
|
|
{{ $tenant->company_name }}
|
||
|
|
</option>
|
||
|
|
@endforeach
|
||
|
|
</select>
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 우측: 현재 테넌트 정보 -->
|
||
|
|
<div class="flex items-center gap-2">
|
||
|
|
@if(session('selected_tenant_id'))
|
||
|
|
<span class="badge">
|
||
|
|
{{ $currentTenant->company_name }} 데이터만 표시 중
|
||
|
|
</span>
|
||
|
|
@else
|
||
|
|
<span>전체 테넌트 데이터 표시 중</span>
|
||
|
|
@endif
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2.3 Tenant Selector 동작 방식
|
||
|
|
|
||
|
|
1. **드롭다운 변경** → 폼 자동 제출
|
||
|
|
2. **POST /tenant/switch** → TenantController@switch
|
||
|
|
3. **세션 저장** → `session('selected_tenant_id')`
|
||
|
|
4. **페이지 리로드** → 선택된 테넌트 데이터만 표시
|
||
|
|
|
||
|
|
### 2.4 백엔드 연동
|
||
|
|
|
||
|
|
**TenantController@switch** (예시):
|
||
|
|
```php
|
||
|
|
public function switch(Request $request): RedirectResponse
|
||
|
|
{
|
||
|
|
$tenantId = $request->input('tenant_id');
|
||
|
|
|
||
|
|
if ($tenantId === 'all') {
|
||
|
|
session()->forget('selected_tenant_id');
|
||
|
|
} else {
|
||
|
|
session(['selected_tenant_id' => $tenantId]);
|
||
|
|
}
|
||
|
|
|
||
|
|
return redirect()->back();
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Service Layer** (자동 필터링):
|
||
|
|
```php
|
||
|
|
public function getRoles(array $filters = []): LengthAwarePaginator
|
||
|
|
{
|
||
|
|
$tenantId = session('selected_tenant_id');
|
||
|
|
|
||
|
|
$query = Role::query();
|
||
|
|
|
||
|
|
// Tenant 필터링
|
||
|
|
if ($tenantId) {
|
||
|
|
$query->where('tenant_id', $tenantId);
|
||
|
|
}
|
||
|
|
|
||
|
|
return $query->paginate(15);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 3. 페이지별 적용 가이드
|
||
|
|
|
||
|
|
### 3.1 일반 데이터 관리 페이지 (Tenant Selector 포함)
|
||
|
|
|
||
|
|
**적용 대상**: 역할, 사용자, 부서, 제품, 자재, BOM, 카테고리 등
|
||
|
|
|
||
|
|
**템플릿 구조**:
|
||
|
|
```blade
|
||
|
|
@extends('layouts.app')
|
||
|
|
|
||
|
|
@section('title', '역할 관리')
|
||
|
|
|
||
|
|
@section('content')
|
||
|
|
<!-- Tenant Selector (필수) -->
|
||
|
|
@include('partials.tenant-selector')
|
||
|
|
|
||
|
|
<!-- 페이지 헤더 -->
|
||
|
|
<div class="flex justify-between items-center mt-6 mb-6">
|
||
|
|
<h1 class="text-2xl font-bold text-gray-800">🔑 역할 관리</h1>
|
||
|
|
<a href="{{ route('roles.create') }}" class="btn-primary">
|
||
|
|
+ 새 역할
|
||
|
|
</a>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 필터 영역 -->
|
||
|
|
<div class="bg-white rounded-lg shadow-sm p-4 mb-6">
|
||
|
|
<form id="filterForm">
|
||
|
|
<!-- 검색, 필터 -->
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 컨텐츠 영역 (HTMX) -->
|
||
|
|
<div id="role-table" hx-get="/api/admin/roles">
|
||
|
|
<!-- 로딩 스피너 -->
|
||
|
|
</div>
|
||
|
|
@endsection
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.2 테넌트 관리 페이지 (Tenant Selector 제외)
|
||
|
|
|
||
|
|
**적용 대상**: `tenants/index.blade.php`
|
||
|
|
|
||
|
|
**템플릿 구조**:
|
||
|
|
```blade
|
||
|
|
@extends('layouts.app')
|
||
|
|
|
||
|
|
@section('title', '테넌트 관리')
|
||
|
|
|
||
|
|
@section('content')
|
||
|
|
<!-- Tenant Selector 없음 -->
|
||
|
|
|
||
|
|
<!-- 페이지 헤더 -->
|
||
|
|
<div class="flex justify-between items-center mb-6">
|
||
|
|
<h1 class="text-2xl font-bold text-gray-800">🏢 테넌트 관리</h1>
|
||
|
|
<a href="{{ route('tenants.create') }}" class="btn-primary">
|
||
|
|
+ 새 테넌트
|
||
|
|
</a>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 필터 영역 -->
|
||
|
|
<div class="bg-white rounded-lg shadow-sm p-4 mb-6">
|
||
|
|
<form id="filterForm">
|
||
|
|
<!-- 검색, 상태 필터, 삭제된 항목 포함 -->
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 컨텐츠 영역 (HTMX) -->
|
||
|
|
<div id="tenant-table" hx-get="/api/admin/tenants">
|
||
|
|
<!-- 로딩 스피너 -->
|
||
|
|
</div>
|
||
|
|
@endsection
|
||
|
|
```
|
||
|
|
|
||
|
|
**이유**: 테넌트 관리는 모든 테넌트를 관리하는 페이지이므로 테넌트 필터링이 불필요
|
||
|
|
|
||
|
|
### 3.3 대시보드 (Tenant Selector 포함)
|
||
|
|
|
||
|
|
**템플릿 구조**:
|
||
|
|
```blade
|
||
|
|
@extends('layouts.app')
|
||
|
|
|
||
|
|
@section('title', '대시보드')
|
||
|
|
|
||
|
|
@section('content')
|
||
|
|
<!-- Tenant Selector (포함) -->
|
||
|
|
@include('partials.tenant-selector')
|
||
|
|
|
||
|
|
<!-- Welcome Card -->
|
||
|
|
<div class="bg-white rounded-lg shadow mt-6">
|
||
|
|
<div class="p-6">
|
||
|
|
<h2>환영합니다!</h2>
|
||
|
|
<!-- 통계, 퀵 액션 등 -->
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
@endsection
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 4. 컨텐츠 영역 구조
|
||
|
|
|
||
|
|
### 4.1 페이지 헤더
|
||
|
|
|
||
|
|
```blade
|
||
|
|
<div class="flex justify-between items-center {{ $hasTenantSelector ? 'mt-6 mb-6' : 'mb-6' }}">
|
||
|
|
<h1 class="text-2xl font-bold text-gray-800">
|
||
|
|
[아이콘] 페이지 제목
|
||
|
|
</h1>
|
||
|
|
<a href="{{ route('resource.create') }}" class="btn-primary">
|
||
|
|
+ 새 항목
|
||
|
|
</a>
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
**주의사항**:
|
||
|
|
- Tenant Selector가 있는 경우 → `mt-6` 추가 (위쪽 여백)
|
||
|
|
- Tenant Selector가 없는 경우 → `mt-6` 생략
|
||
|
|
|
||
|
|
### 4.2 필터 영역
|
||
|
|
|
||
|
|
```blade
|
||
|
|
<div class="bg-white rounded-lg shadow-sm p-4 mb-6">
|
||
|
|
<form id="filterForm" class="flex gap-4">
|
||
|
|
<!-- 검색 입력 -->
|
||
|
|
<div class="flex-1">
|
||
|
|
<input type="text" name="search" placeholder="검색...">
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 추가 필터 (선택사항) -->
|
||
|
|
<div class="w-48">
|
||
|
|
<select name="status">
|
||
|
|
<option value="">전체 상태</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 검색 버튼 -->
|
||
|
|
<button type="submit" class="btn-secondary">검색</button>
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4.3 HTMX 동적 컨텐츠 영역
|
||
|
|
|
||
|
|
```blade
|
||
|
|
<div id="resource-table"
|
||
|
|
hx-get="/api/admin/resources"
|
||
|
|
hx-trigger="load, filterSubmit from:body"
|
||
|
|
hx-include="#filterForm"
|
||
|
|
hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'
|
||
|
|
class="bg-white rounded-lg shadow-sm overflow-hidden">
|
||
|
|
<!-- 로딩 스피너 -->
|
||
|
|
<div class="flex justify-center items-center p-12">
|
||
|
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 5. 체크리스트
|
||
|
|
|
||
|
|
### 5.1 Tenant Selector 포함 여부 확인
|
||
|
|
|
||
|
|
**포함해야 하는 페이지** (✅):
|
||
|
|
- [ ] 역할 관리 (`roles/index.blade.php`)
|
||
|
|
- [ ] 사용자 관리 (`users/index.blade.php`)
|
||
|
|
- [ ] 부서 관리 (`departments/index.blade.php`)
|
||
|
|
- [ ] 제품 관리 (`products/index.blade.php`)
|
||
|
|
- [ ] 자재 관리 (`materials/index.blade.php`)
|
||
|
|
- [ ] BOM 관리 (`boms/index.blade.php`)
|
||
|
|
- [ ] 카테고리 관리 (`categories/index.blade.php`)
|
||
|
|
- [ ] 대시보드 (`dashboard/index.blade.php`)
|
||
|
|
|
||
|
|
**제외해야 하는 페이지** (❌):
|
||
|
|
- [ ] 테넌트 관리 (`tenants/index.blade.php`)
|
||
|
|
- [ ] 시스템 설정 (전역 설정 페이지)
|
||
|
|
- [ ] 감사 로그 (전체 시스템 로그)
|
||
|
|
|
||
|
|
### 5.2 레이아웃 구현 체크리스트
|
||
|
|
|
||
|
|
**페이지 구조**:
|
||
|
|
- [ ] `@extends('layouts.app')` 상속
|
||
|
|
- [ ] `@section('title', '페이지 제목')` 정의
|
||
|
|
- [ ] `@section('content')` 내부 구조:
|
||
|
|
- [ ] `@include('partials.tenant-selector')` (필요 시)
|
||
|
|
- [ ] 페이지 헤더 (`mt-6` 여백 확인)
|
||
|
|
- [ ] 필터 영역
|
||
|
|
- [ ] HTMX 동적 컨텐츠 영역
|
||
|
|
|
||
|
|
**스타일 일관성**:
|
||
|
|
- [ ] 카드 스타일: `bg-white rounded-lg shadow-sm`
|
||
|
|
- [ ] 버튼 스타일: `btn-primary`, `btn-secondary`
|
||
|
|
- [ ] 간격 일관성: `mb-6`, `mt-6`, `p-4`, `p-6`
|
||
|
|
|
||
|
|
**HTMX 설정**:
|
||
|
|
- [ ] `hx-get` 엔드포인트 설정
|
||
|
|
- [ ] `hx-trigger="load, filterSubmit from:body"` 설정
|
||
|
|
- [ ] `hx-include="#filterForm"` 설정
|
||
|
|
- [ ] CSRF 토큰 헤더 포함
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 6. 예시 코드
|
||
|
|
|
||
|
|
### 6.1 완전한 페이지 예시 (Tenant Selector 포함)
|
||
|
|
|
||
|
|
```blade
|
||
|
|
@extends('layouts.app')
|
||
|
|
|
||
|
|
@section('title', '역할 관리')
|
||
|
|
|
||
|
|
@section('content')
|
||
|
|
<!-- Tenant Selector -->
|
||
|
|
@include('partials.tenant-selector')
|
||
|
|
|
||
|
|
<!-- 페이지 헤더 -->
|
||
|
|
<div class="flex justify-between items-center mt-6 mb-6">
|
||
|
|
<h1 class="text-2xl font-bold text-gray-800">🔑 역할 관리</h1>
|
||
|
|
<a href="{{ route('roles.create') }}"
|
||
|
|
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition">
|
||
|
|
+ 새 역할
|
||
|
|
</a>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 필터 영역 -->
|
||
|
|
<div class="bg-white rounded-lg shadow-sm p-4 mb-6">
|
||
|
|
<form id="filterForm" class="flex gap-4">
|
||
|
|
<div class="flex-1">
|
||
|
|
<input type="text" name="search" 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>
|
||
|
|
<button type="submit"
|
||
|
|
class="bg-gray-600 hover:bg-gray-700 text-white px-6 py-2 rounded-lg transition">
|
||
|
|
검색
|
||
|
|
</button>
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 테이블 영역 (HTMX로 로드) -->
|
||
|
|
<div id="role-table"
|
||
|
|
hx-get="/api/admin/roles"
|
||
|
|
hx-trigger="load, filterSubmit from:body"
|
||
|
|
hx-include="#filterForm"
|
||
|
|
hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'
|
||
|
|
class="bg-white rounded-lg shadow-sm overflow-hidden">
|
||
|
|
<!-- 로딩 스피너 -->
|
||
|
|
<div class="flex justify-center items-center p-12">
|
||
|
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
@endsection
|
||
|
|
|
||
|
|
@push('scripts')
|
||
|
|
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||
|
|
<script>
|
||
|
|
// 폼 제출 시 HTMX 이벤트 트리거
|
||
|
|
document.getElementById('filterForm').addEventListener('submit', function(e) {
|
||
|
|
e.preventDefault();
|
||
|
|
htmx.trigger('#role-table', 'filterSubmit');
|
||
|
|
});
|
||
|
|
|
||
|
|
// HTMX 응답 처리
|
||
|
|
document.body.addEventListener('htmx:afterSwap', function(event) {
|
||
|
|
if (event.detail.target.id === 'role-table') {
|
||
|
|
const response = JSON.parse(event.detail.xhr.response);
|
||
|
|
if (response.html) {
|
||
|
|
event.detail.target.innerHTML = response.html;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// 삭제 확인
|
||
|
|
window.confirmDelete = function(id, name) {
|
||
|
|
if (confirm(`"${name}" 역할을 삭제하시겠습니까?`)) {
|
||
|
|
htmx.ajax('DELETE', `/api/admin/roles/${id}`, {
|
||
|
|
target: '#role-table',
|
||
|
|
swap: 'none',
|
||
|
|
headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' }
|
||
|
|
}).then(() => {
|
||
|
|
htmx.trigger('#role-table', 'filterSubmit');
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
</script>
|
||
|
|
@endpush
|
||
|
|
```
|
||
|
|
|
||
|
|
### 6.2 ViewComposer로 $globalTenants 자동 주입
|
||
|
|
|
||
|
|
**파일**: `app/Providers/ViewServiceProvider.php`
|
||
|
|
|
||
|
|
```php
|
||
|
|
use Illuminate\Support\Facades\View;
|
||
|
|
use App\Models\Tenant;
|
||
|
|
|
||
|
|
public function boot(): void
|
||
|
|
{
|
||
|
|
// 모든 뷰에 $globalTenants 변수 자동 주입
|
||
|
|
View::composer('partials.tenant-selector', function ($view) {
|
||
|
|
$view->with('globalTenants', Tenant::orderBy('company_name')->get());
|
||
|
|
});
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 7. 주의사항
|
||
|
|
|
||
|
|
### 7.1 Tenant Selector 관련
|
||
|
|
|
||
|
|
1. **ViewComposer 필수**: `$globalTenants` 변수가 자동으로 주입되도록 ViewComposer 설정 필요
|
||
|
|
2. **세션 관리**: `selected_tenant_id` 세션이 Service Layer에서 자동으로 필터링에 사용됨
|
||
|
|
3. **페이지 리로드**: 테넌트 변경 시 전체 페이지가 리로드되어 모든 데이터가 새로 로드됨
|
||
|
|
4. **HTMX 연동**: Tenant 변경 후 HTMX 테이블도 자동으로 새로 로드됨
|
||
|
|
|
||
|
|
### 7.2 레이아웃 스타일
|
||
|
|
|
||
|
|
1. **컨테이너 없음**: `@section('content')` 내부에는 기본 컨테이너가 없음
|
||
|
|
- Tenant Selector는 자체 패딩(`p-6`) 포함
|
||
|
|
- 나머지 컨텐츠는 페이지별로 여백 조정
|
||
|
|
|
||
|
|
2. **간격 일관성**:
|
||
|
|
- Tenant Selector 하단: `mb-6` (내부 카드에 포함)
|
||
|
|
- 페이지 헤더: `mt-6 mb-6` (Tenant Selector가 있을 때)
|
||
|
|
- 필터 영역: `mb-6`
|
||
|
|
- 컨텐츠 영역: 별도 여백 불필요
|
||
|
|
|
||
|
|
3. **반응형 디자인**: Tailwind CSS 유틸리티 클래스 사용
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
**작성자:** Claude
|
||
|
|
**최종 수정일:** 2025-01-24
|
||
|
|
**버전:** 1.0
|
||
|
|
**참고**: Dashboard, Tenant 관리 시스템 레이아웃 기반
|