fix(i18n): 브라우저 탭 타이틀을 common.app.title 기반으로 국제화 동기화

라우터 afterEach에서 가장 깊은 meta.titleKey 사용, locale 변경 watch에서 동일 로직으로 재계산, 초기 진입 시 common.app.title로 교체. docs에 검증 기록 추가.
This commit is contained in:
Yu Sung
2026-05-08 16:00:57 +09:00
parent ab6660fa16
commit aba06048b3
3 changed files with 57 additions and 1 deletions

View File

@@ -31,6 +31,7 @@
- [ ] P4: 공통 남은 텍스트 및 주석 정리(사용자 노출 X 주석은 후순위)
- [ ] 팝업 다이얼로그 사용자 노출 텍스트 전수 치환(i18n)
- [x] 시그니처 관리 페이지(`views/Signature/SignatureManagement.vue`) 치환
- [x] 브라우저 문서 타이틀 i18n 동기화(라우터 afterEach + locale 변경 watch)
## 키 네이밍 규칙
- 네임스페이스 기반: `common.*`, `comp.*`, `view.*`
@@ -109,6 +110,23 @@
- `src/store/accountStore.js`의 사용자 노출 에러 메시지 i18n 치환
- P2 (업무 핵심): 레거시 정산 화면 `views/Calculate/*`
- 테이블 헤더/합계/필터 라벨/버튼 등 일괄 키 도출 → `view.calculate.*` 네임스페이스 제안
### 4차 수정 브라우저 타이틀 국제화 (2026-05-08)
- 무엇을: 라우트 이동 및 언어 전환 시 `document.title`이 선택된 언어로 표시되도록 동기화 구현(키: `common.app.title`)
- 왜: i18n 전환이 뷰 내부 텍스트에는 적용되지만 브라우저 탭 타이틀은 자동 반영되지 않기 때문
- 어떻게:
- 코드 변경 1: `src/router/index.js`
- `import i18n from '@/i18n'` 추가
- `router.afterEach` 훅에서 `to.matched`의 가장 깊은 라우트에서 `meta.titleKey`를 찾아 `document.title = "<번역> - i18n.t('common.app.title')"`로 설정, 없으면 `i18n.t('common.app.title')` 기본값 사용
- 시그니처 화면 라우트에 `meta: { titleKey: 'view.signature.title' }` 추가
- 코드 변경 2: `src/main.js`
- 앱 초기 구동 시 index.html의 하드코딩 타이틀을 `i18n.t('common.app.title')`로 1회 교체
- `i18n.vm.$watch('locale', ...)` 내에서 현재 라우트의 가장 깊은 `meta.titleKey`를 기준으로 `document.title` 재계산, 없으면 `i18n.t('common.app.title')` 사용
- 검증:
- 실행: `npm run serve`
- 브라우저에서 `/signature` 진입 → 탭 타이틀이 `시그니처 관리 - <앱명(언어별: common.app.title)>`로 표시됨 ✓
- App의 언어 드롭다운으로 `en/ja` 전환 → 탭 타이틀이 해당 언어로 즉시 갱신됨 ✓
- `meta.titleKey`가 없는 라우트 이동 시 탭 타이틀이 `i18n.t('common.app.title')`로 유지됨 ✓
- P3 (업무 빈도 높음): 콘텐츠 관리 `views/Content/*`
- 목록/상세/시리즈 화면의 라벨/버튼/알림 메시지 치환 → `view.content.*` 네임스페이스 제안
- P4: 공통 남은 텍스트 및 주석 정리(사용자 노출 X 주석은 후순위)

View File

@@ -16,6 +16,14 @@ Vue.use(VuetifyDialog, {
}
})
// 초기 진입 시 index.html의 하드코딩 타이틀을 i18n의 common.app.title로 교체
try {
const appTitle = i18n && typeof i18n.t === 'function' ? i18n.t('common.app.title') : 'Soda Admin'
if (appTitle) document.title = `${appTitle}`
} catch (e) {
// ignore
}
// Vuetify 언어와 vue-i18n 로케일 동기화
try {
vuetify.framework.lang.current = i18n.locale
@@ -23,6 +31,19 @@ try {
i18n.vm.$watch('locale', (val) => {
vuetify.framework.lang.current = val
try { localStorage.setItem('locale', val) } catch (e) { /* ignore */ }
// 언어 변경 시 현재 라우트 기준으로 문서 타이틀 재계산
try {
const to = router.currentRoute
const matched = (to && to.matched) ? to.matched : []
const deepest = matched.length ? matched[matched.length - 1] : to
const key = deepest && deepest.meta && deepest.meta.titleKey
const appTitle = i18n && typeof i18n.t === 'function' ? i18n.t('common.app.title') : 'Soda Admin'
const localized = key ? i18n.t(key) : ''
document.title = key ? `${localized} - ${appTitle}` : `${appTitle}`
} catch (e) {
// ignore
}
})
}
} catch (e) {

View File

@@ -1,6 +1,7 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import store from '@/store';
import i18n from '@/i18n'
import DefaultLayout from '@/layouts/default'
@@ -99,7 +100,8 @@ const routes = [
{
path: '/signature',
name: 'SignatureManagement',
component: () => import(/* webpackChunkName: "signature" */ '../views/Signature/SignatureManagement.vue')
component: () => import(/* webpackChunkName: "signature" */ '../views/Signature/SignatureManagement.vue'),
meta: { titleKey: 'view.signature.title' }
}
]
},
@@ -135,4 +137,19 @@ router.beforeEach((to, from, next) => {
}
})
// 라우트 변경 시 문서 타이틀을 i18n으로 갱신
router.afterEach((to) => {
try {
// 가장 깊은 매칭 라우트에서 titleKey 탐색
const matched = (to && to.matched) ? to.matched : []
const deepest = matched.length ? matched[matched.length - 1] : to
const key = deepest && deepest.meta && deepest.meta.titleKey
const appTitle = i18n && typeof i18n.t === 'function' ? i18n.t('common.app.title') : 'Soda Admin'
const localized = key ? i18n.t(key) : ''
document.title = key ? `${localized} - ${appTitle}` : `${appTitle}`
} catch (e) {
// ignore
}
})
export default router