fix. 개발 도구 적용. (typescript, prettier, es-lint)

This commit is contained in:
clkim
2025-09-16 13:01:17 +09:00
parent be15192e59
commit 2c07ff4fce
65 changed files with 6849 additions and 2548 deletions

14
.eslintignore Normal file
View File

@@ -0,0 +1,14 @@
temp/
**/temp/**
node_modules/
.nuxt/
.output/
dist/
build/
coverage/
.nyc_output/
*.log
*.tsbuildinfo
.env
.env.*
.DS_Store

2
.nvmrc
View File

@@ -1 +1 @@
echo "20" > .nvmrc
v22.11.0

22
.prettierignore Normal file
View File

@@ -0,0 +1,22 @@
# Ignore artifacts
.nuxt
.output
dist
build
coverage
.nyc_output
# Ignore dependencies
node_modules
# Ignore logs
*.log
*.tsbuildinfo
# Ignore environment files
.env
.env.*
# Ignore OS files
.DS_Store

14
.prettierrc Normal file
View File

@@ -0,0 +1,14 @@
{
"semi": false,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "avoid",
"endOfLine": "lf",
"vueIndentScriptAndStyle": false,
"htmlWhitespaceSensitivity": "ignore"
}

View File

@@ -8,39 +8,33 @@
</NuxtLayout>
</template>
<style>
@import "#layers/assets/css/app.css";
</style>
<script setup lang="ts">
import { useNuxtApp } from "nuxt/app";
import { useGameDataStore } from "#layers/stores/useGameDataStore";
import type {
gameData,
GameDataMetaTag,
GameDataValue,
} from "#layers/types/api/gameData";
import { useNuxtApp } from 'nuxt/app'
import MoleculesLoadingFull from '#layers/components/molecules/loading/Full.vue'
import MoleculesLoadingLocal from '#layers/components/molecules/loading/Local.vue'
import { useGameDataStore } from '#layers/stores/useGameDataStore'
import type { GameDataMetaTag, GameDataValue } from '#layers/types/api/gameData'
const nuxtApp = useNuxtApp();
const getGameData = ref<GameDataValue | null>(null);
const metaData = ref<GameDataMetaTag | null>(null);
const { setGameData } = useGameDataStore();
const nuxtApp = useNuxtApp()
const getGameData = ref<GameDataValue | null>(null)
const metaData = ref<GameDataMetaTag | null>(null)
const { setGameData } = useGameDataStore()
// SSR에서만 접근 가능
const gameDataFromServer = import.meta.server
? nuxtApp.ssrContext?.event.context.gameData
: null;
: null
if (gameDataFromServer) {
getGameData.value = gameDataFromServer;
setGameData(gameDataFromServer);
getGameData.value = gameDataFromServer
setGameData(gameDataFromServer)
}
const meta = gameDataFromServer?.meta_tag ?? null;
const theme = gameDataFromServer?.design_theme === 1 ? "dark" : "light";
const meta = gameDataFromServer?.meta_tag ?? null
const theme = gameDataFromServer?.design_theme === 1 ? 'dark' : 'light'
if (gameDataFromServer && meta) {
metaData.value = meta;
metaData.value = meta
useSeoMeta({
title: meta.page_title,
description: meta.page_desc,
@@ -51,15 +45,19 @@ if (gameDataFromServer && meta) {
twitterImage: meta.x_image,
twitterTitle: meta.x_title,
twitterDescription: meta.x_desc,
});
})
useHead({
htmlAttrs: {
"data-game": gameDataFromServer.game_name || "",
"data-theme": theme || "",
'data-game': gameDataFromServer.game_name || '',
'data-theme': theme || '',
lang: gameDataFromServer.default_lang_code,
},
//meta: [...(updatedMetaTags.value || [])],
//link: [...(links.value || [])]
});
})
}
</script>
<style>
@import '#layers/assets/css/app.css';
</style>

View File

@@ -1,34 +1,34 @@
<script setup lang="ts">
import { storeToRefs } from "pinia";
import { usePageDataStore } from "#layers/stores/usePageDataStore";
import Template from "#layers/components/molecules/Template.vue";
import { storeToRefs } from 'pinia'
import { usePageDataStore } from '#layers/stores/usePageDataStore'
import Template from '#layers/components/molecules/Template.vue'
const pageDataStore = usePageDataStore();
const { pageData } = storeToRefs(pageDataStore);
const pageDataStore = usePageDataStore()
const { pageData } = storeToRefs(pageDataStore)
// const layout = pageData.value?.meta?.layout ?? "default";
const layout = "default"; // 기본 레이아웃 사용
const layout = 'default' // 기본 레이아웃 사용
// definePageMeta를 사용하여 레이아웃을 미리 설정
definePageMeta({
layout: false, // 기본 레이아웃 비활성화
});
})
// definePageMeta를 사용하여 레이아웃을 미리 설정
watchEffect(() => {
if (pageData.value?.meta_tag) {
useSeoMeta({
title: pageData.value.meta_tag.page_title ?? "",
description: pageData.value.meta_tag.page_desc ?? "",
ogTitle: pageData.value.meta_tag.og_title ?? "",
ogDescription: pageData.value.meta_tag.og_desc ?? "",
ogImage: pageData.value.meta_tag.og_image ?? "",
twitterTitle: pageData.value.meta_tag.x_title ?? "",
twitterImage: pageData.value.meta_tag.x_image ?? "",
twitterDescription: pageData.value.meta_tag.x_desc ?? "",
});
title: pageData.value.meta_tag.page_title ?? '',
description: pageData.value.meta_tag.page_desc ?? '',
ogTitle: pageData.value.meta_tag.og_title ?? '',
ogDescription: pageData.value.meta_tag.og_desc ?? '',
ogImage: pageData.value.meta_tag.og_image ?? '',
twitterTitle: pageData.value.meta_tag.x_title ?? '',
twitterImage: pageData.value.meta_tag.x_image ?? '',
twitterDescription: pageData.value.meta_tag.x_desc ?? '',
})
}
});
})
</script>
<template>

View File

@@ -1,34 +1,34 @@
<script setup lang="ts">
import { storeToRefs } from "pinia";
import { usePageDataStore } from "#layers/stores/usePageDataStore";
import Template from "#layers/components/molecules/Template.vue";
import { storeToRefs } from 'pinia'
import { usePageDataStore } from '#layers/stores/usePageDataStore'
import Template from '#layers/components/molecules/Template.vue'
const pageDataStore = usePageDataStore();
const { pageData } = storeToRefs(pageDataStore);
const pageDataStore = usePageDataStore()
const { pageData } = storeToRefs(pageDataStore)
// const layout = pageData.value?.meta?.layout ?? "default";
const layout = "default"; // 기본 레이아웃 사용
const layout = 'default' // 기본 레이아웃 사용
// definePageMeta를 사용하여 레이아웃을 미리 설정
definePageMeta({
layout: false, // 기본 레이아웃 비활성화
});
})
// definePageMeta를 사용하여 레이아웃을 미리 설정
watchEffect(() => {
if (pageData.value?.meta_tag) {
useSeoMeta({
title: pageData.value.meta_tag.page_title ?? "",
description: pageData.value.meta_tag.page_desc ?? "",
ogTitle: pageData.value.meta_tag.og_title ?? "",
ogDescription: pageData.value.meta_tag.og_desc ?? "",
ogImage: pageData.value.meta_tag.og_image ?? "",
twitterTitle: pageData.value.meta_tag.x_title ?? "",
twitterImage: pageData.value.meta_tag.x_image ?? "",
twitterDescription: pageData.value.meta_tag.x_desc ?? "",
});
title: pageData.value.meta_tag.page_title ?? '',
description: pageData.value.meta_tag.page_desc ?? '',
ogTitle: pageData.value.meta_tag.og_title ?? '',
ogDescription: pageData.value.meta_tag.og_desc ?? '',
ogImage: pageData.value.meta_tag.og_image ?? '',
twitterTitle: pageData.value.meta_tag.x_title ?? '',
twitterImage: pageData.value.meta_tag.x_image ?? '',
twitterDescription: pageData.value.meta_tag.x_desc ?? '',
})
}
});
})
</script>
<template>

View File

@@ -1,34 +1,34 @@
<script setup lang="ts">
import { storeToRefs } from "pinia";
import { usePageDataStore } from "#layers/stores/usePageDataStore";
import Template from "#layers/components/molecules/Template.vue";
import { storeToRefs } from 'pinia'
import { usePageDataStore } from '#layers/stores/usePageDataStore'
import Template from '#layers/components/molecules/Template.vue'
const pageDataStore = usePageDataStore();
const { pageData } = storeToRefs(pageDataStore);
const pageDataStore = usePageDataStore()
const { pageData } = storeToRefs(pageDataStore)
// const layout = pageData.value?.meta?.layout ?? "default";
const layout = "default"; // 기본 레이아웃 사용
const layout = 'default' // 기본 레이아웃 사용
// definePageMeta를 사용하여 레이아웃을 미리 설정
definePageMeta({
layout: false, // 기본 레이아웃 비활성화
});
})
// definePageMeta를 사용하여 레이아웃을 미리 설정
watchEffect(() => {
if (pageData.value?.meta_tag) {
useSeoMeta({
title: pageData.value.meta_tag.page_title ?? "",
description: pageData.value.meta_tag.page_desc ?? "",
ogTitle: pageData.value.meta_tag.og_title ?? "",
ogDescription: pageData.value.meta_tag.og_desc ?? "",
ogImage: pageData.value.meta_tag.og_image ?? "",
twitterTitle: pageData.value.meta_tag.x_title ?? "",
twitterImage: pageData.value.meta_tag.x_image ?? "",
twitterDescription: pageData.value.meta_tag.x_desc ?? "",
});
title: pageData.value.meta_tag.page_title ?? '',
description: pageData.value.meta_tag.page_desc ?? '',
ogTitle: pageData.value.meta_tag.og_title ?? '',
ogDescription: pageData.value.meta_tag.og_desc ?? '',
ogImage: pageData.value.meta_tag.og_image ?? '',
twitterTitle: pageData.value.meta_tag.x_title ?? '',
twitterImage: pageData.value.meta_tag.x_image ?? '',
twitterDescription: pageData.value.meta_tag.x_desc ?? '',
})
}
});
})
</script>
<template>

View File

@@ -1,21 +1,21 @@
<script setup lang="ts">
import { storeToRefs } from "pinia";
import { usePageDataStore } from "#layers/stores/usePageDataStore";
import Template from "#layers/components/molecules/Template.vue";
import { storeToRefs } from 'pinia'
import { usePageDataStore } from '#layers/stores/usePageDataStore'
import Template from '#layers/components/molecules/Template.vue'
const pageDataStore = usePageDataStore();
const { pageData } = storeToRefs(pageDataStore);
const pageDataStore = usePageDataStore()
const { pageData } = storeToRefs(pageDataStore)
// 동적 i18n 라우트 설정
// const { getI18nRouteConfig } = useDynamicI18nRoutes();
// const layout = pageData.value?.meta?.layout ?? "default";
const layout = "default"; // 기본 레이아웃 사용
const layout = 'default' // 기본 레이아웃 사용
// definePageMeta를 사용하여 레이아웃을 미리 설정
definePageMeta({
layout: false, // 기본 레이아웃 비활성화
});
})
// // gameData.lang_codes를 기반으로 동적 언어 제외 설정
// const i18nRouteConfig = getI18nRouteConfig();
@@ -27,17 +27,17 @@ definePageMeta({
watchEffect(() => {
if (pageData.value?.meta_tag) {
useSeoMeta({
title: pageData.value.meta_tag.page_title ?? "",
description: pageData.value.meta_tag.page_desc ?? "",
ogTitle: pageData.value.meta_tag.og_title ?? "",
ogDescription: pageData.value.meta_tag.og_desc ?? "",
ogImage: pageData.value.meta_tag.og_image ?? "",
twitterTitle: pageData.value.meta_tag.x_title ?? "",
twitterImage: pageData.value.meta_tag.x_image ?? "",
twitterDescription: pageData.value.meta_tag.x_desc ?? "",
});
title: pageData.value.meta_tag.page_title ?? '',
description: pageData.value.meta_tag.page_desc ?? '',
ogTitle: pageData.value.meta_tag.og_title ?? '',
ogDescription: pageData.value.meta_tag.og_desc ?? '',
ogImage: pageData.value.meta_tag.og_image ?? '',
twitterTitle: pageData.value.meta_tag.x_title ?? '',
twitterImage: pageData.value.meta_tag.x_image ?? '',
twitterDescription: pageData.value.meta_tag.x_desc ?? '',
})
}
});
})
</script>
<template>

49
eslint.config.js Normal file
View File

@@ -0,0 +1,49 @@
import { createConfigForNuxt } from '@nuxt/eslint-config/flat'
export default createConfigForNuxt({
features: {
typescript: true,
vue: true,
},
dirs: {
src: ['layers', 'app'],
},
ignores: ['temp/**/*'],
}).append({
rules: {
// 포맷팅 관련 규칙 비활성화 (Prettier가 담당)
'prettier/prettier': 'off',
'@typescript-eslint/indent': 'off',
'@typescript-eslint/quotes': 'off',
'@typescript-eslint/semi': 'off',
'@typescript-eslint/comma-dangle': 'off',
'@typescript-eslint/brace-style': 'off',
'@typescript-eslint/space-before-function-paren': 'off',
// 타입 관련 규칙 완화
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
// Vue 관련 규칙 완화
'vue/multi-word-component-names': 'off',
'vue/no-v-html': 'off',
'vue/require-default-prop': 'off',
'vue/require-explicit-emits': 'off',
'vue/no-multiple-template-root': 'off',
'vue/no-required-prop-with-default': 'off',
'vue/require-directive': 'off',
// 일반 규칙 (품질/버그 탐지)
'no-console': 'warn',
'no-debugger': 'error',
'no-unused-vars': 'off', // TypeScript 버전 사용
'prefer-const': 'warn',
'no-var': 'error',
},
})

View File

@@ -1,63 +1,63 @@
import type { LocaleObject, NuxtI18nOptions } from "@nuxtjs/i18n";
import type { LocaleObject, NuxtI18nOptions } from '@nuxtjs/i18n'
const LANG_DIR: string = "./i18n/locales";
// const LANG_DIR: string = './i18n/locales'
const DEFAULT_COVERAGES: string[] = [
"en",
"ja",
"ko",
"zh-tw",
"fr",
"de",
"es",
"pt",
"th",
"zh-cn",
];
'en',
'ja',
'ko',
'zh-tw',
'fr',
'de',
'es',
'pt',
'th',
'zh-cn',
]
const DEFAULT_LOCALES: Record<string, LocaleObject> = {
en: { code: "en", name: "English", iso: "en", dir: "ltr" },
"zh-tw": { code: "zh-tw", name: "繁體中文", iso: "zh-tw", dir: "ltr" },
ja: { code: "ja", name: "日本語", iso: "ja", dir: "ltr" },
ko: { code: "ko", name: "한국어", iso: "ko-KR", dir: "ltr" },
fr: { code: "fr", name: "Français", iso: "fr", dir: "ltr" },
de: { code: "de", name: "Deutsch", iso: "de", dir: "ltr" },
es: { code: "es", name: "Español", iso: "es", dir: "ltr" },
pt: { code: "pt", name: "Português", iso: "pt", dir: "ltr" },
th: { code: "th", name: "ภาษาไทย", iso: "th", dir: "ltr" },
"zh-cn": { code: "zh-cn", name: "简体中文", iso: "zh-cn", dir: "ltr" },
};
const DEFAULT_LOCALE_CODE = "ko";
en: { code: 'en', name: 'English', iso: 'en', dir: 'ltr' },
'zh-tw': { code: 'zh-tw', name: '繁體中文', iso: 'zh-tw', dir: 'ltr' },
ja: { code: 'ja', name: '日本語', iso: 'ja', dir: 'ltr' },
ko: { code: 'ko', name: '한국어', iso: 'ko-KR', dir: 'ltr' },
fr: { code: 'fr', name: 'Français', iso: 'fr', dir: 'ltr' },
de: { code: 'de', name: 'Deutsch', iso: 'de', dir: 'ltr' },
es: { code: 'es', name: 'Español', iso: 'es', dir: 'ltr' },
pt: { code: 'pt', name: 'Português', iso: 'pt', dir: 'ltr' },
th: { code: 'th', name: 'ภาษาไทย', iso: 'th', dir: 'ltr' },
'zh-cn': { code: 'zh-cn', name: '简体中文', iso: 'zh-cn', dir: 'ltr' },
}
const DEFAULT_LOCALE_CODE = 'ko'
// getI18n 함수가 NuxtI18nOptions 타입의 값을 반환하도록 명시적으로 타입을 지정합니다.
const getI18n = (allowedLangCodes?: string[]): NuxtI18nOptions => {
// allowedLangCodes가 제공되면 해당 언어들만 사용, 그렇지 않으면 모든 기본 언어 사용
const targetCoverages =
allowedLangCodes && allowedLangCodes.length > 0
? DEFAULT_COVERAGES.filter((code) => allowedLangCodes.includes(code))
: DEFAULT_COVERAGES;
? DEFAULT_COVERAGES.filter(code => allowedLangCodes.includes(code))
: DEFAULT_COVERAGES
const DEFAULT_LOCALE_COVERAGES_SET: LocaleObject[] = targetCoverages.map(
(code: string): LocaleObject => ({
code,
code => ({
code: code as LocaleObject['code'],
file: `${code}.ts`,
// 아래의 옵셔널 체이닝(?.)은 해당 코드가 undefined일 경우를 안전하게 처리합니다.
name: DEFAULT_LOCALES[code]?.name ?? code,
iso: DEFAULT_LOCALES[code]?.iso ?? code,
// dir 속성은 모든 로케일 객체에 공통적으로 존재하므로, 여기서 추가할 수도 있습니다.
dir: DEFAULT_LOCALES[code]?.dir ?? "ltr",
dir: DEFAULT_LOCALES[code]?.dir ?? 'ltr',
})
);
)
return {
strategy: "prefix",
vueI18n: "custom",
strategy: 'prefix',
vueI18n: 'custom',
locales: DEFAULT_LOCALE_COVERAGES_SET,
defaultLocale: DEFAULT_LOCALE_CODE || "ko",
defaultLocale: DEFAULT_LOCALE_CODE || 'ko',
detectBrowserLanguage: {
fallbackLocale: DEFAULT_LOCALE_CODE || "ko",
fallbackLocale: DEFAULT_LOCALE_CODE || 'ko',
useCookie: false,
redirectOn: "root",
redirectOn: 'root',
},
compilation: {
strictMessage: false,
@@ -65,30 +65,30 @@ const getI18n = (allowedLangCodes?: string[]): NuxtI18nOptions => {
},
debug: false,
// 동적으로 언어 제외 설정을 위한 pages 설정
customRoutes: "config",
customRoutes: 'config',
pages:
allowedLangCodes && allowedLangCodes.length > 0
? generatePageExclusions(allowedLangCodes)
: {},
// 추가적인 설정이 필요하다면 여기에 포함시킬 수 있습니다.
};
};
}
}
// gameData.lang_codes를 기반으로 언어 제외 설정을 생성하는 함수
const generatePageExclusions = (
allowedLangCodes: string[]
): Record<string, any> => {
const exclusions: Record<string, any> = {};
const exclusions: Record<string, any> = {}
// 모든 기본 언어에 대해 제외 설정 생성
DEFAULT_COVERAGES.forEach((langCode) => {
DEFAULT_COVERAGES.forEach(langCode => {
if (!allowedLangCodes.includes(langCode)) {
// 해당 언어가 허용되지 않으면 모든 페이지에서 제외
exclusions[langCode] = false;
exclusions[langCode] = false
}
});
})
return exclusions;
};
return exclusions
}
export { DEFAULT_LOCALE_CODE, DEFAULT_COVERAGES, getI18n };
export { DEFAULT_LOCALE_CODE, DEFAULT_COVERAGES, getI18n }

View File

@@ -1,5 +1,5 @@
@import "./base/_theme.css";
@import "./base/_reset.css";
@import './base/_theme.css';
@import './base/_reset.css';
@tailwind base;
@tailwind components;

View File

@@ -20,7 +20,7 @@
}
/* 다크 테마 색상 */
[data-theme="dark"] {
[data-theme='dark'] {
--foreground: #191919;
--foreground-10: #292929;

View File

@@ -1,32 +1,32 @@
<script setup lang="ts">
interface Props {
size?: "large" | "medium" | "small" | "extra-small";
disabled?: boolean;
icon?: string;
size?: 'large' | 'medium' | 'small' | 'extra-small'
disabled?: boolean
icon?: string
}
const props = withDefaults(defineProps<Props>(), {
size: "medium",
size: 'medium',
disabled: false,
icon: "",
});
icon: '',
})
// 크기별 스타일 클래스
const sizeClasses = {
large: "px-10 h-16 text-lg rounded-lg",
medium: "px-10 h-14 text-base rounded-lg",
small: "px-10 h-12 text-sm rounded-lg",
"extra-small": "px-6 h-10 text-sm rounded",
};
large: 'px-10 h-16 text-lg rounded-lg',
medium: 'px-10 h-14 text-base rounded-lg',
small: 'px-10 h-12 text-sm rounded-lg',
'extra-small': 'px-6 h-10 text-sm rounded',
}
// 상태별 스타일 클래스
const getStateClasses = (disabled: boolean) => {
if (disabled) {
return "bg-white/10 text-gray-400 cursor-not-allowed";
return 'bg-white/10 text-gray-400 cursor-not-allowed'
}
return "bg-gray-700 text-white cursor-pointer";
};
return 'bg-gray-700 text-white cursor-pointer'
}
</script>
<template>
@@ -41,7 +41,7 @@ const getStateClasses = (disabled: boolean) => {
<span
v-if="!disabled"
class="absolute inset-0 bg-white opacity-0 group-hover:opacity-20 transition-opacity duration-200 rounded-inherit"
></span>
/>
<span class="relative">
<slot />
<span v-if="icon" :class="['flex-shrink-0']" v-html="icon" />

View File

@@ -2,14 +2,13 @@
const props = defineProps({
to: {
type: String,
default: null,
required: true,
},
});
})
const localePath = useLocalePath();
const localePath = useLocalePath()
</script>
<template>
<NuxtLink :to="localePath(props.to)"><slot></slot></NuxtLink>
<NuxtLink :to="localePath(props.to)"><slot /></NuxtLink>
</template>

View File

@@ -1,15 +1,15 @@
<script setup lang="ts">
interface Props {
size?: number | string;
color?: string;
className?: string;
size?: number | string
color?: string
className?: string
}
withDefaults(defineProps<Props>(), {
size: 12,
color: "#7F7F7F",
className: "",
});
color: '#7F7F7F',
className: '',
})
</script>
<template>

View File

@@ -1,15 +1,15 @@
<script setup lang="ts">
interface Props {
size?: number | string;
color?: string;
className?: string;
size?: number | string
color?: string
className?: string
}
withDefaults(defineProps<Props>(), {
size: 16,
color: "#B2B2B2",
className: "",
});
color: '#B2B2B2',
className: '',
})
</script>
<template>

View File

@@ -1,15 +1,15 @@
<script setup lang="ts">
interface Props {
size?: number | string;
color?: string;
className?: string;
size?: number | string
color?: string
className?: string
}
withDefaults(defineProps<Props>(), {
size: 12,
color: "#FD3886",
className: "",
});
color: '#FD3886',
className: '',
})
</script>
<template>

View File

@@ -1,34 +1,34 @@
<template>
<div id="header-stove" class="relative z-[5]"></div>
<div id="header-stove" class="relative z-[5]" />
</template>
<script setup lang="ts">
const runtimeConfig = useRuntimeConfig();
const { locale } = useI18n();
const { $i18n } = useNuxtApp();
const { gameData } = useGameDataStore();
import { useGameDataStore } from '#layers/stores/useGameDataStore'
const stoveInflowPath = runtimeConfig.public.stoveInflowPath;
const stoveGameNo = runtimeConfig.public.stoveGameNo;
const gnbData = gameData?.stove_gnb;
const runtimeConfig = useRuntimeConfig()
const { locale, availableLocales } = useI18n()
const { gameData } = useGameDataStore()
const stoveInflowPath = runtimeConfig.public.stoveInflowPath
const stoveGameNo = runtimeConfig.public.stoveGameNo
const gnbData = gameData?.stove_gnb
const languageCodes = computed(() => {
const availableLocales = ($i18n as any).availableLocales;
if (Array.isArray(availableLocales)) {
return availableLocales.map(
(localeCode: any) => localeCode.code || localeCode
);
)
}
return [locale.value];
});
return [locale]
})
function loadGnb(locale: string) {
locale = locale.toLowerCase();
locale = locale.toLowerCase()
const gnbOption = {
wrapper: "#header-stove",
wrapper: '#header-stove',
isResponsive: true,
skin: gnbData?.skin_type || "gnb-dark-mini",
skin: gnbData?.skin_type || 'gnb-dark-mini',
widget: {
gameListAndService: false,
languageSelect: false,
@@ -37,25 +37,25 @@ function loadGnb(locale: string) {
},
global: {
userGds: true,
defaultSelectedLanguage: locale || "en",
defaultSelectedLanguage: locale || 'en',
languageCoverages: languageCodes.value,
},
loginMethod: {
params: {
inflow_path: stoveInflowPath,
game_no: stoveGameNo,
show_play_button: gnbData?.stove_install_button_visible || "Y",
show_play_button: gnbData?.stove_install_button_visible || 'Y',
},
redirectCurrentPage: true,
windowTitle: undefined,
},
};
}
const cpHeader = new (window as any).cp.Header(gnbOption);
cpHeader.render();
const cpHeader = new (window as any).cp.Header(gnbOption)
cpHeader.render()
}
onMounted(() => {
loadGnb(locale.value);
});
loadGnb(locale.value)
})
</script>

View File

@@ -1,10 +1,11 @@
<script setup lang="ts">
import type { GameDataValue, GameDataGnb } from "#layers/types/api/gameData";
import { useGameDataStore } from '#layers/stores/useGameDataStore'
import type { GameDataValue, GameDataGnb } from '#layers/types/api/gameData'
const gameDataStore = useGameDataStore();
const gameDataStore = useGameDataStore()
const gameData = gameDataStore.gameData as GameDataValue;
const gnbList = gameData?.gnb?.menus as GameDataGnb["menus"];
const gameData = gameDataStore.gameData as GameDataValue
const gnbList = gameData?.gnb?.menus as GameDataGnb['menus']
</script>
<template>
@@ -45,7 +46,7 @@ const gnbList = gameData?.gnb?.menus as GameDataGnb["menus"];
<AtomsIconsArrowDown v-if="gnbItem.children" class="ml-1" />
<span
class="absolute bottom-0 left-0 w-full h-2 border-b-2 border-transparent transition-border-color group-hover:border-theme-foreground-reversal group-active:border-theme-foreground-reversal-10"
></span>
/>
</MoleculesHybridLink>
<div
v-if="gnbItem.children"
@@ -74,7 +75,7 @@ const gnbList = gameData?.gnb?.menus as GameDataGnb["menus"];
</template>
<!-- 구분선 -->
<div class="w-px h-4 bg-theme-foreground-reversal-30"></div>
<div class="w-px h-4 bg-theme-foreground-reversal-30" />
<!-- 이벤트 -->
<a href="#" class="flex items-center space-x-[3px] text-gradient-pink">

View File

@@ -1,14 +1,14 @@
<script setup lang="ts">
interface Props {
to: string;
target?: string;
class?: string;
to: string
target?: string
class?: string
}
const props = withDefaults(defineProps<Props>(), {
target: "",
class: "",
});
target: '',
class: '',
})
</script>
<template>

View File

@@ -1,8 +1,8 @@
<template>
<div class="bg-white">
<select
class="text-black px-2 py-1 rounded-md"
v-model="selectedLocale"
class="text-black px-2 py-1 rounded-md"
@change="switchLanguage"
>
<option
@@ -17,36 +17,36 @@
</template>
<script setup lang="ts">
const gameDataStore = useGameDataStore();
const gameDataStore = useGameDataStore()
// 사용 가능한 언어 목록
const availableLanguages = computed(() => {
return gameDataStore.gameData?.lang_codes || ["ko"];
});
return gameDataStore.gameData?.lang_codes || ['ko']
})
const { locale } = useI18n();
const switchLocalePath = useSwitchLocalePath();
const router = useRouter();
const { locale } = useI18n()
const switchLocalePath = useSwitchLocalePath()
const router = useRouter()
const selectedLocale = ref(locale.value);
const selectedLocale = ref(locale.value)
// 언어 변경 함수
const switchLanguage = async () => {
console.log(
"🚀 ~ switchLanguage ~ selectedLocale.value:",
'🚀 ~ switchLanguage ~ selectedLocale.value:',
selectedLocale.value
);
)
if (selectedLocale.value) {
// URL 경로를 통해 언어 변경
const path = switchLocalePath(selectedLocale.value);
const path = switchLocalePath(selectedLocale.value)
if (path) {
await router.push(path);
await router.push(path)
}
}
};
}
// locale이 변경될 때 selectedLocale도 동기화
watch(locale, (newLocale) => {
selectedLocale.value = newLocale;
});
watch(locale, newLocale => {
selectedLocale.value = newLocale
})
</script>

View File

@@ -1,13 +1,13 @@
<script setup lang="ts">
import { useTemplateRegistry } from "#layers/composables/useTemplateRegistry";
import type { PageDataTemplate } from "#layers/types/api/pageData";
import { useTemplateRegistry } from '#layers/composables/useTemplateRegistry'
import type { PageDataTemplate } from '#layers/types/api/pageData'
const props = defineProps<{ templates: PageDataTemplate[] }>();
const registry = useTemplateRegistry() as Record<string, { component: any }>;
const props = defineProps<{ templates: PageDataTemplate[] }>()
const registry = useTemplateRegistry() as Record<string, { component: any }>
const isShowTemplate = (template: PageDataTemplate) => {
return template?.components && Object.keys(template.components).length > 0;
};
return template?.components && Object.keys(template.components).length > 0
}
</script>
<template>
@@ -16,8 +16,8 @@ const isShowTemplate = (template: PageDataTemplate) => {
:key="template.template_code ?? index"
>
<component
v-if="isShowTemplate(template)"
:is="registry[template.template_code]?.component"
v-if="isShowTemplate(template)"
:components="template.components"
/>
</template>

View File

@@ -1,12 +1,12 @@
<script setup lang="ts">
interface Props {
tag: string;
text: string;
imageSrc?: any;
imageClass?: string;
tag: string
text: string
imageSrc?: any
imageClass?: string
}
const props = defineProps<Props>();
const props = defineProps<Props>()
</script>
<template>

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import { useLoadingStore } from "#layers/stores/useLoadingStore";
import { useLoadingStore } from '#layers/stores/useLoadingStore'
const loadingStore = useLoadingStore();
const { fullLoading } = storeToRefs(loadingStore);
const loadingStore = useLoadingStore()
const { fullLoading } = storeToRefs(loadingStore)
</script>
<template>
@@ -23,22 +23,22 @@ const { fullLoading } = storeToRefs(loadingStore);
<!-- 외부 -->
<div
class="absolute inset-0 border-4 border-transparent border-t-blue-500 rounded-full animate-spin"
></div>
/>
<!-- 중간 -->
<div
class="absolute inset-1 border-4 border-transparent border-t-purple-500 rounded-full animate-spin"
style="animation-delay: -0.3s"
></div>
/>
<!-- 내부 -->
<div
class="absolute inset-2 border-4 border-transparent border-t-cyan-500 rounded-full animate-spin"
style="animation-delay: -0.6s"
></div>
/>
<!-- 중심 -->
<div
class="absolute inset-3 border-4 border-transparent border-t-emerald-500 rounded-full animate-spin"
style="animation-delay: -0.9s"
></div>
/>
</div>
</div>
</Transition>

View File

@@ -1,8 +1,14 @@
<script setup lang="ts">
import { useLoadingStore } from "#layers/stores/useLoadingStore";
import { useLoadingStore } from '#layers/stores/useLoadingStore'
const loadingStore = useLoadingStore();
const { localLoadings } = storeToRefs(loadingStore);
const loadingStore = useLoadingStore()
const localLoadings = computed(() => Object.entries(loadingStore.localLoadings))
const canTeleport = (localId: string) => {
if (!import.meta.client) {
return false
}
return !!document.getElementById(localId)
}
</script>
<template>
@@ -25,22 +31,22 @@ const { localLoadings } = storeToRefs(loadingStore);
<!-- 외부 -->
<div
class="absolute inset-0 border-4 border-transparent border-t-blue-500 rounded-full animate-spin"
></div>
/>
<!-- 중간 -->
<div
class="absolute inset-1 border-4 border-transparent border-t-purple-500 rounded-full animate-spin"
style="animation-delay: -0.3s"
></div>
/>
<!-- 내부 -->
<div
class="absolute inset-2 border-4 border-transparent border-t-cyan-500 rounded-full animate-spin"
style="animation-delay: -0.6s"
></div>
/>
<!-- 중심 -->
<div
class="absolute inset-3 border-4 border-transparent border-t-emerald-500 rounded-full animate-spin"
style="animation-delay: -0.9s"
></div>
/>
</div>
</div>
</Transition>

View File

@@ -21,17 +21,13 @@
leave-from-class="opacity-100 scale-100"
leave-to-class="opacity-0 scale-95"
>
<div
v-if="isOpen"
class="relative w-full max-w-4xl mx-4"
@click.stop
>
<div v-if="isOpen" class="relative w-full max-w-4xl mx-4" @click.stop>
<!-- 헤더 -->
<div class="flex justify-end">
<button
@click="closeModal"
class="p-1 text-white rounded-full transition-colors"
aria-label="모달 닫기"
@click="closeModal"
>
<svg
class="w-8 h-8"
@@ -85,7 +81,7 @@ interface Emits {
const props = withDefaults(defineProps<Props>(), {
title: '',
description: '',
closeOnBackdrop: true
closeOnBackdrop: true,
})
const emit = defineEmits<Emits>()
@@ -120,13 +116,16 @@ onUnmounted(() => {
})
// 모달이 열릴 때 body 스크롤 방지
watch(() => props.isOpen, (isOpen) => {
if (isOpen) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = ''
watch(
() => props.isOpen,
isOpen => {
if (isOpen) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = ''
}
}
})
)
// 컴포넌트 언마운트 시 body 스크롤 복원
onUnmounted(() => {

View File

@@ -3,34 +3,34 @@ import {
getResourcesData,
getResponsiveClass,
getResponsiveSrc,
} from "#layers/utils/dataUtil";
import type { PageDataComponent } from "#layers/types/api/pageData";
} from '#layers/utils/dataUtil'
import type { PageDataComponent } from '#layers/types/api/pageData'
const props = defineProps<{
componentData: PageDataComponent;
gradientClass?: string;
groupSets?: boolean;
}>();
componentData: PageDataComponent
gradientClass?: string
groupSets?: boolean
}>()
const resourcesData = computed(() => {
return getResourcesData({
resources: props.componentData?.resources,
groupSets: props.groupSets,
});
});
})
})
const bgStyles = computed(() => {
return getResponsiveSrc(resourcesData.value?.res_path, {
resourcesType: "bg",
});
});
resourcesType: 'bg',
})
})
const videoSrc = computed(() => {
return getResponsiveSrc(resourcesData.value?.res_path, {
resourcesType: "video",
});
});
resourcesType: 'video',
})
})
const posterSrc = computed(() => {
return getResponsiveSrc(resourcesData.value?.res_path);
});
return getResponsiveSrc(resourcesData.value?.res_path)
})
</script>
<template>
@@ -41,7 +41,7 @@ const posterSrc = computed(() => {
class="w-full h-full bg-cover bg-center bg-no-repeat"
:class="getResponsiveClass()"
:style="bgStyles"
></div>
/>
<!-- 비디오 타입 -->
<template v-else-if="resourcesData?.group_type === 'video'">
@@ -73,6 +73,6 @@ const posterSrc = computed(() => {
</video>
</template>
<div class="absolute inset-0" :class="gradientClass"></div>
<div class="absolute inset-0" :class="gradientClass" />
</div>
</template>

View File

@@ -1,30 +1,24 @@
<script setup lang="ts">
import { getResourcesData } from "#layers/utils/dataUtil";
import type { PageDataComponent } from "#layers/types/api/pageData";
import { getResourcesData } from '#layers/utils/dataUtil'
import type { PageDataComponent } from '#layers/types/api/pageData'
const props = defineProps<{
componentData: PageDataComponent;
groupSets?: boolean;
}>();
componentData: PageDataComponent
groupSets?: boolean
}>()
const resourcesData = computed(() => {
return getResourcesData({
resources: props.componentData?.resources,
isMultiple: true,
groupSets: props.groupSets,
});
});
console.log("ButtonList resourcesData:", resourcesData.value);
})
})
</script>
<template>
<template
v-if="resourcesData"
v-for="button in resourcesData"
:key="button.group_label"
>
<AtomsButton>
<template v-if="resourcesData">
<AtomsButton v-for="button in resourcesData" :key="button.group_label">
{{ button.btn_info?.txt_btn_name }}
</AtomsButton>

View File

@@ -1,21 +1,21 @@
<script setup lang="ts">
import { getResourcesData, getResponsiveSrc } from "#layers/utils/dataUtil";
import type { PageDataComponent } from "#layers/types/api/pageData";
import { getResourcesData, getResponsiveSrc } from '#layers/utils/dataUtil'
import type { PageDataComponent } from '#layers/types/api/pageData'
const props = defineProps<{
componentData: PageDataComponent;
groupSets?: boolean;
}>();
componentData: PageDataComponent
groupSets?: boolean
}>()
const resourcesData = computed(() => {
return getResourcesData({
resources: props.componentData?.resources,
groupSets: props.groupSets,
});
});
})
})
const displayText = resourcesData.value?.display?.txt;
const imageSrc = getResponsiveSrc(resourcesData.value?.res_path);
const displayText = resourcesData.value?.display?.txt
const imageSrc = getResponsiveSrc(resourcesData.value?.res_path)
</script>
<template>

View File

@@ -1,21 +1,21 @@
<script setup lang="ts">
import { getResourcesData, getResponsiveSrc } from "#layers/utils/dataUtil";
import type { PageDataComponent } from "#layers/types/api/pageData";
import { getResourcesData, getResponsiveSrc } from '#layers/utils/dataUtil'
import type { PageDataComponent } from '#layers/types/api/pageData'
const props = defineProps<{
componentData: PageDataComponent;
groupSets?: boolean;
}>();
componentData: PageDataComponent
groupSets?: boolean
}>()
const resourcesData = computed(() => {
return getResourcesData({
resources: props.componentData?.resources,
groupSets: props.groupSets,
});
});
})
})
const displayText = resourcesData.value?.display?.txt;
const imageSrc = getResponsiveSrc(resourcesData.value?.res_path);
const displayText = resourcesData.value?.display?.txt
const imageSrc = getResponsiveSrc(resourcesData.value?.res_path)
</script>
<template>

View File

@@ -1,21 +1,21 @@
<script setup lang="ts">
import { getResourcesData, getResponsiveSrc } from "#layers/utils/dataUtil";
import type { PageDataComponent } from "#layers/types/api/pageData";
import { getResourcesData, getResponsiveSrc } from '#layers/utils/dataUtil'
import type { PageDataComponent } from '#layers/types/api/pageData'
const props = defineProps<{
componentData: PageDataComponent;
groupSets?: boolean;
}>();
componentData: PageDataComponent
groupSets?: boolean
}>()
const resourcesData = computed(() => {
return getResourcesData({
resources: props.componentData?.resources,
groupSets: props.groupSets,
});
});
})
})
const displayText = resourcesData.value?.display?.txt;
const imageSrc = getResponsiveSrc(resourcesData.value?.res_path);
const displayText = resourcesData.value?.display?.txt
const imageSrc = getResponsiveSrc(resourcesData.value?.res_path)
</script>
<template>

View File

@@ -3,19 +3,19 @@ import {
getResourcesData,
getResponsiveSrc,
getResponsiveClass,
} from "#layers/utils/dataUtil";
import type { PageDataComponent } from "#layers/types/api/pageData";
} from '#layers/utils/dataUtil'
import type { PageDataComponent } from '#layers/types/api/pageData'
const props = defineProps<{ componentData: PageDataComponent }>();
const props = defineProps<{ componentData: PageDataComponent }>()
const resourcesData = computed(() => {
return getResourcesData({
resources: props.componentData?.resources,
});
});
})
})
const bgStyles = getResponsiveSrc(resourcesData.value?.res_path, {
resourcesType: "bg",
});
resourcesType: 'bg',
})
</script>
<template>

View File

@@ -6,15 +6,20 @@ import { DEFAULT_COVERAGES } from '@/i18n.config'
*/
export const useDynamicI18nRoutes = () => {
const gameDataStore = useGameDataStore()
/**
* 현재 gameData의 lang_codes를 기반으로 허용된 언어 목록을 반환
*/
const getAllowedLangCodes = (): string[] => {
const gameData = gameDataStore.gameData
return gameData?.lang_codes || []
// Ensure we only return string values and filter out undefined/null
return Array.isArray(gameData?.lang_codes)
? gameData.lang_codes.filter(
(code): code is string => typeof code === 'string'
)
: []
}
/**
* 특정 언어가 허용되는지 확인
*/
@@ -22,7 +27,7 @@ export const useDynamicI18nRoutes = () => {
const allowedLangCodes = getAllowedLangCodes()
return allowedLangCodes.length === 0 || allowedLangCodes.includes(langCode)
}
/**
* defineI18nRoute에서 사용할 수 있는 언어 제외 설정을 생성
* @param pagePath - 페이지 경로 (선택사항)
@@ -30,18 +35,18 @@ export const useDynamicI18nRoutes = () => {
*/
const getI18nRouteConfig = (pagePath?: string) => {
const allowedLangCodes = getAllowedLangCodes()
// 허용된 언어가 없으면 모든 언어 허용
if (allowedLangCodes.length === 0) {
return undefined
}
// 허용된 언어만 포함하는 설정 반환
return {
locales: allowedLangCodes
locales: allowedLangCodes,
}
}
/**
* 특정 언어를 제외하는 설정을 생성
* @param excludedLangCodes - 제외할 언어 코드 배열
@@ -49,21 +54,33 @@ export const useDynamicI18nRoutes = () => {
*/
const getExcludedLangConfig = (excludedLangCodes: string[]) => {
const allowedLangCodes = getAllowedLangCodes()
// 허용된 언어에서 제외할 언어를 제거
const finalAllowedCodes = allowedLangCodes.length > 0
? allowedLangCodes.filter(code => !excludedLangCodes.includes(code))
: ['en', 'ja', 'ko', 'zh-tw', 'fr', 'de', 'es', 'pt', 'th', 'zh-cn'].filter(code => !excludedLangCodes.includes(code))
const finalAllowedCodes =
allowedLangCodes.length > 0
? allowedLangCodes.filter(code => !excludedLangCodes.includes(code))
: [
'en',
'ja',
'ko',
'zh-tw',
'fr',
'de',
'es',
'pt',
'th',
'zh-cn',
].filter(code => !excludedLangCodes.includes(code))
return {
locales: finalAllowedCodes
locales: finalAllowedCodes,
}
}
return {
getAllowedLangCodes,
isLangAllowed,
getI18nRouteConfig,
getExcludedLangConfig
getExcludedLangConfig,
}
}

View File

@@ -1,53 +1,53 @@
import { getHeader } from "h3";
import { useRuntimeConfig, useRequestEvent } from "nuxt/app";
import { getHeader } from 'h3'
import { useRuntimeConfig, useRequestEvent } from 'nuxt/app'
export const useGetGameAlias = () => {
const config = useRuntimeConfig();
const baseDomain = (config.public.baseDomain || ".onstove.com") as string;
const config = useRuntimeConfig()
const baseDomain = (config.public.baseDomain || '.onstove.com') as string
// 서버 사이드에서 실행되는 경우
if (!import.meta.client) {
try {
const event = useRequestEvent();
const event = useRequestEvent()
if (event) {
// 미들웨어에서 설정한 gameAlias가 있다면 우선 사용
if (event.context.gameAlias) {
return event.context.gameAlias;
return event.context.gameAlias
}
const host = getHeader(event, "host") || "";
const isGameAliasExtractable = host.includes(baseDomain);
const host = getHeader(event, 'host') || ''
const isGameAliasExtractable = host.includes(baseDomain)
if (isGameAliasExtractable) {
const subdomain = host.split(".")[0];
const subdomain = host.split('.')[0]
if (subdomain && subdomain !== "www") {
return subdomain;
if (subdomain && subdomain !== 'www') {
return subdomain
}
}
}
} catch (error) {
console.error("useGetGameAlias server error: ", error);
console.error('useGetGameAlias server error: ', error)
}
}
// 클라이언트 사이드에서 실행되는 경우
if (import.meta.client) {
try {
const host = window.location.host;
const isGameAliasExtractable = host.includes(baseDomain);
const host = window.location.host
const isGameAliasExtractable = host.includes(baseDomain)
if (isGameAliasExtractable) {
const subdomain = host.split(".")[0];
const subdomain = host.split('.')[0]
if (subdomain && subdomain !== "www") {
return subdomain;
if (subdomain && subdomain !== 'www') {
return subdomain
}
}
} catch (error) {
console.error("useGetGameAlias client error: ", error);
console.error('useGetGameAlias client error: ', error)
}
}
return "";
};
return ''
}

View File

@@ -2,38 +2,38 @@ export const usePathResolver = () => {
const getPathAfterLanguage = (url?: string): string => {
// URL이 제공되지 않으면 현재 URL 사용
const targetUrl =
url || (import.meta.client ? window.location.pathname : "");
url || (import.meta.client ? window.location.pathname : '')
// URL에서 언어 코드 패턴을 찾아서 그 뒤의 경로를 추출
// 예: /ko/about/story -> /about/story
// 예: /en/test/page -> /test/page
// 예: /ko -> "" (빈 문자열)
const languagePattern = /^\/[a-z]{2}\/(.+)$/;
const match = targetUrl.match(languagePattern);
const languagePattern = /^\/[a-z]{2}\/(.+)$/
const match = targetUrl.match(languagePattern)
if (match && match[1]) {
return `/${match[1]}`;
return `/${match[1]}`
}
// 언어 코드만 있고 뒤에 아무것도 없는 경우 (예: /ko, /en)
const languageOnlyPattern = /^\/[a-z]{2}$/;
const languageOnlyPattern = /^\/[a-z]{2}$/
if (languageOnlyPattern.test(targetUrl)) {
return "";
return ''
}
// 언어 코드가 없는 경우 원본 경로 그대로 반환 (이미 /로 시작)
return targetUrl;
};
return targetUrl
}
const getCurrentPath = (): string => {
if (import.meta.client) {
return getPathAfterLanguage();
return getPathAfterLanguage()
}
return "";
};
return ''
}
return {
getPathAfterLanguage,
getCurrentPath,
};
};
}
}

View File

@@ -1,2 +1,2 @@
import { templateRegistry } from "#layers/registry";
export const useTemplateRegistry = () => templateRegistry;
import { templateRegistry } from '#layers/registry'
export const useTemplateRegistry = () => templateRegistry

View File

@@ -4,7 +4,7 @@
<div>
<LayoutDefaultHeader />
<main>
<slot></slot>
<slot />
</main>
<LayoutDefaultFooter />
</div>

View File

@@ -2,7 +2,7 @@
<template>
<div class="promotion-wrap">
<slot></slot>
<slot />
</div>
</template>

View File

@@ -1,35 +1,35 @@
import { useGameDataStore } from "#layers/stores/useGameDataStore";
import { useGameDataStore } from '#layers/stores/useGameDataStore'
export default defineNuxtRouteMiddleware(async (to, from) => {
export default defineNuxtRouteMiddleware(async (to, _from) => {
// 서버 사이드에서는 스킵
if (import.meta.server) {
return;
return
}
const gameDataStore = useGameDataStore();
const gameDataStore = useGameDataStore()
// gameData가 로드되지 않았으면 스킵 (다른 미들웨어에서 로드됨)
if (!gameDataStore.gameData) {
return;
return
}
// 현재 경로에서 언어 코드 추출
// 예: /ko/about/story -> ko
// 예: /en/test/page -> en
const languagePattern = /^\/([a-z]{2})(?:\/|$)/;
const match = to.path.match(languagePattern);
const currentLangCode = match ? match[1] : null;
console.log("🚀 3333~ currentLangCode:", currentLangCode)
const languagePattern = /^\/([a-z]{2})(?:\/|$)/
const match = to.path.match(languagePattern)
const currentLangCode = match ? match[1] : null
// console.log('🚀 3333~ currentLangCode:', currentLangCode)
// 허용된 언어 코드 목록
const allowedLangCodes = gameDataStore.gameData.lang_codes || [];
console.log("🚀 ~ allowedLangCodes:", allowedLangCodes)
const allowedLangCodes = gameDataStore.gameData.lang_codes || []
// console.log('🚀 ~ allowedLangCodes:', allowedLangCodes)
// 현재 언어가 허용된 언어 목록에 없으면 404로 리다이렉트
if (currentLangCode && !allowedLangCodes.includes(currentLangCode)) {
throw createError({
statusCode: 404,
statusMessage: "Language not supported"
});
statusMessage: 'Language not supported',
})
}
});
})

View File

@@ -1,44 +1,44 @@
import { commonFetch } from "#layers/utils/apiUtil";
import { usePageDataStore } from "#layers/stores/usePageDataStore";
import { useGetGameAlias } from "#layers/composables/useGetGameAlias";
import { usePathResolver } from "#layers/composables/usePathResolver";
import type { PageDataResponse } from "#layers/types/api/pageData";
import { commonFetch } from '#layers/utils/apiUtil'
import { usePageDataStore } from '#layers/stores/usePageDataStore'
import { useGetGameAlias } from '#layers/composables/useGetGameAlias'
import { usePathResolver } from '#layers/composables/usePathResolver'
import type { PageDataResponse } from '#layers/types/api/pageData'
export default defineNuxtRouteMiddleware(async (to, from) => {
export default defineNuxtRouteMiddleware(async (to, _from) => {
if (import.meta.server) {
return;
return
}
const config = useRuntimeConfig();
const store = usePageDataStore();
const gameAlias = useGetGameAlias();
const { getPathAfterLanguage } = usePathResolver();
const config = useRuntimeConfig()
const store = usePageDataStore()
const gameAlias = useGetGameAlias()
const { getPathAfterLanguage } = usePathResolver()
const stoveApiBaseUrl = config.public.stoveApiUrl;
const apiUrl = `${stoveApiBaseUrl}/pub-comm/v1.0/template/page`;
const stoveApiBaseUrl = config.public.stoveApiUrl
const apiUrl = `${stoveApiBaseUrl}/pub-comm/v1.0/template/page`
try {
const pageUrl = getPathAfterLanguage(to.path);
const pageUrl = getPathAfterLanguage(to.path)
const queryParams: Record<string, string> = {
game_alias: gameAlias,
lang_code: "ko",
lang_code: 'ko',
page_url: pageUrl,
_t: Date.now().toString(), // 캐시 무효화를 위한 타임스탬프
};
}
const response = (await commonFetch("GET", apiUrl, {
const response = (await commonFetch('GET', apiUrl, {
query: queryParams,
loading: true,
})) as PageDataResponse | null;
})) as PageDataResponse | null
if (response?.code === 0 && "value" in response) {
const cleanData = JSON.parse(JSON.stringify(response.value));
store.setPageData(cleanData);
if (response?.code === 0 && 'value' in response) {
const cleanData = JSON.parse(JSON.stringify(response.value))
store.setPageData(cleanData)
} else {
store.clearPageData();
store.clearPageData()
}
} catch (error) {
console.error(error);
store.clearPageData();
console.error(error)
store.clearPageData()
}
});
})

View File

@@ -1,8 +1,13 @@
import { defineNuxtConfig } from "nuxt/config";
import { defineNuxtConfig } from 'nuxt/config'
export default defineNuxtConfig({
// Layer-specific configuration
imports: {
dirs: ["composables", "stores", "types", "middleware", "server", "utils"],
dirs: ['composables', 'stores', 'types', 'middleware', 'server', 'utils'],
global: true,
},
});
components: {
dirs: ['components'],
global: true,
},
})

View File

@@ -1,24 +1,30 @@
export default defineNuxtPlugin(() => {
const { $i18n } = useNuxtApp()
const gameDataStore = useGameDataStore()
// gameData가 로드되면 언어 제외 설정 적용
watchEffect(() => {
const gameData = gameDataStore.gameData
if (gameData && gameData.lang_codes && gameData.lang_codes.length > 0) {
const allowedLangCodes = gameData.lang_codes
// 현재 설정된 locales에서 허용된 언어만 필터링
const availableLocales = $i18n.locales.value.filter((locale: any) =>
const availableLocales = $i18n.locales.value.filter((locale: any) =>
allowedLangCodes.includes(locale.code)
)
// locales 업데이트
$i18n.locales.value = availableLocales
// 에러로 인해 주석처리
// $i18n.locales.value = availableLocales
// 현재 locale이 허용되지 않은 경우 기본 locale로 변경
if (!allowedLangCodes.includes($i18n.locale.value)) {
$i18n.locale.value = gameData.default_lang_code || 'ko'
const defaultLang = allowedLangCodes.includes(
gameData.default_lang_code
)
? gameData.default_lang_code
: 'ko'
$i18n.locale.value = defaultLang as any
}
}
})

View File

@@ -1,22 +1,27 @@
export default defineNuxtPlugin(() => {
const { $i18n } = useNuxtApp()
// SSR에서 gameData를 가져와서 언어 제외 설정 적용
const gameDataFromServer = process.server
const gameDataFromServer = import.meta.server
? useNuxtApp().ssrContext?.event.context.gameData
: null
if (gameDataFromServer && gameDataFromServer.lang_codes && gameDataFromServer.lang_codes.length > 0) {
if (
gameDataFromServer &&
gameDataFromServer.lang_codes &&
gameDataFromServer.lang_codes.length > 0
) {
const allowedLangCodes = gameDataFromServer.lang_codes
// 현재 설정된 locales에서 허용된 언어만 필터링
const availableLocales = $i18n.locales.value.filter((locale: any) =>
const availableLocales = $i18n.locales.value.filter((locale: any) =>
allowedLangCodes.includes(locale.code)
)
// locales 업데이트
$i18n.locales.value = availableLocales
// 에러로 인해 주석처리
// $i18n.locales.value = availableLocales
// 현재 locale이 허용되지 않은 경우 기본 locale로 변경
if (!allowedLangCodes.includes($i18n.locale.value)) {
$i18n.locale.value = gameDataFromServer.default_lang_code || 'ko'

View File

@@ -1,10 +1,10 @@
export default defineNuxtPlugin(() => {
const runtimeConfig = useRuntimeConfig();
const callerInfoStore = useCallerInfoStore();
const runtimeConfig = useRuntimeConfig()
const callerInfoStore = useCallerInfoStore()
const callerId = `${runtimeConfig.public.stoveGameId}`;
const callerDetail = `${useCookie("sgs_da_uuid").value || ""}`;
const callerId = `${runtimeConfig.public.stoveGameId}`
const callerDetail = `${useCookie('sgs_da_uuid').value || ''}`
callerInfoStore.setCallerId(callerId);
callerInfoStore.setCallerDetail(callerDetail);
});
callerInfoStore.setCallerId(callerId)
callerInfoStore.setCallerDetail(callerDetail)
})

View File

@@ -1,6 +1,6 @@
import GrVisual01 from "#layers/templates/GrVisual01/index.vue";
import GrVisual02 from "#layers/templates/GrVisual02/index.vue";
import GrVisual03 from "#layers/templates/GrVisual03/index.vue";
import GrVisual01 from '#layers/templates/GrVisual01/index.vue'
import GrVisual02 from '#layers/templates/GrVisual02/index.vue'
import GrVisual03 from '#layers/templates/GrVisual03/index.vue'
// import GrGallery01 from "#layers/templates/GrGallery01/index.vue";
// import GrGallery02 from "#layers/templates/GrGallery02/index.vue";
// import GrGallery03 from "#layers/templates/GrGallery03/index.vue";
@@ -22,6 +22,6 @@ export const templateRegistry = {
// GR_DETAIL_02: { component: GrDetail02 },
// GR_DETAIL_03: { component: GrDetail03 },
// GR_CONTENTS_01: { component: GrContents01 },
} as const;
} as const
export type TemplateKey = keyof typeof templateRegistry;
export type TemplateKey = keyof typeof templateRegistry

View File

@@ -3,67 +3,70 @@ import {
getRequestHost,
defineEventHandler,
getRequestURL,
} from "h3";
} from 'h3'
export default defineEventHandler(async (event) => {
const url = getRequestURL(event);
export default defineEventHandler(async event => {
const url = getRequestURL(event)
// 정적 자산, API, 파비콘 등은 제외하고 페이지 요청만 처리
if (
url.pathname.startsWith("/api/") ||
url.pathname.startsWith("/_nuxt/") ||
url.pathname.startsWith("/favicon") ||
url.pathname.includes(".") ||
url.pathname.startsWith("/_")
url.pathname.startsWith('/api/') ||
url.pathname.startsWith('/_nuxt/') ||
url.pathname.startsWith('/favicon') ||
url.pathname.includes('.') ||
url.pathname.startsWith('/_')
) {
return;
return
}
const host =
(getHeader(event, "host") || getRequestHost(event)).toString() || "";
const baseDomain = process.env.BASE_DOMAIN || ".onstove.com";
const isGameAliasExtractable = host.includes(baseDomain);
(getHeader(event, 'host') || getRequestHost(event)).toString() || ''
const baseDomain = process.env.BASE_DOMAIN || '.onstove.com'
const isGameAliasExtractable = host.includes(baseDomain)
if (isGameAliasExtractable) {
const gameAlias = host.split(".")[0];
const gameAlias = host.split('.')[0]
if (gameAlias && gameAlias !== "www") {
event.context.gameAlias = gameAlias;
if (gameAlias && gameAlias !== 'www') {
event.context.gameAlias = gameAlias
}
}
// gameData를 직접 가져와서 context에 저장 (API 호출 없이)
try {
const config = useRuntimeConfig();
const stoveApiBaseUrl = config.public.stoveApiUrl;
const apiUrl = `${stoveApiBaseUrl}/pub-comm/v1.0/template/game`;
const config = useRuntimeConfig()
const stoveApiBaseUrl = config.public.stoveApiUrl
const apiUrl = `${stoveApiBaseUrl}/pub-comm/v1.0/template/game`
// URL의 첫 번째 path를 lang_code로 사용 (파비콘, API 경로 제외)
const url = getRequestURL(event);
const url = getRequestURL(event)
const pathSegments = url.pathname
.split("/")
.split('/')
.filter(
(segment) =>
segment =>
segment &&
!segment.includes("favicon") &&
!segment.includes("api") &&
!segment.startsWith("_")
);
const langCode = pathSegments[0] || "ko";
!segment.includes('favicon') &&
!segment.includes('api') &&
!segment.startsWith('_')
)
const langCode = pathSegments[0] || 'ko'
const queryParams: Record<string, string> = {
game_alias: event.context.gameAlias || "",
game_alias: event.context.gameAlias || '',
lang_code: langCode,
};
}
const response = await $fetch(apiUrl, {
query: queryParams,
});
})
if (response?.code === 0 && "value" in response) {
event.context.gameData = response.value;
// 타입 단언을 사용하여 response의 타입 오류를 해결
const res = response as { code?: number; value?: unknown }
if (res?.code === 0 && 'value' in res) {
event.context.gameData = res.value
}
} catch (error) {
console.error("gameData load error:", error);
console.error('gameData load error:', error)
}
});
})

View File

@@ -1,26 +1,26 @@
export const useCallerInfoStore = defineStore("callerInfoStore", () => {
const callerId = ref<string>("");
const callerDetail = ref<string>("");
export const useCallerInfoStore = defineStore('callerInfoStore', () => {
const callerId = ref<string>('')
const callerDetail = ref<string>('')
const setCallerId = (id: string): void => {
callerId.value = id;
};
callerId.value = id
}
const setCallerDetail = (detail: string): void => {
callerDetail.value = detail;
};
callerDetail.value = detail
}
// 초기화
const resetCallerInfo = (): void => {
callerId.value = "";
callerDetail.value = "";
};
callerId.value = ''
callerDetail.value = ''
}
// 현재 정보 반환
const getCallerInfo = (): { callerId: string; callerDetail: string } => ({
callerId: callerId.value,
callerDetail: callerDetail.value,
});
})
return {
callerId,
@@ -29,5 +29,5 @@ export const useCallerInfoStore = defineStore("callerInfoStore", () => {
setCallerDetail,
resetCallerInfo,
getCallerInfo,
};
});
}
})

View File

@@ -1,21 +1,19 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import type { GameDataValue } from "#layers/types/api/gameData";
import type { GameDataValue } from '#layers/types/api/gameData'
export const useGameDataStore = defineStore("gameData", () => {
const gameData = ref<GameDataValue | null>(null);
export const useGameDataStore = defineStore('gameData', () => {
const gameData = ref<GameDataValue | null>(null)
const setGameData = (data: GameDataValue) => {
gameData.value = data;
};
gameData.value = data
}
const clearGameData = () => {
gameData.value = null;
};
gameData.value = null
}
return {
gameData,
setGameData,
clearGameData,
};
});
}
})

View File

@@ -1,53 +1,55 @@
import { defineStore } from "pinia";
import { defineStore } from 'pinia'
export const useLoadingStore = defineStore("loadingStore", () => {
export const useLoadingStore = defineStore('loadingStore', () => {
// 글로벌 로딩 표기
const fullLoading = ref(false);
const fullLoading = ref(false)
// 컴포넌트별 로딩 표기 - Map 대신 일반 객체 사용
const localLoadings = ref<Record<string, { active: boolean }>>({});
const localLoadings = ref<Record<string, { active: boolean }>>({})
// 로딩 상태만 표기
const isLoading = ref(false);
const isLoading = ref(false)
/**
* 모든 로딩 상태 초기화
*/
const initializeStore = () => {
fullLoading.value = false;
localLoadings.value = {};
};
fullLoading.value = false
localLoadings.value = {}
}
/**
* Full 로딩
*/
const startFullLoading = () => {
fullLoading.value = true;
};
fullLoading.value = true
}
const stopFullLoading = () => {
fullLoading.value = false;
};
fullLoading.value = false
}
/**
* Local 로딩
*/
const startLocalLoading = (localId: string) => {
localLoadings.value[localId] = { active: true };
};
localLoadings.value[localId] = { active: true }
}
const stopLocalLoading = (localId: string) => {
delete localLoadings.value[localId];
};
if (localLoadings.value[localId]) {
localLoadings.value[localId].active = false
}
}
const isLocalLoading = (localId: string) => {
return localLoadings.value[localId]?.active || false;
};
return !!localLoadings.value[localId]?.active
}
/**
* 로딩 상태 변경
*/
const setLoading = (state: boolean) => {
isLoading.value = state;
};
isLoading.value = state
}
return {
fullLoading,
@@ -61,5 +63,5 @@ export const useLoadingStore = defineStore("loadingStore", () => {
stopLocalLoading,
isLocalLoading,
setLoading,
};
});
}
})

View File

@@ -1,21 +1,19 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import type { PageDataValue } from "#layers/types/api/pageData";
import type { PageDataValue } from '#layers/types/api/pageData'
export const usePageDataStore = defineStore("pageData", () => {
const pageData = ref<PageDataValue | null>(null);
export const usePageDataStore = defineStore('pageData', () => {
const pageData = ref<PageDataValue | null>(null)
const setPageData = (response: PageDataValue) => {
pageData.value = response;
};
pageData.value = response
}
const clearPageData = () => {
pageData.value = null;
};
pageData.value = null
}
return {
pageData,
setPageData,
clearPageData,
};
});
}
})

View File

@@ -1,29 +1,29 @@
<script setup lang="ts">
import YouTubeModal from "#layers/components/molecules/modal/YouTubeModal.vue";
import YouTubeModal from '#layers/components/molecules/modal/YouTubeModal.vue'
interface Props {
components: Record<string, any>;
components: Record<string, any>
}
const props = defineProps<Props>();
const props = defineProps<Props>()
// YouTube 모달 상태 관리
const isYouTubeModalOpen = ref(false);
const youtubeVideoId = ref("");
const isYouTubeModalOpen = ref(false)
const youtubeVideoId = ref('')
// 비디오 플레이 버튼 클릭 핸들러
const handleVideoPlayClick = () => {
// TODO: 실제 YouTube 비디오 ID를 설정해야 합니다
// 예시: 'dQw4w9WgXcQ' (Rick Astley - Never Gonna Give You Up)
youtubeVideoId.value = "UKVsZYHxYTc"; // 임시로 설정
isYouTubeModalOpen.value = true;
};
youtubeVideoId.value = 'UKVsZYHxYTc' // 임시로 설정
isYouTubeModalOpen.value = true
}
// 모달 닫기 핸들러
const handleCloseModal = () => {
isYouTubeModalOpen.value = false;
youtubeVideoId.value = "";
};
isYouTubeModalOpen.value = false
youtubeVideoId.value = ''
}
</script>
<template>
@@ -58,6 +58,6 @@ const handleCloseModal = () => {
:is-open="isYouTubeModalOpen"
:youtube-id="youtubeVideoId"
@close="handleCloseModal"
@update:is-open="isYouTubeModalOpen = $event"
@update:is-open="(value: boolean) => (isYouTubeModalOpen = value)"
/>
</template>

View File

@@ -1,11 +1,11 @@
<script setup lang="ts">
interface Props {
components: Record<string, any>;
components: Record<string, any>
}
const props = defineProps<Props>();
const _props = defineProps<Props>()
</script>
<template>
<section class="template-section"></section>
<section class="template-section" />
</template>

View File

@@ -1,11 +1,11 @@
<script setup lang="ts">
interface Props {
components: Record<string, any>;
components: Record<string, any>
}
const props = defineProps<Props>();
const props = defineProps<Props>()
console.log("components:", props.components);
console.log('components:', props.components)
</script>
<template>

View File

@@ -1,317 +1,331 @@
// API 요청 파라미터 타입
export interface GameDataRequest {
game_alias: string;
lang_code: string;
q?: string;
qc?: string;
game_alias: string
lang_code: string
q?: string
qc?: string
}
// API 응답 데이터 타입
export interface GameDataResponse {
code: number;
message: string;
value: GameDataValue;
code: number
message: string
value: GameDataValue
}
// API 응답의 value 객체 타입
export interface GameDataValue {
game_id: string;
game_code: number;
s3_folder_name: string;
game_name: string;
ga_code: string;
favicon_path: string;
design_theme: number;
lang_codes: string; // JSON 문자열로 변경
key_color_codes: string; // JSON 문자열로 변경
use_game_font: boolean;
footer_dev_ci_img_yn: boolean;
footer_dev_ci_img_path: string;
game_font: string; // JSON 문자열로 변경
globals: GameDataGlobal[]; // 배열로 변경
gnb: GameDataGnb;
intro: GameDataIntro;
inspection: Record<string, any>; // 동적 객체
stove_gnb: string; // JSON 문자열로 변경
meta_tag: string; // JSON 문자열로 변경
sns: string; // JSON 문자열로 변경
footer: string; // JSON 문자열로 변경
game_id: string
game_code: number
s3_folder_name: string
game_name: string
ga_code: string
design_theme: number
lang_codes: string // JSON 문자열로 변경
key_color_codes: string // JSON 문자열로 변경
use_game_font: boolean
comm_sns_bg_color_code: string
comm_multilang_filename: string
footer_dev_ci_img_yn: boolean
footer_dev_ci_img_path: string
default_lang_code?: string // 기본 언어 코드 추가
game_font: string // JSON 문자열로 변경
globals: GameDataGlobal[] // 배열로 변경
gnb: GameDataGnb
intro: GameDataIntro
inspection: Record<string, any> // 동적 객체
stove_gnb: GameDataStoveGnb // JSON 문자열로 변경
favicon_path: string // JSON 문자열로 변경
meta_tag: string // JSON 문자열로 변경
sns: string // JSON 문자열로 변경
footer: string // JSON 문자열로 변경
}
// Global 설정 타입
export interface GameDataGlobal {
system_font: string; // JSON 문자열로 변경
lang: string; // JSON 문자열로 변경
system_font: string // JSON 문자열로 변경
lang: string // JSON 문자열로 변경
}
// 폰트 타입
export interface GameDataFont {
"font-family": string;
'font-family': string
}
// 언어 설정 타입
export interface GameDataLang {
dir: string;
iso: string;
code: string;
name: string;
dir: string
iso: string
code: string
name: string
}
// GNB 설정 타입
export interface GameDataGnb {
game_gnb_ver: string;
display_start_dt: string; // ISO 문자열로 변경
theme_type: string;
bi_path: string;
lang_codes: string; // JSON 문자열로 변경
buttons: GameDataButton[];
menus: Record<string, GameDataMenu>; // 동적 객체로 변경
game_gnb_ver: string
display_start_dt: string // ISO 문자열로 변경
theme_type: string
bi_path: string
lang_codes: string // JSON 문자열로 변경
buttons: GameDataButton[]
menus: Record<string, GameDataMenu> // 동적 객체로 변경
}
// 버튼 타입
export interface GameDataButton {
depth_type: number;
button: string; // JSON 문자열로 변경
depth_type: number
button: string // JSON 문자열로 변경
}
// 메뉴 타입
export interface GameDataMenu {
path_code: string;
depth: number;
sort_order: number;
menu_name: string;
click_action_type: number;
url_path: string;
link_target: string;
children: Record<string, GameDataMenu>; // 중첩 메뉴를 위한 children 속성 추가
tracking: string; // JSON 문자열로 변경
path_code: string
depth: number
sort_order: number
menu_name: string
click_action_type: number
url_path: string
link_target: string
children: Record<string, GameDataMenu> // 중첩 메뉴를 위한 children 속성 추가
tracking: string | GameDataTracking // JSON 문자열 또는 객체로 변경
}
// 인트로 타입
export interface GameDataIntro {
seq: number;
display_start_dt: string;
display_end_dt: string;
page_url: string;
seq: number
display_start_dt: string
display_end_dt: string
page_url: string
}
// 트래킹 타입
export interface GameDataTracking {
viewType: string;
actionType: string;
clickSarea: string;
viewType: string
actionType: string
clickSarea: string
}
// 퀵 메뉴 타입
export interface GameDataQuickMenu {
banner_seq: number;
promotion_name: string;
thumbnail: string;
page_url_type: number;
page_url: string;
link_type: number;
display_start_dt: string;
display_end_dt: string;
sort_order: number;
banner_title: string;
banner_seq: number
promotion_name: string
thumbnail: string
page_url_type: number
page_url: string
link_type: number
display_start_dt: string
display_end_dt: string
sort_order: number
banner_title: string
}
// Stove GNB Skin Type
export type StoveGnbSkinType =
| 'gnb-default'
| 'gnb-default-fixed'
| 'gnb-dark-theme'
| 'gnb-dark-mix'
| 'gnb-dark-mini'
| 'gnb-dark-mix-mini'
| 'gnb-default-mini'
| 'gnb-mobile-timeline'
// Stove GNB 타입
export interface GameDataStoveGnb {
skin_type: string;
stove_install_button_visible: string;
skin_type: StoveGnbSkinType
stove_install_button_visible: string
}
// 파비콘 경로 타입
export interface GameDataFaviconPath {
"16_16": string;
"32_32": string;
"72_72": string;
"180_180": string;
"192_192": string;
'16_16': string
'32_32': string
'72_72': string
'180_180': string
'192_192': string
}
// 공통 이미지 타입
export interface GameDataCommImg {
groups: GameDataCommImgGroup[];
groups: GameDataCommImgGroup[]
}
// 공통 이미지 그룹 타입
export interface GameDataCommImgGroup {
img_path: GameDataImgPath;
required: boolean;
img_scope: string;
group_code: string;
group_label: string;
img_path: GameDataImgPath
required: boolean
img_scope: string
group_code: string
group_label: string
}
// 이미지 경로 타입
export interface GameDataImgPath {
comm: string;
comm: string
}
// 메타 태그 타입
export interface GameDataMetaTag {
x_desc: string;
og_desc: string;
x_image: string;
x_title: string;
og_image: string;
og_title: string;
page_desc: string;
page_title: string;
x_desc: string
og_desc: string
x_image: string
x_title: string
og_image: string
og_title: string
page_desc: string
page_title: string
}
// YouTube 설정 타입
export interface GameDataYoutube {
use_yn: number;
api_key: string;
use_yn: number
api_key: string
}
// SNS 설정 타입
export interface GameDataSns {
kakao: GameDataSnsItem;
tiktok: GameDataSnsItem;
discord: GameDataSnsItem;
twitter: GameDataSnsItem;
youtube: GameDataSnsItem;
facebook: GameDataSnsItem;
instagram: GameDataSnsItem;
kakao: GameDataSnsItem
tiktok: GameDataSnsItem
discord: GameDataSnsItem
twitter: GameDataSnsItem
youtube: GameDataSnsItem
facebook: GameDataSnsItem
instagram: GameDataSnsItem
}
// SNS 아이템 타입
export interface GameDataSnsItem {
url: string | null;
use_yn: number;
url: string | null
use_yn: number
}
// 마켓 설정 타입
export interface GameDataMarket {
pc: GameDataMarketItem;
app_store: GameDataMarketItem;
google_play: GameDataMarketItem;
pc: GameDataMarketItem
app_store: GameDataMarketItem
google_play: GameDataMarketItem
}
// 마켓 아이템 타입
export interface GameDataMarketItem {
url: string | null;
use_yn: number;
url: string | null
use_yn: number
}
// 푸터 설정 타입
export interface GameDataFooter {
dev_ci_url: string;
use_dev_ci_url: boolean;
fund_display_yn: boolean;
use_game_rating: boolean;
fund_display_url: string;
game_rating_info: GameDataGameRatingInfo;
dev_ci_url: string
use_dev_ci_url: boolean
fund_display_yn: boolean
use_game_rating: boolean
fund_display_url: string
game_rating_info: GameDataGameRatingInfo
}
// 게임 등급 정보 타입
export interface GameDataGameRatingInfo {
title: string;
reg_no: string;
prod_date: string;
rating_type: string;
company_name: string;
content_info: string;
rating_grade: string;
rating_class_no: string;
title: string
reg_no: string
prod_date: string
rating_type: string
company_name: string
content_info: string
rating_grade: string
rating_class_no: string
}
// API 에러 응답 타입
export interface GameDataErrorResponse {
code: number;
message: string;
error?: string;
code: number
message: string
error?: string
}
// API 응답 래퍼 타입 (성공/실패 통합)
export type GameDataApiResponse = GameDataResponse | GameDataErrorResponse;
export type GameDataApiResponse = GameDataResponse | GameDataErrorResponse
// API 호출 옵션 타입
export interface GameDataApiOptions {
gameAlias: string;
langCode: string;
q: string;
qc: string;
baseUrl?: string;
gameAlias: string
langCode: string
q: string
qc: string
baseUrl?: string
}
// API 상태 타입
export type GameDataApiStatus = "idle" | "loading" | "success" | "error";
export type GameDataApiStatus = 'idle' | 'loading' | 'success' | 'error'
// API 결과 타입 (상태와 데이터를 함께 관리)
export interface GameDataApiResult {
status: GameDataApiStatus;
data: GameDataResponse | null;
error: string | null;
status: GameDataApiStatus
data: GameDataResponse | null
error: string | null
}
// JSON 문자열 파싱을 위한 유틸리티 타입들
export interface ParsedKeyColorCodes {
extra: string;
primary: string;
secondary: string;
"text-primary": string;
"text-secondary": string;
extra: string
primary: string
secondary: string
'text-primary': string
'text-secondary': string
}
export interface ParsedGameFont {
"font-family": string;
'font-family': string
}
export interface ParsedGlobal {
system_font: ParsedGameFont;
lang: GameDataLang;
system_font: ParsedGameFont
lang: GameDataLang
}
export interface ParsedButton {
google_play: {
label: Record<string, string>;
url: string;
tracking: GameDataTracking;
};
label: Record<string, string>
url: string
tracking: GameDataTracking
}
app_store: {
label: Record<string, string>;
url: string;
tracking: GameDataTracking;
};
label: Record<string, string>
url: string
tracking: GameDataTracking
}
}
export interface ParsedStoveGnb {
skin_type: string;
stove_install_button_visible: string;
skin_type: StoveGnbSkinType
stove_install_button_visible: string
}
export interface ParsedSns {
kakao: GameDataSnsItem;
twitter: GameDataSnsItem;
discord: GameDataSnsItem;
youtube: GameDataSnsItem;
instagram: GameDataSnsItem;
facebook: GameDataSnsItem;
tiktok: GameDataSnsItem;
kakao: GameDataSnsItem
twitter: GameDataSnsItem
discord: GameDataSnsItem
youtube: GameDataSnsItem
instagram: GameDataSnsItem
facebook: GameDataSnsItem
tiktok: GameDataSnsItem
}
export interface ParsedFooter {
use_game_rating: boolean;
game_rating_info: GameDataGameRatingInfo;
use_dev_ci_url: boolean;
dev_ci_url: string;
fund_display_yn: boolean;
fund_display_url: string;
use_game_rating: boolean
game_rating_info: GameDataGameRatingInfo
use_dev_ci_url: boolean
dev_ci_url: string
fund_display_yn: boolean
fund_display_url: string
}
// 파비콘 경로 파싱 타입
export interface ParsedFaviconPath {
"16_16": string;
"32_32": string;
"72_72": string;
"180_180": string;
"192_192": string;
'16_16': string
'32_32': string
'72_72': string
'180_180': string
'192_192': string
}
// 기존 gameData 타입과의 호환성을 위한 별칭
export type gameData = GameDataValue;
export type gameData = GameDataValue

View File

@@ -1,147 +1,170 @@
// API 요청 파라미터 타입
export interface PageDataRequest {
game_alias: string;
lang_code: string;
page_url: string;
q?: string;
qc?: string;
game_alias: string
lang_code: string
page_url: string
q?: string
qc?: string
}
// API 요청 헤더 타입
export interface PageDataRequestHeaders {
"caller-Detail"?: string;
"caller-ID"?: string;
Authorization?: string;
'caller-Detail'?: string
'caller-ID'?: string
Authorization?: string
}
// API 응답 데이터 타입
export interface PageDataResponse {
code: number;
message: string;
value: PageDataValue;
code: number
message: string
value: PageDataValue
}
// API 응답의 value 객체 타입
export interface PageDataValue {
page_seq: number;
page_type: number;
page_name: string;
page_name_en: string;
page_ver: number;
display_start_dt: string;
lang_codes: string; // JSON string
is_login_required: boolean;
meta_tag_type: number;
fit_page_height: boolean;
use_top_btn: boolean;
use_sns_btn: boolean;
use_lnb: boolean;
lnb_menus: PageDataLnbMenu[];
templates: PageDataTemplate[];
meta_tag: PageDataMetaTag;
page_seq: number
page_type: number
page_name: string
page_name_en: string
page_ver: number
display_start_dt: string
lang_codes: string // JSON string
is_login_required: boolean
meta_tag_type: number
fit_page_height: boolean
use_top_btn: boolean
use_sns_btn: boolean
use_lnb: boolean
lnb_menus: PageDataLnbMenu[]
templates: PageDataTemplate[]
meta_tag: PageDataMetaTag // JSON string
}
// 메타 태그 타입
export interface PageDataMetaTag {
x_desc: string;
og_desc: string;
x_image: string;
x_title: string;
og_image: string;
og_title: string;
page_desc: string;
page_title: string;
x_desc: string
og_desc: string
x_image: string
x_title: string
og_image: string
og_title: string
page_desc: string
page_title: string
}
// LNB 메뉴 타입
export interface PageDataLnbMenu {
text_color_code_active: string;
text_color_code_deactive: string;
path_code: string;
depth: number;
sort_order: number;
menu_name: string;
target_type: number;
page_ver_tmpl_name_en: string;
tracking: string; // JSON string
text_color_code_active: string
text_color_code_deactive: string
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 {
tmpl_sort_order: number;
page_ver_tmpl_name: string;
page_ver_tmpl_name_en: string;
template_code: string;
template_type_code: string;
template_category: string;
template_name: string;
template_sample_img_path: string;
template_sample_zip_path: string;
template_type_name: string;
is_selectable_on_create: boolean;
components: Record<string, PageDataComponent>;
tmpl_sort_order: number
page_ver_tmpl_name: string
page_ver_tmpl_name_en: string
template_code: string
components: Record<string, PageDataComponent>
}
// 컴포넌트 타입
export interface PageDataComponent {
component_sort_order: number;
component_type: string;
set_count: number;
component_name: string;
is_required: boolean;
has_operational_resource: boolean;
min_count: number;
max_count: number;
component_id: string;
operate_resources: PageDataOperateResource[];
[key: string]: any; // Additional Properties
component_sort_order: number
set_count: number
component_name: string
has_operational_resource: boolean
resources: PageDataResource[]
list_resources: PageDataListResource[]
flag_resources: PageDataFlagResource[]
}
// 운영 리소스 타입
// 리소스 타입
export interface PageDataResource {
resource_sort_order: number
resource_name: string
resource_type: string
groups: Record<string, any>
group_sets: Record<string, any>
}
// 리스트 리소스 타입
export interface PageDataListResource {
seq: number
title: string
img_path: string
url: string
link_target: string
display_start_dt: string
display_end_dt: string
is_display_status: number
display_status: number
option01: number
option02: number
option03: string
}
// 플래그 리소스 타입
export interface PageDataFlagResource {
seq: number
flag_type: number
option01: number
option02: number
option03: string
}
// 운영 리소스 타입 (기존 호환성 유지)
export interface PageDataOperateResource {
seq: number;
col: string;
col2: string;
col3: string;
col4: string;
col5: string;
col6: string;
col7: string;
resource_control_type: number;
resource_type: string;
[key: string]: any; // Additional Properties
seq: number
col: string
col2: string
col3: string
col4: string
col5: string
col6: string
col7: string
resource_control_type: number
resource_type: string
[key: string]: any // Additional Properties
}
// API 에러 응답 타입
export interface PageDataErrorResponse {
code: number;
message: string;
error?: string;
code: number
message: string
error?: string
}
// API 응답 래퍼 타입 (성공/실패 통합)
export type PageDataApiResponse = PageDataResponse | PageDataErrorResponse;
export type PageDataApiResponse = PageDataResponse | PageDataErrorResponse
// API 호출 옵션 타입
export interface PageDataApiOptions {
gameAlias: string;
langCode: string;
pageUrl: string;
q?: string;
qc?: string;
headers?: PageDataRequestHeaders;
baseUrl?: string;
gameAlias: string
langCode: string
pageUrl: string
q?: string
qc?: string
headers?: PageDataRequestHeaders
baseUrl?: string
}
// API 상태 타입
export type PageDataApiStatus = "idle" | "loading" | "success" | "error";
export type PageDataApiStatus = 'idle' | 'loading' | 'success' | 'error'
// API 결과 타입 (상태와 데이터를 함께 관리)
export interface PageDataApiResult {
status: PageDataApiStatus;
data: PageDataResponse | null;
error: string | null;
status: PageDataApiStatus
data: PageDataResponse | null
error: string | null
}
// 페이지 데이터 별칭 (기존 호환성)
export type pageData = PageDataValue;
export type pageData = PageDataValue

View File

@@ -1,4 +1,4 @@
import { ref } from "vue";
import { ref } from 'vue'
/**
* 공통 API 호출
@@ -13,15 +13,15 @@ import { ref } from "vue";
*/
export const commonFetch = async (
method:
| "GET"
| "HEAD"
| "PATCH"
| "POST"
| "PUT"
| "DELETE"
| "CONNECT"
| "OPTIONS"
| "TRACE" = "GET", // Required
| 'GET'
| 'HEAD'
| 'PATCH'
| 'POST'
| 'PUT'
| 'DELETE'
| 'CONNECT'
| 'OPTIONS'
| 'TRACE' = 'GET', // Required
url: string, // Required
{
query, // Optional
@@ -30,40 +30,40 @@ export const commonFetch = async (
key, // Optional
loading = false, // Optional
}: {
query?: object | null;
headers?: object | null;
body?: object | null;
key?: string | null;
loading?: { localId?: string } | boolean;
query?: object | null
headers?: object | null
body?: object | null
key?: string | null
loading?: { localId?: string } | boolean
} = {}
) => {
let result = null;
const currCallerId = ref("");
const currCallerDetail = ref("");
let result = null
const currCallerId = ref('')
const currCallerDetail = ref('')
// 로딩 스토어 가져오기 (클라이언트에서만)
let loadingStore: ReturnType<typeof useLoadingStore> | null = null;
let loadingStore: ReturnType<typeof useLoadingStore> | null = null
if (import.meta.client) {
try {
loadingStore = useLoadingStore();
loadingStore = useLoadingStore()
// 로딩 시작
loadingStore.setLoading(true);
loadingStore.setLoading(true)
if (loading && loadingStore) {
if (typeof loading === "object" && loading.localId) {
loadingStore.startLocalLoading(loading.localId);
if (typeof loading === 'object' && loading.localId) {
loadingStore.startLocalLoading(loading.localId)
} else {
loadingStore.startFullLoading();
loadingStore.startFullLoading()
}
}
} catch (e) {
console.warn("[Warning] Loading store not available:", e);
console.warn('[Warning] Loading store not available:', e)
}
}
try {
const { callerId, callerDetail } = storeToRefs(useCallerInfoStore());
currCallerId.value = `${callerId.value}`;
currCallerDetail.value = `${callerDetail.value}`;
const { callerId, callerDetail } = storeToRefs(useCallerInfoStore())
currCallerId.value = `${callerId.value}`
currCallerDetail.value = `${callerDetail.value}`
} catch (e) {
// SSR: pinia store 생성 전이므로 빈 값('') 세팅
}
@@ -71,74 +71,74 @@ export const commonFetch = async (
try {
const options: {
method:
| "GET"
| "HEAD"
| "PATCH"
| "POST"
| "PUT"
| "DELETE"
| "CONNECT"
| "OPTIONS"
| "TRACE";
headers: Record<string, string>;
query?: object;
body?: object;
key?: string;
| 'GET'
| 'HEAD'
| 'PATCH'
| 'POST'
| 'PUT'
| 'DELETE'
| 'CONNECT'
| 'OPTIONS'
| 'TRACE'
headers: Record<string, string>
query?: object
body?: object
key?: string
} = {
method,
headers: {
"Content-Type": "application/json;charset=UTF-8",
'Content-Type': 'application/json;charset=UTF-8',
},
};
}
if (query) {
options.query = query;
options.query = query
}
// 플랫폼 환경 API 호출 시 Caller-Id, Caller-Detail 헤더 추가
if (url.includes(".onstove.com") || url.includes(".gate8.com")) {
if (url.includes('.onstove.com') || url.includes('.gate8.com')) {
const callerInfo = {
"Caller-Id": `${currCallerId.value}`,
"Caller-Detail": `${currCallerDetail.value}`,
};
options.headers = { ...options.headers, ...callerInfo };
'Caller-Id': `${currCallerId.value}`,
'Caller-Detail': `${currCallerDetail.value}`,
}
options.headers = { ...options.headers, ...callerInfo }
}
if (headers) {
options.headers = { ...options.headers, ...headers };
options.headers = { ...options.headers, ...headers }
}
if (body) {
options.body = body;
options.body = body
}
if (key) {
options.key = key;
options.key = key
}
result = await $fetch(url, options);
result = await $fetch(url, options)
} catch (e: unknown) {
console.error("[Exception] apiUtil.commonFetch: ", e);
console.error('[Exception] apiUtil.commonFetch: ', e)
const error = e as {
data?: unknown;
statusCode?: number;
statusMessage?: string;
};
data?: unknown
statusCode?: number
statusMessage?: string
}
result = error.data || {
code: error.statusCode,
message: error.statusMessage,
};
}
} finally {
// 로딩 종료
if (loadingStore) {
loadingStore.setLoading(false);
loadingStore.setLoading(false)
if (loading) {
if (typeof loading === "object" && loading.localId) {
loadingStore.stopLocalLoading(loading.localId);
if (typeof loading === 'object' && loading.localId) {
loadingStore.stopLocalLoading(loading.localId)
} else {
loadingStore.stopFullLoading();
loadingStore.stopFullLoading()
}
}
}
}
return result;
};
return result
}
/**
* 사용자 IP 조회
@@ -146,36 +146,35 @@ export const commonFetch = async (
* @param {object} request - 요청 객체
*/
export const getTrueClientIp = (request: {
headers: Record<string, string>;
socket: { remoteAddress?: string };
headers: Record<string, string>
socket: { remoteAddress?: string }
}) => {
const requestHeaders = request.headers;
const requestHeaders = request.headers
const targetHeaders = [
"True-Client-IP",
"X-Real-IP",
"X-Forwarded-For",
"Proxy-Client-IP",
"WL-Proxy-Client-IP",
"HTTP_CLIENT_IP",
"HTTP_X_FORWARDED_FOR",
];
'True-Client-IP',
'X-Real-IP',
'X-Forwarded-For',
'Proxy-Client-IP',
'WL-Proxy-Client-IP',
'HTTP_CLIENT_IP',
'HTTP_X_FORWARDED_FOR',
]
for (const targetHeader of targetHeaders) {
let ip =
requestHeaders[targetHeader] ||
requestHeaders[targetHeader.toLowerCase()];
if (ip !== undefined && ip != null && ip !== "") {
if (ip.includes(",")) {
ip = ip.split(",")[0];
requestHeaders[targetHeader] || requestHeaders[targetHeader.toLowerCase()]
if (ip !== undefined && ip != null && ip !== '') {
if (ip.includes(',')) {
ip = ip.split(',')[0]
}
return ip;
return ip
}
}
if (
request.socket.remoteAddress !== undefined &&
request.socket.remoteAddress != null &&
request.socket.remoteAddress !== ""
request.socket.remoteAddress !== ''
) {
return request.socket.remoteAddress;
return request.socket.remoteAddress
}
return "";
};
return ''
}

View File

@@ -1,14 +1,14 @@
// 이미지 호스트 리턴하는 함수
// [TODO] 환경변수 처리 수정
export const getResolvedHost = (path: string): string => {
const config = useRuntimeConfig();
const config = useRuntimeConfig()
// const isDev = process.env.NODE_ENV === "development";
// const rootPath = isDev ? "/images" : `${config.public.staticUrl}`;
const rootPath = config.public.staticUrl;
const rootPath = config.public.staticUrl
return `${rootPath}${path}`;
};
return `${rootPath}${path}`
}
// 리소스 데이터 리턴하는 함수
// [TODO] data 타입 정의
@@ -17,54 +17,54 @@ export const getResourcesData = ({
isMultiple = false,
groupSets = false,
}: {
resources: any;
isMultiple?: boolean;
groupSets?: boolean;
resources: any
isMultiple?: boolean
groupSets?: boolean
}) => {
const groups = groupSets
? resources[0]?.group_sets[0]?.groups
: resources[0]?.groups;
: resources[0]?.groups
if (isMultiple) {
return groups;
return groups
}
return groups?.[0] ?? null;
};
return groups?.[0] ?? null
}
// 반응형 클래스 리턴하는 함수
export const getResponsiveClass = () => {
return ["bg-[image:var(--mobile-bg)]", "sm:bg-[image:var(--pc-bg)]"];
};
return ['bg-[image:var(--mobile-bg)]', 'sm:bg-[image:var(--pc-bg)]']
}
// 통합된 반응형 리소스 함수
export const getResponsiveSrc = (
pathArray: any,
options: {
resourcesType?: "image" | "bg" | "video";
resourcesType?: 'image' | 'bg' | 'video'
} = {}
) => {
const { resourcesType = "image" } = options;
const pcField = resourcesType === "video" ? "path_vid_pc" : "path_pc";
const mobileField = resourcesType === "video" ? "path_vid_mo" : "path_mo";
const { resourcesType = 'image' } = options
const pcField = resourcesType === 'video' ? 'path_vid_pc' : 'path_pc'
const mobileField = resourcesType === 'video' ? 'path_vid_mo' : 'path_mo'
if (!pathArray?.[mobileField]) {
return null;
return null
}
const resolvedImages = {
pc: getResolvedHost(pathArray[pcField] || pathArray[mobileField]),
mobile: getResolvedHost(pathArray[mobileField]),
};
}
if (resourcesType === "bg") {
if (resourcesType === 'bg') {
return {
"--pc-bg": `url(${resolvedImages.pc})`,
"--mobile-bg": `url(${resolvedImages.mobile})`,
};
'--pc-bg': `url(${resolvedImages.pc})`,
'--mobile-bg': `url(${resolvedImages.mobile})`,
}
}
return {
mobileSrc: resolvedImages.mobile,
pcSrc: resolvedImages.pc,
};
};
}
}

View File

@@ -3,13 +3,13 @@ export const csrDecodedJWT = (base64EncodeVal: string) => {
decodeURIComponent(
window
.atob(base64EncodeVal)
.split("")
.split('')
.map(function (c) {
return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
})
.join("")
.join('')
)
);
)
return decodeVal;
};
return decodeVal
}

View File

@@ -1,78 +1,78 @@
import { csrDecodedJWT } from "#layers/utils/jwtUtil";
import { csrDecodedJWT } from '#layers/utils/jwtUtil'
/**
* Stove 로그인
*/
export const csrGoStoveLogin = () => {
const runtimeConfig = useRuntimeConfig();
const runtimeConfig = useRuntimeConfig()
const loginUrl = runtimeConfig.public.stoveLoginUrl;
const stoveGameId = runtimeConfig.public.stoveGameId;
const stoveGameNo = runtimeConfig.public.stoveGameNo;
const redirectUrl = encodeURIComponent(location.href);
const loginUrl = runtimeConfig.public.stoveLoginUrl
const stoveGameId = runtimeConfig.public.stoveGameId
const stoveGameNo = runtimeConfig.public.stoveGameNo
const redirectUrl = encodeURIComponent(location.href)
const url = `${loginUrl}?redirect_url=${redirectUrl}&inflow_path=${stoveGameId}&game_no=${stoveGameNo}`;
location.href = url;
};
const url = `${loginUrl}?redirect_url=${redirectUrl}&inflow_path=${stoveGameId}&game_no=${stoveGameNo}`
location.href = url
}
/**
* Stove memberNo 조회
*/
export const csrGetStoveMemberNo = () => {
let memberNo = 0;
let memberNo = 0
try {
const suat = useCookie("SUAT");
const suat = useCookie('SUAT')
if (suat.value !== undefined && suat.value !== "") {
const base64Payload = suat.value?.split(".")[1] ?? "";
const decodeVal = csrDecodedJWT(base64Payload);
memberNo = Number(`${decodeVal.member_no}`) ?? 0;
if (suat.value && suat.value !== '') {
const base64Payload = suat.value?.split('.')[1] ?? ''
const decodeVal = csrDecodedJWT(base64Payload)
memberNo = Number(`${decodeVal.member_no}`) || 0
} else {
memberNo = 0;
memberNo = 0
}
} catch (e) {
console.error("[Exception] stoveUtil.csrGetStoveMemberNo: ", e);
return 0;
console.error('[Exception] stoveUtil.csrGetStoveMemberNo: ', e)
return 0
}
return memberNo;
};
return memberNo
}
/**
* AccessToken 조회
*/
export const csrGetAccessToken = () => {
let accessToken = "";
let accessToken = ''
try {
const suat = useCookie("SUAT");
const suat = useCookie('SUAT')
if (suat.value !== undefined && suat.value !== "") {
accessToken = `${suat.value || ""}`;
if (suat.value && suat.value !== '') {
accessToken = `${suat.value || ''}`
} else {
accessToken = "";
accessToken = ''
}
} catch (e) {
console.error("[Exception] stoveUtil.csrGetAccessToken: ", e);
return "";
console.error('[Exception] stoveUtil.csrGetAccessToken: ', e)
return ''
}
return accessToken;
};
return accessToken
}
/**
* 국가 코드 조회
*/
export const csrGetCountry = () => {
let countryCode = "";
let countryCode = ''
try {
const nnto = useCookie("NNTO");
const nnto = useCookie('NNTO')
if (nnto.value !== undefined && nnto.value !== "") {
countryCode = `${nnto.value || ""}`;
if (nnto.value !== undefined && nnto.value !== '') {
countryCode = `${nnto.value || ''}`
} else {
countryCode = "";
countryCode = ''
}
} catch (e) {
console.error("[Exception] stoveUtil.csrGetCountry: ", e);
return "";
console.error('[Exception] stoveUtil.csrGetCountry: ', e)
return ''
}
return countryCode;
};
return countryCode
}

View File

@@ -1,22 +1,22 @@
import { defineNuxtConfig } from "nuxt/config";
import { resolve } from "node:path";
import { getI18n } from "./i18n.config";
import { defineNuxtConfig } from 'nuxt/config'
import { resolve } from 'node:path'
import { getI18n } from './i18n.config'
export default defineNuxtConfig({
app: {
head: {
viewport: "width=device-width, initial-scale=1, maximum-scale=5",
viewport: 'width=device-width, initial-scale=1, maximum-scale=5',
script: [
{
type: "text/javascript",
type: 'text/javascript',
src: process.env.STOVE_GNB,
},
{
type: "text/javascript",
type: 'text/javascript',
src: process.env.STOVE_81PLUG,
},
{
type: "text/javascript",
type: 'text/javascript',
src: process.env.STOVE_LAUNCHER_SCRIPT,
async: true,
defer: true,
@@ -25,46 +25,44 @@ export default defineNuxtConfig({
},
},
modules: [
"@vueuse/nuxt",
"@nuxtjs/i18n",
"@pinia/nuxt",
"@nuxtjs/tailwindcss",
'@vueuse/nuxt',
'@nuxtjs/i18n',
'@pinia/nuxt',
'@nuxtjs/tailwindcss',
],
imports: {
dirs: [
"layers/types",
"layers/components",
"layers/composables",
"layers/layouts",
"layers/middleware",
"layers/plugins",
"layers/registry",
"layers/server",
"layers/stores",
"layers/utils",
'layers/types',
'layers/components',
'layers/composables',
'layers/layouts',
'layers/middleware',
'layers/plugins',
'layers/registry',
'layers/server',
'layers/stores',
'layers/utils',
],
global: true,
},
components: {
dirs: ["~/components", "layers/components"],
dirs: ['~/components', 'layers/components'],
global: true,
},
alias: {
"@": resolve(__dirname, "."),
"#layers": resolve(__dirname, "layers"),
'@': resolve(__dirname, '.'),
'#layers': resolve(__dirname, 'layers'),
},
extends: [resolve(__dirname, "layers")],
extends: ['./layers'],
// i18n 설정 - 런타임에 동적으로 설정됨
i18n: getI18n(),
experimental: {
payloadExtraction: false,
},
typescript: {
// [test] 타입 체크 비활성화
typeCheck: false,
strict: true,
typeCheck: true,
strict: false,
},
nitro: {
prerender: { routes: [] },
@@ -104,6 +102,6 @@ export default defineNuxtConfig({
// 개발 환경에서는 모든 호스트 허용
allowedHosts: true,
},
base: "/",
base: '/',
},
});
})

View File

@@ -11,7 +11,12 @@
"build:sandbox": "nuxt build --dotenv .env.sandbox",
"build:live": "nuxt build --dotenv .env.live",
"preview": "nuxt preview",
"typecheck": "nuxt typecheck"
"typecheck": "vue-tsc --noEmit",
"lint": "eslint . --ext .vue,.js,.ts,.jsx,.tsx",
"lint:fix": "eslint . --ext .vue,.js,.ts,.jsx,.tsx --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"check": "pnpm typecheck && pnpm lint && pnpm format:check"
},
"dependencies": {
"@nuxtjs/i18n": "^10.0.6",
@@ -24,12 +29,24 @@
"vue": "^3.5.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.35.0",
"@nuxt/eslint-config": "^1.9.0",
"@nuxtjs/tailwindcss": "^6.14.0",
"@types/node": "^24.3.0",
"@typescript-eslint/eslint-plugin": "^8.43.0",
"@typescript-eslint/parser": "^8.43.0",
"autoprefixer": "^10.4.21",
"eslint": "^9.35.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-nuxt": "^4.0.0",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-vue": "^10.4.0",
"postcss": "^8.5.6",
"prettier": "^3.6.2",
"tailwindcss": "^3.4.17",
"typescript": "^5.5.0"
"typescript": "^5.5.0",
"vue-tsc": "^3.0.7"
},
"engines": {
"node": ">=18.20.0"

7117
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,25 @@
import type { Config } from "tailwindcss";
import type { Config } from 'tailwindcss'
export default {
content: ["./app/**/*.{js,vue,ts}", "./layers/**/*.{js,vue,ts}"],
content: ['./app/**/*.{js,vue,ts}', './layers/**/*.{js,vue,ts}'],
theme: {
extend: {
screens: {
xs: "360px", // Mobile: 360px ~ 767px
sm: "768px", // Tablet: 768px ~ 1023px
md: "1024px", // PC: 1024px ~ 1439px
lg: "1440px", // Large PC: 1440px+
xs: '360px', // Mobile: 360px ~ 767px
sm: '768px', // Tablet: 768px ~ 1023px
md: '1024px', // PC: 1024px ~ 1439px
lg: '1440px', // Large PC: 1440px+
},
spacing: {},
colors: {
"theme-foreground": "var(--foreground)",
"theme-foreground-10": "var(--foreground-10)",
'theme-foreground': 'var(--foreground)',
'theme-foreground-10': 'var(--foreground-10)',
"theme-foreground-reversal": "var(--foreground-reversal)",
"theme-foreground-reversal-10": "var(--foreground-reversal-10)",
"theme-foreground-reversal-30": "var(--foreground-reversal-30)",
"theme-foreground-reversal-40": "var(--foreground-reversal-40)",
"theme-foreground-reversal-70": "var(--foreground-reversal-70)",
'theme-foreground-reversal': 'var(--foreground-reversal)',
'theme-foreground-reversal-10': 'var(--foreground-reversal-10)',
'theme-foreground-reversal-30': 'var(--foreground-reversal-30)',
'theme-foreground-reversal-40': 'var(--foreground-reversal-40)',
'theme-foreground-reversal-70': 'var(--foreground-reversal-70)',
// "theme-primary": "var(--light-primary)",
// "theme-secondary": "var(--light-secondary)",
@@ -30,4 +30,4 @@ export default {
},
},
},
} satisfies Config;
} satisfies Config

View File

@@ -1,12 +1,19 @@
{
"extends": "./.nuxt/tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"types": ["nuxt", "node", "@nuxtjs/i18n"],
"moduleResolution": "bundler",
"paths": {
"@/*": ["."],
"#layers/*": ["layers/*"]
}
}
"strict": false,
"noImplicitAny": false,
"strictNullChecks": false,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true
},
"include": [
".nuxt/nuxt.d.ts",
".nuxt/auto-imports.d.ts",
"types/**/*",
"layers/**/*",
"app/**/*"
],
"exclude": [".nuxt/types/**/*", "node_modules"]
}