Files
Gnuboard7/docs/extension/template-routing.md
T
2026-04-01 10:30:52 +09:00

367 lines
9.5 KiB
Markdown

# 템플릿 라우트/언어 파일 규칙
> **관련 문서**: [index.md](index.md) | [template-basics.md](template-basics.md) | [template-security.md](template-security.md)
---
## 목차
1. [라우트 정의 (routes.json)](#라우트-정의-routesjson)
2. [템플릿 언어 파일 경로 규칙](#템플릿-언어-파일-경로-규칙)
3. [TemplateService 구현](#templateservice-구현)
4. [API Controller 구현](#api-controller-구현)
5. [라우트 정의](#라우트-정의)
6. [프론트엔드에서 사용](#프론트엔드에서-사용)
7. [보안 규칙](#보안-규칙)
8. [에러 처리](#에러-처리)
9. [모범 사례](#모범-사례)
10. [template.json과의 관계](#templatejson과의-관계)
---
## 라우트 정의 (routes.json)
**위치**: `/templates/[vendor-template]/routes.json`
### 구조
```json
{
"routes": [
{
"path": "/admin/dashboard",
"layout_name": "admin_dashboard",
"permissions": ["core.dashboard.view"],
"meta": {
"title": "대시보드",
"icon": "dashboard"
}
},
{
"path": "/admin/users",
"layout_name": "admin_users_list",
"permissions": ["core.users.view"],
"meta": {
"title": "사용자 관리",
"icon": "users"
}
}
]
}
```
### 필드 설명
| 필드 | 설명 |
|------|------|
| `path` | 라우트 경로 (Laravel 라우트와 일치) |
| `layout_name` | `template_layouts` 테이블의 레이아웃명 |
| `permissions` | 필요한 권한 배열 (빈 배열 시 인증만 필요) |
| `meta` | 메타 정보 (제목, 아이콘 등) |
---
## 템플릿 언어 파일 경로 규칙
```
중요: 템플릿 언어 파일은 API를 통해 제공
✅ 필수: 경로 트래버설 방지 및 로케일 검증
```
### 핵심 원칙
| 항목 | 규칙 |
|------|------|
| **파일 위치** | `/templates/{vendor-template}/lang/{locale}.json` |
| **로케일 형식** | ISO 639-1 (2자리 소문자, 예: `ko`, `en`, `ja`) |
| **제공 방식** | API 엔드포인트 (`/api/templates/{identifier}/lang/{locale}`) |
| **검증** | supported_locales 확인, 경로 탈출 방지 |
### 언어 파일 구조
```json
// /templates/sirsoft-admin_basic/lang/ko.json
{
"dashboard": {
"title": "대시보드",
"subtitle": "시스템 개요",
"stats": {
"users": "총 사용자 수",
"orders": "주문 수"
}
},
"templates": {
"list": "템플릿 목록",
"install": "템플릿 설치",
"activate": "활성화"
}
}
```
```json
// /templates/sirsoft-admin_basic/lang/en.json
{
"dashboard": {
"title": "Dashboard",
"subtitle": "System Overview",
"stats": {
"users": "Total Users",
"orders": "Orders"
}
},
"templates": {
"list": "Template List",
"install": "Install Template",
"activate": "Activate"
}
}
```
---
## TemplateService 구현
```php
<?php
namespace App\Services\Template;
use Illuminate\Support\Facades\File;
class TemplateService
{
/**
* 템플릿 언어 파일 경로 반환
*
* @param string $identifier 템플릿 식별자
* @param string $locale 로케일 (ko, en 등)
* @return array{success: bool, filePath?: string, error?: string}
*/
public function getLanguageFilePath(string $identifier, string $locale): array
{
// 1. 템플릿 활성화 여부 확인
if (!$this->isTemplateActive($identifier)) {
return ['success' => false, 'error' => 'Template not active'];
}
// 2. 로케일 형식 검증 (ISO 639-1: 2자리 소문자)
if (!preg_match('/^[a-z]{2}$/', $locale)) {
return ['success' => false, 'error' => 'Invalid locale format'];
}
// 3. supported_locales 확인
$supportedLocales = config('app.supported_locales', ['ko', 'en']);
if (!in_array($locale, $supportedLocales)) {
return ['success' => false, 'error' => 'Locale not supported'];
}
// 4. 파일 경로 생성
$basePath = base_path("templates/{$identifier}/lang");
$filePath = "{$basePath}/{$locale}.json";
// 5. 경로 트래버설 방지
$realPath = realpath($filePath);
$realBasePath = realpath($basePath);
if (!$realPath || !str_starts_with($realPath, $realBasePath)) {
return ['success' => false, 'error' => 'Invalid file path'];
}
// 6. 파일 존재 확인
if (!File::exists($realPath) || !is_file($realPath)) {
return ['success' => false, 'error' => 'Language file not found'];
}
return ['success' => true, 'filePath' => $realPath];
}
/**
* 템플릿 언어 데이터 로드
*
* @param string $identifier 템플릿 식별자
* @param string $locale 로케일
* @return array
*/
public function getLanguageData(string $identifier, string $locale): array
{
$result = $this->getLanguageFilePath($identifier, $locale);
if (!$result['success']) {
return [];
}
$content = File::get($result['filePath']);
return json_decode($content, true) ?? [];
}
}
```
---
## API Controller 구현
```php
<?php
namespace App\Http\Controllers\Api\Public;
use App\Http\Controllers\Api\Base\PublicBaseController;
use App\Services\Template\TemplateService;
use Illuminate\Http\JsonResponse;
class TemplateLanguageController extends PublicBaseController
{
public function __construct(
private TemplateService $templateService
) {}
/**
* 템플릿 언어 파일 제공
*
* @param string $identifier 템플릿 식별자
* @param string $locale 로케일 (ko, en 등)
* @return JsonResponse
*/
public function getLanguageFile(string $identifier, string $locale): JsonResponse
{
$data = $this->templateService->getLanguageData($identifier, $locale);
if (empty($data)) {
return $this->notFound('messages.template.language_file_not_found');
}
return response()->json($data, 200, [
'Content-Type' => 'application/json',
'Cache-Control' => 'public, max-age=3600',
'Access-Control-Allow-Origin' => '*',
]);
}
}
```
---
## 라우트 정의
```php
// routes/api.php
Route::get('/templates/{identifier}/lang/{locale}', [TemplateLanguageController::class, 'getLanguageFile'])
->where('locale', '[a-z]{2}')
->name('api.templates.language');
```
**주요 포인트**:
- `where('locale', '[a-z]{2}')`: ISO 639-1 형식만 허용
- 라우트 레벨에서 잘못된 형식 차단
---
## 프론트엔드에서 사용
### React 컴포넌트
```javascript
// React 컴포넌트에서 언어 파일 로드
const loadLanguageFile = async (identifier, locale) => {
const response = await fetch(`/api/templates/${identifier}/lang/${locale}`);
if (!response.ok) {
console.error('Failed to load language file');
return {};
}
return await response.json();
};
// 사용 예시
const translations = await loadLanguageFile('sirsoft-admin_basic', 'ko');
console.log(translations.dashboard.title); // "대시보드"
```
### Blade 템플릿
```php
<!-- resources/views/app.blade.php -->
<script>
window.TEMPLATE_LANG_URL = "{{ route('api.templates.language', [
'identifier' => $activeTemplate->identifier,
'locale' => app()->getLocale()
]) }}";
</script>
```
---
## 보안 규칙
### 허용 (✅)
- 활성화된 템플릿만 접근 가능
- ISO 639-1 형식 검증 (정규표현식: `/^[a-z]{2}$/`)
- supported_locales 화이트리스트 확인
- 경로 트래버설 방지 (`realpath()`, `str_starts_with()`)
- 파일 확장자 검증 (`.json`만 허용)
- 적절한 캐싱 헤더 (`Cache-Control: max-age=3600`)
### 금지 (❌)
- 비활성 템플릿 언어 파일 접근 금지
- 임의의 파일 경로 접근 금지
- supported_locales 외 로케일 접근 금지
---
## 에러 처리
| 요청 | 응답 | 사유 |
|------|------|------|
| `GET /api/templates/sirsoft-admin_basic/lang/ko-KR` | 404 Not Found | 라우트 패턴 불일치 |
| `GET /api/templates/sirsoft-admin_basic/lang/ja` | 404 Not Found | supported_locales에 없음 |
| `GET /api/templates/inactive-template/lang/ko` | 404 Not Found | 템플릿 미활성화 |
| `GET /api/templates/sirsoft-admin_basic/lang/fr` | 404 Not Found | 파일 존재하지 않음 |
---
## 모범 사례
### 권장 사항 (✅)
- 모든 템플릿은 최소 `ko.json`, `en.json` 필수 제공
- JSON 파일은 UTF-8 인코딩 사용
- 키 구조는 계층적으로 구성 (`dashboard.stats.users`)
- API를 통한 동적 로딩으로 초기 로딩 시간 최적화
- 브라우저 캐싱 활용 (max-age=3600)
### 금지 사항 (❌)
- 언어 파일에 HTML 포함 금지 (XSS 방지)
- 하드코딩된 텍스트 사용 금지 (모든 텍스트는 언어 파일에서)
---
## template.json과의 관계
```json
// template.json
{
"identifier": "sirsoft-admin_basic",
"locales": ["ko", "en"], // 이 배열에 포함된 언어만 제공
// ...
}
```
### 언어 결정 규칙
- `template.json`의 `locales` 배열이 제공 가능한 언어 명세
- `config('app.supported_locales')`와 교집합이 실제 제공 언어
- 예: `template.json`에 `["ko", "en", "ja"]`, `supported_locales`에 `["ko", "en"]`이면 `ko`, `en`만 제공
---
## 관련 문서
- [index.md](index.md) - 확장 시스템 가이드 인덱스
- [template-basics.md](template-basics.md) - 템플릿 기초
- [template-security.md](template-security.md) - 템플릿 보안 정책
- [template-caching.md](template-caching.md) - 템플릿 캐싱 전략