- 개발 단계별 문서 추가 (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 업데이트
16 KiB
16 KiB
MNG HTMX + API 패턴 가이드
작성일: 2025-01-24 목적: MNG 프로젝트의 표준 HTMX + API 패턴 문서화 (Tenant 패턴 기반)
관련 문서:
- LAYOUT_PATTERN.md - 페이지 레이아웃 및 Tenant Selector 패턴
- 99_TECHNICAL_STANDARDS.md - SAM API Rules 기반 기술 표준
📋 목차
1. 패턴 개요
1.1 왜 HTMX + API 패턴인가?
MNG 프로젝트의 표준 아키텍처 패턴입니다.
- 일관성: 모든 CRUD 기능이 동일한 패턴 사용
- 성능: 페이지 전체 리로드 없이 동적 업데이트
- 유지보수성: Blade 템플릿 + HTMX로 간단한 인터랙션
- 확장성: API는 HTMX와 독립적으로 사용 가능
1.2 기본 원칙
- Blade View는 화면만 담당 - 데이터 처리 로직 없음
- API Controller는 HTMX와 JSON 모두 지원
- HTMX 요청 시 HTML partial 반환
- 일반 요청 시 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-headersCSRF 토큰 포함- 로딩 스피너 추가
@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 /roles→index()GET /roles/create→create()GET /roles/{id}/edit→edit()
api.php
Route::middleware(['web', 'auth'])->prefix('admin')적용Route::prefix('roles')->name('api.admin.roles.')그룹GET /api/admin/roles→index()POST /api/admin/roles→store()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 주의사항
-
HTMX 요청 감지 필수
if ($request->header('HX-Request')) { // HTMX 전용 로직 } -
CSRF 토큰 포함 필수
hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}' -
JSON 응답 구조 일관성
{ "html": "<table>...</table>", "success": true, "message": "작업 완료" } -
Blade와 API Controller 분리
- Blade Controller: 화면만 반환
- API Controller: 데이터 처리 + HTMX/JSON 응답
7. 마이그레이션 가이드 (Admin → MNG)
7.1 작업 순서
- DB 확인 - 테이블이 이미 존재하는지 확인 (migrations 실행 불필요)
- Admin 파일 참고 - Controller, Service, Model 복사/참고
- 패턴 적용 - HTMX + API 패턴으로 변환
- 테스트 - 브라우저에서 동작 확인
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 관리 시스템 구현 패턴 기반