- 앱 아이콘: 브랜드(초록 #00bc7e + 흰 막대 차트) 적응형 아이콘 생성(@capacitor/assets) · assets/ 에 SVG 소스 + 전 해상도 mipmap 재생성, 적응형 XML 인셋 제거(배경 풀블리드) - 데스크톱: Electron 로 dist 를 감싼 Windows 설치파일(.exe) · vite electron 모드(상대 base)·라우터 해시 히스토리·App 게이트 Electron 감지 · webSecurity:false 로 file:// CORS 우회, 백엔드는 https://app.sblog.kr/api · scripts: build:electron / electron:dev / electron:build(electron-builder NSIS) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@@ -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
|
||||
*.local
|
||||
|
||||
# Electron desktop build output
|
||||
release-desktop
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.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">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
@@ -8,7 +7,6 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<activity
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"
|
||||
android:name=".MainActivity"
|
||||
@@ -16,12 +14,10 @@
|
||||
android:theme="@style/AppTheme.NoActionBarLaunch"
|
||||
android:launchMode="singleTask"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
@@ -29,9 +25,7 @@
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths"></meta-data>
|
||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
<!-- 카드 결제 알림 자동인식: 알림 접근 리스너 서비스 -->
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@mipmap/ic_launcher_background" />
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@mipmap/ic_launcher_background" />
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
||||
</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",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "electron/main.cjs",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"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:watch": "vitest",
|
||||
"cap:sync": "npm run build:app && cap sync",
|
||||
@@ -16,6 +20,26 @@
|
||||
"lint:oxlint": "oxlint . --fix",
|
||||
"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": {
|
||||
"@capacitor/android": "^8.3.4",
|
||||
"@capacitor/app": "^8.1.0",
|
||||
@@ -33,10 +57,13 @@
|
||||
"vue-router": "^5.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/assets": "^3.0.5",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@pinia/testing": "^1.0.3",
|
||||
"@vitejs/plugin-vue": "^6.0.6",
|
||||
"@vue/test-utils": "^2.4.11",
|
||||
"electron": "^42.3.3",
|
||||
"electron-builder": "^26.15.0",
|
||||
"eslint": "^10.2.1",
|
||||
"eslint-plugin-oxlint": "~1.60.0",
|
||||
"eslint-plugin-vue": "~10.8.0",
|
||||
|
||||
@@ -15,9 +15,10 @@ const auth = useAuthStore()
|
||||
const ui = useUiStore()
|
||||
const route = useRoute()
|
||||
|
||||
// 앱(Capacitor 네이티브)에서만 전체 이용. 웹(브라우저)은 안내 페이지만 노출.
|
||||
// 단, 개발 모드(npm run dev)에서는 브라우저로도 전체 앱을 테스트할 수 있게 예외.
|
||||
const isApp = Capacitor.isNativePlatform() || import.meta.env.DEV
|
||||
// 앱(Capacitor 네이티브) 또는 데스크톱(Electron) 클라이언트에서 전체 이용.
|
||||
// 웹(브라우저)은 안내 페이지만 노출. 개발 모드(npm run dev)는 예외로 전체 앱 표시.
|
||||
const isDesktop = /electron/i.test(navigator.userAgent)
|
||||
const isApp = Capacitor.isNativePlatform() || import.meta.env.DEV || isDesktop
|
||||
|
||||
// http.js 의 401 처리에서 발생시키는 이벤트 → 세션 정리 후 로그인 팝업
|
||||
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 { useAuthStore } from '@/stores/auth'
|
||||
import { useUiStore } from '@/stores/ui'
|
||||
|
||||
// Electron(데스크톱)은 file:// 로 로드 → 해시 히스토리라야 새로고침/딥링크가 안전
|
||||
const isDesktop = typeof navigator !== 'undefined' && /electron/i.test(navigator.userAgent)
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
history: isDesktop ? createWebHashHistory() : createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
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'))
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
export default defineConfig(({ mode }) => ({
|
||||
// Electron(데스크톱)은 file:// 로 로드하므로 상대경로 자산이 필요. 그 외(웹/Capacitor)는 '/'.
|
||||
base: mode === 'electron' ? './' : '/',
|
||||
define: {
|
||||
__APP_VERSION__: JSON.stringify(pkg.version),
|
||||
},
|
||||
@@ -37,4 +39,4 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}))
|
||||
|
||||