Files
2026-04-01 10:30:52 +09:00

9.5 KiB

템플릿 라우트/언어 파일 규칙

관련 문서: index.md | template-basics.md | template-security.md


목차

  1. 라우트 정의 (routes.json)
  2. 템플릿 언어 파일 경로 규칙
  3. TemplateService 구현
  4. API Controller 구현
  5. 라우트 정의
  6. 프론트엔드에서 사용
  7. 보안 규칙
  8. 에러 처리
  9. 모범 사례
  10. template.json과의 관계

라우트 정의 (routes.json)

위치: /templates/[vendor-template]/routes.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 확인, 경로 탈출 방지

언어 파일 구조

// /templates/sirsoft-admin_basic/lang/ko.json
{
  "dashboard": {
    "title": "대시보드",
    "subtitle": "시스템 개요",
    "stats": {
      "users": "총 사용자 수",
      "orders": "주문 수"
    }
  },
  "templates": {
    "list": "템플릿 목록",
    "install": "템플릿 설치",
    "activate": "활성화"
  }
}
// /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

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

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' => '*',
        ]);
    }
}

라우트 정의

// 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 컴포넌트

// 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 템플릿

<!-- 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과의 관계

// 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만 제공

관련 문서