package com.sb.web.account.controller; import com.sb.web.account.dto.AccountEntryRequest; import com.sb.web.account.dto.AccountEntryResponse; import com.sb.web.account.dto.AccountSummary; import com.sb.web.account.dto.ParsedEntryResponse; import com.sb.web.account.dto.AccountTagRequest; import com.sb.web.account.dto.AccountTagResponse; import com.sb.web.account.dto.CategoryStat; import com.sb.web.account.dto.ConfirmRequest; import com.sb.web.account.dto.NotificationRequest; import com.sb.web.account.dto.TagReorderRequest; import com.sb.web.account.dto.NetWorthPoint; import com.sb.web.account.dto.NetWorthResponse; import com.sb.web.account.dto.PeriodStat; import com.sb.web.account.dto.RepaymentRequest; import com.sb.web.account.dto.WalletReorderRequest; import com.sb.web.account.dto.WalletRequest; import com.sb.web.account.dto.WalletResponse; import com.sb.web.account.service.AccountService; import com.sb.web.auth.dto.SessionUser; import com.sb.web.auth.web.AuthInterceptor; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.Map; /** * 가계부 API. (/api/account/** — 로그인 필요, 본인 데이터만 접근) * GET /entries?year=&month= 내역 목록(본인) * GET /summary?year=&month= 요약(수입/지출/잔액) * POST /entries 등록 * PUT /entries/{id} 수정(본인) * DELETE /entries/{id} 삭제(본인) */ @RestController @RequestMapping("/api/account") @RequiredArgsConstructor public class AccountController { private final AccountService accountService; /* ===== 계좌/카드 (사용자별) ===== */ @GetMapping("/wallets") public List wallets( @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { return accountService.listWallets(current.getId()); } @PostMapping("/wallets") public ResponseEntity createWallet( @Valid @RequestBody WalletRequest req, @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { return ResponseEntity.status(HttpStatus.CREATED).body(accountService.createWallet(req, current.getId())); } @PutMapping("/wallets/{id}") public WalletResponse updateWallet( @PathVariable Long id, @Valid @RequestBody WalletRequest req, @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { return accountService.updateWallet(id, req, current.getId()); } @DeleteMapping("/wallets/{id}") public ResponseEntity deleteWallet( @PathVariable Long id, @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { accountService.deleteWallet(id, current.getId()); return ResponseEntity.noContent().build(); } /** 계좌/카드 순서 변경 (드래그앤드랍) */ @PutMapping("/wallets/reorder") public List reorderWallets( @RequestBody WalletReorderRequest req, @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { return accountService.reorderWallets(current.getId(), req.getType(), req.getIds()); } @GetMapping("/networth") public NetWorthResponse netWorth( @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { return accountService.netWorth(current.getId()); } /** 특정 계좌의 연관 내역 */ @GetMapping("/wallets/{id}/entries") public List walletEntries( @PathVariable Long id, @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { return accountService.listEntriesByWallet(id, current.getId()); } /** 대출/카드 상환·납부 (원금=이체, 이자=지출 자동 분리) */ @PostMapping("/repayment") public ResponseEntity repay( @Valid @RequestBody RepaymentRequest req, @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { accountService.repay(req, current.getId()); return ResponseEntity.status(HttpStatus.CREATED).build(); } /* ===== 가계부 태그 (사용자별) ===== */ @GetMapping("/tags") public List tags( @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { return accountService.listTags(current.getId()); } @PostMapping("/tags") public ResponseEntity createTag( @Valid @RequestBody AccountTagRequest req, @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { return ResponseEntity.status(HttpStatus.CREATED).body(accountService.createTag(req, current.getId())); } @PutMapping("/tags/{id}") public AccountTagResponse updateTag( @PathVariable Long id, @Valid @RequestBody AccountTagRequest req, @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { return accountService.updateTag(id, req, current.getId()); } @DeleteMapping("/tags/{id}") public ResponseEntity deleteTag( @PathVariable Long id, @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { accountService.deleteTag(id, current.getId()); return ResponseEntity.noContent().build(); } /** 태그 순서 변경 */ @PutMapping("/tags/reorder") public List reorderTags( @RequestBody TagReorderRequest req, @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { return accountService.reorderTags(current.getId(), req.getIds()); } @GetMapping("/entries") public List list( @RequestParam(required = false) Integer year, @RequestParam(required = false) Integer month, @RequestParam(required = false) String type, @RequestParam(required = false) String category, @RequestParam(required = false) Long walletId, @RequestParam(required = false) String keyword, @RequestParam(required = false) Long tagId, @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { return accountService.list(current.getId(), year, month, type, category, walletId, keyword, tagId); } /** 분류(카테고리)별 합계 — 파이차트용 */ @GetMapping("/category-stats") public List categoryStats( @RequestParam(defaultValue = "EXPENSE") String type, @RequestParam(required = false) Integer year, @RequestParam(required = false) Integer month, @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { return accountService.categoryStats(current.getId(), type, year, month); } /** 순자산 추이 (unit=WEEK 면 주별, weeks 개수 / 그 외 월별, months 개수) */ @GetMapping("/networth/trend") public List netWorthTrend( @RequestParam(required = false) Integer months, @RequestParam(required = false) Integer weeks, @RequestParam(defaultValue = "MONTH") String unit, @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { if ("WEEK".equalsIgnoreCase(unit)) { return accountService.netWorthTrendWeekly(current.getId(), weeks); } return accountService.netWorthTrend(current.getId(), months); } @GetMapping("/stats") public List stats( @RequestParam(defaultValue = "MONTH") String unit, @RequestParam(required = false) Integer year, @RequestParam(required = false) Integer month, @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { return accountService.stats(current.getId(), unit, year, month); } @GetMapping("/summary") public AccountSummary summary( @RequestParam(required = false) Integer year, @RequestParam(required = false) Integer month, @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { return accountService.summary(current.getId(), year, month); } @PostMapping("/entries") public ResponseEntity create( @Valid @RequestBody AccountEntryRequest req, @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { return ResponseEntity.status(HttpStatus.CREATED).body(accountService.create(req, current.getId())); } @PutMapping("/entries/{id}") public AccountEntryResponse update( @PathVariable Long id, @Valid @RequestBody AccountEntryRequest req, @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { return accountService.update(id, req, current.getId()); } @DeleteMapping("/entries/{id}") public ResponseEntity delete( @PathVariable Long id, @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { accountService.delete(id, current.getId()); return ResponseEntity.noContent().build(); } /* ===== 카드 결제 알림 자동인식 ===== */ /** 미확인(확인 필요) 내역 개수 */ @GetMapping("/entries/pending/count") public Map pendingCount( @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { return Map.of("count", accountService.countPending(current.getId())); } /** 카드 결제 푸시 알림 → 미확인 지출 내역 생성 (네이티브 알림 리스너가 호출). 생성 안 되면 204 */ @PostMapping("/entries/notification") public ResponseEntity fromNotification( @RequestBody NotificationRequest req, @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { AccountEntryResponse created = accountService.createFromNotification(req, current.getId()); if (created == null) { return ResponseEntity.noContent().build(); } return ResponseEntity.status(HttpStatus.CREATED).body(created); } /** 문자/푸시 텍스트 파싱(저장 안 함) → 추가 폼 자동채움. */ @PostMapping("/entries/parse") public ParsedEntryResponse parseText( @RequestBody NotificationRequest req, @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { return accountService.parseText(req); } /** 미확인 내역 확정 (분류/계좌 보정) */ @PostMapping("/entries/{id}/confirm") public AccountEntryResponse confirm( @PathVariable Long id, @RequestBody(required = false) ConfirmRequest req, @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { return accountService.confirm(id, req, current.getId()); } }