# MNG 테이블 레이아웃 표준 > **기준 페이지**: `/permissions` (권한 관리) > **작성일**: 2025-11-25 > **목적**: mng 프로젝트의 모든 테이블 페이지에서 일관된 레이아웃과 UX를 제공 --- ## 📋 목차 1. [페이지 구조](#1-페이지-구조) 2. [페이지 헤더](#2-페이지-헤더) 3. [필터 영역](#3-필터-영역) 4. [테이블 구조](#4-테이블-구조) 5. [페이지네이션](#5-페이지네이션) 6. [기술 스택](#6-기술-스택) 7. [체크리스트](#7-체크리스트) --- ## 1. 페이지 구조 ### 1.1 전체 레이아웃 순서 ```blade @extends('layouts.app') @section('content')
@endsection ``` ### 1.2 파일 구조 ``` resources/views/{resource}/ ├── index.blade.php # 메인 페이지 (레이아웃만) ├── create.blade.php # 생성 폼 ├── edit.blade.php # 수정 폼 └── partials/ └── table.blade.php # 테이블 + 페이지네이션 ``` --- ## 2. 페이지 헤더 ### 2.1 기본 구조 ```blade

{페이지 제목}

+ {액션 버튼 레이블}
``` ### 2.2 스타일 규칙 - **제목**: `text-2xl font-bold text-gray-800` - **액션 버튼**: `bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition` - **간격**: `mb-6` (하단 여백) --- ## 3. 필터 영역 ### 3.1 기본 구조 ```blade
``` ### 3.2 스타일 규칙 - **컨테이너**: `bg-white rounded-lg shadow-sm p-4 mb-6` - **폼**: `flex gap-4` (가로 배치, 간격 4) - **검색 입력**: `flex-1` (가변 폭) - **드롭다운**: `w-48` (고정 폭 192px) - **버튼**: `bg-gray-600 hover:bg-gray-700 text-white px-6 py-2 rounded-lg transition` ### 3.3 JavaScript 이벤트 ```javascript document.getElementById('filterForm').addEventListener('submit', function(e) { e.preventDefault(); htmx.trigger('#{resource}-table', 'filterSubmit'); }); ``` --- ## 4. 테이블 구조 ### 4.1 HTMX 컨테이너 ```blade
``` ### 4.2 테이블 Partial (`partials/table.blade.php`) ```blade
@forelse($items as $item) @empty @endforelse
{컬럼명} 액션
{{ $item->속성 }} 수정
등록된 {항목명}이(가) 없습니다.
@include('partials.pagination', [ 'paginator' => $items, 'target' => '#{resource}-table', 'includeForm' => '#filterForm' ]) ``` ### 4.3 스타일 규칙 #### 테이블 헤더 - **배경**: `bg-gray-50` - **텍스트**: `text-sm font-semibold text-gray-700 uppercase tracking-wider` - **정렬**: `text-left` (일반), `text-right` (액션) - **패딩**: `px-4 py-2` #### 테이블 본문 - **행 구분**: `divide-y divide-gray-200` - **셀 패딩**: `px-4 py-3` - **텍스트**: `text-sm text-gray-900` (일반), `text-gray-500` (보조) - **공백 처리**: `whitespace-nowrap` (줄바꿈 방지) #### 액션 버튼 - **수정**: `text-blue-600 hover:text-blue-900 mr-3` - **삭제**: `text-red-600 hover:text-red-900` ### 4.4 배지 스타일 (선택사항) #### Inline 스타일 배지 ```blade {배지 텍스트} ``` #### 배지 색상 시스템 | 용도 | 배경색 (RGB) | 텍스트색 (RGB) | 사용 예시 | |------|-------------|---------------|----------| | **Primary (파란색)** | `rgb(219 234 254)` | `rgb(30 64 175)` | Guard, 기본 태그 | | **Success (초록색)** | `rgb(220 252 231)` | `rgb(21 128 61)` | 역할, 활성 상태 | | **Warning (노란색)** | `rgb(254 249 195)` | `rgb(133 77 14)` | 부서, 경고 | | **Danger (빨간색)** | `rgb(254 202 202)` | `rgb(153 27 27)` | 삭제 권한 | | **Gray (회색)** | `rgb(243 244 246)` | `rgb(31 41 55)` | 메뉴 태그, 중립 | | **Orange (주황색)** | `rgb(254 215 170)` | `rgb(154 52 18)` | 수정 권한 | | **Purple (보라색)** | `rgb(233 213 255)` | `rgb(107 33 168)` | 승인 권한 | | **Cyan (청록색)** | `rgb(207 250 254)` | `rgb(14 116 144)` | 내보내기 권한 | #### Tailwind 클래스 배지 (대안) ```blade {배지 텍스트} ``` ### 4.5 Empty State ```blade @empty 등록된 {항목명}이(가) 없습니다. @endforelse ``` --- ## 5. 페이지네이션 ### 5.1 Include 방식 ```blade @include('partials.pagination', [ 'paginator' => $items, 'target' => '#{resource}-table', 'includeForm' => '#filterForm' ]) ``` ### 5.2 페이지네이션 기능 #### 데스크톱 (>=640px) - **전체 개수 표시**: "전체 N개 중 X ~ Y" - **페이지당 항목 수 선택**: 10/20/30/50/100/200/500개씩 - **네비게이션 버튼**: - 처음 (첫 페이지로) - 이전 (이전 페이지로) - 페이지 번호 (최대 10개 표시) - 다음 (다음 페이지로) - 끝 (마지막 페이지로) #### 모바일 (<640px) - 이전/다음 버튼만 표시 - 간소화된 네비게이션 ### 5.3 JavaScript 핸들러 ```javascript // 페이지 변경 function handlePageChange(page) { const form = document.getElementById('filterForm'); const formData = new FormData(form); formData.append('page', page); const params = new URLSearchParams(formData).toString(); htmx.ajax('GET', `/api/admin/{resource}?${params}`, { target: '#{resource}-table', swap: 'innerHTML' }); } // 페이지당 항목 수 변경 function handlePerPageChange(perPage) { const form = document.getElementById('filterForm'); const formData = new FormData(form); formData.append('per_page', perPage); formData.append('page', 1); // 첫 페이지로 리셋 const params = new URLSearchParams(formData).toString(); htmx.ajax('GET', `/api/admin/{resource}?${params}`, { target: '#{resource}-table', swap: 'innerHTML' }); } ``` ### 5.4 스타일 규칙 - **컨테이너**: `bg-white px-4 py-3 border-t border-gray-200 sm:px-6` - **전체 개수**: `text-sm text-gray-700`, 숫자는 `font-medium` - **페이지당 항목 선택**: `px-3 py-1 border border-gray-300 rounded-lg text-sm` - **버튼 (활성)**: `bg-white text-gray-700 hover:bg-gray-50` - **버튼 (비활성)**: `bg-gray-100 text-gray-400 cursor-not-allowed` - **현재 페이지**: `bg-blue-50 text-blue-600` --- ## 6. 기술 스택 ### 6.1 필수 라이브러리 ```html ``` ### 6.2 API 컨트롤러 ```php namespace App\Http\Controllers\Api\Admin; class {Resource}Controller extends Controller { public function __construct( private {Resource}Service $service ) {} public function index(Request $request) { $items = $this->service->get{Resources}( $request->all(), $request->input('per_page', 20) ); // HTMX 요청 시 부분 HTML 반환 if ($request->header('HX-Request')) { return view('{resource}.partials.table', compact('items')); } // 일반 요청 시 JSON 반환 return response()->json([ 'success' => true, 'data' => $items->items(), 'meta' => [ 'current_page' => $items->currentPage(), 'total' => $items->total(), 'per_page' => $items->perPage(), 'last_page' => $items->lastPage(), ], ]); } } ``` ### 6.3 라우트 ```php // web.php (화면) Route::get('/{resource}', [{Resource}Controller::class, 'index']) ->name('{resource}.index'); // api.php (데이터) Route::prefix('api/admin')->group(function () { Route::get('/{resource}', [Api\Admin\{Resource}Controller::class, 'index']); Route::post('/{resource}', [Api\Admin\{Resource}Controller::class, 'store']); Route::get('/{resource}/{id}', [Api\Admin\{Resource}Controller::class, 'show']); Route::put('/{resource}/{id}', [Api\Admin\{Resource}Controller::class, 'update']); Route::delete('/{resource}/{id}', [Api\Admin\{Resource}Controller::class, 'destroy']); }); ``` --- ## 7. 체크리스트 ### 7.1 페이지 생성 체크리스트 ```markdown ## 새 테이블 페이지 생성 체크리스트 ### 파일 구조 - [ ] `resources/views/{resource}/index.blade.php` 생성 - [ ] `resources/views/{resource}/partials/table.blade.php` 생성 - [ ] `app/Http/Controllers/{Resource}Controller.php` 생성 - [ ] `app/Http/Controllers/Api/Admin/{Resource}Controller.php` 생성 - [ ] `app/Services/{Resource}Service.php` 생성 ### 페이지 헤더 - [ ] 제목 (`text-2xl font-bold text-gray-800`) - [ ] 액션 버튼 (`bg-blue-600 hover:bg-blue-700`) - [ ] 하단 여백 (`mb-6`) ### 필터 영역 (선택사항) - [ ] 검색 입력 (`flex-1`) - [ ] 드롭다운 필터 (`w-48`) - [ ] 검색 버튼 (`bg-gray-600`) - [ ] JavaScript 이벤트 핸들러 ### 테이블 - [ ] HTMX 컨테이너 (`hx-get`, `hx-trigger`, `hx-include`) - [ ] 로딩 스피너 - [ ] 테이블 헤더 (`bg-gray-50`) - [ ] 테이블 본문 (`divide-y divide-gray-200`) - [ ] Empty State - [ ] 액션 버튼 (수정, 삭제) ### 페이지네이션 - [ ] `@include('partials.pagination')` 추가 - [ ] `handlePageChange()` 함수 구현 - [ ] `handlePerPageChange()` 함수 구현 ### API - [ ] `index()` 메서드 (HTMX + JSON 분기) - [ ] Service 계층 (비즈니스 로직) - [ ] FormRequest (검증) - [ ] 라우트 등록 ### 테스트 - [ ] 필터 검색 동작 확인 - [ ] 페이지네이션 동작 확인 - [ ] 액션 버튼 동작 확인 - [ ] 반응형 레이아웃 확인 (모바일/데스크톱) ``` ### 7.2 스타일 일관성 체크 ```markdown ## 스타일 일관성 체크리스트 ### 색상 - [ ] Primary 버튼: `bg-blue-600 hover:bg-blue-700` - [ ] Secondary 버튼: `bg-gray-600 hover:bg-gray-700` - [ ] 텍스트: `text-gray-800` (제목), `text-gray-700` (본문), `text-gray-500` (보조) ### 간격 - [ ] 페이지 헤더 하단: `mb-6` - [ ] 필터 영역 하단: `mb-6` - [ ] 필터 요소 간격: `gap-4` - [ ] 테이블 셀 패딩: `px-4 py-3` (본문), `px-4 py-2` (헤더) ### 둥근 모서리 - [ ] 버튼: `rounded-lg` - [ ] 입력 필드: `rounded-lg` - [ ] 배지: `rounded` (0.375rem) - [ ] 컨테이너: `rounded-lg` ### 그림자 - [ ] 컨테이너: `shadow-sm` - [ ] 페이지네이션: `shadow-sm` ``` --- ## 8. 참고 사항 ### 8.1 권한 관리 페이지 특수 기능 권한 관리 페이지는 다음과 같은 특수 기능을 포함합니다: 1. **권한명 파싱**: `menu:{menu_id}.{permission_type}` 형식 파싱 2. **권한 타입 배지**: V(조회), C(생성), U(수정), D(삭제), A(승인), E(내보내기), M(관리) 3. **메뉴 태그**: 회색 배지로 메뉴 ID 표시 4. **역할/부서 배지**: 여러 개 배지를 가로 나열 (`flex flex-nowrap gap-1`) 이러한 특수 기능은 다른 페이지에서 필요에 따라 적용하거나 생략할 수 있습니다. ### 8.2 성능 최적화 - **Eager Loading**: 관계 데이터를 미리 로드하여 N+1 쿼리 방지 ```php $items = Model::with(['relation1', 'relation2'])->paginate(20); ``` - **페이지네이션**: 기본값 20개, 최대 500개까지 지원 - **HTMX**: 부분 HTML만 교체하여 빠른 반응성 제공 ### 8.3 접근성 - **시맨틱 HTML**: ``, ``, `` 사용 - **버튼 레이블**: 명확한 액션 설명 - **키보드 네비게이션**: 버튼과 링크에 포커스 가능 --- ## 9. 예제 코드 ### 9.1 최소 구현 예제 #### `resources/views/products/index.blade.php` ```blade @extends('layouts.app') @section('title', '제품 관리') @section('content')

제품 관리

+ 새 제품
@endsection @push('scripts') @endpush ``` #### `resources/views/products/partials/table.blade.php` ```blade
@forelse($products as $product) @empty @endforelse
ID 제품명 생성일 액션
{{ $product->id }} {{ $product->name }} {{ $product->created_at?->format('Y-m-d H:i') ?? '-' }} 수정
등록된 제품이 없습니다.
@include('partials.pagination', [ 'paginator' => $products, 'target' => '#product-table', 'includeForm' => '#filterForm' ]) ``` --- ## 10. React 테이블 (Blade + Babel) > **참조 파일**: `resources/views/barobill/etax/index.blade.php` > **작성일**: 2026-02-03 Blade 템플릿 내에서 React(Babel)를 사용하는 경우, 테이블 컬럼 너비 설정 시 주의해야 할 사항이 있습니다. ### 10.1 colgroup을 사용한 컬럼 너비 지정 React에서 `table-fixed` 레이아웃과 함께 컬럼 너비를 정확하게 지정하려면 `colgroup`을 사용해야 합니다. Tailwind의 `w-[]` 클래스만으로는 정확한 너비 적용이 어려울 수 있습니다. #### 잘못된 예시 (Tailwind 클래스만 사용) ```jsx // ❌ 테이블 셀에 Tailwind 클래스만 적용 - 비율이 의도대로 안 될 수 있음
품목명 수량 단가
``` #### 올바른 예시 (colgroup 사용) ```jsx // ✅ colgroup으로 명시적 너비 지정 {/* 품목명 - 가장 넓게 */} {/* 수량 - 작게 고정 */} {/* 단가 - 수량보다 넓게 */} {/* 공급가액 */} {/* 세액 */} {/* 금액 */} {/* 과세 (select) */} {/* 삭제 버튼 */} {/* ... */}
품목명 수량 단가
``` ### 10.2 Blade 템플릿에서 React 스타일 객체 이스케이프 **중요**: Blade 템플릿(`.blade.php`)에서 React의 스타일 객체 `{{ }}`를 사용하면 Blade가 이를 PHP echo 구문으로 해석하여 에러가 발생합니다. #### 에러 발생 코드 ```jsx // ❌ Blade가 {{ }}를 PHP 변수로 해석 → 에러 발생 // Error: Unknown named parameter $tableLayout ``` #### 해결 방법: `@{{ }}` 사용 ```jsx // ✅ @를 붙여 Blade 이스케이프 처리
``` `@{{ }}`를 사용하면 Blade가 해당 구문을 처리하지 않고 그대로 `{{ }}`로 출력하여 React/Babel이 정상적으로 해석합니다. ### 10.3 입력 테이블 컬럼 비율 가이드 품목 입력 테이블의 권장 컬럼 비율: | 컬럼 | 너비 | 설명 | |------|------|------| | 품목명 | **30%** | 텍스트 입력, 가장 넓게 | | 수량 | **60px** | 작은 숫자 입력, 고정 너비 | | 단가 | **100px** | 금액 입력, 수량보다 넓게 | | 공급가액 | **12%** | 계산된 금액 표시 | | 세액 | **10%** | 계산된 금액 표시 | | 금액 | **12%** | 합계 금액 표시 | | 과세유형 | **70px** | select 박스 | | 삭제 | **40px** | 버튼 | #### 비율 설정 원칙 1. **입력 필드**는 내용에 맞는 적절한 너비 확보 2. **수량**은 보통 작은 숫자이므로 좁게 (60px) 3. **단가/금액**은 큰 숫자를 표시하므로 넉넉하게 4. **품목명**은 텍스트 입력이므로 가장 넓게 (%) 5. **버튼/아이콘**은 고정 픽셀 (px) ### 10.4 전체 예제 코드 ```jsx // Blade 템플릿 내 React 코드 (@push('scripts') 내부)
{/* 데이터 행들 */}
품목명 수량 단가 공급가액 세액 금액 과세
``` --- ## 11. 문서 이력 | 버전 | 날짜 | 작성자 | 변경 내용 | |------|------|--------|----------| | 1.0 | 2025-11-25 | Claude | 초안 작성 (권한 관리 페이지 기반) | | 1.1 | 2026-02-03 | Claude | React 테이블 섹션 추가 (colgroup, Blade 이스케이프) | --- ## 12. 문의 이 문서에 대한 문의사항이나 개선 제안은 프로젝트 관리자에게 연락하세요.