diff --git a/docs/BACKEND.md b/docs/BACKEND.md index 8d56309..fe2be36 100644 --- a/docs/BACKEND.md +++ b/docs/BACKEND.md @@ -1,11 +1,11 @@ -# Slim Budget — 백엔드 (sb_bt) +# 돈돼지 가계부 — 백엔드 (sb_bt) -Spring Boot 기반의 가계부·게시판 REST API 서버. +Spring Boot 기반의 가계부·자산·게시판 REST API 서버. (구 Slim Budget) ## 기술 스택 - **Spring Boot 3.5** / **Java 17** - **MyBatis** (XML 매퍼) + **MariaDB** -- **Redis** (세션 저장) +- **Redis** (세션 저장 — DB 이중 저장으로 유실 대비) - **spring-security-crypto** (BCrypt 비밀번호 해시) - Lombok, Bean Validation @@ -13,74 +13,97 @@ Spring Boot 기반의 가계부·게시판 REST API 서버. ``` com.sb.web ├── account # 가계부 (controller / service / mapper / domain / dto) -├── auth # 인증 (로그인/세션, AuthInterceptor) -├── board # 게시판 +│ └ invest·budget·category·recurring·quick·ocr·fx·backup 포함 +├── auth # 인증 (로그인/세션/구글로그인, AuthInterceptor, 포인트) +├── billing # 인앱결제·구독(Google Play Billing 검증, RTDN) +├── board # 게시판 (글/댓글/추천/공지/이미지) ├── user # 사용자 -├── admin # 관리자 (태그 카테고리, 게시판 설정) -└── common # 공통 (예외, 응답, 설정) +├── admin # 관리자 (기본분류·게시판/가입 설정·회원 관리) +└── common # 공통 (예외, 응답, 헬스체크, 설정) ``` ## 스키마 자동 초기화 -`spring.sql.init.schema-locations` 로 기동 시 적용 (멱등 — `CREATE TABLE IF NOT EXISTS`, `ALTER TABLE ... ADD COLUMN IF NOT EXISTS`): +`spring.sql.init` 로 기동 시 적용. 기본 `mode: never`, 환경변수 **`SQL_INIT_MODE=always`** 로 켜면 실행(멱등 — `CREATE TABLE IF NOT EXISTS`, `ALTER TABLE ... ADD COLUMN IF NOT EXISTS`): ``` -classpath:db/member.sql, db/board.sql, db/account.sql, db/dev-seed.sql +schema-locations: db/member.sql, db/board.sql, db/account.sql, db/dev-seed.sql +continue-on-error: true ``` -주요 테이블: `member`, `wallet`, `account_entry`, `account_tag`, `account_entry_tag`, -`account_category`, `budget`, `recurring`, `invest_holding`, `invest_trade`, -`post`, `comment`, `tag`, `tag_category` 등. +주요 테이블: +- **회원/인증**: `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 ` 로 인증 +- 로그인/구글로그인 시 토큰 발급, 이후 `Authorization: Bearer ` 로 인증 +- **구글 로그인**: `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 자격증명 등은 `.env`(gitignore)로 관리, `.env.example` 템플릿 제공 +- DB/Redis 자격증명·`ACCOUNT_CRYPTO_KEY`(계좌번호 AES-256-GCM)·`GOOGLE_CLIENT_ID` 등은 `.env`(gitignore), `.env.example` 템플릿 제공 ## REST API 개요 ### 인증 · 사용자 · 관리자 -- `POST /api/auth/login` 외 인증 엔드포인트 -- `/api/users` — 사용자 관리 -- `/api/admin/**` — 태그 카테고리·게시판 설정(관리자) -- `/api/board/**` — 게시판(글/댓글/태그/열람제한) +- `/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` | 계좌/카드/대출/투자(BANK·CARD·LOAN·INVEST) | -| `GET /networth` `GET /networth/trend` | 순자산 / 월별 순자산 추이 | -| `GET /wallets/{id}/entries` `POST /repayment` | 계좌별 내역 / 상환·납부(원금=이체, 이자=지출) | -| `GET/POST/PUT/DELETE /tags` | 사용자별 가계부 태그 | -| `/api/account/categories` (+`/import`) | 분류 CRUD, 기존 내역에서 가져오기 | -| `/api/account/budgets` (+`/status`,`/period`) | 예산 CRUD, 진행률, 기간별 | -| `/api/account/recurrings` (+`/run`) | 정기 거래 규칙, 밀린 회차 자동 생성 | -| `/api/account/invest/**` | 투자 포트폴리오(아래) | +| `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) +### 고정지출 (recurring) - 빈도(DAILY/WEEKLY/MONTHLY/YEARLY) + 일/요일/월 지정 -- `last_run_date` 커서로 **중복 없이** 밀린 회차를 따라잡아 `account_entry` 생성 +- `last_run_date` 커서로 **중복 없이** 밀린 회차를 따라잡아 `account_entry` 생성. 자동 생성분은 **'확인 필요'** 상태로 만들어 실제 처리 확인 후 확정 -### 투자 포트폴리오 (`/api/account/invest`) — 종목 단위(C) +### 투자 포트폴리오 (`/api/account/invest`) — 종목 단위 | 경로 | 설명 | |---|---| -| `GET/POST/PUT/DELETE /holdings` | 보유 종목 (종목명·코드·현재가). 집계: 수량·평단·평가금액·평가손익·수익률·실현손익 | -| `GET/POST /holdings/{id}/trades`, `DELETE /trades/{id}` | 매수/매도 이력 | -- 매매이력(시간순)으로 **이동평균 평단**·실현손익·평가손익 산출 -- 매수 시 평단 재계산 `(기존수량×평단 + 매수수량×단가 + 수수료)/총수량`, 매도 시 실현손익 = (매도가 − 평단)×수량 − 수수료 -- 매도수량이 보유수량 초과 시 400 반환 +| `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 자격증명 입력 +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)