# 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')
    <!-- ① 페이지 헤더 -->
    <div class="flex justify-between items-center mb-6">
        <!-- 제목 + 액션 버튼 -->
    </div>

    <!-- ② 필터 영역 (선택사항) -->
    <div class="bg-white rounded-lg shadow-sm p-4 mb-6">
        <!-- 검색, 필터 폼 -->
    </div>

    <!-- ③ 테이블 영역 (HTMX) -->
    <div id="{resource}-table"
         hx-get="/api/admin/{resource}"
         hx-trigger="load, filterSubmit from:body"
         hx-include="#filterForm"
         class="bg-white rounded-lg shadow-sm overflow-hidden">
        <!-- 로딩 스피너 -->
        <!-- 테이블 partial 로드됨 -->
    </div>
@endsection
```

### 1.2 파일 구조

```
resources/views/{resource}/
├── index.blade.php              # 메인 페이지 (레이아웃만)
├── create.blade.php             # 생성 폼
├── edit.blade.php               # 수정 폼
└── partials/
    └── table.blade.php          # 테이블 + 페이지네이션
```

---

## 2. 페이지 헤더

### 2.1 기본 구조

```blade
<div class="flex justify-between items-center mb-6">
    <h1 class="text-2xl font-bold text-gray-800">{페이지 제목}</h1>
    <a href="{{ route('{resource}.create') }}"
       class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition">
        + {액션 버튼 레이블}
    </a>
</div>
```

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

        <!-- 드롭다운 필터 (선택사항) -->
        <div class="w-48">
            <select name="{filter_name}"
                    class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
                <option value="">전체 {필터명}</option>
                <!-- 옵션들 -->
            </select>
        </div>

        <!-- 검색 버튼 -->
        <button type="submit"
                class="bg-gray-600 hover:bg-gray-700 text-white px-6 py-2 rounded-lg transition">
            검색
        </button>
    </form>
</div>
```

### 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
<div id="{resource}-table"
     hx-get="/api/admin/{resource}"
     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>
```

### 4.2 테이블 Partial (`partials/table.blade.php`)

```blade
<div class="overflow-x-auto">
    <table class="min-w-full divide-y divide-gray-200">
        <thead class="bg-gray-50">
            <tr>
                <th class="px-4 py-2 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">
                    {컬럼명}
                </th>
                <!-- 추가 컬럼들 -->
                <th class="px-4 py-2 text-right text-sm font-semibold text-gray-700 uppercase tracking-wider">
                    액션
                </th>
            </tr>
        </thead>
        <tbody class="bg-white divide-y divide-gray-200">
            @forelse($items as $item)
            <tr>
                <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
                    {{ $item->속성 }}
                </td>
                <!-- 추가 컬럼들 -->
                <td class="px-4 py-3 whitespace-nowrap text-right text-sm font-medium">
                    <a href="{{ route('{resource}.edit', $item->id) }}"
                       class="text-blue-600 hover:text-blue-900 mr-3">
                        수정
                    </a>
                    <button onclick="confirmDelete({{ $item->id }}, '{{ $item->name }}')"
                            class="text-red-600 hover:text-red-900">
                        삭제
                    </button>
                </td>
            </tr>
            @empty
            <tr>
                <td colspan="{컬럼수}" class="px-6 py-12 text-center text-gray-500">
                    등록된 {항목명}이(가) 없습니다.
                </td>
            </tr>
            @endforelse
        </tbody>
    </table>
</div>

<!-- 페이지네이션 -->
@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
<span style="display: inline-flex; align-items: center; padding: 0.125rem 0.5rem; font-size: 0.75rem; font-weight: 500; border-radius: 0.375rem; background-color: rgb(219 234 254); color: rgb(30 64 175);">
    {배지 텍스트}
</span>
```

#### 배지 색상 시스템

| 용도 | 배경색 (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
<span class="inline-flex items-center px-2 py-0.5 text-xs font-medium rounded bg-blue-100 text-blue-800">
    {배지 텍스트}
</span>
```

### 4.5 Empty State

```blade
@empty
<tr>
    <td colspan="{컬럼수}" class="px-6 py-12 text-center text-gray-500">
        등록된 {항목명}이(가) 없습니다.
    </td>
</tr>
@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
<!-- HTMX -->
<script src="https://unpkg.com/htmx.org@1.9.10"></script>

<!-- Tailwind CSS (이미 레이아웃에 포함) -->
```

### 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**: `<table>`, `<thead>`, `<tbody>` 사용
- **버튼 레이블**: 명확한 액션 설명
- **키보드 네비게이션**: 버튼과 링크에 포커스 가능

---

## 9. 예제 코드

### 9.1 최소 구현 예제

#### `resources/views/products/index.blade.php`

```blade
@extends('layouts.app')

@section('title', '제품 관리')

@section('content')
    <!-- 페이지 헤더 -->
    <div class="flex justify-between items-center mb-6">
        <h1 class="text-2xl font-bold text-gray-800">제품 관리</h1>
        <a href="{{ route('products.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>

    <!-- 테이블 영역 -->
    <div id="product-table"
         hx-get="/api/admin/products"
         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>
    document.getElementById('filterForm').addEventListener('submit', function(e) {
        e.preventDefault();
        htmx.trigger('#product-table', 'filterSubmit');
    });

    function confirmDelete(id, name) {
        if (confirm(`"${name}" 제품을 삭제하시겠습니까?`)) {
            fetch(`/api/admin/products/${id}`, {
                method: 'DELETE',
                headers: {
                    'X-CSRF-TOKEN': '{{ csrf_token() }}',
                    'Accept': 'application/json'
                }
            })
            .then(response => response.json())
            .then(data => {
                if (data.success) {
                    htmx.trigger('#product-table', 'filterSubmit');
                    alert(data.message);
                } else {
                    alert(data.message);
                }
            })
            .catch(error => {
                alert('제품 삭제 중 오류가 발생했습니다.');
            });
        }
    }
</script>
@endpush
```

#### `resources/views/products/partials/table.blade.php`

```blade
<div class="overflow-x-auto">
    <table class="min-w-full divide-y divide-gray-200">
        <thead class="bg-gray-50">
            <tr>
                <th class="px-4 py-2 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">ID</th>
                <th class="px-4 py-2 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">제품명</th>
                <th class="px-4 py-2 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">생성일</th>
                <th class="px-4 py-2 text-right text-sm font-semibold text-gray-700 uppercase tracking-wider">액션</th>
            </tr>
        </thead>
        <tbody class="bg-white divide-y divide-gray-200">
            @forelse($products as $product)
            <tr>
                <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
                    {{ $product->id }}
                </td>
                <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
                    {{ $product->name }}
                </td>
                <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
                    {{ $product->created_at?->format('Y-m-d H:i') ?? '-' }}
                </td>
                <td class="px-4 py-3 whitespace-nowrap text-right text-sm font-medium">
                    <a href="{{ route('products.edit', $product->id) }}"
                       class="text-blue-600 hover:text-blue-900 mr-3">
                        수정
                    </a>
                    <button onclick="confirmDelete({{ $product->id }}, '{{ $product->name }}')"
                            class="text-red-600 hover:text-red-900">
                        삭제
                    </button>
                </td>
            </tr>
            @empty
            <tr>
                <td colspan="4" class="px-6 py-12 text-center text-gray-500">
                    등록된 제품이 없습니다.
                </td>
            </tr>
            @endforelse
        </tbody>
    </table>
</div>

@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 클래스만 적용 - 비율이 의도대로 안 될 수 있음
<table className="w-full text-sm table-fixed">
    <thead>
        <tr>
            <th className="w-[30%]">품목명</th>
            <th className="w-[60px]">수량</th>
            <th className="w-[100px]">단가</th>
        </tr>
    </thead>
</table>
```

#### 올바른 예시 (colgroup 사용)

```jsx
// ✅ colgroup으로 명시적 너비 지정
<table className="w-full text-sm" style=@{{tableLayout: 'fixed'}}>
    <colgroup>
        <col style=@{{width: '30%'}} />   {/* 품목명 - 가장 넓게 */}
        <col style=@{{width: '60px'}} />  {/* 수량 - 작게 고정 */}
        <col style=@{{width: '100px'}} /> {/* 단가 - 수량보다 넓게 */}
        <col style=@{{width: '12%'}} />   {/* 공급가액 */}
        <col style=@{{width: '10%'}} />   {/* 세액 */}
        <col style=@{{width: '12%'}} />   {/* 금액 */}
        <col style=@{{width: '70px'}} />  {/* 과세 (select) */}
        <col style=@{{width: '40px'}} />  {/* 삭제 버튼 */}
    </colgroup>
    <thead className="bg-stone-100 border-b border-stone-200">
        <tr>
            <th className="px-3 py-2.5 text-left font-medium text-stone-700">품목명</th>
            <th className="px-2 py-2.5 text-right font-medium text-stone-700">수량</th>
            <th className="px-2 py-2.5 text-right font-medium text-stone-700">단가</th>
            {/* ... */}
        </tr>
    </thead>
</table>
```

### 10.2 Blade 템플릿에서 React 스타일 객체 이스케이프

**중요**: Blade 템플릿(`.blade.php`)에서 React의 스타일 객체 `{{ }}`를 사용하면 Blade가 이를 PHP echo 구문으로 해석하여 에러가 발생합니다.

#### 에러 발생 코드

```jsx
// ❌ Blade가 {{ }}를 PHP 변수로 해석 → 에러 발생
<table style={{tableLayout: 'fixed'}}>
// Error: Unknown named parameter $tableLayout
```

#### 해결 방법: `@{{ }}` 사용

```jsx
// ✅ @를 붙여 Blade 이스케이프 처리
<table style=@{{tableLayout: 'fixed'}}>
<col style=@{{width: '30%'}} />
```

`@{{ }}`를 사용하면 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') 내부)
<table className="w-full text-sm" style=@{{tableLayout: 'fixed'}}>
    <colgroup>
        <col style=@{{width: '30%'}} />
        <col style=@{{width: '60px'}} />
        <col style=@{{width: '100px'}} />
        <col style=@{{width: '12%'}} />
        <col style=@{{width: '10%'}} />
        <col style=@{{width: '12%'}} />
        <col style=@{{width: '70px'}} />
        <col style=@{{width: '40px'}} />
    </colgroup>
    <thead className="bg-stone-100 border-b border-stone-200">
        <tr>
            <th className="px-3 py-2.5 text-left font-medium text-stone-700">품목명</th>
            <th className="px-2 py-2.5 text-right font-medium text-stone-700">수량</th>
            <th className="px-2 py-2.5 text-right font-medium text-stone-700">단가</th>
            <th className="px-2 py-2.5 text-right font-medium text-stone-700">공급가액</th>
            <th className="px-2 py-2.5 text-right font-medium text-stone-700">세액</th>
            <th className="px-2 py-2.5 text-right font-medium text-stone-700">금액</th>
            <th className="px-1 py-2.5 text-center font-medium text-stone-700">과세</th>
            <th className="px-1 py-2.5 text-center font-medium text-stone-700"></th>
        </tr>
    </thead>
    <tbody className="divide-y divide-stone-100">
        {/* 데이터 행들 */}
    </tbody>
</table>
```

---

## 11. 문서 이력

| 버전 | 날짜 | 작성자 | 변경 내용 |
|------|------|--------|----------|
| 1.0 | 2025-11-25 | Claude | 초안 작성 (권한 관리 페이지 기반) |
| 1.1 | 2026-02-03 | Claude | React 테이블 섹션 추가 (colgroup, Blade 이스케이프) |

---

## 12. 문의

이 문서에 대한 문의사항이나 개선 제안은 프로젝트 관리자에게 연락하세요.