fix. 코드 정리 (format 적용, 불필요한 파일 제거)

This commit is contained in:
clkim
2026-01-05 13:58:35 +09:00
parent 1eb9ed5360
commit 743c3b2b3c
25 changed files with 534 additions and 1301 deletions

View File

@@ -1,92 +0,0 @@
# 메모리 누수 분석 및 개선 리포트
## 🔴 심각한 메모리 누수 (즉시 수정 필요)
### 1. **app.vue - watch가 정리되지 않음**
- **위치**: `app/app.vue:131-146`
- **문제**: `onMounted` 내부에서 생성된 `watch``onBeforeUnmount`에서 정리되지 않음
- **영향**: 컴포넌트가 언마운트되어도 watch가 계속 실행되어 메모리 누수 발생
### 2. **useSplideArrow.ts - 이벤트 리스너가 정리되지 않음**
- **위치**: `layers/composables/useSplideArrow.ts:55, 59`
- **문제**: `addEventListener`로 추가된 이벤트 리스너가 제거되지 않음
- **영향**: 컴포넌트가 언마운트되어도 이벤트 리스너가 남아있어 메모리 누수 발생
### 3. **useModalStore.ts - setTimeout이 정리되지 않음**
- **위치**: `layers/stores/useModalStore.ts:125`
- **문제**: `setTimeout`이 사용되지만 컴포넌트 언마운트 시 정리되지 않음
- **영향**: 컴포넌트가 언마운트된 후에도 타이머가 실행되어 메모리 누수 발생
### 4. **useLoadingStore.ts - setTimeout이 정리되지 않음**
- **위치**: `layers/stores/useLoadingStore.ts:38`
- **문제**: `setTimeout`이 사용되지만 정리되지 않음
- **영향**: 스토어가 초기화되어도 타이머가 실행되어 메모리 누수 발생
### 5. **Video.vue - setTimeout이 정리되지 않음**
- **위치**: `layers/components/atoms/Video.vue:34`
- **문제**: `setTimeout`이 사용되지만 컴포넌트 언마운트 시 정리되지 않음
- **영향**: 컴포넌트가 언마운트된 후에도 타이머가 실행되어 메모리 누수 발생
### 6. **GrGallery01/index.vue - setTimeout이 정리되지 않음**
- **위치**: `layers/templates/GrGallery01/index.vue:93`
- **문제**: `setTimeout`이 사용되지만 컴포넌트 언마운트 시 정리되지 않음
- **영향**: 컴포넌트가 언마운트된 후에도 타이머가 실행되어 메모리 누수 발생
### 7. **useGameStart.ts - setTimeout이 정리되지 않음**
- **위치**: `layers/composables/useGameStart.ts:70`
- **문제**: `setTimeout`이 사용되지만 정리되지 않음
- **영향**: 컴포저블이 사용되지 않아도 타이머가 실행되어 메모리 누수 발생
### 8. **Thumbnail.vue - 이벤트 리스너가 정리되지 않음**
- **위치**: `layers/components/blocks/slide/Thumbnail.vue:114`
- **문제**: `addArrowClickListeners`로 추가된 이벤트 리스너가 제거되지 않음
- **영향**: 컴포넌트가 언마운트되어도 이벤트 리스너가 남아있어 메모리 누수 발생
## 🟡 중간 수준 이슈 (개선 권장)
### 9. **amplitude.client.ts - 이벤트 리스너가 정리되지 않음**
- **위치**: `layers/plugins/amplitude.client.ts:34`
- **문제**: `window.addEventListener('pagehide')`가 추가되지만 제거되지 않음
- **영향**: 플러그인은 앱 전체 생명주기 동안 유지되므로 큰 문제는 아니지만, 명시적으로 정리하는 것이 좋음
### 10. **app.vue - removeEventListener 사용 오류**
- **위치**: `app/app.vue:150`
- **문제**: `useEventListener`로 추가한 이벤트 리스너를 `removeEventListener`로 제거하려고 함
- **영향**: `useEventListener`는 자동으로 정리되므로 불필요한 코드 (메모리 누수는 아님)
## 📋 수정 우선순위
1.**즉시 수정** (메모리 누수): #1, #2, #3, #4, #5, #6, #7, #8 - **완료**
2. **단기 개선** (안정성): #9, #10 - 추후 검토
## ✅ 수정 완료 내역
### 수정된 파일 목록
1. `app/app.vue` - watch 정리 추가, removeEventListener 제거
2. `layers/composables/useSplideArrow.ts` - 이벤트 리스너 제거 함수 반환하도록 수정
3. `layers/components/blocks/slide/Thumbnail.vue` - 이벤트 리스너 정리 추가
4. `layers/stores/useModalStore.ts` - setTimeout 정리 추가
5. `layers/stores/useLoadingStore.ts` - setTimeout 정리 추가
6. `layers/components/atoms/Video.vue` - setTimeout 정리 추가
7. `layers/templates/GrGallery01/index.vue` - setTimeout 정리 추가
8. `layers/composables/useGameStart.ts` - setTimeout 정리 추가
### 수정 방법 요약
- **watch 정리**: `watch` 반환값을 저장하고 `onBeforeUnmount`에서 호출
- **setTimeout 정리**: 타이머 ID를 저장하고 `onBeforeUnmount` 또는 함수 재호출 시 `clearTimeout`으로 정리
- **이벤트 리스너 정리**: 이벤트 리스너 제거 함수를 반환하고 `onBeforeUnmount`에서 호출
## 🎯 개선 효과
1. **메모리 누수 방지**: 컴포넌트 언마운트 시 모든 리소스가 정리되어 메모리 누수 방지
2. **성능 향상**: 불필요한 타이머 및 이벤트 리스너 제거로 성능 향상
3. **안정성 향상**: 컴포넌트 생명주기 관리가 명확해져 버그 발생 가능성 감소
4. **코드 품질 향상**: 리소스 정리 패턴이 일관되어 유지보수성 향상
## 📝 추가 권장 사항
1. **자동 정리 유틸 함수**: 공통 패턴을 유틸 함수로 추출하여 재사용성 향상
2. **테스트 강화**: 컴포넌트 언마운트 시 리소스 정리 테스트 추가
3. **ESLint 규칙**: 메모리 누수 방지를 위한 ESLint 규칙 추가 고려
4. **성능 모니터링**: 수정 후 메모리 사용량 모니터링

View File

@@ -1,113 +0,0 @@
# SSR/CSR 동작 분석 및 개선 리포트
## 🔴 심각한 버그 (✅ 수정 완료)
### 1. ✅ **app.vue - Google Analytics 초기화가 SSR에서 실행됨**
- **위치**: `app/app.vue:115-120`
- **문제**: `useGtag()` 초기화가 SSR에서 실행되어 서버 사이드 에러 발생 가능
- **영향**: 서버 렌더링 시 에러 발생, 성능 저하
- **수정**: `if (import.meta.client)` 체크 추가
### 2. ✅ **useAnalytics.ts - window/navigator/location 사용 시 클라이언트 체크 없음**
- **위치**: `layers/composables/useAnalytics.ts:108, 201-202, 255`
- **문제**: `window.location`, `navigator.userAgent`, `location.href` 사용 시 `import.meta.client` 체크 없음
- **영향**: SSR에서 `window is not defined` 에러 발생
- **수정**: 모든 분석 함수에 `if (!import.meta.client) return` 체크 추가
### 3. ✅ **useScrollStore.ts - document.body 사용 시 클라이언트 체크 없음**
- **위치**: `layers/stores/useScrollStore.ts:27, 29`
- **문제**: `document.body.classList` 조작 시 `import.meta.client` 체크 없음
- **영향**: SSR에서 `document is not defined` 에러 발생
- **수정**: `controlScrollLock` 함수에 `if (!import.meta.client) return` 체크 추가
### 4. ✅ **useModalStore.ts - document.body 사용 시 클라이언트 체크 없음**
- **위치**: `layers/stores/useModalStore.ts:29, 31`
- **문제**: `document.body.classList` 조작 시 `import.meta.client` 체크 없음
- **영향**: SSR에서 `document is not defined` 에러 발생
- **수정**: `handleControlDimmed` 함수에 `if (!import.meta.client) return` 체크 추가
### 5. ✅ **localeUtil.ts - navigator 사용 시 클라이언트 체크 없음**
- **위치**: `layers/utils/localeUtil.ts:76`
- **문제**: `navigator.language` 사용 시 `import.meta.client` 체크 없음
- **영향**: SSR에서 `navigator is not defined` 에러 발생
- **수정**: 브라우저 언어 체크 부분을 `if (import.meta.client)` 블록으로 감쌈
### 6. ✅ **stoveUtil.ts - location 사용 시 클라이언트 체크 없음**
- **위치**: `layers/utils/stoveUtil.ts:18, 21`
- **문제**: `location.href` 사용 시 `import.meta.client` 체크 없음
- **영향**: SSR에서 `location is not defined` 에러 발생
- **수정**: `csrGoStoveLogin` 함수에 `if (!import.meta.client) return` 체크 추가
## 🟡 중간 수준 이슈 (개선 권장)
### 7. ✅ **pageData.global.ts - window.history 사용**
- **위치**: `layers/middleware/pageData.global.ts:103`
- **문제**: `window.history.replaceState` 사용은 체크되어 있지만, 더 안전한 처리 필요
- **영향**: 미미하지만 타입 안정성 개선 가능
- **수정**: `if (import.meta.client)` 체크 추가
### 8. **Header.vue - 불필요한 클라이언트 체크**
- **위치**: `layers/components/layouts/Header.vue:55, 66`
- **문제**: `hasActiveChild`, `isNavItemActive` 함수에서 클라이언트 체크가 있지만, computed에서 사용 시 불필요한 체크
- **영향**: 성능 미미한 저하
- **상태**: 현재 동작에는 문제 없으나, 성능 최적화를 위해 추후 개선 가능
### 9. ✅ **app.vue - scroll 이벤트 리스너**
- **위치**: `app/app.vue:125`
- **문제**: `useEventListener`는 클라이언트에서만 동작하지만, 명시적 체크 없음
- **영향**: 미미하지만 명확성 개선 가능
- **수정**: `onMounted` 내부에 `if (!import.meta.client) return` 체크 추가
## 🟢 성능 개선 사항
### 10. **ClientOnly 컴포넌트 최적화**
- **위치**: `layers/components/layouts/Header.vue:386-416`
- **개선**: 이미 `ClientOnly`로 감싸져 있으나, 더 세밀한 최적화 가능
- **영향**: 초기 렌더링 성능 개선
### 11. **불필요한 SSR 실행 방지**
- **위치**: 여러 컴포저블 및 유틸 함수
- **개선**: 클라이언트 전용 함수는 명시적으로 `.client.ts` 확장자 사용 권장
- **영향**: 번들 크기 및 초기 로드 시간 개선
### 12. **useWindowSize 사용 최적화**
- **위치**: `layers/components/layouts/Header.vue:14, 121`
- **개선**: `useWindowSize`는 클라이언트에서만 동작하지만, SSR에서도 초기화됨
- **영향**: 미미한 성능 개선
## 📋 수정 우선순위
1.**즉시 수정** (버그): #1, #2, #3, #4, #5, #6 - **완료**
2.**단기 개선** (안정성): #7, #9 - **완료** (#8은 성능 최적화로 추후 검토)
3. **중기 개선** (성능): #10, #11, #12 - 추후 검토
## ✅ 수정 완료 내역
### 수정된 파일 목록
1. `app/app.vue` - Google Analytics 초기화 및 scroll 이벤트 리스너 클라이언트 체크 추가
2. `layers/composables/useAnalytics.ts` - 모든 분석 함수에 클라이언트 체크 추가
3. `layers/stores/useScrollStore.ts` - `controlScrollLock` 함수에 클라이언트 체크 추가
4. `layers/stores/useModalStore.ts` - `handleControlDimmed` 함수에 클라이언트 체크 추가
5. `layers/utils/localeUtil.ts` - 브라우저 언어 체크에 클라이언트 체크 추가
6. `layers/utils/stoveUtil.ts` - `csrGoStoveLogin` 함수에 클라이언트 체크 추가
7. `layers/middleware/pageData.global.ts` - `window.history` 사용 시 클라이언트 체크 추가
### 수정 방법 요약
- 모든 브라우저 전용 API (`window`, `document`, `navigator`, `location`) 사용 시 `import.meta.client` 체크 추가
- 클라이언트 전용 함수는 함수 시작 부분에 `if (!import.meta.client) return` 추가
- 클라이언트 전용 코드 블록은 `if (import.meta.client) { ... }` 로 감쌈
## 🎯 개선 효과
1. **SSR 안정성 향상**: 서버 사이드에서 브라우저 API 접근 시 발생하던 에러 방지
2. **타입 안정성 개선**: 클라이언트 전용 코드의 명확한 구분
3. **성능 개선**: 불필요한 서버 사이드 실행 방지로 초기 렌더링 성능 향상
4. **코드 품질 향상**: SSR/CSR 경계가 명확해져 유지보수성 향상
## 📝 추가 권장 사항
1. **클라이언트 전용 유틸 함수 분리**: `csr` 접두사가 붙은 함수들은 `.client.ts` 확장자로 분리 고려
2. **타입 가드 활용**: TypeScript 타입 가드를 활용하여 클라이언트 전용 코드 타입 안정성 향상
3. **테스트 강화**: SSR/CSR 환경에서의 동작 테스트 추가
4. **성능 모니터링**: 수정 후 SSR 성능 및 에러 로그 모니터링

View File

@@ -50,7 +50,9 @@
text-color="#FFFFFF"
@click="handleError"
>
<span v-dompurify-html="`${gameName} ${tm('Error_Official_Page')}`"></span>
<span
v-dompurify-html="`${gameName} ${tm('Error_Official_Page')}`"
></span>
</AtomsButton>
</div>
</div>
@@ -125,7 +127,7 @@ const handleKeydown = (e: KeyboardEvent) => {
// 500 에러 발생 시 /error 페이지로 리다이렉트
onMounted(() => {
const statusCode = currentError.value?.statusCode
console.log("🚀 ~ statusCode:", nuxtError)
console.log('🚀 ~ statusCode:', nuxtError)
if (statusCode === 500) {
errorTitle.value = tm('Error_500_Inconvenience')

View File

@@ -15,8 +15,8 @@ module.exports = {
merge_logs: true, // 클러스터 모드 사용 시 각 클러스터에서 생성되는 로그를 한 파일로 병합
env: {
PORT: 3000,
NODE_ENV: 'dev'
}
NODE_ENV: 'dev',
},
},
{
name: 'dev2',
@@ -33,8 +33,8 @@ module.exports = {
merge_logs: true, // 클러스터 모드 사용 시 각 클러스터에서 생성되는 로그를 한 파일로 병합
env: {
PORT: 3000,
NODE_ENV: 'dev'
}
NODE_ENV: 'dev',
},
},
{
name: 'qa',
@@ -51,8 +51,8 @@ module.exports = {
merge_logs: true, // 클러스터 모드 사용 시 각 클러스터에서 생성되는 로그를 한 파일로 병합
env: {
PORT: 3000,
NODE_ENV: 'qa'
}
NODE_ENV: 'qa',
},
},
{
name: 'qa2',
@@ -69,8 +69,8 @@ module.exports = {
merge_logs: true, // 클러스터 모드 사용 시 각 클러스터에서 생성되는 로그를 한 파일로 병합
env: {
PORT: 3000,
NODE_ENV: 'qa'
}
NODE_ENV: 'qa',
},
},
{
name: 'perf',
@@ -87,8 +87,8 @@ module.exports = {
merge_logs: true, // 클러스터 모드 사용 시 각 클러스터에서 생성되는 로그를 한 파일로 병합
env: {
PORT: 3000,
NODE_ENV: 'perf'
}
NODE_ENV: 'perf',
},
},
{
name: 'sandbox',
@@ -105,8 +105,8 @@ module.exports = {
merge_logs: true, // 클러스터 모드 사용 시 각 클러스터에서 생성되는 로그를 한 파일로 병합
env: {
PORT: 3000,
NODE_ENV: 'sandbox'
}
NODE_ENV: 'sandbox',
},
},
{
name: 'live',
@@ -123,8 +123,8 @@ module.exports = {
merge_logs: true, // 클러스터 모드 사용 시 각 클러스터에서 생성되는 로그를 한 파일로 병합
env: {
PORT: 3000,
NODE_ENV: 'live'
}
}
]
NODE_ENV: 'live',
},
},
],
}

View File

@@ -1,514 +0,0 @@
{
"ko": {
"common": {
"loading": "로딩 중...",
"error": "오류가 발생했습니다",
"success": "성공했습니다",
"cancel": "취소",
"confirm": "확인",
"save": "저장",
"delete": "삭제",
"edit": "편집",
"close": "닫기",
"back": "뒤로",
"next": "다음",
"previous": "이전",
"search": "검색",
"filter": "필터",
"sort": "정렬",
"refresh": "새로고침",
"download": "다운로드",
"upload": "업로드",
"copy": "복사",
"paste": "붙여넣기",
"cut": "잘라내기",
"undo": "실행 취소",
"redo": "다시 실행"
},
"navigation": {
"home": "홈",
"about": "소개",
"contact": "연락처",
"services": "서비스",
"products": "제품",
"news": "뉴스",
"support": "지원",
"login": "로그인",
"logout": "로그아웃",
"register": "회원가입",
"profile": "프로필",
"settings": "설정"
},
"messages": {
"title_test_lang": "언어 설정 테스트!",
"welcome": "환영합니다!",
"GameData_load_status": "GameData 로드 상태",
"current_language": "현재 언어",
"default_language": "기본 언어",
"available_languages": "사용 가능한 언어",
"current_url": "현재 URL",
"no_results": "검색 결과가 없습니다",
"try_again": "다시 시도해주세요"
}
},
"en": {
"common": {
"loading": "Loading...",
"error": "An error occurred",
"success": "Success",
"cancel": "Cancel",
"confirm": "Confirm",
"save": "Save",
"delete": "Delete",
"edit": "Edit",
"close": "Close",
"back": "Back",
"next": "Next",
"previous": "Previous",
"search": "Search",
"filter": "Filter",
"sort": "Sort",
"refresh": "Refresh",
"download": "Download",
"upload": "Upload",
"copy": "Copy",
"paste": "Paste",
"cut": "Cut",
"undo": "Undo",
"redo": "Redo"
},
"navigation": {
"home": "Home",
"about": "About",
"contact": "Contact",
"services": "Services",
"products": "Products",
"news": "News",
"support": "Support",
"login": "Login",
"logout": "Logout",
"register": "Register",
"profile": "Profile",
"settings": "Settings"
},
"messages": {
"welcome": "Welcome!",
"goodbye": "Goodbye",
"thank_you": "Thank you",
"sorry": "Sorry",
"please_wait": "Please wait",
"no_data": "No data available",
"no_results": "No results found",
"try_again": "Please try again"
}
},
"zh-tw": {
"common": {
"loading": "載入中...",
"error": "發生錯誤",
"success": "成功",
"cancel": "取消",
"confirm": "確認",
"save": "儲存",
"delete": "刪除",
"edit": "編輯",
"close": "關閉",
"back": "返回",
"next": "下一步",
"previous": "上一步",
"search": "搜尋",
"filter": "篩選",
"sort": "排序",
"refresh": "重新整理",
"download": "下載",
"upload": "上傳",
"copy": "複製",
"paste": "貼上",
"cut": "剪下",
"undo": "復原",
"redo": "重做"
},
"navigation": {
"home": "首頁",
"about": "關於我們",
"contact": "聯絡我們",
"services": "服務",
"products": "產品",
"news": "新聞",
"support": "支援",
"login": "登入",
"logout": "登出",
"register": "註冊",
"profile": "個人資料",
"settings": "設定"
},
"messages": {
"title_test_lang": "語言設定測試!",
"welcome": "歡迎!",
"GameData_load_status": "GameData 載入狀態",
"current_language": "目前語言",
"default_language": "預設語言",
"available_languages": "可用語言",
"current_url": "目前網址",
"no_results": "找不到結果",
"try_again": "請再試一次"
}
},
"ja": {
"common": {
"loading": "読み込み中...",
"error": "エラーが発生しました",
"success": "成功",
"cancel": "キャンセル",
"confirm": "確認",
"save": "保存",
"delete": "削除",
"edit": "編集",
"close": "閉じる",
"back": "戻る",
"next": "次へ",
"previous": "前へ",
"search": "検索",
"filter": "フィルター",
"sort": "並び替え",
"refresh": "更新",
"download": "ダウンロード",
"upload": "アップロード",
"copy": "コピー",
"paste": "貼り付け",
"cut": "切り取り",
"undo": "元に戻す",
"redo": "やり直し"
},
"navigation": {
"home": "ホーム",
"about": "概要",
"contact": "お問い合わせ",
"services": "サービス",
"products": "製品",
"news": "ニュース",
"support": "サポート",
"login": "ログイン",
"logout": "ログアウト",
"register": "登録",
"profile": "プロフィール",
"settings": "設定"
},
"messages": {
"welcome": "ようこそ!",
"goodbye": "さようなら",
"thank_you": "ありがとうございます",
"sorry": "申し訳ありません",
"please_wait": "お待ちください",
"no_data": "データがありません",
"no_results": "結果が見つかりません",
"try_again": "もう一度お試しください"
}
},
"fr": {
"common": {
"loading": "Chargement...",
"error": "Une erreur s'est produite",
"success": "Succès",
"cancel": "Annuler",
"confirm": "Confirmer",
"save": "Enregistrer",
"delete": "Supprimer",
"edit": "Modifier",
"close": "Fermer",
"back": "Retour",
"next": "Suivant",
"previous": "Précédent",
"search": "Rechercher",
"filter": "Filtrer",
"sort": "Trier",
"refresh": "Actualiser",
"download": "Télécharger",
"upload": "Téléverser",
"copy": "Copier",
"paste": "Coller",
"cut": "Couper",
"undo": "Annuler",
"redo": "Rétablir"
},
"navigation": {
"home": "Accueil",
"about": "À propos",
"contact": "Contact",
"services": "Services",
"products": "Produits",
"news": "Actualités",
"support": "Support",
"login": "Connexion",
"logout": "Déconnexion",
"register": "S'inscrire",
"profile": "Profil",
"settings": "Paramètres"
},
"messages": {
"welcome": "Bienvenue !",
"goodbye": "Au revoir",
"thank_you": "Merci",
"sorry": "Désolé",
"please_wait": "Veuillez patienter",
"no_data": "Aucune donnée disponible",
"no_results": "Aucun résultat trouvé",
"try_again": "Veuillez réessayer"
}
},
"de": {
"common": {
"loading": "Laden...",
"error": "Ein Fehler ist aufgetreten",
"success": "Erfolg",
"cancel": "Abbrechen",
"confirm": "Bestätigen",
"save": "Speichern",
"delete": "Löschen",
"edit": "Bearbeiten",
"close": "Schließen",
"back": "Zurück",
"next": "Weiter",
"previous": "Vorherige",
"search": "Suchen",
"filter": "Filtern",
"sort": "Sortieren",
"refresh": "Aktualisieren",
"download": "Herunterladen",
"upload": "Hochladen",
"copy": "Kopieren",
"paste": "Einfügen",
"cut": "Ausschneiden",
"undo": "Rückgängig",
"redo": "Wiederholen"
},
"navigation": {
"home": "Startseite",
"about": "Über uns",
"contact": "Kontakt",
"services": "Dienstleistungen",
"products": "Produkte",
"news": "Nachrichten",
"support": "Support",
"login": "Anmelden",
"logout": "Abmelden",
"register": "Registrieren",
"profile": "Profil",
"settings": "Einstellungen"
},
"messages": {
"welcome": "Willkommen!",
"goodbye": "Auf Wiedersehen",
"thank_you": "Danke",
"sorry": "Entschuldigung",
"please_wait": "Bitte warten",
"no_data": "Keine Daten verfügbar",
"no_results": "Keine Ergebnisse gefunden",
"try_again": "Bitte versuchen Sie es erneut"
}
},
"es": {
"common": {
"loading": "Cargando...",
"error": "Ocurrió un error",
"success": "Éxito",
"cancel": "Cancelar",
"confirm": "Confirmar",
"save": "Guardar",
"delete": "Eliminar",
"edit": "Editar",
"close": "Cerrar",
"back": "Atrás",
"next": "Siguiente",
"previous": "Anterior",
"search": "Buscar",
"filter": "Filtrar",
"sort": "Ordenar",
"refresh": "Actualizar",
"download": "Descargar",
"upload": "Subir",
"copy": "Copiar",
"paste": "Pegar",
"cut": "Cortar",
"undo": "Deshacer",
"redo": "Rehacer"
},
"navigation": {
"home": "Inicio",
"about": "Acerca de",
"contact": "Contacto",
"services": "Servicios",
"products": "Productos",
"news": "Noticias",
"support": "Soporte",
"login": "Iniciar sesión",
"logout": "Cerrar sesión",
"register": "Registrarse",
"profile": "Perfil",
"settings": "Configuración"
},
"messages": {
"welcome": "¡Bienvenido!",
"goodbye": "Adiós",
"thank_you": "Gracias",
"sorry": "Lo siento",
"please_wait": "Por favor espere",
"no_data": "No hay datos disponibles",
"no_results": "No se encontraron resultados",
"try_again": "Por favor intente de nuevo"
}
},
"pt": {
"common": {
"loading": "Carregando...",
"error": "Ocorreu um erro",
"success": "Sucesso",
"cancel": "Cancelar",
"confirm": "Confirmar",
"save": "Salvar",
"delete": "Excluir",
"edit": "Editar",
"close": "Fechar",
"back": "Voltar",
"next": "Próximo",
"previous": "Anterior",
"search": "Pesquisar",
"filter": "Filtrar",
"sort": "Ordenar",
"refresh": "Atualizar",
"download": "Baixar",
"upload": "Enviar",
"copy": "Copiar",
"paste": "Colar",
"cut": "Cortar",
"undo": "Desfazer",
"redo": "Refazer"
},
"navigation": {
"home": "Início",
"about": "Sobre",
"contact": "Contato",
"services": "Serviços",
"products": "Produtos",
"news": "Notícias",
"support": "Suporte",
"login": "Entrar",
"logout": "Sair",
"register": "Registrar",
"profile": "Perfil",
"settings": "Configurações"
},
"messages": {
"welcome": "Bem-vindo!",
"goodbye": "Tchau",
"thank_you": "Obrigado",
"sorry": "Desculpe",
"please_wait": "Por favor aguarde",
"no_data": "Nenhum dado disponível",
"no_results": "Nenhum resultado encontrado",
"try_again": "Por favor tente novamente"
}
},
"th": {
"common": {
"loading": "กำลังโหลด...",
"error": "เกิดข้อผิดพลาด",
"success": "สำเร็จ",
"cancel": "ยกเลิก",
"confirm": "ยืนยัน",
"save": "บันทึก",
"delete": "ลบ",
"edit": "แก้ไข",
"close": "ปิด",
"back": "กลับ",
"next": "ถัดไป",
"previous": "ก่อนหน้า",
"search": "ค้นหา",
"filter": "กรอง",
"sort": "เรียงลำดับ",
"refresh": "รีเฟรช",
"download": "ดาวน์โหลด",
"upload": "อัปโหลด",
"copy": "คัดลอก",
"paste": "วาง",
"cut": "ตัด",
"undo": "เลิกทำ",
"redo": "ทำซ้ำ"
},
"navigation": {
"home": "หน้าแรก",
"about": "เกี่ยวกับ",
"contact": "ติดต่อ",
"services": "บริการ",
"products": "ผลิตภัณฑ์",
"news": "ข่าวสาร",
"support": "สนับสนุน",
"login": "เข้าสู่ระบบ",
"logout": "ออกจากระบบ",
"register": "สมัครสมาชิก",
"profile": "โปรไฟล์",
"settings": "การตั้งค่า"
},
"messages": {
"welcome": "ยินดีต้อนรับ!",
"goodbye": "ลาก่อน",
"thank_you": "ขอบคุณ",
"sorry": "ขออภัย",
"please_wait": "กรุณารอสักครู่",
"no_data": "ไม่มีข้อมูล",
"no_results": "ไม่พบผลลัพธ์",
"try_again": "กรุณาลองใหม่อีกครั้ง"
}
},
"zh-cn": {
"common": {
"loading": "加载中...",
"error": "发生错误",
"success": "成功",
"cancel": "取消",
"confirm": "确认",
"save": "保存",
"delete": "删除",
"edit": "编辑",
"close": "关闭",
"back": "返回",
"next": "下一步",
"previous": "上一步",
"search": "搜索",
"filter": "筛选",
"sort": "排序",
"refresh": "刷新",
"download": "下载",
"upload": "上传",
"copy": "复制",
"paste": "粘贴",
"cut": "剪切",
"undo": "撤销",
"redo": "重做"
},
"navigation": {
"home": "首页",
"about": "关于我们",
"contact": "联系我们",
"services": "服务",
"products": "产品",
"news": "新闻",
"support": "支持",
"login": "登录",
"logout": "退出",
"register": "注册",
"profile": "个人资料",
"settings": "设置"
},
"messages": {
"welcome": "欢迎!",
"goodbye": "再见",
"thank_you": "谢谢",
"sorry": "抱歉",
"please_wait": "请稍候",
"no_data": "无可用数据",
"no_results": "未找到结果",
"try_again": "请重试"
}
}
}

View File

@@ -44,7 +44,7 @@ const imagePaths = computed(() => {
<source media="(min-width: 1024px)" :srcset="imagePaths.pc" />
<source media="(max-width: 1023px)" :srcset="imagePaths.mo" />
<img
:src="imagePaths.pc"
:src="imagePaths.mo"
:alt="alt"
v-bind="$attrs"
:loading="priority === 'high' ? 'eager' : 'lazy'"

View File

@@ -121,9 +121,9 @@ const calculateTooltipPosition = (trigger: HTMLElement) => {
Object.values(isPositions.value).filter(Boolean).length === 1
let topPosition: number | null = null
let bottomPosition: number | null = null
const bottomPosition: number | null = null
let leftPosition: number | null = null
let rightPosition: number | null = null
const rightPosition: number | null = null
if (isLefts) {
if (isOnlyLeft) {

View File

@@ -52,7 +52,7 @@ const mainOptions = computed<Options>(() => ({
pagination: false,
drag: props.drag,
updateOnMove: true,
lazyLoad: 'nearby', // 성능 최적화: 이미지 지연 로딩
lazyLoad: 'nearby',
}))
const thumbOptions = computed<Options>(() => ({

View File

@@ -15,29 +15,6 @@ declare const ttq: any
// 유틸 함수
// ============================================================================
// target에 {XX1, XX2}와 같은 형태가 포함되어 있을 경우 options.clickItem으로부터 값 추출하여 세팅 [TODO: ]
const findValueFromOption = (target: string, { options = {} }: any) => {
if (target.includes('{') && target.includes('}')) {
const strTargetClickItem = target.substring(
target.indexOf('{') + 1,
target.indexOf('}')
)
const arrTargetClickItem = strTargetClickItem.split(',')
const arrTargetClickItemValue = []
for (let targetClickItem of arrTargetClickItem) {
targetClickItem = targetClickItem.trim()
arrTargetClickItemValue.push(options.clickItem[targetClickItem])
}
target = target.replaceAll(
`{${strTargetClickItem}}`,
arrTargetClickItemValue.join(',')
)
}
return target
}
/** 브라우저 환경인지 체크 */
const isClient = () => typeof window !== 'undefined' && import.meta.client
@@ -404,7 +381,6 @@ const sendMarketingLog = ({
export default () => {
return {
sendSA,
sendLog,
sendMarketingLog,
useAnalyticsData,

View File

@@ -91,7 +91,7 @@ export const useCheckGameStart = () => {
clearTimeout(launcherTimeoutId)
launcherTimeoutId = null
}
isShowCheckLauncher.value = false
isShowDownloadLauncher.value = false
}

View File

@@ -1,52 +0,0 @@
import type {
GameDataResponse,
GameDataRequest,
} from '#layers/types/api/gameData'
export const useGetGameDataExternal = () => {
const { setGameData } = useGameDataStore()
const logPrefix = {
exception: '[Exception] /composables/useGetGameDataExternal',
failure: '[Failure] /composables/useGetGameDataExternal',
}
const webGameData = ref<GameDataResponse | null>(null)
const getGameDataExternal = async (req: GameDataRequest) => {
const runtimeConfig = useRuntimeConfig()
const stoveApiBaseUrl = runtimeConfig.public.stoveApiUrl
const apiUrl = `${stoveApiBaseUrl}/pub-comm/v1.0/template/game?game_domain=${req.gameDomain}&lang_code=${req.langCode}`
try {
const response = (await commonFetch('GET', apiUrl)) as GameDataResponse
console.log('🚀 ~ getGameDataExternal:', response.value)
// FIXME: 테스트용 데이터 ---------------------------------------------------
/* if (['local', 'local-gate8', 'dev'].includes(`${runtimeConfig.public.runType}`)) {
response.value = {
inspection_status: 1,
inspection: {
inspection_status: 1,
start_date: '2025-09-19 10:00:00',
end_date: '2025-09-19 12:00:00',
ts_start_date: new Date().getTime(),
ts_end_date: new Date().getTime(),
back_ground_image_type: 'image',
back_ground_image_url: 'https://www.onstove.com',
inspection_title1: '',
inspection_title2: ''
}
}
} */
// ------------------------------------------------------------------------
if (response?.value) {
webGameData.value = response
setGameData(response.value)
}
} catch (e) {
console.error(`${logPrefix.exception}.getGameDataExternal: `, e)
}
}
return { webGameData, getGameDataExternal }
}

View File

@@ -9,5 +9,5 @@ export default defineNuxtConfig({
components: {
dirs: ['components'],
global: true,
}
},
})

View File

@@ -1,7 +1,7 @@
import * as amplitude from '@amplitude/analytics-browser'
// Nuxt 플러그인 정의
export default defineNuxtPlugin((nuxtApp) => {
export default defineNuxtPlugin(nuxtApp => {
// const { memberNo } = useAnalytics() as { memberNo: string }
const memberNo = csrGetStoveMemberNo()
@@ -13,11 +13,11 @@ export default defineNuxtPlugin((nuxtApp) => {
pageViews: true, // 페이지 뷰 추적 활성화
sessions: false, // 세션 추적 비활성화
formInteractions: false, // 폼 상호작용 추적 비활성화
fileDownloads: false // 파일 다운로드 추적 비활성화
fileDownloads: false, // 파일 다운로드 추적 비활성화
},
autocapture: {
attribution: true
}
attribution: true,
},
})
// Identify 이벤트 생성 및 설정
@@ -27,7 +27,6 @@ export default defineNuxtPlugin((nuxtApp) => {
// Identify 이벤트 전송 및 사용자 ID 설정
amplitude.identify(identifyEvent)
amplitude.setUserId(`${memberNo}`)
;(window as any).amplitude = amplitude // amplitude 객체 전역으로 설정(Stove GNB에서 사용)
// 페이지가 숨겨질 때 이벤트 리스너 추가

View File

@@ -1,11 +1,14 @@
import { getTrueClientIp } from '#layers/utils/apiUtil'
export default defineEventHandler((event) => {
export default defineEventHandler(event => {
let clientIP = ''
try {
clientIP = getTrueClientIp(event.node.req as any)
} catch (e) {
console.error('[Exception] /server/api/clientIp - Cannot Get Client IP: ', e)
console.error(
'[Exception] /server/api/clientIp - Cannot Get Client IP: ',
e
)
}
return clientIP || ''
})

View File

@@ -1,6 +1,6 @@
export default defineEventHandler(() => {
return {
code: 200,
message: 'success'
message: 'success',
}
})

View File

@@ -11,7 +11,7 @@ function getIpAddress(event: H3Event): string {
return getTrueClientIp(event.node.req as any) || 'unknown'
}
export default defineNitroPlugin((nitroApp) => {
export default defineNitroPlugin(nitroApp => {
// 정적 파일 체크 함수 추가
const isStaticFile = (path: string): boolean => {
return /\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$/i.test(path)
@@ -22,7 +22,7 @@ export default defineNitroPlugin((nitroApp) => {
return path === '/health'
}
nitroApp.hooks.hook('request', (event) => {
nitroApp.hooks.hook('request', event => {
// 정적 파일 요청은 로깅 제외
if (isStaticFile(event.path) || isHealthCheck(event.path)) {
return
@@ -51,18 +51,21 @@ export default defineNitroPlugin((nitroApp) => {
}
})
nitroApp.hooks.hook('error', (error) => {
nitroApp.hooks.hook('error', error => {
console.error('[Nitro Error]', {
message: error.message,
stack: error.stack,
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
})
})
// 응답 헤더에서 'x-powered-by' 제거
nitroApp.hooks.hook('render:response', (response: Partial<RenderResponse>) => {
if (response?.headers) {
delete response.headers['x-powered-by']
nitroApp.hooks.hook(
'render:response',
(response: Partial<RenderResponse>) => {
if (response?.headers) {
delete response.headers['x-powered-by']
}
}
})
)
})

View File

@@ -31,6 +31,6 @@ export const useInspectionStore = defineStore('inspection', () => {
setWebInspectionData,
setWebInspectionStatus,
setGameMaintenanceData,
setGameMaintenanceStatus
setGameMaintenanceStatus,
}
})

View File

@@ -1,13 +0,0 @@
export const useCallerInfoStore = defineStore('callerInfoStore', () => {
const callerId = ref<string | null>('')
const callerDetail = ref<string | null>('')
const setCallerId = (paramCallerId: string | null) => {
callerId.value = paramCallerId
}
const setCallerDetail = (paramCalleDetail: string | null) => {
callerDetail.value = paramCalleDetail
}
return { callerId, callerDetail, setCallerId, setCallerDetail }
})

View File

@@ -38,7 +38,6 @@ const { pageData } = storeToRefs(pageDataStore)
const { getOperateResources } = useOperateResources()
const { sendLog } = useAnalytics()
// Constants
const COLOR_INDEX = { BACKGROUND: 0, TEXT: 1 } as const
const preregistModalRef = ref<{
@@ -71,7 +70,6 @@ const preregistSNS = computed(
() => getSupportedPlatforms('2', gameData?.value?.os_type) as Platform[]
)
// SNS Buttons
const snsButtonsData = computed(() => {
const buttons = getComponentGroupAry(props.components, 'imgSnsButton')
const links = getComponentGroupAry(props.components, 'txtSnsLink')
@@ -85,7 +83,6 @@ const snsButtonsData = computed(() => {
}))
})
// Button Colors
const buttonColors = computed(() => {
const colorData = getComponentGroupAry(
props.components,
@@ -124,7 +121,6 @@ const accPaginationData = computed(() =>
getComponentGroupAry(props.components, 'pagination')
)
// Async Data - 리워드 완료 데이터
const { data: rewardCompletedData } = await useAsyncData(
`fx-preregist-resources-${pageData.value?.page_seq}-${pageData.value?.page_ver}-${props.pageVerTmplSeq}`,
async () => {

View File

@@ -96,5 +96,5 @@ export type {
// [E] Type in czn_homepage_brand_siteConfig.json ----------------------------------------
DataizationType,
ReqGetDataization,
ResGetDataization
ResGetDataization,
}

View File

@@ -111,7 +111,7 @@ export const hasComponentGroup = (
/**
* 컴포넌트 컨테이너를 반환합니다.
* @param components props.components
* @param components props.components 또는 group 객체
* @param componentName 컴포넌트 이름
* @param options 옵션
* - isGroup: groups 속성에서 데이터 가져오기 (기본값: false)

View File

@@ -78,11 +78,13 @@ export const getYouTubeUrl = (
* 유튜브 URL에서 비디오 ID를 추출하고, 비디오 ID로부터 썸네일 URL을 생성합니다.
* @param url - 유튜브 URL
* @param quality - 썸네일 품질 ('default', 'medium', 'high', 'standard', 'maxres')
* @param format - 이미지 포맷 ('jpg' | 'webp'), 기본값은 'webp'
* @returns 썸네일 URL
*/
export const getYouTubeThumbnail = (
url: string,
quality: 'default' | 'medium' | 'high' | 'standard' | 'maxres' = 'standard'
quality: 'default' | 'medium' | 'high' | 'standard' | 'maxres' = 'standard',
format: 'jpg' | 'webp' = 'webp'
): string => {
const videoId = getYouTubeId(url)
if (!videoId) return ''
@@ -95,5 +97,8 @@ export const getYouTubeThumbnail = (
maxres: 'maxresdefault',
}
return `https://img.youtube.com/vi/${videoId}/${qualityMap[quality]}.jpg`
const basePath = format === 'webp' ? 'vi_webp' : 'vi'
const extension = format === 'webp' ? 'webp' : 'jpg'
return `https://img.youtube.com/${basePath}/${videoId}/${qualityMap[quality]}.${extension}`
}

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,9 @@
"types/**/*",
"i18n/**/*",
"layers/**/*",
"app/**/*"
, "temp/inspection.ts", "temp/middleware.ts" ],
"app/**/*",
"temp/inspection.ts",
"temp/middleware.ts"
],
"exclude": [".nuxt/types/**/*", "node_modules"]
}

5
types/i18n.d.ts vendored
View File

@@ -1,8 +1,9 @@
declare global {
function defineI18nLocale(
loader: (locale: string) => Promise<Record<string, any>> | Record<string, any>
loader: (
locale: string
) => Promise<Record<string, any>> | Record<string, any>
): any
}
export {}