Compare commits
434 Commits
a255204ad8
..
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 | |||
| a25fef65f8 | |||
| 27beaf28d4 | |||
| 5abe6cfc09 | |||
| f55443b823 | |||
| d5ea6f048e | |||
| 6ab8d331e3 | |||
| 74017243bb | |||
| b1f85cee52 | |||
| ca6c8be229 | |||
| 8cc9cbb383 | |||
| f54db96a30 | |||
| 43b01da1ff | |||
| 1ae5f7ec69 | |||
| 8133963f89 | |||
| 5f99fb3b5c | |||
| a4a4aec314 | |||
| a56b8265c7 | |||
| 9aa5f5d690 | |||
| 2582bbb59b | |||
| 9f079fcead | |||
| eaa5c33d2c | |||
| d88854d11b | |||
| 76b86712ef | |||
| dba0741cca | |||
| 73895edca6 | |||
| 9807472474 | |||
| 162768d030 | |||
| 2cde893d55 | |||
| 89eca761b5 | |||
| c67c2ba7ed | |||
| bbe6e93130 | |||
| 7276d878dd | |||
| bb6621fae5 | |||
| 2e2c784f4c | |||
| 6b7955e098 | |||
| f7f75b7fb1 | |||
| 868f889505 | |||
| f45014f827 | |||
| 5f23f5f0b8 | |||
| cbc6245cc7 | |||
| 20210f6716 | |||
| bfba296b91 | |||
| 3862c33690 | |||
| 0238e0b22f | |||
| 1d9a2a584e | |||
| b4aa5c2ad0 | |||
| 28182cc324 | |||
| 9c61f888a5 | |||
| 3e1410a522 | |||
| 7f6a4c62e5 | |||
| 8a9badb01d | |||
| 561c56a8e0 | |||
| ecd850f468 | |||
| f86f8343ef | |||
| 9d15644180 | |||
| dc31cf4bfa | |||
| 3fb0e36413 | |||
| 0f9f7b4507 | |||
| 32522ed512 | |||
| 786f7aae61 | |||
| 33b3e3dc26 | |||
| 9de26cf417 | |||
| 6253ddef3d | |||
| c128948d39 | |||
| 2cc4bb301f | |||
| cb5cf1f0bf | |||
| 33be9a80da | |||
| 095ba942eb | |||
| 981920b38e | |||
| 006fc87f70 | |||
| f1a5e729f1 | |||
| 16b4bce136 | |||
| f03062c92f | |||
| e76b5383d3 | |||
| 3c5686e8fa | |||
| 15222c1971 | |||
| 5604a232fb | |||
| 43e2de6c3c | |||
| 2978ec7c90 | |||
| c9ff605ac9 | |||
| 0f8134eca4 | |||
| c5e4c2dad7 | |||
| 214334a01c | |||
| 4e6a89fd7a | |||
| 468852d839 | |||
| 67aa635dd8 | |||
| 3066e3b37e | |||
| 3be1d8792c | |||
| 488cc79bca | |||
| 633aa54274 | |||
| af272f0ffb | |||
| 8f8744ba4b | |||
| da8f64b738 | |||
| f45be09ff1 | |||
| 6448756aef | |||
| 7acd2163fb | |||
| e8c5237ed0 | |||
| f50859140d | |||
| 3f94ca4d2b | |||
| 537cea31b0 | |||
| c8690b6d62 | |||
| 41d6f11531 | |||
| 5bf4e9f516 | |||
| 68dd926cce | |||
| 0bd0e847c5 | |||
| 715dae70ab | |||
| efb1b7d1b3 | |||
| e1d2ec5bd7 | |||
| 90fedccd84 | |||
| efbe298a1f | |||
| 208f87c24f | |||
| 483cf755ed | |||
| 0feb655327 | |||
| 252f00e527 | |||
| 0606f280b4 | |||
| 63a8a71e3b | |||
| 05912d8e7e | |||
| 385f6c970e | |||
| 80e717e281 | |||
| 964e615c54 | |||
| f0932cbc0c | |||
| 3cc3328b23 | |||
| b9833f6d23 | |||
| 772bc85bae | |||
| f479a35cef | |||
| 840ec84e0d | |||
| ef2e0db4fd | |||
| 245c026e50 | |||
| a352655662 | |||
| 510eb0f4f8 | |||
| fcfb279b16 | |||
| 9e81029a7d | |||
| d94513934b | |||
| 09cfb8e562 |
@@ -0,0 +1,10 @@
|
|||||||
|
# 앱(Capacitor) 빌드 전용 환경변수 — `vite build --mode capacitor` 가 로드합니다.
|
||||||
|
# 앱 안에서는 상대경로(/api)가 서버로 가지 않으므로 백엔드 절대 URL 을 지정해야 합니다.
|
||||||
|
#
|
||||||
|
# [운영 서버 HTTPS] app.sblog.kr 의 /api 를 Nginx 가 백엔드로 프록시
|
||||||
|
VITE_API_BASE_URL=https://app.sblog.kr/api
|
||||||
|
#
|
||||||
|
# [로컬 PC + 실기기] 같은 Wi-Fi
|
||||||
|
# VITE_API_BASE_URL=http://172.30.1.43:8080/api
|
||||||
|
# [에뮬레이터]
|
||||||
|
# VITE_API_BASE_URL=http://10.0.2.2:8080/api
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
# 운영 환경: 실제 백엔드 주소로 교체하세요
|
# 운영 웹 빌드(`npm run build`) — Nginx 가 같은 도메인의 /api 를 백엔드(8080)로 프록시하므로 상대경로 사용
|
||||||
VITE_API_BASE_URL=http://localhost:8080/api
|
VITE_API_BASE_URL=/api
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
# main 은 Deploy 워크플로가 테스트+빌드를 수행하므로 CI 중복 실행 제외(배포 시간 단축).
|
||||||
push:
|
push:
|
||||||
branches: [main, dev]
|
branches: [dev]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main, dev]
|
branches: [main, dev]
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ jobs:
|
|||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
cache: 'npm'
|
# cache 제거: Gitea 캐시 서버 미연결 시 'Save cache' 후처리가 수 분 hang → '가짜 실패' 유발
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
@@ -29,6 +30,10 @@ jobs:
|
|||||||
npx oxlint .
|
npx oxlint .
|
||||||
npx eslint .
|
npx eslint .
|
||||||
|
|
||||||
|
# 단위/컴포넌트 테스트 — 실패 시 빌드/배포 차단(게이트)
|
||||||
|
- name: Test
|
||||||
|
run: npm test
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
name: Deploy (manual)
|
name: Deploy
|
||||||
|
|
||||||
# 배포 대상이 정해지면 아래 단계의 주석을 풀고 시크릿을 등록하세요.
|
# main 푸시 시 자동 배포: dist 빌드 → 서버 Nginx 루트로 업로드 → nginx reload
|
||||||
# 저장소 Settings > Actions > Secrets 에 등록 필요:
|
#
|
||||||
# DEPLOY_HOST, DEPLOY_USER, DEPLOY_SSH_KEY, DEPLOY_PATH
|
# 저장소 Settings > Actions > Secrets 에 등록:
|
||||||
|
# DEPLOY_HOST 배포 서버 호스트/IP
|
||||||
|
# DEPLOY_USER SSH 계정 (nginx reload 무비밀번호 sudo 필요 — deploy/README.md 참고)
|
||||||
|
# DEPLOY_SSH_KEY SSH 개인키 (PEM 전체)
|
||||||
|
# DEPLOY_PATH Nginx 웹 루트 (예: /var/www/sb-front)
|
||||||
|
# DEPLOY_PORT SSH 포트 (선택, 기본 22)
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
workflow_dispatch: {}
|
workflow_dispatch: {}
|
||||||
# push:
|
|
||||||
# tags: ['v*']
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
@@ -19,24 +24,43 @@ jobs:
|
|||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
cache: 'npm'
|
# cache 제거: Gitea 캐시 서버 미연결 시 'Save cache' 후처리가 수 분 hang → '가짜 실패' 유발
|
||||||
|
|
||||||
|
- name: Install
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
# 테스트 게이트 — 실패 시 이후 빌드/배포 스텝이 실행되지 않음
|
||||||
|
- name: Test
|
||||||
|
run: npm test
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Deploy to Nginx (SSH)
|
||||||
|
env:
|
||||||
|
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
||||||
|
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
|
||||||
|
DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }}
|
||||||
|
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||||
|
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
|
||||||
run: |
|
run: |
|
||||||
npm ci
|
set -euo pipefail
|
||||||
npm run build
|
PORT="${DEPLOY_PORT:-22}"
|
||||||
|
DEST="${DEPLOY_PATH:-}"
|
||||||
|
if [ -z "$DEST" ] || [ "$DEST" = "/" ]; then
|
||||||
|
echo "::error::DEPLOY_PATH 시크릿이 비어 있거나 잘못되었습니다."; exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# ===== 배포 예시 (택1) — 사용 시 주석 해제 =====
|
mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
||||||
# 1) 정적 파일을 원격 서버로 rsync (Nginx 등 정적 호스팅)
|
printf '%s\n' "$DEPLOY_SSH_KEY" | tr -d '\r' > ~/.ssh/id_deploy
|
||||||
# - name: Deploy via SSH/rsync
|
chmod 600 ~/.ssh/id_deploy
|
||||||
# uses: appleboy/scp-action@v0.1.7
|
ssh-keyscan -p "$PORT" "$DEPLOY_HOST" >> ~/.ssh/known_hosts 2>/dev/null
|
||||||
# with:
|
SSH="ssh -p $PORT -i $HOME/.ssh/id_deploy -o StrictHostKeyChecking=yes"
|
||||||
# host: ${{ secrets.DEPLOY_HOST }}
|
|
||||||
# username: ${{ secrets.DEPLOY_USER }}
|
|
||||||
# key: ${{ secrets.DEPLOY_SSH_KEY }}
|
|
||||||
# source: "dist/*"
|
|
||||||
# target: ${{ secrets.DEPLOY_PATH }}
|
|
||||||
# strip_components: 1
|
|
||||||
|
|
||||||
- name: Notice
|
echo "Deploy dist/ -> $DEPLOY_USER@$DEPLOY_HOST:$DEST"
|
||||||
run: echo "dist 빌드 완료. 배포 대상 지정 후 위 단계를 활성화하세요."
|
$SSH "$DEPLOY_USER@$DEPLOY_HOST" "mkdir -p '$DEST'"
|
||||||
|
# 기존 파일 정리 후 새 빌드 업로드 (tar 파이프 — 추가 의존성 없음)
|
||||||
|
tar -czf - -C dist . | $SSH "$DEPLOY_USER@$DEPLOY_HOST" "find '$DEST' -mindepth 1 -delete && tar -xzf - -C '$DEST'"
|
||||||
|
# Nginx 설정 검증 후 reload (실패해도 배포 자체는 유지)
|
||||||
|
$SSH "$DEPLOY_USER@$DEPLOY_HOST" "sudo nginx -t && sudo nginx -s reload" || echo "nginx reload 생략됨"
|
||||||
|
echo "✅ 배포 완료"
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ dist-ssr
|
|||||||
coverage
|
coverage
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
|
# Electron desktop build output
|
||||||
|
release-desktop
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
|
|||||||
@@ -1,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).
|
## 기술 스택
|
||||||
|
- **Vue 3** (`<script setup>` Composition API) + **Vite**
|
||||||
## Recommended Browser Setup
|
- **Pinia** (상태관리), **Vue Router** (라우팅 + 인증 가드)
|
||||||
|
- **Axios** (공통 인스턴스 + 토큰/401 인터셉터)
|
||||||
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
|
- **Toast UI Editor** + Prism.js (게시판 마크다운 작성/뷰어)
|
||||||
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
|
- **Capacitor** (안드로이드 하이브리드 앱), **Electron** (Windows 데스크톱)
|
||||||
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
|
- **Vitest** + `@vue/test-utils` (테스트)
|
||||||
- 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
|
|
||||||
|
|
||||||
|
## 빠른 시작
|
||||||
```sh
|
```sh
|
||||||
npm install
|
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)
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
|
||||||
|
|
||||||
|
# Built application files
|
||||||
|
*.apk
|
||||||
|
*.aar
|
||||||
|
*.ap_
|
||||||
|
*.aab
|
||||||
|
|
||||||
|
# Files for the ART/Dalvik VM
|
||||||
|
*.dex
|
||||||
|
|
||||||
|
# Java class files
|
||||||
|
*.class
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
bin/
|
||||||
|
gen/
|
||||||
|
out/
|
||||||
|
# Uncomment the following line in case you need and you don't have the release build type files in your app
|
||||||
|
# release/
|
||||||
|
|
||||||
|
# Gradle files
|
||||||
|
.gradle/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Local configuration file (sdk path, etc)
|
||||||
|
local.properties
|
||||||
|
|
||||||
|
# Proguard folder generated by Eclipse
|
||||||
|
proguard/
|
||||||
|
|
||||||
|
# Log Files
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Android Studio Navigation editor temp files
|
||||||
|
.navigation/
|
||||||
|
|
||||||
|
# Android Studio captures folder
|
||||||
|
captures/
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
*.iml
|
||||||
|
.idea/workspace.xml
|
||||||
|
.idea/tasks.xml
|
||||||
|
.idea/gradle.xml
|
||||||
|
.idea/assetWizardSettings.xml
|
||||||
|
.idea/dictionaries
|
||||||
|
.idea/libraries
|
||||||
|
# Android Studio 3 in .gitignore file.
|
||||||
|
.idea/caches
|
||||||
|
.idea/modules.xml
|
||||||
|
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
|
||||||
|
.idea/navEditor.xml
|
||||||
|
|
||||||
|
# Keystore files
|
||||||
|
# Uncomment the following lines if you do not want to check your keystore files in.
|
||||||
|
#*.jks
|
||||||
|
#*.keystore
|
||||||
|
|
||||||
|
# External native build folder generated in Android Studio 2.2 and later
|
||||||
|
.externalNativeBuild
|
||||||
|
.cxx/
|
||||||
|
|
||||||
|
# Google Services (e.g. APIs or Firebase)
|
||||||
|
# google-services.json
|
||||||
|
|
||||||
|
# Freeline
|
||||||
|
freeline.py
|
||||||
|
freeline/
|
||||||
|
freeline_project_description.json
|
||||||
|
|
||||||
|
# fastlane
|
||||||
|
fastlane/report.xml
|
||||||
|
fastlane/Preview.html
|
||||||
|
fastlane/screenshots
|
||||||
|
fastlane/test_output
|
||||||
|
fastlane/readme.md
|
||||||
|
|
||||||
|
# Version control
|
||||||
|
vcs.xml
|
||||||
|
|
||||||
|
# lint
|
||||||
|
lint/intermediates/
|
||||||
|
lint/generated/
|
||||||
|
lint/outputs/
|
||||||
|
lint/tmp/
|
||||||
|
# lint/reports/
|
||||||
|
|
||||||
|
# Android Profiling
|
||||||
|
*.hprof
|
||||||
|
|
||||||
|
# Cordova plugins for Capacitor
|
||||||
|
capacitor-cordova-android-plugins
|
||||||
|
|
||||||
|
# Copied web assets
|
||||||
|
app/src/main/assets/public
|
||||||
|
|
||||||
|
# Generated Config files
|
||||||
|
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
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/build/*
|
||||||
|
!/build/.npmkeep
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
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
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "kr.sblog.slimbudget"
|
||||||
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
|
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.
|
||||||
|
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
flatDir{
|
||||||
|
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||||
|
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
||||||
|
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
||||||
|
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
||||||
|
implementation project(':capacitor-android')
|
||||||
|
testImplementation "junit:junit:$junitVersion"
|
||||||
|
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
||||||
|
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
||||||
|
implementation project(':capacitor-cordova-android-plugins')
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: 'capacitor.build.gradle'
|
||||||
|
|
||||||
|
try {
|
||||||
|
def servicesJSON = file('google-services.json')
|
||||||
|
if (servicesJSON.text) {
|
||||||
|
apply plugin: 'com.google.gms.google-services'
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_21
|
||||||
|
targetCompatibility JavaVersion.VERSION_21
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (hasProperty('postBuildExtras')) {
|
||||||
|
postBuildExtras()
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.getcapacitor.myapp;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumented test, which will execute on an Android device.
|
||||||
|
*
|
||||||
|
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class ExampleInstrumentedTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void useAppContext() throws Exception {
|
||||||
|
// Context of the app under test.
|
||||||
|
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||||
|
|
||||||
|
assertEquals("com.getcapacitor.app", appContext.getPackageName());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/AppTheme">
|
||||||
|
<activity
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:label="@string/title_activity_main"
|
||||||
|
android:theme="@style/AppTheme.NoActionBarLaunch"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:windowSoftInputMode="adjustResize"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<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"
|
||||||
|
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.service.notification.NotificationListenerService" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
-->
|
||||||
|
</application>
|
||||||
|
|
||||||
|
<!-- Permissions -->
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
|
<!-- 영수증 OCR: 카메라 촬영 / 갤러리 선택 -->
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package kr.sblog.slimbudget;
|
||||||
|
|
||||||
|
import android.app.Notification;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.service.notification.NotificationListenerService;
|
||||||
|
import android.service.notification.StatusBarNotification;
|
||||||
|
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 카드사 결제 푸시 알림을 가로채 백엔드로 전달한다.
|
||||||
|
* - 결제로 보이는 알림(금액+승인/결제/카드)만 전송 (개인정보 최소화)
|
||||||
|
* - 인증 토큰은 Capacitor Preferences(SharedPreferences "CapacitorStorage")의 token 사용
|
||||||
|
* - 백엔드가 파싱·중복제거·미확인 내역 생성
|
||||||
|
*/
|
||||||
|
public class CardNotifListenerService extends NotificationListenerService {
|
||||||
|
|
||||||
|
private static final String API_URL = "https://app.sblog.kr/api/account/entries/notification";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNotificationPosted(StatusBarNotification sbn) {
|
||||||
|
try {
|
||||||
|
if (sbn == null || sbn.getNotification() == null) return;
|
||||||
|
Bundle ex = sbn.getNotification().extras;
|
||||||
|
if (ex == null) return;
|
||||||
|
|
||||||
|
CharSequence titleCs = ex.getCharSequence(Notification.EXTRA_TITLE);
|
||||||
|
CharSequence textCs = ex.getCharSequence(Notification.EXTRA_TEXT);
|
||||||
|
CharSequence bigCs = ex.getCharSequence(Notification.EXTRA_BIG_TEXT);
|
||||||
|
|
||||||
|
String title = titleCs == null ? "" : titleCs.toString();
|
||||||
|
String text = (bigCs != null ? bigCs : (textCs == null ? "" : textCs)).toString();
|
||||||
|
|
||||||
|
if (!looksLikePayment(title + " " + text)) return;
|
||||||
|
|
||||||
|
String token = getSharedPreferences("CapacitorStorage", MODE_PRIVATE)
|
||||||
|
.getString("token", null);
|
||||||
|
if (token == null || token.isEmpty()) return;
|
||||||
|
|
||||||
|
final String pkg = sbn.getPackageName();
|
||||||
|
new Thread(() -> post(pkg, title, text, token)).start();
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
// 알림 처리 실패는 무시 (앱 동작에 영향 없도록)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 결제/이체 알림 후보 판별: 금액(####원) + 승인/결제/출금/입금/이체/취소 키워드.
|
||||||
|
* '카드'는 광고에도 흔해 제외. 강한 신호 없이 광고성 표현이 있으면 제외.
|
||||||
|
* (1차 필터 — 최종 판별/분류는 백엔드 파서)
|
||||||
|
*/
|
||||||
|
private boolean looksLikePayment(String s) {
|
||||||
|
boolean money = s.matches("(?s).*\\d[\\d,]{2,}\\s*원.*");
|
||||||
|
boolean kw = s.contains("승인") || s.contains("결제") || s.contains("출금")
|
||||||
|
|| s.contains("입금") || s.contains("이체") || s.contains("송금");
|
||||||
|
if (!money || !kw) return false;
|
||||||
|
boolean strong = s.contains("승인") || s.contains("출금") || s.contains("입금")
|
||||||
|
|| s.contains("이체") || s.contains("송금") || s.contains("취소") || s.contains("환불");
|
||||||
|
boolean adLike = s.matches("(?s).*(이벤트|광고|혜택|쿠폰|할인|적립|포인트|캐시백|마일리지|리워드|당첨|응모|추첨|프로모션|배너|무료|증정|모집).*");
|
||||||
|
return strong || !adLike;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void post(String pkg, String title, String text, String token) {
|
||||||
|
HttpURLConnection conn = null;
|
||||||
|
try {
|
||||||
|
URL url = new URL(API_URL);
|
||||||
|
conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("POST");
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json");
|
||||||
|
conn.setRequestProperty("Authorization", "Bearer " + token);
|
||||||
|
conn.setConnectTimeout(8000);
|
||||||
|
conn.setReadTimeout(8000);
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
|
||||||
|
JSONObject body = new JSONObject();
|
||||||
|
body.put("app", pkg);
|
||||||
|
body.put("title", title);
|
||||||
|
body.put("text", text);
|
||||||
|
|
||||||
|
try (OutputStream os = conn.getOutputStream()) {
|
||||||
|
os.write(body.toString().getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
conn.getResponseCode(); // 요청 전송 (응답은 무시)
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
// 네트워크 실패는 무시
|
||||||
|
} finally {
|
||||||
|
if (conn != null) conn.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package kr.sblog.slimbudget;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.provider.Settings;
|
||||||
|
|
||||||
|
import com.getcapacitor.JSObject;
|
||||||
|
import com.getcapacitor.Plugin;
|
||||||
|
import com.getcapacitor.PluginCall;
|
||||||
|
import com.getcapacitor.PluginMethod;
|
||||||
|
import com.getcapacitor.annotation.CapacitorPlugin;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 카드 결제 알림 자동인식 플러그인.
|
||||||
|
* - isEnabled: 알림 접근 권한(NotificationListener) 허용 여부
|
||||||
|
* - openSettings: 알림 접근 설정 화면 열기
|
||||||
|
* 실제 알림 수신/전송은 CardNotifListenerService 가 담당한다.
|
||||||
|
*/
|
||||||
|
@CapacitorPlugin(name = "CardNotif")
|
||||||
|
public class CardNotifPlugin extends Plugin {
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
public void isEnabled(PluginCall call) {
|
||||||
|
String pkg = getContext().getPackageName();
|
||||||
|
String flat = Settings.Secure.getString(
|
||||||
|
getContext().getContentResolver(), "enabled_notification_listeners");
|
||||||
|
boolean enabled = flat != null && flat.contains(pkg);
|
||||||
|
JSObject ret = new JSObject();
|
||||||
|
ret.put("enabled", enabled);
|
||||||
|
call.resolve(ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
public void openSettings(PluginCall call) {
|
||||||
|
Intent intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS);
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
getContext().startActivity(intent);
|
||||||
|
call.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package kr.sblog.slimbudget;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.core.graphics.Insets;
|
||||||
|
import androidx.core.view.ViewCompat;
|
||||||
|
import androidx.core.view.WindowCompat;
|
||||||
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
|
|
||||||
|
import com.getcapacitor.BridgeActivity;
|
||||||
|
|
||||||
|
public class MainActivity extends BridgeActivity {
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
registerPlugin(CardNotifPlugin.class);
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
// targetSdk 35+ 의 에지투에지 강제 환경에서 콘텐츠가 상태바/소프트키 뒤로 깔리는 것을 방지.
|
||||||
|
// 구글 권장 패턴: 콘텐츠 루트에 시스템 바 인셋만큼 패딩을 적용하고 인셋을 소비(CONSUMED)한다.
|
||||||
|
// → 헤더는 상태바 아래, 하단 고정 내비는 소프트키 위에 위치.
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||||
|
final View content = findViewById(android.R.id.content);
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(content, (v, windowInsets) -> {
|
||||||
|
Insets bars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||||
|
Insets ime = windowInsets.getInsets(WindowInsetsCompat.Type.ime());
|
||||||
|
// 키보드(IME)가 올라오면 그 높이만큼 하단 패딩 → WebView가 키보드 위로 줄어든다.
|
||||||
|
// (입력 모달이 키보드에 가려지지 않고, 포커스된 입력칸이 보이도록 재배치됨)
|
||||||
|
int bottom = Math.max(bars.bottom, ime.bottom);
|
||||||
|
v.setPadding(bars.left, bars.top, bars.right, bottom);
|
||||||
|
return WindowInsetsCompat.CONSUMED;
|
||||||
|
});
|
||||||
|
ViewCompat.requestApplyInsets(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,34 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportHeight="108"
|
||||||
|
android:viewportWidth="108">
|
||||||
|
<path
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:strokeWidth="1">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:endX="78.5885"
|
||||||
|
android:endY="90.9159"
|
||||||
|
android:startX="48.7653"
|
||||||
|
android:startY="61.0927"
|
||||||
|
android:type="linear">
|
||||||
|
<item
|
||||||
|
android:color="#44000000"
|
||||||
|
android:offset="0.0" />
|
||||||
|
<item
|
||||||
|
android:color="#00000000"
|
||||||
|
android:offset="1.0" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:strokeWidth="1" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportHeight="108"
|
||||||
|
android:viewportWidth="108">
|
||||||
|
<path
|
||||||
|
android:fillColor="#26A69A"
|
||||||
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M9,0L9,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,0L19,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,0L29,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,0L39,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,0L49,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,0L59,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,0L69,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,0L79,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M89,0L89,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M99,0L99,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,9L108,9"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,19L108,19"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,29L108,29"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,39L108,39"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,49L108,49"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,59L108,59"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,69L108,69"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,79L108,79"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,89L108,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,99L108,99"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,29L89,29"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,39L89,39"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,49L89,49"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,59L89,59"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,69L89,69"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,79L89,79"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,19L29,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,19L39,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,19L49,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,19L59,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,19L69,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,19L79,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
</vector>
|
||||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".MainActivity">
|
||||||
|
|
||||||
|
<WebView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<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>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 299 B |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 160 B |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 211 B |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 425 B |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 834 B |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 36 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#FFFFFF</color>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<resources>
|
||||||
|
<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>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||||
|
<item name="windowActionBar">false</item>
|
||||||
|
<item name="windowNoTitle">true</item>
|
||||||
|
<item name="android:background">@null</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
||||||
|
<item name="android:background">@drawable/splash</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<external-path name="my_images" path="." />
|
||||||
|
<cache-path name="my_cache_images" path="." />
|
||||||
|
</paths>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.getcapacitor.myapp;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
*
|
||||||
|
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||||
|
*/
|
||||||
|
public class ExampleUnitTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void addition_isCorrect() throws Exception {
|
||||||
|
assertEquals(4, 2 + 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.android.tools.build:gradle:8.13.0'
|
||||||
|
classpath 'com.google.gms:google-services:4.4.4'
|
||||||
|
|
||||||
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
// in the individual module build.gradle files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "variables.gradle"
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task clean(type: Delete) {
|
||||||
|
delete rootProject.buildDir
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||||
|
include ':capacitor-android'
|
||||||
|
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
||||||
|
|
||||||
|
include ':capacitor-app'
|
||||||
|
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
|
||||||
|
|
||||||
|
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')
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Project-wide Gradle settings.
|
||||||
|
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
org.gradle.jvmargs=-Xmx1536m
|
||||||
|
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
|
# org.gradle.parallel=true
|
||||||
|
|
||||||
|
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||||
|
# Android operating system, and which are packaged with your app's APK
|
||||||
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
|
android.useAndroidX=true
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015-2021 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH="\\\"\\\""
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Collect all arguments for the java command:
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
|
# and any embedded shellness will be escaped.
|
||||||
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%"=="" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
include ':app'
|
||||||
|
include ':capacitor-cordova-android-plugins'
|
||||||
|
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
|
||||||
|
|
||||||
|
apply from: 'capacitor.settings.gradle'
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
ext {
|
||||||
|
minSdkVersion = 24
|
||||||
|
compileSdkVersion = 36
|
||||||
|
targetSdkVersion = 36
|
||||||
|
androidxActivityVersion = '1.11.0'
|
||||||
|
androidxAppCompatVersion = '1.7.1'
|
||||||
|
androidxCoordinatorLayoutVersion = '1.3.0'
|
||||||
|
androidxCoreVersion = '1.17.0'
|
||||||
|
androidxFragmentVersion = '1.8.9'
|
||||||
|
coreSplashScreenVersion = '1.2.0'
|
||||||
|
androidxWebkitVersion = '1.14.0'
|
||||||
|
junitVersion = '4.13.2'
|
||||||
|
androidxJunitVersion = '1.3.0'
|
||||||
|
androidxEspressoCoreVersion = '3.7.0'
|
||||||
|
cordovaAndroidVersion = '14.0.1'
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// 앱 아이콘은 이제 '돈돼지' 래스터 이미지(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')
|
||||||
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
|
||||||
|
<rect width="1024" height="1024" fill="#00bc7e"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 151 B |
|
After Width: | Height: | Size: 419 KiB |
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
|
||||||
|
<!-- 적응형 아이콘 전경: 안전영역(중앙 ~66%) 안에 흰 막대 차트 -->
|
||||||
|
<g fill="#ffffff">
|
||||||
|
<rect x="262" y="492" width="120" height="280" rx="60"/>
|
||||||
|
<rect x="452" y="372" width="120" height="400" rx="60"/>
|
||||||
|
<rect x="642" y="252" width="120" height="520" rx="60"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 400 B |
|
After Width: | Height: | Size: 542 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
|
||||||
|
<!-- 레거시(비적응형) 아이콘: 초록 라운드 사각 + 흰 막대 차트 (브랜드 마크) -->
|
||||||
|
<rect width="1024" height="1024" rx="224" fill="#00bc7e"/>
|
||||||
|
<g fill="#ffffff">
|
||||||
|
<rect x="240" y="544" width="108.8" height="256" rx="54.4"/>
|
||||||
|
<rect x="457.6" y="400" width="108.8" height="400" rx="54.4"/>
|
||||||
|
<rect x="675.2" y="256" width="108.8" height="544" rx="54.4"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 496 B |
|
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
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"appId": "kr.sblog.slimbudget",
|
||||||
|
"appName": "돈돼지 가계부",
|
||||||
|
"webDir": "dist",
|
||||||
|
"server": {
|
||||||
|
"cleartext": true
|
||||||
|
},
|
||||||
|
"android": {
|
||||||
|
"allowMixedContent": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# 프론트엔드 배포 (Nginx 정적 호스팅)
|
||||||
|
|
||||||
|
`main` 푸시 시 [.gitea/workflows/deploy.yaml](../.gitea/workflows/deploy.yaml) 가
|
||||||
|
`dist` 를 빌드해 서버 Nginx 웹 루트로 업로드하고 nginx 를 reload 합니다.
|
||||||
|
|
||||||
|
## 1. Gitea 저장소 시크릿 (Settings → Actions → Secrets)
|
||||||
|
| 시크릿 | 설명 | 예시 |
|
||||||
|
|---|---|---|
|
||||||
|
| `DEPLOY_HOST` | 서버 호스트/IP | `10.0.0.5` |
|
||||||
|
| `DEPLOY_USER` | SSH 계정 | `deploy` |
|
||||||
|
| `DEPLOY_SSH_KEY` | SSH 개인키(PEM 전체) | `-----BEGIN OPENSSH PRIVATE KEY-----...` |
|
||||||
|
| `DEPLOY_PATH` | Nginx 웹 루트 | `/var/www/sb-front` |
|
||||||
|
| `DEPLOY_PORT` | SSH 포트(선택) | `22` |
|
||||||
|
|
||||||
|
## 2. 서버 1회 준비
|
||||||
|
```bash
|
||||||
|
sudo apt-get install -y nginx
|
||||||
|
sudo mkdir -p /var/www/sb-front && sudo chown deploy:deploy /var/www/sb-front
|
||||||
|
|
||||||
|
# Nginx 서버블록 설치 (deploy/nginx-sb-front.conf 참고)
|
||||||
|
sudo cp deploy/nginx-sb-front.conf /etc/nginx/sites-available/sb-front
|
||||||
|
sudo ln -sf /etc/nginx/sites-available/sb-front /etc/nginx/sites-enabled/sb-front
|
||||||
|
sudo nginx -t && sudo systemctl reload nginx
|
||||||
|
|
||||||
|
# deploy 계정에 nginx 검증/reload 무비밀번호 sudo 허용
|
||||||
|
echo 'deploy ALL=(root) NOPASSWD: /usr/sbin/nginx -t, /usr/sbin/nginx -s reload' \
|
||||||
|
| sudo tee /etc/sudoers.d/sb-front
|
||||||
|
|
||||||
|
# 러너 공개키를 deploy 계정 authorized_keys 에 등록 (백엔드와 동일 키 사용 가능)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 동작
|
||||||
|
- main 푸시 → CI(빌드) + Deploy(빌드·업로드·reload) 가 각각 실행
|
||||||
|
- 업로드는 웹 루트를 비우고 새 `dist` 로 교체 (해시 자산명이라 안전)
|
||||||
|
- 수동 배포: 저장소 Actions → Deploy → Run workflow
|
||||||
|
|
||||||
|
## 참고
|
||||||
|
- API 는 같은 도메인의 `/api/` 로 Nginx 가 백엔드(8080)에 프록시합니다.
|
||||||
|
별도 도메인/포트로 띄운다면 빌드시 `VITE_API_BASE_URL` 환경변수를 지정하세요.
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# /etc/nginx/sites-available/sb-front (심볼릭 링크로 sites-enabled 에 연결)
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _; # 도메인으로 교체
|
||||||
|
|
||||||
|
root /var/www/sb-front; # DEPLOY_PATH 와 동일
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# SPA: 새로고침/직접 진입 시 index.html 로 폴백 (vue-router history 모드)
|
||||||
|
# 진입 HTML(루트·폴백)은 캐시 금지 — 배포로 청크 해시가 바뀌어도 항상 최신 진입점을 받도록.
|
||||||
|
# (루트 '/' 는 아래 '= /index.html' 매칭을 안 타므로 여기에 헤더를 둬야 함)
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
add_header Cache-Control "no-cache";
|
||||||
|
}
|
||||||
|
|
||||||
|
# index.html 은 캐시 금지 — 배포로 청크 해시가 바뀌어도 항상 최신 진입점을 받도록.
|
||||||
|
# (캐시되면 옛 index.html 이 삭제된 옛 청크를 불러 메뉴가 안 열리는 문제 발생)
|
||||||
|
location = /index.html {
|
||||||
|
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||||
|
expires -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 해시 자산 장기 캐시 (파일명에 해시가 있어 immutable 안전)
|
||||||
|
location /assets/ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# 설치파일 다운로드 — 배포(dist 교체)에 지워지지 않도록 root 밖의 영속 디렉터리에서 서빙.
|
||||||
|
# 서버에 /var/www/sb-downloads/ 를 만들고 설치파일(SlimBudget-Setup.exe)을 올려두세요.
|
||||||
|
# 예) sudo mkdir -p /var/www/sb-downloads && sudo cp SlimBudget-Setup.exe /var/www/sb-downloads/
|
||||||
|
location /download/ {
|
||||||
|
alias /var/www/sb-downloads/;
|
||||||
|
add_header Content-Disposition "attachment";
|
||||||
|
autoindex off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# API 프록시 → 백엔드(8080)
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://127.0.0.1:8080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# 안드로이드 하이브리드 앱 (Capacitor)
|
||||||
|
|
||||||
|
Vue `dist` 를 네이티브 WebView로 패키징해 Android 앱으로 빌드합니다.
|
||||||
|
|
||||||
|
## 구성
|
||||||
|
- `capacitor.config.json` — appId `kr.sblog.slimbudget`, appName `Slim Budget`, webDir `dist`
|
||||||
|
- `android/` — 네이티브 프로젝트(커밋됨, 빌드 산출물은 .gitignore)
|
||||||
|
- `.env.capacitor` — **앱 전용 환경변수** (`vite build --mode capacitor` 가 로드)
|
||||||
|
- `src/capacitor.js` — 하드웨어 뒤로가기 → 라우터 back, safe-area 클래스(`is-native`)
|
||||||
|
- npm 스크립트: `build:app`, `cap:sync`, `cap:android`
|
||||||
|
|
||||||
|
## ⚠️ 빌드 전 반드시 설정
|
||||||
|
**`.env.capacitor` 의 백엔드 절대 URL 을 운영 도메인으로 교체** (앱 안에선 `/api` 상대경로가 안 됨):
|
||||||
|
```
|
||||||
|
VITE_API_BASE_URL=https://yourdomain.com/api
|
||||||
|
```
|
||||||
|
백엔드는 이미 CORS에 Capacitor 오리진(`https://localhost`, `capacitor://localhost`, `http://localhost`)을 허용합니다. **백엔드는 HTTPS** 여야 합니다(안드로이드 평문 차단).
|
||||||
|
|
||||||
|
## 사전 준비 (PC)
|
||||||
|
- **Android Studio** + Android SDK + **JDK 17**
|
||||||
|
- 에뮬레이터 또는 실기기(USB 디버깅 on)
|
||||||
|
|
||||||
|
## 빌드 & 실행
|
||||||
|
```bash
|
||||||
|
# 1) 웹 자산 빌드 + 네이티브로 동기화
|
||||||
|
npm run cap:sync # = vite build --mode capacitor && cap sync
|
||||||
|
|
||||||
|
# 2) Android Studio 열기 → Run ▶ 으로 에뮬레이터/기기 실행
|
||||||
|
npm run cap:android # = cap open android
|
||||||
|
```
|
||||||
|
코드 변경 시마다 `npm run cap:sync` 후 Android Studio에서 다시 실행.
|
||||||
|
|
||||||
|
## (선택) 아이콘 · 스플래시
|
||||||
|
```bash
|
||||||
|
npm i -D @capacitor/assets
|
||||||
|
# 1024x1024 아이콘을 assets/icon.png 등으로 준비 후
|
||||||
|
npx capacitor-assets generate --android
|
||||||
|
```
|
||||||
|
|
||||||
|
## 배포 (Play Store)
|
||||||
|
1. Android Studio → Build → **Generate Signed Bundle/APK** → **Android App Bundle(.aab)**
|
||||||
|
2. **keystore 생성·보관**(분실 시 업데이트 불가) → 서명
|
||||||
|
3. **Google Play Console**(계정 1회 $25) → 앱 생성 → `.aab` 업로드 → 심사
|
||||||
|
|
||||||
|
## 자주 막히는 부분
|
||||||
|
- **네트워크 안 됨/빈 화면**: `.env.capacitor` 의 `VITE_API_BASE_URL` 이 실제 HTTPS 백엔드인지, 재빌드(`cap:sync`) 했는지
|
||||||
|
- **CORS 오류**: 백엔드가 앱 오리진 허용하는지(`CorsConfig`), HTTPS 인지
|
||||||
|
- **평문 차단**: 백엔드가 http면 안드로이드가 차단 → HTTPS 사용
|
||||||
|
- **로그인 유지**: 토큰은 WebView `localStorage` 에 저장되어 유지됨(정상)
|
||||||