Merge branch 'dev'
@@ -0,0 +1,3 @@
|
|||||||
|
# 데스크톱(Electron) 빌드 전용 — `vite build --mode electron` 이 로드.
|
||||||
|
# file:// 로 로드되므로 백엔드 절대 URL 을 지정한다.
|
||||||
|
VITE_API_BASE_URL=https://app.sblog.kr/api
|
||||||
@@ -14,6 +14,9 @@ dist-ssr
|
|||||||
coverage
|
coverage
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
|
# Electron desktop build output
|
||||||
|
release-desktop
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
@@ -8,7 +7,6 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
@@ -16,12 +14,10 @@
|
|||||||
android:theme="@style/AppTheme.NoActionBarLaunch"
|
android:theme="@style/AppTheme.NoActionBarLaunch"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
@@ -29,9 +25,7 @@
|
|||||||
android:authorities="${applicationId}.fileprovider"
|
android:authorities="${applicationId}.fileprovider"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:grantUriPermissions="true">
|
android:grantUriPermissions="true">
|
||||||
<meta-data
|
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
|
||||||
android:resource="@xml/file_paths"></meta-data>
|
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
<!-- 카드 결제 알림 자동인식: 알림 접근 리스너 서비스 -->
|
<!-- 카드 결제 알림 자동인식: 알림 접근 리스너 서비스 -->
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@mipmap/ic_launcher_background" />
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@mipmap/ic_launcher_background" />
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 339 B |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 809 B |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 720 B |
|
After Width: | Height: | Size: 158 B |
|
After Width: | Height: | Size: 474 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 208 B |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 575 B |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 403 B |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 778 B |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 5.1 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
import sharp from 'sharp'
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
|
||||||
|
const files = ['icon-only', 'icon-foreground', 'icon-background']
|
||||||
|
for (const f of files) {
|
||||||
|
const svg = readFileSync(`assets/${f}.svg`)
|
||||||
|
await sharp(svg, { density: 384 }).resize(1024, 1024).png().toFile(`assets/${f}.png`)
|
||||||
|
console.log('wrote assets/' + f + '.png')
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
|
||||||
|
<rect width="1024" height="1024" fill="#00bc7e"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 151 B |
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
|
||||||
|
<!-- 적응형 아이콘 전경: 안전영역(중앙 ~66%) 안에 흰 막대 차트 -->
|
||||||
|
<g fill="#ffffff">
|
||||||
|
<rect x="262" y="492" width="120" height="280" rx="60"/>
|
||||||
|
<rect x="452" y="372" width="120" height="400" rx="60"/>
|
||||||
|
<rect x="642" y="252" width="120" height="520" rx="60"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 400 B |
|
After Width: | Height: | Size: 38 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
|
||||||
|
<!-- 레거시(비적응형) 아이콘: 초록 라운드 사각 + 흰 막대 차트 (브랜드 마크) -->
|
||||||
|
<rect width="1024" height="1024" rx="224" fill="#00bc7e"/>
|
||||||
|
<g fill="#ffffff">
|
||||||
|
<rect x="240" y="544" width="108.8" height="256" rx="54.4"/>
|
||||||
|
<rect x="457.6" y="400" width="108.8" height="400" rx="54.4"/>
|
||||||
|
<rect x="675.2" y="256" width="108.8" height="544" rx="54.4"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 496 B |
@@ -0,0 +1,43 @@
|
|||||||
|
// Slim Budget 데스크톱(Electron) 메인 프로세스.
|
||||||
|
// dist(웹 빌드, --mode electron)를 창에 로드한다. 백엔드는 https://app.sblog.kr/api 사용.
|
||||||
|
const { app, BrowserWindow, shell } = require('electron')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
function createWindow() {
|
||||||
|
const win = new BrowserWindow({
|
||||||
|
width: 1200,
|
||||||
|
height: 820,
|
||||||
|
minWidth: 360,
|
||||||
|
minHeight: 560,
|
||||||
|
backgroundColor: '#1a1a1a',
|
||||||
|
title: 'Slim Budget',
|
||||||
|
webPreferences: {
|
||||||
|
contextIsolation: true,
|
||||||
|
nodeIntegration: false,
|
||||||
|
// file:// 출처에서 자기 백엔드(https://app.sblog.kr/api) 호출 시 CORS 차단을 피한다.
|
||||||
|
// 신뢰된 로컬 dist + 자체 API 만 로드하는 데스크톱 클라이언트라 허용 가능한 완화.
|
||||||
|
webSecurity: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 기본 메뉴바 제거(앱 자체 내비게이션 사용)
|
||||||
|
win.removeMenu()
|
||||||
|
win.loadFile(path.join(__dirname, '..', 'dist', 'index.html'))
|
||||||
|
|
||||||
|
// 외부 링크(target=_blank 등)는 기본 브라우저로 열기
|
||||||
|
win.webContents.setWindowOpenHandler(({ url }) => {
|
||||||
|
if (/^https?:\/\//i.test(url)) shell.openExternal(url)
|
||||||
|
return { action: 'deny' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
app.whenReady().then(() => {
|
||||||
|
createWindow()
|
||||||
|
app.on('activate', () => {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (process.platform !== 'darwin') app.quit()
|
||||||
|
})
|
||||||
@@ -3,10 +3,14 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"main": "electron/main.cjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"build:app": "vite build --mode capacitor",
|
"build:app": "vite build --mode capacitor",
|
||||||
|
"build:electron": "vite build --mode electron",
|
||||||
|
"electron:dev": "npm run build:electron && electron .",
|
||||||
|
"electron:build": "npm run build:electron && electron-builder --win",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"cap:sync": "npm run build:app && cap sync",
|
"cap:sync": "npm run build:app && cap sync",
|
||||||
@@ -16,6 +20,26 @@
|
|||||||
"lint:oxlint": "oxlint . --fix",
|
"lint:oxlint": "oxlint . --fix",
|
||||||
"lint:eslint": "eslint . --fix --cache"
|
"lint:eslint": "eslint . --fix --cache"
|
||||||
},
|
},
|
||||||
|
"build": {
|
||||||
|
"appId": "kr.sblog.slimbudget",
|
||||||
|
"productName": "Slim Budget",
|
||||||
|
"directories": {
|
||||||
|
"output": "release-desktop"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/**/*",
|
||||||
|
"electron/**/*"
|
||||||
|
],
|
||||||
|
"win": {
|
||||||
|
"target": "nsis",
|
||||||
|
"icon": "assets/icon-only.png"
|
||||||
|
},
|
||||||
|
"nsis": {
|
||||||
|
"oneClick": false,
|
||||||
|
"perMachine": false,
|
||||||
|
"allowToChangeInstallationDirectory": true
|
||||||
|
}
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor/android": "^8.3.4",
|
"@capacitor/android": "^8.3.4",
|
||||||
"@capacitor/app": "^8.1.0",
|
"@capacitor/app": "^8.1.0",
|
||||||
@@ -33,10 +57,13 @@
|
|||||||
"vue-router": "^5.0.4"
|
"vue-router": "^5.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@capacitor/assets": "^3.0.5",
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@pinia/testing": "^1.0.3",
|
"@pinia/testing": "^1.0.3",
|
||||||
"@vitejs/plugin-vue": "^6.0.6",
|
"@vitejs/plugin-vue": "^6.0.6",
|
||||||
"@vue/test-utils": "^2.4.11",
|
"@vue/test-utils": "^2.4.11",
|
||||||
|
"electron": "^42.3.3",
|
||||||
|
"electron-builder": "^26.15.0",
|
||||||
"eslint": "^10.2.1",
|
"eslint": "^10.2.1",
|
||||||
"eslint-plugin-oxlint": "~1.60.0",
|
"eslint-plugin-oxlint": "~1.60.0",
|
||||||
"eslint-plugin-vue": "~10.8.0",
|
"eslint-plugin-vue": "~10.8.0",
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ const auth = useAuthStore()
|
|||||||
const ui = useUiStore()
|
const ui = useUiStore()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
// 앱(Capacitor 네이티브)에서만 전체 이용. 웹(브라우저)은 안내 페이지만 노출.
|
// 앱(Capacitor 네이티브) 또는 데스크톱(Electron) 클라이언트에서 전체 이용.
|
||||||
// 단, 개발 모드(npm run dev)에서는 브라우저로도 전체 앱을 테스트할 수 있게 예외.
|
// 웹(브라우저)은 안내 페이지만 노출. 개발 모드(npm run dev)는 예외로 전체 앱 표시.
|
||||||
const isApp = Capacitor.isNativePlatform() || import.meta.env.DEV
|
const isDesktop = /electron/i.test(navigator.userAgent)
|
||||||
|
const isApp = Capacitor.isNativePlatform() || import.meta.env.DEV || isDesktop
|
||||||
|
|
||||||
// http.js 의 401 처리에서 발생시키는 이벤트 → 세션 정리 후 로그인 팝업
|
// http.js 의 401 처리에서 발생시키는 이벤트 → 세션 정리 후 로그인 팝업
|
||||||
function onUnauthorized() {
|
function onUnauthorized() {
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
|
||||||
import HomeView from '../views/HomeView.vue'
|
import HomeView from '../views/HomeView.vue'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { useUiStore } from '@/stores/ui'
|
import { useUiStore } from '@/stores/ui'
|
||||||
|
|
||||||
|
// Electron(데스크톱)은 file:// 로 로드 → 해시 히스토리라야 새로고침/딥링크가 안전
|
||||||
|
const isDesktop = typeof navigator !== 'undefined' && /electron/i.test(navigator.userAgent)
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: isDesktop ? createWebHashHistory() : createWebHistory(import.meta.env.BASE_URL),
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import vueDevTools from 'vite-plugin-vue-devtools'
|
|||||||
const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf-8'))
|
const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf-8'))
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig(({ mode }) => ({
|
||||||
|
// Electron(데스크톱)은 file:// 로 로드하므로 상대경로 자산이 필요. 그 외(웹/Capacitor)는 '/'.
|
||||||
|
base: mode === 'electron' ? './' : '/',
|
||||||
define: {
|
define: {
|
||||||
__APP_VERSION__: JSON.stringify(pkg.version),
|
__APP_VERSION__: JSON.stringify(pkg.version),
|
||||||
},
|
},
|
||||||
@@ -37,4 +39,4 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
}))
|
||||||
|
|||||||