diff --git a/electron/main.cjs b/electron/main.cjs
index e606362..fd79574 100644
--- a/electron/main.cjs
+++ b/electron/main.cjs
@@ -1,10 +1,14 @@
// Slim Budget 데스크톱(Electron) 메인 프로세스.
// 라이브 사이트(https://app.sblog.kr)를 직접 로드한다 → 프론트 변경이 자동 반영(재설치 불필요),
// 같은 출처라 CORS 우회도 불필요. 배포된 웹 게이트가 Electron 을 감지해 안내페이지 대신 전체 앱을 띄운다.
-const { app, BrowserWindow, shell, session, ipcMain } = require('electron')
+const { app, BrowserWindow, shell, session, ipcMain, Menu } = require('electron')
const fs = require('fs')
const path = require('path')
+// 기본 애플리케이션 메뉴(File/Edit/View/Window) 제거 — 메인 창뿐 아니라
+// 구글 로그인 팝업 등 자식 창에도 메뉴바가 안 붙도록 전역으로 비활성화.
+Menu.setApplicationMenu(null)
+
// 네이티브 대화상자(alert/confirm) 제목은 app.getName() 을 쓴다 → package.json name('sb_pt') 대신 표기.
app.setName('돈돼지 가계부')
diff --git a/src/App.vue b/src/App.vue
index 1d910db..1fa8be6 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -36,12 +36,20 @@ function onUnauthorized() {
auth.clear()
ui.openLogin()
}
+// http.js 의 403 PREMIUM_REQUIRED 처리 → 업그레이드 안내 화면으로
+function onPremiumRequired() {
+ if (route.name !== 'upgrade') router.push({ name: 'upgrade' })
+}
onMounted(() => {
if (!isApp) return // 웹은 안내만 — 앱 전용 초기화 생략
window.addEventListener('auth:unauthorized', onUnauthorized)
+ window.addEventListener('premium:required', onPremiumRequired)
ui.loadSignupEnabled() // 회원가입 허용 여부 선로딩(랜딩/로그인 가입 버튼에 반영)
})
-onUnmounted(() => window.removeEventListener('auth:unauthorized', onUnauthorized))
+onUnmounted(() => {
+ window.removeEventListener('auth:unauthorized', onUnauthorized)
+ window.removeEventListener('premium:required', onPremiumRequired)
+})
// 페이지 이동 시 모바일 사이드바 자동 닫힘
watch(() => route.fullPath, () => ui.closeSidebar())
diff --git a/src/api/adminApi.js b/src/api/adminApi.js
index 210277b..48a9a94 100644
--- a/src/api/adminApi.js
+++ b/src/api/adminApi.js
@@ -62,6 +62,9 @@ export const adminApi = {
updateMemberRole(id, role) {
return http.put(`/admin/members/${id}/role`, { role })
},
+ updateMemberPlan(id, plan) {
+ return http.put(`/admin/members/${id}/plan`, { plan })
+ },
updateMemberStatus(id, status) {
return http.put(`/admin/members/${id}/status`, { status })
},
diff --git a/src/api/http.js b/src/api/http.js
index 4bffdc6..ca6b346 100644
--- a/src/api/http.js
+++ b/src/api/http.js
@@ -31,6 +31,9 @@ http.interceptors.response.use(
localStorage.removeItem('token')
localStorage.removeItem('auth_user')
window.dispatchEvent(new CustomEvent('auth:unauthorized'))
+ } else if (status === 403 && error.response?.data?.code === 'PREMIUM_REQUIRED') {
+ // 유료 전용 API 를 무료 회원이 호출(우회/직접 접근) — 업그레이드 안내
+ window.dispatchEvent(new CustomEvent('premium:required'))
}
return Promise.reject(error)
},
diff --git a/src/components/layout/AppSidebar.vue b/src/components/layout/AppSidebar.vue
index 1f8fc72..430b86e 100644
--- a/src/components/layout/AppSidebar.vue
+++ b/src/components/layout/AppSidebar.vue
@@ -10,6 +10,8 @@ const auth = useAuthStore()
const ui = useUiStore()
// 로그인 또는 둘러보기(데모)면 메뉴 노출. 데모에선 잠금 배지를 함께 표시.
const showMenu = computed(() => auth.isAuthenticated || demo.on)
+// 유료 전용 메뉴 잠금 배지: 데모이거나, 로그인했지만 무료 회원일 때.
+const lockPremium = computed(() => demo.on || (auth.isAuthenticated && !auth.isPremium))
// 메뉴 아이콘 (lucide 스타일 인라인 SVG path — 하단 내비와 동일 톤). 값은 정적/신뢰 마크업.
const icons = {
@@ -45,11 +47,12 @@ const icons = {