fix. gnb 작업 1차

This commit is contained in:
clkim
2025-10-02 17:34:41 +09:00
parent a70b3c8795
commit 1da6227c46
18 changed files with 413 additions and 448 deletions

View File

@@ -1,107 +0,0 @@
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

@@ -1,94 +0,0 @@
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();
});

View File

@@ -1,64 +0,0 @@
import { getHeader, defineEventHandler } from 'h3'
import { useRuntimeConfig } from 'nuxt/app'
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.error('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

@@ -1,40 +0,0 @@
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 });
}
});

View File

@@ -4,22 +4,22 @@
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 runtimeConfig = useRuntimeConfig()
const baseType = runtimeConfig.public.baseType
const translationItems = runtimeConfig.public.translationItems
const translationItemsArr = translationItems.split(',')
const staticUrl = config.public.staticUrl
const staticUrl = runtimeConfig.public.staticUrl
const translationApi = translationItemsArr.map((item: string): string => {
return `${staticUrl}/${baseType}/tmp/${item}.json`
})
// API 데이터 가져오기
const fetchDataPromises = translationApi.map((apiUrl) => {
const fetchDataPromises = translationApi.map(apiUrl => {
return useFetch(apiUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
'Content-Type': 'application/json;charset=UTF-8',
},
})
})
@@ -27,7 +27,7 @@ export default defineI18nLocale(async (locale: string) => {
const fetchResults = await Promise.all(fetchDataPromises)
// 각 결과에서 locale에 맞는 데이터를 추출
const apiData = fetchResults.map((result) => {
const apiData = fetchResults.map(result => {
return result.data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환
})
@@ -38,7 +38,7 @@ export default defineI18nLocale(async (locale: string) => {
// common.json에서 해당 locale의 데이터를 가져와서 병합
const commonLocaleData = commonData[locale] || {}
// API 데이터와 common.json 데이터를 병합 (common.json이 우선순위)
const finalResult = { ...mergedResult, ...commonLocaleData }

View File

@@ -4,22 +4,22 @@
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 runtimeConfig = useRuntimeConfig()
const baseType = runtimeConfig.public.baseType
const translationItems = runtimeConfig.public.translationItems
const translationItemsArr = translationItems.split(',')
const staticUrl = config.public.staticUrl
const staticUrl = runtimeConfig.public.staticUrl
const translationApi = translationItemsArr.map((item: string): string => {
return `${staticUrl}/${baseType}/tmp/${item}.json`
})
// API 데이터 가져오기
const fetchDataPromises = translationApi.map((apiUrl) => {
const fetchDataPromises = translationApi.map(apiUrl => {
return useFetch(apiUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
'Content-Type': 'application/json;charset=UTF-8',
},
})
})
@@ -27,7 +27,7 @@ export default defineI18nLocale(async (locale: string) => {
const fetchResults = await Promise.all(fetchDataPromises)
// 각 결과에서 locale에 맞는 데이터를 추출
const apiData = fetchResults.map((result) => {
const apiData = fetchResults.map(result => {
return result.data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환
})
@@ -38,7 +38,7 @@ export default defineI18nLocale(async (locale: string) => {
// common.json에서 해당 locale의 데이터를 가져와서 병합
const commonLocaleData = commonData[locale] || {}
// API 데이터와 common.json 데이터를 병합 (common.json이 우선순위)
const finalResult = { ...mergedResult, ...commonLocaleData }

View File

@@ -4,22 +4,22 @@
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 runtimeConfig = useRuntimeConfig()
const baseType = runtimeConfig.public.baseType
const translationItems = runtimeConfig.public.translationItems
const translationItemsArr = translationItems.split(',')
const staticUrl = config.public.staticUrl
const staticUrl = runtimeConfig.public.staticUrl
const translationApi = translationItemsArr.map((item: string): string => {
return `${staticUrl}/${baseType}/tmp/${item}.json`
})
// API 데이터 가져오기
const fetchDataPromises = translationApi.map((apiUrl) => {
const fetchDataPromises = translationApi.map(apiUrl => {
return useFetch(apiUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
'Content-Type': 'application/json;charset=UTF-8',
},
})
})
@@ -27,7 +27,7 @@ export default defineI18nLocale(async (locale: string) => {
const fetchResults = await Promise.all(fetchDataPromises)
// 각 결과에서 locale에 맞는 데이터를 추출
const apiData = fetchResults.map((result) => {
const apiData = fetchResults.map(result => {
return result.data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환
})
@@ -38,7 +38,7 @@ export default defineI18nLocale(async (locale: string) => {
// common.json에서 해당 locale의 데이터를 가져와서 병합
const commonLocaleData = commonData[locale] || {}
// API 데이터와 common.json 데이터를 병합 (common.json이 우선순위)
const finalResult = { ...mergedResult, ...commonLocaleData }

View File

@@ -4,22 +4,22 @@
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 runtimeConfig = useRuntimeConfig()
const baseType = runtimeConfig.public.baseType
const translationItems = runtimeConfig.public.translationItems
const translationItemsArr = translationItems.split(',')
const staticUrl = config.public.staticUrl
const staticUrl = runtimeConfig.public.staticUrl
const translationApi = translationItemsArr.map((item: string): string => {
return `${staticUrl}/${baseType}/tmp/${item}.json`
})
// API 데이터 가져오기
const fetchDataPromises = translationApi.map((apiUrl) => {
const fetchDataPromises = translationApi.map(apiUrl => {
return useFetch(apiUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
'Content-Type': 'application/json;charset=UTF-8',
},
})
})
@@ -27,7 +27,7 @@ export default defineI18nLocale(async (locale: string) => {
const fetchResults = await Promise.all(fetchDataPromises)
// 각 결과에서 locale에 맞는 데이터를 추출
const apiData = fetchResults.map((result) => {
const apiData = fetchResults.map(result => {
return result.data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환
})
@@ -38,7 +38,7 @@ export default defineI18nLocale(async (locale: string) => {
// common.json에서 해당 locale의 데이터를 가져와서 병합
const commonLocaleData = commonData[locale] || {}
// API 데이터와 common.json 데이터를 병합 (common.json이 우선순위)
const finalResult = { ...mergedResult, ...commonLocaleData }

View File

@@ -1,8 +1,10 @@
<script setup lang="ts">
import type { ClassType } from '#layers/types/Common'
interface Props {
to: string
target?: string
class?: string
class?: ClassType
}
const props = withDefaults(defineProps<Props>(), {

View File

@@ -17,11 +17,12 @@ const _breakpoints = useResponsiveBreakpointsReliable()
const { isPassedStoveGnb } = storeToRefs(scrollStore)
const gameData = gameDataStore.gameData as GameDataValue
const gnbList = (gameData?.gnb?.menus ?? {}) as GameDataMenuChildren
const isMenuOpen = ref(false)
const navAreaRef = ref<HTMLElement>()
const startRef = ref<HTMLElement>()
const gameData = gameDataStore.gameData as GameDataValue
const gnbData = gameData?.gnb
const isMenuOpen = ref(false)
const navWidth = ref(0)
const startWidth = ref(0)
const officialItemWidths = ref<number[]>([])
@@ -71,7 +72,8 @@ const calculateNavWidth = () => {
if (!navAreaRef.value) return 0
const navAreaWidth = navAreaRef.value.offsetWidth
return navAreaWidth
const moreWidth = 72
return navAreaWidth + moreWidth
}
// startRef의 넓이를 구하는 함수
@@ -79,7 +81,8 @@ const calculateStartWidth = () => {
if (!startRef.value) return 0
const startWidth = startRef.value.offsetWidth
return startWidth + 40
const headerPadding = 40
return startWidth + headerPadding
}
// official 자식들의 넓이를 구하는 함수 (뒤에서부터 순서대로)
@@ -103,8 +106,6 @@ const calculateOfficialItemWidths = () => {
calculateOverflow()
}
console.log(0, Object.keys(gnbList).length)
// 오버플로우 계산 함수
const calculateOverflow = () => {
if (!navAreaRef.value) return
@@ -112,18 +113,9 @@ const calculateOverflow = () => {
const totalNavWidth = navWidth.value + startWidth.value
const screenWidth = width.value
console.log('calculateOverflow called:', {
screenWidth,
totalNavWidth,
navWidth: navWidth.value,
startWidth: startWidth.value,
officialItemWidths: officialItemWidths.value,
})
// 모바일(1024px 미만)에서는 overflowNam을 0으로 설정
if (screenWidth < 1024) {
overflowNam.value = 0
console.log('Mobile view - overflowNam set to 0')
return
}
@@ -132,12 +124,10 @@ const calculateOverflow = () => {
let removedCount = 0
let currentTotal = totalNavWidth
// officialItemWidths를 하나씩 빼면서 해상도보다 작아지는지 확인
for (let i = 0; i < officialItemWidths.value.length; i++) {
currentTotal -= officialItemWidths.value[i]
removedCount++
// 해상도보다 작아지면 중단
if (currentTotal <= screenWidth) {
break
}
@@ -147,7 +137,6 @@ const calculateOverflow = () => {
console.log('Overflow calculated:', overflowNam.value)
} else {
overflowNam.value = 0
console.log('No overflow needed, setting to 0')
}
}
@@ -155,7 +144,6 @@ const calculateOverflow = () => {
onMounted(() => {
// 초기화
overflowNam.value = 0
console.log('onMounted - overflowNam 초기화:', overflowNam.value)
nextTick(() => {
if (navAreaRef.value && startRef.value) {
@@ -182,7 +170,7 @@ onClickOutside(navAreaRef, () => (isMenuOpen.value = false))
<div class="game-wrapper" :class="{ 'is-fixed': isPassedStoveGnb }">
<AtomsLocaleLink to="/brand" class="mx-auto md:hidden">
<img
:src="gameData?.gnb?.bi_path"
:src="gnbData?.bi_path"
:alt="gameData?.game_name"
class="h-[30px]"
/>
@@ -198,31 +186,35 @@ onClickOutside(navAreaRef, () => (isMenuOpen.value = false))
<div class="nav-logo">
<AtomsLocaleLink to="/brand">
<img
:src="gameData?.gnb?.bi_path"
:src="gnbData?.bi_path"
:alt="gameData?.game_name"
class="h-[30px]"
/>
</AtomsLocaleLink>
</div>
<nav class="nav-list">
<div v-if="gnbList" class="official">
<div v-if="gnbData?.menus" class="official">
<div
v-for="(gnbItem, key) in gnbList"
v-for="(gnbItem, key) in gnbData?.menus"
:key="key"
class="nav-item"
:class="{
'is-hidden':
overflowNam > 0 &&
Number(key) >= Object.keys(gnbList).length - overflowNam,
Number(key) >=
Object.keys(gnbData?.menus).length - overflowNam,
}"
>
<BlocksHybridLink
:to="gnbItem.url_path"
:target="gnbItem.link_target"
:class="`nav-1depth ${isNavItemActive(gnbItem) ? 'active' : ''}`"
:class="['nav-1depth', { active: isNavItemActive(gnbItem) }]"
>
<span>{{ gnbItem.menu_name }}</span>
<AtomsIconsArrowDown v-if="gnbItem.children" />
<AtomsIconsArrowDown
v-if="gnbItem.children"
class="hidden md:block"
/>
</BlocksHybridLink>
<div v-if="gnbItem.children" class="nav-2depth">
<ul>
@@ -244,44 +236,46 @@ onClickOutside(navAreaRef, () => (isMenuOpen.value = false))
</div>
</div>
</div>
<div v-if="gnbList && overflowNam > 0" class="more">
<div v-if="gnbData?.menus && overflowNam > 0" class="more">
<button class="btn-more">
<span class="sr-only">more</span>
</button>
<div class="more-list">
<div
v-for="(gnbItem, key) in gnbList"
:key="key"
:class="{
hidden:
Number(key) < Object.keys(gnbList).length - overflowNam,
}"
>
<BlocksHybridLink
:to="gnbItem.url_path"
:target="gnbItem.link_target"
:class="`${isNavItemActive(gnbItem) ? 'active' : ''}`"
<div class="list-inner">
<div
v-for="(gnbItem, key) in gnbData?.menus"
:key="key"
:class="{
hidden:
Number(key) <
Object.keys(gnbData?.menus).length - overflowNam,
}"
>
<span>{{ gnbItem.menu_name }}</span>
<AtomsIconsArrowDown v-if="gnbItem.children" />
</BlocksHybridLink>
<div v-if="gnbItem.children">
<ul>
<li
v-for="child in gnbItem.children"
:key="child.menu_name"
>
<BlocksHybridLink
:to="child.url_path"
:target="child.link_target"
<BlocksHybridLink
:to="gnbItem.url_path"
:target="gnbItem.link_target"
:class="`${isNavItemActive(gnbItem) ? 'active' : ''}`"
>
<span>{{ gnbItem.menu_name }}</span>
</BlocksHybridLink>
<div v-if="gnbItem.children">
<ul>
<li
v-for="child in gnbItem.children"
:key="child.menu_name"
>
<span>{{ child.menu_name }}</span>
<AtomsIconsLinkOut
v-if="child.link_target === '_blank'"
/>
</BlocksHybridLink>
</li>
</ul>
<BlocksHybridLink
:to="child.url_path"
:target="child.link_target"
>
<span>{{ child.menu_name }}</span>
<AtomsIconsLinkOut
v-if="child.link_target === '_blank'"
/>
</BlocksHybridLink>
</li>
</ul>
</div>
</div>
</div>
</div>
@@ -340,9 +334,6 @@ onClickOutside(navAreaRef, () => (isMenuOpen.value = false))
.btn-start {
@apply relative mt-2 px-5 md:absolute md:right-0 md:mt-0 md:px-0;
}
.btn-more {
@apply w-[40px] h-[40px] bg-[red];
}
.gnb-game {
@apply absolute top-0 left-0 w-0 md:relative md:w-full md:!h-full;
@@ -355,12 +346,12 @@ onClickOutside(navAreaRef, () => (isMenuOpen.value = false))
@apply content-[''] absolute inset-0 w-[100vw] h-full bg-[rgba(0,0,0,0.6)] md:hidden;
}
.gnb-game.is-open .nav-area {
@apply h-full translate-x-0 transition-transform duration-300 md:translate-x-0;
@apply h-full translate-x-0 transition-transform duration-300 md:transform-none;
}
.nav-area {
@apply flex flex-col w-[360px] bg-theme-foreground-10 translate-x-[-100%]
md:inline-flex md:flex-row md:w-auto md:h-full md:pl-[40px] md:items-center md:bg-transparent transform-none;
md:inline-flex md:flex-row md:w-auto md:h-full md:pl-[40px] md:items-center md:bg-transparent md:transform-none;
}
.nav-logo {
@@ -369,7 +360,7 @@ onClickOutside(navAreaRef, () => (isMenuOpen.value = false))
.nav-list {
@apply overflow-hidden flex flex-col order-1 h-full mt-2 mb-4 px-2
md:flex-row md:order-none md:h-full md:my-0 md:mx-10 md:px-0 md:overflow-visible;
md:flex-row md:order-none md:h-full md:my-0 md:ml-10 md:mr-6 md:px-0 md:overflow-visible;
}
.nav-item {
@@ -400,15 +391,15 @@ onClickOutside(navAreaRef, () => (isMenuOpen.value = false))
}
.nav-2depth {
@apply block text-[15px] md:hidden md:absolute md:top-[64px] md:left-[-28px] md:min-w-[190px] md:pt-1 md:z-50;
@apply text-[15px] md:hidden md:absolute md:top-[64px] md:left-[-28px] md:pt-1;
}
.nav-2depth ul {
@apply bg-theme-foreground-10 rounded-[20px] md:shadow-lg md:p-3;
@apply bg-theme-foreground-10 rounded-[20px] md:min-w-[190px] md:p-3 md:shadow-lg;
}
.nav-2depth a {
@apply flex items-center gap-1 px-5 py-[9px] rounded-[12px] transition-colors
hover:bg-theme-foreground-reversal-4 active:bg-theme-foreground-reversal-10
md:px-4 md:py-[11px];
@apply flex items-center gap-1 py-[9px] px-5 rounded-[12px] transition-colors
md:py-[11px] md:px-4
hover:bg-theme-foreground-reversal-4 active:bg-theme-foreground-reversal-10;
}
.official {
@@ -416,14 +407,30 @@ onClickOutside(navAreaRef, () => (isMenuOpen.value = false))
}
.more {
@apply relative hidden md:block;
@apply relative hidden ml-[32px] pt-[11px] md:block;
}
.more:hover .more-list {
@apply md:block;
}
.btn-more {
@apply w-[40px] h-[40px] rounded-[12px] bg-theme-foreground-reversal-6 hover:bg-theme-foreground-reversal-10 active:bg-theme-foreground-reversal-4;
}
.more-list {
@apply absolute;
@apply hidden absolute top-[64px] left-[-20px] pt-1;
}
.list-inner {
@apply min-w-[190px] p-3 rounded-[20px] bg-theme-foreground-10 shadow-lg;
}
.more-list a {
@apply flex items-center gap-1 py-[10px] px-4 rounded-[12px] transition-colors
hover:bg-theme-foreground-reversal-4 active:bg-theme-foreground-reversal-10;
}
.more-list li a {
@apply px-6;
}
.event {
@apply ml-[100px];
@apply relative md:ml-[64px] md:after:content-[''] md:after:absolute md:after:top-[50%] md:after:left-[-32px] md:after:w-[1px] md:after:h-[16px] md:after:bg-theme-foreground-reversal-30 md:after:translate-y-[-50%];
}
.is-hidden {

View File

@@ -1,7 +1,10 @@
import * as amplitude from '@amplitude/analytics-browser'
import type { AnalyticsDetailType, AnalyticsLogDataTracking } from '../types/AnalyticsType'
import type {
AnalyticsDetailType,
AnalyticsLogDataTracking,
} from '../types/AnalyticsType'
import type { PageDataResourceGroup } from '#layers/types/api/pageData'
import type { IdentityInfo, ActionInfo, MarketingInfo } from '../types/Stove81Plug'
import type { IdentityInfo, ActionInfo, MarketingInfo } from '../types/Stove'
declare const svcLog: any
declare const twq: any
@@ -24,32 +27,37 @@ export const useAnalyticsLogData = (
return ref({} as AnalyticsDetailType)
}
const pageDataTrack = (typeof resourcesData.tracking === 'object' ? resourcesData.tracking : {}) as AnalyticsLogDataTracking
console.log("🚀 ~ useAnalyticsLogData ~ pageDataTrack:", pageData)
const pageDataTrack = (
typeof resourcesData.tracking === 'object' ? resourcesData.tracking : {}
) as AnalyticsLogDataTracking
console.log('🚀 ~ useAnalyticsLogData ~ pageDataTrack:', pageData)
const logData = ref({
actionType: pageDataTrack?.action_type,
// logSourceType:pageDataTrack.logSourceType,
// viewArea:pageDataTrack.viewArea,
// viewType:pageDataTrack.viewType,
clickArea:pageData.page_name_en,
clickArea: pageData.page_name_en,
clickSarea: pageData.templates[pageVerTmplSeq].page_ver_tmpl_name_en,
clickItem: `${pageData.templates[pageVerTmplSeq].page_ver_tmpl_name}_${pageDataTrack?.click_item}`,
event: pageData.page_name,
eventCategory: `${pageData.page_name}_${pageDataTrack?.click_item}`,
template_code: pageData.templates[pageVerTmplSeq].template_code,
page_ver_tmpl_name: pageData.templates[pageVerTmplSeq].page_ver_tmpl_name,
page_ver_tmpl_name_en: pageData.templates[pageVerTmplSeq].page_ver_tmpl_name_en,
page_ver_tmpl_name_en:
pageData.templates[pageVerTmplSeq].page_ver_tmpl_name_en,
} as unknown as AnalyticsDetailType)
return logData
}
// target에 {XX1, XX2}와 같은 형태가 포함되어 있을 경우 options.clickItem으로부터 값 추출하여 세팅
const findValueFromOption = (target: string, { options = {} }: any) => {
if (target.includes('{') && target.includes('}')) {
const strTargetClickItem = target.substring(target.indexOf('{') + 1, target.indexOf('}'))
const strTargetClickItem = target.substring(
target.indexOf('{') + 1,
target.indexOf('}')
)
const arrTargetClickItem = strTargetClickItem.split(',')
const arrTargetClickItemValue = []
@@ -58,7 +66,10 @@ const findValueFromOption = (target: string, { options = {} }: any) => {
targetClickItem = targetClickItem.trim()
arrTargetClickItemValue.push(options.clickItem[targetClickItem])
}
target = target.replaceAll(`{${strTargetClickItem}}`, arrTargetClickItemValue.join(','))
target = target.replaceAll(
`{${strTargetClickItem}}`,
arrTargetClickItemValue.join(',')
)
}
return target
}
@@ -70,7 +81,7 @@ const findValueFromOption = (target: string, { options = {} }: any) => {
* @param {object} options
*/
const sendGA = (analytics: AnalyticsDetailType, { options = {} }: any) => {
console.log("🚀 ~ 1111 sendGA ~ analytics:", analytics)
console.log('🚀 ~ 1111 sendGA ~ analytics:', analytics)
try {
const { gtag } = useGtag()
@@ -84,7 +95,7 @@ const sendGA = (analytics: AnalyticsDetailType, { options = {} }: any) => {
gtag('set', 'cookie_domain', `${window?.location?.hostname || ''}`) // env 값으로 설정 시 쿠키 생성 안 돼서 window.location.hostname으로 설정
gtag('set', 'cookie_expires', '0') // 0으로 설정 시 쿠키가 Session 기반 쿠키로 전환
gtag('event', `${eventName}`, {
event_category: eventLabel
event_category: eventLabel,
})
} catch (e) {
console.error('[Exception] useAnalytics.sendGA: ', e)
@@ -98,7 +109,10 @@ const sendGA = (analytics: AnalyticsDetailType, { options = {} }: any) => {
* @param {string} mcode
* @param {object} options
*/
const sendSA = (analytics: AnalyticsDetailType, { mcode = '', options = {} }: any) => {
const sendSA = (
analytics: AnalyticsDetailType,
{ mcode = '', options = {} }: any
) => {
const gameDataStore = useGameDataStore()
const { gameData } = storeToRefs(gameDataStore)
@@ -116,7 +130,9 @@ const sendSA = (analytics: AnalyticsDetailType, { mcode = '', options = {} }: an
const viewArea = analytics.viewArea || ''
const viewType = analytics.viewType || ''
const clickArea = analytics.clickArea || ''
const clickSarea = findValueFromOption(analytics.clickSarea || '', { options })
const clickSarea = findValueFromOption(analytics.clickSarea || '', {
options,
})
const eventLocale = analytics.eventLocale || ''
const identityInfo: IdentityInfo = {
@@ -126,14 +142,14 @@ const sendSA = (analytics: AnalyticsDetailType, { mcode = '', options = {} }: an
locale: eventLocale,
lang_cd: eventLocale,
member_no: memberNo,
channeling_cd: 'SO'
channeling_cd: 'SO',
}
const marketingInfo: MarketingInfo = {
marketing_code: mcode || '',
device_type: deviceType,
media_type: '',
media_page: ''
media_page: '',
}
let actionParam = {}
@@ -145,8 +161,8 @@ const sendSA = (analytics: AnalyticsDetailType, { mcode = '', options = {} }: an
view_info: {
game_no: gameNo,
lang_cd: eventLocale,
...options?.viewInfo
}
...options?.viewInfo,
},
}
} else if (actionType === 'click') {
actionParam = {
@@ -156,21 +172,21 @@ const sendSA = (analytics: AnalyticsDetailType, { mcode = '', options = {} }: an
click_item: analytics.clickItem,
game_no: gameNo,
lang_cd: eventLocale,
...options?.clickItem
}
...options?.clickItem,
},
}
}
const actionInfo: ActionInfo = {
action_type: actionType,
action_param: actionParam,
marketing_info: marketingInfo
marketing_info: marketingInfo,
}
const amplitudeActionInfo = {
...actionInfo,
url: `${location?.href || ''}`,
agent: `${navigator?.userAgent || ''}`
agent: `${navigator?.userAgent || ''}`,
}
const amplitudeActionParams: {
@@ -178,7 +194,7 @@ const sendSA = (analytics: AnalyticsDetailType, { mcode = '', options = {} }: an
event_properties: ActionInfo & { url: string; agent: string }
} = {
event_type: actionType,
event_properties: amplitudeActionInfo
event_properties: amplitudeActionInfo,
}
svcLog.identity(identityInfo)
@@ -196,9 +212,8 @@ const sendSA = (analytics: AnalyticsDetailType, { mcode = '', options = {} }: an
* @param {AnalyticsDetailType} analytics
*/
const sendLog = (locale: string, analytics: AnalyticsDetailType) => {
console.log("🚀 ~ sendLog ~ analytics:", analytics)
console.log('🚀 ~ sendLog ~ analytics:', analytics)
// 언어 코드 대문자 변환
analytics.eventLocale = locale.toUpperCase()
@@ -207,7 +222,7 @@ console.log("🚀 ~ sendLog ~ analytics:", analytics)
sendGA(analytics, { options: analytics.options })
// SA 전송 : actionType, logSourceType 유무로 판별
if (
analytics.actionType &&
analytics.actionType &&
analytics.actionType !== ''
// analytics.logSourceType &&
// analytics.logSourceType !== ''
@@ -291,7 +306,7 @@ const sendMarketingScript = ({
gaEventName,
fbEventName,
twEventName,
ttEventName
ttEventName,
}: {
gaEventName?: string
fbEventName?: string

View File

@@ -0,0 +1,191 @@
import { useDebounceFn } from '@vueuse/core'
import { useTokenValidation } from '#layers/composables/useTokenValidation'
import { csrGoStoveLogin } from '#layers/utils/stoveUtil'
export const useCheckGameStart = () => {
// const { tm } = useI18n()
const modalStore = useModalStore()
const runtimeConfig = useRuntimeConfig()
const disabledDoubleClick = ref(false) // 연속 호출 클릭 방지
const isCheckLauncher = ref(false) // 런처 실행 로딩 상태
const isShowDownloadLauncher = ref(false) // 런처 다운로드 표시
const customerService = { title: '확인', link: 'https://www.google.com' } //[TODO]
// 로그인 모달 표시
const showLoginModal = () => {
modalStore.handleOpenAlert({
contentText: '로그인',
confirmButtonText: '스토브 로그인',
className: 'modal-login',
confirmButtonEvent: () => {
modalStore.handleResetAlert()
csrGoStoveLogin()
},
closeButtonEvent: () => {
modalStore.handleResetAlert()
disabledDoubleClick.value = false
},
})
}
// 모든 런처 버튼 비활성화
const setLauncherButtonDisabled = (disabled: boolean) => {
const launcherButton = document.querySelectorAll(
'#btn-launcher'
) as NodeListOf<HTMLButtonElement>
launcherButton.forEach(button => {
button.disabled = disabled
})
}
// 런처 실행 로딩 시작 UI 처리
const startLoadingForLauncher = () => {
if (import.meta.client) {
setLauncherButtonDisabled(true)
isCheckLauncher.value = true
setTimeout(() => {
if (isCheckLauncher.value) {
isShowDownloadLauncher.value = true
}
}, 5000)
}
}
// 런처 실행 로딩 종료 UI 처리
const stopLoadingForLauncher = () => {
if (import.meta.client) {
setLauncherButtonDisabled(false)
isCheckLauncher.value = false
isShowDownloadLauncher.value = false
}
}
// 런처 호출
const runLauncher = async () => {
// 클라이언트에서만 실행
if (!import.meta.client) return
const gameDataStore = useGameDataStore()
const stoveGameId = gameDataStore.gameData?.game_id || ''
const accessTokenSub = useCookie('SUAT')
const nationCookie = useCookie('NNTO').value
const localeCookie = useCookie('LOCALE').value
const isAgent = true
disabledDoubleClick.value = true
window.stoveJsService = window.stoveJsService || {}
// 토큰 유효성 체크
const { validateToken } = useTokenValidation()
const validateTokenResult = await validateToken(accessTokenSub.value || '')
if (validateTokenResult) {
startLoadingForLauncher()
window.stoveJsService.launcher
.run({
gameId: stoveGameId,
nation: nationCookie,
lang: localeCookie,
isSkipMaintenance: isAgent,
})
.then(() => {
// 런처 실행 성공 시 처리
stopLoadingForLauncher()
})
.catch((error: any) => {
// 런처 실행 실패시 처리
if (error.code !== 601) {
stopLoadingForLauncher()
}
errorHandler(error.code)
})
.finally(() => {
disabledDoubleClick.value = false
})
} else {
showLoginModal()
}
}
// PC 클라이언트 설치 전 (에러 처리)
const errorHandler = (errorCode: number) => {
switch (errorCode) {
case 601: // PC 클라이언트 미설치
break
case 40101: // 로그인 정보 확인 중 오류가 발생했습니다. 재로그인 후 다시 이용해 주세요.
modalStore.handleOpenAlert({
contentText:
'로그인 정보 확인 중 오류가 발생했습니다. 재로그인 후 다시 이용해 주세요.',
confirmButtonText: '스토브 로그인',
className: 'modal-login',
confirmButtonEvent: () => {
modalStore.handleResetAlert()
csrGoStoveLogin()
},
})
break
case 40103: // 로그인 정보가 만료되었습니다. 재로그인 후 다시 이용해 주세요.
modalStore.handleOpenAlert({
contentText:
'로그인 정보가 만료되었습니다. 재로그인 후 다시 이용해 주세요.',
confirmButtonText: '스토브 로그인',
className: 'modal-login',
confirmButtonEvent: () => {
modalStore.handleResetAlert()
csrGoStoveLogin()
},
})
break
case 602:
case 504:
case 70051:
case 500000:
case 701:
case 70052:
default:
// 일시적으로 오류가 발생했습니다. 잠시 후 다시 이용해 주세요. 동일한 현상이 계속 발생할 경우 고객센터로 문의해 주세요.
modalStore.handleOpenConfirm({
contentText:
'일시적으로 오류가 발생했습니다. 잠시 후 다시 이용해 주세요. 동일한 현상이 계속 발생할 경우 고객센터로 문의해 주세요.',
confirmButtonText: customerService.title,
cancelButtonText: '취소',
confirmButtonEvent: () => {
modalStore.handleResetConfirm()
window.open(customerService.link, '_blank')
},
})
break
}
}
// 디바운스 설정
const debounceHandler = useDebounceFn(runLauncher, 500)
// 런처 상태 검사
const validateLauncher = () => {
if (!disabledDoubleClick.value) {
debounceHandler()
}
}
// 런처 다운로드 함수
const downloadLauncher = () => {
const stoveClientDownloadUrl = runtimeConfig.public.stoveClientDownloadUrl
location.href = stoveClientDownloadUrl
disabledDoubleClick.value = false
}
return {
disabledDoubleClick, // 연속 클릭 방지
isCheckLauncher, // 런처 실행 로딩 상태
isShowDownloadLauncher, // 런처 다운로드 표시
validateLauncher, // 런처 검사 함수
downloadLauncher, // 런처 실행 함수
stopLoadingForLauncher, // 런처 실행 로딩 종료 함수
}
}

View File

@@ -0,0 +1,30 @@
export function useTokenValidation() {
const runtimeConfig = useRuntimeConfig()
const apiBaseUrl = `${runtimeConfig.public.stoveApiUrl}`
const isTokenValid = ref(false)
const validateToken = async (token: string) => {
try {
const result = await $fetch<{ code: number }>(
`${apiBaseUrl}/auth/v5/user_token/check`,
{
method: 'GET',
headers: {
Authorization: `bearer ${token}`,
'Content-Type': 'application/json;charset=UTF-8',
},
}
)
isTokenValid.value = result.code === 0
return isTokenValid.value
} catch (error) {
isTokenValid.value = false
return false
}
}
return {
isTokenValid,
validateToken,
}
}

View File

@@ -17,7 +17,6 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
try {
const pageUrl = getPathAfterLanguage(to.path)
console.log('🚀 ~ pageUrl:', pageUrl)
// pageUrl이 빈값이거나 null이면 /brand로 리다이렉트
if (!pageUrl || pageUrl === '' || pageUrl === '/') {

10
layers/types/Common.ts Normal file
View File

@@ -0,0 +1,10 @@
import type { HTMLAttributes } from 'vue'
import type { StoveJsService } from '@/layers/types/Stove'
export type ClassType = HTMLAttributes['class']
declare global {
interface Window {
stoveJsService?: StoveJsService
}
}

View File

@@ -1,3 +1,6 @@
/**
* Stove 81 Plug
*/
export interface IdentityInfo {
app_id: string
log_source_type: string
@@ -36,3 +39,10 @@ export interface ActionInfo {
action_param: ActionParam
marketing_info?: MarketingInfo
}
/**
* Stove JS Service
*/
export interface StoveJsService {
launcher?: any
}

View File

@@ -55,9 +55,14 @@ const buildRequestOptions = (
url: string,
options: FetchOptions
): FetchRequestOptions => {
const runtimeConfig = useRuntimeConfig()
const callerId = runtimeConfig.public.stoveGameId || ''
const callerDetail = useCookie('sgs_da_uuid').value || ''
let stoveGameId = ''
let callerDetail = ''
if (import.meta.client) {
const gameDataStore = useGameDataStore()
stoveGameId = gameDataStore.gameData?.game_id || ''
callerDetail = useCookie('sgs_da_uuid').value || ''
}
const requestOptions: FetchRequestOptions = {
method,
@@ -75,7 +80,7 @@ const buildRequestOptions = (
if (url.includes('.onstove.com') || url.includes('.gate8.com')) {
requestOptions.headers = {
...requestOptions.headers,
'Caller-Id': callerId as string,
'Caller-Id': stoveGameId as string,
'Caller-Detail': callerDetail as string,
}
}

View File

@@ -10,10 +10,11 @@ import { csrFormatJWT } from '#layers/utils/formatUtil'
*/
export const csrGoStoveLogin = () => {
const runtimeConfig = useRuntimeConfig()
const gameDataStore = useGameDataStore()
const loginUrl = runtimeConfig.public.stoveLoginUrl
const stoveGameId = runtimeConfig.public.stoveGameId
const stoveGameNo = runtimeConfig.public.stoveGameNo
const stoveGameId = gameDataStore.gameData?.game_id
const stoveGameNo = gameDataStore.gameData?.game_code
const redirectUrl = encodeURIComponent(location.href)
const url = `${loginUrl}?redirect_url=${redirectUrl}&inflow_path=${stoveGameId}&game_no=${stoveGameNo}`