From c5e4c2dad7e95158ac1a5e496ac521173b693bf0 Mon Sep 17 00:00:00 2001 From: ByungCheol Date: Sat, 6 Jun 2026 00:16:50 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=9B=B9=20=EC=95=88=EB=82=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80(=EC=95=B1=EC=A0=84=EC=9A=A9)=C2=B7=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EA=B0=80=EC=9E=85=20=EC=A0=9C=ED=95=9C/=EB=B4=87?= =?UTF-8?q?=EC=B0=A8=EB=8B=A8=C2=B7=EC=B9=B4=EB=93=9C=20=EC=9D=B8=EC=8B=9D?= =?UTF-8?q?=20=EB=B3=B4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 웹(브라우저)은 안내 페이지만, 실제 이용은 앱(Capacitor). 개발모드는 예외 - 회원가입: 약관동의, 관리자 제한 토글, 진입부터 차단, 허니팟 - 카드 알림: pending 건 계좌종류 '카드' 기본선택(현금 X), 확인실패 시 HTTP 상태 표시 - 네이티브 알림필터: '카드' 키워드 제거+광고성 표현 차단 - docs/release-2026-06-06.md 정리 Co-Authored-By: Claude Opus 4.8 --- .../slimbudget/CardNotifListenerService.java | 13 +- docs/release-2026-06-06.md | 46 +++++++ src/App.vue | 42 +++++-- src/api/adminApi.js | 8 ++ src/api/authApi.js | 3 + src/components/LoginModal.vue | 7 +- src/components/SignupModal.vue | 43 ++++++- src/components/WebOnlyNotice.vue | 119 ++++++++++++++++++ src/stores/ui.js | 16 ++- src/views/HomeView.vue | 4 +- src/views/UsersView.vue | 103 ++++++++++++++- src/views/account/AccountView.vue | 9 +- 12 files changed, 386 insertions(+), 27 deletions(-) create mode 100644 docs/release-2026-06-06.md create mode 100644 src/components/WebOnlyNotice.vue diff --git a/android/app/src/main/java/kr/sblog/slimbudget/CardNotifListenerService.java b/android/app/src/main/java/kr/sblog/slimbudget/CardNotifListenerService.java index 41bcdce..3e67010 100644 --- a/android/app/src/main/java/kr/sblog/slimbudget/CardNotifListenerService.java +++ b/android/app/src/main/java/kr/sblog/slimbudget/CardNotifListenerService.java @@ -49,11 +49,18 @@ public class CardNotifListenerService extends NotificationListenerService { } } - /** 결제 알림 후보 판별: 금액(####원) + 승인/결제/카드 키워드 */ + /** + * 결제 알림 후보 판별: 금액(####원) + 결제/승인/출금 키워드. + * '카드'는 광고에도 흔해 제외. 강한 승인신호(승인/출금/취소/환불) 없이 광고성 표현이 있으면 제외. + * (1차 필터 — 최종 판별은 백엔드 파서) + */ private boolean looksLikePayment(String s) { boolean money = s.matches("(?s).*\\d[\\d,]{2,}\\s*원.*"); - boolean kw = s.contains("승인") || s.contains("결제") || s.contains("카드"); - return money && kw; + boolean kw = s.contains("승인") || s.contains("결제") || s.contains("출금"); + if (!money || !kw) return false; + boolean strong = 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) { diff --git a/docs/release-2026-06-06.md b/docs/release-2026-06-06.md new file mode 100644 index 0000000..8f141cb --- /dev/null +++ b/docs/release-2026-06-06.md @@ -0,0 +1,46 @@ +# Slim Budget — 변경 내역 (2026-06-06 배포) + +이번 작업 세션에서 추가·수정한 기능 정리. (프론트 `sb_pt`, 백엔드 `sb_bt`, 안드로이드 앱 포함) + +--- + +## 1. 홈 대시보드 +- **일별 수입·지출 캘린더** 추가 (예산 대비 지출 아래). 날짜에 마우스 오버/탭 시 그 날의 내역 목록 표시. + +## 2. 투자 / 계좌 +- **시세 자동조회**: 종목코드(6자리)로 네이버 금융에서 현재가 자동 갱신. 투자 탭 진입·포트폴리오 펼침 시 자동, "↻ 시세 갱신" 버튼도 제공. +- **퇴직연금·연금 등 평가액 직접입력형 계좌**: 종목 없이 현재 평가액만 입력해 관리(누적 적립원금 대비 손익% 표시). +- 보유 종목 표기 2줄 정리(수량 / 평단·현재가). +- **계좌번호 암호화**: DB에 AES-256-GCM으로 저장(키는 서버 `.env` `ACCOUNT_CRYPTO_KEY`). 화면에서는 **끝 4자리만 노출 + 👁 전체보기 토글**로 마스킹. + +## 3. 가계부 내역 / 고정 지출 +- "정기 거래" → **"고정 지출"** 명칭 변경, 월간/년간 분리·카테고리 소계/합계. +- 내역·고정지출 추가 시 계좌 종류 **라디오 + 셀렉트** 패턴, 저장 버튼 sticky. + +## 4. 카드 결제 알림 자동인식 (버그 수정) +- **현금으로 잘못 선택되던 문제**: 알림 자동인식(pending) 건은 매칭 실패해도 계좌 종류를 **'카드'로 기본 선택**. 백엔드 카드 매칭을 양방향 부분일치(`KB국민↔국민`) + 카드가 1개뿐이면 자동 매칭으로 개선. +- **광고 푸시 오인식 차단**: 강한 승인신호(승인/출금/취소/환불) 없이 광고성 표현(이벤트·할인·적립·포인트·캐시백·쿠폰 등)이 있으면 결제로 보지 않음. 네이티브 1차 필터에서 '카드' 키워드 제거. +- 확인(확정) 실패 시 **HTTP 상태코드 노출** + 즉시 UI 반영. + +## 5. 게시판 +- "자유게시판" → **커뮤니티 / 짠테크 수다방 / 재테크 팁** 3개 게시판으로 분리(카테고리). +- 사이드바를 **가계부 / 게시판 / 관리자 영역 구분선**으로 정리. +- 글 번호 컬럼 미노출. + +## 6. 회원가입 / 인증 +- **이용약관·개인정보 수집 동의**(필수 체크) 추가. +- **관리자 회원가입 제한 토글**: 회원 관리 화면에서 신규 가입 on/off. 제한 시 가입 진입(랜딩·로그인·가입 팝업)부터 차단. +- **봇 가입 차단**: 허니팟(숨김 필드) + IP 레이트리밋(Redis, 1시간 5회). *Cloudflare Turnstile은 앱 환경 제약으로 제외.* +- **모바일 웹 새로고침 시 로그아웃 버그 수정**: 세션 복원을 localStorage 우선으로(Preferences 예외에 견고). +- **세션 유실(주기적 로그아웃) 해결**: Redis AOF 활성화로 재시작에도 세션 영속(서버 설정). + +## 7. 웹 접근 정책 +- **PC·모바일 웹은 안내 페이지만** 노출(앱 다운로드 안내). 실제 이용은 **앱(Capacitor)에서만**. 개발 모드(`npm run dev`)는 예외로 전체 앱 표시. + +--- + +## 운영 반영 시 체크 +- 서버 `/opt/sb-backend/.env` + - `ACCOUNT_CRYPTO_KEY` — 계좌번호 암호화를 켤 때만(미설정 시 평문, 깨지지 않음). +- `app_setting` 테이블 — 배포 시 `member.sql` 초기화로 자동 생성(회원가입 허용 플래그). +- Redis `appendonly yes` — 세션 영속(이미 적용). diff --git a/src/App.vue b/src/App.vue index 533575f..f692043 100644 --- a/src/App.vue +++ b/src/App.vue @@ -7,6 +7,8 @@ import AppFooter from '@/components/layout/AppFooter.vue' import LoginModal from '@/components/LoginModal.vue' import SignupModal from '@/components/SignupModal.vue' import ChangePasswordModal from '@/components/ChangePasswordModal.vue' +import WebOnlyNotice from '@/components/WebOnlyNotice.vue' +import { Capacitor } from '@capacitor/core' import { useAuthStore } from '@/stores/auth' import { useUiStore } from '@/stores/ui' @@ -14,12 +16,20 @@ const auth = useAuthStore() const ui = useUiStore() const route = useRoute() +// 앱(Capacitor 네이티브)에서만 전체 이용. 웹(브라우저)은 안내 페이지만 노출. +// 단, 개발 모드(npm run dev)에서는 브라우저로도 전체 앱을 테스트할 수 있게 예외. +const isApp = Capacitor.isNativePlatform() || import.meta.env.DEV + // http.js 의 401 처리에서 발생시키는 이벤트 → 세션 정리 후 로그인 팝업 function onUnauthorized() { auth.clear() ui.openLogin() } -onMounted(() => window.addEventListener('auth:unauthorized', onUnauthorized)) +onMounted(() => { + if (!isApp) return // 웹은 안내만 — 앱 전용 초기화 생략 + window.addEventListener('auth:unauthorized', onUnauthorized) + ui.loadSignupEnabled() // 회원가입 허용 여부 선로딩(랜딩/로그인 가입 버튼에 반영) +}) onUnmounted(() => window.removeEventListener('auth:unauthorized', onUnauthorized)) // 페이지 이동 시 모바일 사이드바 자동 닫힘 @@ -27,19 +37,25 @@ watch(() => route.fullPath, () => ui.closeSidebar()) diff --git a/src/stores/ui.js b/src/stores/ui.js index f78aa18..0f486b2 100644 --- a/src/stores/ui.js +++ b/src/stores/ui.js @@ -1,5 +1,6 @@ import { defineStore } from 'pinia' import { ref } from 'vue' +import { authApi } from '@/api/authApi' // 전역 UI 상태 (로그인/회원가입 레이어 팝업 등) export const useUiStore = defineStore('ui', () => { @@ -7,6 +8,17 @@ export const useUiStore = defineStore('ui', () => { const signupOpen = ref(false) const redirectPath = ref('') + // 회원가입 허용 여부(관리자 설정). 비로그인도 조회 가능한 공개 API. + const signupEnabled = ref(true) + async function loadSignupEnabled() { + try { + const res = await authApi.signupEnabled() + signupEnabled.value = !!res.enabled + } catch { + signupEnabled.value = true // 조회 실패 시 기본 허용(서버가 최종 검증) + } + } + // 모바일 사이드바 드로어 const sidebarOpen = ref(false) function toggleSidebar() { @@ -36,10 +48,11 @@ export const useUiStore = defineStore('ui', () => { redirectPath.value = '' } - // 회원가입 팝업 (로그인 팝업은 닫고 전환) + // 회원가입 팝업 (로그인 팝업은 닫고 전환). 열 때 최신 허용 여부 갱신. function openSignup() { loginOpen.value = false signupOpen.value = true + loadSignupEnabled() } function closeSignup() { signupOpen.value = false @@ -47,6 +60,7 @@ export const useUiStore = defineStore('ui', () => { return { loginOpen, signupOpen, redirectPath, openLogin, closeLogin, openSignup, closeSignup, + signupEnabled, loadSignupEnabled, sidebarOpen, toggleSidebar, closeSidebar, passwordOpen, openPassword, closePassword, } diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 05abe01..fe3daa5 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -278,9 +278,9 @@ onMounted(load)

슬림하게 관리하는 가계부 · 자산 · 예산

- +
-

로그인하면 나의 가계부 요약을 볼 수 있어요.

+

{{ ui.signupEnabled ? '로그인하면 나의 가계부 요약을 볼 수 있어요.' : '현재 회원가입이 제한되어 있습니다.' }}