feat(i18n): 사이드메뉴 국제화 키 매핑 적용 및 리소스 추가

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
Yu Sung
2026-05-08 16:27:12 +09:00
parent aba06048b3
commit 84ff76d58c
7 changed files with 236 additions and 1 deletions

View File

@@ -0,0 +1,140 @@
사이드메뉴 국제화(i18n) 적용 계획
초안 생성: 2026-05-08
1. 배경/목표
- 현재 SideMenu는 서버에서 메뉴 목록을 받아 렌더링한다.
- 클라이언트 코드(`src/components/SideMenu.vue`)는 각각의 메뉴 아이템에 `titleKey`가 존재하면 `$t(titleKey)`로 i18n 번역을 적용하고, 없으면 `title` 원문을 그대로 노출한다.
- 목표: 서버가 내려주는 메뉴 중 i18n 적용 대상 텍스트는 설정된 언어(ko/en/ja)에 맞춰 번역이 적용되도록 한다. 추가로, 메뉴 텍스트 사전을 문서에 유지하여 번역 항목을 관리한다.
2. 현행 동작 확인(근거)
- 렌더링 로직(발췌):
- 단일 항목: `item.titleKey ? $t(item.titleKey) : item.title`
- 그룹 타이틀: `item.titleKey ? $t(item.titleKey) : item.title`
- 자식 항목: `childItem.titleKey ? $t(childItem.titleKey) : childItem.title`
- 기본(fallback) 메뉴 키: `comp.sideMenu.creators`, `comp.sideMenu.calc.*` 일련의 키 사용 중.
- 관련 파일: `src/components/SideMenu.vue`, `src/locales/ko.json`, `src/locales/en.json`, `src/locales/ja.json`
3. 제안 방법(서버/클라이언트 계약)
- 서버 응답 스키마 확정(제안):
```json
[
{
"titleKey": "comp.sideMenu.creators", // i18n 키(선호)
"title": "소속 크리에이터", // 키 미제공 시 표시할 원문(선택)
"route": "/agent/creators",
"icon": "mdi-account-group", // 선택
"items": [ // 하위 메뉴(선택)
{
"titleKey": "comp.sideMenu.calc.live",
"title": "크리에이터별 라이브 정산",
"route": "/agent/calculate/live"
}
]
}
]
```
- 클라이언트 적용 원칙(변경 없음):
- `titleKey`가 존재하면 `$t(titleKey)`로 번역 표시.
- `titleKey`가 없으면 `title` 값을 그대로 표시(서버 원문 노출).
- 그룹/자식 항목 동일 규칙 적용.
- i18n 키 네이밍 규칙: `comp.sideMenu.<group?>.<name>`를 사용(기존 패턴 유지).
3-1. 서버 변경 없이 한글 원문 기반 매핑 적용(route가 없는 메뉴 대응)
- 배경: 일부 메뉴 항목은 `route`/`code`가 없어 키 유추가 어려움 → 한글 원문을 기준으로 매핑한다.
- 방법: 클라이언트에서 "한글 원문 → i18n 키" 사전을 유지하고, 메뉴 응답 수신 시 전처리로 `titleKey`를 주입한다.
- 정규화 권장: `NFC` 정규화 + 다중 공백 축소 + `trim` 적용 후 매핑해 표기 미세 차이를 흡수한다.
- 운영 원칙:
- "기존에 있는 키는 재사용"한다(예: `comp.sideMenu.calc.live`, `comp.sideMenu.calc.community`, `comp.sideMenu.calc.channelDonation`, `view.signature.title`).
- 서버가 "채널후원 정산"(공백 없음)으로 내려와도 키는 `comp.sideMenu.calc.channelDonation`으로 매핑해, 화면 ko는 기존 리소스값 "채널 후원 정산"으로 노출한다.
- 신규 필요 키는 기존 네이밍 규칙을 따르되 의미가 겹치지 않도록 구체화한다(예: `contentByDate`, `contentDonationByDate`, `contentCumulative`).
3-2. 한글 원문 → i18n 키 매핑(초안)
- 그룹 타이틀:
- `콘텐츠 관리` → `comp.sideMenu.content.title` (신규)
- `정산` → `comp.sideMenu.calc.title` (신규)
- `시그니처 관리` → `view.signature.title` (기존 키 재사용)
- 콘텐츠 관리 하위(신규):
- `내 콘텐츠 리스트` → `comp.sideMenu.content.myList`
- `카테고리 관리` → `comp.sideMenu.content.category`
- `시리즈 관리` → `comp.sideMenu.content.series`
- 정산 하위:
- `라이브 정산` → `comp.sideMenu.calc.live` (기존 재사용)
- `일자별 콘텐츠 정산` → `comp.sideMenu.calc.contentByDate` (신규)
- `콘텐츠별 누적 현황` → `comp.sideMenu.calc.contentCumulative` (신규)
- `일자별 콘텐츠 후원 정산` → `comp.sideMenu.calc.contentDonationByDate` (신규)
- `커뮤니티 정산` → `comp.sideMenu.calc.community` (기존 재사용)
- `채널후원 정산` → `comp.sideMenu.calc.channelDonation` (기존 재사용, 화면 ko는 "채널 후원 정산")
4. 구현 체크리스트
- [ ] 서버 메뉴 응답에 `titleKey`를 포함하도록 계약/스키마 합의한다.
- [ ] 새로 추가되는 메뉴도 동일 키 네이밍 규칙을 따른다(`comp.sideMenu.*`).
- [ ] (route 없는 항목 대응) 한글 원문 기반 매핑 사전(`titleKoToKey`)을 정의·유지한다.
- [ ] (route 없는 항목 대응) 전처리 유틸(`attachTitleKeyByKo`)로 메뉴 응답에 `titleKey`를 주입한다.
- [ ] "채널후원 정산" → `comp.sideMenu.calc.channelDonation` 매핑이 적용되어 화면 ko가 "채널 후원 정산"으로 노출되는지 확인한다.
- [ ] `src/locales/ko.json`에 모든 `titleKey`의 한국어 값을 등록/확인한다.
- [ ] `src/locales/en.json`에 모든 `titleKey`의 영어 값을 등록/확인한다.
- [ ] `src/locales/ja.json`에 모든 `titleKey`의 일본어 값을 등록/확인한다.
- [ ] 서버가 `titleKey`를 누락한 경우 `title`로 정상 노출되는지 확인한다(회귀 테스트).
- [ ] 빈 메뉴 응답 시 클라이언트 기본 메뉴(fallback)로 정상 노출/번역되는지 확인한다.
5. 메뉴 텍스트 사전(기존 키 선기입 + 추후 확장)
- 기준 컬럼: `key | ko | en | ja | route`
| key | ko | en | ja | route |
|---|---|---|---|---|
| comp.sideMenu.content.title | 콘텐츠 관리 | Content management | コンテンツ管理 | |
| comp.sideMenu.content.myList | 내 콘텐츠 리스트 | My contents | 自分のコンテンツ一覧 | |
| comp.sideMenu.content.category | 카테고리 관리 | Category management | カテゴリ管理 | |
| comp.sideMenu.content.series | 시리즈 관리 | Series management | シリーズ管理 | |
| comp.sideMenu.calc.title | 정산 | Settlement | 精算 | |
| comp.sideMenu.creators | 소속 크리에이터 | Affiliated creators | 所属クリエイター | /agent/creators |
| comp.sideMenu.calc.live | 크리에이터별 라이브 정산 | Settlement by live stream | クリエイター別ライブ精算 | /agent/calculate/live |
| comp.sideMenu.calc.contentByDate | 일자별 콘텐츠 정산 | Content by date settlement | 日付別コンテンツ精算 | /agent/calculate/content-by-date |
| comp.sideMenu.calc.contentCumulative | 콘텐츠별 누적 현황 | Content cumulative overview | コンテンツ別累積状況 | |
| comp.sideMenu.calc.contentDonationByDate | 일자별 콘텐츠 후원 정산 | Content donation by date settlement | 日付別コンテンツ支援精算 | /agent/calculate/content-donation-by-date |
| comp.sideMenu.calc.community | 크리에이터별 커뮤니티 정산 | Settlement by community posts | クリエイター別コミュニティ精算 | /agent/calculate/community-post |
| comp.sideMenu.calc.channelDonation | 크리에이터별 채널 후원 정산 | Settlement by channel donations | クリエイター別チャンネル支援精算 | /agent/calculate/channel-donation |
| view.signature.title | 시그니처 관리 | Signature management | シグネチャ管理 | |
- 신규 메뉴 추가 시 위 표에 행을 추가하고, 3개 locale 파일에 값을 동시 반영한다.
5-1. 한글 원문 기반 매핑 사전(ko → key) 초안
```
// 예시: 클라이언트 전처리용 사전
{
"콘텐츠 관리": "comp.sideMenu.content.title",
"내 콘텐츠 리스트": "comp.sideMenu.content.myList",
"카테고리 관리": "comp.sideMenu.content.category",
"시리즈 관리": "comp.sideMenu.content.series",
"정산": "comp.sideMenu.calc.title",
"라이브 정산": "comp.sideMenu.calc.live",
"일자별 콘텐츠 정산": "comp.sideMenu.calc.contentByDate",
"콘텐츠별 누적 현황": "comp.sideMenu.calc.contentCumulative",
"일자별 콘텐츠 후원 정산": "comp.sideMenu.calc.contentDonationByDate",
"커뮤니티 정산": "comp.sideMenu.calc.community",
"채널후원 정산": "comp.sideMenu.calc.channelDonation",
"시그니처 관리": "view.signature.title"
}
```
6. 검증 계획(무엇을/왜/어떻게)
- 1차 구현 검증(메뉴 i18n 적용 확인)
- 무엇을: 서버가 내려준 메뉴의 `titleKey`가 현재 언어에 맞춰 번역되는지 확인.
- 왜: 사용자의 언어 설정에 따라 일관된 UI 텍스트 제공.
- 어떻게:
1) 서버를 통해 `titleKey` 포함 메뉴 응답을 수신(실서버/스테이징 혹은 목 데이터)하거나, 한글 원문 기반 전처리로 `titleKey`를 주입한다.
2) 앱 언어를 ko/en/ja로 각각 전환 후 사이드메뉴 라벨 스냅샷 확인.
3) 기대 결과: 표의 ko/en/ja 값과 화면 라벨이 일치.
- 2차 회귀 검증(fallback 및 누락 케이스)
- 무엇을: `titleKey` 누락 또는 빈 메뉴 응답 시 동작 확인.
- 왜: 서버 데이터 이상/변경 시에도 UX 붕괴 방지.
- 어떻게:
1) `titleKey` 없이 `title`만 포함된 메뉴 응답 → 한글 원문 기반 전처리가 `titleKey`를 주입하는지 확인. 매핑 누락 시에는 원문(title) 그대로 노출되는지 확인.
2) 빈 배열 응답 → 클라이언트 기본 메뉴 6개가 노출되고 번역이 적용되는지 확인.
7. 정정/추가 기록
- (추후 변경 시 이 섹션에 `정정` 항목을 누적 기록한다. 원문 삭제 금지.)

View File

@@ -65,6 +65,7 @@
<script>
import * as api from '@/api/menu'
import { attachTitleKeyByKo } from '@/utils/menuMapping'
export default {
name: "SideMenu",
@@ -94,7 +95,8 @@ export default {
let res = await api.getMenus();
if (res.status === 200 && res.data.success === true) {
if (res.data.data && res.data.data.length > 0) {
this.items = res.data.data
// 서버가 titleKey를 내려주지 않는 항목은 한글 원문 기반 매핑으로 titleKey를 주입한다.
this.items = attachTitleKeyByKo(res.data.data)
} else {
// 빈 메뉴일 경우 기본 단독 메뉴를 제공한다.
// 요구사항: 정산 관련 기본 메뉴를 추가한다.

28
src/i18n/menuTitleMap.js Normal file
View File

@@ -0,0 +1,28 @@
// 한글 원문 → i18n 키 매핑 사전
// 운영 원칙:
// - 기존 키는 재사용한다.
// - 표기 미세 차이는 전처리 단계의 정규화(normKo)로 흡수한다.
export const titleKoToKey = {
// 그룹 타이틀
'콘텐츠 관리': 'comp.sideMenu.content.title',
'정산': 'comp.sideMenu.calc.title',
// 콘텐츠 관리 하위
'내 콘텐츠 리스트': 'comp.sideMenu.content.myList',
'카테고리 관리': 'comp.sideMenu.content.category',
'시리즈 관리': 'comp.sideMenu.content.series',
// 정산 하위
'라이브 정산': 'comp.sideMenu.calc.live', // 기존 키 재사용
'일자별 콘텐츠 정산': 'comp.sideMenu.calc.contentByDate',
'콘텐츠별 누적 현황': 'comp.sideMenu.calc.contentCumulative',
'일자별 콘텐츠 후원 정산': 'comp.sideMenu.calc.contentDonationByDate',
'커뮤니티 정산': 'comp.sideMenu.calc.community', // 기존 키 재사용
// 서버 원문은 공백 없이 내려옴: "채널후원 정산" → 기존 키로 매핑
'채널후원 정산': 'comp.sideMenu.calc.channelDonation',
// 시그니처 관리(사이드메뉴 타이틀은 view.signature.title 재사용)
'시그니처 관리': 'view.signature.title',
}

View File

@@ -55,11 +55,21 @@
},
"comp": {
"sideMenu": {
"content": {
"title": "Content management",
"myList": "My contents",
"category": "Category management",
"series": "Series management"
},
"creators": "Affiliated creators",
"calc": {
"title": "Settlement",
"live": "Settlement by live stream",
"content": "Settlement by content",
"contentByDate": "Content by date settlement",
"contentDonation": "Settlement by content donations",
"contentDonationByDate": "Content donation by date settlement",
"contentCumulative": "Content cumulative overview",
"community": "Settlement by community posts",
"channelDonation": "Settlement by channel donations"
}

View File

@@ -55,11 +55,21 @@
},
"comp": {
"sideMenu": {
"content": {
"title": "コンテンツ管理",
"myList": "自分のコンテンツ一覧",
"category": "カテゴリ管理",
"series": "シリーズ管理"
},
"creators": "所属クリエイター",
"calc": {
"title": "精算",
"live": "クリエイター別ライブ精算",
"content": "クリエイター別コンテンツ精算",
"contentByDate": "日付別コンテンツ精算",
"contentDonation": "クリエイター別コンテンツ支援精算",
"contentDonationByDate": "日付別コンテンツ支援精算",
"contentCumulative": "コンテンツ別累積状況",
"community": "クリエイター別コミュニティ精算",
"channelDonation": "クリエイター別チャンネル支援精算"
}

View File

@@ -55,11 +55,21 @@
},
"comp": {
"sideMenu": {
"content": {
"title": "콘텐츠 관리",
"myList": "내 콘텐츠 리스트",
"category": "카테고리 관리",
"series": "시리즈 관리"
},
"creators": "소속 크리에이터",
"calc": {
"title": "정산",
"live": "크리에이터별 라이브 정산",
"content": "크리에이터별 콘텐츠 정산",
"contentByDate": "일자별 콘텐츠 정산",
"contentDonation": "크리에이터별 콘텐츠 후원 정산",
"contentDonationByDate": "일자별 콘텐츠 후원 정산",
"contentCumulative": "콘텐츠별 누적 현황",
"community": "크리에이터별 커뮤니티 정산",
"channelDonation": "크리에이터별 채널 후원 정산"
}

35
src/utils/menuMapping.js Normal file
View File

@@ -0,0 +1,35 @@
import { titleKoToKey } from '@/i18n/menuTitleMap'
export function normKo(s) {
if (!s || typeof s !== 'string') return s
try {
return s
.normalize('NFC')
.replace(/\s+/g, ' ')
.trim()
} catch (e) {
// 일부 환경에서 normalize 미지원 시 안전하게 진행
return String(s).replace(/\s+/g, ' ').trim()
}
}
export function attachTitleKeyByKo(items) {
if (!Array.isArray(items)) return items
return items.map(item => {
const next = { ...item }
const title = normKo(item && item.title)
const key = title ? titleKoToKey[title] : undefined
if (key) {
next.titleKey = key
} else if (process && process.env && process.env.NODE_ENV !== 'production') {
// 개발 환경에서만 경고 로그
// eslint-disable-next-line no-console
console.warn('[menu-i18n] missing map for title:', item && item.title)
}
if (Array.isArray(item && item.items) && item.items.length > 0) {
next.items = attachTitleKeyByKo(item.items)
}
return next
})
}