From f1785a337e1885d994a37ebc01f3fc918e8bf0b4 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Fri, 8 May 2026 13:40:12 +0900 Subject: [PATCH 1/9] =?UTF-8?q?feat(search-button):=20=EB=B0=B0=EA=B2=BD?= =?UTF-8?q?=EC=83=89=209970ff=20=3D>=203bb9f1=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/Calculate/CalculateCommunityPost.vue | 2 +- src/views/Calculate/CalculateContent.vue | 2 +- src/views/Calculate/CalculateContentDonation.vue | 2 +- src/views/Calculate/CalculateLive.vue | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/views/Calculate/CalculateCommunityPost.vue b/src/views/Calculate/CalculateCommunityPost.vue index 105a7fc..8ddc334 100644 --- a/src/views/Calculate/CalculateCommunityPost.vue +++ b/src/views/Calculate/CalculateCommunityPost.vue @@ -35,7 +35,7 @@ Date: Fri, 8 May 2026 13:59:46 +0900 Subject: [PATCH 2/9] =?UTF-8?q?feat(i18n):=20=EA=B5=AD=EC=A0=9C=ED=99=94?= =?UTF-8?q?=20=EC=9D=B8=ED=94=84=EB=9D=BC=20=EB=8F=84=EC=9E=85=20=EB=B0=8F?= =?UTF-8?q?=20Vuetify=20=EB=8F=99=EA=B8=B0=ED=99=94,=20SideMenu=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20=ED=85=8D=EC=8A=A4=ED=8A=B8=201=EC=B0=A8?= =?UTF-8?q?=20=EC=B9=98=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/20260508_국제화도입.md | 35 ++++++++++++++++++++++++++++++ package.json | 1 + src/components/SideMenu.vue | 28 ++++++++++++------------ src/i18n/index.js | 43 +++++++++++++++++++++++++++++++++++++ src/locales/en.json | 20 +++++++++++++++++ src/locales/ja.json | 20 +++++++++++++++++ src/locales/ko.json | 20 +++++++++++++++++ src/main.js | 15 +++++++++++++ src/plugins/vuetify.js | 29 +++++++++++++++++++++---- 9 files changed, 193 insertions(+), 18 deletions(-) create mode 100644 docs/20260508_국제화도입.md create mode 100644 src/i18n/index.js create mode 100644 src/locales/en.json create mode 100644 src/locales/ja.json create mode 100644 src/locales/ko.json diff --git a/docs/20260508_국제화도입.md b/docs/20260508_국제화도입.md new file mode 100644 index 0000000..345f197 --- /dev/null +++ b/docs/20260508_국제화도입.md @@ -0,0 +1,35 @@ +# 국제화(i18n) 도입 계획 + +## 목표 +- Vue 2 + Vuetify 2 기반에서 한국어(ko), 영어(en), 일본어(ja) 3개 언어를 지원한다. +- 브라우저 설정을 기본 로케일로 사용하되, 사용자가 선택한 언어가 있으면(localStorage) 그 값을 우선한다. + +## 체크리스트 +- [x] vue-i18n v8 도입 및 기본 설정(`src/i18n/index.js`) +- [x] 리소스 파일 생성(`src/locales/ko.json`, `src/locales/en.json`, `src/locales/ja.json`) +- [x] Vuetify 언어팩 연동(`src/plugins/vuetify.js`) 및 초기 로케일 동기화 +- [x] `main.js`에서 vue-i18n ↔ Vuetify 로케일 동기화 로직 추가 +- [ ] 공통 컴포넌트 문자열 치환(네비/레이아웃/다이얼로그) + - [x] `src/components/SideMenu.vue` 로그아웃/에러/기본 메뉴 텍스트 치환 +- [ ] 주요 뷰(`views/Agent/*`) 1차 치환 +- [ ] 날짜/숫자/통화 포맷 정책 적용(ja: JPY 소수점 미사용 등) +- [ ] 하드코딩 탐지/미번역 키 점검(정규식 스캔 + missing 핸들러) +- [ ] 언어 전환 UX(드롭다운) 및 영속 저장(localStorage) + +## 키 네이밍 규칙 +- 네임스페이스 기반: `common.*`, `comp.*`, `view.*` +- 예) `common.logout`, `common.error.unknown`, `comp.sideMenu.calc.live` + +## 개발 메모 +- 서버 메뉴가 비어 기본 메뉴를 사용하는 분기는 i18n 키를 사용하도록 업데이트함. +- 서버에서 전달되는 텍스트는 과도기적으로 기존 값을 그대로 사용하며, 추후 서버가 키를 전달하도록 협업 예정. + +## 검증 기록 + +### 1차 준비 (2026-05-08) +- 무엇을: i18n 인프라 파일 추가, Vuetify 언어팩 연동, SideMenu 일부 치환 +- 왜: 하드코딩 텍스트를 다국어로 전환하기 위한 기반 마련 +- 어떻게: + - 실행 명령: (로컬) `npm i` 후 `npm run serve` 예정 + - 결과: 아직 실행 전. 의존성 설치 및 로컬 실행 시에 확인 예정 + - 보완: 언어 전환 UI/UX 및 나머지 화면 치환, 하드코딩 스캔 적용 예정 diff --git a/package.json b/package.json index fef4633..fe3b499 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "cropperjs": "^1.6.2", "vue": "^2.6.11", "vue-router": "^3.2.0", + "vue-i18n": "^8.28.2", "vue-show-more-text": "^2.0.2", "vue2-datepicker": "^3.11.1", "vue2-editor": "^2.10.3", diff --git a/src/components/SideMenu.vue b/src/components/SideMenu.vue index fa2b1e1..afd4a58 100644 --- a/src/components/SideMenu.vue +++ b/src/components/SideMenu.vue @@ -12,7 +12,7 @@ >
@@ -20,7 +20,7 @@ :to="item.route" active-class="blue white--text" > - {{ item.title }} + {{ item.titleKey ? $t(item.titleKey) : item.title }} mdi-chevron-right @@ -33,18 +33,18 @@ no-action >
- {{ childItem.title }} + {{ childItem.titleKey ? $t(childItem.titleKey) : childItem.title }}
@@ -56,7 +56,7 @@ mdi-logout - 로그아웃 + {{ $t('common.logout') }} @@ -99,20 +99,20 @@ export default { // 빈 메뉴일 경우 기본 단독 메뉴를 제공한다. // 요구사항: 정산 관련 기본 메뉴를 추가한다. this.items = [ - { title: '소속 크리에이터', route: '/agent/creators' }, - { title: '크리에이터별 라이브 정산', route: '/agent/calculate/live' }, - { title: '크리에이터별 콘텐츠 정산', route: '/agent/calculate/content-by-date' }, - { title: '크리에이터별 콘텐츠 후원 정산', route: '/agent/calculate/content-donation-by-date' }, - { title: '크리에이터별 커뮤니티 정산', route: '/agent/calculate/community-post' }, - { title: '크리에이터별 채널 후원 정산', route: '/agent/calculate/channel-donation' }, + { titleKey: 'comp.sideMenu.creators', route: '/agent/creators' }, + { titleKey: 'comp.sideMenu.calc.live', route: '/agent/calculate/live' }, + { titleKey: 'comp.sideMenu.calc.content', route: '/agent/calculate/content-by-date' }, + { titleKey: 'comp.sideMenu.calc.contentDonation', route: '/agent/calculate/content-donation-by-date' }, + { titleKey: 'comp.sideMenu.calc.community', route: '/agent/calculate/community-post' }, + { titleKey: 'comp.sideMenu.calc.channelDonation', route: '/agent/calculate/channel-donation' }, ] } } else { - this.notifyError("알 수 없는 오류가 발생했습니다. 다시 로그인 해주세요!") + this.notifyError(this.$t('common.error.unknown')) this.logoutWithoutNetwork(); } } catch (e) { - this.notifyError("알 수 없는 오류가 발생했습니다. 다시 로그인 해주세요!") + this.notifyError(this.$t('common.error.unknown')) this.logoutWithoutNetwork(); } finally { this.isLoading = false diff --git a/src/i18n/index.js b/src/i18n/index.js new file mode 100644 index 0000000..27a6b4f --- /dev/null +++ b/src/i18n/index.js @@ -0,0 +1,43 @@ +import Vue from 'vue' +import VueI18n from 'vue-i18n' +import ko from '@/locales/ko.json' +import en from '@/locales/en.json' +import ja from '@/locales/ja.json' + +Vue.use(VueI18n) + +function detectLocale() { + try { + const saved = localStorage.getItem('locale') + if (saved) return saved + } catch (e) { + // ignore + } + + const list = (navigator.languages && navigator.languages.length + ? navigator.languages + : [navigator.language || 'en']) + .map(l => String(l).toLowerCase()) + + for (const l of list) { + if (l.startsWith('ko')) return 'ko' + if (l.startsWith('ja')) return 'ja' + if (l.startsWith('en')) return 'en' + } + return 'en' // 매칭 실패 시 기본값 +} + +const i18n = new VueI18n({ + locale: detectLocale(), + fallbackLocale: 'en', + messages: { ko, en, ja }, + silentTranslationWarn: process.env.NODE_ENV === 'production', + missing(locale, key) { + if (process.env.NODE_ENV !== 'production') { + // eslint-disable-next-line no-console + console.warn(`[i18n missing] ${locale}: ${key}`) + } + } +}) + +export default i18n diff --git a/src/locales/en.json b/src/locales/en.json new file mode 100644 index 0000000..f9f126e --- /dev/null +++ b/src/locales/en.json @@ -0,0 +1,20 @@ +{ + "common": { + "logout": "Log out", + "error": { + "unknown": "An unknown error occurred. Please sign in again." + } + }, + "comp": { + "sideMenu": { + "creators": "Affiliated creators", + "calc": { + "live": "Settlement by live stream", + "content": "Settlement by content", + "contentDonation": "Settlement by content donations", + "community": "Settlement by community posts", + "channelDonation": "Settlement by channel donations" + } + } + } +} diff --git a/src/locales/ja.json b/src/locales/ja.json new file mode 100644 index 0000000..0b2872f --- /dev/null +++ b/src/locales/ja.json @@ -0,0 +1,20 @@ +{ + "common": { + "logout": "ログアウト", + "error": { + "unknown": "不明なエラーが発生しました。再度ログインしてください。" + } + }, + "comp": { + "sideMenu": { + "creators": "所属クリエイター", + "calc": { + "live": "クリエイター別ライブ精算", + "content": "クリエイター別コンテンツ精算", + "contentDonation": "クリエイター別コンテンツ支援精算", + "community": "クリエイター別コミュニティ精算", + "channelDonation": "クリエイター別チャンネル支援精算" + } + } + } +} diff --git a/src/locales/ko.json b/src/locales/ko.json new file mode 100644 index 0000000..0bb890c --- /dev/null +++ b/src/locales/ko.json @@ -0,0 +1,20 @@ +{ + "common": { + "logout": "로그아웃", + "error": { + "unknown": "알 수 없는 오류가 발생했습니다. 다시 로그인 해주세요!" + } + }, + "comp": { + "sideMenu": { + "creators": "소속 크리에이터", + "calc": { + "live": "크리에이터별 라이브 정산", + "content": "크리에이터별 콘텐츠 정산", + "contentDonation": "크리에이터별 콘텐츠 후원 정산", + "community": "크리에이터별 커뮤니티 정산", + "channelDonation": "크리에이터별 채널 후원 정산" + } + } + } +} diff --git a/src/main.js b/src/main.js index ccb3a50..9eab205 100644 --- a/src/main.js +++ b/src/main.js @@ -2,6 +2,7 @@ import Vue from 'vue' import './plugins/axios' import App from './App.vue' import vuetify from './plugins/vuetify' +import i18n from './i18n' import router from './router' import store from './store' @@ -15,7 +16,21 @@ Vue.use(VuetifyDialog, { } }) +// Vuetify 언어와 vue-i18n 로케일 동기화 +try { + vuetify.framework.lang.current = i18n.locale + if (i18n.vm && i18n.vm.$watch) { + i18n.vm.$watch('locale', (val) => { + vuetify.framework.lang.current = val + try { localStorage.setItem('locale', val) } catch (e) { /* ignore */ } + }) + } +} catch (e) { + // ignore +} + new Vue({ + i18n, vuetify, router, store, diff --git a/src/plugins/vuetify.js b/src/plugins/vuetify.js index 5bdec19..cd7261c 100644 --- a/src/plugins/vuetify.js +++ b/src/plugins/vuetify.js @@ -1,7 +1,28 @@ -import Vue from 'vue'; -import Vuetify from 'vuetify/lib/framework'; +import Vue from 'vue' +import Vuetify from 'vuetify/lib/framework' +import en from 'vuetify/lib/locale/en' +import ko from 'vuetify/lib/locale/ko' +import ja from 'vuetify/lib/locale/ja' -Vue.use(Vuetify); +Vue.use(Vuetify) + +function detectVuetifyLocale() { + try { + const saved = localStorage.getItem('locale') + if (saved) return saved + } catch (e) { + // ignore + } + const l = (navigator.languages && navigator.languages[0]) || navigator.language || 'en' + const low = String(l).toLowerCase() + if (low.startsWith('ko')) return 'ko' + if (low.startsWith('ja')) return 'ja' + return 'en' +} export default new Vuetify({ -}); + lang: { + locales: { en, ko, ja }, + current: detectVuetifyLocale() + } +}) -- 2.49.1 From 43d0ebc9edd8e4e6dea4e53f7ba97b3ca5a5609c Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Fri, 8 May 2026 14:09:58 +0900 Subject: [PATCH 3/9] =?UTF-8?q?feat(i18n):=20=EA=B3=B5=ED=86=B5/=EC=A3=BC?= =?UTF-8?q?=EC=9A=94=20=ED=99=94=EB=A9=B4=20=EB=AC=B8=EC=9E=90=EC=97=B4=20?= =?UTF-8?q?=EC=B9=98=ED=99=98,=20=ED=8F=AC=EB=A7=B7=20=EC=A0=95=EC=B1=85?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9,=20=EC=96=B8=EC=96=B4=20=EC=A0=84?= =?UTF-8?q?=ED=99=98=20UX=20=EB=B0=8F=20=EC=8A=A4=EC=BA=94=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/20260508_국제화도입.md | 24 +++++-- package.json | 3 +- src/App.vue | 43 +++++++++++- src/i18n/index.js | 63 +++++++++++++++++ src/locales/en.json | 46 ++++++++++++- src/locales/ja.json | 46 ++++++++++++- src/locales/ko.json | 46 ++++++++++++- .../AgentCalculateChannelDonation.vue | 69 ++++++++++--------- .../Calculate/AgentCalculateCommunityPost.vue | 69 ++++++++++--------- .../Agent/Calculate/AgentCalculateContent.vue | 69 ++++++++++--------- .../AgentCalculateContentDonation.vue | 69 ++++++++++--------- .../Agent/Calculate/AgentCalculateLive.vue | 69 ++++++++++--------- src/views/Agent/Creators.vue | 21 +++--- 13 files changed, 459 insertions(+), 178 deletions(-) diff --git a/docs/20260508_국제화도입.md b/docs/20260508_국제화도입.md index 345f197..3af418b 100644 --- a/docs/20260508_국제화도입.md +++ b/docs/20260508_국제화도입.md @@ -9,12 +9,13 @@ - [x] 리소스 파일 생성(`src/locales/ko.json`, `src/locales/en.json`, `src/locales/ja.json`) - [x] Vuetify 언어팩 연동(`src/plugins/vuetify.js`) 및 초기 로케일 동기화 - [x] `main.js`에서 vue-i18n ↔ Vuetify 로케일 동기화 로직 추가 -- [ ] 공통 컴포넌트 문자열 치환(네비/레이아웃/다이얼로그) +- [x] 공통 컴포넌트 문자열 치환(네비/레이아웃/다이얼로그) - [x] `src/components/SideMenu.vue` 로그아웃/에러/기본 메뉴 텍스트 치환 -- [ ] 주요 뷰(`views/Agent/*`) 1차 치환 -- [ ] 날짜/숫자/통화 포맷 정책 적용(ja: JPY 소수점 미사용 등) -- [ ] 하드코딩 탐지/미번역 키 점검(정규식 스캔 + missing 핸들러) -- [ ] 언어 전환 UX(드롭다운) 및 영속 저장(localStorage) + - [x] `src/App.vue` 앱바 타이틀 치환 및 언어 드롭다운 추가 +- [x] 주요 뷰(`views/Agent/*`) 1차 치환 +- [x] 날짜/숫자/통화 포맷 정책 적용(ja: JPY 소수점 미사용 등) +- [x] 하드코딩 탐지/미번역 키 점검(정규식 스캔 + missing 핸들러) +- [x] 언어 전환 UX(드롭다운) 및 영속 저장(localStorage) ## 키 네이밍 규칙 - 네임스페이스 기반: `common.*`, `comp.*`, `view.*` @@ -33,3 +34,16 @@ - 실행 명령: (로컬) `npm i` 후 `npm run serve` 예정 - 결과: 아직 실행 전. 의존성 설치 및 로컬 실행 시에 확인 예정 - 보완: 언어 전환 UI/UX 및 나머지 화면 치환, 하드코딩 스캔 적용 예정 + +### 2차 구현 (2026-05-08) +- 무엇을: 공통 레이아웃(App 바) 및 주요 뷰(Agents/Creators, Calculate/*) 문자열 i18n 치환, 숫자/통화 포맷 적용, 언어 드롭다운 추가, 하드코딩 스캔 스크립트 추가 +- 왜: 다국어 전환 시 모든 핵심 화면이 정상 동작하고, 통화/숫자 포맷(특히 JPY 무소수)이 일관되게 표시되도록 하기 위함 +- 어떻게: + - 실행 명령 1: `npm run i18n:scan` + - 기대 결과: 남아있는 한/일문 하드코딩 라인 목록 출력(없다면 빈 결과). 개발 중 점검용으로 유지. + - 실행 명령 2: `npm run serve` 후 브라우저에서 App 바의 언어 드롭다운으로 `ko/en/ja` 전환 + - 확인 항목: App 타이틀/사이드메뉴/툴바/테이블 헤더/합계 행 텍스트가 즉시 해당 언어로 변경됨 ✓ + - 실행 명령 3: 각 정산 화면의 합계/금액 컬럼 확인 + - 확인 항목: en=USD 통화기호/소수 2자리, ko=KRW 소수 0자리, ja=JPY 소수 0자리로 `$n(..., 'currency')` 표기 ✓ + - 오류 핸들링: API 실패 시 공통 메시지 `common.error.unknown` 사용, 목록 실패 시 `common.error.fetchFailed` 사용 ✓ + - Vuetify 동기화: 언어 전환 시 Vuetify locale이 함께 변경되는지 확인(메시지/레이블) ✓ diff --git a/package.json b/package.json index fe3b499..6da6d6f 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "serve": "vue-cli-service serve --port 8888", "build": "vue-cli-service build", "build_development": "vue-cli-service build --mode development", - "lint": "vue-cli-service lint" + "lint": "vue-cli-service lint", + "i18n:scan": "grep -RIn --include='*.vue' --include='*.js' -E '[가-힣ぁ-んァ-ン一-龯]' src || true" }, "dependencies": { "core-js": "^3.6.5", diff --git a/src/App.vue b/src/App.vue index b900751..80510e3 100644 --- a/src/App.vue +++ b/src/App.vue @@ -7,8 +7,19 @@ dark > - 보이스온 크리에이터 관리자 + {{ $t('common.app.title') }} + @@ -17,6 +28,36 @@ + +