Files
HeuJung b10e89bef0 fix(core,board,ecommerce,page,gdpr,ckeditor5,kginicis,admin_basic,basic): 목록 조회·검증 계층 정비 및 감사 지적 전건 처리
목록 조회 성능 축(깊은 OFFSET·정렬·색인)과 그 검증 계층에서 계획서 전수검수와 3회에 걸친
감사가 지적한 항목을 처리했다. 세부 경위는 의 각 회차 문서에 있다.

검증 계층 — 컨트롤러가 base Request 를 직접 주입받던 확장 13곳을 전용 FormRequest 로 옮겼다.
옮기면서 기존 동작 계약은 그대로 두었다: 상한 초과 limit 을 거부하지 않고 상한까지 반환하던
공개 API, per_page 를 범위로 조정하던 관리자 목록, 미지원 period 를 year 로 해석하던 인기글
목록 모두 종전과 같은 응답을 낸다. 상한/폴백을 rules 로 승격시키면 200 이던 응답이 422 가
되어 기존 링크가 깨지므로, 규칙은 타입만 닫고 클램프·폴백은 접근자가 맡는다. period 는
접근자가 닫힌 집합만 반환해 캐시 키 공간도 함께 닫힌다.

ckeditor5 이미지 업로드만 ResponseHelper 봉투를 쓰지 않는다. 응답을 파싱하는 주체가 CDN 으로
로드되는 상위 CKEditor5 43.3.1 의 SimpleUploadAdapter 라 규약을 바꿀 수 없어, 각 응답 지점에
사유를 명시한 면제를 부착하고 근거를 API 문서에 남겼다.

하네스 — 룰 5개의 대상 경로 패턴이 매처와 맞지 않아 번들 확장 컨트롤러가 검사 대상에서
통째로 빠져 있었다. 패턴을 고치자 확장 위반 17건이 드러나 전건 처리했다. severity 오타가
요약 집계 양쪽에 안 잡혀 "0 error" 로 보고되던 문제도 런너 사전 검증으로 막았다.

정렬 게이트 대조 하네스는 관계 정렬 변형만 쓰는 저장소를 탐지하지 못한 채 통과시키고 있었다.
탐지·제외·인자 파싱을 함께 고치고 단위 테스트를 신설했다.
2026-08-02 15:29:29 +09:00

10 KiB

모듈 다국어 시스템

관련 문서: index.md | module-basics.md | module-routing.md


TL;DR (5초 요약)

1. 백엔드: /src/lang/{locale}/*.php → __('vendor-module::key') — 이 경로만 로드된다
2. 프론트엔드: /resources/lang/{locale}.json → $t:key (resources/lang 아래 *.php 는 미로드)
3. JSON에 moduleIdentifier 포함 금지! (자동 병합됨)
4. 지원 언어: ko, en 필수
5. 키 충돌 방지: 모듈별 고유 prefix 사용 권장

목차


핵심 원칙

다국어 파일 구조

중요: 백엔드와 프론트엔드 다국어 파일 경로 구분
✅ 필수: resources/lang/*.json 파일에 moduleIdentifier 없이 작성
구분 파일 경로 형식 사용처
백엔드 /src/lang/{locale}/*.php PHP 배열 Laravel __() 함수
프론트엔드 /resources/lang/{locale}.json JSON 레이아웃 JSON $t: 문법

모듈의 백엔드 다국어는 src/lang 한 곳이다. TranslationServiceProvider 가 그 경로만 등록하고, 확장 설치 검증(ValidatesTranslationPath)도 같은 경로를 강제한다. 언어팩 시스템 (LanguagePackService · LanguagePackServiceProvider) 도 같은 경로를 스캔한다.

resources/lang/{locale}/*.php 에 백엔드 문구를 두면 아무도 읽지 않는다 — 화면에는 src/lang 값이 나오고, 그 파일을 고쳐도 반영되지 않으며, 그 사실이 화면에 드러나지 않는다. resources/lang 아래에서 유효한 것은 프런트엔드용 {locale}.json 과 partial/{locale}/*.json 뿐이다. (플러그인은 lang/{locale}/*.php, 템플릿은 lang/{locale}.json 이 각각 그 자리다.)

코어 자체 다국어 자원도 동일 구조 — 코어는 lang/{ko,en}/*.php (백엔드) + lang/{ko,en}.json (+ lang/partial/{ko,en}/*.json) (프론트엔드) 를 사용한다. 모듈/플러그인/템플릿/코어 모두 같은 디렉토리 구조와 $partial 디렉티브 메커니즘을 공유한다. 상세: docs/extension/language-packs.md "코어 다국어 자원의 위치".

moduleIdentifier 규칙

✅ 프론트엔드 다국어 파일에는 moduleIdentifier 없이 순수 키만 작성
✅ 템플릿 서빙 시 자동으로 moduleIdentifier를 최상위 키로 병합
❌ 내부 JSON 파일에 { "sirsoft-sample": { ... } } 형태로 작성 금지

백엔드 다국어

파일 위치

modules/sirsoft-sample/
└── src/
    └── lang/
        ├── ko/
        │   └── messages.php
        └── en/
            └── messages.php

중요: 백엔드 다국어 파일은 반드시 src/lang/ 디렉토리에 위치해야 합니다. TranslationServiceProvider가 이 경로에서 파일을 자동으로 로드합니다.

파일 형식 (PHP 배열)

// src/lang/ko/messages.php
return [
    'product_created' => '상품이 생성되었습니다.',
    'order_confirmed' => '주문이 확인되었습니다.',
    'validation' => [
        'name_required' => '상품명은 필수 항목입니다.',
        'price_min' => '가격은 0 이상이어야 합니다.',
    ],
];
// lang/en/messages.php
return [
    'product_created' => 'Product has been created.',
    'order_confirmed' => 'Order has been confirmed.',
    'validation' => [
        'name_required' => 'Product name is required.',
        'price_min' => 'Price must be at least 0.',
    ],
];

사용법

Laravel의 __() 함수를 사용합니다. 모듈 다국어는 더블 콜론(::) 문법을 사용합니다:

// 모듈 네임스페이스 사용 (더블 콜론 ::)
__('sirsoft-sample::messages.product_created');
// 결과: '상품이 생성되었습니다.' (ko 로케일)

// 중첩 키
__('sirsoft-sample::messages.validation.name_required');
// 결과: '상품명은 필수 항목입니다.'

// 파라미터 치환
__('sirsoft-sample::messages.greeting', ['name' => '홍길동']);
// messages.php: 'greeting' => ':name님, 환영합니다.'
// 결과: '홍길동님, 환영합니다.'

// ❌ 잘못된 사용 (점 . 사용)
__('sirsoft-sample.messages.product_created');  // 작동하지 않음!

// ✅ 올바른 사용 (더블 콜론 :: 사용)
__('sirsoft-sample::messages.product_created');  // 정상 작동

프론트엔드 다국어

파일 위치

modules/sirsoft-sample/
└── resources/
    └── lang/
        ├── ko.json
        └── en.json

파일 형식 (JSON)

// resources/lang/ko.json
{
  "admin": {
    "index": {
      "title": "샘플 항목 관리",
      "description": "샘플 모듈의 항목을 관리합니다"
    },
    "create": {
      "title": "새 항목 생성",
      "submit": "생성하기"
    }
  },
  "messages": {
    "save_success": "저장되었습니다.",
    "delete_confirm": "정말 삭제하시겠습니까?"
  }
}
// resources/lang/en.json
{
  "admin": {
    "index": {
      "title": "Sample Item Management",
      "description": "Manage sample module items"
    },
    "create": {
      "title": "Create New Item",
      "submit": "Create"
    }
  },
  "messages": {
    "save_success": "Saved successfully.",
    "delete_confirm": "Are you sure you want to delete?"
  }
}

moduleIdentifier 자동 병합

템플릿 서빙 시 시스템이 자동으로 moduleIdentifier를 최상위 키로 병합합니다:

// 원본 파일 (resources/lang/ko.json)
{
  "admin": {
    "index": {
      "title": "샘플 항목 관리"
    }
  }
}

// 서빙 후 (자동 변환)
{
  "sirsoft-sample": {
    "admin": {
      "index": {
        "title": "샘플 항목 관리"
      }
    }
  }
}

코어/템플릿 도메인과의 충돌

모듈 lang 데이터는 모듈 identifier wrap (sirsoft-sample.*) 으로 코어/템플릿 도메인과 자연 격리된다. 다만 코어/템플릿 lang 과 동일 top-level 키 (layout_editor, core, auth 등) 를 모듈이 직접 정의하는 것은 권장하지 않는다 — TemplateService::getLanguageDataWithModules 는 deep merge (재귀 병합) 정책이라 양쪽 leaf 가 보존되긴 하지만, 동일 키 경로 leaf 충돌 시 모듈이 우선순위가 높아 코어/템플릿 leaf 를 덮어쓴다. 의도된 오버라이드가 아닌 한 모듈 identifier wrap 안에만 정의.

상세 병합 정책: docs/extension/language-packs.md / docs/frontend/data-binding-i18n.md


$partial Fragment 시스템

개요

프론트엔드 다국어 JSON 파일이 커지면 $partial 디렉티브를 사용하여 도메인별로 분리할 수 있습니다. ResolvesLanguageFragments trait이 JSON 로드 시 fragment를 자동으로 병합합니다.

디렉토리 구조

modules/_bundled/vendor-module/
└── resources/lang/
    ├── ko.json                       ← 메인 JSON (fragment 참조 포함)
    ├── en.json
    └── partial/                      ← fragment 파일 디렉토리
        ├── ko/
        │   ├── common.json
        │   └── admin/
        │       ├── locale.json
        │       ├── products.json
        │       └── orders.json
        └── en/
            ├── common.json
            └── admin/
                └── ...

$partial 디렉티브 문법

메인 JSON 파일에서 $partial 키로 fragment 파일을 참조합니다:

{
    "common": {
        "$partial": "partial/ko/common.json"
    },
    "admin": {
        "locale": {
            "$partial": "partial/ko/admin/locale.json"
        },
        "products": {
            "$partial": "partial/ko/admin/products.json"
        }
    }
}
  • $partial 값은 resources/lang/ 기준 상대 경로
  • $partial이 포함된 객체는 fragment 파일의 내용으로 완전 교체됨
  • fragment 파일 내부에서 다시 $partial을 사용한 중첩 참조 가능 (최대 깊이 제한 적용)

Fragment 해석 규칙

규칙 설명
최대 깊이 MAX_FRAGMENT_DEPTH = 10 (초과 시 해석 중단)
순환 참조 fragmentStack으로 감지, 순환 시 해당 fragment 무시
파일 미존재 해당 $partial 객체가 빈 객체로 대체
재귀 해석 fragment 내 $partial도 재귀적으로 해석

사용 기준

상황 권장
JSON 파일 500줄 이하 단일 파일 유지
JSON 파일 500줄 초과 도메인별 $partial 분리
admin/user 섹션 분리 필요 partial/{locale}/admin/, partial/{locale}/user/

참고: $partial은 프론트엔드 JSON(resources/lang/)에서만 사용됩니다. 백엔드 PHP(src/lang/)에서는 사용할 수 없습니다.


레이아웃 JSON에서 사용

기본 문법

레이아웃 JSON에서 $t: 접두사를 사용하여 다국어 키를 참조합니다:

{
  "id": "page-title",
  "type": "basic",
  "name": "H1",
  "props": {
    "text": "$t:sirsoft-sample.admin.index.title"
  }
}

키 구조

$t:[moduleIdentifier].[섹션].[하위섹션].[키]

예시:
$t:sirsoft-sample.admin.index.title
$t:sirsoft-sample.messages.save_success

다양한 사용 예시

{
  "id": "page-header",
  "type": "composite",
  "name": "PageHeader",
  "props": {
    "title": "$t:sirsoft-sample.admin.index.title",
    "description": "$t:sirsoft-sample.admin.index.description"
  }
}
{
  "id": "submit-button",
  "type": "basic",
  "name": "Button",
  "props": {
    "text": "$t:sirsoft-sample.admin.create.submit",
    "variant": "primary"
  }
}

관련 문서