Merge branch 'feature/20250910-all' into feature/20251001-gil
This commit is contained in:
14
.vscode/settings.json
vendored
Normal file
14
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"css.validate": false,
|
||||||
|
"tailwindCSS.includeLanguages": {
|
||||||
|
"vue": "html",
|
||||||
|
"css": "css"
|
||||||
|
},
|
||||||
|
"tailwindCSS.experimental.classRegex": [
|
||||||
|
["class[:]\\s*['\"`]([^'\"`]*)['\"`]", "([^'\"`]*)"],
|
||||||
|
["@apply\\s+([^;]+)", "([^;]+)"]
|
||||||
|
],
|
||||||
|
"files.associations": {
|
||||||
|
"*.css": "tailwindcss"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@ const getGameDataFromServer = (): GameDataValue | null => {
|
|||||||
const setupAllMetaData = (data: GameDataValue) => {
|
const setupAllMetaData = (data: GameDataValue) => {
|
||||||
const meta = data.meta_tag
|
const meta = data.meta_tag
|
||||||
const faviconPath = data.favicon_path
|
const faviconPath = data.favicon_path
|
||||||
const theme = data.design_theme === 1 ? 'dark' : 'light'
|
const theme = data.gnb.theme_type || 'dark'
|
||||||
|
|
||||||
// 파비콘 링크 생성
|
// 파비콘 링크 생성
|
||||||
const faviconLinks = [
|
const faviconLinks = [
|
||||||
|
|||||||
@@ -32,11 +32,11 @@ export default createConfigForNuxt({
|
|||||||
'vue/no-multiple-template-root': 'off',
|
'vue/no-multiple-template-root': 'off',
|
||||||
'vue/no-required-prop-with-default': 'off',
|
'vue/no-required-prop-with-default': 'off',
|
||||||
'vue/require-directive': 'off',
|
'vue/require-directive': 'off',
|
||||||
|
'vue/html-self-closing': 'off',
|
||||||
|
|
||||||
// 일반 규칙 (품질/버그 탐지)
|
// 일반 규칙 (품질/버그 탐지)
|
||||||
'no-console': 'warn',
|
'no-console': 'warn',
|
||||||
'no-debugger': 'error',
|
'no-debugger': 'error',
|
||||||
'no-unused-vars': 'off',
|
|
||||||
'prefer-const': 'warn',
|
'prefer-const': 'warn',
|
||||||
'no-var': 'error',
|
'no-var': 'error',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,22 +4,22 @@
|
|||||||
import commonData from '../../layers/assets/data/common.json'
|
import commonData from '../../layers/assets/data/common.json'
|
||||||
|
|
||||||
export default defineI18nLocale(async (locale: string) => {
|
export default defineI18nLocale(async (locale: string) => {
|
||||||
const config = useRuntimeConfig()
|
const runtimeConfig = useRuntimeConfig()
|
||||||
const baseType = config.public.baseType
|
const baseType = runtimeConfig.public.baseType
|
||||||
const translationItems = config.public.translationItems
|
const translationItems = runtimeConfig.public.translationItems
|
||||||
const translationItemsArr = translationItems.split(',')
|
const translationItemsArr = translationItems.split(',')
|
||||||
const staticUrl = config.public.staticUrl
|
const staticUrl = runtimeConfig.public.staticUrl
|
||||||
const translationApi = translationItemsArr.map((item: string): string => {
|
const translationApi = translationItemsArr.map((item: string): string => {
|
||||||
return `${staticUrl}/${baseType}/tmp/${item}.json`
|
return `${staticUrl}/${baseType}/tmp/${item}.json`
|
||||||
})
|
})
|
||||||
|
|
||||||
// API 데이터 가져오기
|
// API 데이터 가져오기
|
||||||
const fetchDataPromises = translationApi.map((apiUrl) => {
|
const fetchDataPromises = translationApi.map(apiUrl => {
|
||||||
return useFetch(apiUrl, {
|
return useFetch(apiUrl, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
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)
|
const fetchResults = await Promise.all(fetchDataPromises)
|
||||||
|
|
||||||
// 각 결과에서 locale에 맞는 데이터를 추출
|
// 각 결과에서 locale에 맞는 데이터를 추출
|
||||||
const apiData = fetchResults.map((result) => {
|
const apiData = fetchResults.map(result => {
|
||||||
return result.data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환
|
return result.data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -4,22 +4,22 @@
|
|||||||
import commonData from '../../layers/assets/data/common.json'
|
import commonData from '../../layers/assets/data/common.json'
|
||||||
|
|
||||||
export default defineI18nLocale(async (locale: string) => {
|
export default defineI18nLocale(async (locale: string) => {
|
||||||
const config = useRuntimeConfig()
|
const runtimeConfig = useRuntimeConfig()
|
||||||
const baseType = config.public.baseType
|
const baseType = runtimeConfig.public.baseType
|
||||||
const translationItems = config.public.translationItems
|
const translationItems = runtimeConfig.public.translationItems
|
||||||
const translationItemsArr = translationItems.split(',')
|
const translationItemsArr = translationItems.split(',')
|
||||||
const staticUrl = config.public.staticUrl
|
const staticUrl = runtimeConfig.public.staticUrl
|
||||||
const translationApi = translationItemsArr.map((item: string): string => {
|
const translationApi = translationItemsArr.map((item: string): string => {
|
||||||
return `${staticUrl}/${baseType}/tmp/${item}.json`
|
return `${staticUrl}/${baseType}/tmp/${item}.json`
|
||||||
})
|
})
|
||||||
|
|
||||||
// API 데이터 가져오기
|
// API 데이터 가져오기
|
||||||
const fetchDataPromises = translationApi.map((apiUrl) => {
|
const fetchDataPromises = translationApi.map(apiUrl => {
|
||||||
return useFetch(apiUrl, {
|
return useFetch(apiUrl, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
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)
|
const fetchResults = await Promise.all(fetchDataPromises)
|
||||||
|
|
||||||
// 각 결과에서 locale에 맞는 데이터를 추출
|
// 각 결과에서 locale에 맞는 데이터를 추출
|
||||||
const apiData = fetchResults.map((result) => {
|
const apiData = fetchResults.map(result => {
|
||||||
return result.data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환
|
return result.data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -4,22 +4,22 @@
|
|||||||
import commonData from '../../layers/assets/data/common.json'
|
import commonData from '../../layers/assets/data/common.json'
|
||||||
|
|
||||||
export default defineI18nLocale(async (locale: string) => {
|
export default defineI18nLocale(async (locale: string) => {
|
||||||
const config = useRuntimeConfig()
|
const runtimeConfig = useRuntimeConfig()
|
||||||
const baseType = config.public.baseType
|
const baseType = runtimeConfig.public.baseType
|
||||||
const translationItems = config.public.translationItems
|
const translationItems = runtimeConfig.public.translationItems
|
||||||
const translationItemsArr = translationItems.split(',')
|
const translationItemsArr = translationItems.split(',')
|
||||||
const staticUrl = config.public.staticUrl
|
const staticUrl = runtimeConfig.public.staticUrl
|
||||||
const translationApi = translationItemsArr.map((item: string): string => {
|
const translationApi = translationItemsArr.map((item: string): string => {
|
||||||
return `${staticUrl}/${baseType}/tmp/${item}.json`
|
return `${staticUrl}/${baseType}/tmp/${item}.json`
|
||||||
})
|
})
|
||||||
|
|
||||||
// API 데이터 가져오기
|
// API 데이터 가져오기
|
||||||
const fetchDataPromises = translationApi.map((apiUrl) => {
|
const fetchDataPromises = translationApi.map(apiUrl => {
|
||||||
return useFetch(apiUrl, {
|
return useFetch(apiUrl, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
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)
|
const fetchResults = await Promise.all(fetchDataPromises)
|
||||||
|
|
||||||
// 각 결과에서 locale에 맞는 데이터를 추출
|
// 각 결과에서 locale에 맞는 데이터를 추출
|
||||||
const apiData = fetchResults.map((result) => {
|
const apiData = fetchResults.map(result => {
|
||||||
return result.data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환
|
return result.data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -4,22 +4,22 @@
|
|||||||
import commonData from '../../layers/assets/data/common.json'
|
import commonData from '../../layers/assets/data/common.json'
|
||||||
|
|
||||||
export default defineI18nLocale(async (locale: string) => {
|
export default defineI18nLocale(async (locale: string) => {
|
||||||
const config = useRuntimeConfig()
|
const runtimeConfig = useRuntimeConfig()
|
||||||
const baseType = config.public.baseType
|
const baseType = runtimeConfig.public.baseType
|
||||||
const translationItems = config.public.translationItems
|
const translationItems = runtimeConfig.public.translationItems
|
||||||
const translationItemsArr = translationItems.split(',')
|
const translationItemsArr = translationItems.split(',')
|
||||||
const staticUrl = config.public.staticUrl
|
const staticUrl = runtimeConfig.public.staticUrl
|
||||||
const translationApi = translationItemsArr.map((item: string): string => {
|
const translationApi = translationItemsArr.map((item: string): string => {
|
||||||
return `${staticUrl}/${baseType}/tmp/${item}.json`
|
return `${staticUrl}/${baseType}/tmp/${item}.json`
|
||||||
})
|
})
|
||||||
|
|
||||||
// API 데이터 가져오기
|
// API 데이터 가져오기
|
||||||
const fetchDataPromises = translationApi.map((apiUrl) => {
|
const fetchDataPromises = translationApi.map(apiUrl => {
|
||||||
return useFetch(apiUrl, {
|
return useFetch(apiUrl, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
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)
|
const fetchResults = await Promise.all(fetchDataPromises)
|
||||||
|
|
||||||
// 각 결과에서 locale에 맞는 데이터를 추출
|
// 각 결과에서 locale에 맞는 데이터를 추출
|
||||||
const apiData = fetchResults.map((result) => {
|
const apiData = fetchResults.map(result => {
|
||||||
return result.data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환
|
return result.data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -11,4 +11,11 @@
|
|||||||
a {
|
a {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 라이트 테마 색상 */
|
||||||
|
[data-theme='light'] {
|
||||||
|
body {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,4 @@
|
|||||||
/* CSS 변수 정의 - @layer 밖에 위치 */
|
|
||||||
:root {
|
:root {
|
||||||
--foreground: #ffffff;
|
|
||||||
--foreground-10: #ffffff;
|
|
||||||
|
|
||||||
--foreground-reversal: #1f1f1f;
|
|
||||||
--foreground-reversal-4: rgba(0, 0, 0, 0.04);
|
|
||||||
--foreground-reversal-6: rgba(0, 0, 0, 0.06);
|
|
||||||
--foreground-reversal-8: rgba(0, 0, 0, 0.08);
|
|
||||||
--foreground-reversal-10: rgba(0, 0, 0, 0.1);
|
|
||||||
--foreground-reversal-15: rgba(0, 0, 0, 0.15);
|
|
||||||
--foreground-reversal-30: #ebebeb; /* gray-80 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 다크 테마 색상 */
|
|
||||||
[data-theme='dark'] {
|
|
||||||
--foreground: #191919;
|
--foreground: #191919;
|
||||||
--foreground-10: #292929;
|
--foreground-10: #292929;
|
||||||
|
|
||||||
@@ -23,7 +8,26 @@
|
|||||||
--foreground-reversal-8: rgba(255, 255, 255, 0.08);
|
--foreground-reversal-8: rgba(255, 255, 255, 0.08);
|
||||||
--foreground-reversal-10: rgba(255, 255, 255, 0.1);
|
--foreground-reversal-10: rgba(255, 255, 255, 0.1);
|
||||||
--foreground-reversal-15: rgba(255, 255, 255, 0.15);
|
--foreground-reversal-15: rgba(255, 255, 255, 0.15);
|
||||||
--foreground-reversal-30: #404040; /* gray-750 */
|
|
||||||
|
--foreground-gray-750: #404040;
|
||||||
|
--foreground-gray-500: #7f7f7f;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 라이트 테마 색상 */
|
||||||
|
[data-theme='light'] {
|
||||||
|
--foreground: #ffffff;
|
||||||
|
--foreground-10: #ffffff;
|
||||||
|
|
||||||
|
--foreground-reversal: #1f1f1f;
|
||||||
|
--foreground-reversal-4: rgba(0, 0, 0, 0.04);
|
||||||
|
--foreground-reversal-6: rgba(0, 0, 0, 0.06);
|
||||||
|
--foreground-reversal-8: rgba(0, 0, 0, 0.08);
|
||||||
|
--foreground-reversal-10: rgba(0, 0, 0, 0.1);
|
||||||
|
--foreground-reversal-15: rgba(0, 0, 0, 0.15);
|
||||||
|
--foreground-reversal-50: rgba(0, 0, 0, 0.5); /* #7F7F7F */
|
||||||
|
|
||||||
|
--foreground-gray-750: #ebebeb; /* gray-80 */
|
||||||
|
--foreground-gray-500: #999999; /* gray-400 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 커스텀 컴포넌트 스타일 */
|
/* 커스텀 컴포넌트 스타일 */
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ interface Props {
|
|||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
withDefaults(defineProps<Props>(), {
|
||||||
size: 12,
|
size: 12,
|
||||||
color: '#7F7F7F',
|
color: 'var(--foreground-gray-500)',
|
||||||
className: '',
|
className: '',
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -23,7 +23,7 @@ withDefaults(defineProps<Props>(), {
|
|||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M5.29499 7.715L2.39999 4.875C2.07499 4.555 2.29999 4 2.75999 4L9.23499 4C9.69499 4 9.91999 4.555 9.59499 4.875L6.69999 7.715C6.30999 8.095 5.68999 8.095 5.29999 7.715H5.29499Z"
|
d="M5.29499 7.715L2.39999 4.875C2.07499 4.555 2.29999 4 2.75999 4L9.23499 4C9.69499 4 9.91999 4.555 9.59499 4.875L6.69999 7.715C6.30999 8.095 5.68999 8.095 5.29999 7.715H5.29499Z"
|
||||||
fill="#7F7F7F"
|
:fill="color"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
interface Props {
|
|
||||||
size?: number | string
|
|
||||||
color?: string
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
|
||||||
size: 32,
|
|
||||||
color: '#EBEBEB',
|
|
||||||
className: '',
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
:width="size"
|
|
||||||
:height="size"
|
|
||||||
viewBox="0 0 32 33"
|
|
||||||
fill="none"
|
|
||||||
:class="className"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M26.2768 8.10939C26.7975 7.5887 26.7975 6.74448 26.2768 6.22378C25.7561 5.70308 24.9119 5.70308 24.3912 6.22378L16.0007 14.6143L7.61013 6.22378C7.08943 5.70308 6.24521 5.70308 5.72451 6.22378C5.20381 6.74448 5.20381 7.5887 5.72451 8.10939L14.115 16.4999L5.72451 24.8904C5.20381 25.4111 5.20381 26.2554 5.72451 26.7761C6.24521 27.2968 7.08943 27.2968 7.61013 26.7761L16.0007 18.3855L24.3912 26.7761C24.9119 27.2968 25.7561 27.2968 26.2768 26.7761C26.7975 26.2554 26.7975 25.4111 26.2768 24.8904L17.8863 16.4999L26.2768 8.10939Z"
|
|
||||||
:fill="color"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||
29
layers/components/atoms/icons/CloseLine.vue
Normal file
29
layers/components/atoms/icons/CloseLine.vue
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
interface Props {
|
||||||
|
size?: number | string
|
||||||
|
color?: string
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
size: 32,
|
||||||
|
color: '#EBEBEB',
|
||||||
|
className: '',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
:width="size"
|
||||||
|
:height="size"
|
||||||
|
viewBox="0 0 32 32"
|
||||||
|
fill="none"
|
||||||
|
:class="className"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M26.2768 8.10947C26.7975 7.58877 26.7975 6.74455 26.2768 6.22385C25.7561 5.70315 24.9119 5.70315 24.3912 6.22385L16.0007 14.6144L7.61013 6.22385C7.08943 5.70315 6.24521 5.70315 5.72451 6.22385C5.20381 6.74455 5.20381 7.58877 5.72451 8.10947L14.115 16.5L5.72451 24.8905C5.20381 25.4112 5.20381 26.2554 5.72451 26.7761C6.24521 27.2968 7.08943 27.2968 7.61013 26.7761L16.0007 18.3856L24.3912 26.7761C24.9119 27.2968 25.7561 27.2968 26.2768 26.7761C26.7975 26.2554 26.7975 25.4112 26.2768 24.8905L17.8863 16.5L26.2768 8.10947Z"
|
||||||
|
:fill="color"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
29
layers/components/atoms/icons/MenuBoldLine.vue
Normal file
29
layers/components/atoms/icons/MenuBoldLine.vue
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
interface Props {
|
||||||
|
size?: number | string
|
||||||
|
color?: string
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
size: 24,
|
||||||
|
color: 'var(--foreground-reversal)',
|
||||||
|
className: '',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
:width="size"
|
||||||
|
:height="size"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
:class="className"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M20 16C20.7594 16 21.375 16.6156 21.375 17.375C21.375 18.1344 20.7594 18.75 20 18.75H4C3.24061 18.75 2.625 18.1344 2.625 17.375C2.625 16.6156 3.24061 16 4 16H20ZM20 10.5C20.7594 10.5 21.375 11.1156 21.375 11.875C21.375 12.6344 20.7594 13.25 20 13.25H4C3.24061 13.25 2.625 12.6344 2.625 11.875C2.625 11.1156 3.24061 10.5 4 10.5H20ZM20 5C20.7594 5 21.375 5.61561 21.375 6.375C21.375 7.13439 20.7594 7.75 20 7.75H4C3.24061 7.75 2.625 7.13439 2.625 6.375C2.625 5.61561 3.24061 5 4 5H20Z"
|
||||||
|
:fill="color"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
29
layers/components/atoms/icons/MenuCloseLine.vue
Normal file
29
layers/components/atoms/icons/MenuCloseLine.vue
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
interface Props {
|
||||||
|
size?: number | string
|
||||||
|
color?: string
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
size: 16,
|
||||||
|
color: 'var(--foreground-reversal)',
|
||||||
|
className: '',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
:width="size"
|
||||||
|
:height="size"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
:class="className"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M15.7071 1.70711C16.0976 1.31658 16.0976 0.683417 15.7071 0.292893C15.3166 -0.0976311 14.6834 -0.0976311 14.2929 0.292893L8 6.58579L1.70711 0.292893C1.31658 -0.0976311 0.683417 -0.0976311 0.292894 0.292893C-0.0976304 0.683417 -0.0976304 1.31658 0.292894 1.70711L6.58579 8L0.292893 14.2929C-0.0976311 14.6834 -0.0976311 15.3166 0.292893 15.7071C0.683417 16.0976 1.31658 16.0976 1.70711 15.7071L8 9.41421L14.2929 15.7071C14.6834 16.0976 15.3166 16.0976 15.7071 15.7071C16.0976 15.3166 16.0976 14.6834 15.7071 14.2929L9.41421 8L15.7071 1.70711Z"
|
||||||
|
:fill="color"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
29
layers/components/atoms/icons/OptionHorizontalFill.vue
Normal file
29
layers/components/atoms/icons/OptionHorizontalFill.vue
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
interface Props {
|
||||||
|
size?: number | string
|
||||||
|
color?: string
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
size: 24,
|
||||||
|
color: 'var(--foreground-reversal)',
|
||||||
|
className: '',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
:width="size"
|
||||||
|
:height="size"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
:class="className"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M4.5 10.5C3.675 10.5 3 11.175 3 12C3 12.825 3.675 13.5 4.5 13.5C5.325 13.5 6 12.825 6 12C6 11.175 5.325 10.5 4.5 10.5ZM19.5 10.5C18.675 10.5 18 11.175 18 12C18 12.825 18.675 13.5 19.5 13.5C20.325 13.5 21 12.825 21 12C21 11.175 20.325 10.5 19.5 10.5ZM12 10.5C11.175 10.5 10.5 11.175 10.5 12C10.5 12.825 11.175 13.5 12 13.5C12.825 13.5 13.5 12.825 13.5 12C13.5 11.175 12.825 10.5 12 10.5Z"
|
||||||
|
:fill="color"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
@@ -7,7 +7,7 @@ interface Props {
|
|||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
withDefaults(defineProps<Props>(), {
|
||||||
size: 16,
|
size: 16,
|
||||||
color: '#B2B2B2',
|
color: 'var(--foreground-gray-500)',
|
||||||
className: '',
|
className: '',
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -18,16 +18,16 @@ withDefaults(defineProps<Props>(), {
|
|||||||
:width="size"
|
:width="size"
|
||||||
:height="size"
|
:height="size"
|
||||||
viewBox="0 0 16 16"
|
viewBox="0 0 16 16"
|
||||||
:fill="color"
|
fill="none"
|
||||||
:class="className"
|
:class="className"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M3.63636 3.33333C3.469 3.33333 3.33333 3.469 3.33333 3.63636L3.33333 12.3636C3.33333 12.531 3.469 12.6667 3.63636 12.6667H12.3636C12.531 12.6667 12.6667 12.531 12.6667 12.3636V9.93939C12.6667 9.5712 12.9651 9.27273 13.3333 9.27273C13.7015 9.27273 14 9.5712 14 9.93939V12.3636C14 13.2674 13.2674 14 12.3636 14H3.63636C2.73262 14 2 13.2674 2 12.3636L2 3.63636C2 2.73263 2.73262 2 3.63636 2L6.06061 2C6.4288 2 6.72727 2.29848 6.72727 2.66667C6.72727 3.03486 6.4288 3.33333 6.06061 3.33333H3.63636Z"
|
d="M3.63636 3.33333C3.469 3.33333 3.33333 3.469 3.33333 3.63636L3.33333 12.3636C3.33333 12.531 3.469 12.6667 3.63636 12.6667H12.3636C12.531 12.6667 12.6667 12.531 12.6667 12.3636V9.93939C12.6667 9.5712 12.9651 9.27273 13.3333 9.27273C13.7015 9.27273 14 9.5712 14 9.93939V12.3636C14 13.2674 13.2674 14 12.3636 14H3.63636C2.73262 14 2 13.2674 2 12.3636L2 3.63636C2 2.73263 2.73262 2 3.63636 2L6.06061 2C6.4288 2 6.72727 2.29848 6.72727 2.66667C6.72727 3.03486 6.4288 3.33333 6.06061 3.33333H3.63636Z"
|
||||||
fill="#B2B2B2"
|
:fill="color"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M12.6667 4.27614V6.54545C12.6667 6.91364 12.9651 7.21212 13.3333 7.21212C13.7015 7.21212 14 6.91364 14 6.54545V2.66667C14 2.29848 13.7015 2 13.3333 2L9.45455 2C9.08636 2 8.78788 2.29848 8.78788 2.66667C8.78788 3.03486 9.08636 3.33333 9.45455 3.33333L11.7239 3.33333L7.28616 7.77103C7.02581 8.03138 7.02581 8.45349 7.28616 8.71384C7.54651 8.97419 7.96862 8.97419 8.22897 8.71384L12.6667 4.27614Z"
|
d="M12.6667 4.27614V6.54545C12.6667 6.91364 12.9651 7.21212 13.3333 7.21212C13.7015 7.21212 14 6.91364 14 6.54545V2.66667C14 2.29848 13.7015 2 13.3333 2L9.45455 2C9.08636 2 8.78788 2.29848 8.78788 2.66667C8.78788 3.03486 9.08636 3.33333 9.45455 3.33333L11.7239 3.33333L7.28616 7.77103C7.02581 8.03138 7.02581 8.45349 7.28616 8.71384C7.54651 8.97419 7.96862 8.97419 8.22897 8.71384L12.6667 4.27614Z"
|
||||||
fill="#B2B2B2"
|
:fill="color"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { ClassType } from '#layers/types/Common'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
to: string
|
to: string
|
||||||
target?: string
|
target?: string
|
||||||
class?: string
|
class?: ClassType
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const colorCode = computed(() => {
|
|||||||
})
|
})
|
||||||
const currentImageSrc = computed(() => {
|
const currentImageSrc = computed(() => {
|
||||||
if (!imageSrc.value) return ''
|
if (!imageSrc.value) return ''
|
||||||
|
|
||||||
return breakpoints.value.isMobile
|
return breakpoints.value.isMobile
|
||||||
? imageSrc.value.mobileSrc || ''
|
? imageSrc.value.mobileSrc || ''
|
||||||
: imageSrc.value.pcSrc || ''
|
: imageSrc.value.pcSrc || ''
|
||||||
@@ -36,25 +37,20 @@ const currentImageSrc = computed(() => {
|
|||||||
const sanitizedContent = computed(() => {
|
const sanitizedContent = computed(() => {
|
||||||
return displayText.value?.replace(/\n/g, '<br/>') || ''
|
return displayText.value?.replace(/\n/g, '<br/>') || ''
|
||||||
})
|
})
|
||||||
// 이미지가 있는지 확인
|
|
||||||
const hasImage = computed(() => {
|
|
||||||
return imageSrc.value && (imageSrc.value.mobileSrc || imageSrc.value.pcSrc)
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- 이미지 -->
|
<!-- 이미지 -->
|
||||||
<img
|
<img
|
||||||
v-if="hasImage && currentImageSrc"
|
v-if="isTypeImage(resourcesData?.resource_type) && currentImageSrc"
|
||||||
:src="currentImageSrc"
|
:src="currentImageSrc"
|
||||||
:alt="alt || displayText"
|
:alt="alt || displayText"
|
||||||
:class="`w-full h-full object-${objectFit}`"
|
:class="`w-full h-full object-${objectFit}`"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 텍스트 -->
|
<!-- 텍스트 -->
|
||||||
<span
|
<span
|
||||||
v-else-if="displayText"
|
v-else-if="isTypeText(resourcesData?.resource_type)"
|
||||||
v-dompurify-html="sanitizedContent"
|
v-dompurify-html="sanitizedContent"
|
||||||
:style="{ color: getColorCode({ colorName, colorCode }) }"
|
:style="{ color: getColorCode({ colorName, colorCode }) }"
|
||||||
class="block"
|
class="block"
|
||||||
|
|||||||
@@ -22,13 +22,13 @@
|
|||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<!-- 헤더 -->
|
<!-- 헤더 -->
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end mb-3 md:mb-4">
|
||||||
<button
|
<button
|
||||||
class="p-1 text-white rounded-full transition-colors"
|
class="text-white rounded-full transition-colors"
|
||||||
aria-label="모달 닫기"
|
aria-label="모달 닫기"
|
||||||
@click="closeModal"
|
@click="closeModal"
|
||||||
>
|
>
|
||||||
<AtomsIconsClose />
|
<AtomsIconsCloseLine />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Splide, SplideSlide } from '@splidejs/vue-splide'
|
import { Splide, SplideSlide } from '@splidejs/vue-splide'
|
||||||
|
import { getFirstGroup, isTypeVideo } from '#layers/utils/dataUtil'
|
||||||
|
import { getMediaSrc, getYouTubeEmbedUrl } from '#layers/utils/youtube'
|
||||||
import type { Splide as SplideType, Options } from '@splidejs/splide'
|
import type { Splide as SplideType, Options } from '@splidejs/splide'
|
||||||
import type { PageDataResourceGroup } from '#layers/types/api/pageData'
|
import type {
|
||||||
|
PageDataResourceGroups,
|
||||||
|
PageDataResourceGroup,
|
||||||
|
} from '#layers/types/api/pageData'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
slideData: { media: any; set_order: number }[]
|
slideData: { media: PageDataResourceGroups; set_order: number }[]
|
||||||
videoPlay?: PageDataResourceGroup
|
videoPlay?: PageDataResourceGroup
|
||||||
arrows?: boolean
|
arrows?: boolean
|
||||||
pagination?: boolean
|
pagination?: boolean
|
||||||
@@ -46,10 +51,27 @@ const thumbOptions = computed<Options>(() => ({
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const isPassVideo = (groups: PageDataResourceGroups, index: number) => {
|
||||||
|
const firstGroup = getFirstGroup(groups)
|
||||||
|
return (
|
||||||
|
firstGroup &&
|
||||||
|
isTypeVideo(firstGroup?.resource_type) &&
|
||||||
|
index !== playingSlideIndex.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const handleVideoClick = (index: number) => {
|
const handleVideoClick = (index: number) => {
|
||||||
playingSlideIndex.value = index
|
playingSlideIndex.value = index
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getYouTubeEmbedUrlFromMedia = (
|
||||||
|
resourceGroups: PageDataResourceGroup[]
|
||||||
|
) => {
|
||||||
|
const resourceGroup = getFirstGroup(resourceGroups)
|
||||||
|
const mediaSrc = getMediaSrc(resourceGroup)
|
||||||
|
return mediaSrc ? getYouTubeEmbedUrl(mediaSrc, true) : ''
|
||||||
|
}
|
||||||
|
|
||||||
let mainInst: SplideType | null = null
|
let mainInst: SplideType | null = null
|
||||||
let thumbsInst: SplideType | null = null
|
let thumbsInst: SplideType | null = null
|
||||||
|
|
||||||
@@ -87,16 +109,14 @@ onBeforeUnmount(() => {
|
|||||||
:class="{ 'opacity-0': playingSlideIndex === index }"
|
:class="{ 'opacity-0': playingSlideIndex === index }"
|
||||||
/>
|
/>
|
||||||
<AtomsButtonPlay
|
<AtomsButtonPlay
|
||||||
v-if="
|
v-if="isPassVideo(item.media, index)"
|
||||||
getMediaType(item.media) === 'video' && playingSlideIndex !== index
|
|
||||||
"
|
|
||||||
:resources-data="videoPlay"
|
:resources-data="videoPlay"
|
||||||
class="btn-play"
|
class="btn-play"
|
||||||
@click="handleVideoClick(index)"
|
@click="handleVideoClick(index)"
|
||||||
/>
|
/>
|
||||||
<iframe
|
<iframe
|
||||||
v-if="playingSlideIndex === index"
|
v-if="playingSlideIndex === index"
|
||||||
:src="getYouTubeEmbedUrl(getMediaText(item.media), true)"
|
:src="getYouTubeEmbedUrlFromMedia(item.media)"
|
||||||
class="absolute top-0 left-0 w-full h-full"
|
class="absolute top-0 left-0 w-full h-full"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useGameDataStore } from '#layers/stores/useGameDataStore'
|
import { useGameDataStore } from '#layers/stores/useGameDataStore'
|
||||||
import { useScrollStore } from '#layers/stores/useScrollStore'
|
import { useScrollStore } from '#layers/stores/useScrollStore'
|
||||||
import { useWindowScroll, onClickOutside, useWindowSize } from '@vueuse/core'
|
import { onClickOutside, useWindowSize } from '@vueuse/core'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import type {
|
import type {
|
||||||
GameDataValue,
|
GameDataValue,
|
||||||
@@ -10,18 +10,18 @@ import type {
|
|||||||
} from '#layers/types/api/gameData'
|
} from '#layers/types/api/gameData'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { y: windowY } = useWindowScroll()
|
const { width } = useWindowSize()
|
||||||
const gameDataStore = useGameDataStore()
|
const gameDataStore = useGameDataStore()
|
||||||
const scrollStore = useScrollStore()
|
const scrollStore = useScrollStore()
|
||||||
const _breakpoints = useResponsiveBreakpointsReliable()
|
|
||||||
|
|
||||||
const { isPassedStoveGnb } = storeToRefs(scrollStore)
|
const { isPassedStoveGnb, scrollGnbPosition } = storeToRefs(scrollStore)
|
||||||
|
|
||||||
const gameData = gameDataStore.gameData as GameDataValue
|
|
||||||
const gnbList = (gameData?.gnb?.menus ?? {}) as GameDataMenuChildren
|
|
||||||
const isMenuOpen = ref(false)
|
|
||||||
const navAreaRef = ref<HTMLElement>()
|
const navAreaRef = ref<HTMLElement>()
|
||||||
const startRef = 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 navWidth = ref(0)
|
||||||
const startWidth = ref(0)
|
const startWidth = ref(0)
|
||||||
const officialItemWidths = ref<number[]>([])
|
const officialItemWidths = ref<number[]>([])
|
||||||
@@ -34,15 +34,6 @@ const pathMatches = (base: string, current: string) => {
|
|||||||
return current === base || current.startsWith(base + '/')
|
return current === base || current.startsWith(base + '/')
|
||||||
}
|
}
|
||||||
|
|
||||||
/** header overlay 높이 계산 (re-compute 최소화) */
|
|
||||||
const scrollPositionHeight = computed(() => {
|
|
||||||
const gnbHeight = scrollStore.stoveGnbHeight
|
|
||||||
const y = windowY.value
|
|
||||||
if (y === 0) return `${gnbHeight}px`
|
|
||||||
if (y >= gnbHeight) return '0px'
|
|
||||||
return `${gnbHeight - y}`
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 자식 중 활성 링크 존재 여부 */
|
/** 자식 중 활성 링크 존재 여부 */
|
||||||
const hasActiveChild = (children?: GameDataMenuChildren) => {
|
const hasActiveChild = (children?: GameDataMenuChildren) => {
|
||||||
const cur = currentPath.value
|
const cur = currentPath.value
|
||||||
@@ -71,7 +62,8 @@ const calculateNavWidth = () => {
|
|||||||
if (!navAreaRef.value) return 0
|
if (!navAreaRef.value) return 0
|
||||||
|
|
||||||
const navAreaWidth = navAreaRef.value.offsetWidth
|
const navAreaWidth = navAreaRef.value.offsetWidth
|
||||||
return navAreaWidth
|
const moreWidth = 72 // 더보기 버튼 넓이 + 마진
|
||||||
|
return navAreaWidth + moreWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
// startRef의 넓이를 구하는 함수
|
// startRef의 넓이를 구하는 함수
|
||||||
@@ -79,10 +71,11 @@ const calculateStartWidth = () => {
|
|||||||
if (!startRef.value) return 0
|
if (!startRef.value) return 0
|
||||||
|
|
||||||
const startWidth = startRef.value.offsetWidth
|
const startWidth = startRef.value.offsetWidth
|
||||||
return startWidth + 40
|
const headerRightPadding = 40 // 헤더 오른쪽 마진
|
||||||
|
return startWidth + headerRightPadding
|
||||||
}
|
}
|
||||||
|
|
||||||
// official 자식들의 넓이를 구하는 함수 (뒤에서부터 순서대로)
|
// official 자식들의 넓이를 구하는 함수
|
||||||
const calculateOfficialItemWidths = () => {
|
const calculateOfficialItemWidths = () => {
|
||||||
if (!navAreaRef.value) return
|
if (!navAreaRef.value) return
|
||||||
|
|
||||||
@@ -103,8 +96,6 @@ const calculateOfficialItemWidths = () => {
|
|||||||
calculateOverflow()
|
calculateOverflow()
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(0, Object.keys(gnbList).length)
|
|
||||||
|
|
||||||
// 오버플로우 계산 함수
|
// 오버플로우 계산 함수
|
||||||
const calculateOverflow = () => {
|
const calculateOverflow = () => {
|
||||||
if (!navAreaRef.value) return
|
if (!navAreaRef.value) return
|
||||||
@@ -112,18 +103,9 @@ const calculateOverflow = () => {
|
|||||||
const totalNavWidth = navWidth.value + startWidth.value
|
const totalNavWidth = navWidth.value + startWidth.value
|
||||||
const screenWidth = width.value
|
const screenWidth = width.value
|
||||||
|
|
||||||
console.log('calculateOverflow called:', {
|
|
||||||
screenWidth,
|
|
||||||
totalNavWidth,
|
|
||||||
navWidth: navWidth.value,
|
|
||||||
startWidth: startWidth.value,
|
|
||||||
officialItemWidths: officialItemWidths.value,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 모바일(1024px 미만)에서는 overflowNam을 0으로 설정
|
// 모바일(1024px 미만)에서는 overflowNam을 0으로 설정
|
||||||
if (screenWidth < 1024) {
|
if (screenWidth < 1024) {
|
||||||
overflowNam.value = 0
|
overflowNam.value = 0
|
||||||
console.log('Mobile view - overflowNam set to 0')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,30 +114,30 @@ const calculateOverflow = () => {
|
|||||||
let removedCount = 0
|
let removedCount = 0
|
||||||
let currentTotal = totalNavWidth
|
let currentTotal = totalNavWidth
|
||||||
|
|
||||||
// officialItemWidths를 하나씩 빼면서 해상도보다 작아지는지 확인
|
|
||||||
for (let i = 0; i < officialItemWidths.value.length; i++) {
|
for (let i = 0; i < officialItemWidths.value.length; i++) {
|
||||||
currentTotal -= officialItemWidths.value[i]
|
currentTotal -= officialItemWidths.value[i]
|
||||||
removedCount++
|
removedCount++
|
||||||
|
|
||||||
// 해상도보다 작아지면 중단
|
|
||||||
if (currentTotal <= screenWidth) {
|
if (currentTotal <= screenWidth) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
overflowNam.value = removedCount
|
overflowNam.value = removedCount
|
||||||
console.log('Overflow calculated:', overflowNam.value)
|
|
||||||
} else {
|
} else {
|
||||||
overflowNam.value = 0
|
overflowNam.value = 0
|
||||||
console.log('No overflow needed, setting to 0')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 컴포넌트 마운트 후 한 번만 계산
|
onClickOutside(navAreaRef, () => (isMenuOpen.value = false))
|
||||||
|
|
||||||
|
// 화면 크기 변경 시 오버플로우 재계산
|
||||||
|
watch(width, () => {
|
||||||
|
calculateOverflow()
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 초기화
|
|
||||||
overflowNam.value = 0
|
overflowNam.value = 0
|
||||||
console.log('onMounted - overflowNam 초기화:', overflowNam.value)
|
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (navAreaRef.value && startRef.value) {
|
if (navAreaRef.value && startRef.value) {
|
||||||
@@ -165,14 +147,6 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// 화면 크기 변경 시 오버플로우 재계산
|
|
||||||
const { width } = useWindowSize()
|
|
||||||
watch(width, () => {
|
|
||||||
calculateOverflow()
|
|
||||||
})
|
|
||||||
|
|
||||||
onClickOutside(navAreaRef, () => (isMenuOpen.value = false))
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -182,47 +156,52 @@ onClickOutside(navAreaRef, () => (isMenuOpen.value = false))
|
|||||||
<div class="game-wrapper" :class="{ 'is-fixed': isPassedStoveGnb }">
|
<div class="game-wrapper" :class="{ 'is-fixed': isPassedStoveGnb }">
|
||||||
<AtomsLocaleLink to="/brand" class="mx-auto md:hidden">
|
<AtomsLocaleLink to="/brand" class="mx-auto md:hidden">
|
||||||
<img
|
<img
|
||||||
:src="gameData?.gnb?.bi_path"
|
:src="gnbData?.bi_path"
|
||||||
:alt="gameData?.game_name"
|
:alt="gameData?.game_name"
|
||||||
class="h-[30px]"
|
class="h-[30px]"
|
||||||
/>
|
/>
|
||||||
</AtomsLocaleLink>
|
</AtomsLocaleLink>
|
||||||
<button class="btn-open" @click="handleMenuOpen">
|
<button class="btn-open" @click="handleMenuOpen">
|
||||||
|
<AtomsIconsMenuBoldLine class="mx-auto" />
|
||||||
<span class="sr-only">menu open</span>
|
<span class="sr-only">menu open</span>
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
:class="['gnb-game', { 'is-open': isMenuOpen }]"
|
:class="['gnb-game', { 'is-open': isMenuOpen }]"
|
||||||
:style="{ '--scroll-position': scrollPositionHeight }"
|
:style="{ '--scroll-position': scrollGnbPosition + 'px' }"
|
||||||
>
|
>
|
||||||
<div ref="navAreaRef" class="nav-area">
|
<div ref="navAreaRef" class="nav-area">
|
||||||
<div class="nav-logo">
|
<div class="nav-logo">
|
||||||
<AtomsLocaleLink to="/brand">
|
<AtomsLocaleLink to="/brand">
|
||||||
<img
|
<img
|
||||||
:src="gameData?.gnb?.bi_path"
|
:src="gnbData?.bi_path"
|
||||||
:alt="gameData?.game_name"
|
:alt="gameData?.game_name"
|
||||||
class="h-[30px]"
|
class="h-[30px]"
|
||||||
/>
|
/>
|
||||||
</AtomsLocaleLink>
|
</AtomsLocaleLink>
|
||||||
</div>
|
</div>
|
||||||
<nav class="nav-list">
|
<nav class="nav-list">
|
||||||
<div v-if="gnbList" class="official">
|
<div v-if="gnbData?.menus" class="official">
|
||||||
<div
|
<div
|
||||||
v-for="(gnbItem, key) in gnbList"
|
v-for="(gnbItem, key) in gnbData?.menus"
|
||||||
:key="key"
|
:key="key"
|
||||||
class="nav-item"
|
class="nav-item"
|
||||||
:class="{
|
:class="{
|
||||||
'is-hidden':
|
'is-hidden':
|
||||||
overflowNam > 0 &&
|
overflowNam > 0 &&
|
||||||
Number(key) >= Object.keys(gnbList).length - overflowNam,
|
Number(key) >=
|
||||||
|
Object.keys(gnbData?.menus).length - overflowNam,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<BlocksHybridLink
|
<BlocksHybridLink
|
||||||
:to="gnbItem.url_path"
|
:to="gnbItem.url_path"
|
||||||
:target="gnbItem.link_target"
|
:target="gnbItem.link_target"
|
||||||
:class="`nav-1depth ${isNavItemActive(gnbItem) ? 'active' : ''}`"
|
:class="['nav-1depth', { active: isNavItemActive(gnbItem) }]"
|
||||||
>
|
>
|
||||||
<span>{{ gnbItem.menu_name }}</span>
|
<span>{{ gnbItem.menu_name }}</span>
|
||||||
<AtomsIconsArrowDown v-if="gnbItem.children" />
|
<AtomsIconsArrowDownFill
|
||||||
|
v-if="gnbItem.children"
|
||||||
|
class="hidden md:block"
|
||||||
|
/>
|
||||||
</BlocksHybridLink>
|
</BlocksHybridLink>
|
||||||
<div v-if="gnbItem.children" class="nav-2depth">
|
<div v-if="gnbItem.children" class="nav-2depth">
|
||||||
<ul>
|
<ul>
|
||||||
@@ -235,7 +214,7 @@ onClickOutside(navAreaRef, () => (isMenuOpen.value = false))
|
|||||||
:target="child.link_target"
|
:target="child.link_target"
|
||||||
>
|
>
|
||||||
<span>{{ child.menu_name }}</span>
|
<span>{{ child.menu_name }}</span>
|
||||||
<AtomsIconsLinkOut
|
<AtomsIconsWebLinkLine
|
||||||
v-if="child.link_target === '_blank'"
|
v-if="child.link_target === '_blank'"
|
||||||
/>
|
/>
|
||||||
</BlocksHybridLink>
|
</BlocksHybridLink>
|
||||||
@@ -244,17 +223,20 @@ onClickOutside(navAreaRef, () => (isMenuOpen.value = false))
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="gnbList && overflowNam > 0" class="more">
|
<div v-if="gnbData?.menus && overflowNam > 0" class="more">
|
||||||
<button class="btn-more">
|
<button class="btn-more">
|
||||||
|
<AtomsIconsOptionHorizontalFill class="mx-auto" />
|
||||||
<span class="sr-only">more</span>
|
<span class="sr-only">more</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="more-list">
|
<div class="more-list">
|
||||||
|
<div class="list-inner">
|
||||||
<div
|
<div
|
||||||
v-for="(gnbItem, key) in gnbList"
|
v-for="(gnbItem, key) in gnbData?.menus"
|
||||||
:key="key"
|
:key="key"
|
||||||
:class="{
|
:class="{
|
||||||
hidden:
|
hidden:
|
||||||
Number(key) < Object.keys(gnbList).length - overflowNam,
|
Number(key) <
|
||||||
|
Object.keys(gnbData?.menus).length - overflowNam,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<BlocksHybridLink
|
<BlocksHybridLink
|
||||||
@@ -263,7 +245,6 @@ onClickOutside(navAreaRef, () => (isMenuOpen.value = false))
|
|||||||
:class="`${isNavItemActive(gnbItem) ? 'active' : ''}`"
|
:class="`${isNavItemActive(gnbItem) ? 'active' : ''}`"
|
||||||
>
|
>
|
||||||
<span>{{ gnbItem.menu_name }}</span>
|
<span>{{ gnbItem.menu_name }}</span>
|
||||||
<AtomsIconsArrowDown v-if="gnbItem.children" />
|
|
||||||
</BlocksHybridLink>
|
</BlocksHybridLink>
|
||||||
<div v-if="gnbItem.children">
|
<div v-if="gnbItem.children">
|
||||||
<ul>
|
<ul>
|
||||||
@@ -276,7 +257,7 @@ onClickOutside(navAreaRef, () => (isMenuOpen.value = false))
|
|||||||
:target="child.link_target"
|
:target="child.link_target"
|
||||||
>
|
>
|
||||||
<span>{{ child.menu_name }}</span>
|
<span>{{ child.menu_name }}</span>
|
||||||
<AtomsIconsLinkOut
|
<AtomsIconsWebLinkLine
|
||||||
v-if="child.link_target === '_blank'"
|
v-if="child.link_target === '_blank'"
|
||||||
/>
|
/>
|
||||||
</BlocksHybridLink>
|
</BlocksHybridLink>
|
||||||
@@ -286,6 +267,7 @@ onClickOutside(navAreaRef, () => (isMenuOpen.value = false))
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="event">
|
<div class="event">
|
||||||
<div class="nav-item">
|
<div class="nav-item">
|
||||||
<BlocksHybridLink
|
<BlocksHybridLink
|
||||||
@@ -293,7 +275,9 @@ onClickOutside(navAreaRef, () => (isMenuOpen.value = false))
|
|||||||
:target="'_self'"
|
:target="'_self'"
|
||||||
class="nav-1depth text-gradient-pink"
|
class="nav-1depth text-gradient-pink"
|
||||||
>
|
>
|
||||||
|
<AtomsIconsStarFill />
|
||||||
<span>이벤트</span>
|
<span>이벤트</span>
|
||||||
|
<AtomsIconsStarFill />
|
||||||
</BlocksHybridLink>
|
</BlocksHybridLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -304,6 +288,7 @@ onClickOutside(navAreaRef, () => (isMenuOpen.value = false))
|
|||||||
</AtomsButton>
|
</AtomsButton>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-close" @click="handleMenuClose">
|
<button class="btn-close" @click="handleMenuClose">
|
||||||
|
<AtomsIconsMenuCloseLine class="mx-auto" />
|
||||||
<span class="sr-only">menu close</span>
|
<span class="sr-only">menu close</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -317,7 +302,7 @@ onClickOutside(navAreaRef, () => (isMenuOpen.value = false))
|
|||||||
@apply bg-theme-foreground text-theme-foreground-reversal relative z-50;
|
@apply bg-theme-foreground text-theme-foreground-reversal relative z-50;
|
||||||
}
|
}
|
||||||
.game-wrapper {
|
.game-wrapper {
|
||||||
@apply absolute flex w-full h-[48px] items-center whitespace-nowrap px-[64px] bg-theme-foreground md:h-16 md:pl-0 md:pr-[40px]
|
@apply absolute flex w-full h-[48px] items-center whitespace-nowrap px-[52px] bg-theme-foreground sm:px-[72px] md:h-16 md:pl-0 md:pr-[40px]
|
||||||
before:content-[''] before:absolute before:top-0 before:left-0 before:right-0 before:h-px before:bg-theme-foreground-reversal-6;
|
before:content-[''] before:absolute before:top-0 before:left-0 before:right-0 before:h-px before:bg-theme-foreground-reversal-6;
|
||||||
}
|
}
|
||||||
.game-wrapper.is-fixed {
|
.game-wrapper.is-fixed {
|
||||||
@@ -329,10 +314,10 @@ onClickOutside(navAreaRef, () => (isMenuOpen.value = false))
|
|||||||
|
|
||||||
.btn-open,
|
.btn-open,
|
||||||
.btn-close {
|
.btn-close {
|
||||||
@apply absolute w-[40px] h-[40px] md:hidden bg-[red];
|
@apply absolute w-[40px] h-[40px] md:hidden;
|
||||||
}
|
}
|
||||||
.btn-open {
|
.btn-open {
|
||||||
@apply top-[4px] left-[12px];
|
@apply top-[4px] left-[12px] sm:left-[32px];
|
||||||
}
|
}
|
||||||
.btn-close {
|
.btn-close {
|
||||||
@apply top-[11px] left-[12px];
|
@apply top-[11px] left-[12px];
|
||||||
@@ -340,9 +325,6 @@ onClickOutside(navAreaRef, () => (isMenuOpen.value = false))
|
|||||||
.btn-start {
|
.btn-start {
|
||||||
@apply relative mt-2 px-5 md:absolute md:right-0 md:mt-0 md:px-0;
|
@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 {
|
.gnb-game {
|
||||||
@apply absolute top-0 left-0 w-0 md:relative md:w-full md:!h-full;
|
@apply absolute top-0 left-0 w-0 md:relative md:w-full md:!h-full;
|
||||||
@@ -355,12 +337,12 @@ onClickOutside(navAreaRef, () => (isMenuOpen.value = false))
|
|||||||
@apply content-[''] absolute inset-0 w-[100vw] h-full bg-[rgba(0,0,0,0.6)] md:hidden;
|
@apply content-[''] absolute inset-0 w-[100vw] h-full bg-[rgba(0,0,0,0.6)] md:hidden;
|
||||||
}
|
}
|
||||||
.gnb-game.is-open .nav-area {
|
.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 {
|
.nav-area {
|
||||||
@apply flex flex-col w-[360px] bg-theme-foreground-10 translate-x-[-100%]
|
@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 {
|
.nav-logo {
|
||||||
@@ -369,7 +351,7 @@ onClickOutside(navAreaRef, () => (isMenuOpen.value = false))
|
|||||||
|
|
||||||
.nav-list {
|
.nav-list {
|
||||||
@apply overflow-hidden flex flex-col order-1 h-full mt-2 mb-4 px-2
|
@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 {
|
.nav-item {
|
||||||
@@ -400,15 +382,15 @@ onClickOutside(navAreaRef, () => (isMenuOpen.value = false))
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-2depth {
|
.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 {
|
.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 {
|
.nav-2depth a {
|
||||||
@apply flex items-center gap-1 px-5 py-[9px] rounded-[12px] transition-colors
|
@apply flex items-center gap-1 py-[9px] px-5 rounded-[12px] transition-colors
|
||||||
hover:bg-theme-foreground-reversal-4 active:bg-theme-foreground-reversal-10
|
md:py-[11px] md:px-4
|
||||||
md:px-4 md:py-[11px];
|
hover:bg-theme-foreground-reversal-4 active:bg-theme-foreground-reversal-10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.official {
|
.official {
|
||||||
@@ -416,14 +398,30 @@ onClickOutside(navAreaRef, () => (isMenuOpen.value = false))
|
|||||||
}
|
}
|
||||||
|
|
||||||
.more {
|
.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 {
|
.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 {
|
.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-gray-750 md:after:translate-y-[-50%];
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-hidden {
|
.is-hidden {
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const currentPosterSrc = computed(() => {
|
|||||||
<div class="absolute inset-0 w-full h-full">
|
<div class="absolute inset-0 w-full h-full">
|
||||||
<!-- 이미지 타입-->
|
<!-- 이미지 타입-->
|
||||||
<div
|
<div
|
||||||
v-if="resourcesData?.group_type === 'image'"
|
v-if="isTypeImage(resourcesData?.resource_type)"
|
||||||
class="w-full h-full bg-cover bg-center bg-no-repeat"
|
class="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||||
:class="getResponsiveClass()"
|
:class="getResponsiveClass()"
|
||||||
:style="bgStyles"
|
:style="bgStyles"
|
||||||
@@ -54,7 +54,7 @@ const currentPosterSrc = computed(() => {
|
|||||||
|
|
||||||
<!-- 비디오 타입 -->
|
<!-- 비디오 타입 -->
|
||||||
<video
|
<video
|
||||||
v-else-if="resourcesData?.group_type === 'video' && currentVideoSrc"
|
v-else-if="isTypeVideo(resourcesData?.resource_type) && currentVideoSrc"
|
||||||
class="w-full h-full object-cover"
|
class="w-full h-full object-cover"
|
||||||
:poster="currentPosterSrc"
|
:poster="currentPosterSrc"
|
||||||
autoplay
|
autoplay
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ console.log("🚀 11111~ getButtonProps ~ props.resourcesData:", getButtonProps(
|
|||||||
>
|
>
|
||||||
<AtomsButton
|
<AtomsButton
|
||||||
v-for="(button, index) in props.resourcesData"
|
v-for="(button, index) in props.resourcesData"
|
||||||
:key="`${button.group_code}-${index}`"
|
:key="index"
|
||||||
v-bind="getButtonProps(button)"
|
v-bind="getButtonProps(button)"
|
||||||
v-analytics="useAnalyticsLogDataDirect(getButtonProps(button), props.pageVerTmplSeq)"
|
v-analytics="useAnalyticsLogDataDirect(getButtonProps(button), props.pageVerTmplSeq)"
|
||||||
class="size-extra-small md:size-medium"
|
class="size-extra-small md:size-medium"
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import * as amplitude from '@amplitude/analytics-browser'
|
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 { 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 svcLog: any
|
||||||
declare const twq: any
|
declare const twq: any
|
||||||
@@ -24,26 +27,31 @@ export const useAnalyticsLogData = (
|
|||||||
return ref({} as AnalyticsDetailType)
|
return ref({} as AnalyticsDetailType)
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageDataTrack = (typeof resourcesData.tracking === 'object' ? resourcesData.tracking : {}) as AnalyticsLogDataTracking
|
const pageDataTrack = (
|
||||||
|
typeof resourcesData.tracking === 'object' ? resourcesData.tracking : {}
|
||||||
|
) as AnalyticsLogDataTracking
|
||||||
|
console.log('🚀 ~ useAnalyticsLogData ~ pageDataTrack:', pageData)
|
||||||
|
|
||||||
const logData = ref({
|
const logData = ref({
|
||||||
actionType: pageDataTrack?.action_type,
|
actionType: pageDataTrack?.action_type,
|
||||||
// logSourceType:pageDataTrack.logSourceType,
|
// logSourceType:pageDataTrack.logSourceType,
|
||||||
// viewArea:pageDataTrack.viewArea,
|
// viewArea:pageDataTrack.viewArea,
|
||||||
// viewType:pageDataTrack.viewType,
|
// viewType:pageDataTrack.viewType,
|
||||||
clickArea:pageData.page_name_en,
|
clickArea: pageData.page_name_en,
|
||||||
clickSarea: pageData.templates[pageVerTmplSeq].page_ver_tmpl_name_en,
|
clickSarea: pageData.templates[pageVerTmplSeq].page_ver_tmpl_name_en,
|
||||||
clickItem: `${pageData.templates[pageVerTmplSeq].page_ver_tmpl_name}_${pageDataTrack?.click_item}`,
|
clickItem: `${pageData.templates[pageVerTmplSeq].page_ver_tmpl_name}_${pageDataTrack?.click_item}`,
|
||||||
event: pageData.page_name,
|
event: pageData.page_name,
|
||||||
eventCategory: `${pageData.page_name}_${pageDataTrack?.click_item}`,
|
eventCategory: `${pageData.page_name}_${pageDataTrack?.click_item}`,
|
||||||
template_code: pageData.templates[pageVerTmplSeq].template_code,
|
template_code: pageData.templates[pageVerTmplSeq].template_code,
|
||||||
page_ver_tmpl_name: pageData.templates[pageVerTmplSeq].page_ver_tmpl_name,
|
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)
|
} as unknown as AnalyticsDetailType)
|
||||||
|
|
||||||
return logData
|
return logData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
/**
|
/**
|
||||||
* 페이지 데이터와 템플릿 정보를 기반으로 분석용 로그 데이터를 생성하는 composable (직접 객체 반환)
|
* 페이지 데이터와 템플릿 정보를 기반으로 분석용 로그 데이터를 생성하는 composable (직접 객체 반환)
|
||||||
* @param resourcesData 페이지 리소스 데이터
|
* @param resourcesData 페이지 리소스 데이터
|
||||||
@@ -82,10 +90,15 @@ export const useAnalyticsLogDataDirect = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
=======
|
||||||
|
>>>>>>> feature/20250910-all
|
||||||
// target에 {XX1, XX2}와 같은 형태가 포함되어 있을 경우 options.clickItem으로부터 값 추출하여 세팅
|
// target에 {XX1, XX2}와 같은 형태가 포함되어 있을 경우 options.clickItem으로부터 값 추출하여 세팅
|
||||||
const findValueFromOption = (target: string, { options = {} }: any) => {
|
const findValueFromOption = (target: string, { options = {} }: any) => {
|
||||||
if (target.includes('{') && target.includes('}')) {
|
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 arrTargetClickItem = strTargetClickItem.split(',')
|
||||||
const arrTargetClickItemValue = []
|
const arrTargetClickItemValue = []
|
||||||
@@ -94,7 +107,10 @@ const findValueFromOption = (target: string, { options = {} }: any) => {
|
|||||||
targetClickItem = targetClickItem.trim()
|
targetClickItem = targetClickItem.trim()
|
||||||
arrTargetClickItemValue.push(options.clickItem[targetClickItem])
|
arrTargetClickItemValue.push(options.clickItem[targetClickItem])
|
||||||
}
|
}
|
||||||
target = target.replaceAll(`{${strTargetClickItem}}`, arrTargetClickItemValue.join(','))
|
target = target.replaceAll(
|
||||||
|
`{${strTargetClickItem}}`,
|
||||||
|
arrTargetClickItemValue.join(',')
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return target
|
return target
|
||||||
}
|
}
|
||||||
@@ -106,6 +122,10 @@ const findValueFromOption = (target: string, { options = {} }: any) => {
|
|||||||
* @param {object} options
|
* @param {object} options
|
||||||
*/
|
*/
|
||||||
const sendGA = (analytics: AnalyticsDetailType, { options = {} }: any) => {
|
const sendGA = (analytics: AnalyticsDetailType, { options = {} }: any) => {
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
console.log('🚀 ~ 1111 sendGA ~ analytics:', analytics)
|
||||||
|
>>>>>>> feature/20250910-all
|
||||||
try {
|
try {
|
||||||
const { gtag } = useGtag()
|
const { gtag } = useGtag()
|
||||||
|
|
||||||
@@ -119,7 +139,7 @@ const sendGA = (analytics: AnalyticsDetailType, { options = {} }: any) => {
|
|||||||
gtag('set', 'cookie_domain', `${window?.location?.hostname || ''}`) // env 값으로 설정 시 쿠키 생성 안 돼서 window.location.hostname으로 설정
|
gtag('set', 'cookie_domain', `${window?.location?.hostname || ''}`) // env 값으로 설정 시 쿠키 생성 안 돼서 window.location.hostname으로 설정
|
||||||
gtag('set', 'cookie_expires', '0') // 0으로 설정 시 쿠키가 Session 기반 쿠키로 전환
|
gtag('set', 'cookie_expires', '0') // 0으로 설정 시 쿠키가 Session 기반 쿠키로 전환
|
||||||
gtag('event', `${eventName}`, {
|
gtag('event', `${eventName}`, {
|
||||||
event_category: eventLabel
|
event_category: eventLabel,
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[Exception] useAnalytics.sendGA: ', e)
|
console.error('[Exception] useAnalytics.sendGA: ', e)
|
||||||
@@ -133,8 +153,15 @@ const sendGA = (analytics: AnalyticsDetailType, { options = {} }: any) => {
|
|||||||
* @param {string} mcode
|
* @param {string} mcode
|
||||||
* @param {object} options
|
* @param {object} options
|
||||||
*/
|
*/
|
||||||
|
<<<<<<< HEAD
|
||||||
const sendSA = (analytics: AnalyticsDetailType, { mcode = '', options: _options = {} }: any) => {
|
const sendSA = (analytics: AnalyticsDetailType, { mcode = '', options: _options = {} }: any) => {
|
||||||
console.log("🚀 ~44444 sendSA ~ analytics:", analytics)
|
console.log("🚀 ~44444 sendSA ~ analytics:", analytics)
|
||||||
|
=======
|
||||||
|
const sendSA = (
|
||||||
|
analytics: AnalyticsDetailType,
|
||||||
|
{ mcode = '', options = {} }: any
|
||||||
|
) => {
|
||||||
|
>>>>>>> feature/20250910-all
|
||||||
const gameDataStore = useGameDataStore()
|
const gameDataStore = useGameDataStore()
|
||||||
const { gameData } = storeToRefs(gameDataStore)
|
const { gameData } = storeToRefs(gameDataStore)
|
||||||
|
|
||||||
@@ -152,7 +179,13 @@ const sendSA = (analytics: AnalyticsDetailType, { mcode = '', options: _options
|
|||||||
const viewArea = analytics.viewArea || ''
|
const viewArea = analytics.viewArea || ''
|
||||||
const viewType = analytics.viewType || ''
|
const viewType = analytics.viewType || ''
|
||||||
const clickArea = analytics.clickArea || ''
|
const clickArea = analytics.clickArea || ''
|
||||||
|
<<<<<<< HEAD
|
||||||
const clickSarea = findValueFromOption(analytics.clickSarea || '', { _options })
|
const clickSarea = findValueFromOption(analytics.clickSarea || '', { _options })
|
||||||
|
=======
|
||||||
|
const clickSarea = findValueFromOption(analytics.clickSarea || '', {
|
||||||
|
options,
|
||||||
|
})
|
||||||
|
>>>>>>> feature/20250910-all
|
||||||
const eventLocale = analytics.eventLocale || ''
|
const eventLocale = analytics.eventLocale || ''
|
||||||
|
|
||||||
const identityInfo: IdentityInfo = {
|
const identityInfo: IdentityInfo = {
|
||||||
@@ -162,14 +195,14 @@ const sendSA = (analytics: AnalyticsDetailType, { mcode = '', options: _options
|
|||||||
locale: eventLocale,
|
locale: eventLocale,
|
||||||
lang_cd: eventLocale,
|
lang_cd: eventLocale,
|
||||||
member_no: memberNo,
|
member_no: memberNo,
|
||||||
channeling_cd: 'SO'
|
channeling_cd: 'SO',
|
||||||
}
|
}
|
||||||
|
|
||||||
const marketingInfo: MarketingInfo = {
|
const marketingInfo: MarketingInfo = {
|
||||||
marketing_code: mcode || '',
|
marketing_code: mcode || '',
|
||||||
device_type: deviceType,
|
device_type: deviceType,
|
||||||
media_type: '',
|
media_type: '',
|
||||||
media_page: ''
|
media_page: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
let actionParam = {}
|
let actionParam = {}
|
||||||
@@ -181,8 +214,13 @@ const sendSA = (analytics: AnalyticsDetailType, { mcode = '', options: _options
|
|||||||
view_info: {
|
view_info: {
|
||||||
game_no: gameNo,
|
game_no: gameNo,
|
||||||
lang_cd: eventLocale,
|
lang_cd: eventLocale,
|
||||||
|
<<<<<<< HEAD
|
||||||
..._options?.viewInfo
|
..._options?.viewInfo
|
||||||
}
|
}
|
||||||
|
=======
|
||||||
|
...options?.viewInfo,
|
||||||
|
},
|
||||||
|
>>>>>>> feature/20250910-all
|
||||||
}
|
}
|
||||||
} else if (actionType === 'click') {
|
} else if (actionType === 'click') {
|
||||||
actionParam = {
|
actionParam = {
|
||||||
@@ -192,21 +230,26 @@ const sendSA = (analytics: AnalyticsDetailType, { mcode = '', options: _options
|
|||||||
click_item: analytics.clickItem,
|
click_item: analytics.clickItem,
|
||||||
game_no: gameNo,
|
game_no: gameNo,
|
||||||
lang_cd: eventLocale,
|
lang_cd: eventLocale,
|
||||||
|
<<<<<<< HEAD
|
||||||
..._options?.clickItem
|
..._options?.clickItem
|
||||||
}
|
}
|
||||||
|
=======
|
||||||
|
...options?.clickItem,
|
||||||
|
},
|
||||||
|
>>>>>>> feature/20250910-all
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const actionInfo: ActionInfo = {
|
const actionInfo: ActionInfo = {
|
||||||
action_type: actionType,
|
action_type: actionType,
|
||||||
action_param: actionParam,
|
action_param: actionParam,
|
||||||
marketing_info: marketingInfo
|
marketing_info: marketingInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
const amplitudeActionInfo = {
|
const amplitudeActionInfo = {
|
||||||
...actionInfo,
|
...actionInfo,
|
||||||
url: `${location?.href || ''}`,
|
url: `${location?.href || ''}`,
|
||||||
agent: `${navigator?.userAgent || ''}`
|
agent: `${navigator?.userAgent || ''}`,
|
||||||
}
|
}
|
||||||
|
|
||||||
const amplitudeActionParams: {
|
const amplitudeActionParams: {
|
||||||
@@ -214,7 +257,7 @@ const sendSA = (analytics: AnalyticsDetailType, { mcode = '', options: _options
|
|||||||
event_properties: ActionInfo & { url: string; agent: string }
|
event_properties: ActionInfo & { url: string; agent: string }
|
||||||
} = {
|
} = {
|
||||||
event_type: actionType,
|
event_type: actionType,
|
||||||
event_properties: amplitudeActionInfo
|
event_properties: amplitudeActionInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
svcLog.identity(identityInfo)
|
svcLog.identity(identityInfo)
|
||||||
@@ -232,8 +275,11 @@ const sendSA = (analytics: AnalyticsDetailType, { mcode = '', options: _options
|
|||||||
* @param {AnalyticsDetailType} analytics
|
* @param {AnalyticsDetailType} analytics
|
||||||
*/
|
*/
|
||||||
const sendLog = (locale: string, analytics: AnalyticsDetailType) => {
|
const sendLog = (locale: string, analytics: AnalyticsDetailType) => {
|
||||||
|
<<<<<<< HEAD
|
||||||
console.log("🚀 ~33333 sendLog ~ analytics:", analytics)
|
console.log("🚀 ~33333 sendLog ~ analytics:", analytics)
|
||||||
|
=======
|
||||||
|
console.log('🚀 ~ sendLog ~ analytics:', analytics)
|
||||||
|
>>>>>>> feature/20250910-all
|
||||||
|
|
||||||
// 언어 코드 대문자 변환
|
// 언어 코드 대문자 변환
|
||||||
analytics.eventLocale = locale.toUpperCase()
|
analytics.eventLocale = locale.toUpperCase()
|
||||||
@@ -327,7 +373,7 @@ const sendMarketingScript = ({
|
|||||||
gaEventName,
|
gaEventName,
|
||||||
fbEventName,
|
fbEventName,
|
||||||
twEventName,
|
twEventName,
|
||||||
ttEventName
|
ttEventName,
|
||||||
}: {
|
}: {
|
||||||
gaEventName?: string
|
gaEventName?: string
|
||||||
fbEventName?: string
|
fbEventName?: string
|
||||||
|
|||||||
191
layers/composables/useGameStart.ts
Normal file
191
layers/composables/useGameStart.ts
Normal 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, // 런처 실행 로딩 종료 함수
|
||||||
|
}
|
||||||
|
}
|
||||||
30
layers/composables/useTokenValidation.ts
Normal file
30
layers/composables/useTokenValidation.ts
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,19 +2,23 @@ import { defineStore } from 'pinia'
|
|||||||
import { useWindowScroll } from '@vueuse/core'
|
import { useWindowScroll } from '@vueuse/core'
|
||||||
|
|
||||||
export const useScrollStore = defineStore('scrollStore', () => {
|
export const useScrollStore = defineStore('scrollStore', () => {
|
||||||
const { x: windowX, y: windowY } = useWindowScroll({ behavior: 'smooth' })
|
const { y: windowY } = useWindowScroll({ behavior: 'smooth' })
|
||||||
|
|
||||||
const stoveGnbHeight = 48 as number
|
const stoveGnbHeight = 48 as number
|
||||||
const scrollXValue = ref('0px')
|
|
||||||
const isPassedStoveGnb = ref(false)
|
const isPassedStoveGnb = ref(false)
|
||||||
|
const scrollGnbPosition = ref(stoveGnbHeight)
|
||||||
|
|
||||||
const updateScrollValue = () => {
|
const updateScrollValue = () => {
|
||||||
if (stoveGnbHeight <= windowY.value) {
|
if (stoveGnbHeight <= windowY.value) {
|
||||||
isPassedStoveGnb.value = true
|
isPassedStoveGnb.value = true
|
||||||
scrollXValue.value = `-${windowX.value}px`
|
scrollGnbPosition.value = 0
|
||||||
} else {
|
} else {
|
||||||
isPassedStoveGnb.value = false
|
isPassedStoveGnb.value = false
|
||||||
scrollXValue.value = '0px'
|
if (windowY.value === 0) {
|
||||||
|
scrollGnbPosition.value = stoveGnbHeight
|
||||||
|
} else {
|
||||||
|
scrollGnbPosition.value = stoveGnbHeight - windowY.value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,8 +32,8 @@ export const useScrollStore = defineStore('scrollStore', () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
stoveGnbHeight,
|
stoveGnbHeight,
|
||||||
scrollXValue,
|
|
||||||
isPassedStoveGnb,
|
isPassedStoveGnb,
|
||||||
|
scrollGnbPosition,
|
||||||
|
|
||||||
updateScrollValue,
|
updateScrollValue,
|
||||||
controlScrollLock,
|
controlScrollLock,
|
||||||
|
|||||||
10
layers/types/Common.ts
Normal file
10
layers/types/Common.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* Stove 81 Plug 타입 정의
|
||||||
|
*/
|
||||||
export interface IdentityInfo {
|
export interface IdentityInfo {
|
||||||
app_id: string
|
app_id: string
|
||||||
log_source_type: string
|
log_source_type: string
|
||||||
@@ -36,3 +39,10 @@ export interface ActionInfo {
|
|||||||
action_param: ActionParam
|
action_param: ActionParam
|
||||||
marketing_info?: MarketingInfo
|
marketing_info?: MarketingInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stove JS Service 타입 정의
|
||||||
|
*/
|
||||||
|
export interface StoveJsService {
|
||||||
|
launcher?: any
|
||||||
|
}
|
||||||
@@ -36,12 +36,23 @@ export interface PageDataValue {
|
|||||||
lnb_text_color_code_active: string
|
lnb_text_color_code_active: string
|
||||||
lnb_text_color_code_deactive: string
|
lnb_text_color_code_deactive: string
|
||||||
lnb_menus: PageDataLnbMenu[]
|
lnb_menus: PageDataLnbMenu[]
|
||||||
templates: Record<string, PageDataTemplate>
|
|
||||||
meta_tag: PageDataMetaTag
|
meta_tag: PageDataMetaTag
|
||||||
|
templates: Record<string, PageDataTemplate>
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 세부 데이터 타입들 =====
|
// ===== 세부 데이터 타입들 =====
|
||||||
|
|
||||||
|
// LNB 메뉴 타입
|
||||||
|
export interface PageDataLnbMenu {
|
||||||
|
path_code: string
|
||||||
|
depth: number
|
||||||
|
sort_order: number
|
||||||
|
menu_name: string
|
||||||
|
target_type: number
|
||||||
|
page_ver_tmpl_name_en: string
|
||||||
|
tracking: string
|
||||||
|
}
|
||||||
|
|
||||||
// 메타 태그 타입
|
// 메타 태그 타입
|
||||||
export interface PageDataMetaTag {
|
export interface PageDataMetaTag {
|
||||||
x_desc: string
|
x_desc: string
|
||||||
@@ -54,7 +65,14 @@ export interface PageDataMetaTag {
|
|||||||
page_title: string
|
page_title: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 리소스 그룹 타입
|
export type PageDataResourceGroupType =
|
||||||
|
| 'TXT'
|
||||||
|
| 'BTN'
|
||||||
|
| 'VID'
|
||||||
|
| 'IMG_COMM'
|
||||||
|
| 'IMG_LANG'
|
||||||
|
| 'IMG_COMM_GLOBAL'
|
||||||
|
|
||||||
export interface PageDataResourceGroupResPath {
|
export interface PageDataResourceGroupResPath {
|
||||||
path_mo: string
|
path_mo: string
|
||||||
path_pc?: string
|
path_pc?: string
|
||||||
@@ -72,8 +90,7 @@ export interface PageDataResourceGroupBtnInfo {
|
|||||||
|
|
||||||
// 리소스 그룹 타입
|
// 리소스 그룹 타입
|
||||||
export interface PageDataResourceGroup {
|
export interface PageDataResourceGroup {
|
||||||
group_type?: string
|
resource_type?: PageDataResourceGroupType
|
||||||
group_code?: string
|
|
||||||
res_path?: PageDataResourceGroupResPath
|
res_path?: PageDataResourceGroupResPath
|
||||||
btn_info?: PageDataResourceGroupBtnInfo
|
btn_info?: PageDataResourceGroupBtnInfo
|
||||||
display?: {
|
display?: {
|
||||||
@@ -81,29 +98,27 @@ export interface PageDataResourceGroup {
|
|||||||
color_code?: string
|
color_code?: string
|
||||||
color_name?: string
|
color_name?: string
|
||||||
}
|
}
|
||||||
tracking: string // JSON 문자열
|
tracking: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PageDataResourceGroups = PageDataResourceGroup[]
|
||||||
|
|
||||||
// 컴포넌트 타입
|
// 컴포넌트 타입
|
||||||
export interface PageDataComponent {
|
export interface PageDataComponent {
|
||||||
groups: PageDataResourceGroup[]
|
groups: PageDataResourceGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
// 템플릿 컴포넌트 타입 - 두 가지 패턴 지원
|
export type PageDataTemplateComponent = Record<string, PageDataComponent>
|
||||||
|
|
||||||
|
// 그룹 세트 아이템 타입
|
||||||
|
export type PageDataTemplateComponentSet = PageDataTemplateComponent & {
|
||||||
|
set_order?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 템플릿 컴포넌트 타입 - 두 가지 패턴
|
||||||
export type PageDataTemplateComponents =
|
export type PageDataTemplateComponents =
|
||||||
| Record<string, PageDataComponent> // 직접 PageDataComponent가 들어있는 패턴
|
| PageDataTemplateComponent // 단일 컴포넌트 패턴
|
||||||
| { group_sets: Record<string, any> } // group_sets 안에 PageDataComponent가 들어있는 패턴
|
| { group_sets: PageDataTemplateComponentSet[] } // 그룹 세트 패턴
|
||||||
|
|
||||||
// LNB 메뉴 타입
|
|
||||||
export interface PageDataLnbMenu {
|
|
||||||
path_code: string
|
|
||||||
depth: number
|
|
||||||
sort_order: number
|
|
||||||
menu_name: string
|
|
||||||
target_type: number
|
|
||||||
page_ver_tmpl_name_en: string
|
|
||||||
tracking: string // JSON string
|
|
||||||
}
|
|
||||||
|
|
||||||
// 템플릿 타입
|
// 템플릿 타입
|
||||||
export interface PageDataTemplate {
|
export interface PageDataTemplate {
|
||||||
|
|||||||
@@ -55,9 +55,14 @@ const buildRequestOptions = (
|
|||||||
url: string,
|
url: string,
|
||||||
options: FetchOptions
|
options: FetchOptions
|
||||||
): FetchRequestOptions => {
|
): FetchRequestOptions => {
|
||||||
const runtimeConfig = useRuntimeConfig()
|
let stoveGameId = ''
|
||||||
const callerId = runtimeConfig.public.stoveGameId || ''
|
let callerDetail = ''
|
||||||
const callerDetail = useCookie('sgs_da_uuid').value || ''
|
|
||||||
|
if (import.meta.client) {
|
||||||
|
const gameDataStore = useGameDataStore()
|
||||||
|
stoveGameId = gameDataStore.gameData?.game_id || ''
|
||||||
|
callerDetail = useCookie('sgs_da_uuid').value || ''
|
||||||
|
}
|
||||||
|
|
||||||
const requestOptions: FetchRequestOptions = {
|
const requestOptions: FetchRequestOptions = {
|
||||||
method,
|
method,
|
||||||
@@ -75,7 +80,7 @@ const buildRequestOptions = (
|
|||||||
if (url.includes('.onstove.com') || url.includes('.gate8.com')) {
|
if (url.includes('.onstove.com') || url.includes('.gate8.com')) {
|
||||||
requestOptions.headers = {
|
requestOptions.headers = {
|
||||||
...requestOptions.headers,
|
...requestOptions.headers,
|
||||||
'Caller-Id': callerId as string,
|
'Caller-Id': stoveGameId as string,
|
||||||
'Caller-Detail': callerDetail as string,
|
'Caller-Detail': callerDetail as string,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import type {
|
import type {
|
||||||
PageDataValue,
|
PageDataValue,
|
||||||
PageDataComponent,
|
PageDataComponent,
|
||||||
|
PageDataResourceGroupType,
|
||||||
} from '#layers/types/api/pageData'
|
} from '#layers/types/api/pageData'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,6 +20,44 @@ export const getLayoutType = (
|
|||||||
return pageData?.page_type === 1 ? 'default' : 'promotion'
|
return pageData?.page_type === 1 ? 'default' : 'promotion'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 타입인지 확인합니다.
|
||||||
|
* @param type 리소스 그룹 타입
|
||||||
|
* @returns 이미지 타입 여부
|
||||||
|
*/
|
||||||
|
export const isTypeImage = (type: PageDataResourceGroupType): boolean => {
|
||||||
|
return (
|
||||||
|
type === 'IMG_COMM' || type === 'IMG_LANG' || type === 'IMG_COMM_GLOBAL'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 비디오 타입인지 확인합니다.
|
||||||
|
* @param type 리소스 그룹 타입
|
||||||
|
* @returns 비디오 타입 여부
|
||||||
|
*/
|
||||||
|
export const isTypeVideo = (type: PageDataResourceGroupType): boolean => {
|
||||||
|
return type === 'VID'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 텍스트 타입인지 확인합니다.
|
||||||
|
* @param type 리소스 그룹 타입
|
||||||
|
* @returns 텍스트 타입 여부
|
||||||
|
*/
|
||||||
|
export const isTypeText = (type: PageDataResourceGroupType): boolean => {
|
||||||
|
return type === 'TXT'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 버튼 타입인지 확인합니다.
|
||||||
|
* @param type 리소스 그룹 타입
|
||||||
|
* @returns 버튼 타입 여부
|
||||||
|
*/
|
||||||
|
export const isTypeButton = (type: PageDataResourceGroupType): boolean => {
|
||||||
|
return type === 'BTN'
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 그룹의 첫 번째 데이터를 반환합니다.
|
* 그룹의 첫 번째 데이터를 반환합니다.
|
||||||
* @param source props.components 또는 group 객체
|
* @param source props.components 또는 group 객체
|
||||||
|
|||||||
@@ -94,61 +94,6 @@ export const formatTimestamp = (
|
|||||||
.replace('ss', seconds)
|
.replace('ss', seconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 타임스탬프를 상대적 시간으로 변환합니다 (예: "3일 전", "2시간 전")
|
|
||||||
* @param timestamp 타임스탬프 (밀리초 또는 초)
|
|
||||||
* @param locale 로케일 (기본값: 'ko-KR')
|
|
||||||
* @returns 상대적 시간 문자열
|
|
||||||
*/
|
|
||||||
export const formatRelativeTime = (
|
|
||||||
timestamp: number | string,
|
|
||||||
locale: string = 'ko-KR'
|
|
||||||
): string => {
|
|
||||||
if (!timestamp) return ''
|
|
||||||
|
|
||||||
let ts = typeof timestamp === 'string' ? parseInt(timestamp) : timestamp
|
|
||||||
if (ts < 10000000000) {
|
|
||||||
ts = ts * 1000
|
|
||||||
}
|
|
||||||
|
|
||||||
const date = new Date(ts)
|
|
||||||
const now = new Date()
|
|
||||||
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000)
|
|
||||||
|
|
||||||
if (diffInSeconds < 60) {
|
|
||||||
return locale === 'ko-KR' ? '방금 전' : 'just now'
|
|
||||||
}
|
|
||||||
|
|
||||||
const diffInMinutes = Math.floor(diffInSeconds / 60)
|
|
||||||
if (diffInMinutes < 60) {
|
|
||||||
return locale === 'ko-KR'
|
|
||||||
? `${diffInMinutes}분 전`
|
|
||||||
: `${diffInMinutes} minutes ago`
|
|
||||||
}
|
|
||||||
|
|
||||||
const diffInHours = Math.floor(diffInMinutes / 60)
|
|
||||||
if (diffInHours < 24) {
|
|
||||||
return locale === 'ko-KR'
|
|
||||||
? `${diffInHours}시간 전`
|
|
||||||
: `${diffInHours} hours ago`
|
|
||||||
}
|
|
||||||
|
|
||||||
const diffInDays = Math.floor(diffInHours / 24)
|
|
||||||
if (diffInDays < 30) {
|
|
||||||
return locale === 'ko-KR' ? `${diffInDays}일 전` : `${diffInDays} days ago`
|
|
||||||
}
|
|
||||||
|
|
||||||
const diffInMonths = Math.floor(diffInDays / 30)
|
|
||||||
if (diffInMonths < 12) {
|
|
||||||
return locale === 'ko-KR'
|
|
||||||
? `${diffInMonths}개월 전`
|
|
||||||
: `${diffInMonths} months ago`
|
|
||||||
}
|
|
||||||
|
|
||||||
const diffInYears = Math.floor(diffInMonths / 12)
|
|
||||||
return locale === 'ko-KR' ? `${diffInYears}년 전` : `${diffInYears} years ago`
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 배열 또는 객체를 배열로 변환합니다.
|
* 배열 또는 객체를 배열로 변환합니다.
|
||||||
* @param value 변환할 값 (배열, 객체, 또는 undefined/null)
|
* @param value 변환할 값 (배열, 객체, 또는 undefined/null)
|
||||||
|
|||||||
@@ -10,10 +10,11 @@ import { csrFormatJWT } from '#layers/utils/formatUtil'
|
|||||||
*/
|
*/
|
||||||
export const csrGoStoveLogin = () => {
|
export const csrGoStoveLogin = () => {
|
||||||
const runtimeConfig = useRuntimeConfig()
|
const runtimeConfig = useRuntimeConfig()
|
||||||
|
const gameDataStore = useGameDataStore()
|
||||||
|
|
||||||
const loginUrl = runtimeConfig.public.stoveLoginUrl
|
const loginUrl = runtimeConfig.public.stoveLoginUrl
|
||||||
const stoveGameId = runtimeConfig.public.stoveGameId
|
const stoveGameId = gameDataStore.gameData?.game_id
|
||||||
const stoveGameNo = runtimeConfig.public.stoveGameNo
|
const stoveGameNo = gameDataStore.gameData?.game_code
|
||||||
const redirectUrl = encodeURIComponent(location.href)
|
const redirectUrl = encodeURIComponent(location.href)
|
||||||
|
|
||||||
const url = `${loginUrl}?redirect_url=${redirectUrl}&inflow_path=${stoveGameId}&game_no=${stoveGameNo}`
|
const url = `${loginUrl}?redirect_url=${redirectUrl}&inflow_path=${stoveGameId}&game_no=${stoveGameNo}`
|
||||||
|
|||||||
@@ -3,6 +3,12 @@
|
|||||||
* @description 유튜브 관련 유틸리티 함수를 제공합니다.
|
* @description 유튜브 관련 유틸리티 함수를 제공합니다.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { getFirstGroup, isTypeVideo } from '#layers/utils/dataUtil'
|
||||||
|
import type {
|
||||||
|
PageDataResourceGroups,
|
||||||
|
PageDataResourceGroup,
|
||||||
|
} from '#layers/types/api/pageData'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 유튜브 URL에서 비디오 ID를 추출합니다.
|
* 유튜브 URL에서 비디오 ID를 추출합니다.
|
||||||
* @param url - 유튜브 URL (watch, embed, youtu.be 등 다양한 형태 지원)
|
* @param url - 유튜브 URL (watch, embed, youtu.be 등 다양한 형태 지원)
|
||||||
@@ -95,11 +101,10 @@ export const getYouTubeThumbnailFromUrl = (
|
|||||||
* @param source - 미디어 소스 객체
|
* @param source - 미디어 소스 객체
|
||||||
* @returns 미디어 text(src)
|
* @returns 미디어 text(src)
|
||||||
*/
|
*/
|
||||||
export const getMediaText = (source: Record<string, any>): string => {
|
export const getMediaSrc = (resourceGroup: PageDataResourceGroup): string => {
|
||||||
if (!source) return ''
|
if (!resourceGroup) return ''
|
||||||
const resource = source.groups?.[0]
|
const mediaSrc = resourceGroup?.display?.text
|
||||||
const mediaUrl = resource?.display?.text
|
return mediaSrc || ''
|
||||||
return mediaUrl || ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -109,42 +114,20 @@ export const getMediaText = (source: Record<string, any>): string => {
|
|||||||
* @returns 미디어 이미지 소스
|
* @returns 미디어 이미지 소스
|
||||||
*/
|
*/
|
||||||
export const getMediaImgSrc = (
|
export const getMediaImgSrc = (
|
||||||
source: Record<string, any>,
|
resourceGroups: PageDataResourceGroups,
|
||||||
quality: 'default' | 'medium' | 'high' | 'standard' | 'maxres' = 'high'
|
quality: 'default' | 'medium' | 'high' | 'standard' | 'maxres' = 'high'
|
||||||
): string => {
|
): string => {
|
||||||
if (!source) return ''
|
if (!resourceGroups) return ''
|
||||||
|
|
||||||
const resource = source.groups?.[0]
|
const resourceGroup = getFirstGroup(resourceGroups)
|
||||||
const mediaType = resource?.group_type
|
const mediaSrc = getMediaSrc(resourceGroup)
|
||||||
const mediaUrl = resource?.display?.text
|
const mediaType = resourceGroup?.resource_type
|
||||||
|
|
||||||
if (mediaType === 'video' && mediaUrl) {
|
if (isTypeVideo(mediaType) && mediaSrc) {
|
||||||
const videoId = getYouTubeVideoId(mediaUrl)
|
const videoId = getYouTubeVideoId(mediaSrc)
|
||||||
const thumbnailUrl = getYouTubeThumbnail(videoId, quality)
|
const thumbnailUrl = getYouTubeThumbnail(videoId, quality)
|
||||||
return thumbnailUrl
|
return thumbnailUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
return mediaUrl || ''
|
return mediaSrc || ''
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 미디어 타입을 확인합니다.
|
|
||||||
* @param source - 미디어 소스 객체
|
|
||||||
* @returns 미디어 타입 ('video' | 'image' | '')
|
|
||||||
*/
|
|
||||||
export const getMediaType = (source: Record<string, any>): string => {
|
|
||||||
if (!source) return ''
|
|
||||||
|
|
||||||
const resource = source.groups?.[0]
|
|
||||||
const mediaType = resource?.group_type
|
|
||||||
return mediaType || ''
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 비디오 아이템인지 확인합니다.
|
|
||||||
* @param source - 미디어 소스 객체
|
|
||||||
* @returns 비디오 여부
|
|
||||||
*/
|
|
||||||
export const isVideoItem = (source: Record<string, any>): boolean => {
|
|
||||||
return getMediaType(source) === 'video'
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ export default {
|
|||||||
'theme-foreground-reversal-8': 'var(--foreground-reversal-8)',
|
'theme-foreground-reversal-8': 'var(--foreground-reversal-8)',
|
||||||
'theme-foreground-reversal-10': 'var(--foreground-reversal-10)',
|
'theme-foreground-reversal-10': 'var(--foreground-reversal-10)',
|
||||||
'theme-foreground-reversal-15': 'var(--foreground-reversal-15)',
|
'theme-foreground-reversal-15': 'var(--foreground-reversal-15)',
|
||||||
'theme-foreground-reversal-30': 'var(--foreground-reversal-30)',
|
|
||||||
|
'theme-foreground-gray-750': 'var(--foreground-gray-750)',
|
||||||
|
'theme-foreground-gray-500': 'var(--foreground-gray-500)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user