Files

110 lines
7.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 돈돼지 가계부 — 백엔드 (sb_bt)
Spring Boot 기반의 가계부·자산·게시판 REST API 서버. (구 Slim Budget)
## 기술 스택
- **Spring Boot 3.5** / **Java 17**
- **MyBatis** (XML 매퍼) + **MariaDB**
- **Redis** (세션 저장 — DB 이중 저장으로 유실 대비)
- **spring-security-crypto** (BCrypt 비밀번호 해시)
- Lombok, Bean Validation
## 패키지 구조
```
com.sb.web
├── account # 가계부 (controller / service / mapper / domain / dto)
│ └ invest·budget·category·recurring·quick·ocr·fx·backup 포함
├── auth # 인증 (로그인/세션/구글로그인, AuthInterceptor, 포인트)
├── billing # 인앱결제·구독(Google Play Billing 검증, RTDN)
├── board # 게시판 (글/댓글/추천/공지/이미지)
├── user # 사용자
├── admin # 관리자 (기본분류·게시판/가입 설정·회원 관리)
└── common # 공통 (예외, 응답, 헬스체크, 설정)
```
## 스키마 자동 초기화
`spring.sql.init` 로 기동 시 적용. 기본 `mode: never`, 환경변수 **`SQL_INIT_MODE=always`** 로 켜면 실행(멱등 — `CREATE TABLE IF NOT EXISTS`, `ALTER TABLE ... ADD COLUMN IF NOT EXISTS`):
```
schema-locations: db/member.sql, db/board.sql, db/account.sql, db/dev-seed.sql
continue-on-error: true
```
주요 테이블:
- **회원/인증**: `member`, `auth_session`(세션 DB 백업), `point_history`, `app_setting`(가입 허용 등), `iap_purchase`(인앱결제)
- **가계부**: `wallet`, `account_entry`, `account_category`(`parent_id` 대/소분류), `account_tag`, `account_entry_tag`, `budget`, `budget_income`, `recurring`, `quick_entry`, `invest_holding`, `invest_trade`
- **게시판**: `post`, `comment`, `post_vote`, `comment_vote`, `post_tag`, `tag`, `tag_category`, `tag_category_board`, `board_image`, `report`
- **기타**: `default_category`(기본 분류 템플릿)
## 인증 / 보안
- 로그인/구글로그인 시 토큰 발급, 이후 `Authorization: Bearer <token>` 로 인증
- **구글 로그인**: `POST /api/auth/google` 로 ID 토큰 수신 → **tokeninfo 검증(aud=웹 클라이언트 ID)** 후 세션 발급. 검증된 동일 이메일이면 기존 계정에 `member.google_id` 로 연결
- `AuthInterceptor` 가 현재 사용자(`SessionUser`)를 요청 속성(`CURRENT_MEMBER`)으로 주입
- **세션 이중 저장**: Redis 유실·재시작 시 `auth_session`(DB)에서 복원·재수화
- **무차별 대입 방지**: 실패 시도만 카운트 — 로그인 IP당 10회/10분, 비밀번호 재인증 회원당 5회/10분 초과 시 429
- **봇 가입 차단**: 허니팟 + IP 레이트리밋(Redis)
- **모든 가계부 데이터는 `member_id`(소유자)로 격리** — 본인 데이터만 조회/수정
- DB/Redis 자격증명·`ACCOUNT_CRYPTO_KEY`(계좌번호 AES-256-GCM)·`GOOGLE_CLIENT_ID` 등은 `.env`(gitignore), `.env.example` 템플릿 제공
## REST API 개요
### 인증 · 사용자 · 관리자
- `/api/auth``login`·`signup`·`logout`·`google`·`me`(GET/DELETE) / `password`·`verify-password`·`profile`·`profile-image` / `points`·`point-history` / `signup-enabled`·`google-client-id`
- `/api/users` — 사용자 CRUD
- `/api/admin``signup-setting`(가입 허용 토글), `default-categories`(기본 분류), `members`(회원 목록·`role`·`plan`·`status` 변경·삭제), 태그 카테고리
- `/api/billing``products`·`subscription`·`google/verify`·`cancel`·`resume`·`restore`·`google/rtdn`(정기 알림)
- `/api/board` — 글/댓글 CRUD, `posts/{id}/vote`(추천), `notice`/`unnotice`(공지), `block`/`unblock`(열람제한), `my/posts`·`my/comments`, `tag-groups`, `recommenders`
- `/api/board/images` (업로드) · `/api/images/{id}` (공개 조회) — 게시판 이미지(DB 저장, 절대 URL 반환)
### 가계부 `/api/account`
| 메서드·경로 | 설명 |
|---|---|
| `GET/POST/PUT/DELETE /entries` | 내역 CRUD. 조회 필터: `year,month,type,category,walletId,keyword,tagId` |
| `POST /entries/notification` `POST /entries/parse` | 카드/은행 알림 자동인식 등록 · 자연어 파싱 |
| `GET /entries/pending/count` `POST /entries/{id}/confirm` | 확인 필요(pending) 건수 · 확정 |
| `GET /summary` `GET /stats` | 월 요약 / 기간 버킷(일·주·월·년) 수입·지출 |
| `GET /category-stats` | 분류별 합계 (파이차트, 대/소분류) |
| `GET/POST/PUT/DELETE /wallets` (+`/reorder`) | 계좌(BANK·CASH·CARD·LOAN·INVEST) |
| `GET /networth` `GET /networth/trend` | 순자산 / 순자산 추이(`unit=WEEK&weeks=N` 주간 지원) |
| `GET /wallets/{id}/entries` `POST /repayment` | 계좌별 내역 / 상환·납부(원금=이체·이자=지출·연회비=지출) |
| `GET/POST/PUT/DELETE /tags` (+`/reorder`) | 사용자별 가계부 태그 |
| `/categories` (+`/reorder`,`/import`) | 분류 CRUD, 순서변경, 기존 내역에서 가져오기 |
| `/budgets` (+`/income`,`/copy`,`/status`,`/period`) | 월별 예산 CRUD, 수입, 전월 복사, 진행률, 기간별 |
| `/recurrings` (+`/run`) | 고정지출 규칙, 밀린 회차 자동 생성 |
| `/quick-entries` | 자주 쓰는 내역 |
| `/invest/**` | 투자 포트폴리오(아래) |
| `/ocr/receipt` | 영수증 OCR (Google Vision) |
| `POST /restore` | 백업 데이터 복구 (엑셀·JSON) |
> 환율: `GET /api/fx/rate` — 외화 입력 시 환율 자동조회(원화 환산).
### 잔액 · 순자산 모델
- 부호 모델: 자산(+) / 부채(). 현금(CASH)은 은행(BANK)과 동일하게 자산 합산
- 지갑 잔액 = 개시잔액 + 수입 − 지출 − 이체출금 + 이체입금 (`WalletMapper.findBalances`)
- 순자산 = Σ자산 − Σ부채. 투자계좌는 **예수금 + 주식 평가금액**으로 반영
- 순자산 추이: 현금흐름 누적(이체는 상쇄) + 투자 손익은 현재 시점에 가산. 주간(월요일 기준) 집계 지원
### 고정지출 (recurring)
- 빈도(DAILY/WEEKLY/MONTHLY/YEARLY) + 일/요일/월 지정
- `last_run_date` 커서로 **중복 없이** 밀린 회차를 따라잡아 `account_entry` 생성. 자동 생성분은 **'확인 필요'** 상태로 만들어 실제 처리 확인 후 확정
### 투자 포트폴리오 (`/api/account/invest`) — 종목 단위
| 경로 | 설명 |
|---|---|
| `GET/POST/PUT/DELETE /holdings` | 보유 종목(종목명·코드·현재가). 집계: 수량·평단·평가금액·평가손익·수익률·실현손익 |
| `GET/POST /holdings/{id}/trades`, `PUT/DELETE /trades/{id}` | 매수/매도 이력 (수정 시 시점별 시뮬레이션으로 보유수량 음수 방지) |
| `POST /holdings/refresh-prices`, `POST /refresh-prices` | 종목코드로 현재가 자동 갱신(네이버 금융) |
- 매매이력(시간순)으로 **이동평균 평단**·실현손익·평가손익 산출. 매도수량이 보유수량 초과 시 400
- 투자계좌 가치 = 예수금(현금 ± 매매) + 주식평가, 순자산/추이에 연동
- 원화·정수 수량 기준 (해외주식 소수점/환율·자동 시세는 범위 외)
- ℹ️ 백엔드는 종목 단위 API를 유지하나, **현재 프론트 UI 는 투자금+평가액만 입력하는 단순형**으로 노출
## 개발/실행
```bash
cp .env.example .env # DB/Redis 자격증명 입력 (SQL_INIT_MODE=always 로 스키마 자동생성)
./gradlew bootRun # http://localhost:8080
./gradlew test # 단위 + @WebMvcTest 통합 테스트
```
## 배포
- `main` 푸시 → Gitea Actions 가 CI(빌드·테스트) + Deploy(jar 전송·systemd 재시작) 실행. 테스트 실패 시 배포 중단
- 상세: [deploy/README.md](../deploy/README.md)