Compare commits
300 Commits
a25fef65f8
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 21d79fa040 | |||
| f738c6f5b1 | |||
| 75587e7645 | |||
| f50f1b8fc5 | |||
| f1dc11d402 | |||
| f367379c84 | |||
| 42cfeccbaf | |||
| 466fbe8136 | |||
| 1a85e448aa | |||
| 00d2945e39 | |||
| 24db9b0587 | |||
| 6f49a7eef0 | |||
| cdaa7d09ac | |||
| 2ee5cae6ea | |||
| 2d70fced79 | |||
| ecc3ef0f4e | |||
| c7875b0577 | |||
| d20d3c3027 | |||
| ed607e1cda | |||
| 641d0649ae | |||
| a4e0b38b2f | |||
| ca09dce2bd | |||
| 13a0f0d40f | |||
| 28712a026d | |||
| d63c6997c9 | |||
| d8326568ff | |||
| 486942b1c9 | |||
| ba7ffaff92 | |||
| 5567a239df | |||
| 2b58e976fc | |||
| e4ab683ac8 | |||
| a5264f4dff | |||
| 18f015f7fd | |||
| b039069d6c | |||
| a42715834b | |||
| d40c75d3e5 | |||
| fcd3f23a32 | |||
| 1ee264f2ab | |||
| 98ad7b9e98 | |||
| b70742b79c | |||
| 5beb999535 | |||
| 654fc44f48 | |||
| 061fb47b97 | |||
| e95e14378b | |||
| f446fb3f03 | |||
| 4d8881342a | |||
| c694dd9d04 | |||
| ec130e85a6 | |||
| 1169f3e542 | |||
| 3429ddc235 | |||
| d84101ecda | |||
| d7cddba77c | |||
| e655e79133 | |||
| 17e7dd53a6 | |||
| 1f128940f5 | |||
| 951cbdce34 | |||
| 1b2d6a2c02 | |||
| 810a5d9380 | |||
| e2d9b8ea37 | |||
| cdf94b75cb | |||
| e3ab2f9f3c | |||
| 769aa38b52 | |||
| d5b4c46163 | |||
| 5b46ba93f3 | |||
| a52ac6532c | |||
| 253e88e0fa | |||
| 296ae594f8 | |||
| ac20174378 | |||
| 87c7695cc3 | |||
| 9d1ae0e402 | |||
| fb4accdca6 | |||
| 03e11cc79c | |||
| d1dd19d5a3 | |||
| f3f11fa15b | |||
| 4c325544a6 | |||
| 0090c31294 | |||
| 59a17f9087 | |||
| e9e397434f | |||
| 9f9fc14c64 | |||
| a9d4ea8410 | |||
| 7affe5c318 | |||
| 5660451f04 | |||
| c069e70de4 | |||
| e23a694675 | |||
| 7e13a42848 | |||
| 6a4d01b6e1 | |||
| 9994456f30 | |||
| 066ada961a | |||
| a561eccef8 | |||
| 902b213f60 | |||
| 42406b4c75 | |||
| 16e8973532 | |||
| 0d7e9ba4f5 | |||
| 1525460149 | |||
| cbb3d47bfa | |||
| 1cbf47ff87 | |||
| a7e5bf9c0f | |||
| 9de1949da8 | |||
| 7861eef05b | |||
| a261dd2ca2 | |||
| ea9fbc359b | |||
| d364b52c73 | |||
| 0b64f4f8cd | |||
| c0e5421c33 | |||
| 282fb7476d | |||
| 1b53af525e | |||
| 31cb4b7198 | |||
| f8e6c6b0ee | |||
| f15820f204 | |||
| e1c077dccd | |||
| 7965706167 | |||
| 1e9ff0408c | |||
| 8d1567cb4b | |||
| b2f3b156b2 | |||
| 1996c3355e | |||
| 28ebe06767 | |||
| 37b4a9f4cf | |||
| afbec5e3c3 | |||
| ee7a1fb471 | |||
| daf6b840a9 | |||
| 1a8aa8e13c | |||
| e87bc244fe | |||
| 27d405b154 | |||
| f8369f5108 | |||
| 8604fb5030 | |||
| 80d7b8d552 | |||
| 98172b3c56 | |||
| 764436ebc9 | |||
| 8d659ea956 | |||
| 970e9e14ad | |||
| 46a8cb5515 | |||
| f996075da7 | |||
| 4c846c5bee | |||
| 5795f328a1 | |||
| 4f08b47fc3 | |||
| e2d3be29da | |||
| dab6dc3411 | |||
| caa591a16d | |||
| 20f67bbc76 | |||
| 971e08282f | |||
| f42805f96e | |||
| 8e7955b5d7 | |||
| a0aba1d578 | |||
| ce7488076a | |||
| 2eaf8695b0 | |||
| 2329da1090 | |||
| f23b4963a9 | |||
| 60794733a1 | |||
| b50a81028f | |||
| 6972cb5a63 | |||
| b39190baa2 | |||
| bd5f1afd47 | |||
| 48c3664136 | |||
| bac3ce1ded | |||
| 091b42eb81 | |||
| 3f9b0daf53 | |||
| d5be3c4fcd | |||
| cbe469552d | |||
| 659ea3b2fa | |||
| b7d8ede48f | |||
| 7633bdc166 | |||
| 6bb6a9727d | |||
| 9d60ea8f12 | |||
| 5534a2438b | |||
| ee5e9d4adb | |||
| 36178572e1 | |||
| 791aaad1c0 | |||
| a2cda9b4ee | |||
| 6b8001b4ad | |||
| e8d3549f02 | |||
| ddf0074047 | |||
| 4838eba9af | |||
| 27b8ace100 | |||
| 29ff09a8d6 | |||
| 6cbe1440b3 | |||
| 8be7072614 | |||
| 077b9598ef | |||
| 59f38b3348 | |||
| 9c932e7258 | |||
| 0fa8c95139 | |||
| 98e413b1a1 | |||
| 127551da12 | |||
| bcc0c4821f | |||
| fa3b12b369 | |||
| d56a8c75c3 | |||
| db68ea1680 | |||
| aed3f996fc | |||
| e8dc6b5568 | |||
| 8cca35151c | |||
| d243600f41 | |||
| c114e7672b | |||
| d7639225f7 | |||
| 0b8064e569 | |||
| 1000bf06af | |||
| 5349cf55f4 | |||
| 821aec72b9 | |||
| 199b0c84e9 | |||
| 0dda0b6ccd | |||
| ad219fd0bb | |||
| 09634757f7 | |||
| 3b542cb33d | |||
| 823bdcafcf | |||
| f0b5f87656 | |||
| ce5cefe64a | |||
| 9be60000d9 | |||
| 56d2ad6a8a | |||
| c2499c382a | |||
| 7e096d5e7b | |||
| bf40cbb254 | |||
| c167875567 | |||
| 3eee749ab9 | |||
| 7e38eae2b7 | |||
| 8733cf1ee0 | |||
| 7699cfc55f | |||
| 6125bf9ad8 | |||
| 1636c35aef | |||
| 09a00a1c37 | |||
| 206364d2f1 | |||
| 1da98b6618 | |||
| ca66c1d53e | |||
| 7855298eac | |||
| 24ab91b257 | |||
| 866abee860 | |||
| 2d6af690d4 | |||
| 9861ba90ae | |||
| 2755d118c2 | |||
| 3f809b80bb | |||
| c9b58f4ad0 | |||
| d8ab99d1ec | |||
| 9aa21696d9 | |||
| e64de9c247 | |||
| 7d8f947156 | |||
| b8260e9860 | |||
| 457fd8127a | |||
| 4ffc01f484 | |||
| 80e1faec09 | |||
| a6829d4157 | |||
| cee5b661e7 | |||
| 54a285a3b6 | |||
| a3bdd2e76d | |||
| e4e1be49ca | |||
| f34980048b | |||
| 85670a1b9f | |||
| 5bfa45ed30 | |||
| c4c58d2e24 | |||
| 4e03bf6393 | |||
| 59b17051a4 | |||
| 639a50f8bc | |||
| c4872c93a8 | |||
| 6fe7f8b168 | |||
| b737720d29 | |||
| 75228cbb64 | |||
| 63b4792e58 | |||
| f07cb349e2 | |||
| ef1d718fb8 | |||
| a92063dd64 | |||
| a86d77b2c5 | |||
| 6aa3590c5a | |||
| 4916d28604 | |||
| d87dd0f56b | |||
| dbe255cf1c | |||
| 579ee3e4e6 | |||
| 11ab23ed14 | |||
| 8254460751 | |||
| 82b08df826 | |||
| b8c1440e4d | |||
| a5c81cc0f2 | |||
| a310c7a471 | |||
| 091abd40be | |||
| 1ca09b7439 | |||
| 38484c51a6 | |||
| 4ef5850613 | |||
| 1c8d9c427c | |||
| d706339649 | |||
| 383977bcb5 | |||
| dc62d9883a | |||
| e455e8b3a8 | |||
| e4d7df2554 | |||
| 25b5938ab8 | |||
| 4e4c28bafe | |||
| b4b8833fc1 | |||
| 0eb3eaaa67 | |||
| 02290413b2 | |||
| 5a5959a26e | |||
| c8c774f060 | |||
| eeea19e20b | |||
| 0f56db744c | |||
| ca803272c4 | |||
| b5d6204a2d | |||
| 8e651c786f | |||
| 67ac385e26 | |||
| dd17fae5f4 | |||
| 2e48570876 | |||
| f533df0024 | |||
| 40b517ad10 | |||
| 643bb8011b | |||
| f9aca601b6 | |||
| 8add41d94f | |||
| 9f8d9bd7b5 | |||
| cad1aba0ca |
@@ -1,8 +1,9 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
# main 은 Deploy 워크플로가 테스트+빌드를 수행하므로 CI 중복 실행 제외(배포 시간 단축).
|
||||
push:
|
||||
branches: [main, dev]
|
||||
branches: [dev]
|
||||
pull_request:
|
||||
branches: [main, dev]
|
||||
|
||||
@@ -17,7 +18,7 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
# cache 제거: Gitea 캐시 서버 미연결 시 'Save cache' 후처리가 수 분 hang → '가짜 실패' 유발
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
# cache 제거: Gitea 캐시 서버 미연결 시 'Save cache' 후처리가 수 분 hang → '가짜 실패' 유발
|
||||
|
||||
- name: Install
|
||||
run: npm ci
|
||||
|
||||
@@ -1,44 +1,71 @@
|
||||
# sb_pt
|
||||
# 돈돼지 가계부 — 프론트엔드 (sb_pt)
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
Vue 3 + Vite 기반의 가계부·자산·게시판 앱 프론트엔드. 웹 / 안드로이드(Capacitor) / Windows 데스크톱(Electron) 3개 플랫폼을 하나의 코드베이스로 빌드합니다.
|
||||
|
||||
## Recommended IDE Setup
|
||||
- 서비스명 **돈돼지 가계부** (구 Slim Budget)
|
||||
- 운영: [app.sblog.kr](https://app.sblog.kr) · 안내/정책 페이지 [www.sblog.kr](https://www.sblog.kr)
|
||||
|
||||
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||
|
||||
## Recommended Browser Setup
|
||||
|
||||
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
|
||||
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
|
||||
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
|
||||
- Firefox:
|
||||
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
|
||||
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
## 기술 스택
|
||||
- **Vue 3** (`<script setup>` Composition API) + **Vite**
|
||||
- **Pinia** (상태관리), **Vue Router** (라우팅 + 인증 가드)
|
||||
- **Axios** (공통 인스턴스 + 토큰/401 인터셉터)
|
||||
- **Toast UI Editor** + Prism.js (게시판 마크다운 작성/뷰어)
|
||||
- **Capacitor** (안드로이드 하이브리드 앱), **Electron** (Windows 데스크톱)
|
||||
- **Vitest** + `@vue/test-utils` (테스트)
|
||||
|
||||
## 빠른 시작
|
||||
```sh
|
||||
npm install
|
||||
npm run dev # 개발 서버 (전체 앱 표시 — 웹 접근 제한 예외)
|
||||
```
|
||||
- Node: `^20.19.0 || >=22.12.0`
|
||||
- API 베이스 URL: `VITE_API_BASE_URL` (기본 `/api`, Nginx 가 백엔드 8080 으로 프록시)
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
## 주요 스크립트
|
||||
| 명령 | 설명 |
|
||||
|---|---|
|
||||
| `npm run dev` | 개발 서버 (HMR) |
|
||||
| `npm run build` | `dist/` 정적 빌드 (웹 배포용) |
|
||||
| `npm run build:app` | Capacitor 모드 빌드 (`--mode capacitor`, 앱 전용 env) |
|
||||
| `npm run cap:sync` | 앱 빌드 + `cap sync` (안드로이드 동기화) |
|
||||
| `npm run cap:android` | Android Studio 열기 |
|
||||
| `npm run electron:dev` | Electron 데스크톱 실행 |
|
||||
| `npm run electron:build` | Windows 설치파일(`DonDwaeji-Setup.exe`) 빌드 |
|
||||
| `npm test` | Vitest 실행 |
|
||||
| `npm run lint` | oxlint + eslint (`--fix`) |
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
## 플랫폼별 동작
|
||||
- **웹(PC·모바일)**: 안내 페이지만 노출(앱 다운로드 유도). 실제 이용은 앱에서. (`npm run dev` 는 예외로 전체 앱 표시)
|
||||
- **안드로이드**: Capacitor WebView 패키징. 빌드 전 `.env.capacitor` 의 백엔드 절대 URL 설정 필수 → [docs/ANDROID.md](docs/ANDROID.md)
|
||||
- **Windows**: Electron 이 라이브 사이트(app.sblog.kr)를 직접 로드 → 프론트 변경이 재설치 없이 반영
|
||||
|
||||
### Compile and Minify for Production
|
||||
## 인증
|
||||
- 로그인 시 발급된 **Bearer 토큰**을 `localStorage` 에 저장, `http.js` 인터셉터가 `Authorization` 헤더에 주입
|
||||
- **구글 로그인**(웹/PC=GIS, 앱=네이티브 플러그인) — 현재 소셜 로그인 전용(아이디/비번 폼은 `features.js` 플래그로 숨김)
|
||||
- 401 응답 시 토큰 정리 후 로그인 모달, 라우트 `meta.requiresAuth` / `requiresAdmin` 가드
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
## 주요 기능
|
||||
- **가계부** — 수입·지출·이체 기록, 검색·필터, 날짜 그룹 접기/펴기, 외화 입력(환율 자동환산)
|
||||
- **AI 보조** — 자연어 한 줄 입력("어제 스타벅스 5천원"), 영수증 사진 구조화, 분류·계좌 자동 추천, 통계 화면 재무 코멘트·절약 가이드 (Claude Haiku, 집계만 전송·저장은 사용자 확인 후)
|
||||
- **계좌·자산** — 은행·현금·카드·대출·투자 계좌, 순자산 자동 집계
|
||||
- **고정 지출** — 반복 규칙 자동 생성 + '확인' 확정 흐름
|
||||
- **통계** — '이번 달 현황'(예산·수입 대비 통합), 분류별 분석, 순자산 추이, 월말 예상 지출(일회성 큰 지출 제외 결정적 계산), 지난달 대비
|
||||
- **예산** — 월별 카테고리 예산, 예산 대비 진행률
|
||||
- **활동 포인트** — 가계부·게시판 활동 적립, 한 달 개근(모든 날 기록) 랜덤 보너스
|
||||
- **게시판** — 커뮤니티/짠테크/재테크 3개 게시판, 댓글·태그·이미지 업로드·공지
|
||||
- **테마** — 시스템/라이트/다크 모드
|
||||
|
||||
### Lint with [ESLint](https://eslint.org/)
|
||||
## 문서
|
||||
| 문서 | 내용 |
|
||||
|---|---|
|
||||
| [docs/FRONTEND.md](docs/FRONTEND.md) | 아키텍처·디렉터리 구조·화면 명세 |
|
||||
| [docs/release-2026-06-06.md](docs/release-2026-06-06.md) | 누적 변경 이력 (릴리스 노트) |
|
||||
| [docs/ANDROID.md](docs/ANDROID.md) | 안드로이드(Capacitor) 빌드·배포 |
|
||||
| [docs/plan-membership-tiers.md](docs/plan-membership-tiers.md) | 무료/유료 티어 설계 (미구현) |
|
||||
| [deploy/README.md](deploy/README.md) | 프론트 배포(Nginx + Gitea Actions) |
|
||||
| [brand/LAUNCH-PROGRESS.md](brand/LAUNCH-PROGRESS.md) | Play 스토어 출시 진행 상황·로드맵 |
|
||||
|
||||
```sh
|
||||
npm run lint
|
||||
```
|
||||
## 배포
|
||||
- `main` 푸시 → Gitea Actions 가 CI(빌드·테스트) + Deploy(Nginx 업로드·reload) 실행
|
||||
- **테스트 게이트**: CI·배포 워크플로 모두 테스트 실패 시 배포 중단
|
||||
- 상세: [deploy/README.md](deploy/README.md)
|
||||
|
||||
@@ -99,3 +99,8 @@ app/src/main/assets/public
|
||||
app/src/main/assets/capacitor.config.json
|
||||
app/src/main/assets/capacitor.plugins.json
|
||||
app/src/main/res/xml/config.xml
|
||||
|
||||
# 릴리스 서명 (절대 커밋 금지)
|
||||
*.jks
|
||||
*.keystore
|
||||
key.properties
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
// 릴리스 서명: android/key.properties 가 있으면 사용, 없으면 디버그 서명으로 폴백.
|
||||
def keystorePropsFile = rootProject.file("key.properties")
|
||||
def keystoreProps = new Properties()
|
||||
if (keystorePropsFile.exists()) {
|
||||
keystoreProps.load(new FileInputStream(keystorePropsFile))
|
||||
}
|
||||
def hasReleaseKeystore = keystorePropsFile.exists()
|
||||
|
||||
android {
|
||||
namespace = "kr.sblog.slimbudget"
|
||||
compileSdk = rootProject.ext.compileSdkVersion
|
||||
@@ -7,8 +15,8 @@ android {
|
||||
applicationId "kr.sblog.slimbudget"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
versionCode 6
|
||||
versionName "1.0.5"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
@@ -16,8 +24,20 @@ android {
|
||||
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||
}
|
||||
}
|
||||
signingConfigs {
|
||||
if (hasReleaseKeystore) {
|
||||
release {
|
||||
storeFile rootProject.file(keystoreProps['storeFile'])
|
||||
storePassword keystoreProps['storePassword']
|
||||
keyAlias keystoreProps['keyAlias']
|
||||
keyPassword keystoreProps['keyPassword']
|
||||
}
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
// 키스토어가 있으면 릴리스 서명, 없으면 디버그 서명(로컬 테스트용)
|
||||
signingConfig hasReleaseKeystore ? signingConfigs.release : signingConfigs.debug
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
|
||||
@@ -11,7 +11,11 @@ apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||
dependencies {
|
||||
implementation project(':capacitor-app')
|
||||
implementation project(':capacitor-camera')
|
||||
implementation project(':capacitor-filesystem')
|
||||
implementation project(':capacitor-local-notifications')
|
||||
implementation project(':capacitor-preferences')
|
||||
implementation project(':capacitor-share')
|
||||
implementation project(':capawesome-capacitor-google-sign-in')
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,10 @@
|
||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
<!-- 카드 결제 알림 자동인식: 알림 접근 리스너 서비스 -->
|
||||
<!-- 카드 결제 알림 자동인식(알림 접근 리스너) — 스토어 첫 출시에서는 비활성화.
|
||||
Google Play NotificationListenerService(알림 접근) 정책 심사 회피용으로 선언 제거.
|
||||
추후 정당화/선언 후 재활성화하려면 아래 service 블록 주석을 해제할 것. -->
|
||||
<!--
|
||||
<service
|
||||
android:name=".CardNotifListenerService"
|
||||
android:label="@string/app_name"
|
||||
@@ -39,6 +42,7 @@
|
||||
<action android:name="android.service.notification.NotificationListenerService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
-->
|
||||
</application>
|
||||
|
||||
<!-- Permissions -->
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background" />
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
||||
<background>
|
||||
<inset android:drawable="@mipmap/ic_launcher_background" android:inset="16.7%" />
|
||||
</background>
|
||||
<foreground>
|
||||
<inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" />
|
||||
</foreground>
|
||||
</adaptive-icon>
|
||||
@@ -1,5 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background" />
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
||||
<background>
|
||||
<inset android:drawable="@mipmap/ic_launcher_background" android:inset="16.7%" />
|
||||
</background>
|
||||
<foreground>
|
||||
<inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" />
|
||||
</foreground>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 339 B After Width: | Height: | Size: 299 B |
|
Before Width: | Height: | Size: 809 B After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 720 B After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 158 B After Width: | Height: | Size: 160 B |
|
Before Width: | Height: | Size: 474 B After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 208 B After Width: | Height: | Size: 211 B |
|
Before Width: | Height: | Size: 575 B After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 403 B After Width: | Height: | Size: 425 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 778 B After Width: | Height: | Size: 834 B |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 36 KiB |
@@ -1,7 +1,7 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<resources>
|
||||
<string name="app_name">Slim Budget</string>
|
||||
<string name="title_activity_main">Slim Budget</string>
|
||||
<string name="app_name">돈돼지가계부</string>
|
||||
<string name="title_activity_main">돈돼지가계부</string>
|
||||
<string name="package_name">kr.sblog.slimbudget</string>
|
||||
<string name="custom_url_scheme">kr.sblog.slimbudget</string>
|
||||
</resources>
|
||||
|
||||
@@ -8,5 +8,17 @@ project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/
|
||||
include ':capacitor-camera'
|
||||
project(':capacitor-camera').projectDir = new File('../node_modules/@capacitor/camera/android')
|
||||
|
||||
include ':capacitor-filesystem'
|
||||
project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacitor/filesystem/android')
|
||||
|
||||
include ':capacitor-local-notifications'
|
||||
project(':capacitor-local-notifications').projectDir = new File('../node_modules/@capacitor/local-notifications/android')
|
||||
|
||||
include ':capacitor-preferences'
|
||||
project(':capacitor-preferences').projectDir = new File('../node_modules/@capacitor/preferences/android')
|
||||
|
||||
include ':capacitor-share'
|
||||
project(':capacitor-share').projectDir = new File('../node_modules/@capacitor/share/android')
|
||||
|
||||
include ':capawesome-capacitor-google-sign-in'
|
||||
project(':capawesome-capacitor-google-sign-in').projectDir = new File('../node_modules/@capawesome/capacitor-google-sign-in/android')
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
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')
|
||||
}
|
||||
// 앱 아이콘은 이제 '돈돼지' 래스터 이미지(assets/piggy-fg.png) 기반입니다.
|
||||
// 재생성: piggy-fg.png → sharp 합성으로 icon-foreground/background/only.png 만든 뒤
|
||||
// `npx @capacitor/assets generate --android` 로 안드로이드 아이콘 생성.
|
||||
// (구 SVG 래스터화는 더 이상 사용하지 않음 — 실행해도 PNG 를 덮어쓰지 않도록 비활성화)
|
||||
console.log('skip: icons are raster (piggy). use piggy-fg.png + @capacitor/assets generate')
|
||||
|
||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 419 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 542 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 319 KiB |
@@ -0,0 +1,198 @@
|
||||
# 돈돼지 가계부 — Play 스토어 첫 출시 진행 상황
|
||||
|
||||
> 최종 업데이트: 2026-07-11. 첫 정식 출시(무료 앱) 준비 로그 + 남은 로드맵.
|
||||
> 관련 문서: [play-store-listing.md](play-store-listing.md) · [production-access-draft.md](production-access-draft.md)
|
||||
|
||||
---
|
||||
|
||||
## ✅ 완료된 작업
|
||||
|
||||
### 출시 전략 결정
|
||||
- **무료 앱**으로 첫 출시 (수익화 기능은 순차 도입)
|
||||
- 첫 출시 기능 플래그 (`src/config/features.js`, 전부 `false`):
|
||||
- `BILLING_ENABLED=false` — 인앱결제 스텁이라 결제 없이 프리미엄 부여되는 악용 차단
|
||||
- `ADS_ENABLED=false` — 애드핏 미승인 + 데이터 안전 양식 단순화(광고 식별자 미수집)
|
||||
- `CARD_NOTIF_ENABLED=false` — NotificationListenerService 정책 심사 회피
|
||||
- 패키지명 `kr.sblog.slimbudget` **유지**(출시 후 영구 고정. 사용자 비노출이라 무방)
|
||||
|
||||
### 인프라 / 정책 페이지
|
||||
- **www.sblog.kr** 도메인 + 인증서 확장 (app.sblog.kr와 동일 dist 서빙)
|
||||
- 공개 URL: `/privacy`, `/terms`, `/account-deletion` (모두 200 확인)
|
||||
- 개인정보처리방침 광고 문구 → "현재 광고 없음 + 향후 도입 가능" 미래형으로 정합화
|
||||
|
||||
### 빌드 / 서명
|
||||
- **AAB**: `android/app/build/outputs/bundle/release/app-release.aab`
|
||||
- targetSdk 36, versionCode 1, versionName 1.0
|
||||
- 업로드 키(CN=Don Dwaeji) 서명 확인 (디버그 아님)
|
||||
- 결제·광고·카드알림 전부 OFF 클린 (NotificationListener 권한 없음 검증)
|
||||
- 키스토어: `android/upload-keystore.jks` + `android/key.properties` (gitignore, **이 PC에만 존재 — 분실 시 업데이트 불가, 반드시 백업**)
|
||||
|
||||
### 스토어 그래픽 (`brand/store/`)
|
||||
- `icon-512.png` (512×512 불투명)
|
||||
- `feature-1024x500.png` / `feature-1024x500-text.png` (피처그래픽, 택1)
|
||||
- 생성기 `brand/gen-store-assets.mjs` + 마스터 `brand/logo-1024.png`
|
||||
|
||||
### 스크린샷 (`brand/screenshots/`, 7컷, 실기기 1080×2340)
|
||||
- shot0 홈 대시보드(리드) / shot1 가계부 내역 / shot2 통계 / shot3 정기결제 / shot4 계좌 / shot5 게시판 / shot6 게시글 상세
|
||||
|
||||
### 등록 문서
|
||||
- `brand/play-store-listing.md` — 등록정보(제목·설명)·데이터안전 양식·콘텐츠등급 답안
|
||||
- `brand/production-access-draft.md` — 프로덕션 액세스 신청서 초안(4문항)
|
||||
|
||||
### 코드 수정 (배포 완료)
|
||||
- 인앱결제 진입점 비활성화 (UpgradeView "곧 출시 예정", AccountInfoView 정기결제 카드 숨김)
|
||||
- 광고 비활성화 (AdBanner + App.vue 게이팅, 하단 광고 여백 제거)
|
||||
- 둘러보기(데모) 모드 메뉴 접근 버그 수정 (AppHeader 햄버거 `demo.on` 노출)
|
||||
- 계정삭제 공개 페이지 추가
|
||||
|
||||
### 테스트 데이터 (캡처용, `sb_bt/scripts/`, gitignore 로컬 전용)
|
||||
- 테스트 계정 `kenneth73510@gmail.com` (DB로 PREMIUM 부여)
|
||||
- `seed-test-kenneth.sql`(계정+가계부), `seed-test-board.sql`(게시판 더미)
|
||||
- `cleanup-test-board.sql`(게시판 더미 삭제 — **캡처 후 정리 완료**)
|
||||
|
||||
---
|
||||
|
||||
## 🚧 현재 상태 (2026-07-05)
|
||||
- ✅ 개발자 계정 신원확인 완료 → 앱 생성 완료(`kr.sblog.slimbudget`)
|
||||
- ✅ 필수 설정(데이터안전·콘텐츠등급(전체이용가)·앱액세스(둘러보기)·광고없음·비거래자·방침URL) 완료
|
||||
- ✅ 스토어 등록정보·이미지(아이콘512·피처·스크린샷7) 완료
|
||||
- ✅ **비공개 테스트 트랙에 v2(AAB) 게시 → 검토 통과**
|
||||
- ✅ 구글 로그인 SHA-1 이슈 해결(Play 앱서명 SHA-1 등록) → 로그인 정상
|
||||
- ✅ Google Search Console sblog.kr 도메인 소유권 인증 완료 (DNS TXT)
|
||||
- ⏳ **테스터 12명 opt-in 대기**(업체 의뢰) → 14일 → 프로덕션 신청
|
||||
- ⏳ **v4 AAB 빌드 완료** (versionCode 4, 1.0.3) → 비공개 트랙 업로드 대기
|
||||
- ⏳ 개인사업자 계정 전환 → DUNS 신청 완료(D&B 발급 대기) → 발급 후 Play Console에서 마저 진행
|
||||
- Play Console 웹사이트 인증(www.sblog.kr) 완료
|
||||
|
||||
### 🍎 iOS(App Store) 트랙 — 2026-07-11
|
||||
- ✅ **애플 개발자 등록 완료** (Team ID `G9PDM2C4L6`)
|
||||
- ✅ **애플 로그인(Sign in with Apple) 실기기 동작 확인** — App Store 4.8 요건 충족
|
||||
- ✅ **iOS 번들ID `kr.sblog.slimbudget.app`** (기존 `kr.sblog.slimbudget`는 타 계정 선점 → iOS 전용 새 ID, 안드로이드는 그대로)
|
||||
- App ID + Sign in with Apple/Push capability, `CODE_SIGN_ENTITLEMENTS` 연결, `ITSAppUsesNonExemptEncryption=false`
|
||||
- ✅ **App Store Connect 앱 생성 + v1.0 (build 1) 심사 제출 완료** — 연령 15+, App Privacy(이메일·사용자콘텐츠·사용자ID / 추적없음 / 앱기능), 무료, 콘텐츠권한 없음, 스크린샷(6.5" 1242×2688 iPhone + iPad 13")
|
||||
- 로그인은 구글/애플 소셜 전용 → 심사 메모에 **둘러보기(데모)** 안내
|
||||
- ⏳ **심사 결과 대기** (승인 시 출시 / 거부 시 Resolution Center 대응)
|
||||
|
||||
---
|
||||
|
||||
## 📋 남은 로드맵 (프로덕션까지)
|
||||
|
||||
1. ⏳ **테스터 12명 opt-in** (업체 의뢰) — 14일 카운트 시작 조건
|
||||
2. ⏳ **14일 연속** 12명 유지 → **프로덕션 액세스 신청** (자료: production-access-draft.md)
|
||||
3. ⏳ **v4 AAB 비공개 트랙 업로드** — 빌드 완료(versionCode 4, 1.0.3), Play Console 업로드 후 검토 제출 (14일 카운트 리셋 안 됨)
|
||||
4. 승인 → **프로덕션 출시**
|
||||
|
||||
### ⚠️ 폐쇄 테스트 의무 (개인 계정 + 2023-11-13 이후 개설)
|
||||
- **테스터 12명 이상 · 14일 연속** 폐쇄 테스트 후 프로덕션 신청 가능
|
||||
- 실기기·실계정 필수 (에뮬·봇·중복 불인정)
|
||||
- 14일 카운트 = "릴리스 승인 + 12명 opt-in" 둘 다 충족 시점부터
|
||||
- 테스터 확보: 지인 비공개 방침 → 품앗이 커뮤니티(무료·안전) 또는 평판 좋은 유료 서비스(실사용 보장형만)
|
||||
|
||||
### 익명 셋업 체크리스트
|
||||
- [ ] 전용 Gmail 생성 (개발자 연락처 + 품앗이용)
|
||||
- [ ] 공개 개발자 이름 → 브랜드명 (법적이름은 구글 내부 전용, 비공개)
|
||||
- [ ] 연락처 이메일(계정+앱) → 전용 메일
|
||||
- [ ] 거래자 상태 → **비거래자(Non-trader)** (주소 비공개)
|
||||
- [ ] 품앗이 노출은 전용 Gmail로만
|
||||
|
||||
---
|
||||
|
||||
## ✅ 적용 완료 (전부 웹/소스 배포됨)
|
||||
|
||||
**v2(versionCode 2, 1.0.1, 06-29)에 포함 — 비공개 테스트 게시·검토통과**
|
||||
- 신규 유저 빈 상태 CTA(홈 '시작하기'·내역·계좌) / 오프라인·네트워크 오류 배너
|
||||
- 앱 잠금(PIN, 옵트인) + 카메라/갤러리 복귀 오작동 수정
|
||||
- 전역 에러 폴백(흰 화면 방지) / 안드로이드 뒤로가기 통합(오버레이→뒤로→종료토스트)
|
||||
- 분류 순서변경 보호('순서변경' 토글)
|
||||
|
||||
**v2 이후 추가 — 웹 배포됨, v3 빌드 대기**
|
||||
- 앱 잠금 **1시간 유예**(잦은 PIN 입력 해소)
|
||||
- **외화 입/지출** — 통화+환율 자동조회(open.er-api.com 무료)→원화 환산, 원본 보존. `amount`는 환산 원화라 통계·예산 무변경
|
||||
- **외화 환율 갱신 시각** — 환율 자동조회 후 '방금 / N분 전 / N시간 전' 레이블(30초 ticker), FxController `updatedAt` 응답 포함
|
||||
- **월별 예산 + 전월/다음달 복사** — budget에 year/month 추가, 기존 예산은 현재 월로 1회 이관
|
||||
- 내역추가 **전체화면 모달**(바깥 닫힘 방지) / 분류 **아코디언**(대분류 삼각형) / 계좌 셀렉트 정리(종류별 안내·이름만)
|
||||
- 구글 로그인 **Play 서명 SHA-1 등록**(Google Cloud Android OAuth 클라 추가)
|
||||
- **UI 전면 개편 (2026-07-04)** — 모든 입력 모달 전체화면 슬라이드업(AppModal) 적용:
|
||||
- 공통 `CategoryPicker.vue` 컴포넌트 — 대분류/소분류 아코디언 칩 + 인라인 추가 (가계부·고정지출·예산 공통)
|
||||
- `AccountView` 계좌종류·구분 셀렉트→뱃지, 분류→CategoryPicker
|
||||
- `RecurringView` 구분·주기 셀렉트→뱃지, 계좌/카드→뱃지, 분류→CategoryPicker
|
||||
- `AccountWalletView` 카드종류·상환방식 셀렉트→뱃지
|
||||
- `BudgetView` 비고정/고정 라디오→뱃지, 카테고리→CategoryPicker
|
||||
- `CategoryView` 리스트 UI → **칩 그리드 관리 모드** (CategoryPicker admin prop): ✏ 이름수정·✕ 삭제·⠿ 순서변경(토글 후 드래그)
|
||||
- **게시판 포인트 정책** — 작성 10P(일 5회·50P 한도), 스팸 키워드 포함 시 미지급; 추천 +5P(작성자·일 50P)/비추 -1P; 남글추천 +1P(투표자·일 10P)/취소 -1P
|
||||
- **가계부 작성 포인트** — 건당 10P, 일 5회(50P) 한도
|
||||
- **스팸 키워드 자동 블라인드** — 14개 키워드 포함 시 글/댓글 즉시 블라인드, 포인트 미지급
|
||||
- **관리자 신고글 목록 고도화** — 신고 누적 + 스팸 감지 통합; 🤖 스팸감지 뱃지·blockReason 표시; 스팸 항목 신고 무시 버튼 숨김
|
||||
|
||||
**2026-07-09~11 추가 (웹·서버 배포 완료 / iOS는 재빌드 시 반영)**
|
||||
- **애플 로그인(Sign in with Apple)** — 프런트(`native/appleAuth`·LoginModal)·백엔드(`/auth/apple`, 애플 JWKS 검증·aud=번들ID)·DB(`member.apple_id`)·iOS entitlements 전 구간. iOS 번들ID `kr.sblog.slimbudget.app`로 변경, 서버 `apple-client-id` 정합. 실기기 로그인 확인
|
||||
- **입력 UI 모달 → 페이지 전환 + 바텀시트** (톤 통일)
|
||||
- **계좌**(`WalletFormView`)·**정기결제**(`RecurringFormView`) → 별도 라우트 페이지, 생성 시 계좌 종류 선택 복원
|
||||
- **내역·예산** → 콘텐츠 영역 인라인 페이지(`?entry`/`?budget` 라우트 바인딩, 뒤로가기로 닫힘) — 핵심 로직(AI·OCR·상환 등) 이관 없이 프레젠테이션만 변경(회귀 위험 최소화)
|
||||
- 선택 항목 **바텀시트**: `BottomSheet`·`SheetSelect`·**`AccountPicker`(종류→계좌 아코디언)** 공통 컴포넌트. 계좌·분류·검색 필터에 적용(분류는 CategoryPicker `hideAdd`)
|
||||
- **화면 헤더 뒤로가기 버튼 제거** → 하단 네비 뒤로가기로 유도
|
||||
- **계좌 관리 개편** — 탭 제거→종류별 그룹 목록, 계좌명 클릭 시 **상세 화면**(`AccountWalletDetailView`), 순서변경 잠금 토글, 투자계좌는 투자금만
|
||||
- **대출·카드할부 상환 조회(유료)** `RepaymentScheduleView`+`utils/repayment.js` — 상환방식별 상각+카드할부(무이자), 회차별 원금·이자·합계·잔액. **대출 월 상환금(loanPayment) 필드**로 은행 조회액과 정확 일치. 이자율 소수점 3자리. 홈 **'이번 달 상환 예정' 카드(유료)**
|
||||
- **예산 단순화** — '월 예산'만 등록, 고정/비고정·일/주/년·**1회성 예산 제거**
|
||||
- **레이아웃** — 하단 고정 내비에 가리던 문제 수정(본문 `padding-bottom`)
|
||||
- **정기결제 등록 자동세팅** — 원리금균등은 저장된 월 상환금 우선
|
||||
- **연락처 이메일** → `dondwaeji.app@gmail.com` (약관·계정삭제·스토어 문의)
|
||||
|
||||
**2026-07-07 추가 (웹·서버 배포 완료)**
|
||||
- **정기결제 리네이밍** — 전체 화면 '고정지출' → '정기결제' (백업 엑셀 시트명은 기존 백업 복원 호환 위해 유지)
|
||||
- **정기결제 주기 월/년만** — DAILY·WEEKLY 제거, 요일선택 UI 제거 (`RecurringView`)
|
||||
- **계좌 관리 화면 개편** — 탭 제거 → **종류별 그룹 목록**, 계좌명 클릭 시 **상세 화면 분리**(`/account/wallets/:id` `AccountWalletDetailView`), 순서변경 **잠금 토글**, 투자계좌는 투자금만 표기·상세 비활성
|
||||
- **태그관리 순서변경 토글** — 기본 비활성, '순서변경' 버튼 클릭 시 드래그 활성 (`AccountTagView`)
|
||||
- **대출·카드할부 상환 조회 (유료)** — `RepaymentScheduleView.vue` + `utils/repayment.js`. 대출 상환방식별 상각(원리금균등·원금균등·만기일시) + 카드 할부(무이자) **남은 스케줄**, 회차별 **원금·이자·합계·남은잔액**, 이번 달/남은 총액. 사이드바 유료 메뉴
|
||||
- **대출 월 상환금(loanPayment) 필드** — 백엔드 컬럼(`wallet.loan_payment`)+DTO/매퍼, 계좌 폼 선택 입력. 원리금균등을 **은행 조회액과 정확히 일치**(이자=잔액×이율, 원금=월상환금−이자), **마지막 회차 잔여 반올림 정산**. 미입력 시 잔액·개월수로 PMT 추정
|
||||
- **대출 이자율 소수점 3자리** 입력(step 0.001, `loan_rate` DECIMAL(7,4))
|
||||
- **가계부 대출 상환 자동세팅** — 원리금균등 선택 시 저장된 **월 상환금 우선** 반영(옛 PMT 추정 대체), 마지막 회차 초과상환 방지 (`AccountView`)
|
||||
- **홈 대시보드 '이번 달 상환 예정' 카드 (유료)** — 현재일 기준 대출·카드할부 상환 예정 합계, **가계부에 상환 기록 있으면 제외**, 모두 기록 시 '상환 완료' 표시
|
||||
- **AI 교통 절약 팁 제외** — 버스·지하철·대중교통 분류는 정기권 최저비용 사용 중으로 tips에서 제외 (`AiEntryParser`)
|
||||
|
||||
**2026-07-05 추가 (웹 배포됨, v3 빌드 대기)**
|
||||
- **AI 편의 기능** — 자연어 입력·영수증 비전·분류/계좌 추천·재무 코멘트·절약 가이드 (상세는 아래 🤖 섹션)
|
||||
- **홈 대시보드 오늘 기록 넛지** — '안녕하세요 OO님' → 오늘 기록 상태로 대체(없으면 '지금 기록하기' CTA / 있으면 'N건'·연속 2일+ 'N일 연속 🔥')
|
||||
- **통계 화면 정리** — '예산 대비'+'수입 대비' 두 패널 → '이번 달 현황' 단일 패널 통합; 예상 지출은 **결정적 계산**(일회성 큰 지출 제외, 매 접속 동일값); AI 코멘트 로딩 인디케이터
|
||||
- **한 달 개근 보너스 포인트** — 현재 월 모든 날 기록 시 **랜덤 1~100P**(월 1회, 전월 소급 제외, 화면 안내 없음, `MONTH_COMPLETE_yyyyMM`로 중복 차단)
|
||||
- **iOS 안전영역·클립보드 수정** — 사이드바 상단 상태바 겹침 해결; 모달 열 때 클립보드 자동읽기 제거(iOS '붙여넣기' 토스트 방지). 음성 입력=iOS 키보드 받아쓰기(실기기 확인)
|
||||
|
||||
---
|
||||
|
||||
## 📌 남은 백로그 (출시 후 업데이트)
|
||||
1. **앱 업데이트 안내** — 새 버전 알림 + 구글플레이 바로가기
|
||||
2. (선택) 첫 실행 온보딩 / Play 인앱 리뷰 / 생체 인증
|
||||
3. **조직 계정 전환** — 사업자등록→DUNS→앱이전
|
||||
4. ✅ **포인트 상점** (2026-07-10 구현 완료)
|
||||
- **계좌 슬롯**: 500P/개, 최대 3개, 영구. `extra_wallet_slots` (member 테이블)
|
||||
- **AI 통계 분석**: 1000P → 10회 이용권. `ai_stat_credits` (member 테이블)
|
||||
- FREE 회원이 크레딧 없이 AI 코멘트 호출 시 402 → 통계 화면에서 상점 안내
|
||||
- PREMIUM 회원은 AI 코멘트 무제한 (크레딧 불필요)
|
||||
|
||||
### 🤖 AI 기능 — ✅ 구현·배포 완료 (2026-07-05)
|
||||
- 백엔드: `AiEntryParser`(Claude Haiku 4.5, raw HTTP `/v1/messages`), 키는 서버 `/opt/sb-backend/.env`(`ANTHROPIC_API_KEY`), 실패/미설정 시 규칙기반·Google Vision OCR 폴백.
|
||||
- ✅ **① 자연어 한 줄 입력** — "어제 스타벅스 5천원" → 폼 자동 채움. 내역 화면 '✨ 빠른 입력' 바 진입점. (음성=iOS 키보드 받아쓰기, 실기기 확인)
|
||||
- ✅ **② 영수증 사진 → AI 구조화** — Claude Vision이 금액·상호·날짜·분류·계좌 추출(`/ocr/receipt-ai`), 실패 시 Google Vision 폴백.
|
||||
- ✅ **③ 분류·계좌 자동 추천** — 사용자 실제 분류·계좌 목록 중에서 선택(할루시네이션 방지 검증).
|
||||
- ✅ **AI 재무 코멘트 + 절약 가이드** — 통계 화면, **집계 수치만** 전송(거래 원문 미전송), 정중체. 예상 지출은 비결정성 때문에 **결정적 계산**(일회성 큰 지출 제외)으로 분리.
|
||||
- ✅ **개인정보처리방침·데이터안전 갱신** — 처리자 Anthropic(Claude) 명시, 전송 데이터 범위·학습 미사용 고지 (terms.js §2·§3, play-store-listing.md 데이터안전).
|
||||
- 원칙 준수: AI는 폼 prefill·참고 코멘트만, **저장은 사용자 확인 후**.
|
||||
|
||||
---
|
||||
|
||||
## 🔨 빌드 상태
|
||||
- **v2** (versionCode 2, 1.0.1): 비공개 테스트 게시·검토통과
|
||||
- **v3** (versionCode 3): 위 'v2 이후 추가' + **AI 기능(자연어·영수증·분류/계좌·재무코멘트)** 전부 반영 — 빌드 후 비공개 트랙 업로드 (14일 카운트 리셋 안 됨)
|
||||
- ⚠️ 빌드 전 서버 `.env`에 `ANTHROPIC_API_KEY`(AI) 확인
|
||||
- ✅ 가계부 내역 오류 해결됨 (2026-07-05)
|
||||
- ✅ 1회성 예산(BudgetOnetime) 백엔드 배포 완료 (2026-07-05)
|
||||
---
|
||||
|
||||
## 다음 세션 재개 지점
|
||||
0. ⏳ **[진행중] iOS App Store 심사 결과 대기** — v1.0 제출 완료. 승인 시 출시(자동/수동), 거부 시 Resolution Center 대응(주로 데모 접근 안내)
|
||||
1. **테스터 12명 opt-in 진행상황 확인**(업체 의뢰) → 12명 + 14일 연속 충족되면 **프로덕션 액세스 신청**
|
||||
2. **v5 AAB 비공개 트랙 업로드** — 이번 작업분(애플로그인·페이지개편·상환조회 등) 반영해 AAB 재빌드·업로드
|
||||
3. **DUNS 발급 대기** → 발급 시 Play Console 개인사업자 계정 전환
|
||||
4. **수익화(출시 후)** — 구독(iOS StoreKit 미구현 → 결제코드+ASC상품+영수증검증 필요) / 광고(광고SDK+ATT+App Privacy 갱신). 둘 다 무료 출시 후 업데이트로
|
||||
5. (선택) **음성 전용 🎤 버튼** — `@capacitor-community/speech-recognition`
|
||||
6. **CI 러너 안정화** — Gitea act_runner 멈춤(재부팅 후 대기)로 배포 지연된 적 있음 → 자동재시작/헬스체크
|
||||
@@ -0,0 +1,93 @@
|
||||
// Play Console 스토어 그래픽 생성기.
|
||||
// 입력: brand/logo-1024.png (사용자가 저장한 1024x1024 돈돼지 마스터 아트)
|
||||
// 출력: brand/store/icon-512.png (스토어 고해상도 아이콘 512x512, 불투명)
|
||||
// brand/store/feature-1024x500.png (피처 그래픽 — 텍스트 없음, 항상 안정)
|
||||
// brand/store/feature-1024x500-text.png(피처 그래픽 — 한글 텍스트 포함, best-effort)
|
||||
//
|
||||
// 실행: node brand/gen-store-assets.mjs [입력경로]
|
||||
import sharp from 'sharp'
|
||||
import { mkdirSync, existsSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const SRC = resolve(process.argv[2] || join(__dirname, 'logo-1024.png'))
|
||||
const OUT = join(__dirname, 'store')
|
||||
|
||||
// 브랜드 색 (아이콘 배경의 크림/옐로 계열)
|
||||
const CREAM = '#FBF7E9'
|
||||
const YELLOW = '#F2C94C'
|
||||
const BROWN = '#B8860B'
|
||||
|
||||
if (!existsSync(SRC)) {
|
||||
console.error(`\n[!] 마스터 아트가 없습니다: ${SRC}`)
|
||||
console.error(` 1024x1024 돈돼지 PNG 를 위 경로에 저장한 뒤 다시 실행하세요.\n`)
|
||||
process.exit(1)
|
||||
}
|
||||
mkdirSync(OUT, { recursive: true })
|
||||
|
||||
const log = (f) => console.log(' ✓', f)
|
||||
|
||||
// 1) 스토어 아이콘 512x512 — 불투명 크림 배경 위에 마스터를 합성(투명 PNG 대비)
|
||||
async function icon512() {
|
||||
const logo = await sharp(SRC).resize(512, 512, { fit: 'contain', background: CREAM }).png().toBuffer()
|
||||
const out = join(OUT, 'icon-512.png')
|
||||
await sharp({ create: { width: 512, height: 512, channels: 4, background: CREAM } })
|
||||
.composite([{ input: logo }])
|
||||
.flatten({ background: CREAM })
|
||||
.png()
|
||||
.toFile(out)
|
||||
log('store/icon-512.png (512x512, 불투명)')
|
||||
}
|
||||
|
||||
// 공통: 1024x500 배경 — 피그가 놓일 우측(크림)에서 좌측(옐로 살짝)으로.
|
||||
// 중심을 피그 쪽 크림으로 두어 마스터의 크림 배경 사각이 자연스럽게 녹도록 한다.
|
||||
function bgSvg(cx = '80%') {
|
||||
return Buffer.from(`<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="500">
|
||||
<defs>
|
||||
<radialGradient id="g" cx="${cx}" cy="48%" r="95%">
|
||||
<stop offset="0%" stop-color="${CREAM}"/>
|
||||
<stop offset="62%" stop-color="${CREAM}"/>
|
||||
<stop offset="100%" stop-color="${YELLOW}" stop-opacity="0.55"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<rect width="1024" height="500" fill="url(#g)"/>
|
||||
</svg>`)
|
||||
}
|
||||
|
||||
// 2) 피처 그래픽 (텍스트 없음) — 가운데 큼직하게, 박스 없이 녹아들게
|
||||
async function featurePlain() {
|
||||
const bg = await sharp(bgSvg('50%')).png().toBuffer()
|
||||
const pig = await sharp(SRC).resize(420, 420, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toBuffer()
|
||||
const out = join(OUT, 'feature-1024x500.png')
|
||||
await sharp(bg).composite([{ input: pig, top: 40, left: 302 }]).png().toFile(out)
|
||||
log('store/feature-1024x500.png (텍스트 없음)')
|
||||
}
|
||||
|
||||
// 3) 피처 그래픽 (한글 텍스트) — 좌측 텍스트 / 우측 피그, 겹침 없도록 간격 조정.
|
||||
async function featureWithText() {
|
||||
const bg = await sharp(bgSvg('82%')).png().toBuffer()
|
||||
const pig = await sharp(SRC).resize(380, 380, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toBuffer()
|
||||
const text = Buffer.from(`<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="500">
|
||||
<style>
|
||||
.t { font-family: 'Malgun Gothic','맑은 고딕',sans-serif; fill:${BROWN}; }
|
||||
.title { font-size: 80px; font-weight: 800; }
|
||||
.sub { font-size: 29px; font-weight: 500; fill:#8a6d10; }
|
||||
</style>
|
||||
<text x="64" y="232" class="t title">돈돼지 가계부</text>
|
||||
<text x="68" y="290" class="t sub">슬림하게 관리하는</text>
|
||||
<text x="68" y="332" class="t sub">가계부 · 자산 · 예산</text>
|
||||
</svg>`)
|
||||
const out = join(OUT, 'feature-1024x500-text.png')
|
||||
await sharp(bg)
|
||||
.composite([{ input: pig, top: 60, left: 630 }, { input: text }])
|
||||
.png()
|
||||
.toFile(out)
|
||||
log('store/feature-1024x500-text.png (한글 텍스트, 폰트 렌더 확인 필요)')
|
||||
}
|
||||
|
||||
console.log(`\n[스토어 에셋 생성] 입력: ${SRC}\n`)
|
||||
await icon512()
|
||||
await featurePlain()
|
||||
await featureWithText()
|
||||
console.log(`\n완료 → ${OUT}\n`)
|
||||
|
After Width: | Height: | Size: 908 KiB |
@@ -0,0 +1,136 @@
|
||||
# Play Console 등록 자료 — 돈돼지 가계부
|
||||
|
||||
> 첫 출시(무료 앱, 인앱결제·광고 모두 비활성화) 기준. Play Console 각 항목에 그대로 붙여넣기.
|
||||
> 광고(카카오 애드핏)를 껐으므로 광고 식별자 수집/제3자 공유가 없어 데이터 안전이 단순함.
|
||||
|
||||
---
|
||||
|
||||
## 1. 스토어 등록정보 (Main store listing)
|
||||
|
||||
### 앱 이름 (30자 이내)
|
||||
```
|
||||
돈돼지 가계부
|
||||
```
|
||||
|
||||
### 간단한 설명 / Short description (80자 이내)
|
||||
```
|
||||
가계부·자산·예산을 슬림하게. 고정지출 자동, 통계 차트, 영수증 인식까지.
|
||||
```
|
||||
|
||||
### 자세한 설명 / Full description (4000자 이내)
|
||||
```
|
||||
🐷 돈돼지 가계부 — 슬림하게 관리하는 가계부 · 자산 · 예산
|
||||
|
||||
복잡한 가계부는 그만! 수입·지출을 빠르게 기록하고, 자산과 예산을 한눈에 관리하세요.
|
||||
군더더기 없는 깔끔한 화면으로 매일 쓰기 부담 없는 가계부입니다.
|
||||
|
||||
▪ 간편한 가계부
|
||||
· 수입·지출·이체를 몇 번의 터치로 기록
|
||||
· 자주 쓰는 내역을 칩으로 등록해 1초 입력
|
||||
· 월별 수입/지출/잔액 요약과 일별 내역 정리
|
||||
|
||||
▪ 자산 · 부채 관리
|
||||
· 은행·현금·신용/체크카드·대출·투자 계좌를 한 곳에서
|
||||
· 총자산 · 총부채 · 순자산을 자동 집계
|
||||
|
||||
▪ 고정 지출 자동 반영 (프리미엄)
|
||||
· 월세·구독료·급여 등 반복 거래를 등록해두면 주기에 맞춰 자동 생성
|
||||
· 실제 결제만 '확인'으로 확정 — 빠뜨림 없는 가계부
|
||||
|
||||
▪ 통계와 예산 (프리미엄)
|
||||
· 분류별 지출, 수입 대비 지출, 지난달 대비, 월말 예상까지 차트로 분석
|
||||
· 카테고리별 예산을 정하고 예산 대비 지출을 한눈에
|
||||
|
||||
▪ 영수증 인식 (프리미엄)
|
||||
· 영수증을 촬영하면 금액·내역을 자동 인식해 입력 시간을 단축
|
||||
|
||||
▪ 커뮤니티
|
||||
· 짠테크·재테크 노하우를 나누는 게시판
|
||||
· 추천·댓글로 소통하고 좋은 글을 발견
|
||||
|
||||
▪ 그 외
|
||||
· 다크 모드 지원
|
||||
· 엑셀 백업/복구로 내 데이터를 안전하게 (프리미엄)
|
||||
· 로그인 없이 '둘러보기'로 먼저 체험
|
||||
|
||||
무료로 핵심 가계부 기능을 모두 사용할 수 있으며, 통계·예산·고정지출 자동화·영수증 인식·백업 등은 프리미엄에서 제공됩니다. (구독 기능은 순차적으로 제공될 예정입니다.)
|
||||
|
||||
오늘부터 돈돼지와 함께 슬림한 가계 관리를 시작해보세요!
|
||||
|
||||
문의: dondwaeji.app@gmail.com
|
||||
```
|
||||
|
||||
### 기타 등록정보 필드
|
||||
- **앱 카테고리**: 금융 (Finance)
|
||||
- **태그**: 가계부, 예산, 자산관리
|
||||
- **연락처 이메일**: dondwaeji.app@gmail.com
|
||||
- **개인정보처리방침 URL**: https://www.sblog.kr/privacy
|
||||
- **그래픽 에셋**:
|
||||
- 앱 아이콘: brand/store/icon-512.png
|
||||
- 그래픽 이미지(피처): brand/store/feature-1024x500-text.png (또는 -text 없는 버전)
|
||||
- 휴대전화 스크린샷: brand/screenshots/shot1~6 (6장)
|
||||
|
||||
---
|
||||
|
||||
## 2. 데이터 안전 (Data safety) 양식
|
||||
|
||||
> "앱 콘텐츠 → 데이터 보안" 에서 작성. 아래 기준으로 답하면 개인정보처리방침과 일치함.
|
||||
|
||||
### 개요 질문
|
||||
- 데이터를 수집하나요? → **예**
|
||||
- 데이터를 제3자와 공유하나요? → **아니요** (광고 끔. OCR·AI(Anthropic)는 대행 처리(처리 위탁)이며 Google Play 기준 '공유'에 해당하지 않음)
|
||||
- 전송 중 데이터가 암호화되나요? → **예** (HTTPS)
|
||||
- 사용자가 데이터 삭제를 요청할 수 있나요? → **예** (앱 내 '회원 탈퇴' + 이메일 요청, https://www.sblog.kr/account-deletion)
|
||||
|
||||
### 수집하는 데이터 유형 (각 항목: 수집됨 / 목적 / 공유 여부)
|
||||
|
||||
| 데이터 유형 | 수집 | 공유 | 목적 |
|
||||
|---|---|---|---|
|
||||
| **개인 정보 → 이름** | 예 | 아니요 | 앱 기능, 계정 관리 |
|
||||
| **개인 정보 → 이메일 주소** | 예 | 아니요 | 앱 기능, 계정 관리 |
|
||||
| **금융 정보 → 기타 금융 정보** (사용자가 입력한 수입·지출·자산 등 가계부 데이터) | 예 | 아니요 | 앱 기능 |
|
||||
| **사진 및 동영상 → 사진** (프로필·영수증·게시판 이미지) | 예 | 아니요 | 앱 기능 |
|
||||
| **앱 활동 → 사용자가 생성한 콘텐츠** (게시판 글·댓글) | 예 | 아니요 | 앱 기능 |
|
||||
| **앱 활동 → 앱 상호작용** | 예 | 아니요 | 앱 기능, 부정 이용 방지 |
|
||||
|
||||
각 항목 공통:
|
||||
- **수집이 필수인가요?** → "데이터 수집이 필수임" (서비스 제공에 필요)
|
||||
- **데이터가 암호화되나요(전송 중)?** → 예
|
||||
- **삭제 요청 가능?** → 예
|
||||
|
||||
> ✅ 광고를 껐으므로 **광고 식별자(기기 ID) 수집/공유가 없습니다** — 데이터 안전 양식이 단순해지고 심사 불일치 리스크가 사라졌습니다.
|
||||
|
||||
### AI 편의 기능의 데이터 처리 (처리 위탁, 공유 아님)
|
||||
> 아래는 개인정보처리방침 3항과 정합. 회원이 해당 기능을 쓸 때에만, 기능 수행 목적으로 Anthropic(Claude) API에 전송됨. Anthropic은 학습에 사용하지 않고, 서비스도 별도 저장하지 않음(폼 자동 채움용). API 키는 서버 보관.
|
||||
> - 자연어 한 줄 입력: 입력 텍스트 → 금액·분류 등 구조화
|
||||
> - 영수증 사진 구조화: 영수증 이미지 → 금액·상호·날짜·분류
|
||||
> - 분류·계좌 자동 추천: 분류·계좌 '이름' 목록
|
||||
> - 재무 코멘트: 이번 달 **집계 수치만**(개별 거래 원문 미전송)
|
||||
>
|
||||
> Play 데이터 안전에서 위 데이터 유형(금융 정보·사진)의 **공유 여부는 계속 '아니요'** (대행 처리는 공유 아님). 다만 방침에 처리자(Anthropic)를 명시했으므로 심사자 교차확인에도 일치함.
|
||||
|
||||
---
|
||||
|
||||
## 3. 콘텐츠 등급 설문 (IARC)
|
||||
|
||||
> "앱 콘텐츠 → 콘텐츠 등급" 설문. 핵심 답안:
|
||||
|
||||
- 앱 카테고리: **유틸리티/생산성/기타** (또는 금융)
|
||||
- 폭력/성적/혐오/약물/도박 콘텐츠: **모두 없음**
|
||||
- **사용자 간 상호작용/사용자 생성 콘텐츠(게시판)**: **있음** → "사용자가 콘텐츠를 만들고 공유할 수 있음" 으로 정직하게 표시
|
||||
- 신고 기능(5건 누적 블라인드)·관리자 차단 등 **중재 수단 보유** → 관련 질문에 "콘텐츠 신고/차단 기능 있음"으로 답변
|
||||
- 위치 공유 / 개인정보 공유 기능: **없음**
|
||||
- 디지털 구매(인앱결제): 첫 출시는 **비활성화** → "없음" (추후 활성화 시 등급 갱신)
|
||||
- **광고 포함 여부**: **아니요** (광고 껐음 — 추후 활성화 시 갱신)
|
||||
- 예상 등급: 대체로 **만 3세 이상(전체이용가)** 수준, UGC로 인해 일부 지역에서 Teen 가능 — 설문 결과 따름
|
||||
|
||||
---
|
||||
|
||||
## 4. 기타 출시 전 체크
|
||||
- **앱 액세스 권한**: 로그인 필요 → 심사용 테스트 계정 제공 (Play Console "앱 액세스 권한"에 kenneth73510 계정 또는 데모 안내). 데모(둘러보기)로도 핵심 화면 확인 가능함을 메모.
|
||||
- **타겟 출시 국가**: 대한민국 (필요 시 확대)
|
||||
- **데이터 삭제 URL**: https://www.sblog.kr/account-deletion (데이터 안전 양식 + 앱 콘텐츠)
|
||||
- **앱 카테고리/연락처/방침 URL** 위 1번 참고
|
||||
- 광고 포함 여부 질문: **아니요(광고 없음)** — 광고를 껐으므로 "광고 없음"으로 표시
|
||||
- ⚠️ **개인정보처리방침 정합성**: 현재 방침 4항이 "무료 회원에게 광고가 표시되며…"로 적혀 있어 광고 OFF 와 불일치. 심사자 교차확인 대비해 방침의 광고 문구를 "향후 광고가 도입될 수 있으며…"(미래형)로 완화 권장. (원하면 수정·배포 도와드림)
|
||||
```
|
||||
@@ -0,0 +1,57 @@
|
||||
# 프로덕션 액세스 신청서 초안 — 돈돼지 가계부
|
||||
|
||||
> 14일 폐쇄 테스트 완료 후 Play Console "프로덕션 액세스 신청" 폼에 입력.
|
||||
> ⚠️ [대괄호] 부분은 실제 테스트 후 본인 경험으로 교체하세요. 거짓·과장 금지 — 실제로 테스트한 내용을 적어야 합니다.
|
||||
|
||||
---
|
||||
|
||||
## Q1. 테스터를 어떻게 모집했나요? (How did you recruit your testers?)
|
||||
|
||||
```
|
||||
실제 Android 기기와 Google 계정을 보유한 [12]명의 테스터를 모집해 비공개 테스트를 진행했습니다.
|
||||
테스터는 안드로이드 사용자 지인 및 개발자 커뮤니티의 베타 테스터 모임을 통해 모았으며,
|
||||
모두 실제 기기에 앱을 설치하고 14일간 사용했습니다.
|
||||
```
|
||||
> 유료/품앗이 서비스를 쓴 경우에도 "실제 기기·실제 계정 테스터"라는 사실 위주로 적되, 허위로 쓰지 마세요.
|
||||
|
||||
## Q2. 테스터와 어떻게 소통하고 피드백을 수집했나요?
|
||||
|
||||
```
|
||||
이메일과 메신저(오픈채팅)로 테스터에게 설치 안내와 점검 항목을 전달했습니다.
|
||||
점검 항목: 회원가입/구글 로그인, 가계부 수입·지출 기록, 계좌·자산 관리, 예산·통계 화면,
|
||||
고정지출 자동 반영, 커뮤니티 게시판(글·댓글·추천), 다크 모드.
|
||||
테스터가 사용 중 발견한 버그·불편 사항을 회신받아 정리했습니다.
|
||||
```
|
||||
|
||||
## Q3. 어떤 피드백을 받았고, 무엇을 개선했나요?
|
||||
|
||||
```
|
||||
주요 피드백과 개선:
|
||||
- 둘러보기(로그인 없이 체험) 모드에서 일부 화면의 메뉴 접근이 불편 → 메뉴 진입 동선 수정
|
||||
- 특정 기기에서 한글 입력 시 자동완성/맞춤법 검사로 입력이 끊김 → 입력 필드 설정 수정으로 해결
|
||||
- 무료/유료 기능 구분 및 화면 흐름이 헷갈린다는 의견 → 안내 문구·접근 동선 정리
|
||||
- [본인 테스트 중 실제로 발견한 버그/불편과 그에 대한 수정 내용을 추가 기재]
|
||||
크래시나 데이터 유실 등 치명적 문제는 발견되지 않았습니다.
|
||||
```
|
||||
|
||||
## Q4. 앱이 프로덕션에 준비되었다고 보는 이유는?
|
||||
|
||||
```
|
||||
돈돼지 가계부는 웹·Windows 데스크톱에서 이미 운영 중인 개인 가계부 서비스이며,
|
||||
이번 비공개 테스트로 Android 실기기에서 핵심 기능(가계부 기록, 계좌·자산, 예산·통계,
|
||||
고정지출 자동 반영, 커뮤니티, 구글 로그인)의 안정성을 14일간 검증했습니다.
|
||||
다양한 기기에서 크래시 없이 정상 동작하며, 테스트 중 제기된 개선 사항을 반영했습니다.
|
||||
개인정보처리방침·데이터 삭제 안내 등 정책 요건도 갖추어 정식 출시 준비가 되었다고 판단합니다.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## (선택) 영어 버전 — 폼이 영어로 표시될 경우
|
||||
|
||||
**Q1 Recruit:** I recruited [12] testers who own real Android devices with genuine Google accounts, through acquaintances and a developer beta-tester community. All installed the app and used it over the 14-day period.
|
||||
|
||||
**Q2 Feedback collection:** I communicated via email and messenger, sharing install instructions and a checklist (Google sign-in, expense/income tracking, accounts & assets, budget & statistics, recurring transactions, community board, dark mode), and collected bug reports and usability feedback.
|
||||
|
||||
**Q3 Findings & changes:** Key fixes included improving menu navigation in the guest "explore" mode, resolving a Korean-input (IME) issue on certain devices, and clarifying free/premium feature flow. [Add the real bugs you found.] No crashes or data-loss issues were found.
|
||||
|
||||
**Q4 Readiness:** Don Dwaeji budget app already runs in production on web and Windows desktop. The closed test validated the Android build's core features on real devices over 14 days with no crashes, and feedback was addressed. Privacy policy and data-deletion pages are in place, so the app is ready for production.
|
||||
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 184 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 219 KiB |
|
After Width: | Height: | Size: 203 KiB |
|
After Width: | Height: | Size: 232 KiB |
|
After Width: | Height: | Size: 279 KiB |
@@ -0,0 +1,9 @@
|
||||
; 기본 설치 경로 지정 (사용자 폴더 = 관리자 권한 불필요 → UAC 프롬프트 없음).
|
||||
; productName 이 한글이라 electron-builder 가 기본 경로를 package.json name(sb_pt)으로 폴백하는 문제도 함께 회피.
|
||||
; → %LOCALAPPDATA%\Programs\DonDwaeji 로 고정 (사용자는 설치 시 변경 가능).
|
||||
!macro preInit
|
||||
SetRegView 64
|
||||
WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "$LOCALAPPDATA\Programs\DonDwaeji"
|
||||
SetRegView 32
|
||||
WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "$LOCALAPPDATA\Programs\DonDwaeji"
|
||||
!macroend
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"appId": "kr.sblog.slimbudget",
|
||||
"appName": "Slim Budget",
|
||||
"appName": "돈돼지 가계부",
|
||||
"webDir": "dist",
|
||||
"server": {
|
||||
"cleartext": true
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
# Slim Budget — 프론트엔드 (sb_pt)
|
||||
# 돈돼지 가계부 — 프론트엔드 (sb_pt)
|
||||
|
||||
Vue 3 + Vite 기반의 가계부·게시판 웹 애플리케이션 프론트엔드.
|
||||
Vue 3 + Vite 기반의 가계부·자산·게시판 앱 프론트엔드. 하나의 코드베이스로 **웹 / 안드로이드(Capacitor) / Windows 데스크톱(Electron)** 3개 플랫폼을 빌드합니다.
|
||||
|
||||
## 기술 스택
|
||||
- **Vue 3** (`<script setup>` Composition API) + **Vite**
|
||||
- **Pinia** (상태관리), **Vue Router** (라우팅)
|
||||
- **Axios** (HTTP, 공통 인스턴스 + 토큰/401 인터셉터)
|
||||
- **Toast UI Editor** (`@toast-ui/editor`) + Prism.js (게시판 마크다운 작성/뷰어·코드 하이라이트)
|
||||
- **Capacitor**(안드로이드 하이브리드 앱) · **Electron**(Windows 데스크톱) · **Vitest**(테스트)
|
||||
|
||||
## 브랜딩
|
||||
- 서비스명 **Slim Budget** (헤더 로고/타이틀/푸터)
|
||||
- 로고: `public/logo.svg` (브랜드 그린 `#00bc7e` 배지 + 슬림 막대 차트), 파비콘으로도 사용
|
||||
- 서비스명 **돈돼지 가계부** (구 Slim Budget)
|
||||
- 로고: 돈돼지(투명 배경) — 헤더/사이드바 로고·파비콘·앱 런처 아이콘. 브랜드 그린 `#00bc7e`
|
||||
|
||||
## 플랫폼별 동작
|
||||
- **웹(PC·모바일)**: 안내 페이지([WebOnlyNotice.vue](../src/components/WebOnlyNotice.vue))만 노출(앱 다운로드 유도). 실제 이용은 앱에서. `npm run dev` 는 예외로 전체 앱 표시
|
||||
- **안드로이드**: Capacitor WebView 패키징 → [ANDROID.md](ANDROID.md)
|
||||
- **Windows**: Electron 이 라이브 사이트(app.sblog.kr)를 직접 로드 → 프론트 변경이 재설치 없이 반영
|
||||
- 기능 플래그 [src/config/features.js](../src/config/features.js): `ID_LOGIN_ENABLED`(아이디 로그인)·`BILLING_ENABLED`(인앱결제)·`ADS_ENABLED`(광고)·`CARD_NOTIF_ENABLED`(카드알림) — 첫 출시 기준 전부 `false`
|
||||
|
||||
## 디렉터리 구조
|
||||
```
|
||||
@@ -21,65 +28,76 @@ src/
|
||||
│ ├── boardApi.js # 게시판
|
||||
│ ├── adminApi.js # 관리자(태그 카테고리)
|
||||
│ └── accountApi.js # 가계부 전체
|
||||
│ ├── billingApi.js # 인앱결제/구독
|
||||
├── stores/ # Pinia (auth, ui, user)
|
||||
├── config/features.js # 기능 플래그(아이디로그인·결제·광고·카드알림)
|
||||
├── router/index.js # 라우트 + 인증 가드
|
||||
├── components/
|
||||
│ ├── layout/ # AppHeader / AppSidebar / AppFooter
|
||||
│ ├── ui/IconBtn.vue # 공통 SVG 아이콘 버튼
|
||||
│ ├── layout/ # AppHeader / AppSidebar / AppFooter / AppBottomNav
|
||||
│ ├── ui/ # IconBtn / AppModal(전체화면 슬라이드업) / AppDialog / CategoryPicker
|
||||
│ ├── editor/ # MarkdownEditor / MarkdownViewer
|
||||
│ ├── LoginModal.vue / SignupModal.vue
|
||||
│ ├── LoginModal.vue / SignupModal.vue / AppLockScreen.vue(PIN) / AdBanner.vue / WebOnlyNotice.vue
|
||||
├── views/
|
||||
│ ├── HomeView.vue # 홈(대시보드 자리, 현재 빈 공간)
|
||||
│ ├── UsersView.vue # 사용자 관리
|
||||
│ ├── HomeView.vue # 홈 대시보드(요약·일별 캘린더)
|
||||
│ ├── board/ # 게시판 (목록/상세/작성)
|
||||
│ ├── account/ # 가계부 (아래 참조)
|
||||
│ └── admin/ # 태그 관리(관리자)
|
||||
└── assets/main.css # 전역 스타일
|
||||
│ └── admin/ # 회원/태그 관리(관리자)
|
||||
└── assets/ # base.css(data-theme 다크모드) 등 전역 스타일
|
||||
```
|
||||
|
||||
## 인증
|
||||
- 로그인 시 발급된 **Bearer 토큰**을 `localStorage`에 저장, `http.js` 요청 인터셉터가 `Authorization` 헤더에 주입
|
||||
- **구글 로그인**(웹/PC=Google Identity Services, 앱=네이티브 플러그인) — 현재 소셜 로그인 전용(`ID_LOGIN_ENABLED=false` 로 아이디/비번 폼·회원가입 UI 숨김)
|
||||
- 401 응답 시 토큰 정리 후 `auth:unauthorized` 이벤트 → 로그인 모달 표시
|
||||
- 라우트 `meta.requiresAuth` / `requiresAdmin` 가드
|
||||
|
||||
## 레이아웃 / 반응형
|
||||
- 그리드 레이아웃(헤더·사이드바·본문·푸터)
|
||||
## 레이아웃 / 반응형 / 테마
|
||||
- 그리드 레이아웃 — 사이드바가 좌측 상단(헤더 행)까지, 헤더는 현재 화면 타이틀 표시. 본문 폭 `--page-max`(760px)로 통일
|
||||
- **하단 고정 내비**([AppBottomNav.vue](../src/components/layout/AppBottomNav.vue)): 뒤로·앞으로·홈·새로고침·설정 (안드로이드 소프트키 위 안전영역)
|
||||
- **모바일(≤768px)**: 사이드바가 **오프캔버스 드로어**(햄버거 토글 + 백드롭, 라우트 이동 시 자동 닫힘)
|
||||
- 모든 액션 버튼은 **아이콘화**(`IconBtn`: 등록/수정/삭제/검색/페이징 등), `title`로 접근성·툴팁 제공
|
||||
- 한 줄에 담기 어려운 목록은 **2줄 표기**(게시판: 제목/메타, 가계부 내역: 분류·금액/날짜·메모·액션 등)
|
||||
- **입력 모달**: 전체화면 슬라이드업([AppModal.vue](../src/components/ui/AppModal.vue), 바깥 닫힘 방지). 셀렉트 대신 **뱃지/칩** 선택, 분류는 공통 [CategoryPicker.vue](../src/components/ui/CategoryPicker.vue)(대/소분류 아코디언)
|
||||
- **테마**: 시스템 / 라이트 / 다크 (설정 화면, `data-theme` 기반)
|
||||
- 모든 액션 버튼은 **아이콘화**(`IconBtn`), `title`로 접근성·툴팁 제공
|
||||
|
||||
## 게시판 (`/board`)
|
||||
- 목록(페이징·태그 필터·검색[제목/본문/댓글]), 상세, 작성/수정(마크다운)
|
||||
- 댓글, 태그, 관리자 **열람 제한/해제**
|
||||
- **커뮤니티 / 짠테크 수다방 / 재테크 팁** 3개 게시판(카테고리)
|
||||
- 목록(페이징·태그 필터·검색[제목/본문/댓글]·공지 최상단), 상세, 작성/수정(마크다운·이미지 업로드)
|
||||
- 댓글, 추천(vote), 태그, **공지**(관리자), 관리자 **열람 제한/해제**, 신고
|
||||
|
||||
## 가계부 (`/account`)
|
||||
사용자별 데이터 격리(본인 데이터만). 주요 화면(사이드바 하위 메뉴):
|
||||
|
||||
| 경로 | 화면 | 설명 |
|
||||
|------|------|------|
|
||||
| `/account` | 대시보드 | 예산 대비 지출 도넛, **분류별 파이차트**, 기간별 예산대비지출 막대(툴팁), 수입대비지출, 월별 총액, **순자산 추이** 라인차트 |
|
||||
| `/account/entries` | 가계부 내역 | 월별 내역 CRUD, **검색·필터**(키워드/구분/분류/계좌/태그), 분류 인라인 추가 |
|
||||
| `/account/recurrings` | 정기 거래 | 일/주/월/년 반복 규칙, 진입 시 자동 반영(중복 없이) |
|
||||
| `/account/wallets` | 계좌 관리 | 은행/카드/대출/**투자** 계좌, 잔액·순자산, 상환/납부, **투자 포트폴리오** |
|
||||
| `/account/categories` | 분류 관리 | 수입/지출 분류 CRUD, 이름 변경 전파, 기존 내역에서 가져오기 |
|
||||
| `/account/budget` | 예산 설정 | 고정(일수 기준 자동 환산)/비고정, 예산 대비 진행률 |
|
||||
| `/account` | 대시보드 | 예산 대비 지출 도넛, **분류별 파이차트**(대분류 집계), 수입대비지출(월말 예상·지난달 대비), 월별 총액, **순자산 추이**(주간) 라인차트, 일별 캘린더 |
|
||||
| `/account/entries` | 가계부 내역 | 월별 내역 CRUD, **검색·필터**(키워드/구분/분류/계좌/태그), 날짜 그룹 접기/펴기, **외화 입력**(환율 자동환산), 내역→고정지출 바로 등록 |
|
||||
| `/account/recurrings` | 고정 지출 | 일/주/월/년 반복 규칙, 진입 시 자동 반영(중복 없이·'확인 필요'로 생성) |
|
||||
| `/account/wallets` | 계좌 관리 | 은행/**현금**/카드/대출/**투자** 계좌, 잔액·순자산, 상환/납부(원금·이자·연회비 분리), 월별 이용내역 |
|
||||
| `/account/categories` | 분류 관리 | 수입/지출 분류 CRUD(**대/소분류 2단계**), 칩 그리드 관리, 순서변경, 기존 내역에서 가져오기 |
|
||||
| `/account/budget` | 예산 설정 | **월별 예산**(전월/다음달 복사), 고정(일수 기준 자동 환산)/비고정, 예산 대비 진행률 |
|
||||
| `/account/tags` | 태그 관리 | 사용자별 가계부 태그 |
|
||||
|
||||
### 잔액·순자산 모델 (표시)
|
||||
- 자산(+) / 부채(−) 부호 모델. 지갑 잔액 = 개시잔액 + 수입 − 지출 − 이체출금 + 이체입금
|
||||
- 자산(+) / 부채(−) 부호 모델. 현금은 은행과 동일하게 자산 합산. 지갑 잔액 = 개시잔액 + 수입 − 지출 − 이체출금 + 이체입금
|
||||
- 순자산 = 총자산 − 총부채 (투자계좌는 평가금액 반영)
|
||||
|
||||
### 투자 포트폴리오 (계좌 관리 > 투자 탭)
|
||||
### 투자 계좌 (계좌 관리 > 투자 탭)
|
||||
- 투자계좌를 펼치면 [InvestPortfolio.vue](../src/views/account/InvestPortfolio.vue) 패널
|
||||
- **종목별** 보유: 수량·평균매입단가·현재가(수동)·평가금액·**평가손익·수익률**·실현손익
|
||||
- **매수/매도** 건별 기록(이동평균 평단 자동 산출, 매도수량 초과 검증)
|
||||
- 입금은 가계부 **이체**(은행→투자)로, 예수금·주식평가는 자동 계산되어 순자산·추이에 반영
|
||||
- 원화·정수 수량 기준 (해외주식 소수점/환율·자동 시세는 범위 외)
|
||||
- **단순형 UI**: **투자금(투입원금) + 현재 평가액**만 입력 → 손익·수익률 자동 계산(퇴직연금·연금 등 포함). 계좌 펼침 시 투자금/평가액/손익(%) 요약 카드
|
||||
- 입금은 가계부 **이체**(은행→투자)로, 평가액은 순자산·추이에 반영
|
||||
- ℹ️ 백엔드는 종목 단위(매수·매도·시세 자동조회) API 를 유지하나 현재 UI 에는 노출하지 않음. 원화 기준
|
||||
|
||||
## 설정 화면
|
||||
- 하단 내비 → 설정: 계정정보(이름·이메일·비밀번호 변경) · 테마(시스템/라이트/다크) · 앱 잠금(PIN, 1시간 유예) · 앱 버전 · App Data 삭제 · (PC)창 크기 프리셋
|
||||
|
||||
## 개발/빌드
|
||||
```bash
|
||||
npm install
|
||||
npm run dev # 개발 서버
|
||||
npm run build # dist/ 정적 빌드
|
||||
npm run dev # 개발 서버(전체 앱 표시)
|
||||
npm run build # dist/ 정적 빌드(웹 배포)
|
||||
npm run cap:sync # 앱 빌드 + 안드로이드 동기화
|
||||
npm run electron:build # Windows 설치파일
|
||||
npm test # Vitest
|
||||
```
|
||||
- API 베이스 URL: `VITE_API_BASE_URL` (기본 `/api`)
|
||||
- 상세: [../README.md](../README.md) · 앱 [ANDROID.md](ANDROID.md) · 배포 [../deploy/README.md](../deploy/README.md)
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# iOS 하이브리드 앱 (Capacitor)
|
||||
|
||||
Vue `dist` 를 네이티브 WebView로 패키징해 iOS 앱으로 빌드합니다. 안드로이드와 **동일 코드베이스**이며, 플랫폼 분기는 `Capacitor.isNativePlatform()` / `getPlatform() === 'ios'` 로 처리합니다.
|
||||
|
||||
## 구성
|
||||
- `capacitor.config.json` — appId `kr.sblog.slimbudget`(안드와 동일), appName `돈돼지 가계부`, webDir `dist`
|
||||
- `ios/` — 네이티브 프로젝트(커밋됨, 빌드 산출물 `ios/App/build`·`Pods` 는 .gitignore)
|
||||
- **Capacitor 8 = Swift Package Manager(SPM)** 사용 (CocoaPods 아님 — Podfile 불필요)
|
||||
- 웹 게이팅: iOS 는 `isNativePlatform()` 이 true → 안내 페이지가 아닌 전체 앱 표시
|
||||
|
||||
## 사전 준비 (Mac)
|
||||
- **Xcode**(App Store, 대용량) + iOS SDK/시뮬레이터 런타임
|
||||
```bash
|
||||
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
|
||||
sudo xcodebuild -license accept
|
||||
xcodebuild -downloadPlatform iOS # 시뮬레이터 런타임(별도 온디맨드 다운로드)
|
||||
```
|
||||
- **Apple Developer Program($99/년)** — 실기기 배포·App Store·Apple 로그인 Capability 에 필요. (시뮬레이터 UI 테스트는 불필요)
|
||||
|
||||
## 최초 셋업 (1회)
|
||||
```bash
|
||||
npm i @capacitor/ios
|
||||
npx cap add ios # ios/ 생성
|
||||
npm run build:app && npx cap sync ios
|
||||
npx capacitor-assets generate --ios # 브랜드 아이콘 생성(assets/ 기준)
|
||||
```
|
||||
|
||||
## 빌드 & 실행
|
||||
```bash
|
||||
npm run build:app && npx cap sync ios # 코드 변경 시마다
|
||||
npx cap open ios # Xcode 열기
|
||||
# 또는 CLI 로 시뮬레이터에 바로:
|
||||
npx cap run ios --target <시뮬레이터-UDID>
|
||||
```
|
||||
시뮬레이터 목록: `xcrun simctl list devices available | grep iPhone`
|
||||
|
||||
### 실기기 실행 (Xcode)
|
||||
1. **데이터 지원 케이블**로 아이폰 연결 → "이 컴퓨터 신뢰" (충전 전용 케이블은 인식 안 됨)
|
||||
2. Xcode → **App 타겟 → Signing & Capabilities → Automatically manage signing → Team 선택**(무료 Apple ID 도 7일 한정 가능)
|
||||
3. 상단 툴바 중앙 **기기 선택**(스킴 `App` 옆) → 연결된 아이폰 → **▶ Run**
|
||||
4. 최초엔 아이폰이 **개발자 모드**를 요구 → Run 시도 후 **설정 → 개인정보 보호 및 보안 → 개발자 모드 → 켬 → 재시동**
|
||||
5. "신뢰할 수 없는 개발자" → **설정 → 일반 → VPN 및 기기 관리 → 개발자 앱 → 신뢰**
|
||||
|
||||
## 필수 설정 (반영 완료)
|
||||
- **번들 ID** `kr.sblog.slimbudget` (안드로이드와 동일 — App Store Connect 에서 그대로 사용)
|
||||
- **Info.plist 권한 문구** (없으면 카메라/사진 접근 시 크래시):
|
||||
- `NSCameraUsageDescription`(영수증·프로필·게시판 촬영)
|
||||
- `NSPhotoLibraryUsageDescription` / `NSPhotoLibraryAddUsageDescription`
|
||||
- **safe-area**: `index.html` 의 `viewport-fit=cover` + CSS `env(safe-area-inset-*)`.
|
||||
⚠️ 전역 `box-sizing: border-box` 에서 고정 `height` 를 쓰면 safe-area 패딩이 높이에 포함돼 콘텐츠가 눌린다 → 헤더는 `min-height`+세로패딩, 하단 내비는 `height: calc(56px + env(safe-area-inset-bottom))` 로 처리(적용됨).
|
||||
|
||||
## Apple 로그인 (Sign in with Apple)
|
||||
> App Store 심사 가이드라인 **4.8**: 제3자 소셜 로그인(구글)을 제공하면 **Apple 로그인도 동등하게 제공**해야 함. iOS 출시에 **사실상 필수**.
|
||||
|
||||
**구현 상태(코드 완료):**
|
||||
- 프론트: `@capacitor-community/apple-sign-in` + [LoginModal.vue](../src/components/LoginModal.vue) Apple 버튼(iOS 전용) + [appleAuth.js](../src/native/appleAuth.js)
|
||||
- ⚠️ 이 플러그인은 Capacitor 7 기준(`capacitor-swift-pm 7.x`) → **patch-package** 로 8.x 로 패치(`patches/`). `npm ci` 후 `postinstall` 이 자동 적용. @capawesome 구글 플러그인과 iOS 공존.
|
||||
- 백엔드: `POST /api/auth/apple` — **nimbus-jose-jwt 로 Apple JWKS(공개키) 서명 검증** + iss/aud(번들ID)/exp 확인, `member.apple_id` 로 구글과 동일하게 이메일 자동연결. `app.apple-client-id`(기본 `kr.sblog.slimbudget`).
|
||||
- `ios/App/App/App.entitlements` — `applesignin` 엔틀먼트 준비됨.
|
||||
|
||||
**작동시키려면(유료 승인 후):**
|
||||
1. Apple Developer 포털: App ID `kr.sblog.slimbudget` 에 **Sign in with Apple 활성화**
|
||||
2. Xcode → Signing & Capabilities → **+ Capability → "Sign in with Apple"** (준비된 `App.entitlements` 연결 + 프로비저닝)
|
||||
3. 테스트: 시뮬레이터 **설정 → Apple ID 로그인** 후 `cap run ios` → Apple 버튼
|
||||
- Sign in with Apple 은 **시뮬레이터에서도 작동**(카메라/푸시와 달리). 단 1~2 가 갖춰져야 함.
|
||||
|
||||
## 배포 (App Store)
|
||||
- **의무 테스트 기간 없음** — 안드로이드(Play)의 "테스터 12명·14일"과 달리, iOS 는 **바로 심사 제출 가능**. TestFlight 은 **선택**.
|
||||
- 절차: App Store Connect 앱 생성 → 아이콘·스크린샷(6.x" iPhone 규격)·App Privacy 작성 → Xcode Archive → 업로드 → 심사(보통 1~3일)
|
||||
- **TestFlight**(선택): 유료 승인 후 빌드 업로드 → **케이블 없이 아이폰에 무선 설치**(데이터 케이블 없을 때 유용). 내부 100명 즉시 / 외부 10,000명(가벼운 베타 심사).
|
||||
|
||||
## 자주 막히는 부분
|
||||
- **SPM 의존성 충돌**: 플러그인이 서로 다른 `capacitor-swift-pm`/`googlesignin-ios` 버전을 요구하면 빌드 실패. Capacitor 8 전용 플러그인을 쓰거나(patch-package) 버전 정렬 필요.
|
||||
- **USB 연결이 충전만 됨**: 충전 전용 케이블 → **데이터 케이블**로 교체. 무선 디버깅도 최초 1회 유선 페어링 필요.
|
||||
- **개발자 모드 메뉴가 없음**: Xcode 로 기기 설치를 **한 번 시도한 뒤** 설정에 항목이 생김(iOS 16+).
|
||||
- **Developer 등록 "보류"**: 개인 등록은 Apple 이 추가 본인확인을 요청할 수 있음 → **Apple ID 이메일 확인 후 회신**해야 진행됨(자동으로 안 풀림).
|
||||
- **카메라/푸시는 시뮬레이터 제한** → 실기기 필요. Apple 로그인은 시뮬레이터 가능.
|
||||
|
||||
## 참고
|
||||
- 안드로이드 빌드: [ANDROID.md](ANDROID.md) · 아키텍처: [FRONTEND.md](FRONTEND.md)
|
||||
- 앱 전용 백엔드 URL: `.env.capacitor` 의 `VITE_API_BASE_URL`(HTTPS 필수)
|
||||
@@ -0,0 +1,51 @@
|
||||
# 멤버십(무료/유료) 메뉴·기능 구분 — 설계 (추후 구현)
|
||||
|
||||
> 상태: **설계만** (미구현). 결제·게이팅은 추후 작업.
|
||||
> 방향: **핵심 기록은 무료로 개방**(진입장벽 ↓), **과금 기능·자동화·고급 분석·무제한·백업은 유료**.
|
||||
> 계좌 개수 제한은 **두지 않음**(1~소수 제한은 핵심 기능을 막아 거부감 → 제외).
|
||||
|
||||
---
|
||||
|
||||
## 1. 무료 / 유료 구분
|
||||
|
||||
| 메뉴 / 기능 | 무료 | 유료(프리미엄) | 비고 |
|
||||
|---|:--:|:--:|---|
|
||||
| **가계부 내역**(수입·지출 기록) | ✅ | ✅ | 핵심 — 무료 개방 |
|
||||
| **계좌 관리**(은행·현금·카드·대출) | ✅ **무제한** | ✅ 무제한 | 개수 제한 없음 |
|
||||
| 자주 쓰는 내역(빠른 등록) | ✅ | ✅ | 편의, 무료 |
|
||||
| 월별 요약(수입/지출/잔액) | ✅ | ✅ | 기본 요약은 무료 |
|
||||
| 분류 관리 | ✅ 기본+소수 | ✅ 무제한·소분류(2단계) | 무료는 기본 분류 위주 |
|
||||
| 태그 관리 | — | ✅ | |
|
||||
| **통계**(순자산 추이·예상 지출·지난달 대비·분류 분석) | — | ✅ | 고급 분석은 유료 |
|
||||
| 예산 설정 | — | ✅ | |
|
||||
| 고정 지출(자동 생성·확인) | — | ✅ | 자동화 = 유료 |
|
||||
| **영수증 OCR** | — | ✅ | **Google Vision 과금** → 유료 필수 |
|
||||
| **카드 결제 알림 자동인식** | — | ✅ | 강력한 유료 훅(편의·자동화) |
|
||||
| 투자/자산(평가액) 관리 | — | ✅ | |
|
||||
| 데이터 백업/복구(엑셀·JSON) | — | ✅ | 추후 백업 기능과 연계 |
|
||||
| 게시판(커뮤니티/짠테크/재테크 팁) | ✅ | ✅ | 커뮤니티는 전체 개방 |
|
||||
| 다크모드·테마 | ✅ | ✅ | 기본 제공 |
|
||||
| 광고 | (도입 시) 표시 | 제거 | 광고 모델은 미정 |
|
||||
|
||||
## 2. 구분 기준
|
||||
- **무료** = 기본 기록 + (무제한)계좌 + 기본 분류 + 월 요약 → "충분히 써볼 만한" 수준
|
||||
- **유료** = ① 과금 기능(OCR) · ② 자동화(카드 알림 인식 · 고정지출) · ③ 고급 분석(통계 전체 · 예산) · ④ 무제한(태그/소분류) · ⑤ 백업/복구
|
||||
- 원칙: **무료를 일부러 불편하게 만들지 않고, 유료에 "확실히 편한 것"을 몰아준다**(전환율 우선).
|
||||
|
||||
## 3. 구현 시 필요한 것 (추후)
|
||||
- **데이터**: `member.plan`(`FREE`/`PREMIUM`) + `plan_expires_at`(만료일) 컬럼.
|
||||
- **게이팅(이중)**:
|
||||
- 프론트: 유료 메뉴에 **잠금 배지 + 업그레이드 유도**(데모의 잠금 화면 재사용 가능), 비유료는 진입 차단.
|
||||
- 백엔드: 해당 API에 **plan 권한 체크**(프론트 우회 방지). OCR·알림 인식·통계·예산·고정지출·투자·백업 엔드포인트.
|
||||
- **결제 연동**: 안드로이드 인앱결제(Google Play Billing) / 웹·PC는 PG. 정기결제 시 만료일 갱신.
|
||||
- **기존 데이터 보호**: 유료→무료 강등(만료) 시 데이터는 보존하되 **유료 메뉴만 잠금**(읽기 정도는 허용 검토).
|
||||
|
||||
## 4. 추후 결정 필요(미정)
|
||||
- 가격·주기(월/연), 무료체험 기간.
|
||||
- 광고 모델 도입 여부(무료=광고 / 유료=제거).
|
||||
- 무료 "기본 분류" 범위(개수·기본 템플릿만 허용 여부).
|
||||
- 강등 시 잠금 정책(완전 차단 vs 읽기 허용).
|
||||
|
||||
---
|
||||
|
||||
*참고: 데모(`/demo`)의 잠금 안내 화면을 유료 게이팅 UI 의 기반으로 재사용할 수 있음.*
|
||||
@@ -156,13 +156,67 @@
|
||||
|
||||
---
|
||||
|
||||
# (이어진 작업 4) 구글 로그인 · 다크모드 · UI 개편 (2026-06-28)
|
||||
|
||||
## 34. 구글 로그인/가입 (소셜 로그인)
|
||||
- **웹/PC/앱 3플랫폼 구글 로그인**: 웹·PC(Electron)는 **Google Identity Services(GIS)** ID 토큰, 앱(안드로이드)은 **네이티브 플러그인 `@capawesome/capacitor-google-sign-in`**(Credential Manager). 모두 ID 토큰을 백엔드 `POST /api/auth/google` 로 전달 → **tokeninfo 로 검증(aud=웹 클라이언트 ID)** 후 세션 발급.
|
||||
- **PC(Electron) 팝업 수정**: `accounts.google.com` 팝업을 외부 브라우저로 보내 결과가 안 돌아오던 문제 → **앱 내부 자식 창**으로 열어 opener 로 credential 전달.
|
||||
- **같은 이메일 자동 연결**: 구글 로그인 시 `member.google_id`(신규 컬럼)로 조회, 없으면 **검증된 동일 이메일의 기존 계정에 연결**(provider 유지 → 아이디 로그인/프로필 편집 보존), 그래도 없으면 신규 생성.
|
||||
- 설정 필요: Google Cloud **웹 + Android OAuth 클라이언트**(패키지 `kr.sblog.slimbudget` + 서명 SHA-1), 서버 `.env` `GOOGLE_CLIENT_ID`.
|
||||
|
||||
## 35. 구글 전용 로그인 (아이디 로그인/회원가입 숨김)
|
||||
- `src/config/features.js` `ID_LOGIN_ENABLED=false` 플래그로 **아이디/비번 폼·회원가입 UI 숨김**(로그인 모달·홈 랜딩). 백엔드 `/auth/login`·`/signup` 은 비상용으로 유지(복구 가능).
|
||||
|
||||
## 36. 브랜딩 — 돈돼지 가계부
|
||||
- 앱/파비콘/PC 아이콘을 **돈돼지(투명 배경)** 로 교체, 헤더 로고·런처 라벨 정리. 런처 라벨 `돈돼지가계부`(한 줄), 앱 표시명 `돈돼지 가계부`.
|
||||
|
||||
## 37. PC 설치파일 (Electron)
|
||||
- 산출물명 `SlimBudget-Setup.exe` → **`DonDwaeji-Setup.exe`**, 다운로드 URL `/download/DonDwaeji-Setup.exe`.
|
||||
- **사용자 폴더 설치**(`%LOCALAPPDATA%\Programs\DonDwaeji`, `build/installer.nsh`)로 변경 → **관리자 권한(UAC) 프롬프트 제거**. (게시자 미서명 경고/SmartScreen 은 인증서 없이는 잔존)
|
||||
|
||||
## 38. 게시판 공지 기능 (관리자)
|
||||
- `post.notice` 컬럼(멱등 ALTER), 목록 **정렬 `notice DESC, id DESC`(공지 최상단)**, 목록·상세 **공지 배지**.
|
||||
- 공지 설정은 **글 작성/수정 시 체크박스**로 일원화(커뮤니티+관리자). 상세의 공지 토글 버튼 제거, **목록 이동 아이콘** 추가.
|
||||
|
||||
## 39. 라이트/다크 테마
|
||||
- 설정 화면에 **시스템 / 라이트 / 다크** 선택. `base.css` 를 **`data-theme` 기반 수동 다크 + 시스템 폴백**으로 전환, `theme.js` 유틸 + ui 스토어 상태, `main.js` 시작 시 즉시 적용(깜빡임 방지).
|
||||
|
||||
## 40. 사이드바/헤더 개편 · 레이아웃 정리
|
||||
- 사이드바 상단에 **돼지 로고 + '돈돼지 가계부'**(메뉴 텍스트 대체), 사이드바 **메뉴 아이콘**(lucide) 추가. **헤더는 현재 화면 타이틀** 표시(돼지 로고 제거).
|
||||
- **PC 레이아웃**: 그리드 `'left top'` → 사이드바가 **좌측 상단(헤더 행)까지** 차지, 헤더는 우측 콘텐츠 영역만.
|
||||
- **화면 내 페이지 타이틀(`<h1>`) 제거 → 헤더로 일원화**, **화면 내 뒤로가기 버튼 제거**(하단 내비 뒤로가기만 유지).
|
||||
- **전 화면 본문 폭 통일**: `--page-max`(760px) + `.layout-body > *` 전역 규칙(뷰별 제각각 max-width 대체).
|
||||
|
||||
## 41. 화면별 액션 아이콘 위치 정리
|
||||
- 가계부/예산 **month-nav**: 좌 spacer + 우 액션으로 **월 변경은 가운데, 추가·검색/필터는 우측**. 고정지출: 안내문구 아래. 커뮤니티 글쓰기: 태그 필터와 같은 줄. 분류: 지출/수입 탭과 같은 줄.
|
||||
- **커뮤니티 검색 모달**: input `min-width:0` 으로 **검색 버튼이 모달 밖으로 밀리던 문제** 수정.
|
||||
|
||||
## 42. 자주 쓰는 내역 UX 정리
|
||||
- 등록 모달의 **'자주 쓰는 내역 불러오기' 버튼·피커 모달 제거**(번거로운 3단계 해소, 저장 기능은 유지). 가계부 quick-bar 에 **'⭐ 자주 쓰는 내역' 라벨** 추가.
|
||||
- 빠른등록(`useQuick`) **등록 성공과 목록 갱신 분리** — 갱신 오류가 '등록 실패'로 오인되던 케이스 방지.
|
||||
|
||||
## 43. 계정정보 단순화 · 구글 계정 이름 변경
|
||||
- 계정정보에서 **아이디·가입유형 행 제거**(이름/이메일/권한만). **소셜(구글) 계정도 비밀번호 재인증 없이 이름만 변경** 가능(ProfileEdit), 백엔드 `updateProfile` 그대로.
|
||||
|
||||
## 44. PC 화면 해상도 변경
|
||||
- Electron **preload(`window.desktop`) + IPC(`window:setSize`)** 추가, 설정 화면(PC)에 **창 크기 프리셋**(1024×720 ~ 1920×1080). 선택 크기는 Electron 창 상태 저장으로 다음 실행에도 유지.
|
||||
|
||||
## 45. 기타
|
||||
- 통계 '분류별 지출/수입'을 **대분류 기준 집계**(소분류 펼침 제거로 스크롤 단축).
|
||||
- 게시판 작성일: 1시간 이내 'N분 전' / 1일 이내 'N시간 전' / 그 이상 **`YYYY.MM.DD`**.
|
||||
- 웹 안내 페이지 한국어 줄바꿈 **절(어절) 단위**(`word-break: keep-all` + inline-block 묶음).
|
||||
|
||||
---
|
||||
|
||||
## 운영 반영 시 체크
|
||||
- 서버 `/opt/sb-backend/.env`
|
||||
- `ACCOUNT_CRYPTO_KEY` — 계좌번호 암호화를 켤 때만(미설정 시 평문, 깨지지 않음).
|
||||
- `GOOGLE_CLIENT_ID` — 구글 로그인 웹 클라이언트 ID(미설정 시 구글 버튼 미노출). **앱 구글 로그인**은 Google Cloud 에 **Android OAuth 클라이언트**(패키지 `kr.sblog.slimbudget` + 서명 SHA-1) 등록 필요.
|
||||
- `member.google_id`(계정 연결) · `post.notice`(공지) 컬럼 — 기동 시 **멱등 ALTER** 자동 적용.
|
||||
- `app_setting` · `auth_session` · `board_image` 테이블 — 배포 시 `member.sql`/`board.sql` 초기화로 자동 생성(가입 허용 / 세션 DB 백업 / 게시글 이미지).
|
||||
- Redis `appendonly yes` — 세션 영속(이미 적용). 단, 세션 DB 이중 저장으로 Redis 유실에도 로그인 유지.
|
||||
- 서버 `.env`/환경변수 `PUBLIC_URL`(기본 `https://app.sblog.kr`) — 업로드 이미지 절대 URL 베이스(앱에서 이미지 로드에 필요).
|
||||
- **nginx**(`deploy/nginx-sb-front.conf` 참고): `location /` 에 `Cache-Control no-cache`(진입 HTML 최신화), `location /download/` → `/var/www/sb-downloads`(설치파일, 배포에 안 지워지는 영속 경로).
|
||||
- 앱 버전 표기는 `package.json` 의 `version`(현재 `1.0.0`)을 단일 출처로 사용.
|
||||
- **PC 설치파일**: `npm run electron:build` → `release-desktop/SlimBudget-Setup.exe`(서명 없음, SmartScreen 경고 정상).
|
||||
- **PC 설치파일**: `npm run electron:build` → `release-desktop/DonDwaeji-Setup.exe`(**사용자 폴더 설치=UAC 없음**, 미서명 → SmartScreen 경고 정상). 서버는 `/var/www/sb-downloads/DonDwaeji-Setup.exe` 로 업로드.
|
||||
- 배포는 `main` 푸시 → CI·Deploy 워크플로 모두 **테스트 통과해야 진행**(Gitea Actions).
|
||||
|
||||
@@ -1,12 +1,43 @@
|
||||
// Slim Budget 데스크톱(Electron) 메인 프로세스.
|
||||
// 돈돼지 가계부 데스크톱(Electron) 메인 프로세스.
|
||||
// 라이브 사이트(https://app.sblog.kr)를 직접 로드한다 → 프론트 변경이 자동 반영(재설치 불필요),
|
||||
// 같은 출처라 CORS 우회도 불필요. 배포된 웹 게이트가 Electron 을 감지해 안내페이지 대신 전체 앱을 띄운다.
|
||||
const { app, BrowserWindow, shell, session } = require('electron')
|
||||
//
|
||||
// 로딩 UX: 예전에는 시작 시 HTTP 캐시를 통째로 비워 매번 전체 자산을 재다운로드 → 흰 화면이 오래 노출됐다.
|
||||
// 이제 index.html 만 캐시버스트(?_=t)로 항상 최신을 받고, 해시된 정적 자산(JS/CSS)은 캐시를 유지해
|
||||
// 재시작이 빠르다. 로드되는 동안에는 스플래시(로딩) 창을 띄워 빈 화면을 가린다.
|
||||
// 또한 단일 인스턴스 잠금으로 중복 실행을 막아(두 번째 실행 시 사이트를 다시 로드하며 빈 화면이 보이던 문제),
|
||||
// 이미 실행 중이면 기존 창을 활성화한다.
|
||||
const { app, BrowserWindow, shell, 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('돈돼지 가계부')
|
||||
|
||||
const APP_URL = 'https://app.sblog.kr'
|
||||
|
||||
// 현재 메인 창 참조(중복 실행 시 활성화 대상).
|
||||
let mainWindow = null
|
||||
|
||||
// 중복 실행 방지: 잠금을 못 얻으면 이미 실행 중 → 새 인스턴스를 즉시 종료한다.
|
||||
// (두 번째 실행에서 라이브 사이트를 다시 로드하느라 빈 화면이 보이던 현상 해결)
|
||||
if (!app.requestSingleInstanceLock()) {
|
||||
app.quit()
|
||||
} else {
|
||||
// 두 번째 실행 시도가 들어오면 기존 창을 복원/활성화.
|
||||
app.on('second-instance', () => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
if (mainWindow.isMinimized()) mainWindow.restore()
|
||||
if (!mainWindow.isVisible()) mainWindow.show()
|
||||
mainWindow.focus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 창 크기/위치를 userData 에 저장해 다음 실행 때 복원 (기본 해상도 고정 대신 사용자가 맞춘 크기 유지)
|
||||
function stateFile() {
|
||||
return path.join(app.getPath('userData'), 'window-state.json')
|
||||
@@ -28,9 +59,26 @@ function saveState(win) {
|
||||
}
|
||||
}
|
||||
|
||||
// 스플래시(로딩) 화면 — 라이브 사이트가 첫 렌더될 때까지 빈 흰 화면 대신 표시.
|
||||
function splashHtml() {
|
||||
const html = `<!doctype html><html><head><meta charset="utf-8"><title>돈돼지 가계부</title>
|
||||
<style>html,body{height:100%;margin:0;display:flex;align-items:center;justify-content:center;
|
||||
background:#1a1a1a;color:#eee;font-family:system-ui,'Segoe UI',sans-serif;-webkit-user-select:none;user-select:none}
|
||||
.box{text-align:center}
|
||||
.pig{font-size:46px;line-height:1}
|
||||
.name{margin-top:10px;font-size:17px;font-weight:700;letter-spacing:-0.01em}
|
||||
.spin{margin:18px auto 0;width:26px;height:26px;border:3px solid rgba(255,255,255,.2);
|
||||
border-top-color:#00bc7e;border-radius:50%;animation:r .8s linear infinite}
|
||||
@keyframes r{to{transform:rotate(360deg)}}
|
||||
.tip{margin-top:12px;font-size:12px;opacity:.5}</style></head>
|
||||
<body><div class="box"><div class="pig">🐷</div><div class="name">돈돼지 가계부</div>
|
||||
<div class="spin"></div><div class="tip">불러오는 중…</div></div></body></html>`
|
||||
return 'data:text/html;charset=utf-8,' + encodeURIComponent(html)
|
||||
}
|
||||
|
||||
// 오프라인/로드 실패 시 안내 화면 (다시 시도 버튼)
|
||||
function offlineHtml() {
|
||||
const html = `<!doctype html><html><head><meta charset="utf-8"><title>Slim Budget</title>
|
||||
const html = `<!doctype html><html><head><meta charset="utf-8"><title>돈돼지 가계부</title>
|
||||
<style>html,body{height:100%;margin:0;display:flex;align-items:center;justify-content:center;
|
||||
background:#1a1a1a;color:#eee;font-family:system-ui,'Segoe UI',sans-serif}
|
||||
.box{text-align:center}h2{margin:0 0 8px}p{opacity:.7;margin:0}
|
||||
@@ -50,31 +98,70 @@ function createWindow() {
|
||||
y: s?.y,
|
||||
minWidth: 360,
|
||||
minHeight: 560,
|
||||
show: false, // 첫 렌더(ready-to-show) 전까지 숨김 → 빈 흰 화면 노출 방지(스플래시가 대신 보임)
|
||||
backgroundColor: '#1a1a1a',
|
||||
title: 'Slim Budget',
|
||||
title: '돈돼지 가계부',
|
||||
webPreferences: {
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
preload: path.join(__dirname, 'preload.cjs'),
|
||||
// 한글 IME 첫 글자 중복 입력 방지 — Chromium 맞춤법 검사기가 조합을 방해하는 Electron 알려진 버그 회피
|
||||
spellcheck: false,
|
||||
},
|
||||
})
|
||||
if (s?.maximized) win.maximize()
|
||||
mainWindow = win
|
||||
|
||||
// 로딩 스플래시(프레임 없는 작은 창) — 메인이 준비되면 닫는다.
|
||||
const splash = new BrowserWindow({
|
||||
width: 300,
|
||||
height: 300,
|
||||
frame: false,
|
||||
resizable: false,
|
||||
movable: false,
|
||||
skipTaskbar: true,
|
||||
alwaysOnTop: true,
|
||||
backgroundColor: '#1a1a1a',
|
||||
title: '돈돼지 가계부',
|
||||
})
|
||||
splash.loadURL(splashHtml())
|
||||
splash.center()
|
||||
|
||||
// 메인 창을 한 번만 노출하고 스플래시를 정리(이벤트 중복/지연 대비).
|
||||
let revealed = false
|
||||
const reveal = () => {
|
||||
if (revealed) return
|
||||
revealed = true
|
||||
if (!win.isDestroyed()) win.show()
|
||||
if (!splash.isDestroyed()) splash.destroy()
|
||||
}
|
||||
win.once('ready-to-show', reveal)
|
||||
// 안전장치: 로드가 비정상적으로 지연돼도 12초 후엔 메인 창을 띄운다.
|
||||
setTimeout(reveal, 12000)
|
||||
|
||||
// 기본 메뉴바 제거(앱 자체 내비게이션 사용)
|
||||
win.removeMenu()
|
||||
win.loadURL(APP_URL)
|
||||
// index.html 만 캐시버스트(?_=t) → 항상 최신 프론트. 해시 자산은 캐시 유지 → 빠른 재시작.
|
||||
win.loadURL(APP_URL + '/?_=' + Date.now())
|
||||
|
||||
// 닫을 때 창 크기/위치 저장 → 다음 실행에 복원
|
||||
win.on('close', () => saveState(win))
|
||||
|
||||
// 메인 프레임 로드 실패(오프라인 등) 시 안내 화면. -3(aborted, 리다이렉트 등)은 무시.
|
||||
win.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL, isMainFrame) => {
|
||||
if (isMainFrame && errorCode !== -3) win.loadURL(offlineHtml())
|
||||
if (isMainFrame && errorCode !== -3) {
|
||||
win.loadURL(offlineHtml())
|
||||
reveal() // 오프라인 안내도 보이도록 메인 창 노출
|
||||
}
|
||||
})
|
||||
|
||||
// 외부(앱 외 도메인) 링크는 기본 브라우저로, 앱 내 링크는 창에서 처리
|
||||
win.webContents.setWindowOpenHandler(({ url }) => {
|
||||
// 구글 로그인(GIS) 팝업은 앱 내부 자식 창으로 열어야 opener 로 로그인 결과(postMessage)가 돌아온다.
|
||||
// 외부 브라우저로 보내면 계정 선택 후 credential 이 앱으로 전달되지 않아 로그인이 끊긴다.
|
||||
if (/^https:\/\/accounts\.google\.com\//i.test(url)) {
|
||||
return { action: 'allow' }
|
||||
}
|
||||
if (/^https?:\/\//i.test(url) && !url.startsWith(APP_URL)) {
|
||||
shell.openExternal(url)
|
||||
return { action: 'deny' }
|
||||
@@ -83,14 +170,22 @@ function createWindow() {
|
||||
})
|
||||
}
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
// 시작 시 HTTP 캐시 비움 → 라이브 사이트의 최신 프론트를 항상 로드(옛 HTML 캐시로 인한 미반영 방지).
|
||||
// localStorage(로그인 세션)는 캐시가 아니라 영향 없음.
|
||||
try {
|
||||
await session.defaultSession.clearCache()
|
||||
} catch {
|
||||
/* 캐시 클리어 실패 무시 */
|
||||
}
|
||||
// 데스크톱 창 크기(해상도) 변경 — 렌더러(설정 화면)에서 호출
|
||||
ipcMain.handle('window:setSize', (e, w, h) => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
if (!win) return false
|
||||
if (win.isMaximized()) win.unmaximize()
|
||||
win.setSize(Math.max(360, Math.round(w)), Math.max(560, Math.round(h)))
|
||||
win.center()
|
||||
return true
|
||||
})
|
||||
ipcMain.handle('window:getSize', (e) => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
return win ? win.getSize() : [0, 0]
|
||||
})
|
||||
|
||||
app.whenReady().then(() => {
|
||||
// 캐시는 비우지 않는다(빠른 재시작). 최신 프론트는 index.html 캐시버스트로 보장.
|
||||
createWindow()
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// 렌더러(웹)에서 안전하게 호출할 데스크톱 전용 API 노출 (contextIsolation 유지).
|
||||
const { contextBridge, ipcRenderer } = require('electron')
|
||||
|
||||
contextBridge.exposeInMainWorld('desktop', {
|
||||
// 데스크톱 창 크기(해상도) 변경
|
||||
setWindowSize: (w, h) => ipcRenderer.invoke('window:setSize', w, h),
|
||||
getWindowSize: () => ipcRenderer.invoke('window:getSize'),
|
||||
})
|
||||
@@ -2,11 +2,10 @@
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" type="image/svg+xml" href="/logo.svg">
|
||||
<link rel="alternate icon" href="/favicon.ico">
|
||||
<link rel="apple-touch-icon" href="/logo.svg">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<title>Slim Budget</title>
|
||||
<link rel="icon" type="image/png" href="/favicon.png">
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||
<title>돈돼지 가계부</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
App/build
|
||||
App/Pods
|
||||
App/output
|
||||
App/App/public
|
||||
DerivedData
|
||||
xcuserdata
|
||||
|
||||
# Cordova plugins for Capacitor
|
||||
capacitor-cordova-ios-plugins
|
||||
|
||||
# Generated Config files
|
||||
App/App/capacitor.config.json
|
||||
App/App/config.xml
|
||||
@@ -0,0 +1,393 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 60;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
|
||||
4D22ABE92AF431CB00220026 /* CapApp-SPM in Frameworks */ = {isa = PBXBuildFile; productRef = 4D22ABE82AF431CB00220026 /* CapApp-SPM */; };
|
||||
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
|
||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
|
||||
504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };
|
||||
504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
|
||||
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
|
||||
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
|
||||
327E84D03001463F00AB0467 /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = "<group>"; };
|
||||
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
||||
504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
||||
958DCC722DB07C7200EA8C5F /* debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = debug.xcconfig; path = ../debug.xcconfig; sourceTree = SOURCE_ROOT; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
504EC3011FED79650016851F /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
4D22ABE92AF431CB00220026 /* CapApp-SPM in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
504EC2FB1FED79650016851F = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
958DCC722DB07C7200EA8C5F /* debug.xcconfig */,
|
||||
504EC3061FED79650016851F /* App */,
|
||||
504EC3051FED79650016851F /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC3051FED79650016851F /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
504EC3041FED79650016851F /* App.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC3061FED79650016851F /* App */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
327E84D03001463F00AB0467 /* App.entitlements */,
|
||||
50379B222058CBB4000EE86E /* capacitor.config.json */,
|
||||
504EC3071FED79650016851F /* AppDelegate.swift */,
|
||||
504EC30B1FED79650016851F /* Main.storyboard */,
|
||||
504EC30E1FED79650016851F /* Assets.xcassets */,
|
||||
504EC3101FED79650016851F /* LaunchScreen.storyboard */,
|
||||
504EC3131FED79650016851F /* Info.plist */,
|
||||
2FAD9762203C412B000D30F8 /* config.xml */,
|
||||
50B271D01FEDC1A000F3C39B /* public */,
|
||||
);
|
||||
path = App;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
504EC3031FED79650016851F /* App */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */;
|
||||
buildPhases = (
|
||||
504EC3001FED79650016851F /* Sources */,
|
||||
504EC3011FED79650016851F /* Frameworks */,
|
||||
504EC3021FED79650016851F /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = App;
|
||||
packageProductDependencies = (
|
||||
4D22ABE82AF431CB00220026 /* CapApp-SPM */,
|
||||
);
|
||||
productName = App;
|
||||
productReference = 504EC3041FED79650016851F /* App.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
504EC2FC1FED79650016851F /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastSwiftUpdateCheck = 0920;
|
||||
LastUpgradeCheck = 2660;
|
||||
TargetAttributes = {
|
||||
504EC3031FED79650016851F = {
|
||||
CreatedOnToolsVersion = 9.2;
|
||||
LastSwiftMigration = 1100;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */;
|
||||
compatibilityVersion = "Xcode 8.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 504EC2FB1FED79650016851F;
|
||||
packageReferences = (
|
||||
D4C12C0A2AAA248700AAC8A2 /* XCLocalSwiftPackageReference "CapApp-SPM" */,
|
||||
);
|
||||
productRefGroup = 504EC3051FED79650016851F /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
504EC3031FED79650016851F /* App */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
504EC3021FED79650016851F /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */,
|
||||
50B271D11FEDC1A000F3C39B /* public in Resources */,
|
||||
504EC30F1FED79650016851F /* Assets.xcassets in Resources */,
|
||||
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */,
|
||||
504EC30D1FED79650016851F /* Main.storyboard in Resources */,
|
||||
2FAD9763203C412B000D30F8 /* config.xml in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
504EC3001FED79650016851F /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
504EC30B1FED79650016851F /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
504EC30C1FED79650016851F /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC3101FED79650016851F /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
504EC3111FED79650016851F /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
504EC3141FED79650016851F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 958DCC722DB07C7200EA8C5F /* debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = G9PDM2C4L6;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
504EC3151FED79650016851F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = G9PDM2C4L6;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
504EC3171FED79650016851F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 958DCC722DB07C7200EA8C5F /* debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = kr.sblog.slimbudget.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
504EC3181FED79650016851F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = kr.sblog.slimbudget.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
504EC3141FED79650016851F /* Debug */,
|
||||
504EC3151FED79650016851F /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
504EC3171FED79650016851F /* Debug */,
|
||||
504EC3181FED79650016851F /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCLocalSwiftPackageReference section */
|
||||
D4C12C0A2AAA248700AAC8A2 /* XCLocalSwiftPackageReference "CapApp-SPM" */ = {
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = "CapApp-SPM";
|
||||
};
|
||||
/* End XCLocalSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
4D22ABE82AF431CB00220026 /* CapApp-SPM */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D4C12C0A2AAA248700AAC8A2 /* XCLocalSwiftPackageReference "CapApp-SPM" */;
|
||||
productName = "CapApp-SPM";
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 504EC2FC1FED79650016851F /* Project object */;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"originHash" : "275f5197fc45ecbed402538938c9f019518bacb33270bc32cf5549af9b2891e6",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "app-check",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/app-check.git",
|
||||
"state" : {
|
||||
"revision" : "bb4002485ff867768dec13bf904a2ddb050bd1b1",
|
||||
"version" : "11.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "appauth-ios",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/openid/AppAuth-iOS.git",
|
||||
"state" : {
|
||||
"revision" : "2781038865a80e2c425a1da12cc1327bcd56501f",
|
||||
"version" : "1.7.6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "capacitor-swift-pm",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/ionic-team/capacitor-swift-pm.git",
|
||||
"state" : {
|
||||
"revision" : "2231987d85b8b0b289320b1d0947b4ae8345cde4",
|
||||
"version" : "8.4.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "googlesignin-ios",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/GoogleSignIn-iOS",
|
||||
"state" : {
|
||||
"revision" : "65fb3f1aa6ffbfdc79c4e22178a55cd91561f5e9",
|
||||
"version" : "8.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "googleutilities",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/GoogleUtilities.git",
|
||||
"state" : {
|
||||
"revision" : "9f183ae842be978784f2963a343682e0c46d8fb3",
|
||||
"version" : "8.1.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "gtm-session-fetcher",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/gtm-session-fetcher.git",
|
||||
"state" : {
|
||||
"revision" : "a2ab612cb980066ee56d90d60d8462992c07f24b",
|
||||
"version" : "3.5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "gtmappauth",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/GTMAppAuth.git",
|
||||
"state" : {
|
||||
"revision" : "5d7d66f647400952b1758b230e019b07c0b4b22a",
|
||||
"version" : "4.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "interop-ios-for-google-sdks",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/interop-ios-for-google-sdks.git",
|
||||
"state" : {
|
||||
"revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe",
|
||||
"version" : "101.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "ion-ios-camera",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/ionic-team/ion-ios-camera.git",
|
||||
"state" : {
|
||||
"revision" : "2cd4f06b0ece21b6458e23357280c51f94bfceb8",
|
||||
"version" : "1.0.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "ion-ios-filesystem",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/ionic-team/ion-ios-filesystem.git",
|
||||
"state" : {
|
||||
"revision" : "0d81e26e828ff9582807e2339112cedf2e0fab85",
|
||||
"version" : "1.1.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "promises",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/promises.git",
|
||||
"state" : {
|
||||
"revision" : "f4a19a3c313dc2616c70bb49d29a799fb16be837",
|
||||
"version" : "2.4.1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.applesignin</key>
|
||||
<array>
|
||||
<string>Default</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,49 @@
|
||||
import UIKit
|
||||
import Capacitor
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationWillResignActive(_ application: UIApplication) {
|
||||
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
|
||||
}
|
||||
|
||||
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
|
||||
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
|
||||
}
|
||||
|
||||
func applicationWillEnterForeground(_ application: UIApplication) {
|
||||
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||
}
|
||||
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
||||
// Called when the app was launched with a url. Feel free to add additional processing here,
|
||||
// but if you want the App API to support tracking app url opens, make sure to keep this call
|
||||
return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
|
||||
// Called when the app was launched with an activity, including Universal Links.
|
||||
// Feel free to add additional processing here, but if you want the App API to support
|
||||
// tracking app url opens, make sure to keep this call
|
||||
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
||||
}
|
||||
|
||||
}
|
||||
|
After Width: | Height: | Size: 507 KiB |
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"idiom": "universal",
|
||||
"size": "1024x1024",
|
||||
"filename": "AppIcon-512@2x.png",
|
||||
"platform": "ios"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "splash-2732x2732-2.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "splash-2732x2732-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "splash-2732x2732.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 40 KiB |
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17132" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17105"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<imageView key="view" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Splash" id="snD-IY-ifK">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
</imageView>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="Splash" width="1366" height="1366"/>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14111" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Bridge View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="CAPBridgeViewController" customModule="Capacitor" sceneMemberID="viewController"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -0,0 +1,70 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>CAPACITOR_DEBUG</key>
|
||||
<string>$(CAPACITOR_DEBUG)</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>영수증·프로필·게시판 사진을 촬영할 때 카메라를 사용합니다.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>영수증·프로필·게시판 사진을 첨부할 때 사진 보관함에서 이미지를 불러옵니다.</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>촬영한 사진을 사진 보관함에 저장할 때 사용합니다.</string>
|
||||
<key>GIDClientID</key>
|
||||
<string>127753308411-9617kn17bjfjmenbscfgvobjgc0l8vne.apps.googleusercontent.com</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>com.googleusercontent.apps.127753308411-9617kn17bjfjmenbscfgvobjgc0l8vne</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>돈돼지 가계부</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,9 @@
|
||||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/config/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
||||
@@ -0,0 +1,41 @@
|
||||
// swift-tools-version: 5.9
|
||||
import PackageDescription
|
||||
|
||||
// DO NOT MODIFY THIS FILE - managed by Capacitor CLI commands
|
||||
let package = Package(
|
||||
name: "CapApp-SPM",
|
||||
platforms: [.iOS(.v15)],
|
||||
products: [
|
||||
.library(
|
||||
name: "CapApp-SPM",
|
||||
targets: ["CapApp-SPM"])
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", exact: "8.4.1"),
|
||||
.package(name: "CapacitorCommunityAppleSignIn", path: "../../../node_modules/@capacitor-community/apple-sign-in"),
|
||||
.package(name: "CapacitorApp", path: "../../../node_modules/@capacitor/app"),
|
||||
.package(name: "CapacitorCamera", path: "../../../node_modules/@capacitor/camera"),
|
||||
.package(name: "CapacitorFilesystem", path: "../../../node_modules/@capacitor/filesystem"),
|
||||
.package(name: "CapacitorLocalNotifications", path: "../../../node_modules/@capacitor/local-notifications"),
|
||||
.package(name: "CapacitorPreferences", path: "../../../node_modules/@capacitor/preferences"),
|
||||
.package(name: "CapacitorShare", path: "../../../node_modules/@capacitor/share"),
|
||||
.package(name: "CapawesomeCapacitorGoogleSignIn", path: "../../../node_modules/@capawesome/capacitor-google-sign-in")
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "CapApp-SPM",
|
||||
dependencies: [
|
||||
.product(name: "Capacitor", package: "capacitor-swift-pm"),
|
||||
.product(name: "Cordova", package: "capacitor-swift-pm"),
|
||||
.product(name: "CapacitorCommunityAppleSignIn", package: "CapacitorCommunityAppleSignIn"),
|
||||
.product(name: "CapacitorApp", package: "CapacitorApp"),
|
||||
.product(name: "CapacitorCamera", package: "CapacitorCamera"),
|
||||
.product(name: "CapacitorFilesystem", package: "CapacitorFilesystem"),
|
||||
.product(name: "CapacitorLocalNotifications", package: "CapacitorLocalNotifications"),
|
||||
.product(name: "CapacitorPreferences", package: "CapacitorPreferences"),
|
||||
.product(name: "CapacitorShare", package: "CapacitorShare"),
|
||||
.product(name: "CapawesomeCapacitorGoogleSignIn", package: "CapawesomeCapacitorGoogleSignIn")
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
# CapApp-SPM
|
||||
|
||||
This package is used to host SPM dependencies for your Capacitor project
|
||||
|
||||
Do not modify the contents of it or there may be unintended consequences.
|
||||
@@ -0,0 +1 @@
|
||||
public let isCapacitorApp = true
|
||||
@@ -0,0 +1 @@
|
||||
CAPACITOR_DEBUG = true
|
||||
@@ -8,12 +8,18 @@
|
||||
"name": "sb_pt",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@capacitor-community/apple-sign-in": "^7.1.0",
|
||||
"@capacitor/android": "^8.3.4",
|
||||
"@capacitor/app": "^8.1.0",
|
||||
"@capacitor/camera": "^8.2.0",
|
||||
"@capacitor/cli": "^8.3.4",
|
||||
"@capacitor/core": "^8.3.4",
|
||||
"@capacitor/filesystem": "^8.1.2",
|
||||
"@capacitor/ios": "^8.4.1",
|
||||
"@capacitor/local-notifications": "^8.2.0",
|
||||
"@capacitor/preferences": "^8.0.1",
|
||||
"@capacitor/share": "^8.0.1",
|
||||
"@capawesome/capacitor-google-sign-in": "^0.1.2",
|
||||
"@toast-ui/editor": "^3.2.2",
|
||||
"@toast-ui/editor-plugin-code-syntax-highlight": "^3.1.0",
|
||||
"axios": "^1.16.1",
|
||||
@@ -21,7 +27,8 @@
|
||||
"prismjs": "^1.30.0",
|
||||
"sortablejs": "^1.15.7",
|
||||
"vue": "^3.5.32",
|
||||
"vue-router": "^5.0.4"
|
||||
"vue-router": "^5.0.4",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/assets": "^3.0.5",
|
||||
@@ -38,6 +45,7 @@
|
||||
"jsdom": "^29.1.1",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"oxlint": "~1.60.0",
|
||||
"patch-package": "^8.0.1",
|
||||
"vite": "^8.0.8",
|
||||
"vite-plugin-vue-devtools": "^8.1.1",
|
||||
"vitest": "^4.1.8"
|
||||
@@ -595,6 +603,19 @@
|
||||
"specificity": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor-community/apple-sign-in": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor-community/apple-sign-in/-/apple-sign-in-7.1.0.tgz",
|
||||
"integrity": "sha512-df7cA7vfvvvilTtpJKeVvxO31W+py5t3HK9233GBD5MvgAgtGvCqeQ3pI0noNkVsKPF9B1g/po8Mph6s4bVSmw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/scriptjs": "0.0.2",
|
||||
"scriptjs": "^2.5.9"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/android": {
|
||||
"version": "8.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/android/-/android-8.3.4.tgz",
|
||||
@@ -1178,14 +1199,44 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/core": {
|
||||
"version": "8.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.3.4.tgz",
|
||||
"integrity": "sha512-CqRQCkb6HXxcx/N7s+hHTN6ef2CmamFiRMITwm4qB840ph56mS42bzUgn6tKCP+RZjdDweiRHj9ytDDeN6jFag==",
|
||||
"version": "8.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.4.1.tgz",
|
||||
"integrity": "sha512-xqhOGLbTAYeOWK+IDUNSjQJAPapQjRHrIcgk9PYp52or9zFTaaMko31uNi16N6W+CRJ8VrRram6fOYILkBG2Hg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/filesystem": {
|
||||
"version": "8.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/filesystem/-/filesystem-8.1.2.tgz",
|
||||
"integrity": "sha512-doaaMfGoFR2hWU6aV6u83I+5ZsGyJVq+Gz4r9lMpJzUKMm1eMu0hLnFdV1aXZlU9FlK/RndFrVD8oRZfNOqWgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@capacitor/synapse": "^1.0.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/ios": {
|
||||
"version": "8.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-8.4.1.tgz",
|
||||
"integrity": "sha512-EgcAk7NYheHMTyP3CUrA65qKs4D2UEAKgw44HI6Uk3dTw6KjLQkdLOQWvbeRncHaZ2gklfOojUoc5DlSw2lhYg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": "^8.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/local-notifications": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/local-notifications/-/local-notifications-8.2.0.tgz",
|
||||
"integrity": "sha512-fvLY0w2w4MiX+DD4+Wv4DOwOLdzKZsMDwAcRv/Juudd+QbKbn69s6cM3xVqPwAiDqfnqsY4/S8xtQD6M73wY2A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/preferences": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/preferences/-/preferences-8.0.1.tgz",
|
||||
@@ -1195,6 +1246,40 @@
|
||||
"@capacitor/core": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/share": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/share/-/share-8.0.1.tgz",
|
||||
"integrity": "sha512-3cSBKBCJVon54rKDROP2rqGyeGks4pBh9TbaEk9S375Kbek/ZHe72N50zIa0Vn9Eac/SuhwgehO/mmA4CsUOiw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/synapse": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/synapse/-/synapse-1.0.4.tgz",
|
||||
"integrity": "sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@capawesome/capacitor-google-sign-in": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@capawesome/capacitor-google-sign-in/-/capacitor-google-sign-in-0.1.2.tgz",
|
||||
"integrity": "sha512-41Z5LBVZJQ/20lEbzQJUtjfm7rCUhiq0EsfKmytTfKjqUjQB4zy0zlDMowe/z5CtetL8u7WGvERtyRYil2exww==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/capawesome-team/"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/capawesome"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
@@ -3492,6 +3577,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/scriptjs": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/scriptjs/-/scriptjs-0.0.2.tgz",
|
||||
"integrity": "sha512-GFrUgzGYfNX3VWZwMyzr0JrBwzLv5Kes4BQBvo6hXdRy14/GBbpCiVwwB7v1o2xIEvFf+9GyznmYhuesQQjSag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/slice-ansi": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/slice-ansi/-/slice-ansi-4.0.0.tgz",
|
||||
@@ -3943,6 +4034,13 @@
|
||||
"node": ">=14.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@yarnpkg/lockfile": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
|
||||
"integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/abbrev": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
|
||||
@@ -3995,6 +4093,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/adler-32": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
|
||||
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
@@ -4709,6 +4816,19 @@
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.28.2",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
|
||||
@@ -5100,6 +5220,25 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
|
||||
"integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"get-intrinsic": "^1.3.0",
|
||||
"set-function-length": "^1.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
@@ -5113,6 +5252,23 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bound": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"get-intrinsic": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||
@@ -5162,6 +5318,19 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/cfb": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
|
||||
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"adler-32": "~1.3.0",
|
||||
"crc-32": "~1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
|
||||
@@ -5358,6 +5527,15 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/codepage": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
||||
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/color": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||
@@ -5762,6 +5940,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/crc-32": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"crc32": "bin/crc32.njs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
@@ -6071,7 +6261,6 @@
|
||||
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.0",
|
||||
"es-errors": "^1.3.0",
|
||||
@@ -7355,6 +7544,19 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/find-up": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
||||
@@ -7372,6 +7574,16 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/find-yarn-workspace-root": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz",
|
||||
"integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"micromatch": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/flat-cache": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
|
||||
@@ -7459,6 +7671,15 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/frac": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
||||
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
@@ -7992,7 +8213,6 @@
|
||||
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.0"
|
||||
},
|
||||
@@ -8387,6 +8607,16 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-obj": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
|
||||
@@ -8793,6 +9023,26 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-stable-stringify": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz",
|
||||
"integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.8",
|
||||
"call-bound": "^1.0.4",
|
||||
"isarray": "^2.0.5",
|
||||
"jsonify": "^0.0.1",
|
||||
"object-keys": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/json-stable-stringify-without-jsonify": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
|
||||
@@ -8800,6 +9050,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-stable-stringify/node_modules/isarray": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
|
||||
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-stringify-safe": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
@@ -8838,6 +9095,16 @@
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonify": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz",
|
||||
"integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==",
|
||||
"dev": true,
|
||||
"license": "Public Domain",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonparse": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
|
||||
@@ -8885,6 +9152,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/klaw-sync": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz",
|
||||
"integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.1.11"
|
||||
}
|
||||
},
|
||||
"node_modules/kleur": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
|
||||
@@ -9650,6 +9927,33 @@
|
||||
"semver": "bin/semver"
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
"picomatch": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch/node_modules/picomatch": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
|
||||
@@ -10377,7 +10681,6 @@
|
||||
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
@@ -10707,6 +11010,169 @@
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/patch-package": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz",
|
||||
"integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@yarnpkg/lockfile": "^1.1.0",
|
||||
"chalk": "^4.1.2",
|
||||
"ci-info": "^3.7.0",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"find-yarn-workspace-root": "^2.0.0",
|
||||
"fs-extra": "^10.0.0",
|
||||
"json-stable-stringify": "^1.0.2",
|
||||
"klaw-sync": "^6.0.0",
|
||||
"minimist": "^1.2.6",
|
||||
"open": "^7.4.2",
|
||||
"semver": "^7.5.3",
|
||||
"slash": "^2.0.0",
|
||||
"tmp": "^0.2.4",
|
||||
"yaml": "^2.2.2"
|
||||
},
|
||||
"bin": {
|
||||
"patch-package": "index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14",
|
||||
"npm": ">5"
|
||||
}
|
||||
},
|
||||
"node_modules/patch-package/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/patch-package/node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/patch-package/node_modules/ci-info": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
|
||||
"integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/sibiraj-s"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/patch-package/node_modules/fs-extra": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/patch-package/node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/patch-package/node_modules/is-docker": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
|
||||
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"is-docker": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/patch-package/node_modules/is-wsl": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-docker": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/patch-package/node_modules/open": {
|
||||
"version": "7.4.2",
|
||||
"resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz",
|
||||
"integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-docker": "^2.0.0",
|
||||
"is-wsl": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/patch-package/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
@@ -12068,6 +12534,12 @@
|
||||
"node": ">=v12.22.7"
|
||||
}
|
||||
},
|
||||
"node_modules/scriptjs": {
|
||||
"version": "2.5.9",
|
||||
"resolved": "https://registry.npmjs.org/scriptjs/-/scriptjs-2.5.9.tgz",
|
||||
"integrity": "sha512-qGVDoreyYiP1pkQnbnFAUIS5AjenNwwQBdl7zeos9etl+hYKWahjRTfzAZZYBv5xNHx7vNKCmaLDQZ6Fr2AEXg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/scule": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz",
|
||||
@@ -12132,6 +12604,24 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"define-data-property": "^1.1.4",
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-intrinsic": "^1.2.4",
|
||||
"gopd": "^1.0.1",
|
||||
"has-property-descriptors": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.32.6",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz",
|
||||
@@ -12326,6 +12816,16 @@
|
||||
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/slash": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
|
||||
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/slice-ansi": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
|
||||
@@ -12520,6 +13020,18 @@
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/ssf": {
|
||||
"version": "0.11.2",
|
||||
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
||||
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"frac": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/ssri": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz",
|
||||
@@ -13106,6 +13618,19 @@
|
||||
"tmp": "^0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/totalist": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
||||
@@ -14155,6 +14680,24 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wmf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
||||
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/word": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
|
||||
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||
@@ -14277,6 +14820,27 @@
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xlsx": {
|
||||
"version": "0.18.5",
|
||||
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"adler-32": "~1.3.0",
|
||||
"cfb": "~1.2.1",
|
||||
"codepage": "~1.15.0",
|
||||
"crc-32": "~1.2.1",
|
||||
"ssf": "~0.11.2",
|
||||
"wmf": "~1.0.1",
|
||||
"word": "~0.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"xlsx": "bin/xlsx.njs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/xml-js": {
|
||||
"version": "1.6.11",
|
||||
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"type": "module",
|
||||
"main": "electron/main.cjs",
|
||||
"scripts": {
|
||||
"postinstall": "patch-package",
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"build:app": "vite build --mode capacitor",
|
||||
@@ -21,7 +22,7 @@
|
||||
},
|
||||
"build": {
|
||||
"appId": "kr.sblog.slimbudget",
|
||||
"productName": "Slim Budget",
|
||||
"productName": "돈돼지 가계부",
|
||||
"directories": {
|
||||
"output": "release-desktop"
|
||||
},
|
||||
@@ -30,22 +31,29 @@
|
||||
],
|
||||
"win": {
|
||||
"target": "nsis",
|
||||
"icon": "assets/icon-only.png",
|
||||
"artifactName": "SlimBudget-Setup.${ext}"
|
||||
"icon": "build/icon.ico",
|
||||
"artifactName": "DonDwaeji-Setup.${ext}"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"perMachine": false,
|
||||
"allowToChangeInstallationDirectory": true
|
||||
"allowToChangeInstallationDirectory": true,
|
||||
"include": "build/installer.nsh"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@capacitor-community/apple-sign-in": "^7.1.0",
|
||||
"@capacitor/android": "^8.3.4",
|
||||
"@capacitor/app": "^8.1.0",
|
||||
"@capacitor/camera": "^8.2.0",
|
||||
"@capacitor/cli": "^8.3.4",
|
||||
"@capacitor/core": "^8.3.4",
|
||||
"@capacitor/filesystem": "^8.1.2",
|
||||
"@capacitor/ios": "^8.4.1",
|
||||
"@capacitor/local-notifications": "^8.2.0",
|
||||
"@capacitor/preferences": "^8.0.1",
|
||||
"@capacitor/share": "^8.0.1",
|
||||
"@capawesome/capacitor-google-sign-in": "^0.1.2",
|
||||
"@toast-ui/editor": "^3.2.2",
|
||||
"@toast-ui/editor-plugin-code-syntax-highlight": "^3.1.0",
|
||||
"axios": "^1.16.1",
|
||||
@@ -53,7 +61,8 @@
|
||||
"prismjs": "^1.30.0",
|
||||
"sortablejs": "^1.15.7",
|
||||
"vue": "^3.5.32",
|
||||
"vue-router": "^5.0.4"
|
||||
"vue-router": "^5.0.4",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/assets": "^3.0.5",
|
||||
@@ -70,6 +79,7 @@
|
||||
"jsdom": "^29.1.1",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"oxlint": "~1.60.0",
|
||||
"patch-package": "^8.0.1",
|
||||
"vite": "^8.0.8",
|
||||
"vite-plugin-vue-devtools": "^8.1.1",
|
||||
"vitest": "^4.1.8"
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
diff --git a/node_modules/@capacitor-community/apple-sign-in/Package.swift b/node_modules/@capacitor-community/apple-sign-in/Package.swift
|
||||
index 94246df..7d9d007 100644
|
||||
--- a/node_modules/@capacitor-community/apple-sign-in/Package.swift
|
||||
+++ b/node_modules/@capacitor-community/apple-sign-in/Package.swift
|
||||
@@ -10,7 +10,7 @@ let package = Package(
|
||||
targets: ["SignInWithApple"])
|
||||
],
|
||||
dependencies: [
|
||||
- .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "7.0.0")
|
||||
+ .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0")
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -1,19 +1,40 @@
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, watch } from 'vue'
|
||||
import { RouterView, useRoute } from 'vue-router'
|
||||
import { computed, onErrorCaptured, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { RouterView, useRoute, useRouter } from 'vue-router'
|
||||
import AppHeader from '@/components/layout/AppHeader.vue'
|
||||
import AppSidebar from '@/components/layout/AppSidebar.vue'
|
||||
import AppBottomNav from '@/components/layout/AppBottomNav.vue'
|
||||
import AdBanner from '@/components/AdBanner.vue'
|
||||
import LoginModal from '@/components/LoginModal.vue'
|
||||
import SignupModal from '@/components/SignupModal.vue'
|
||||
import WebOnlyNotice from '@/components/WebOnlyNotice.vue'
|
||||
import AppDialog from '@/components/ui/AppDialog.vue'
|
||||
import AppLockScreen from '@/components/AppLockScreen.vue'
|
||||
import { appLock } from '@/composables/appLock'
|
||||
import { appError, reportFatal } from '@/composables/appError'
|
||||
import { Capacitor } from '@capacitor/core'
|
||||
import { reminders } from '@/native/reminders'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useUiStore } from '@/stores/ui'
|
||||
import { demo, exitDemo } from '@/demo'
|
||||
import { ADS_ENABLED } from '@/config/features'
|
||||
|
||||
const auth = useAuthStore()
|
||||
const ui = useUiStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 로그인 또는 둘러보기(데모) 상태면 사이드바/전체 셸 표시
|
||||
const authed = computed(() => auth.isAuthenticated || demo.on)
|
||||
// 광고: 광고 기능이 켜져 있고 유료(PREMIUM) 회원이 아닐 때만 표시 (레이아웃 하단 여백도 함께 제어)
|
||||
const showAds = computed(() => ADS_ENABLED && !auth.isPremium)
|
||||
// 공개 법적 고지(개인정보처리방침·약관)는 웹 브라우저에서도 접근 허용 (앱 마켓 정책 URL)
|
||||
const isPublicLegal = computed(() => !!route.meta.public)
|
||||
|
||||
function leaveDemo() {
|
||||
exitDemo()
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
// 앱(Capacitor 네이티브) 또는 데스크톱(Electron) 클라이언트에서 전체 이용.
|
||||
// 웹(브라우저)은 안내 페이지만 노출. 개발 모드(npm run dev)는 예외로 전체 앱 표시.
|
||||
@@ -25,38 +46,178 @@ function onUnauthorized() {
|
||||
auth.clear()
|
||||
ui.openLogin()
|
||||
}
|
||||
// http.js 의 403 PREMIUM_REQUIRED 처리 → 업그레이드 안내 화면으로
|
||||
function onPremiumRequired() {
|
||||
if (route.name !== 'upgrade') router.push({ name: 'upgrade' })
|
||||
}
|
||||
|
||||
// ===== 네트워크 상태 배너 =====
|
||||
// 오프라인이거나(서버 연결 끊김) 서버 응답이 없을 때 상단에 안내 배너를 띄운다.
|
||||
const online = ref(typeof navigator !== 'undefined' ? navigator.onLine : true)
|
||||
const netTrouble = ref(false) // 온라인인데 서버 응답이 없는 경우(타임아웃/서버다운) 잠깐 표시
|
||||
let netTimer = null
|
||||
function onOnline() { online.value = true }
|
||||
function onOffline() { online.value = false }
|
||||
function onNetError() {
|
||||
if (typeof navigator !== 'undefined' && !navigator.onLine) { online.value = false; return }
|
||||
netTrouble.value = true
|
||||
clearTimeout(netTimer)
|
||||
netTimer = setTimeout(() => { netTrouble.value = false }, 5000)
|
||||
}
|
||||
const showNetBanner = computed(() => !online.value || netTrouble.value)
|
||||
const netBannerText = computed(() =>
|
||||
!online.value
|
||||
? '📡 인터넷 연결이 끊겼어요. 연결 상태를 확인해 주세요.'
|
||||
: '⚠️ 서버에 연결할 수 없어요. 잠시 후 다시 시도해 주세요.',
|
||||
)
|
||||
function retryReload() {
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
// ===== 앱 잠금(PIN) =====
|
||||
// 잠깐 다른 앱 다녀온 정도로는 잠그지 않고, 일정 시간 이상 백그라운드였을 때만 잠근다.
|
||||
// (완전 종료/재시작은 onMounted 에서 잠금) — 카메라/갤러리/공유 등 인앱 동작은 suppress 로 제외.
|
||||
const LOCK_GRACE_MS = 60 * 60 * 1000 // 1시간 미만 이탈은 잠그지 않음(잦은 PIN 입력 방지)
|
||||
let backgroundedAt = 0
|
||||
function onVisibility() {
|
||||
if (document.hidden) {
|
||||
backgroundedAt = Date.now()
|
||||
} else {
|
||||
const away = backgroundedAt ? Date.now() - backgroundedAt : 0
|
||||
backgroundedAt = 0
|
||||
if (away >= LOCK_GRACE_MS) appLock.lock() // lock() 은 suppress 창이면 건너뜀
|
||||
appLock.clearSuppress() // 복귀 — 일시중지 창 해제
|
||||
}
|
||||
}
|
||||
// 파일 선택(input[type=file]) 클릭 시 다음 백그라운드 전환을 잠금에서 제외
|
||||
function onFileInputClick(e) {
|
||||
const t = e.target
|
||||
if (t && t.tagName === 'INPUT' && t.type === 'file') appLock.suppressLock()
|
||||
}
|
||||
|
||||
// ===== 토스트 (뒤로가기 종료 안내 등) =====
|
||||
const toastMsg = ref('')
|
||||
let toastTimer = null
|
||||
function onToast(e) {
|
||||
toastMsg.value = e.detail || ''
|
||||
clearTimeout(toastTimer)
|
||||
toastTimer = setTimeout(() => { toastMsg.value = '' }, 1800)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!isApp) return // 웹은 안내만 — 앱 전용 초기화 생략
|
||||
window.addEventListener('auth:unauthorized', onUnauthorized)
|
||||
window.addEventListener('premium:required', onPremiumRequired)
|
||||
window.addEventListener('online', onOnline)
|
||||
window.addEventListener('offline', onOffline)
|
||||
window.addEventListener('net:error', onNetError)
|
||||
window.addEventListener('app:toast', onToast)
|
||||
// 앱 잠금: 저장된 세션으로 재진입하는 경우 시작 시 잠금(새 로그인 직후는 잠그지 않음)
|
||||
if (appLock.state.enabled && appLock.hasSession()) appLock.lock()
|
||||
document.addEventListener('visibilitychange', onVisibility)
|
||||
document.addEventListener('click', onFileInputClick, true)
|
||||
ui.loadSignupEnabled() // 회원가입 허용 여부 선로딩(랜딩/로그인 가입 버튼에 반영)
|
||||
// 로그인 상태면 전체 프로필(아바타·포인트) 최신화 — 재로그인 없이 헤더 아바타 반영
|
||||
if (auth.isAuthenticated) {
|
||||
auth.refreshProfile().then(() => {
|
||||
if (reminders.isNative()) {
|
||||
reminders.scheduleExpiryReminder({
|
||||
premium: auth.isPremium,
|
||||
autoRenew: !!auth.user?.planAutoRenew,
|
||||
expiresAt: auth.user?.planExpiresAt,
|
||||
})
|
||||
}
|
||||
})
|
||||
if (reminders.isNative()) reminders.scheduleEntryReminder(false) // 오늘 기록 여부는 홈/내역에서 보정
|
||||
}
|
||||
})
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('auth:unauthorized', onUnauthorized)
|
||||
window.removeEventListener('premium:required', onPremiumRequired)
|
||||
window.removeEventListener('online', onOnline)
|
||||
window.removeEventListener('offline', onOffline)
|
||||
window.removeEventListener('net:error', onNetError)
|
||||
window.removeEventListener('app:toast', onToast)
|
||||
document.removeEventListener('visibilitychange', onVisibility)
|
||||
document.removeEventListener('click', onFileInputClick, true)
|
||||
clearTimeout(netTimer)
|
||||
clearTimeout(toastTimer)
|
||||
})
|
||||
|
||||
// 자식 컴포넌트의 렌더/라이프사이클 오류를 잡아 흰 화면 대신 폴백 표시
|
||||
onErrorCaptured((err, _instance, info) => {
|
||||
reportFatal({ err, info })
|
||||
return false // 상위로 전파 중단
|
||||
})
|
||||
onUnmounted(() => window.removeEventListener('auth:unauthorized', onUnauthorized))
|
||||
|
||||
// 페이지 이동 시 모바일 사이드바 자동 닫힘
|
||||
watch(() => route.fullPath, () => ui.closeSidebar())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 웹(브라우저): 안내 페이지만 -->
|
||||
<WebOnlyNotice v-if="!isApp" />
|
||||
<!-- 치명적 오류 폴백 (흰 화면 방지) -->
|
||||
<div v-if="appError.hasError" class="fatal-screen">
|
||||
<div class="fatal-inner">
|
||||
<div class="fatal-emoji">😵</div>
|
||||
<h1 class="fatal-title">문제가 발생했어요</h1>
|
||||
<p class="fatal-sub">일시적인 오류일 수 있어요. 새로고침 후 다시 시도해 주세요.</p>
|
||||
<button type="button" class="fatal-btn" @click="retryReload">새로고침</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- 웹(브라우저): 공개 법적 고지는 노출, 그 외엔 안내 페이지 -->
|
||||
<template v-if="!isApp">
|
||||
<RouterView v-if="isPublicLegal" />
|
||||
<WebOnlyNotice v-else />
|
||||
</template>
|
||||
|
||||
<!-- 앱(Capacitor): 전체 기능 -->
|
||||
<template v-else>
|
||||
<div class="layout" :class="{ 'sidebar-open': ui.sidebarOpen, 'no-sidebar': !auth.isAuthenticated }">
|
||||
<!-- 앱 잠금 화면 (로그인 상태 + 잠금 활성 시) -->
|
||||
<AppLockScreen v-if="appLock.state.locked && auth.isAuthenticated" />
|
||||
|
||||
<!-- 네트워크 오류/오프라인 안내 배너 -->
|
||||
<Transition name="netbar">
|
||||
<div v-if="showNetBanner" class="net-banner" role="alert">
|
||||
<span class="net-text">{{ netBannerText }}</span>
|
||||
<button type="button" class="net-retry" @click="retryReload">새로고침</button>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<div class="layout" :class="{ 'sidebar-open': ui.sidebarOpen, 'no-sidebar': !authed, 'has-ad': showAds }">
|
||||
<AppHeader class="layout-top" />
|
||||
<!-- 로그인 전에는 메뉴가 없어 사이드바를 숨김(빈 칸 제거·중앙 정렬) -->
|
||||
<template v-if="auth.isAuthenticated">
|
||||
<!-- 로그인/둘러보기 전에는 메뉴가 없어 사이드바를 숨김(빈 칸 제거·중앙 정렬) -->
|
||||
<template v-if="authed">
|
||||
<AppSidebar class="layout-left" />
|
||||
<div class="sidebar-backdrop" @click="ui.closeSidebar()"></div>
|
||||
</template>
|
||||
<main class="layout-body">
|
||||
<div v-if="demo.on" class="demo-bar">
|
||||
<span>👀 <b>둘러보기 모드</b> · 샘플 데이터입니다.</span>
|
||||
<span class="demo-actions">
|
||||
<button type="button" class="db-login" @click="ui.openLogin('/account')">로그인</button>
|
||||
<button type="button" class="db-exit" @click="leaveDemo">나가기</button>
|
||||
</span>
|
||||
</div>
|
||||
<RouterView />
|
||||
</main>
|
||||
<!-- 하단 내비 위 광고 (유료 회원 제외). 앱은 320x50 모바일 배너(임시) -->
|
||||
<AdBanner v-if="showAds" class="app-ad" unit="DAN-qd8As3nqJjXTQKa6" :width="320" :height="50" />
|
||||
<AppBottomNav class="layout-bottom" />
|
||||
</div>
|
||||
|
||||
<LoginModal />
|
||||
<SignupModal />
|
||||
|
||||
<!-- 토스트 (뒤로가기 종료 안내 등) -->
|
||||
<Transition name="toast">
|
||||
<div v-if="toastMsg" class="app-toast" role="status">{{ toastMsg }}</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<!-- 공용 인앱 다이얼로그 (네이티브 alert/confirm/prompt 대체) -->
|
||||
<AppDialog />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -64,12 +225,15 @@ watch(() => route.fullPath, () => ui.closeSidebar())
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
'top top'
|
||||
'left top'
|
||||
'left body'
|
||||
'bottom bottom';
|
||||
grid-template-columns: 220px 1fr;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
min-height: 100vh;
|
||||
/* 앱 셸: 뷰포트 높이에 고정하고 본문(layout-body)만 스크롤 → 상단 헤더·하단 내비 고정, 페이지 전체 스크롤 방지 */
|
||||
height: 100vh; /* 폴백 */
|
||||
height: 100dvh; /* 동적 뷰포트(주소창·안전영역 반영) */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.layout-top {
|
||||
@@ -80,18 +244,187 @@ watch(() => route.fullPath, () => ui.closeSidebar())
|
||||
grid-area: left;
|
||||
}
|
||||
|
||||
.demo-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.55rem 0.9rem;
|
||||
border: 1px solid hsla(160, 100%, 37%, 0.4);
|
||||
border-radius: 8px;
|
||||
background: hsla(160, 100%, 37%, 0.08);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
/* 네트워크 오류/오프라인 배너 — 화면 최상단 고정 */
|
||||
.net-banner {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
padding: calc(env(safe-area-inset-top, 0) + 0.5rem) 0.9rem 0.5rem;
|
||||
background: #c0392b;
|
||||
color: #fff;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.net-text {
|
||||
word-break: keep-all;
|
||||
}
|
||||
.net-retry {
|
||||
flex: none;
|
||||
padding: 0.25rem 0.7rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.7);
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
.net-retry:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
.netbar-enter-active,
|
||||
.netbar-leave-active {
|
||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||
}
|
||||
.netbar-enter-from,
|
||||
.netbar-leave-to {
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 토스트 */
|
||||
.app-toast {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: calc(72px + env(safe-area-inset-bottom, 0));
|
||||
transform: translateX(-50%);
|
||||
z-index: 2600;
|
||||
max-width: 80vw;
|
||||
padding: 0.6rem 1.1rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(0, 0, 0, 0.82);
|
||||
color: #fff;
|
||||
font-size: 0.85rem;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.toast-enter-active,
|
||||
.toast-leave-active {
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
.toast-enter-from,
|
||||
.toast-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(8px);
|
||||
}
|
||||
|
||||
/* 치명적 오류 폴백 화면 */
|
||||
.fatal-screen {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 4000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.5rem;
|
||||
background: var(--color-background);
|
||||
}
|
||||
.fatal-inner {
|
||||
text-align: center;
|
||||
max-width: 360px;
|
||||
}
|
||||
.fatal-emoji {
|
||||
font-size: 2.8rem;
|
||||
}
|
||||
.fatal-title {
|
||||
margin-top: 0.6rem;
|
||||
font-size: 1.2rem;
|
||||
color: var(--color-heading);
|
||||
}
|
||||
.fatal-sub {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.7;
|
||||
line-height: 1.6;
|
||||
word-break: keep-all;
|
||||
}
|
||||
.fatal-btn {
|
||||
margin-top: 1.4rem;
|
||||
padding: 0.7rem 1.8rem;
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
background: hsla(160, 100%, 37%, 1);
|
||||
color: #fff;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
.fatal-btn:hover {
|
||||
background: hsla(160, 100%, 32%, 1);
|
||||
}
|
||||
.demo-actions {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.demo-bar button {
|
||||
padding: 0.35rem 0.8rem;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.db-login {
|
||||
border: 1px solid hsla(160, 100%, 37%, 1);
|
||||
background: hsla(160, 100%, 37%, 1);
|
||||
color: #fff;
|
||||
}
|
||||
.db-exit {
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-background);
|
||||
color: var(--color-text);
|
||||
}
|
||||
.layout-body {
|
||||
grid-area: body;
|
||||
padding: 1.5rem 2rem;
|
||||
/* 하단 고정 내비(56px)+소프트키 인셋만큼 비워 콘텐츠가 가리지 않게 */
|
||||
/* 하단 고정 내비(56px + 안전영역) 뒤로 마지막 내용이 가려지지 않도록 항상 여백 확보 */
|
||||
padding-bottom: calc(56px + env(safe-area-inset-bottom, 0) + 1rem);
|
||||
overflow: auto;
|
||||
/* 그리드 1fr 트랙 안에서만 스크롤(콘텐츠로 인해 트랙이 늘어나 페이지 전체가 스크롤되지 않도록 min-height:0) */
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden; /* 페이지 가로 스크롤 방지 — 넓은 표/차트는 각자 컨테이너에서 스크롤 */
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.layout-bottom {
|
||||
grid-area: bottom;
|
||||
}
|
||||
|
||||
/* 하단 내비(56px) 바로 위 고정 광고 */
|
||||
.app-ad {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: calc(56px + env(safe-area-inset-bottom, 0));
|
||||
z-index: 999;
|
||||
background: var(--color-background);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
/* 광고가 있을 땐 본문 하단 여백을 광고 높이(320x50 배너)만큼 더 확보 */
|
||||
.layout.has-ad .layout-body {
|
||||
padding-bottom: calc(56px + 62px + env(safe-area-inset-bottom, 0) + 1rem);
|
||||
}
|
||||
|
||||
/* 로그인 전: 사이드바 없이 단일 컬럼 — 콘텐츠가 창 중앙에 오도록 */
|
||||
.layout.no-sidebar {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import http from './http'
|
||||
import { demo, demoApi } from '@/demo'
|
||||
|
||||
// 백엔드 /api/account 엔드포인트와 매핑 (본인 데이터만)
|
||||
export const accountApi = {
|
||||
const realApi = {
|
||||
list({ year, month, type, category, walletId, keyword, tagId } = {}) {
|
||||
return http.get('/account/entries', { params: { year, month, type, category, walletId, keyword, tagId } })
|
||||
},
|
||||
@@ -14,6 +15,10 @@ export const accountApi = {
|
||||
categoryStats({ type, year, month } = {}) {
|
||||
return http.get('/account/category-stats', { params: { type, year, month } })
|
||||
},
|
||||
// 통계 화면 집계 → AI 재무 코멘트(유료). 실패해도 화면을 막지 않도록 전역 배너 억제
|
||||
aiComment(payload) {
|
||||
return http.post('/account/ai-comment', payload, { timeout: 30000, suppressNetError: true })
|
||||
},
|
||||
netWorthTrend({ months, weeks, unit } = {}) {
|
||||
return http.get('/account/networth/trend', { params: { months, weeks, unit } })
|
||||
},
|
||||
@@ -26,6 +31,26 @@ export const accountApi = {
|
||||
remove(id) {
|
||||
return http.delete(`/account/entries/${id}`)
|
||||
},
|
||||
// 문자/푸시 텍스트 파싱(저장 안 함) → 추가 폼 자동채움
|
||||
// categoryHints/walletHints: AI가 이 후보들 중에서 분류·결제수단을 고르도록 사용자 목록 전달
|
||||
parseText({ title, text, categoryHints, walletHints } = {}) {
|
||||
return http.post('/account/entries/parse', { title, text, categoryHints, walletHints })
|
||||
},
|
||||
// 환율 조회 — 외화 결제 입력 시 통화→원화 환율 자동 채움
|
||||
fxRate(from, to = 'KRW') {
|
||||
return http.get('/fx/rate', { params: { from, to } })
|
||||
},
|
||||
|
||||
// 자주 쓰는 내역(빠른 등록 템플릿)
|
||||
quickEntries() {
|
||||
return http.get('/account/quick-entries')
|
||||
},
|
||||
createQuickEntry(payload) {
|
||||
return http.post('/account/quick-entries', payload)
|
||||
},
|
||||
removeQuickEntry(id) {
|
||||
return http.delete(`/account/quick-entries/${id}`)
|
||||
},
|
||||
|
||||
// 계좌/카드 (사용자별)
|
||||
wallets() {
|
||||
@@ -56,8 +81,12 @@ export const accountApi = {
|
||||
},
|
||||
|
||||
// 예산 (사용자별)
|
||||
budgets() {
|
||||
return http.get('/account/budgets')
|
||||
budgets({ year, month } = {}) {
|
||||
return http.get('/account/budgets', { params: { year, month } })
|
||||
},
|
||||
// 다른 월 예산을 이 달로 복사(전월 복사 등)
|
||||
copyBudget({ fromYear, fromMonth, toYear, toMonth }) {
|
||||
return http.post('/account/budgets/copy', null, { params: { fromYear, fromMonth, toYear, toMonth } })
|
||||
},
|
||||
budgetStatus({ year, month }) {
|
||||
return http.get('/account/budgets/status', { params: { year, month } })
|
||||
@@ -71,8 +100,8 @@ export const accountApi = {
|
||||
setExpectedIncome({ year, month, expectedIncome }) {
|
||||
return http.put('/account/budgets/income', { expectedIncome }, { params: { year, month } })
|
||||
},
|
||||
createBudget(payload) {
|
||||
return http.post('/account/budgets', payload)
|
||||
createBudget(payload, { year, month } = {}) {
|
||||
return http.post('/account/budgets', payload, { params: { year, month } })
|
||||
},
|
||||
updateBudget(id, payload) {
|
||||
return http.put(`/account/budgets/${id}`, payload)
|
||||
@@ -80,6 +109,19 @@ export const accountApi = {
|
||||
removeBudget(id) {
|
||||
return http.delete(`/account/budgets/${id}`)
|
||||
},
|
||||
// 1회성 예산
|
||||
onetimeBudgets() {
|
||||
return http.get('/account/budgets/onetime')
|
||||
},
|
||||
createOnetimeBudget(payload) {
|
||||
return http.post('/account/budgets/onetime', payload)
|
||||
},
|
||||
updateOnetimeBudget(id, payload) {
|
||||
return http.put(`/account/budgets/onetime/${id}`, payload)
|
||||
},
|
||||
removeOnetimeBudget(id) {
|
||||
return http.delete(`/account/budgets/onetime/${id}`)
|
||||
},
|
||||
createWallet(payload) {
|
||||
return http.post('/account/wallets', payload)
|
||||
},
|
||||
@@ -173,13 +215,82 @@ export const accountApi = {
|
||||
return http.post(`/account/entries/${id}/confirm`, payload || {})
|
||||
},
|
||||
|
||||
// 영수증 OCR (백엔드가 Google Vision 호출 → 전체 텍스트 반환)
|
||||
// 데이터 복구(가져오기) — 전체 데이터 덮어쓰기
|
||||
restore(payload) {
|
||||
return http.post('/account/restore', payload)
|
||||
},
|
||||
|
||||
// 가계부 데이터 전체 삭제 (내역·계좌·분류·태그·예산·고정지출·투자 전부)
|
||||
deleteAllData() {
|
||||
return http.delete('/account/all')
|
||||
},
|
||||
|
||||
// 포인트 상점 — 현황 조회 / 아이템 구매
|
||||
shopStatus() {
|
||||
return http.get('/account/shop/status')
|
||||
},
|
||||
buyShopItem(item) {
|
||||
return http.post('/account/shop/buy', { item })
|
||||
},
|
||||
|
||||
// 영수증 OCR (백엔드가 Google Vision 호출 → 전체 텍스트 반환) — AI 폴백용
|
||||
// suppressNetError: runOcr 이 자체 formError 로 안내하므로 전역 배너 억제
|
||||
ocrReceipt(blob) {
|
||||
const fd = new FormData()
|
||||
fd.append('image', blob, 'receipt.jpg')
|
||||
return http.post('/account/ocr/receipt', fd, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
timeout: 30000,
|
||||
suppressNetError: true,
|
||||
})
|
||||
},
|
||||
// 영수증 AI 구조화 (Claude Vision) — 금액·상호·날짜·분류·계좌를 한 번에. 사용자 후보 목록을 함께 전달
|
||||
// 비전 처리가 느릴 수 있어 타임아웃 상향, 실패 시 OCR 폴백이 있으므로 전역 배너 억제
|
||||
parseReceiptAi(blob, { categoryHints = [], walletHints = [] } = {}) {
|
||||
const fd = new FormData()
|
||||
fd.append('image', blob, 'receipt.jpg')
|
||||
// 멀티파트 파트 수 제한(Tomcat 기본 10) 회피: 후보를 각각 한 파트에 줄바꿈으로 합쳐 전송
|
||||
if (categoryHints.length) fd.append('categoryHints', categoryHints.join('\n'))
|
||||
if (walletHints.length) fd.append('walletHints', walletHints.join('\n'))
|
||||
return http.post('/account/ocr/receipt-ai', fd, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
timeout: 60000,
|
||||
suppressNetError: true,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
// ===== 둘러보기(데모) 모드 =====
|
||||
// demo.on 이면 읽기는 더미 데이터, 쓰기는 차단(안내 메시지)으로 응답한다.
|
||||
const DEMO_READS = {
|
||||
list: demoApi.list,
|
||||
summary: demoApi.summary,
|
||||
wallets: demoApi.wallets,
|
||||
netWorth: demoApi.netWorth,
|
||||
categories: demoApi.categories,
|
||||
recurrings: demoApi.recurrings,
|
||||
quickEntries: demoApi.quickEntries,
|
||||
pendingCount: demoApi.pendingCount,
|
||||
tags: demoApi.tags,
|
||||
walletEntries: demoApi.walletEntries,
|
||||
}
|
||||
const DEMO_WRITES = new Set([
|
||||
'create', 'update', 'remove', 'parseText', 'createQuickEntry', 'removeQuickEntry', 'repayment',
|
||||
'createRecurring', 'updateRecurring', 'removeRecurring', 'runRecurrings',
|
||||
'createWallet', 'updateWallet', 'removeWallet', 'reorderWallets',
|
||||
'createCategory', 'updateCategory', 'removeCategory', 'reorderCategories', 'importCategories',
|
||||
'createTag', 'updateTag', 'removeTag', 'reorderTags', 'confirmEntry', 'ocrReceipt', 'parseReceiptAi', 'aiComment', 'restore', 'deleteAllData', 'buyShopItem',
|
||||
'createBudget', 'updateBudget', 'removeBudget', 'setExpectedIncome',
|
||||
'createHolding', 'updateHolding', 'removeHolding', 'addTrade', 'updateTrade', 'removeTrade',
|
||||
'refreshPrices', 'refreshAllPrices',
|
||||
])
|
||||
|
||||
export const accountApi = new Proxy(realApi, {
|
||||
get(target, prop) {
|
||||
if (demo.on) {
|
||||
if (DEMO_READS[prop]) return DEMO_READS[prop]
|
||||
if (DEMO_WRITES.has(prop)) return () => demoApi.block()
|
||||
}
|
||||
return target[prop]
|
||||
},
|
||||
})
|
||||
|
||||
@@ -23,11 +23,22 @@ export const adminApi = {
|
||||
removeTag(id) {
|
||||
return http.delete(`/admin/tags/${id}`)
|
||||
},
|
||||
getBoardSetting() {
|
||||
return http.get('/admin/board-setting')
|
||||
|
||||
// 기본(디폴트) 분류 관리 (관리자) — 사용자가 '기본 분류 불러오기'로 가져감
|
||||
defaultCategories() {
|
||||
return http.get('/admin/default-categories')
|
||||
},
|
||||
setBoardSetting(tagCategoryId) {
|
||||
return http.put('/admin/board-setting', { tagCategoryId })
|
||||
createDefaultCategory(payload) {
|
||||
return http.post('/admin/default-categories', payload)
|
||||
},
|
||||
updateDefaultCategory(id, payload) {
|
||||
return http.put(`/admin/default-categories/${id}`, payload)
|
||||
},
|
||||
removeDefaultCategory(id) {
|
||||
return http.delete(`/admin/default-categories/${id}`)
|
||||
},
|
||||
reorderDefaultCategories(type, ids) {
|
||||
return http.put('/admin/default-categories/reorder', { type, ids })
|
||||
},
|
||||
|
||||
// 앱 설정 (관리자)
|
||||
@@ -45,6 +56,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 })
|
||||
},
|
||||
|
||||
@@ -11,6 +11,17 @@ export const authApi = {
|
||||
login(payload) {
|
||||
return http.post('/auth/login', payload)
|
||||
},
|
||||
// 구글 로그인/가입 — GIS에서 받은 ID 토큰 전달
|
||||
googleLogin(payload) {
|
||||
return http.post('/auth/google', payload)
|
||||
},
|
||||
googleClientId() {
|
||||
return http.get('/auth/google-client-id')
|
||||
},
|
||||
// 애플 로그인/가입 — Sign in with Apple 에서 받은 identity token 전달
|
||||
appleLogin(payload) {
|
||||
return http.post('/auth/apple', payload)
|
||||
},
|
||||
logout() {
|
||||
return http.post('/auth/logout')
|
||||
},
|
||||
@@ -28,4 +39,25 @@ export const authApi = {
|
||||
updateProfile(payload) {
|
||||
return http.put('/auth/profile', payload)
|
||||
},
|
||||
// 프로필 사진 변경/해제 — image 가 null/빈값이면 해제(구글 사진으로 폴백)
|
||||
updateProfileImage(image) {
|
||||
return http.put('/auth/profile-image', { image })
|
||||
},
|
||||
// 현재 사용자 활동 포인트 (최신값)
|
||||
points() {
|
||||
return http.get('/auth/points')
|
||||
},
|
||||
// 포인트 적립/차감 내역 — year+month 지정 시 해당 월, 미지정 시 최근 100건
|
||||
pointHistory(year, month) {
|
||||
const params = year && month ? { year, month } : {}
|
||||
return http.get('/auth/point-history', { params })
|
||||
},
|
||||
// 현재 사용자 전체 프로필(아바타·포인트 포함) — 재로그인 없이 최신값 동기화
|
||||
profile() {
|
||||
return http.get('/auth/profile')
|
||||
},
|
||||
// 회원 탈퇴 — 계정·데이터 전체 삭제
|
||||
withdraw() {
|
||||
return http.delete('/auth/me')
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import http from './http'
|
||||
|
||||
// 백엔드 /api/billing 엔드포인트와 매핑
|
||||
export const billingApi = {
|
||||
// 구독 상품 목록
|
||||
products() {
|
||||
return http.get('/billing/products')
|
||||
},
|
||||
// Google Play 구매 검증 → 멤버십 부여
|
||||
verifyGoogle(payload) {
|
||||
return http.post('/billing/google/verify', payload)
|
||||
},
|
||||
// 현재 구독 현황 (플랜·만료일·다음 결제일·자동갱신)
|
||||
subscription() {
|
||||
return http.get('/billing/subscription')
|
||||
},
|
||||
// 구독 해지 (자동 갱신 중단)
|
||||
cancel() {
|
||||
return http.post('/billing/cancel')
|
||||
},
|
||||
// 구독 재개 (만료 전 자동 갱신 재개)
|
||||
resume() {
|
||||
return http.post('/billing/resume')
|
||||
},
|
||||
// 구매 복원 (기기 변경·재설치)
|
||||
restore() {
|
||||
return http.post('/billing/restore')
|
||||
},
|
||||
}
|
||||
@@ -17,6 +17,13 @@ export const boardApi = {
|
||||
get(id) {
|
||||
return http.get(`/board/posts/${id}`)
|
||||
},
|
||||
// 내 글/댓글 모아보기
|
||||
myPosts({ page = 1, size = 10 } = {}) {
|
||||
return http.get('/board/my/posts', { params: { page, size } })
|
||||
},
|
||||
myComments({ page = 1, size = 10 } = {}) {
|
||||
return http.get('/board/my/comments', { params: { page, size } })
|
||||
},
|
||||
create(payload) {
|
||||
return http.post('/board/posts', payload)
|
||||
},
|
||||
@@ -35,11 +42,11 @@ export const boardApi = {
|
||||
timeout: 30000,
|
||||
})
|
||||
},
|
||||
tags() {
|
||||
return http.get('/board/tags')
|
||||
tags(category) {
|
||||
return http.get('/board/tags', { params: { category: category || undefined } })
|
||||
},
|
||||
tagGroups() {
|
||||
return http.get('/board/tag-groups')
|
||||
tagGroups(category) {
|
||||
return http.get('/board/tag-groups', { params: { category: category || undefined } })
|
||||
},
|
||||
addComment(id, content) {
|
||||
return http.post(`/board/posts/${id}/comments`, { content })
|
||||
@@ -47,10 +54,47 @@ export const boardApi = {
|
||||
removeComment(commentId) {
|
||||
return http.delete(`/board/comments/${commentId}`)
|
||||
},
|
||||
// 추천/비추천 (토글) — type: 'UP' | 'DOWN'
|
||||
votePost(id, type) {
|
||||
return http.post(`/board/posts/${id}/vote`, { type })
|
||||
},
|
||||
voteComment(commentId, type) {
|
||||
return http.post(`/board/comments/${commentId}/vote`, { type })
|
||||
},
|
||||
// 추천(👍)한 사람 목록
|
||||
recommenders(id) {
|
||||
return http.get(`/board/posts/${id}/recommenders`)
|
||||
},
|
||||
// 신고 (누적 5건 시 블라인드)
|
||||
reportPost(id) {
|
||||
return http.post(`/board/posts/${id}/report`)
|
||||
},
|
||||
reportComment(commentId) {
|
||||
return http.post(`/board/comments/${commentId}/report`)
|
||||
},
|
||||
// 댓글 블라인드 해제 (관리자)
|
||||
unblockComment(commentId) {
|
||||
return http.post(`/board/comments/${commentId}/unblock`)
|
||||
},
|
||||
// 신고 관리 (관리자) — 신고된 글/댓글 목록
|
||||
reports() {
|
||||
return http.get('/board/reports')
|
||||
},
|
||||
// 신고 무시 (관리자) — 블라인드 없이 신고 기록만 초기화
|
||||
dismissReport(targetType, targetId) {
|
||||
return http.post('/board/reports/dismiss', { targetType, targetId })
|
||||
},
|
||||
block(id, reason) {
|
||||
return http.post(`/board/posts/${id}/block`, { reason })
|
||||
},
|
||||
unblock(id) {
|
||||
return http.post(`/board/posts/${id}/unblock`)
|
||||
},
|
||||
// 공지 설정/해제 (관리자) — 목록 최상단 고정
|
||||
setNotice(id) {
|
||||
return http.post(`/board/posts/${id}/notice`)
|
||||
},
|
||||
unsetNotice(id) {
|
||||
return http.post(`/board/posts/${id}/unnotice`)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -31,6 +31,13 @@ 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'))
|
||||
} else if (!error.response && error.code !== 'ERR_CANCELED' && !error.config?.suppressNetError) {
|
||||
// 응답 자체가 없음 = 네트워크 오류(오프라인/타임아웃/서버 연결 불가). 취소 요청은 제외.
|
||||
// suppressNetError: 자체 폴백/에러표시가 있는 호출(예: 영수증 AI)은 전역 배너를 띄우지 않음.
|
||||
window.dispatchEvent(new CustomEvent('net:error'))
|
||||
}
|
||||
return Promise.reject(error)
|
||||
},
|
||||
|
||||