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())
-
슬림하게 관리하는 가계부 · 자산 · 예산
로그인하면 나의 가계부 요약을 볼 수 있어요.
+{{ ui.signupEnabled ? '로그인하면 나의 가계부 요약을 볼 수 있어요.' : '현재 회원가입이 제한되어 있습니다.' }}
가입한 회원의 역할·상태를 관리합니다. (관리자 전용)
+ +{{ error }}
불러오는 중...
@@ -171,6 +212,66 @@ h1 { opacity: 0.7; margin: 0.5rem 0 1rem; } +.signup-toggle { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.8rem 1rem; + margin-bottom: 1rem; + border: 1px solid var(--color-border); + border-radius: 10px; + background: var(--color-background-soft); +} +.signup-toggle.off { + border-color: #c0392b; + background: rgba(192, 57, 43, 0.06); +} +.st-text { + display: flex; + flex-direction: column; + gap: 0.15rem; + min-width: 0; +} +.st-text strong { + font-size: 0.95rem; +} +.st-desc { + font-size: 0.8rem; + opacity: 0.7; +} +.switch { + flex-shrink: 0; + width: 48px; + height: 28px; + border: 0; + border-radius: 999px; + background: #b0b0b0; + position: relative; + cursor: pointer; + transition: background 0.18s; + padding: 0; +} +.switch.on { + background: hsla(160, 100%, 37%, 1); +} +.switch:disabled { + opacity: 0.6; + cursor: default; +} +.switch .knob { + position: absolute; + top: 3px; + left: 3px; + width: 22px; + height: 22px; + border-radius: 50%; + background: #fff; + transition: transform 0.18s; +} +.switch.on .knob { + transform: translateX(20px); +} .member-table { width: 100%; border-collapse: collapse; diff --git a/src/views/account/AccountView.vue b/src/views/account/AccountView.vue index 0be2a7a..6cf983f 100644 --- a/src/views/account/AccountView.vue +++ b/src/views/account/AccountView.vue @@ -39,10 +39,12 @@ async function loadPendingCount() { async function confirmRow(e) { try { await accountApi.confirmEntry(e.id, {}) + e.pending = false // UI 즉시 반영(목록 재로딩 전) await load() await loadPendingCount() } catch (err) { - alert(err.response?.data?.message || '확인 처리에 실패했습니다.') + const st = err.response?.status + alert(err.response?.data?.message || `확인 처리에 실패했습니다.${st ? ' (HTTP ' + st + ')' : ' (네트워크 오류)'}`) } } @@ -378,7 +380,10 @@ function openEdit(e) { category: e.category || '', amount: e.amount, memo: e.memo || '', - walletKind: e.walletId ? walletKindOf(e.walletId) : (e.type === 'TRANSFER' ? '' : 'CASH'), + // 알림 자동인식(pending) 건은 카드 결제이므로 매칭 실패해도 '카드'로 기본 선택(현금 X) + walletKind: e.walletId + ? walletKindOf(e.walletId) + : (e.pending ? 'CARD' : (e.type === 'TRANSFER' ? '' : 'CASH')), walletId: e.walletId || '', toWalletKind: walletKindOf(e.toWalletId), toWalletId: e.toWalletId || '',