feat: 브랜드 앱 아이콘 + Windows 데스크톱(Electron) 클라이언트
CI / build (push) Failing after 12m35s

- 앱 아이콘: 브랜드(초록 #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>
This commit is contained in:
ByungCheol
2026-06-07 16:20:55 +09:00
parent 095ba942eb
commit cb5cf1f0bf
42 changed files with 7474 additions and 19 deletions
+3
View File
@@ -0,0 +1,3 @@
# 데스크톱(Electron) 빌드 전용 — `vite build --mode electron` 이 로드.
# file:// 로 로드되므로 백엔드 절대 URL 을 지정한다.
VITE_API_BASE_URL=https://app.sblog.kr/api
+3
View File
@@ -14,6 +14,9 @@ dist-ssr
coverage
*.local
# Electron desktop build output
release-desktop
# Editor directories and files
.vscode/*
!.vscode/extensions.json
+2 -8
View File
@@ -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>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 809 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 575 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

+9
View File
@@ -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')
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

+3
View File
@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+8
View File
@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

+9
View File
@@ -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

+43
View File
@@ -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()
})
+7350
View File
File diff suppressed because it is too large Load Diff
+27
View File
@@ -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",
+4 -3
View File
@@ -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() {
+5 -2
View File
@@ -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: '/',
+4 -2
View File
@@ -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({
},
},
},
})
}))