Merge branch 'feature/20250907-gil' into 'feature/202509XX-all'

feat. i18n 설정

See merge request sgp-web-d/web-template-fe!2
This commit is contained in:
김채린
2025-09-09 04:09:54 +00:00
45 changed files with 2929 additions and 2374 deletions

View 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 });
});

View 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
View 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 {};
}
});

View 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
View 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)

View File

@@ -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>

View File

@@ -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
View 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>

View File

@@ -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
View 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
View 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
View 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] || {}
}
})

View File

@@ -0,0 +1 @@
export default {}

View File

@@ -0,0 +1 @@
export default {}

View File

@@ -0,0 +1 @@
export default {}

View File

@@ -0,0 +1 @@
export default {}

View File

@@ -0,0 +1 @@
export default {}

View File

@@ -0,0 +1 @@
export default {}

View File

@@ -0,0 +1 @@
export default {}

View File

@@ -0,0 +1 @@
export default {}

View File

@@ -0,0 +1 @@
export default {}

View File

@@ -0,0 +1 @@
export default {}

View File

@@ -0,0 +1 @@
export default {}

54
i18n/locales/fr.ts Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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] || {}
}
})

View 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": "请重试"
}
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
<!-- 메인 네비게이션 -->

View 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>

View File

@@ -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>

View 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
}
}

View 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"
});
}
});

View 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'
}
}
})
})

View 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'
}
}
})

View File

@@ -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);
}
});

View File

@@ -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;
};
/**

View File

@@ -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>

View File

@@ -55,7 +55,7 @@ export default defineNuxtConfig({
},
extends: [resolve(__dirname, "layers")],
// i18n 설정
// i18n 설정 - 런타임에 동적으로 설정됨
i18n: getI18n(),
experimental: {

2939
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff