Files
sam-manage/docs/LAYOUT_PATTERN.md
hskwon 76c8a94e4f docs: MNG 프로젝트 문서 정비
- 개발 단계별 문서 추가 (00_OVERVIEW ~ 06_PHASE)
- 기술 표준 문서 추가 (99_TECHNICAL_STANDARDS)
- 개발 프로세스 및 패턴 문서 추가
  - API_FLOW_TESTER_DESIGN, DEV_PROCESS
  - HTMX_API_PATTERN, LAYOUT_PATTERN
  - SETUP_GUIDE, MNG_PROJECT_PLAN
- 프로젝트 관리 문서 추가 (project-management/)
- INDEX.md, MNG_CRITICAL_RULES.md 업데이트
2025-11-30 21:04:19 +09:00

16 KiB

MNG 레이아웃 패턴 가이드

작성일: 2025-01-24 목적: MNG 프로젝트의 표준 페이지 레이아웃 패턴 문서화


📋 목차

  1. 기본 레이아웃 구조
  2. Tenant Selector 패턴
  3. 페이지별 적용 가이드
  4. 컨텐츠 영역 구조
  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

<!-- 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 (예시):

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 (자동 필터링):

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, 카테고리 등

템플릿 구조:

@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

템플릿 구조:

@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 포함)

템플릿 구조:

@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 페이지 헤더

<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 필터 영역

<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 동적 컨텐츠 영역

<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 포함)

@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

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 관리 시스템 레이아웃 기반