Files
sam-manage/docs/HTMX_API_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 HTMX + API 패턴 가이드

작성일: 2025-01-24 목적: MNG 프로젝트의 표준 HTMX + API 패턴 문서화 (Tenant 패턴 기반)

관련 문서:


📋 목차

  1. 패턴 개요
  2. 아키텍처 구조
  3. 구현 가이드
  4. 파일 구조
  5. 체크리스트

1. 패턴 개요

1.1 왜 HTMX + API 패턴인가?

MNG 프로젝트의 표준 아키텍처 패턴입니다.

  • 일관성: 모든 CRUD 기능이 동일한 패턴 사용
  • 성능: 페이지 전체 리로드 없이 동적 업데이트
  • 유지보수성: Blade 템플릿 + HTMX로 간단한 인터랙션
  • 확장성: API는 HTMX와 독립적으로 사용 가능

1.2 기본 원칙

  1. Blade View는 화면만 담당 - 데이터 처리 로직 없음
  2. API Controller는 HTMX와 JSON 모두 지원
  3. HTMX 요청 시 HTML partial 반환
  4. 일반 요청 시 JSON 반환

2. 아키텍처 구조

2.1 전체 흐름도

[Browser]
    ↓ (HTMX Request with HX-Request header)
[Route: web.php]
    ↓ (Blade View 반환)
[Controller: RoleController]
    ↓ (view('roles.index') - 화면만)
[Blade View: roles/index.blade.php]
    ↓ (hx-get="/api/admin/roles")
[API Route: api.php]
    ↓ (API 엔드포인트)
[Api\Admin\RoleController]
    ↓ (Service 호출)
[RoleService]
    ↓ (비즈니스 로직)
[Database]
    ↓
[RoleService]
    ↓ (데이터 반환)
[Api\Admin\RoleController]
    ↓ (HTMX 요청 감지: HX-Request header)
    ↓ (HTML partial 렌더링)
[Blade Partial: roles/partials/table.blade.php]
    ↓ (JSON with html)
[Browser - HTMX]
    ↓ (DOM 업데이트: #role-table)
[User sees updated table]

2.2 컨트롤러 분리

Blade Controller (화면 전용)

// app/Http/Controllers/RoleController.php
class RoleController extends Controller
{
    public function index(): View
    {
        return view('roles.index'); // 화면만 반환
    }

    public function create(): View
    {
        return view('roles.create');
    }

    public function edit(int $id): View
    {
        $role = $this->roleService->getRoleById($id);
        return view('roles.edit', compact('role'));
    }
}

API Controller (데이터 처리)

// app/Http/Controllers/Api/Admin/RoleController.php
class RoleController extends Controller
{
    public function index(Request $request): JsonResponse
    {
        $roles = $this->roleService->getRoles($request->all());

        // HTMX 요청 감지
        if ($request->header('HX-Request')) {
            $html = view('roles.partials.table', compact('roles'))->render();
            return response()->json(['html' => $html]);
        }

        // 일반 API 요청
        return response()->json([
            'success' => true,
            'data' => $roles->items(),
            'meta' => [/*...*/],
        ]);
    }

    public function store(StoreRoleRequest $request): JsonResponse
    {
        $role = $this->roleService->createRole($request->validated());

        if ($request->header('HX-Request')) {
            return response()->json([
                'success' => true,
                'message' => '역할이 생성되었습니다.',
                'redirect' => route('roles.index'),
            ]);
        }

        return response()->json([
            'success' => true,
            'data' => $role,
        ], 201);
    }

    public function destroy(Request $request, int $id): JsonResponse
    {
        $this->roleService->deleteRole($id);

        if ($request->header('HX-Request')) {
            return response()->json([
                'success' => true,
                'message' => '역할이 삭제되었습니다.',
                'action' => 'remove',
            ]);
        }

        return response()->json([
            'success' => true,
            'message' => '역할이 삭제되었습니다.',
        ]);
    }
}

3. 구현 가이드

3.1 Blade View 구조

index.blade.php (메인 화면)

@extends('layouts.app')

@section('content')
<div class="container mx-auto">
    <h1>🔑 역할 관리</h1>

    <!-- 필터 폼 -->
    <form id="filterForm">
        <input type="text" name="search" placeholder="검색...">
        <button type="submit">검색</button>
    </form>

    <!-- 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">
        <!-- 로딩 스피너 -->
        <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>
</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

partials/table.blade.php (HTMX 응답용 HTML partial)

<table>
    <thead>
        <tr>
            <th>ID</th>
            <th>이름</th>
            <th>설명</th>
            <th>권한 수</th>
            <th>액션</th>
        </tr>
    </thead>
    <tbody>
        @forelse($roles as $role)
        <tr>
            <td>{{ $role->id }}</td>
            <td>{{ $role->name }}</td>
            <td>{{ $role->description }}</td>
            <td>{{ $role->permissions_count }}</td>
            <td>
                <a href="{{ route('roles.edit', $role->id) }}">수정</a>
                <button onclick="confirmDelete({{ $role->id }}, '{{ $role->name }}')">삭제</button>
            </td>
        </tr>
        @empty
        <tr>
            <td colspan="5">등록된 역할이 없습니다.</td>
        </tr>
        @endforelse
    </tbody>
</table>

<!-- 페이지네이션 -->
@include('partials.pagination', [
    'paginator' => $roles,
    'target' => '#role-table',
    'includeForm' => '#filterForm'
])

3.2 라우트 설정

web.php (Blade 화면 라우트)

Route::middleware('auth')->group(function () {
    Route::prefix('roles')->name('roles.')->group(function () {
        Route::get('/', [RoleController::class, 'index'])->name('index');
        Route::get('/create', [RoleController::class, 'create'])->name('create');
        Route::get('/{id}/edit', [RoleController::class, 'edit'])->name('edit');
    });
});

api.php (API 엔드포인트)

Route::middleware(['web', 'auth'])->prefix('admin')->name('api.admin.')->group(function () {
    Route::prefix('roles')->name('roles.')->group(function () {
        Route::get('/', [RoleController::class, 'index'])->name('index');
        Route::post('/', [RoleController::class, 'store'])->name('store');
        Route::get('/{id}', [RoleController::class, 'show'])->name('show');
        Route::put('/{id}', [RoleController::class, 'update'])->name('update');
        Route::delete('/{id}', [RoleController::class, 'destroy'])->name('destroy');
    });
});

3.3 HTMX 핵심 개념

hx-get, hx-post, hx-put, hx-delete

<!-- GET 요청 -->
<div hx-get="/api/admin/roles" hx-trigger="load">로딩 중...</div>

<!-- POST 요청 (폼 제출) -->
<form hx-post="/api/admin/roles" hx-target="#role-table">
    <input name="name" required>
    <button type="submit">생성</button>
</form>

<!-- DELETE 요청 (JavaScript) -->
<button onclick="htmx.ajax('DELETE', '/api/admin/roles/1', {target: '#role-table'})">삭제</button>

hx-trigger

<!-- 페이지 로드 시 -->
<div hx-get="/api/admin/roles" hx-trigger="load"></div>

<!-- 커스텀 이벤트 -->
<div hx-get="/api/admin/roles" hx-trigger="filterSubmit from:body"></div>

<!-- 여러 트리거 조합 -->
<div hx-trigger="load, filterSubmit from:body"></div>

hx-include

<!-- 폼 데이터 포함 -->
<div hx-get="/api/admin/roles" hx-include="#filterForm"></div>

hx-headers

<!-- CSRF 토큰 포함 -->
<div hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'></div>

hx-target, hx-swap

<!-- 특정 엘리먼트 타겟 -->
<button hx-delete="/api/admin/roles/1" hx-target="#role-table">삭제</button>

<!-- swap 전략 -->
<div hx-swap="innerHTML">기본값</div>
<div hx-swap="outerHTML">엘리먼트 자체 교체</div>
<div hx-swap="none">응답 무시</div>

4. 파일 구조

4.1 표준 디렉토리 구조

mng/
├── app/
│   ├── Http/
│   │   ├── Controllers/
│   │   │   ├── RoleController.php          # Blade 화면만
│   │   │   └── Api/
│   │   │       └── Admin/
│   │   │           └── RoleController.php  # API 로직
│   │   └── Requests/
│   │       ├── StoreRoleRequest.php
│   │       └── UpdateRoleRequest.php
│   ├── Services/
│   │   └── RoleService.php                 # 비즈니스 로직
│   └── Models/
│       └── Role.php
├── resources/
│   └── views/
│       └── roles/
│           ├── index.blade.php             # 메인 화면
│           ├── create.blade.php            # 생성 화면
│           ├── edit.blade.php              # 수정 화면
│           └── partials/
│               ├── table.blade.php         # HTMX 응답 HTML
│               └── detail.blade.php        # (선택사항)
└── routes/
    ├── web.php                             # Blade 화면 라우트
    └── api.php                             # API 엔드포인트

4.2 Tenant 패턴 참고 파일

학습 및 복사 기준:

  • app/Http/Controllers/TenantController.php → Blade 컨트롤러 패턴
  • app/Http/Controllers/Api/Admin/TenantController.php → API 컨트롤러 패턴
  • resources/views/tenants/index.blade.php → HTMX 메인 화면 패턴
  • resources/views/tenants/partials/table.blade.php → HTML partial 패턴

5. 체크리스트

5.1 구현 전 확인사항

  • Tenant 패턴 파일 확인: tenants/ 디렉토리 구조 참고
  • Service 작성 완료: RoleService.php 비즈니스 로직 구현
  • FormRequest 작성 완료: StoreRoleRequest, UpdateRoleRequest
  • Model 확인: Role.php 관계 설정 확인

5.2 컨트롤러 체크리스트

Blade Controller (app/Http/Controllers/RoleController.php)

  • index()view('roles.index') 반환만
  • create()view('roles.create') 반환만
  • edit($id) → Service로 데이터 조회 → view('roles.edit', compact('role'))

API Controller (app/Http/Controllers/Api/Admin/RoleController.php)

  • index() → HTMX 요청 감지 ($request->header('HX-Request'))
  • HTMX 요청 시 → view('roles.partials.table')->render() → JSON 반환
  • 일반 요청 시 → JSON 데이터 반환
  • store(), update(), destroy() → HTMX 지원
  • HTMX 응답 시 redirect 또는 action 포함

5.3 Blade View 체크리스트

index.blade.php

  • @extends('layouts.app') 상속
  • 필터 폼 <form id="filterForm"> 생성
  • HTMX 동적 영역 <div id="role-table"> 생성
  • hx-get="/api/admin/roles" 설정
  • hx-trigger="load, filterSubmit from:body" 설정
  • hx-include="#filterForm" 설정
  • hx-headers CSRF 토큰 포함
  • 로딩 스피너 추가
  • @push('scripts') HTMX 스크립트 추가
  • 폼 제출 이벤트 핸들러 (filterSubmit 트리거)
  • HTMX 응답 처리 (htmx:afterSwap)
  • 삭제 확인 함수 (confirmDelete)

partials/table.blade.php

  • <table> 구조 생성
  • @forelse 루프로 데이터 출력
  • @empty 케이스 처리
  • 액션 버튼 (수정, 삭제)
  • 삭제 버튼 onclick="confirmDelete()" 연결
  • 페이지네이션 @include('partials.pagination')

5.4 라우트 체크리스트

web.php

  • Route::middleware('auth') 적용
  • Route::prefix('roles')->name('roles.') 그룹
  • GET /rolesindex()
  • GET /roles/createcreate()
  • GET /roles/{id}/editedit()

api.php

  • Route::middleware(['web', 'auth'])->prefix('admin') 적용
  • Route::prefix('roles')->name('api.admin.roles.') 그룹
  • GET /api/admin/rolesindex()
  • POST /api/admin/rolesstore()
  • GET /api/admin/roles/{id}show()
  • PUT /api/admin/roles/{id}update()
  • DELETE /api/admin/roles/{id}destroy()

5.5 테스트 체크리스트

  • 브라우저에서 /roles 접근 → index 화면 로드
  • HTMX 자동 로드 → 테이블 표시
  • 검색 필터 동작 → 테이블 업데이트
  • 삭제 버튼 → 확인 다이얼로그 → 테이블 업데이트
  • 페이지네이션 동작
  • 개발자 도구 Network 탭 → HX-Request 헤더 확인
  • API 응답 JSON 구조 확인 ({html: "..."})

6. 참고사항

6.1 HTMX vs 전통적 방식 비교

항목 전통적 방식 HTMX 방식
폼 제출 <form method="GET"> → 전체 페이지 리로드 hx-get → 부분 업데이트
데이터 로딩 Controller에서 직접 데이터 전달 API 호출 → HTML partial 반환
삭제 동작 <form method="POST"> + @method('DELETE') htmx.ajax('DELETE')
검색 필터 페이지 리로드 + 쿼리스트링 HTMX 트리거 → 부분 업데이트

6.2 주의사항

  1. HTMX 요청 감지 필수

    if ($request->header('HX-Request')) {
        // HTMX 전용 로직
    }
    
  2. CSRF 토큰 포함 필수

    hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'
    
  3. JSON 응답 구조 일관성

    {
        "html": "<table>...</table>",
        "success": true,
        "message": "작업 완료"
    }
    
  4. Blade와 API Controller 분리

    • Blade Controller: 화면만 반환
    • API Controller: 데이터 처리 + HTMX/JSON 응답

7. 마이그레이션 가이드 (Admin → MNG)

7.1 작업 순서

  1. DB 확인 - 테이블이 이미 존재하는지 확인 (migrations 실행 불필요)
  2. Admin 파일 참고 - Controller, Service, Model 복사/참고
  3. 패턴 적용 - HTMX + API 패턴으로 변환
  4. 테스트 - 브라우저에서 동작 확인

7.2 마이그레이션 체크리스트

  • DB 테이블 존재 확인 (roles, permissions, role_has_permissions)
  • Admin Model 참고 (admin/app/Models/Permissions/Role.php)
  • Admin Controller 참고 (비즈니스 로직 추출)
  • Service 작성 (Admin 로직 → MNG Service)
  • Blade Controller 작성 (화면 반환만)
  • API Controller 작성 (HTMX 패턴)
  • Blade View 작성 (Tenant 패턴 기반)
  • 라우트 등록 (web.php, api.php)
  • 브라우저 테스트

작성자: Claude 최종 수정일: 2025-01-24 버전: 1.0 참고: Tenant 관리 시스템 구현 패턴 기반