Files
web-temp/layers/components/blocks/LanguageSwitcher.vue
clkim 4f83ae2311 refactor. 게임 데이터 초기화 개선
- LanguageSwitcher.vue에서 언어 전환 시 게임 데이터 즉시 갱신 로직 개선
- init-game-data.server.ts 파일 추가로 서버 사이드에서 게임 데이터 초기화 로직 구현
- app.vue에서 불필요한 게임 데이터 초기화 로직 제거
2026-03-25 14:04:43 +09:00

268 lines
9.1 KiB
Vue

<template>
<div
class="flex items-center justify-center relative w-[180px] text-left text-xs text-[#ccc] transition-opacity duration-300"
:class="{ 'opacity-50 pointer-events-none': isChanging }"
>
<button
:disabled="isChanging"
class="flex items-center gap-2 px-3 py-2 rounded-lg text-[#CCCCCC] transition-all duration-300 w-[180px] bg-[#292929] border border-[#595959]"
:class="{ 'opacity-50 cursor-not-allowed': isChanging }"
@click="toggleDropdown"
>
<!-- 지구본 아이콘 -->
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_5964_1685)">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M14.6666 8.00065C14.6666 11.6825 11.6818 14.6673 7.99992 14.6673C4.31802 14.6673 1.33325 11.6825 1.33325 8.00065C1.33325 4.31875 4.31802 1.33398 7.99992 1.33398C11.6818 1.33398 14.6666 4.31875 14.6666 8.00065ZM6.89756 13.2199C6.03596 11.8504 5.50924 10.2901 5.36895 8.66732H2.70785C2.99033 10.9326 4.69347 12.7567 6.89756 13.2199ZM2.70785 7.33398H5.36895C5.50924 5.71116 6.03596 4.15086 6.89756 2.78138C4.69347 3.24458 2.99033 5.06868 2.70785 7.33398ZM13.292 8.66732C13.0095 10.9326 11.3064 12.7567 9.10228 13.2199C9.96388 11.8504 10.4906 10.2901 10.6309 8.66732H13.292ZM13.292 7.33398C13.0095 5.06868 11.3064 3.24458 9.10228 2.78138C9.96388 4.15086 10.4906 5.71116 10.6309 7.33398H13.292ZM7.99992 12.468C7.28662 11.3201 6.84273 10.0202 6.70801 8.66732H9.29183C9.15711 10.0202 8.71322 11.3201 7.99992 12.468ZM6.70801 7.33398H9.29183C9.15711 5.98112 8.71322 4.68121 7.99992 3.5333C7.28662 4.68121 6.84273 5.98112 6.70801 7.33398Z"
fill="#CCCCCC"
/>
</g>
<defs>
<clipPath id="clip0_5964_1685">
<rect width="16" height="16" fill="#CCCCCC" />
</clipPath>
</defs>
</svg>
<span class="flex-1 text-sm text-left transition-all duration-300">
{{ isChanging ? '언어 변경 중...' : getLanguageName(selectedLocale) }}
</span>
<!-- 로딩 스피너 -->
<svg
v-if="isChanging"
class="w-3 h-3 animate-spin"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="12"
cy="12"
r="10"
stroke="#CCCCCC"
stroke-width="2"
stroke-linecap="round"
stroke-dasharray="31.416"
stroke-dashoffset="31.416"
>
<animate
attributeName="stroke-dasharray"
dur="2s"
values="0 31.416;15.708 15.708;0 31.416"
repeatCount="indefinite"
/>
<animate
attributeName="stroke-dashoffset"
dur="2s"
values="0;-15.708;-31.416"
repeatCount="indefinite"
/>
</circle>
</svg>
<!-- 드롭다운 화살표 -->
<svg
v-else
class="w-3 h-3 text-gray-300 transition-transform duration-200"
:class="{ 'rotate-180': !isDropdownOpen }"
viewBox="0 0 12 12"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.69999 4.285L9.59499 7.125C9.91999 7.445 9.69499 8 9.23499 8H2.75999C2.29999 8 2.07499 7.445 2.39999 7.125L5.29499 4.285C5.68499 3.905 6.30499 3.905 6.69499 4.285H6.69999Z"
fill="#EBEBEB"
/>
</svg>
</button>
<div
v-if="isDropdownOpen"
class="absolute bottom-full mb-1 left-0 w-full bg-[#292929] border border-[#595959] rounded-lg p-2.5 text-xs text-[#ccc] z-[5]"
>
<div
v-for="localeItem in availableLanguages"
:key="localeItem.code"
class="dropdown-menu-item"
>
<button
class="flex items-center w-full h-10 gap-2 px-3 text-left bg-transparent border-none cursor-pointer transition-colors duration-200"
:class="{
'text-[#fc4420] font-medium': localeItem.code === selectedLocale,
'text-[#ccc]': localeItem.code !== selectedLocale,
}"
@click="selectLanguage(localeItem.code)"
>
<svg
width="15"
height="11"
viewBox="0 0 15 11"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="transition-opacity duration-200"
:class="{
'opacity-100': localeItem.code === selectedLocale,
'opacity-0': localeItem.code !== selectedLocale,
}"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M14.6339 0.366117C15.1221 0.854272 15.1221 1.64573 14.6339 2.13388L6.30057 10.4672C6.05437 10.7134 5.71664 10.8458 5.36872 10.8324C5.0208 10.8191 4.69421 10.6612 4.46762 10.3968L0.300952 5.53571C-0.148326 5.01155 -0.0876239 4.22243 0.436533 3.77315C0.960691 3.32387 1.74982 3.38458 2.19909 3.90873L5.48729 7.74496L12.8661 0.366117C13.3543 -0.122039 14.1458 -0.122039 14.6339 0.366117Z"
fill="#FC4420"
/>
</svg>
<span class="text-sm">{{ localeItem.name }}</span>
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
interface LanguageOrder {
languageOrder?: any[]
}
const props = defineProps<LanguageOrder>()
const runtimeConfig = useRuntimeConfig()
const baseDomain = `${runtimeConfig.public.baseDomain}`
const gameDataStore = useGameDataStore()
const { gameData } = storeToRefs(gameDataStore)
// 사용 가능한 언어 목록
const availableLanguages = computed(() => {
const langCodes = gameData.value?.lang_codes || []
const allLanguages =
gameData.value?.globals?.map((item: any) => ({
code: item.lang_json?.code,
name: item.lang_json?.name,
})) || []
// lang_codes에 포함된 언어만 필터링
const filteredLanguages = allLanguages.filter(lang =>
langCodes.includes(lang.code)
)
if (filteredLanguages.length === 0) {
return [{ code: 'ko', name: '한국어' }]
}
//languageOrder 값이 있는 경우 정렬, 없는 경우 기본 순서 유지
const defaultLanguageOrder = ['ko', 'en', 'ja', 'zh-cn', 'zh-tw', 'th']
const languageOrder = props.languageOrder || defaultLanguageOrder
// 정렬: 우선순위 언어 먼저, 그 다음 나머지
const sortedLanguages = filteredLanguages.sort((a, b) => {
const indexA = languageOrder.indexOf(a.code)
const indexB = languageOrder.indexOf(b.code)
// 둘 다 우선순위 목록에 있는 경우
if (indexA !== -1 && indexB !== -1) {
return indexA - indexB
}
// a만 우선순위 목록에 있는 경우
if (indexA !== -1) {
return -1
}
// b만 우선순위 목록에 있는 경우
if (indexB !== -1) {
return 1
}
// 둘 다 우선순위 목록에 없는 경우 원래 순서 유지
return 0
})
return sortedLanguages
})
// 언어 코드를 이름으로 변환하는 함수
const getLanguageName = (localeCode: string) => {
const language = availableLanguages.value.find(
lang => lang.code === localeCode
)
return language?.name || localeCode
}
const { locale, setLocale } = useI18n()
const switchLocalePath = useSwitchLocalePath()
const { loadGameData } = useGameDataLoader()
const selectedLocale = ref(locale.value)
const isChanging = ref(false)
const isDropdownOpen = ref(false)
// 드롭다운 토글 함수
const toggleDropdown = () => {
if (!isChanging.value) {
isDropdownOpen.value = !isDropdownOpen.value
}
}
// 언어 선택 함수
const selectLanguage = async (localeCode: string) => {
if (localeCode === selectedLocale.value || isChanging.value) {
isDropdownOpen.value = false
return
}
selectedLocale.value = localeCode as any
isDropdownOpen.value = false
await switchLanguage()
}
// 언어 변경 함수 (서버 미드웨어를 통한 gameData 갱신)
const switchLanguage = async () => {
if (!selectedLocale.value || isChanging.value) return
isChanging.value = true
try {
// URL 경로를 통해 언어 변경
const path = switchLocalePath(selectedLocale.value as any)
if (path) {
// 언어 쿠키 설정 (클라이언트 사이드) - 페이지 이동 전에 설정
const localeCookie = useCookie('LOCALE', {
domain: baseDomain,
path: '/',
sameSite: 'lax',
})
localeCookie.value = selectedLocale.value.toLowerCase()
// gameData 즉시 갱신 (1순위)
await loadGameData(selectedLocale.value)
// i18n locale 변경 (SPA)
await setLocale(selectedLocale.value as any)
// Nuxt SPA 라우팅으로 페이지 이동
await navigateTo(path)
}
} catch {
// 오류 발생 시 이전 언어로 복원
selectedLocale.value = locale.value
} finally {
isChanging.value = false
}
}
// locale이 변경될 때 selectedLocale도 동기화
watch(locale, newLocale => {
selectedLocale.value = newLocale
})
</script>
<style scoped>
/* 페이지 전환 애니메이션 */
.page-enter-active,
.page-leave-active {
transition: opacity 0.2s ease;
}
.page-enter-from,
.page-leave-to {
opacity: 0;
}
</style>