feat. i18n 설정
This commit is contained in:
107
temp/dynamic-i18n-runtime.client.ts
Normal file
107
temp/dynamic-i18n-runtime.client.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
export default defineNuxtPlugin(() => {
|
||||
// 클라이언트 사이드에서 gameData를 가져와서 i18n 설정 업데이트
|
||||
const { $i18n } = useNuxtApp();
|
||||
|
||||
// gameData에서 언어 코드 추출
|
||||
const getGameDataLangCodes = (gameData: any) => {
|
||||
try {
|
||||
console.log("🚀 ~ getGameDataLangCodes ~ gameData:", gameData);
|
||||
|
||||
if (gameData?.lang_codes) {
|
||||
console.log("🚀 ~ getGameDataLangCodes ~ gameData.lang_codes:", gameData.lang_codes);
|
||||
console.log("🚀 ~ getGameDataLangCodes ~ lang_codes type:", typeof gameData.lang_codes);
|
||||
console.log("🚀 ~ getGameDataLangCodes ~ lang_codes isArray:", Array.isArray(gameData.lang_codes));
|
||||
return Array.isArray(gameData.lang_codes) ? gameData.lang_codes : [gameData.lang_codes];
|
||||
} else {
|
||||
console.log("🚀 ~ getGameDataLangCodes ~ gameData.lang_codes is undefined or null");
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to get gameData lang codes:', error);
|
||||
}
|
||||
|
||||
console.log("🚀 ~ getGameDataLangCodes ~ returning default ['ko']");
|
||||
return ['ko']; // 기본값
|
||||
};
|
||||
|
||||
// i18n 설정 업데이트
|
||||
const updateI18nLocales = async (gameData?: any) => {
|
||||
const langCodes = getGameDataLangCodes(gameData);
|
||||
|
||||
if (langCodes && langCodes.length > 0) {
|
||||
// 새로운 로케일 설정 생성
|
||||
const newLocales = langCodes.map(code => ({
|
||||
code,
|
||||
file: `${code}.ts`,
|
||||
name: getLocaleName(code),
|
||||
iso: getLocaleIso(code),
|
||||
dir: 'ltr'
|
||||
}));
|
||||
|
||||
// i18n 설정 업데이트
|
||||
if ($i18n) {
|
||||
// 로케일 메시지 동적 로드
|
||||
for (const code of langCodes) {
|
||||
try {
|
||||
const messages = await import(`../../i18n/locales/${code}.ts`);
|
||||
// defineI18nLocale 함수를 실행하여 실제 메시지 데이터 가져오기
|
||||
const localeMessages = await messages.default(code);
|
||||
console.log(`🚀 ~ loaded messages for ${code}:`, localeMessages);
|
||||
($i18n as any).setLocaleMessage(code, localeMessages);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to load locale messages for ${code}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 로케일 이름 가져오기
|
||||
const getLocaleName = (code: string): string => {
|
||||
const localeNames: Record<string, string> = {
|
||||
en: 'English',
|
||||
'zh-tw': '繁體中文',
|
||||
ja: '日本語',
|
||||
ko: '한국어',
|
||||
fr: 'Français',
|
||||
de: 'Deutsch',
|
||||
es: 'Español',
|
||||
pt: 'Português',
|
||||
th: 'ภาษาไทย',
|
||||
'zh-cn': '简体中文'
|
||||
};
|
||||
return localeNames[code] || code;
|
||||
};
|
||||
|
||||
// 로케일 ISO 코드 가져오기
|
||||
const getLocaleIso = (code: string): string => {
|
||||
const localeIsos: Record<string, string> = {
|
||||
en: 'en',
|
||||
'zh-tw': 'zh-tw',
|
||||
ja: 'ja',
|
||||
ko: 'ko-KR',
|
||||
fr: 'fr',
|
||||
de: 'de',
|
||||
es: 'es',
|
||||
pt: 'pt',
|
||||
th: 'th',
|
||||
'zh-cn': 'zh-cn'
|
||||
};
|
||||
return localeIsos[code] || code;
|
||||
};
|
||||
|
||||
// gameData가 설정될 때까지 기다리거나 즉시 실행
|
||||
const gameDataStore = useGameDataStore();
|
||||
|
||||
// gameData가 이미 설정되어 있으면 즉시 실행
|
||||
if (gameDataStore.gameData) {
|
||||
updateI18nLocales(gameDataStore.gameData);
|
||||
}
|
||||
|
||||
// gameData가 변경될 때마다 실행
|
||||
watch(() => gameDataStore.gameData, async (newGameData) => {
|
||||
if (newGameData) {
|
||||
console.log("🚀 ~ gameData changed, updating i18n locales");
|
||||
await updateI18nLocales(newGameData);
|
||||
}
|
||||
}, { immediate: true });
|
||||
});
|
||||
94
temp/dynamic-i18n-runtime.server.ts
Normal file
94
temp/dynamic-i18n-runtime.server.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
export default defineNuxtPlugin(async () => {
|
||||
// 서버 사이드에서 gameData를 가져와서 i18n 설정 업데이트
|
||||
const { $i18n } = useNuxtApp();
|
||||
|
||||
// gameData에서 언어 코드 추출
|
||||
const getGameDataLangCodes = () => {
|
||||
try {
|
||||
// 서버 사이드에서 gameData 접근
|
||||
const nuxtApp = useNuxtApp();
|
||||
const gameData = nuxtApp.ssrContext?.event.context.gameData;
|
||||
|
||||
if (gameData?.lang_codes) {
|
||||
console.log("🚀 ~ dynamic-i18n-runtime.server ~ gameData.lang_codes:", gameData.lang_codes);
|
||||
return Array.isArray(gameData.lang_codes) ? gameData.lang_codes : [gameData.lang_codes];
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to get gameData lang codes on server:', error);
|
||||
}
|
||||
|
||||
return ['ko']; // 기본값
|
||||
};
|
||||
|
||||
// i18n 설정 업데이트
|
||||
const updateI18nLocales = async () => {
|
||||
const langCodes = getGameDataLangCodes();
|
||||
console.log("🚀 ~77777 updateI18nLocales ~ langCodes:", langCodes)
|
||||
|
||||
if (langCodes && langCodes.length > 0) {
|
||||
console.log("🚀 ~ dynamic-i18n-runtime.server ~ updating locales with:", langCodes);
|
||||
|
||||
// 새로운 로케일 설정 생성
|
||||
const newLocales = langCodes.map(code => ({
|
||||
code,
|
||||
file: `${code}.ts`,
|
||||
name: getLocaleName(code),
|
||||
iso: getLocaleIso(code),
|
||||
dir: 'ltr'
|
||||
}));
|
||||
|
||||
// i18n 설정 업데이트
|
||||
if ($i18n) {
|
||||
// 로케일 메시지 동적 로드
|
||||
for (const code of langCodes) {
|
||||
try {
|
||||
const messages = await import(`../../i18n/locales/${code}.ts`);
|
||||
// defineI18nLocale 함수를 실행하여 실제 메시지 데이터 가져오기
|
||||
const localeMessages = await messages.default(code);
|
||||
console.log(`🚀 ~ loaded messages for ${code}:`, localeMessages);
|
||||
($i18n as any).setLocaleMessage(code, localeMessages);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to load locale messages for ${code}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 로케일 이름 가져오기
|
||||
const getLocaleName = (code: string): string => {
|
||||
const localeNames: Record<string, string> = {
|
||||
en: 'English',
|
||||
'zh-tw': '繁體中文',
|
||||
ja: '日本語',
|
||||
ko: '한국어',
|
||||
fr: 'Français',
|
||||
de: 'Deutsch',
|
||||
es: 'Español',
|
||||
pt: 'Português',
|
||||
th: 'ภาษาไทย',
|
||||
'zh-cn': '简体中文'
|
||||
};
|
||||
return localeNames[code] || code;
|
||||
};
|
||||
|
||||
// 로케일 ISO 코드 가져오기
|
||||
const getLocaleIso = (code: string): string => {
|
||||
const localeIsos: Record<string, string> = {
|
||||
en: 'en',
|
||||
'zh-tw': 'zh-tw',
|
||||
ja: 'ja',
|
||||
ko: 'ko-KR',
|
||||
fr: 'fr',
|
||||
de: 'de',
|
||||
es: 'es',
|
||||
pt: 'pt',
|
||||
th: 'th',
|
||||
'zh-cn': 'zh-cn'
|
||||
};
|
||||
return localeIsos[code] || code;
|
||||
};
|
||||
|
||||
// 서버 사이드에서 즉시 실행
|
||||
await updateI18nLocales();
|
||||
});
|
||||
58
temp/gameData.get.ts
Normal file
58
temp/gameData.get.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { getHeader } from "h3";
|
||||
import type {
|
||||
GameDataResponse,
|
||||
GameDataValue,
|
||||
} from "#layers/types/api/gameData";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const config = useRuntimeConfig();
|
||||
const baseDomain = (config.public.baseDomain || ".onstove.com") as string;
|
||||
const stoveApiBaseUrl = config.public.stoveApiUrl;
|
||||
const apiUrl = `${stoveApiBaseUrl}/pub-comm/v1.0/template/game`;
|
||||
let gameAlias = "";
|
||||
|
||||
try {
|
||||
// 미들웨어에서 설정한 gameAlias가 있다면 우선 사용
|
||||
if (event.context.gameAlias) {
|
||||
gameAlias = event.context.gameAlias;
|
||||
} else {
|
||||
const host = getHeader(event, "host") || "";
|
||||
const isGameAliasExtractable = host.includes(baseDomain);
|
||||
|
||||
if (isGameAliasExtractable) {
|
||||
const subdomain = host.split(".")[0];
|
||||
if (subdomain && subdomain !== "www") {
|
||||
gameAlias = subdomain;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("gameAlias extraction error: ", error);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
const queryParams: Record<string, string> = {
|
||||
game_alias: gameAlias,
|
||||
};
|
||||
|
||||
const response = await $fetch<GameDataResponse>(apiUrl, {
|
||||
query: queryParams,
|
||||
});
|
||||
|
||||
if (response?.code === 0 && "value" in response) {
|
||||
event.context.gameData = response.value;
|
||||
|
||||
// lang_codes를 사용해서 동적으로 i18n 설정 업데이트
|
||||
if (response.value.lang_codes && Array.isArray(response.value.lang_codes)) {
|
||||
event.context.availableLocales = response.value.lang_codes;
|
||||
event.context.defaultLocale = response.value.default_lang_code || response.value.lang_codes[0] || 'ko';
|
||||
}
|
||||
|
||||
return response.value as GameDataValue;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return {};
|
||||
}
|
||||
});
|
||||
40
temp/language-redirect.global.ts
Normal file
40
temp/language-redirect.global.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||
// 서버 사이드에서만 실행
|
||||
if (import.meta.client) {
|
||||
return;
|
||||
}
|
||||
|
||||
const gameDataStore = useGameDataStore();
|
||||
|
||||
// gameData가 로드되지 않았다면 gameData API 호출
|
||||
if (!gameDataStore.gameData) {
|
||||
try {
|
||||
await $fetch('/api/gameData');
|
||||
} catch (error) {
|
||||
console.error('gameData 로드 실패:', error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const availableLangCodes = gameDataStore.gameData?.lang_codes || ['ko'];
|
||||
const defaultLangCode = gameDataStore.gameData?.default_lang_code || availableLangCodes[0];
|
||||
|
||||
// 현재 경로에서 언어 코드 추출
|
||||
const pathSegments = to.path.split('/').filter(Boolean);
|
||||
const currentLangCode = pathSegments[0];
|
||||
|
||||
// 언어 코드가 유효한지 확인
|
||||
const isValidLangCode = availableLangCodes.includes(currentLangCode);
|
||||
|
||||
// 유효하지 않은 언어 코드인 경우 기본 언어로 리다이렉트
|
||||
if (currentLangCode && !isValidLangCode) {
|
||||
const newPath = `/${defaultLangCode}${to.path.replace(`/${currentLangCode}`, '')}`;
|
||||
return navigateTo(newPath, { replace: true });
|
||||
}
|
||||
|
||||
// 언어 코드가 없는 경우 기본 언어 코드 추가
|
||||
if (!currentLangCode || !availableLangCodes.includes(currentLangCode)) {
|
||||
const newPath = `/${defaultLangCode}${to.path}`;
|
||||
return navigateTo(newPath, { replace: true });
|
||||
}
|
||||
});
|
||||
120
DYNAMIC_I18N_ROUTES.md
Normal file
120
DYNAMIC_I18N_ROUTES.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# Dynamic I18n Routes - gameData.lang_codes 기반 언어 제외 설정
|
||||
|
||||
이 문서는 `gameDataFromServer.lang_codes`를 기반으로 특정 언어들을 제외하는 Ignoring Localized Routes 기능의 사용법을 설명합니다.
|
||||
|
||||
## 구현된 기능
|
||||
|
||||
### 1. 동적 언어 제외 설정
|
||||
- `gameDataFromServer.lang_codes`에 포함된 언어만 허용
|
||||
- 포함되지 않은 언어는 자동으로 제외 처리
|
||||
- 런타임에 동적으로 언어 설정 변경
|
||||
|
||||
### 2. 구현된 파일들
|
||||
|
||||
#### `i18n.config.ts`
|
||||
- `getI18n()` 함수에 `allowedLangCodes` 매개변수 추가
|
||||
- `generatePageExclusions()` 함수로 언어 제외 설정 생성
|
||||
- `customRoutes: 'config'` 설정으로 페이지별 언어 제외 지원
|
||||
|
||||
#### `layers/composables/useDynamicI18nRoutes.ts`
|
||||
- `getAllowedLangCodes()`: 허용된 언어 목록 반환
|
||||
- `isLangAllowed()`: 특정 언어 허용 여부 확인
|
||||
- `getI18nRouteConfig()`: `defineI18nRoute`용 설정 생성
|
||||
- `getExcludedLangConfig()`: 특정 언어 제외 설정 생성
|
||||
|
||||
#### `layers/plugins/dynamic-i18n-routes.client.ts`
|
||||
- 클라이언트 사이드에서 gameData 변경 감지
|
||||
- 언어 설정 동적 업데이트
|
||||
|
||||
#### `layers/plugins/dynamic-i18n-routes.server.ts`
|
||||
- 서버 사이드에서 gameData 기반 언어 설정 적용
|
||||
|
||||
## 사용법
|
||||
|
||||
### 1. 페이지에서 동적 언어 제외 설정
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// 동적 i18n 라우트 설정
|
||||
const { getI18nRouteConfig } = useDynamicI18nRoutes();
|
||||
|
||||
// gameData.lang_codes를 기반으로 동적 언어 제외 설정
|
||||
const i18nRouteConfig = getI18nRouteConfig();
|
||||
if (i18nRouteConfig) {
|
||||
defineI18nRoute(i18nRouteConfig);
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 2. 특정 언어만 허용하는 경우
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const { getI18nRouteConfig } = useDynamicI18nRoutes();
|
||||
|
||||
// gameData.lang_codes에 포함된 언어만 허용
|
||||
const i18nRouteConfig = getI18nRouteConfig();
|
||||
if (i18nRouteConfig) {
|
||||
defineI18nRoute(i18nRouteConfig);
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 3. 특정 언어를 제외하는 경우
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const { getExcludedLangConfig } = useDynamicI18nRoutes();
|
||||
|
||||
// 특정 언어들을 제외 (예: 'fr', 'de' 제외)
|
||||
const excludedConfig = getExcludedLangConfig(['fr', 'de']);
|
||||
if (excludedConfig) {
|
||||
defineI18nRoute(excludedConfig);
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 4. 언어 허용 여부 확인
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const { isLangAllowed } = useDynamicI18nRoutes();
|
||||
|
||||
// 특정 언어가 허용되는지 확인
|
||||
const isFrenchAllowed = isLangAllowed('fr');
|
||||
const isKoreanAllowed = isLangAllowed('ko');
|
||||
</script>
|
||||
```
|
||||
|
||||
## 동작 원리
|
||||
|
||||
1. **서버 사이드**: `gameDataFromServer.lang_codes`를 기반으로 초기 언어 설정 적용
|
||||
2. **클라이언트 사이드**: gameData 변경 시 언어 설정 동적 업데이트
|
||||
3. **페이지 레벨**: `defineI18nRoute`를 통해 페이지별 언어 제외 설정
|
||||
4. **자동 리다이렉트**: 허용되지 않은 언어로 접근 시 기본 언어로 리다이렉트
|
||||
|
||||
## 예시 시나리오
|
||||
|
||||
### 시나리오 1: 게임별 언어 제한
|
||||
```typescript
|
||||
// gameData.lang_codes = ['ko', 'en', 'ja']
|
||||
// 결과: 한국어, 영어, 일본어만 허용, 나머지 언어는 자동 제외
|
||||
```
|
||||
|
||||
### 시나리오 2: 특정 언어 제외
|
||||
```typescript
|
||||
// gameData.lang_codes = ['ko', 'en', 'ja', 'zh-tw', 'fr', 'de', 'es', 'pt', 'th', 'zh-cn']
|
||||
// getExcludedLangConfig(['fr', 'de']) 호출
|
||||
// 결과: 프랑스어, 독일어 제외, 나머지 언어 허용
|
||||
```
|
||||
|
||||
## 주의사항
|
||||
|
||||
1. `defineI18nRoute`는 컴파일 타임에 실행되므로, 동적 설정이 필요한 경우 `watchEffect`나 `watch`를 사용
|
||||
2. SSR과 클라이언트 사이드 모두에서 일관된 동작을 위해 플러그인 사용 권장
|
||||
3. 언어 변경 시 사용자 경험을 고려한 적절한 리다이렉트 처리 필요
|
||||
|
||||
## 참고 문서
|
||||
|
||||
- [Nuxt i18n - Ignoring Localized Routes](https://i18n.nuxtjs.org/docs/guide/ignoring-localized-routes)
|
||||
- [Nuxt i18n - defineI18nRoute](https://i18n.nuxtjs.org/docs/compiler-macros/define-i18n-route)
|
||||
131
app/app.vue
131
app/app.vue
@@ -1,89 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import type { GameDataValue } from "#layers/types/api/gameData";
|
||||
|
||||
const { locale } = useI18n();
|
||||
const gameCode = useGetGameAlias();
|
||||
|
||||
const gameDataStore = useGameDataStore();
|
||||
const { data: gameData, error } = await useFetch<GameDataValue>(
|
||||
"/api/gameData"
|
||||
);
|
||||
|
||||
// Store에 데이터 저장
|
||||
watchEffect(() => {
|
||||
if (gameData.value) {
|
||||
gameDataStore.setGameData(gameData.value);
|
||||
}
|
||||
});
|
||||
|
||||
// 메타 태그 생성 헬퍼 함수
|
||||
const createMetaTags = (data: GameDataValue) => {
|
||||
const metaTag = data.meta_tag;
|
||||
|
||||
return [
|
||||
{
|
||||
name: "description",
|
||||
content: metaTag?.page_desc || "",
|
||||
},
|
||||
{
|
||||
property: "og:title",
|
||||
content: metaTag?.og_title || "",
|
||||
},
|
||||
{
|
||||
property: "og:description",
|
||||
content: metaTag?.og_desc || "",
|
||||
},
|
||||
{
|
||||
property: "og:image",
|
||||
content: metaTag?.og_image || "",
|
||||
},
|
||||
{
|
||||
property: "twitter:title",
|
||||
content: metaTag?.x_title || "",
|
||||
},
|
||||
{
|
||||
property: "twitter:image",
|
||||
content: metaTag?.x_image || "",
|
||||
},
|
||||
{
|
||||
property: "twitter:description",
|
||||
content: metaTag?.x_desc || "",
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const createFaviconLinks = () => {
|
||||
return [
|
||||
{
|
||||
rel: "icon",
|
||||
type: "image/png",
|
||||
sizes: "32x32",
|
||||
href: gameData.value?.favicon_path || "",
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
watchEffect(() => {
|
||||
if (gameData.value && !error.value) {
|
||||
useHead(() => ({
|
||||
htmlAttrs: {
|
||||
"data-game-code": gameCode || "",
|
||||
lang: locale.value,
|
||||
},
|
||||
title: gameData.value?.meta_tag?.page_title || "",
|
||||
meta: createMetaTags(gameData.value as GameDataValue),
|
||||
link: createFaviconLinks(),
|
||||
}));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLayout>
|
||||
<h1 class="sr-only">{{ gameDataStore.gameData?.game_name }}</h1>
|
||||
<h1 class="sr-only">dddd</h1>
|
||||
<NuxtPage />
|
||||
|
||||
<MoleculesLoadingFull />
|
||||
<MoleculesLoadingLocal />
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useNuxtApp } from "nuxt/app";
|
||||
import { useGameDataStore } from "#layers/stores/useGameDataStore";
|
||||
import type {
|
||||
gameData,
|
||||
GameDataMetaTag,
|
||||
GameDataValue,
|
||||
} from "#layers/types/api/gameData";
|
||||
|
||||
const nuxtApp = useNuxtApp();
|
||||
const getGameData = ref<GameDataValue | null>(null);
|
||||
const metaData = ref<GameDataMetaTag | null>(null);
|
||||
const { setGameData } = useGameDataStore();
|
||||
|
||||
// SSR에서만 접근 가능
|
||||
const gameDataFromServer = import.meta.server
|
||||
? nuxtApp.ssrContext?.event.context.gameData
|
||||
: null;
|
||||
|
||||
if (gameDataFromServer) {
|
||||
getGameData.value = gameDataFromServer;
|
||||
setGameData(gameDataFromServer);
|
||||
}
|
||||
|
||||
const meta = gameDataFromServer?.meta_tag || null;
|
||||
|
||||
if (meta) {
|
||||
metaData.value = meta;
|
||||
useSeoMeta({
|
||||
title: meta.page_title,
|
||||
description: meta.page_desc,
|
||||
ogTitle: meta.og_title,
|
||||
ogImage: meta.og_image,
|
||||
ogDescription: meta.og_desc,
|
||||
ogUrl: meta.og_url,
|
||||
twitterImage: meta.x_image,
|
||||
twitterTitle: meta.x_title,
|
||||
twitterDescription: meta.x_desc,
|
||||
});
|
||||
useHead({
|
||||
// htmlAttrs: {
|
||||
// lang: locale.value
|
||||
// },
|
||||
//meta: [...(updatedMetaTags.value || [])],
|
||||
//link: [...(links.value || [])]
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -6,6 +6,9 @@ import Section from "#layers/components/molecules/Section.vue";
|
||||
const pageDataStore = usePageDataStore();
|
||||
const { pageData } = storeToRefs(pageDataStore);
|
||||
|
||||
// 동적 i18n 라우트 설정
|
||||
// const { getI18nRouteConfig } = useDynamicI18nRoutes();
|
||||
|
||||
// const layout = pageData.value?.meta?.layout ?? "default";
|
||||
const layout = "default"; // 기본 레이아웃 사용
|
||||
|
||||
@@ -14,6 +17,12 @@ definePageMeta({
|
||||
layout: false, // 기본 레이아웃 비활성화
|
||||
});
|
||||
|
||||
// // gameData.lang_codes를 기반으로 동적 언어 제외 설정
|
||||
// const i18nRouteConfig = getI18nRouteConfig();
|
||||
// if (i18nRouteConfig) {
|
||||
// defineI18nRoute(i18nRouteConfig);
|
||||
// }
|
||||
|
||||
// SEO 메타 태그 설정 - pageData가 로드된 후에만 실행
|
||||
watchEffect(() => {
|
||||
if (pageData.value?.meta_tag) {
|
||||
|
||||
116
app/pages/test-lang.vue
Normal file
116
app/pages/test-lang.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div class="p-8">
|
||||
<h1 class="text-2xl font-bold mb-4">{{ t('messages.title_test_lang') }}</h1>
|
||||
|
||||
<div class="mb-6">
|
||||
<h2 class="text-lg font-semibold mb-2">{{ t('messages.welcome') }}</h2>
|
||||
<p><strong>{{ t('messages.GameData_load_status') }}:</strong>
|
||||
<span :class="isGameDataLoaded ? 'text-green-600' : 'text-red-600'">
|
||||
{{ isGameDataLoaded ? '로드됨' : '로드되지 않음' }}
|
||||
</span>
|
||||
</p>
|
||||
<p><strong>{{ t('messages.current_language') }}:</strong> {{ $i18n.locale.value }}</p>
|
||||
<p><strong>{{ t('messages.default_language') }}:</strong> {{ gameDataStore.gameData?.default_lang_code || 'N/A' }}</p>
|
||||
<p><strong>{{ t('messages.available_languages') }}:</strong> {{ availableLanguages.join(', ') }}</p>
|
||||
<p><strong>{{ t('messages.current_url') }}:</strong> {{ $route.path }}</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<h2 class="text-lg font-semibold mb-2">언어 전환:</h2>
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<h2 class="text-lg font-semibold mb-2">직접 링크 테스트:</h2>
|
||||
<div class="space-x-4">
|
||||
<NuxtLink
|
||||
v-for="lang in availableLanguages"
|
||||
:key="lang"
|
||||
:to="`/${lang}/test-lang`"
|
||||
class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
>
|
||||
{{ lang.toUpperCase() }} 페이지로 이동
|
||||
</NuxtLink>
|
||||
<NuxtLink
|
||||
:to="`/ja/test-lang`"
|
||||
class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
>
|
||||
ja 페이지로 이동
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<h2 class="text-lg font-semibold mb-2">GameData 정보:</h2>
|
||||
<div v-if="isGameDataLoaded" class="space-y-2">
|
||||
<p><strong>게임 ID:</strong> {{ gameDataStore.gameData?.game_id }}</p>
|
||||
<p><strong>게임 코드:</strong> {{ gameDataStore.gameData?.game_code }}</p>
|
||||
<p><strong>S3 폴더명:</strong> {{ gameDataStore.gameData?.s3_folder_name }}</p>
|
||||
<p><strong>디자인 테마:</strong> {{ gameDataStore.gameData?.design_theme }}</p>
|
||||
<p><strong>전체:</strong> {{ gameDataStore.gameData?.key_color_codes?.join(', ') }}</p>
|
||||
</div>
|
||||
<pre class="bg-gray-100 p-4 rounded text-sm overflow-auto max-h-80">{{ gameDataInfo }}</pre>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<h2 class="text-lg font-semibold mb-2">동적 언어 설정 테스트:</h2>
|
||||
<div class="space-y-2">
|
||||
<p><strong>현재 i18n 설정:</strong></p>
|
||||
<ul class="list-disc list-inside ml-4">
|
||||
<li>사용 가능한 언어: {{ $i18n.availableLocales.map((l: any) => l.code || l).join(', ') }}</li>
|
||||
<li>기본 언어: {{ $i18n.defaultLocale }}</li>
|
||||
<li>현재 언어: {{ $i18n.locale.value }}</li>
|
||||
</ul>
|
||||
<p class="text-sm text-gray-600 mt-2">
|
||||
GameData에서 가져온 lang_codes가 동적으로 i18n 설정에 반영되었는지 확인하세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import LanguageSwitcher from '../../layers/components/atoms/LanguageSwitcher.vue';
|
||||
|
||||
const { t, locale, locales } = useI18n();
|
||||
const gameDataStore = useGameDataStore();
|
||||
|
||||
// 사용 가능한 언어 목록
|
||||
const availableLanguages = computed(() => {
|
||||
return gameDataStore.gameData?.lang_codes || ['ko'];
|
||||
});
|
||||
|
||||
|
||||
|
||||
// GameData가 로드되었는지 확인
|
||||
const isGameDataLoaded = computed(() => {
|
||||
return !!gameDataStore.gameData
|
||||
})
|
||||
// GameData 정보를 JSON으로 표시
|
||||
const gameDataInfo = computed(() => {
|
||||
if (!gameDataStore.gameData) {
|
||||
return 'GameData가 로드되지 않았습니다.';
|
||||
}
|
||||
|
||||
return JSON.stringify({
|
||||
game_id: gameDataStore.gameData.game_id,
|
||||
game_code: gameDataStore.gameData.game_code,
|
||||
s3_folder_name: gameDataStore.gameData.s3_folder_name,
|
||||
default_lang_code: gameDataStore.gameData.default_lang_code,
|
||||
lang_codes: gameDataStore.gameData.lang_codes,
|
||||
design_theme: gameDataStore.gameData.design_theme,
|
||||
key_color_codes: gameDataStore.gameData.key_color_codes,
|
||||
use_game_font: gameDataStore.gameData.use_game_font,
|
||||
ga_code: gameDataStore.gameData.ga_code,
|
||||
favicon_path: gameDataStore.gameData.favicon_path
|
||||
}, null, 2);
|
||||
});
|
||||
|
||||
// 페이지 메타 설정
|
||||
useHead({
|
||||
title: `언어 테스트 - ${locale.value.toUpperCase()}`
|
||||
});
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
@@ -1,15 +1,61 @@
|
||||
// import type { LocaleObject, NuxtI18nOptions } from "@nuxtjs/i18n";
|
||||
import type { NuxtI18nOptions } from "@nuxtjs/i18n";
|
||||
import type { LocaleObject, NuxtI18nOptions } from "@nuxtjs/i18n";
|
||||
|
||||
const LANG_DIR: string = "./i18n/locales";
|
||||
|
||||
const DEFAULT_COVERAGES: string[] = [
|
||||
"en",
|
||||
"ja",
|
||||
"ko",
|
||||
"zh-tw",
|
||||
"fr",
|
||||
"de",
|
||||
"es",
|
||||
"pt",
|
||||
"th",
|
||||
"zh-cn",
|
||||
];
|
||||
|
||||
const DEFAULT_LOCALES: Record<string, LocaleObject> = {
|
||||
en: { code: "en", name: "English", iso: "en", dir: "ltr" },
|
||||
"zh-tw": { code: "zh-tw", name: "繁體中文", iso: "zh-tw", dir: "ltr" },
|
||||
ja: { code: "ja", name: "日本語", iso: "ja", dir: "ltr" },
|
||||
ko: { code: "ko", name: "한국어", iso: "ko-KR", dir: "ltr" },
|
||||
fr: { code: "fr", name: "Français", iso: "fr", dir: "ltr" },
|
||||
de: { code: "de", name: "Deutsch", iso: "de", dir: "ltr" },
|
||||
es: { code: "es", name: "Español", iso: "es", dir: "ltr" },
|
||||
pt: { code: "pt", name: "Português", iso: "pt", dir: "ltr" },
|
||||
th: { code: "th", name: "ภาษาไทย", iso: "th", dir: "ltr" },
|
||||
"zh-cn": { code: "zh-cn", name: "简体中文", iso: "zh-cn", dir: "ltr" },
|
||||
};
|
||||
const DEFAULT_LOCALE_CODE = "ko";
|
||||
|
||||
// getI18n 함수가 NuxtI18nOptions 타입의 값을 반환하도록 명시적으로 타입을 지정합니다.
|
||||
const getI18n = (): NuxtI18nOptions => {
|
||||
const getI18n = (allowedLangCodes?: string[]): NuxtI18nOptions => {
|
||||
// allowedLangCodes가 제공되면 해당 언어들만 사용, 그렇지 않으면 모든 기본 언어 사용
|
||||
const targetCoverages =
|
||||
allowedLangCodes && allowedLangCodes.length > 0
|
||||
? DEFAULT_COVERAGES.filter((code) => allowedLangCodes.includes(code))
|
||||
: DEFAULT_COVERAGES;
|
||||
|
||||
const DEFAULT_LOCALE_COVERAGES_SET: LocaleObject[] = targetCoverages.map(
|
||||
(code: string): LocaleObject => ({
|
||||
code,
|
||||
file: `${code}.ts`,
|
||||
// 아래의 옵셔널 체이닝(?.)은 해당 코드가 undefined일 경우를 안전하게 처리합니다.
|
||||
name: DEFAULT_LOCALES[code]?.name ?? code,
|
||||
iso: DEFAULT_LOCALES[code]?.iso ?? code,
|
||||
// dir 속성은 모든 로케일 객체에 공통적으로 존재하므로, 여기서 추가할 수도 있습니다.
|
||||
dir: DEFAULT_LOCALES[code]?.dir ?? "ltr",
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
strategy: "prefix",
|
||||
vueI18n: "custom",
|
||||
locales: ["ko"],
|
||||
defaultLocale: "ko",
|
||||
locales: DEFAULT_LOCALE_COVERAGES_SET,
|
||||
defaultLocale: DEFAULT_LOCALE_CODE || "ko",
|
||||
detectBrowserLanguage: {
|
||||
fallbackLocale: "ko",
|
||||
fallbackLocale: DEFAULT_LOCALE_CODE || "ko",
|
||||
useCookie: false,
|
||||
redirectOn: "root",
|
||||
},
|
||||
@@ -18,7 +64,31 @@ const getI18n = (): NuxtI18nOptions => {
|
||||
escapeHtml: false,
|
||||
},
|
||||
debug: false,
|
||||
// 동적으로 언어 제외 설정을 위한 pages 설정
|
||||
customRoutes: "config",
|
||||
pages:
|
||||
allowedLangCodes && allowedLangCodes.length > 0
|
||||
? generatePageExclusions(allowedLangCodes)
|
||||
: {},
|
||||
// 추가적인 설정이 필요하다면 여기에 포함시킬 수 있습니다.
|
||||
};
|
||||
};
|
||||
|
||||
export { getI18n };
|
||||
// gameData.lang_codes를 기반으로 언어 제외 설정을 생성하는 함수
|
||||
const generatePageExclusions = (
|
||||
allowedLangCodes: string[]
|
||||
): Record<string, any> => {
|
||||
const exclusions: Record<string, any> = {};
|
||||
|
||||
// 모든 기본 언어에 대해 제외 설정 생성
|
||||
DEFAULT_COVERAGES.forEach((langCode) => {
|
||||
if (!allowedLangCodes.includes(langCode)) {
|
||||
// 해당 언어가 허용되지 않으면 모든 페이지에서 제외
|
||||
exclusions[langCode] = false;
|
||||
}
|
||||
});
|
||||
|
||||
return exclusions;
|
||||
};
|
||||
|
||||
export { DEFAULT_LOCALE_CODE, DEFAULT_COVERAGES, getI18n };
|
||||
|
||||
54
i18n/locales/de.ts
Normal file
54
i18n/locales/de.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
// import { TRANSLATION_ITEMS } from '../i18n.config'
|
||||
// common.json 파일을 직접 import
|
||||
// @ts-ignore
|
||||
import commonData from '../../layers/assets/data/common.json'
|
||||
|
||||
export default defineI18nLocale(async (locale: string) => {
|
||||
const config = useRuntimeConfig()
|
||||
const baseType = config.public.baseType
|
||||
const translationItems = config.public.translationItems
|
||||
const translationItemsArr = translationItems.split(',')
|
||||
const staticUrl = config.public.staticUrl
|
||||
const translationApi = translationItemsArr.map((item: string): string => {
|
||||
return `${staticUrl}/${baseType}/tmp/${item}.json`
|
||||
})
|
||||
|
||||
// API 데이터 가져오기
|
||||
const fetchDataPromises = translationApi.map((apiUrl) => {
|
||||
return useFetch(apiUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=UTF-8'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
try {
|
||||
const fetchResults = await Promise.all(fetchDataPromises)
|
||||
|
||||
// 각 결과에서 locale에 맞는 데이터를 추출
|
||||
const apiData = fetchResults.map((result) => {
|
||||
return result.data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환
|
||||
})
|
||||
|
||||
// apiData를 이용해 자동으로 병합
|
||||
const mergedResult = apiData.reduce((acc, data) => {
|
||||
return { ...acc, ...data }
|
||||
}, {})
|
||||
|
||||
// common.json에서 해당 locale의 데이터를 가져와서 병합
|
||||
const commonLocaleData = commonData[locale] || {}
|
||||
|
||||
// API 데이터와 common.json 데이터를 병합 (common.json이 우선순위)
|
||||
const finalResult = { ...mergedResult, ...commonLocaleData }
|
||||
|
||||
// 병합된 결과 출력
|
||||
// console.log('finalResult:', finalResult)
|
||||
|
||||
return finalResult
|
||||
} catch (error) {
|
||||
console.error('Error fetching translation data:', error)
|
||||
// 에러 발생 시 common.json 데이터라도 반환
|
||||
return commonData[locale] || {}
|
||||
}
|
||||
})
|
||||
62
i18n/locales/en.ts
Normal file
62
i18n/locales/en.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
// import { TRANSLATION_ITEMS } from '../i18n.config'
|
||||
// common.json 파일을 직접 import
|
||||
// @ts-ignore
|
||||
import commonData from '../../layers/assets/data/common.json'
|
||||
|
||||
export default defineI18nLocale(async (locale: string) => {
|
||||
// const config = useRuntimeConfig()
|
||||
// const baseType = config.public.baseType
|
||||
// const translationItems = config.public.translationItems
|
||||
// const translationItemsArr = translationItems.split(',')
|
||||
// const staticUrl = config.public.staticUrl
|
||||
// const translationPaths = translationItemsArr.map((item) => {
|
||||
// 경로를 생성하며 ~/assets/data 경로로 설정
|
||||
// return `~/assets/data/${item}.json`
|
||||
// })
|
||||
// const resources = await Promise.all(translationPaths.map((path) => import(`${path}`)))
|
||||
|
||||
// console.log('translationLocal ~ translationLocal:', translationLocal)
|
||||
// const translationApi = translationItemsArr.map((item: string): string => {
|
||||
// return `${staticUrl}/${baseType}/tmp/${item}.json`
|
||||
// })
|
||||
|
||||
// // API 데이터 가져오기
|
||||
// const fetchDataPromises = translationApi.map((apiUrl) => {
|
||||
// return useFetch(apiUrl, {
|
||||
// method: 'GET',
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json;charset=UTF-8'
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
|
||||
try {
|
||||
// const fetchResults = await Promise.all(fetchDataPromises)
|
||||
|
||||
// // 각 결과에서 locale에 맞는 데이터를 추출
|
||||
// const apiData = fetchResults.map((result) => {
|
||||
// return result.data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환
|
||||
// })
|
||||
|
||||
// // apiData를 이용해 자동으로 병합
|
||||
// const mergedResult = apiData.reduce((acc, data) => {
|
||||
// return { ...acc, ...data }
|
||||
// }, {})
|
||||
|
||||
// common.json에서 해당 locale의 데이터를 가져와서 병합
|
||||
const commonLocaleData = commonData[locale] || {}
|
||||
|
||||
// API 데이터와 common.json 데이터를 병합 (common.json이 우선순위)
|
||||
// const finalResult = { ...mergedResult, ...commonLocaleData }
|
||||
const finalResult = { ...commonLocaleData }
|
||||
|
||||
// 병합된 결과 출력
|
||||
// console.log('finalResult:', finalResult)
|
||||
|
||||
return finalResult
|
||||
} catch (error) {
|
||||
console.error('Error fetching translation data:', error)
|
||||
// 에러 발생 시 common.json 데이터라도 반환
|
||||
return commonData[locale] || {}
|
||||
}
|
||||
})
|
||||
54
i18n/locales/es.ts
Normal file
54
i18n/locales/es.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
// import { TRANSLATION_ITEMS } from '../i18n.config'
|
||||
// common.json 파일을 직접 import
|
||||
// @ts-ignore
|
||||
import commonData from '../../layers/assets/data/common.json'
|
||||
|
||||
export default defineI18nLocale(async (locale: string) => {
|
||||
const config = useRuntimeConfig()
|
||||
const baseType = config.public.baseType
|
||||
const translationItems = config.public.translationItems
|
||||
const translationItemsArr = translationItems.split(',')
|
||||
const staticUrl = config.public.staticUrl
|
||||
const translationApi = translationItemsArr.map((item: string): string => {
|
||||
return `${staticUrl}/${baseType}/tmp/${item}.json`
|
||||
})
|
||||
|
||||
// API 데이터 가져오기
|
||||
const fetchDataPromises = translationApi.map((apiUrl) => {
|
||||
return useFetch(apiUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=UTF-8'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
try {
|
||||
const fetchResults = await Promise.all(fetchDataPromises)
|
||||
|
||||
// 각 결과에서 locale에 맞는 데이터를 추출
|
||||
const apiData = fetchResults.map((result) => {
|
||||
return result.data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환
|
||||
})
|
||||
|
||||
// apiData를 이용해 자동으로 병합
|
||||
const mergedResult = apiData.reduce((acc, data) => {
|
||||
return { ...acc, ...data }
|
||||
}, {})
|
||||
|
||||
// common.json에서 해당 locale의 데이터를 가져와서 병합
|
||||
const commonLocaleData = commonData[locale] || {}
|
||||
|
||||
// API 데이터와 common.json 데이터를 병합 (common.json이 우선순위)
|
||||
const finalResult = { ...mergedResult, ...commonLocaleData }
|
||||
|
||||
// 병합된 결과 출력
|
||||
// console.log('finalResult:', finalResult)
|
||||
|
||||
return finalResult
|
||||
} catch (error) {
|
||||
console.error('Error fetching translation data:', error)
|
||||
// 에러 발생 시 common.json 데이터라도 반환
|
||||
return commonData[locale] || {}
|
||||
}
|
||||
})
|
||||
1
i18n/locales/fallback/de.ts
Normal file
1
i18n/locales/fallback/de.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default {}
|
||||
1
i18n/locales/fallback/en.ts
Normal file
1
i18n/locales/fallback/en.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default {}
|
||||
1
i18n/locales/fallback/es.ts
Normal file
1
i18n/locales/fallback/es.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default {}
|
||||
1
i18n/locales/fallback/fr.ts
Normal file
1
i18n/locales/fallback/fr.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default {}
|
||||
1
i18n/locales/fallback/it.ts
Normal file
1
i18n/locales/fallback/it.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default {}
|
||||
1
i18n/locales/fallback/ja.ts
Normal file
1
i18n/locales/fallback/ja.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default {}
|
||||
1
i18n/locales/fallback/ko.ts
Normal file
1
i18n/locales/fallback/ko.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default {}
|
||||
1
i18n/locales/fallback/pt.ts
Normal file
1
i18n/locales/fallback/pt.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default {}
|
||||
1
i18n/locales/fallback/th.ts
Normal file
1
i18n/locales/fallback/th.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default {}
|
||||
1
i18n/locales/fallback/zh-cn.ts
Normal file
1
i18n/locales/fallback/zh-cn.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default {}
|
||||
1
i18n/locales/fallback/zh-tw.ts
Normal file
1
i18n/locales/fallback/zh-tw.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default {}
|
||||
54
i18n/locales/fr.ts
Normal file
54
i18n/locales/fr.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
// import { TRANSLATION_ITEMS } from '../i18n.config'
|
||||
// common.json 파일을 직접 import
|
||||
// @ts-ignore
|
||||
import commonData from '../../layers/assets/data/common.json'
|
||||
|
||||
export default defineI18nLocale(async (locale: string) => {
|
||||
const config = useRuntimeConfig()
|
||||
const baseType = config.public.baseType
|
||||
const translationItems = config.public.translationItems
|
||||
const translationItemsArr = translationItems.split(',')
|
||||
const staticUrl = config.public.staticUrl
|
||||
const translationApi = translationItemsArr.map((item: string): string => {
|
||||
return `${staticUrl}/${baseType}/tmp/${item}.json`
|
||||
})
|
||||
|
||||
// API 데이터 가져오기
|
||||
const fetchDataPromises = translationApi.map((apiUrl) => {
|
||||
return useFetch(apiUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=UTF-8'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
try {
|
||||
const fetchResults = await Promise.all(fetchDataPromises)
|
||||
|
||||
// 각 결과에서 locale에 맞는 데이터를 추출
|
||||
const apiData = fetchResults.map((result) => {
|
||||
return result.data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환
|
||||
})
|
||||
|
||||
// apiData를 이용해 자동으로 병합
|
||||
const mergedResult = apiData.reduce((acc, data) => {
|
||||
return { ...acc, ...data }
|
||||
}, {})
|
||||
|
||||
// common.json에서 해당 locale의 데이터를 가져와서 병합
|
||||
const commonLocaleData = commonData[locale] || {}
|
||||
|
||||
// API 데이터와 common.json 데이터를 병합 (common.json이 우선순위)
|
||||
const finalResult = { ...mergedResult, ...commonLocaleData }
|
||||
|
||||
// 병합된 결과 출력
|
||||
// console.log('finalResult:', finalResult)
|
||||
|
||||
return finalResult
|
||||
} catch (error) {
|
||||
console.error('Error fetching translation data:', error)
|
||||
// 에러 발생 시 common.json 데이터라도 반환
|
||||
return commonData[locale] || {}
|
||||
}
|
||||
})
|
||||
62
i18n/locales/ja.ts
Normal file
62
i18n/locales/ja.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
// import { TRANSLATION_ITEMS } from '../i18n.config'
|
||||
// common.json 파일을 직접 import
|
||||
// @ts-ignore
|
||||
import commonData from '../../layers/assets/data/common.json'
|
||||
|
||||
export default defineI18nLocale(async (locale: string) => {
|
||||
// const config = useRuntimeConfig()
|
||||
// const baseType = config.public.baseType
|
||||
// const translationItems = config.public.translationItems
|
||||
// const translationItemsArr = translationItems.split(',')
|
||||
// const staticUrl = config.public.staticUrl
|
||||
// const translationPaths = translationItemsArr.map((item) => {
|
||||
// 경로를 생성하며 ~/assets/data 경로로 설정
|
||||
// return `~/assets/data/${item}.json`
|
||||
// })
|
||||
// const resources = await Promise.all(translationPaths.map((path) => import(`${path}`)))
|
||||
|
||||
// console.log('translationLocal ~ translationLocal:', translationLocal)
|
||||
// const translationApi = translationItemsArr.map((item: string): string => {
|
||||
// return `${staticUrl}/${baseType}/tmp/${item}.json`
|
||||
// })
|
||||
|
||||
// // API 데이터 가져오기
|
||||
// const fetchDataPromises = translationApi.map((apiUrl) => {
|
||||
// return useFetch(apiUrl, {
|
||||
// method: 'GET',
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json;charset=UTF-8'
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
|
||||
try {
|
||||
// const fetchResults = await Promise.all(fetchDataPromises)
|
||||
|
||||
// // 각 결과에서 locale에 맞는 데이터를 추출
|
||||
// const apiData = fetchResults.map((result) => {
|
||||
// return result.data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환
|
||||
// })
|
||||
|
||||
// // apiData를 이용해 자동으로 병합
|
||||
// const mergedResult = apiData.reduce((acc, data) => {
|
||||
// return { ...acc, ...data }
|
||||
// }, {})
|
||||
|
||||
// common.json에서 해당 locale의 데이터를 가져와서 병합
|
||||
const commonLocaleData = commonData[locale] || {}
|
||||
|
||||
// API 데이터와 common.json 데이터를 병합 (common.json이 우선순위)
|
||||
// const finalResult = { ...mergedResult, ...commonLocaleData }
|
||||
const finalResult = { ...commonLocaleData }
|
||||
|
||||
// 병합된 결과 출력
|
||||
// console.log('finalResult:', finalResult)
|
||||
|
||||
return finalResult
|
||||
} catch (error) {
|
||||
console.error('Error fetching translation data:', error)
|
||||
// 에러 발생 시 common.json 데이터라도 반환
|
||||
return commonData[locale] || {}
|
||||
}
|
||||
})
|
||||
62
i18n/locales/ko.ts
Normal file
62
i18n/locales/ko.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
// import { TRANSLATION_ITEMS } from '../i18n.config'
|
||||
// common.json 파일을 직접 import
|
||||
// @ts-ignore
|
||||
import commonData from '../../layers/assets/data/common.json'
|
||||
|
||||
export default defineI18nLocale(async (locale: string) => {
|
||||
// const config = useRuntimeConfig()
|
||||
// const baseType = config.public.baseType
|
||||
// const translationItems = config.public.translationItems
|
||||
// const translationItemsArr = translationItems.split(',')
|
||||
// const staticUrl = config.public.staticUrl
|
||||
// const translationPaths = translationItemsArr.map((item) => {
|
||||
// 경로를 생성하며 ~/assets/data 경로로 설정
|
||||
// return `~/assets/data/${item}.json`
|
||||
// })
|
||||
// const resources = await Promise.all(translationPaths.map((path) => import(`${path}`)))
|
||||
|
||||
// console.log('translationLocal ~ translationLocal:', translationLocal)
|
||||
// const translationApi = translationItemsArr.map((item: string): string => {
|
||||
// return `${staticUrl}/${baseType}/tmp/${item}.json`
|
||||
// })
|
||||
|
||||
// // API 데이터 가져오기
|
||||
// const fetchDataPromises = translationApi.map((apiUrl) => {
|
||||
// return useFetch(apiUrl, {
|
||||
// method: 'GET',
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json;charset=UTF-8'
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
|
||||
try {
|
||||
// const fetchResults = await Promise.all(fetchDataPromises)
|
||||
|
||||
// // 각 결과에서 locale에 맞는 데이터를 추출
|
||||
// const apiData = fetchResults.map((result) => {
|
||||
// return result.data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환
|
||||
// })
|
||||
|
||||
// // apiData를 이용해 자동으로 병합
|
||||
// const mergedResult = apiData.reduce((acc, data) => {
|
||||
// return { ...acc, ...data }
|
||||
// }, {})
|
||||
|
||||
// common.json에서 해당 locale의 데이터를 가져와서 병합
|
||||
const commonLocaleData = commonData[locale] || {}
|
||||
|
||||
// API 데이터와 common.json 데이터를 병합 (common.json이 우선순위)
|
||||
// const finalResult = { ...mergedResult, ...commonLocaleData }
|
||||
const finalResult = { ...commonLocaleData }
|
||||
|
||||
// 병합된 결과 출력
|
||||
// console.log('finalResult:', finalResult)
|
||||
|
||||
return finalResult
|
||||
} catch (error) {
|
||||
console.error('Error fetching translation data:', error)
|
||||
// 에러 발생 시 common.json 데이터라도 반환
|
||||
return commonData[locale] || {}
|
||||
}
|
||||
})
|
||||
54
i18n/locales/pt.ts
Normal file
54
i18n/locales/pt.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
// import { TRANSLATION_ITEMS } from '../i18n.config'
|
||||
// common.json 파일을 직접 import
|
||||
// @ts-ignore
|
||||
import commonData from '../../layers/assets/data/common.json'
|
||||
|
||||
export default defineI18nLocale(async (locale: string) => {
|
||||
const config = useRuntimeConfig()
|
||||
const baseType = config.public.baseType
|
||||
const translationItems = config.public.translationItems
|
||||
const translationItemsArr = translationItems.split(',')
|
||||
const staticUrl = config.public.staticUrl
|
||||
const translationApi = translationItemsArr.map((item: string): string => {
|
||||
return `${staticUrl}/${baseType}/tmp/${item}.json`
|
||||
})
|
||||
|
||||
// API 데이터 가져오기
|
||||
const fetchDataPromises = translationApi.map((apiUrl) => {
|
||||
return useFetch(apiUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=UTF-8'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
try {
|
||||
const fetchResults = await Promise.all(fetchDataPromises)
|
||||
|
||||
// 각 결과에서 locale에 맞는 데이터를 추출
|
||||
const apiData = fetchResults.map((result) => {
|
||||
return result.data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환
|
||||
})
|
||||
|
||||
// apiData를 이용해 자동으로 병합
|
||||
const mergedResult = apiData.reduce((acc, data) => {
|
||||
return { ...acc, ...data }
|
||||
}, {})
|
||||
|
||||
// common.json에서 해당 locale의 데이터를 가져와서 병합
|
||||
const commonLocaleData = commonData[locale] || {}
|
||||
|
||||
// API 데이터와 common.json 데이터를 병합 (common.json이 우선순위)
|
||||
const finalResult = { ...mergedResult, ...commonLocaleData }
|
||||
|
||||
// 병합된 결과 출력
|
||||
// console.log('finalResult:', finalResult)
|
||||
|
||||
return finalResult
|
||||
} catch (error) {
|
||||
console.error('Error fetching translation data:', error)
|
||||
// 에러 발생 시 common.json 데이터라도 반환
|
||||
return commonData[locale] || {}
|
||||
}
|
||||
})
|
||||
54
i18n/locales/th.ts
Normal file
54
i18n/locales/th.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
// import { TRANSLATION_ITEMS } from '../i18n.config'
|
||||
// common.json 파일을 직접 import
|
||||
// @ts-ignore
|
||||
import commonData from '../../layers/assets/data/common.json'
|
||||
|
||||
export default defineI18nLocale(async (locale: string) => {
|
||||
const config = useRuntimeConfig()
|
||||
const baseType = config.public.baseType
|
||||
const translationItems = config.public.translationItems
|
||||
const translationItemsArr = translationItems.split(',')
|
||||
const staticUrl = config.public.staticUrl
|
||||
const translationApi = translationItemsArr.map((item: string): string => {
|
||||
return `${staticUrl}/${baseType}/tmp/${item}.json`
|
||||
})
|
||||
|
||||
// API 데이터 가져오기
|
||||
const fetchDataPromises = translationApi.map((apiUrl) => {
|
||||
return useFetch(apiUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=UTF-8'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
try {
|
||||
const fetchResults = await Promise.all(fetchDataPromises)
|
||||
|
||||
// 각 결과에서 locale에 맞는 데이터를 추출
|
||||
const apiData = fetchResults.map((result) => {
|
||||
return result.data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환
|
||||
})
|
||||
|
||||
// apiData를 이용해 자동으로 병합
|
||||
const mergedResult = apiData.reduce((acc, data) => {
|
||||
return { ...acc, ...data }
|
||||
}, {})
|
||||
|
||||
// common.json에서 해당 locale의 데이터를 가져와서 병합
|
||||
const commonLocaleData = commonData[locale] || {}
|
||||
|
||||
// API 데이터와 common.json 데이터를 병합 (common.json이 우선순위)
|
||||
const finalResult = { ...mergedResult, ...commonLocaleData }
|
||||
|
||||
// 병합된 결과 출력
|
||||
// console.log('finalResult:', finalResult)
|
||||
|
||||
return finalResult
|
||||
} catch (error) {
|
||||
console.error('Error fetching translation data:', error)
|
||||
// 에러 발생 시 common.json 데이터라도 반환
|
||||
return commonData[locale] || {}
|
||||
}
|
||||
})
|
||||
62
i18n/locales/zh-cn.ts
Normal file
62
i18n/locales/zh-cn.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
// import { TRANSLATION_ITEMS } from '../i18n.config'
|
||||
// common.json 파일을 직접 import
|
||||
// @ts-ignore
|
||||
import commonData from '../../layers/assets/data/common.json'
|
||||
|
||||
export default defineI18nLocale(async (locale: string) => {
|
||||
// const config = useRuntimeConfig()
|
||||
// const baseType = config.public.baseType
|
||||
// const translationItems = config.public.translationItems
|
||||
// const translationItemsArr = translationItems.split(',')
|
||||
// const staticUrl = config.public.staticUrl
|
||||
// const translationPaths = translationItemsArr.map((item) => {
|
||||
// 경로를 생성하며 ~/assets/data 경로로 설정
|
||||
// return `~/assets/data/${item}.json`
|
||||
// })
|
||||
// const resources = await Promise.all(translationPaths.map((path) => import(`${path}`)))
|
||||
|
||||
// console.log('translationLocal ~ translationLocal:', translationLocal)
|
||||
// const translationApi = translationItemsArr.map((item: string): string => {
|
||||
// return `${staticUrl}/${baseType}/tmp/${item}.json`
|
||||
// })
|
||||
|
||||
// // API 데이터 가져오기
|
||||
// const fetchDataPromises = translationApi.map((apiUrl) => {
|
||||
// return useFetch(apiUrl, {
|
||||
// method: 'GET',
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json;charset=UTF-8'
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
|
||||
try {
|
||||
// const fetchResults = await Promise.all(fetchDataPromises)
|
||||
|
||||
// // 각 결과에서 locale에 맞는 데이터를 추출
|
||||
// const apiData = fetchResults.map((result) => {
|
||||
// return result.data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환
|
||||
// })
|
||||
|
||||
// // apiData를 이용해 자동으로 병합
|
||||
// const mergedResult = apiData.reduce((acc, data) => {
|
||||
// return { ...acc, ...data }
|
||||
// }, {})
|
||||
|
||||
// common.json에서 해당 locale의 데이터를 가져와서 병합
|
||||
const commonLocaleData = commonData[locale] || {}
|
||||
|
||||
// API 데이터와 common.json 데이터를 병합 (common.json이 우선순위)
|
||||
// const finalResult = { ...mergedResult, ...commonLocaleData }
|
||||
const finalResult = { ...commonLocaleData }
|
||||
|
||||
// 병합된 결과 출력
|
||||
// console.log('finalResult:', finalResult)
|
||||
|
||||
return finalResult
|
||||
} catch (error) {
|
||||
console.error('Error fetching translation data:', error)
|
||||
// 에러 발생 시 common.json 데이터라도 반환
|
||||
return commonData[locale] || {}
|
||||
}
|
||||
})
|
||||
62
i18n/locales/zh-tw.ts
Normal file
62
i18n/locales/zh-tw.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
// import { TRANSLATION_ITEMS } from '../i18n.config'
|
||||
// common.json 파일을 직접 import
|
||||
// @ts-ignore
|
||||
import commonData from '../../layers/assets/data/common.json'
|
||||
|
||||
export default defineI18nLocale(async (locale: string) => {
|
||||
// const config = useRuntimeConfig()
|
||||
// const baseType = config.public.baseType
|
||||
// const translationItems = config.public.translationItems
|
||||
// const translationItemsArr = translationItems.split(',')
|
||||
// const staticUrl = config.public.staticUrl
|
||||
// const translationPaths = translationItemsArr.map((item) => {
|
||||
// 경로를 생성하며 ~/assets/data 경로로 설정
|
||||
// return `~/assets/data/${item}.json`
|
||||
// })
|
||||
// const resources = await Promise.all(translationPaths.map((path) => import(`${path}`)))
|
||||
|
||||
// console.log('translationLocal ~ translationLocal:', translationLocal)
|
||||
// const translationApi = translationItemsArr.map((item: string): string => {
|
||||
// return `${staticUrl}/${baseType}/tmp/${item}.json`
|
||||
// })
|
||||
|
||||
// // API 데이터 가져오기
|
||||
// const fetchDataPromises = translationApi.map((apiUrl) => {
|
||||
// return useFetch(apiUrl, {
|
||||
// method: 'GET',
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json;charset=UTF-8'
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
|
||||
try {
|
||||
// const fetchResults = await Promise.all(fetchDataPromises)
|
||||
|
||||
// // 각 결과에서 locale에 맞는 데이터를 추출
|
||||
// const apiData = fetchResults.map((result) => {
|
||||
// return result.data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환
|
||||
// })
|
||||
|
||||
// // apiData를 이용해 자동으로 병합
|
||||
// const mergedResult = apiData.reduce((acc, data) => {
|
||||
// return { ...acc, ...data }
|
||||
// }, {})
|
||||
|
||||
// common.json에서 해당 locale의 데이터를 가져와서 병합
|
||||
const commonLocaleData = commonData[locale] || {}
|
||||
|
||||
// API 데이터와 common.json 데이터를 병합 (common.json이 우선순위)
|
||||
// const finalResult = { ...mergedResult, ...commonLocaleData }
|
||||
const finalResult = { ...commonLocaleData }
|
||||
|
||||
// 병합된 결과 출력
|
||||
// console.log('finalResult:', finalResult)
|
||||
|
||||
return finalResult
|
||||
} catch (error) {
|
||||
console.error('Error fetching translation data:', error)
|
||||
// 에러 발생 시 common.json 데이터라도 반환
|
||||
return commonData[locale] || {}
|
||||
}
|
||||
})
|
||||
514
layers/assets/data/common.json
Normal file
514
layers/assets/data/common.json
Normal file
@@ -0,0 +1,514 @@
|
||||
{
|
||||
"ko": {
|
||||
"common": {
|
||||
"loading": "로딩 중...",
|
||||
"error": "오류가 발생했습니다",
|
||||
"success": "성공했습니다",
|
||||
"cancel": "취소",
|
||||
"confirm": "확인",
|
||||
"save": "저장",
|
||||
"delete": "삭제",
|
||||
"edit": "편집",
|
||||
"close": "닫기",
|
||||
"back": "뒤로",
|
||||
"next": "다음",
|
||||
"previous": "이전",
|
||||
"search": "검색",
|
||||
"filter": "필터",
|
||||
"sort": "정렬",
|
||||
"refresh": "새로고침",
|
||||
"download": "다운로드",
|
||||
"upload": "업로드",
|
||||
"copy": "복사",
|
||||
"paste": "붙여넣기",
|
||||
"cut": "잘라내기",
|
||||
"undo": "실행 취소",
|
||||
"redo": "다시 실행"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "홈",
|
||||
"about": "소개",
|
||||
"contact": "연락처",
|
||||
"services": "서비스",
|
||||
"products": "제품",
|
||||
"news": "뉴스",
|
||||
"support": "지원",
|
||||
"login": "로그인",
|
||||
"logout": "로그아웃",
|
||||
"register": "회원가입",
|
||||
"profile": "프로필",
|
||||
"settings": "설정"
|
||||
},
|
||||
"messages": {
|
||||
"title_test_lang": "언어 설정 테스트!",
|
||||
"welcome": "환영합니다!",
|
||||
"GameData_load_status": "GameData 로드 상태",
|
||||
"current_language": "현재 언어",
|
||||
"default_language": "기본 언어",
|
||||
"available_languages": "사용 가능한 언어",
|
||||
"current_url": "현재 URL",
|
||||
"no_results": "검색 결과가 없습니다",
|
||||
"try_again": "다시 시도해주세요"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"common": {
|
||||
"loading": "Loading...",
|
||||
"error": "An error occurred",
|
||||
"success": "Success",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"save": "Save",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"close": "Close",
|
||||
"back": "Back",
|
||||
"next": "Next",
|
||||
"previous": "Previous",
|
||||
"search": "Search",
|
||||
"filter": "Filter",
|
||||
"sort": "Sort",
|
||||
"refresh": "Refresh",
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"copy": "Copy",
|
||||
"paste": "Paste",
|
||||
"cut": "Cut",
|
||||
"undo": "Undo",
|
||||
"redo": "Redo"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Home",
|
||||
"about": "About",
|
||||
"contact": "Contact",
|
||||
"services": "Services",
|
||||
"products": "Products",
|
||||
"news": "News",
|
||||
"support": "Support",
|
||||
"login": "Login",
|
||||
"logout": "Logout",
|
||||
"register": "Register",
|
||||
"profile": "Profile",
|
||||
"settings": "Settings"
|
||||
},
|
||||
"messages": {
|
||||
"welcome": "Welcome!",
|
||||
"goodbye": "Goodbye",
|
||||
"thank_you": "Thank you",
|
||||
"sorry": "Sorry",
|
||||
"please_wait": "Please wait",
|
||||
"no_data": "No data available",
|
||||
"no_results": "No results found",
|
||||
"try_again": "Please try again"
|
||||
}
|
||||
},
|
||||
"zh-tw": {
|
||||
"common": {
|
||||
"loading": "載入中...",
|
||||
"error": "發生錯誤",
|
||||
"success": "成功",
|
||||
"cancel": "取消",
|
||||
"confirm": "確認",
|
||||
"save": "儲存",
|
||||
"delete": "刪除",
|
||||
"edit": "編輯",
|
||||
"close": "關閉",
|
||||
"back": "返回",
|
||||
"next": "下一步",
|
||||
"previous": "上一步",
|
||||
"search": "搜尋",
|
||||
"filter": "篩選",
|
||||
"sort": "排序",
|
||||
"refresh": "重新整理",
|
||||
"download": "下載",
|
||||
"upload": "上傳",
|
||||
"copy": "複製",
|
||||
"paste": "貼上",
|
||||
"cut": "剪下",
|
||||
"undo": "復原",
|
||||
"redo": "重做"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "首頁",
|
||||
"about": "關於我們",
|
||||
"contact": "聯絡我們",
|
||||
"services": "服務",
|
||||
"products": "產品",
|
||||
"news": "新聞",
|
||||
"support": "支援",
|
||||
"login": "登入",
|
||||
"logout": "登出",
|
||||
"register": "註冊",
|
||||
"profile": "個人資料",
|
||||
"settings": "設定"
|
||||
},
|
||||
"messages": {
|
||||
"title_test_lang": "語言設定測試!",
|
||||
"welcome": "歡迎!",
|
||||
"GameData_load_status": "GameData 載入狀態",
|
||||
"current_language": "目前語言",
|
||||
"default_language": "預設語言",
|
||||
"available_languages": "可用語言",
|
||||
"current_url": "目前網址",
|
||||
"no_results": "找不到結果",
|
||||
"try_again": "請再試一次"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"common": {
|
||||
"loading": "読み込み中...",
|
||||
"error": "エラーが発生しました",
|
||||
"success": "成功",
|
||||
"cancel": "キャンセル",
|
||||
"confirm": "確認",
|
||||
"save": "保存",
|
||||
"delete": "削除",
|
||||
"edit": "編集",
|
||||
"close": "閉じる",
|
||||
"back": "戻る",
|
||||
"next": "次へ",
|
||||
"previous": "前へ",
|
||||
"search": "検索",
|
||||
"filter": "フィルター",
|
||||
"sort": "並び替え",
|
||||
"refresh": "更新",
|
||||
"download": "ダウンロード",
|
||||
"upload": "アップロード",
|
||||
"copy": "コピー",
|
||||
"paste": "貼り付け",
|
||||
"cut": "切り取り",
|
||||
"undo": "元に戻す",
|
||||
"redo": "やり直し"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "ホーム",
|
||||
"about": "概要",
|
||||
"contact": "お問い合わせ",
|
||||
"services": "サービス",
|
||||
"products": "製品",
|
||||
"news": "ニュース",
|
||||
"support": "サポート",
|
||||
"login": "ログイン",
|
||||
"logout": "ログアウト",
|
||||
"register": "登録",
|
||||
"profile": "プロフィール",
|
||||
"settings": "設定"
|
||||
},
|
||||
"messages": {
|
||||
"welcome": "ようこそ!",
|
||||
"goodbye": "さようなら",
|
||||
"thank_you": "ありがとうございます",
|
||||
"sorry": "申し訳ありません",
|
||||
"please_wait": "お待ちください",
|
||||
"no_data": "データがありません",
|
||||
"no_results": "結果が見つかりません",
|
||||
"try_again": "もう一度お試しください"
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"common": {
|
||||
"loading": "Chargement...",
|
||||
"error": "Une erreur s'est produite",
|
||||
"success": "Succès",
|
||||
"cancel": "Annuler",
|
||||
"confirm": "Confirmer",
|
||||
"save": "Enregistrer",
|
||||
"delete": "Supprimer",
|
||||
"edit": "Modifier",
|
||||
"close": "Fermer",
|
||||
"back": "Retour",
|
||||
"next": "Suivant",
|
||||
"previous": "Précédent",
|
||||
"search": "Rechercher",
|
||||
"filter": "Filtrer",
|
||||
"sort": "Trier",
|
||||
"refresh": "Actualiser",
|
||||
"download": "Télécharger",
|
||||
"upload": "Téléverser",
|
||||
"copy": "Copier",
|
||||
"paste": "Coller",
|
||||
"cut": "Couper",
|
||||
"undo": "Annuler",
|
||||
"redo": "Rétablir"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Accueil",
|
||||
"about": "À propos",
|
||||
"contact": "Contact",
|
||||
"services": "Services",
|
||||
"products": "Produits",
|
||||
"news": "Actualités",
|
||||
"support": "Support",
|
||||
"login": "Connexion",
|
||||
"logout": "Déconnexion",
|
||||
"register": "S'inscrire",
|
||||
"profile": "Profil",
|
||||
"settings": "Paramètres"
|
||||
},
|
||||
"messages": {
|
||||
"welcome": "Bienvenue !",
|
||||
"goodbye": "Au revoir",
|
||||
"thank_you": "Merci",
|
||||
"sorry": "Désolé",
|
||||
"please_wait": "Veuillez patienter",
|
||||
"no_data": "Aucune donnée disponible",
|
||||
"no_results": "Aucun résultat trouvé",
|
||||
"try_again": "Veuillez réessayer"
|
||||
}
|
||||
},
|
||||
"de": {
|
||||
"common": {
|
||||
"loading": "Laden...",
|
||||
"error": "Ein Fehler ist aufgetreten",
|
||||
"success": "Erfolg",
|
||||
"cancel": "Abbrechen",
|
||||
"confirm": "Bestätigen",
|
||||
"save": "Speichern",
|
||||
"delete": "Löschen",
|
||||
"edit": "Bearbeiten",
|
||||
"close": "Schließen",
|
||||
"back": "Zurück",
|
||||
"next": "Weiter",
|
||||
"previous": "Vorherige",
|
||||
"search": "Suchen",
|
||||
"filter": "Filtern",
|
||||
"sort": "Sortieren",
|
||||
"refresh": "Aktualisieren",
|
||||
"download": "Herunterladen",
|
||||
"upload": "Hochladen",
|
||||
"copy": "Kopieren",
|
||||
"paste": "Einfügen",
|
||||
"cut": "Ausschneiden",
|
||||
"undo": "Rückgängig",
|
||||
"redo": "Wiederholen"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Startseite",
|
||||
"about": "Über uns",
|
||||
"contact": "Kontakt",
|
||||
"services": "Dienstleistungen",
|
||||
"products": "Produkte",
|
||||
"news": "Nachrichten",
|
||||
"support": "Support",
|
||||
"login": "Anmelden",
|
||||
"logout": "Abmelden",
|
||||
"register": "Registrieren",
|
||||
"profile": "Profil",
|
||||
"settings": "Einstellungen"
|
||||
},
|
||||
"messages": {
|
||||
"welcome": "Willkommen!",
|
||||
"goodbye": "Auf Wiedersehen",
|
||||
"thank_you": "Danke",
|
||||
"sorry": "Entschuldigung",
|
||||
"please_wait": "Bitte warten",
|
||||
"no_data": "Keine Daten verfügbar",
|
||||
"no_results": "Keine Ergebnisse gefunden",
|
||||
"try_again": "Bitte versuchen Sie es erneut"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"common": {
|
||||
"loading": "Cargando...",
|
||||
"error": "Ocurrió un error",
|
||||
"success": "Éxito",
|
||||
"cancel": "Cancelar",
|
||||
"confirm": "Confirmar",
|
||||
"save": "Guardar",
|
||||
"delete": "Eliminar",
|
||||
"edit": "Editar",
|
||||
"close": "Cerrar",
|
||||
"back": "Atrás",
|
||||
"next": "Siguiente",
|
||||
"previous": "Anterior",
|
||||
"search": "Buscar",
|
||||
"filter": "Filtrar",
|
||||
"sort": "Ordenar",
|
||||
"refresh": "Actualizar",
|
||||
"download": "Descargar",
|
||||
"upload": "Subir",
|
||||
"copy": "Copiar",
|
||||
"paste": "Pegar",
|
||||
"cut": "Cortar",
|
||||
"undo": "Deshacer",
|
||||
"redo": "Rehacer"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Inicio",
|
||||
"about": "Acerca de",
|
||||
"contact": "Contacto",
|
||||
"services": "Servicios",
|
||||
"products": "Productos",
|
||||
"news": "Noticias",
|
||||
"support": "Soporte",
|
||||
"login": "Iniciar sesión",
|
||||
"logout": "Cerrar sesión",
|
||||
"register": "Registrarse",
|
||||
"profile": "Perfil",
|
||||
"settings": "Configuración"
|
||||
},
|
||||
"messages": {
|
||||
"welcome": "¡Bienvenido!",
|
||||
"goodbye": "Adiós",
|
||||
"thank_you": "Gracias",
|
||||
"sorry": "Lo siento",
|
||||
"please_wait": "Por favor espere",
|
||||
"no_data": "No hay datos disponibles",
|
||||
"no_results": "No se encontraron resultados",
|
||||
"try_again": "Por favor intente de nuevo"
|
||||
}
|
||||
},
|
||||
"pt": {
|
||||
"common": {
|
||||
"loading": "Carregando...",
|
||||
"error": "Ocorreu um erro",
|
||||
"success": "Sucesso",
|
||||
"cancel": "Cancelar",
|
||||
"confirm": "Confirmar",
|
||||
"save": "Salvar",
|
||||
"delete": "Excluir",
|
||||
"edit": "Editar",
|
||||
"close": "Fechar",
|
||||
"back": "Voltar",
|
||||
"next": "Próximo",
|
||||
"previous": "Anterior",
|
||||
"search": "Pesquisar",
|
||||
"filter": "Filtrar",
|
||||
"sort": "Ordenar",
|
||||
"refresh": "Atualizar",
|
||||
"download": "Baixar",
|
||||
"upload": "Enviar",
|
||||
"copy": "Copiar",
|
||||
"paste": "Colar",
|
||||
"cut": "Cortar",
|
||||
"undo": "Desfazer",
|
||||
"redo": "Refazer"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Início",
|
||||
"about": "Sobre",
|
||||
"contact": "Contato",
|
||||
"services": "Serviços",
|
||||
"products": "Produtos",
|
||||
"news": "Notícias",
|
||||
"support": "Suporte",
|
||||
"login": "Entrar",
|
||||
"logout": "Sair",
|
||||
"register": "Registrar",
|
||||
"profile": "Perfil",
|
||||
"settings": "Configurações"
|
||||
},
|
||||
"messages": {
|
||||
"welcome": "Bem-vindo!",
|
||||
"goodbye": "Tchau",
|
||||
"thank_you": "Obrigado",
|
||||
"sorry": "Desculpe",
|
||||
"please_wait": "Por favor aguarde",
|
||||
"no_data": "Nenhum dado disponível",
|
||||
"no_results": "Nenhum resultado encontrado",
|
||||
"try_again": "Por favor tente novamente"
|
||||
}
|
||||
},
|
||||
"th": {
|
||||
"common": {
|
||||
"loading": "กำลังโหลด...",
|
||||
"error": "เกิดข้อผิดพลาด",
|
||||
"success": "สำเร็จ",
|
||||
"cancel": "ยกเลิก",
|
||||
"confirm": "ยืนยัน",
|
||||
"save": "บันทึก",
|
||||
"delete": "ลบ",
|
||||
"edit": "แก้ไข",
|
||||
"close": "ปิด",
|
||||
"back": "กลับ",
|
||||
"next": "ถัดไป",
|
||||
"previous": "ก่อนหน้า",
|
||||
"search": "ค้นหา",
|
||||
"filter": "กรอง",
|
||||
"sort": "เรียงลำดับ",
|
||||
"refresh": "รีเฟรช",
|
||||
"download": "ดาวน์โหลด",
|
||||
"upload": "อัปโหลด",
|
||||
"copy": "คัดลอก",
|
||||
"paste": "วาง",
|
||||
"cut": "ตัด",
|
||||
"undo": "เลิกทำ",
|
||||
"redo": "ทำซ้ำ"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "หน้าแรก",
|
||||
"about": "เกี่ยวกับ",
|
||||
"contact": "ติดต่อ",
|
||||
"services": "บริการ",
|
||||
"products": "ผลิตภัณฑ์",
|
||||
"news": "ข่าวสาร",
|
||||
"support": "สนับสนุน",
|
||||
"login": "เข้าสู่ระบบ",
|
||||
"logout": "ออกจากระบบ",
|
||||
"register": "สมัครสมาชิก",
|
||||
"profile": "โปรไฟล์",
|
||||
"settings": "การตั้งค่า"
|
||||
},
|
||||
"messages": {
|
||||
"welcome": "ยินดีต้อนรับ!",
|
||||
"goodbye": "ลาก่อน",
|
||||
"thank_you": "ขอบคุณ",
|
||||
"sorry": "ขออภัย",
|
||||
"please_wait": "กรุณารอสักครู่",
|
||||
"no_data": "ไม่มีข้อมูล",
|
||||
"no_results": "ไม่พบผลลัพธ์",
|
||||
"try_again": "กรุณาลองใหม่อีกครั้ง"
|
||||
}
|
||||
},
|
||||
"zh-cn": {
|
||||
"common": {
|
||||
"loading": "加载中...",
|
||||
"error": "发生错误",
|
||||
"success": "成功",
|
||||
"cancel": "取消",
|
||||
"confirm": "确认",
|
||||
"save": "保存",
|
||||
"delete": "删除",
|
||||
"edit": "编辑",
|
||||
"close": "关闭",
|
||||
"back": "返回",
|
||||
"next": "下一步",
|
||||
"previous": "上一步",
|
||||
"search": "搜索",
|
||||
"filter": "筛选",
|
||||
"sort": "排序",
|
||||
"refresh": "刷新",
|
||||
"download": "下载",
|
||||
"upload": "上传",
|
||||
"copy": "复制",
|
||||
"paste": "粘贴",
|
||||
"cut": "剪切",
|
||||
"undo": "撤销",
|
||||
"redo": "重做"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "首页",
|
||||
"about": "关于我们",
|
||||
"contact": "联系我们",
|
||||
"services": "服务",
|
||||
"products": "产品",
|
||||
"news": "新闻",
|
||||
"support": "支持",
|
||||
"login": "登录",
|
||||
"logout": "退出",
|
||||
"register": "注册",
|
||||
"profile": "个人资料",
|
||||
"settings": "设置"
|
||||
},
|
||||
"messages": {
|
||||
"welcome": "欢迎!",
|
||||
"goodbye": "再见",
|
||||
"thank_you": "谢谢",
|
||||
"sorry": "抱歉",
|
||||
"please_wait": "请稍候",
|
||||
"no_data": "无可用数据",
|
||||
"no_results": "未找到结果",
|
||||
"try_again": "请重试"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
const { locale } = useI18n();
|
||||
const switchLocalePath = useSwitchLocalePath();
|
||||
|
||||
const selectedLocale = ref(locale.value);
|
||||
|
||||
const switchLanguage = async () => {
|
||||
const path = switchLocalePath(selectedLocale.value);
|
||||
if (path) {
|
||||
await navigateTo(path);
|
||||
}
|
||||
console.log("Language:", locale.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="language-switcher">
|
||||
<select v-model="selectedLocale" @change="switchLanguage">
|
||||
<option value="ko">한국어</option>
|
||||
<option value="en">English</option>
|
||||
<div class="bg-white">
|
||||
<select
|
||||
class="text-black px-2 py-1 rounded-md"
|
||||
v-model="selectedLocale"
|
||||
@change="switchLanguage"
|
||||
>
|
||||
<option
|
||||
v-for="localeOption in availableLanguages"
|
||||
:key="localeOption"
|
||||
:value="localeOption"
|
||||
>
|
||||
{{ localeOption }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.language-switcher {
|
||||
display: inline-block;
|
||||
}
|
||||
<script setup lang="ts">
|
||||
const gameDataStore = useGameDataStore();
|
||||
|
||||
.language-switcher select {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
// 사용 가능한 언어 목록
|
||||
const availableLanguages = computed(() => {
|
||||
return gameDataStore.gameData?.lang_codes || ["ko"];
|
||||
});
|
||||
|
||||
.language-switcher select:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
</style>
|
||||
const { locale } = useI18n();
|
||||
const switchLocalePath = useSwitchLocalePath();
|
||||
const router = useRouter();
|
||||
|
||||
const selectedLocale = ref(locale.value);
|
||||
|
||||
// 언어 변경 함수
|
||||
const switchLanguage = async () => {
|
||||
console.log(
|
||||
"🚀 ~ switchLanguage ~ selectedLocale.value:",
|
||||
selectedLocale.value
|
||||
);
|
||||
if (selectedLocale.value) {
|
||||
// URL 경로를 통해 언어 변경
|
||||
const path = switchLocalePath(selectedLocale.value);
|
||||
if (path) {
|
||||
await router.push(path);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// locale이 변경될 때 selectedLocale도 동기화
|
||||
watch(locale, (newLocale) => {
|
||||
selectedLocale.value = newLocale;
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
<template>
|
||||
<footer class="border-t p-4">Global Footer</footer>
|
||||
<footer class="border-t p-4 bg-black">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<a href="/" class="text-white">로고</a>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<AtomsLanguageSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
@@ -4,12 +4,16 @@ const gameData = computed(() => gameDataStore.gameData);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="bg-gray-900 text-white relative z-50">
|
||||
<header class="bg-black text-white relative z-50">
|
||||
<LayoutStoveGnb />
|
||||
<div class="px-[40px] h-16 flex items-center">
|
||||
<!-- 로고 -->
|
||||
<div class="mr-[40px]">
|
||||
<img :src="gameData?.gnb?.bi_path" :alt="gameData?.game_name" />
|
||||
<img
|
||||
:src="gameData?.gnb?.bi_path"
|
||||
:alt="gameData?.game_name"
|
||||
class="h-[30px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 메인 네비게이션 -->
|
||||
|
||||
135
layers/components/molecules/modal/YouTubeModal.vue
Normal file
135
layers/components/molecules/modal/YouTubeModal.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition
|
||||
enter-active-class="transition duration-300 ease-out"
|
||||
enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-active-class="transition duration-200 ease-in"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-75"
|
||||
@click="handleBackdropClick"
|
||||
>
|
||||
<Transition
|
||||
enter-active-class="transition duration-300 ease-out"
|
||||
enter-from-class="opacity-0 scale-95"
|
||||
enter-to-class="opacity-100 scale-100"
|
||||
leave-active-class="transition duration-200 ease-in"
|
||||
leave-from-class="opacity-100 scale-100"
|
||||
leave-to-class="opacity-0 scale-95"
|
||||
>
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="relative w-full max-w-4xl mx-4"
|
||||
@click.stop
|
||||
>
|
||||
<!-- 헤더 -->
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
@click="closeModal"
|
||||
class="p-1 text-white rounded-full transition-colors"
|
||||
aria-label="모달 닫기"
|
||||
>
|
||||
<svg
|
||||
class="w-8 h-8"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 유튜브 영상 컨테이너 -->
|
||||
<div class="relative w-full" :style="{ paddingBottom: '56.25%' }">
|
||||
<iframe
|
||||
v-if="youtubeId"
|
||||
:src="`https://www.youtube.com/embed/${youtubeId}?autoplay=1&rel=0`"
|
||||
class="absolute top-0 left-0 w-full h-full"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
title="YouTube video player"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
isOpen: boolean
|
||||
youtubeId: string
|
||||
title?: string
|
||||
description?: string
|
||||
closeOnBackdrop?: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'close'): void
|
||||
(e: 'update:isOpen', value: boolean): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: '',
|
||||
description: '',
|
||||
closeOnBackdrop: true
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// ESC 키로 모달 닫기
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape' && props.isOpen) {
|
||||
closeModal()
|
||||
}
|
||||
}
|
||||
|
||||
// 배경 클릭으로 모달 닫기
|
||||
const handleBackdropClick = () => {
|
||||
if (props.closeOnBackdrop) {
|
||||
closeModal()
|
||||
}
|
||||
}
|
||||
|
||||
// 모달 닫기 함수
|
||||
const closeModal = () => {
|
||||
emit('close')
|
||||
emit('update:isOpen', false)
|
||||
}
|
||||
|
||||
// 키보드 이벤트 리스너 등록/해제
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', handleKeydown)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', handleKeydown)
|
||||
})
|
||||
|
||||
// 모달이 열릴 때 body 스크롤 방지
|
||||
watch(() => props.isOpen, (isOpen) => {
|
||||
if (isOpen) {
|
||||
document.body.style.overflow = 'hidden'
|
||||
} else {
|
||||
document.body.style.overflow = ''
|
||||
}
|
||||
})
|
||||
|
||||
// 컴포넌트 언마운트 시 body 스크롤 복원
|
||||
onUnmounted(() => {
|
||||
document.body.style.overflow = ''
|
||||
})
|
||||
</script>
|
||||
@@ -40,5 +40,15 @@ const assetPath = computed(() => {
|
||||
<source :src="assetPath" type="video/mp4" />
|
||||
<source :src="assetPath" type="video/webm" />
|
||||
</video>
|
||||
<div
|
||||
class="absolute inset-0"
|
||||
style="
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(16, 13, 15, 0) 0%,
|
||||
#100d0f 95%
|
||||
);
|
||||
"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
69
layers/composables/useDynamicI18nRoutes.ts
Normal file
69
layers/composables/useDynamicI18nRoutes.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import type { GameDataValue } from '#layers/types/api/gameData'
|
||||
import { DEFAULT_COVERAGES } from '@/i18n.config'
|
||||
|
||||
/**
|
||||
* gameData.lang_codes를 기반으로 동적으로 언어 제외 설정을 생성하는 컴포저블
|
||||
*/
|
||||
export const useDynamicI18nRoutes = () => {
|
||||
const gameDataStore = useGameDataStore()
|
||||
|
||||
/**
|
||||
* 현재 gameData의 lang_codes를 기반으로 허용된 언어 목록을 반환
|
||||
*/
|
||||
const getAllowedLangCodes = (): string[] => {
|
||||
const gameData = gameDataStore.gameData
|
||||
return gameData?.lang_codes || []
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 언어가 허용되는지 확인
|
||||
*/
|
||||
const isLangAllowed = (langCode: string): boolean => {
|
||||
const allowedLangCodes = getAllowedLangCodes()
|
||||
return allowedLangCodes.length === 0 || allowedLangCodes.includes(langCode)
|
||||
}
|
||||
|
||||
/**
|
||||
* defineI18nRoute에서 사용할 수 있는 언어 제외 설정을 생성
|
||||
* @param pagePath - 페이지 경로 (선택사항)
|
||||
* @returns 언어 제외 설정 객체
|
||||
*/
|
||||
const getI18nRouteConfig = (pagePath?: string) => {
|
||||
const allowedLangCodes = getAllowedLangCodes()
|
||||
|
||||
// 허용된 언어가 없으면 모든 언어 허용
|
||||
if (allowedLangCodes.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// 허용된 언어만 포함하는 설정 반환
|
||||
return {
|
||||
locales: allowedLangCodes
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 언어를 제외하는 설정을 생성
|
||||
* @param excludedLangCodes - 제외할 언어 코드 배열
|
||||
* @returns 언어 제외 설정 객체
|
||||
*/
|
||||
const getExcludedLangConfig = (excludedLangCodes: string[]) => {
|
||||
const allowedLangCodes = getAllowedLangCodes()
|
||||
|
||||
// 허용된 언어에서 제외할 언어를 제거
|
||||
const finalAllowedCodes = allowedLangCodes.length > 0
|
||||
? allowedLangCodes.filter(code => !excludedLangCodes.includes(code))
|
||||
: ['en', 'ja', 'ko', 'zh-tw', 'fr', 'de', 'es', 'pt', 'th', 'zh-cn'].filter(code => !excludedLangCodes.includes(code))
|
||||
|
||||
return {
|
||||
locales: finalAllowedCodes
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
getAllowedLangCodes,
|
||||
isLangAllowed,
|
||||
getI18nRouteConfig,
|
||||
getExcludedLangConfig
|
||||
}
|
||||
}
|
||||
35
layers/middleware/00.route.global.ts
Normal file
35
layers/middleware/00.route.global.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useGameDataStore } from "#layers/stores/useGameDataStore";
|
||||
|
||||
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||
// 서버 사이드에서는 스킵
|
||||
if (import.meta.server) {
|
||||
return;
|
||||
}
|
||||
|
||||
const gameDataStore = useGameDataStore();
|
||||
|
||||
// gameData가 로드되지 않았으면 스킵 (다른 미들웨어에서 로드됨)
|
||||
if (!gameDataStore.gameData) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 현재 경로에서 언어 코드 추출
|
||||
// 예: /ko/about/story -> ko
|
||||
// 예: /en/test/page -> en
|
||||
const languagePattern = /^\/([a-z]{2})(?:\/|$)/;
|
||||
const match = to.path.match(languagePattern);
|
||||
const currentLangCode = match ? match[1] : null;
|
||||
console.log("🚀 3333~ currentLangCode:", currentLangCode)
|
||||
|
||||
// 허용된 언어 코드 목록
|
||||
const allowedLangCodes = gameDataStore.gameData.lang_codes || [];
|
||||
console.log("🚀 ~ allowedLangCodes:", allowedLangCodes)
|
||||
|
||||
// 현재 언어가 허용된 언어 목록에 없으면 404로 리다이렉트
|
||||
if (currentLangCode && !allowedLangCodes.includes(currentLangCode)) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: "Language not supported"
|
||||
});
|
||||
}
|
||||
});
|
||||
25
layers/plugins/dynamic-i18n-routes.client.ts
Normal file
25
layers/plugins/dynamic-i18n-routes.client.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export default defineNuxtPlugin(() => {
|
||||
const { $i18n } = useNuxtApp()
|
||||
const gameDataStore = useGameDataStore()
|
||||
|
||||
// gameData가 로드되면 언어 제외 설정 적용
|
||||
watchEffect(() => {
|
||||
const gameData = gameDataStore.gameData
|
||||
if (gameData && gameData.lang_codes && gameData.lang_codes.length > 0) {
|
||||
const allowedLangCodes = gameData.lang_codes
|
||||
|
||||
// 현재 설정된 locales에서 허용된 언어만 필터링
|
||||
const availableLocales = $i18n.locales.value.filter((locale: any) =>
|
||||
allowedLangCodes.includes(locale.code)
|
||||
)
|
||||
|
||||
// locales 업데이트
|
||||
$i18n.locales.value = availableLocales
|
||||
|
||||
// 현재 locale이 허용되지 않은 경우 기본 locale로 변경
|
||||
if (!allowedLangCodes.includes($i18n.locale.value)) {
|
||||
$i18n.locale.value = gameData.default_lang_code || 'ko'
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
25
layers/plugins/dynamic-i18n-routes.server.ts
Normal file
25
layers/plugins/dynamic-i18n-routes.server.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export default defineNuxtPlugin(() => {
|
||||
const { $i18n } = useNuxtApp()
|
||||
|
||||
// SSR에서 gameData를 가져와서 언어 제외 설정 적용
|
||||
const gameDataFromServer = process.server
|
||||
? useNuxtApp().ssrContext?.event.context.gameData
|
||||
: null
|
||||
|
||||
if (gameDataFromServer && gameDataFromServer.lang_codes && gameDataFromServer.lang_codes.length > 0) {
|
||||
const allowedLangCodes = gameDataFromServer.lang_codes
|
||||
|
||||
// 현재 설정된 locales에서 허용된 언어만 필터링
|
||||
const availableLocales = $i18n.locales.value.filter((locale: any) =>
|
||||
allowedLangCodes.includes(locale.code)
|
||||
)
|
||||
|
||||
// locales 업데이트
|
||||
$i18n.locales.value = availableLocales
|
||||
|
||||
// 현재 locale이 허용되지 않은 경우 기본 locale로 변경
|
||||
if (!allowedLangCodes.includes($i18n.locale.value)) {
|
||||
$i18n.locale.value = gameDataFromServer.default_lang_code || 'ko'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,6 +1,24 @@
|
||||
import { getHeader, getRequestHost, defineEventHandler } from "h3";
|
||||
import {
|
||||
getHeader,
|
||||
getRequestHost,
|
||||
defineEventHandler,
|
||||
getRequestURL,
|
||||
} from "h3";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const url = getRequestURL(event);
|
||||
|
||||
// 정적 자산, API, 파비콘 등은 제외하고 페이지 요청만 처리
|
||||
if (
|
||||
url.pathname.startsWith("/api/") ||
|
||||
url.pathname.startsWith("/_nuxt/") ||
|
||||
url.pathname.startsWith("/favicon") ||
|
||||
url.pathname.includes(".") ||
|
||||
url.pathname.startsWith("/_")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
export default defineEventHandler((event) => {
|
||||
const host =
|
||||
(getHeader(event, "host") || getRequestHost(event)).toString() || "";
|
||||
const baseDomain = process.env.BASE_DOMAIN || ".onstove.com";
|
||||
@@ -13,4 +31,39 @@ export default defineEventHandler((event) => {
|
||||
event.context.gameAlias = gameAlias;
|
||||
}
|
||||
}
|
||||
|
||||
// gameData를 직접 가져와서 context에 저장 (API 호출 없이)
|
||||
try {
|
||||
const config = useRuntimeConfig();
|
||||
const stoveApiBaseUrl = config.public.stoveApiUrl;
|
||||
const apiUrl = `${stoveApiBaseUrl}/pub-comm/v1.0/template/game`;
|
||||
|
||||
// URL의 첫 번째 path를 lang_code로 사용 (파비콘, API 경로 제외)
|
||||
const url = getRequestURL(event);
|
||||
const pathSegments = url.pathname
|
||||
.split("/")
|
||||
.filter(
|
||||
(segment) =>
|
||||
segment &&
|
||||
!segment.includes("favicon") &&
|
||||
!segment.includes("api") &&
|
||||
!segment.startsWith("_")
|
||||
);
|
||||
const langCode = pathSegments[0] || "ko";
|
||||
|
||||
const queryParams: Record<string, string> = {
|
||||
game_alias: event.context.gameAlias || "",
|
||||
lang_code: langCode,
|
||||
};
|
||||
|
||||
const response = await $fetch(apiUrl, {
|
||||
query: queryParams,
|
||||
});
|
||||
|
||||
if (response?.code === 0 && "value" in response) {
|
||||
event.context.gameData = response.value;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("gameData load error:", error);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,10 +3,8 @@ import { defineStore } from "pinia";
|
||||
export const useLoadingStore = defineStore("loadingStore", () => {
|
||||
// 글로벌 로딩 표기
|
||||
const fullLoading = ref(false);
|
||||
// 컴포넌트별 로딩 표기
|
||||
const localLoadings = ref<
|
||||
Map<string, { active: boolean; element?: HTMLElement }>
|
||||
>(new Map());
|
||||
// 컴포넌트별 로딩 표기 - Map 대신 일반 객체 사용
|
||||
const localLoadings = ref<Record<string, { active: boolean }>>({});
|
||||
// 로딩 상태만 표기
|
||||
const isLoading = ref(false);
|
||||
|
||||
@@ -15,7 +13,7 @@ export const useLoadingStore = defineStore("loadingStore", () => {
|
||||
*/
|
||||
const initializeStore = () => {
|
||||
fullLoading.value = false;
|
||||
localLoadings.value.clear();
|
||||
localLoadings.value = {};
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -33,15 +31,15 @@ export const useLoadingStore = defineStore("loadingStore", () => {
|
||||
* Local 로딩
|
||||
*/
|
||||
const startLocalLoading = (localId: string) => {
|
||||
localLoadings.value.set(localId, { active: true });
|
||||
localLoadings.value[localId] = { active: true };
|
||||
};
|
||||
|
||||
const stopLocalLoading = (localId: string) => {
|
||||
localLoadings.value.delete(localId);
|
||||
delete localLoadings.value[localId];
|
||||
};
|
||||
|
||||
const isLocalLoading = (localId: string) => {
|
||||
return localLoadings.value.get(localId)?.active || false;
|
||||
return localLoadings.value[localId]?.active || false;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,9 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import YouTubeModal from "#layers/components/molecules/modal/YouTubeModal.vue";
|
||||
|
||||
interface Props {
|
||||
components: Record<string, any>;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
console.log("components:", props.components);
|
||||
|
||||
// YouTube 모달 상태 관리
|
||||
const isYouTubeModalOpen = ref(false);
|
||||
const youtubeVideoId = ref("");
|
||||
|
||||
// 비디오 플레이 버튼 클릭 핸들러
|
||||
const handleVideoPlayClick = () => {
|
||||
// TODO: 실제 YouTube 비디오 ID를 설정해야 합니다
|
||||
// 예시: 'dQw4w9WgXcQ' (Rick Astley - Never Gonna Give You Up)
|
||||
youtubeVideoId.value = "UKVsZYHxYTc"; // 임시로 설정
|
||||
isYouTubeModalOpen.value = true;
|
||||
};
|
||||
|
||||
// 모달 닫기 핸들러
|
||||
const handleCloseModal = () => {
|
||||
isYouTubeModalOpen.value = false;
|
||||
youtubeVideoId.value = "";
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -22,6 +43,15 @@ const props = defineProps<Props>();
|
||||
<TemplatesVideoPlay
|
||||
v-if="props.components.videoPlay"
|
||||
:component-data="props.components.videoPlay"
|
||||
@click="handleVideoPlayClick"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- YouTube 모달 -->
|
||||
<YouTubeModal
|
||||
:is-open="isYouTubeModalOpen"
|
||||
:youtube-id="youtubeVideoId"
|
||||
@close="handleCloseModal"
|
||||
@update:is-open="isYouTubeModalOpen = $event"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -55,7 +55,7 @@ export default defineNuxtConfig({
|
||||
},
|
||||
extends: [resolve(__dirname, "layers")],
|
||||
|
||||
// i18n 설정
|
||||
// i18n 설정 - 런타임에 동적으로 설정됨
|
||||
i18n: getI18n(),
|
||||
|
||||
experimental: {
|
||||
|
||||
2939
pnpm-lock.yaml
generated
2939
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user