Compare commits

..

59 Commits

Author SHA1 Message Date
ByungCheol 204c5ce430 Merge branch 'dev'
Deploy / deploy (push) Failing after 13m50s
2026-06-28 18:25:50 +09:00
ByungCheol 706deb057e Merge branch 'dev'
Deploy / deploy (push) Successful in 53s
2026-06-28 18:16:58 +09:00
ByungCheol e5d1b5d40c Merge branch 'dev'
Deploy / deploy (push) Failing after 14m27s
2026-06-28 18:05:13 +09:00
ByungCheol d028d957b6 Merge branch 'dev'
Deploy / deploy (push) Failing after 15m25s
2026-06-28 17:29:17 +09:00
ByungCheol f28ecd853e Merge branch 'dev'
Deploy / deploy (push) Failing after 13m16s
2026-06-28 16:56:25 +09:00
ByungCheol 5fec464f90 Merge branch 'dev'
Deploy / deploy (push) Failing after 15m19s
2026-06-28 16:04:22 +09:00
ByungCheol 483aaa6473 Merge branch 'dev'
Deploy / deploy (push) Failing after 15m15s
2026-06-28 15:44:26 +09:00
ByungCheol 5bd2616de9 Merge branch 'dev'
Deploy / deploy (push) Failing after 15m28s
2026-06-28 15:24:13 +09:00
ByungCheol ad40af18b2 Merge branch 'dev'
Deploy / deploy (push) Failing after 11m55s
2026-06-28 15:12:47 +09:00
ByungCheol 95d84c8138 Merge branch 'dev'
Deploy / deploy (push) Failing after 15m14s
2026-06-28 14:44:25 +09:00
ByungCheol 08338d516f Merge branch 'dev'
Deploy / deploy (push) Failing after 14m43s
2026-06-28 14:05:01 +09:00
ByungCheol dc4e99a6db Merge branch 'dev'
Deploy / deploy (push) Failing after 15m22s
2026-06-28 13:34:21 +09:00
ByungCheol f408715e17 Merge branch 'dev'
Deploy / deploy (push) Failing after 13m19s
2026-06-28 13:21:21 +09:00
ByungCheol 74a2cae9ef Merge branch 'dev'
Deploy / deploy (push) Failing after 15m11s
2026-06-28 12:29:27 +09:00
ByungCheol 6e269888c2 Merge branch 'dev'
Deploy / deploy (push) Failing after 11m49s
2026-06-28 11:07:51 +09:00
ByungCheol 39a11d299c Merge branch 'dev'
Deploy / deploy (push) Failing after 11m53s
2026-06-28 09:22:46 +09:00
ByungCheol f19f653715 Merge branch 'dev'
Deploy / deploy (push) Failing after 13m31s
CI / build (push) Failing after 15m20s
2026-06-28 09:04:22 +09:00
ByungCheol 16ddf88de3 Merge branch 'dev'
Deploy / deploy (push) Failing after 14m0s
CI / build (push) Failing after 15m50s
2026-06-28 08:38:52 +09:00
ByungCheol d794549f66 Merge branch 'dev'
Deploy / deploy (push) Failing after 12m43s
CI / build (push) Failing after 14m31s
2026-06-27 23:00:14 +09:00
ByungCheol 0e26237577 Merge branch 'dev'
CI / build (push) Failing after 12m28s
Deploy / deploy (push) Failing after 15m38s
2026-06-27 22:27:13 +09:00
ByungCheol 1646ec0a7f Merge branch 'dev'
Deploy / deploy (push) Failing after 11m22s
CI / build (push) Failing after 13m9s
2026-06-27 19:02:12 +09:00
ByungCheol fe0837cfd6 Merge branch 'dev'
Deploy / deploy (push) Successful in 50s
CI / build (push) Failing after 12m12s
2026-06-27 18:03:44 +09:00
ByungCheol 68f6c985bc Merge branch 'dev'
Deploy / deploy (push) Failing after 12m4s
CI / build (push) Failing after 14m3s
2026-06-27 14:47:00 +09:00
ByungCheol 47a01356a5 Merge branch 'dev'
CI / build (push) Failing after 12m36s
Deploy / deploy (push) Failing after 15m41s
2026-06-24 23:03:38 +09:00
ByungCheol 236332637a Merge branch 'dev'
Deploy / deploy (push) Failing after 13m31s
CI / build (push) Failing after 15m21s
2026-06-22 23:16:03 +09:00
ByungCheol 0e71e7b8cc Merge branch 'dev'
Deploy / deploy (push) Failing after 11m25s
CI / build (push) Failing after 13m19s
2026-06-22 22:38:06 +09:00
ByungCheol 9d4a415f2e Merge branch 'dev'
Deploy / deploy (push) Successful in 51s
CI / build (push) Failing after 12m48s
2026-06-20 17:13:14 +09:00
ByungCheol 5230ba1587 Merge branch 'dev'
Deploy / deploy (push) Failing after 11m6s
CI / build (push) Failing after 13m0s
2026-06-14 15:43:30 +09:00
ByungCheol 1f8b968588 Merge branch 'dev'
Deploy / deploy (push) Failing after 11m55s
CI / build (push) Failing after 13m43s
2026-06-12 22:52:23 +09:00
ByungCheol b90f9ea3b8 Merge branch 'dev'
Deploy / deploy (push) Failing after 11m21s
CI / build (push) Failing after 13m9s
2026-06-12 22:22:59 +09:00
ByungCheol a61700bdf6 Merge branch 'dev'
Deploy / deploy (push) Failing after 13m5s
CI / build (push) Failing after 14m58s
2026-06-12 22:16:09 +09:00
ByungCheol 484732c5ed Merge branch 'dev'
Deploy / deploy (push) Failing after 11m54s
CI / build (push) Failing after 13m49s
2026-06-09 22:47:32 +09:00
ByungCheol e3a24848bc Merge branch 'dev'
Deploy / deploy (push) Failing after 12m3s
CI / build (push) Failing after 13m52s
2026-06-07 20:32:38 +09:00
ByungCheol fd25968d0e Merge branch 'dev'
Deploy / deploy (push) Failing after 12m12s
CI / build (push) Failing after 14m4s
2026-06-07 19:17:25 +09:00
ByungCheol c61cec6dad Merge branch 'dev'
Deploy / deploy (push) Failing after 11m5s
CI / build (push) Failing after 12m55s
2026-06-06 17:33:33 +09:00
ByungCheol 2eef5c3b3e Merge branch 'dev'
Deploy / deploy (push) Failing after 11m56s
CI / build (push) Failing after 13m46s
2026-06-06 16:42:34 +09:00
ByungCheol 4f8ade5ff3 Merge branch 'dev'
CI / build (push) Failing after 12m27s
Deploy / deploy (push) Failing after 15m41s
2026-06-06 15:03:53 +09:00
ByungCheol 8bfe596f4e Merge branch 'dev'
CI / build (push) Failing after 11m31s
Deploy / deploy (push) Failing after 14m44s
2026-06-06 14:49:48 +09:00
ByungCheol 76bbf195df Merge branch 'dev'
Deploy / deploy (push) Failing after 12m0s
CI / build (push) Failing after 13m45s
2026-06-06 14:27:33 +09:00
ByungCheol b62b51b789 Merge branch 'dev'
Deploy / deploy (push) Failing after 12m22s
CI / build (push) Failing after 14m11s
2026-06-06 14:14:15 +09:00
ByungCheol 91cc5558e4 Merge branch 'dev'
Deploy / deploy (push) Failing after 11m20s
CI / build (push) Failing after 13m8s
2026-06-06 14:08:13 +09:00
ByungCheol 27b8829def Merge branch 'dev'
Deploy / deploy (push) Failing after 11m29s
CI / build (push) Failing after 13m15s
2026-06-06 08:13:03 +09:00
ByungCheol ad2ee4e75d Merge branch 'dev'
Deploy / deploy (push) Failing after 13m1s
CI / build (push) Failing after 14m52s
2026-06-06 00:16:30 +09:00
ByungCheol eb24a315d3 Merge branch 'dev'
CI / build (push) Failing after 11m1s
Deploy / deploy (push) Failing after 14m15s
2026-06-04 05:00:27 +09:00
ByungCheol 396e986792 Merge branch 'dev'
Deploy / deploy (push) Failing after 11m21s
CI / build (push) Failing after 13m3s
2026-06-03 18:51:32 +09:00
ByungCheol 191f1cfbe4 Merge branch 'dev'
CI / build (push) Failing after 12m15s
Deploy / deploy (push) Failing after 15m32s
2026-06-03 17:37:20 +09:00
ByungCheol 1d4dbc2183 Merge branch 'dev'
CI / build (push) Failing after 12m10s
Deploy / deploy (push) Failing after 15m27s
2026-06-03 16:42:22 +09:00
ByungCheol 478a75b6d8 Merge branch 'dev' into main
Deploy / deploy (push) Failing after 13m38s
CI / build (push) Failing after 15m20s
2026-06-01 22:54:25 +09:00
ByungCheol 41a5780e14 Merge branch 'dev' into main
CI / build (push) Successful in 50s
Deploy / deploy (push) Failing after 15m16s
2026-06-01 22:37:43 +09:00
ByungCheol 86c56f6c6f Merge branch 'dev' into main
Deploy / deploy (push) Failing after 11m36s
CI / build (push) Failing after 13m18s
2026-05-31 21:46:30 +09:00
ByungCheol bd1164866c Merge branch 'dev' into main
Deploy / deploy (push) Failing after 13m16s
CI / build (push) Failing after 14m57s
2026-05-31 20:49:52 +09:00
ByungCheol f884cb7b06 Merge branch 'dev' into main
CI / build (push) Failing after 11m54s
Deploy / deploy (push) Failing after 15m13s
2026-05-31 19:12:56 +09:00
ByungCheol e5fd31843a Merge branch 'dev' into main
Deploy / deploy (push) Failing after 11m27s
CI / build (push) Failing after 13m7s
2026-05-31 19:01:43 +09:00
ByungCheol a821cd8f89 Merge branch 'dev' into main
Deploy / deploy (push) Failing after 11m24s
CI / build (push) Failing after 13m6s
2026-05-31 18:46:44 +09:00
ByungCheol cf275fabf6 Merge branch 'dev' into main
CI / build (push) Failing after 11m53s
Deploy / deploy (push) Failing after 15m12s
2026-05-31 18:22:53 +09:00
ByungCheol d373e3e9e4 Merge branch 'dev' into main
Deploy / deploy (push) Has been cancelled
CI / build (push) Has been cancelled
fix(ci): SSH 개인키 CR 제거
2026-05-31 17:40:10 +09:00
ByungCheol f79c2412fd Merge branch 'dev' into main
Deploy / deploy (push) Has been cancelled
CI / build (push) Has been cancelled
ci: 자동 배포 워크플로 + 서버 설정 파일
2026-05-31 15:55:35 +09:00
ByungCheol f3230dcc5b Merge branch 'dev' into main
CI / build (push) Failing after 12m12s
ci: Gitea Actions CI/CD 워크플로 추가
2026-05-31 15:49:08 +09:00
ByungCheol ef7d0f7f83 Merge branch 'dev' into main
가계부·게시판 백엔드 API 구현 (account/auth/board/user/admin)
2026-05-31 15:45:25 +09:00
20 changed files with 26 additions and 301 deletions
-4
View File
@@ -2,10 +2,6 @@ HELP.md
.gradle
build/
# 로컬 전용 테스트/시드 스크립트 (실제 이메일·프리미엄 부여 포함 — 커밋 금지)
scripts/seed-test-*.sql
scripts/cleanup-test-*.sql
# 환경변수 / 비밀 정보 (실제 .env 는 커밋 금지, .env.example 만 추적)
.env
!gradle/wrapper/gradle-wrapper.jar
@@ -50,22 +50,8 @@ public class BudgetController {
@GetMapping
public List<BudgetResponse> list(
@RequestParam int year,
@RequestParam int month,
@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) {
return budgetService.list(current.getId(), year, month);
}
/** 다른 월의 예산을 이 달로 복사(전월 복사 등). 같은 분류는 덮어씀 */
@PostMapping("/copy")
public List<BudgetResponse> copy(
@RequestParam int fromYear,
@RequestParam int fromMonth,
@RequestParam int toYear,
@RequestParam int toMonth,
@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) {
budgetService.copy(current.getId(), fromYear, fromMonth, toYear, toMonth);
return budgetService.list(current.getId(), toYear, toMonth);
return budgetService.list(current.getId());
}
@GetMapping("/status")
@@ -88,11 +74,9 @@ public class BudgetController {
@PostMapping
public ResponseEntity<BudgetResponse> create(
@RequestParam int year,
@RequestParam int month,
@Valid @RequestBody BudgetRequest req,
@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) {
return ResponseEntity.status(HttpStatus.CREATED).body(budgetService.create(req, current.getId(), year, month));
return ResponseEntity.status(HttpStatus.CREATED).body(budgetService.create(req, current.getId()));
}
@PutMapping("/{id}")
@@ -1,37 +0,0 @@
package com.sb.web.account.controller;
import com.sb.web.account.service.FxService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
/**
* 환율 조회 API. 외화 결제 입력 시 통화→원화 환율 자동 채움용.
* GET /api/fx/rate?from=USD&to=KRW → { from, to, rate }
*/
@RestController
@RequestMapping("/api/fx")
public class FxController {
private final FxService fxService;
public FxController(FxService fxService) {
this.fxService = fxService;
}
@GetMapping("/rate")
public Map<String, Object> rate(@RequestParam String from,
@RequestParam(defaultValue = "KRW") String to) {
BigDecimal rate = fxService.getRate(from, to);
Map<String, Object> res = new HashMap<>();
res.put("from", from == null ? null : from.toUpperCase());
res.put("to", to == null ? null : to.toUpperCase());
res.put("rate", rate); // 조회 실패 시 null
return res;
}
}
@@ -6,7 +6,6 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@@ -31,9 +30,6 @@ public class AccountEntry implements Serializable {
private Long toWalletId; // 이체 입금 계좌
private String toWalletName;
private Integer installmentMonths; // 카드 할부 개월수(2~24, 일시불은 null)
private String currency; // 외화 통화코드(USD 등). null/KRW = 원화
private BigDecimal originalAmount; // 외화 원본 금액
private BigDecimal exchangeRate; // 적용 환율(외화 1단위→원화)
private Boolean pending; // 확인 필요(미확정) 여부 — 알림 자동인식 건
private String notifKey; // 알림 자동인식 중복방지 키
private LocalDateTime createdAt;
@@ -28,8 +28,6 @@ public class Budget implements Serializable {
private Long weekly;
private Long monthly;
private Long yearly;
private Integer year; // 예산 연도(월별 예산)
private Integer month; // 예산 월(1~12)
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -6,7 +6,6 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@@ -30,11 +29,6 @@ public class Wallet implements Serializable {
private Long openingBalance; // 초기 잔액(부호있음: 자산+, 부채-)
private LocalDate openingDate;
private Long currentValue; // 현재 평가금액 (INVEST 전용, 수동 갱신)
private Long loanAmount; // 대출 실행 금액(원금, 처음 빌린 금액) (LOAN 전용)
private BigDecimal loanRate; // 연이자율(%) e.g. 5.25 (LOAN 전용)
private String loanMethod; // 상환방식: EQUAL_PAYMENT / EQUAL_PRINCIPAL / BULLET (LOAN 전용)
private Integer loanMonths; // 대출기간(개월) (LOAN 전용)
private LocalDate loanStart; // 대출시작일 (LOAN 전용)
private Integer sortOrder; // 표시 순서 (타입 내 정렬)
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@@ -9,7 +9,6 @@ import jakarta.validation.constraints.PositiveOrZero;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
@@ -49,10 +48,4 @@ public class AccountEntryRequest {
/** 선택한 태그 ID 목록 (태그 관리에 등록된 태그, 선택) */
private List<Long> tagIds;
/** 외화 결제(선택). currency 가 있으면 amount 는 환산된 원화여야 한다. */
@Size(max = 3, message = "통화코드가 올바르지 않습니다.")
private String currency; // USD/JPY 등. null/KRW = 원화
private BigDecimal originalAmount; // 외화 원본 금액
private BigDecimal exchangeRate; // 적용 환율(외화 1단위→원화)
}
@@ -4,7 +4,6 @@ import com.sb.web.account.domain.AccountEntry;
import lombok.Builder;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
@@ -27,9 +26,6 @@ public class AccountEntryResponse {
private String toWalletName;
private Integer installmentMonths;
private Boolean pending;
private String currency;
private BigDecimal originalAmount;
private BigDecimal exchangeRate;
private List<String> tags;
public static AccountEntryResponse from(AccountEntry e, List<String> tags) {
@@ -46,9 +42,6 @@ public class AccountEntryResponse {
.toWalletName(e.getToWalletName())
.installmentMonths(e.getInstallmentMonths())
.pending(Boolean.TRUE.equals(e.getPending()))
.currency(e.getCurrency())
.originalAmount(e.getOriginalAmount())
.exchangeRate(e.getExchangeRate())
.tags(tags)
.build();
}
@@ -1,13 +1,10 @@
package com.sb.web.account.dto;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
@@ -40,17 +37,4 @@ public class WalletRequest {
/** 현재 평가금액 (INVEST 전용, 수동 갱신) */
private Long currentValue;
// ===== 대출(LOAN) 전용 =====
private Long loanAmount; // 대출 실행 금액(원금)
@DecimalMin(value = "0.0", inclusive = true)
@DecimalMax(value = "100.0", message = "이자율은 100% 이하여야 합니다.")
private BigDecimal loanRate; // 연이자율(%)
@Pattern(regexp = "EQUAL_PAYMENT|EQUAL_PRINCIPAL|BULLET|", message = "상환방식이 올바르지 않습니다.")
private String loanMethod; // 상환방식
private Integer loanMonths; // 대출기간(개월)
private LocalDate loanStart; // 대출시작일
}
@@ -4,7 +4,6 @@ import com.sb.web.account.domain.Wallet;
import lombok.Builder;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
@@ -32,13 +31,6 @@ public class WalletResponse {
private Long currentValue; // 평가액 직접입력값(퇴직연금·연금 등 수동 갱신). 있으면 총평가로 사용
private Boolean manualValuation; // true면 평가액 직접입력형(종목 자동계산 대신)
// 대출(LOAN) 전용
private Long loanAmount; // 대출 실행 금액(원금)
private BigDecimal loanRate; // 연이자율(%) e.g. 5.25
private String loanMethod; // EQUAL_PAYMENT / EQUAL_PRINCIPAL / BULLET
private Integer loanMonths; // 대출기간(개월)
private LocalDate loanStart; // 대출시작일
public static WalletResponse from(Wallet w, long balance) {
return WalletResponse.builder()
.id(w.getId())
@@ -51,11 +43,6 @@ public class WalletResponse {
.openingDate(w.getOpeningDate())
.balance(balance)
.currentValue(w.getCurrentValue())
.loanAmount(w.getLoanAmount())
.loanRate(w.getLoanRate())
.loanMethod(w.getLoanMethod())
.loanMonths(w.getLoanMonths())
.loanStart(w.getLoanStart())
.build();
}
}
@@ -12,24 +12,13 @@ import java.util.List;
@Mapper
public interface BudgetMapper {
List<Budget> findByMember(@Param("memberId") Long memberId,
@Param("year") Integer year,
@Param("month") Integer month);
List<Budget> findByMember(@Param("memberId") Long memberId);
Budget findByIdAndMember(@Param("id") Long id, @Param("memberId") Long memberId);
int countByCategory(@Param("memberId") Long memberId,
@Param("category") String category,
@Param("excludeId") Long excludeId,
@Param("year") Integer year,
@Param("month") Integer month);
/** fromYear/fromMonth 예산을 toYear/toMonth 로 복사(같은 분류는 덮어씀). 복사된 건수 반환 */
int copyMonth(@Param("memberId") Long memberId,
@Param("fromYear") Integer fromYear,
@Param("fromMonth") Integer fromMonth,
@Param("toYear") Integer toYear,
@Param("toMonth") Integer toMonth);
@Param("excludeId") Long excludeId);
int insert(Budget budget);
@@ -234,9 +234,6 @@ public class AccountService {
.walletId(req.getWalletId())
.toWalletId(transfer ? req.getToWalletId() : null)
.installmentMonths(installmentOf(req))
.currency(req.getCurrency())
.originalAmount(req.getOriginalAmount())
.exchangeRate(req.getExchangeRate())
.build();
mapper.insert(entry);
applyTags(entry.getId(), req.getTagIds(), memberId);
@@ -256,9 +253,6 @@ public class AccountService {
entry.setWalletId(req.getWalletId());
entry.setToWalletId(transfer ? req.getToWalletId() : null);
entry.setInstallmentMonths(installmentOf(req));
entry.setCurrency(req.getCurrency());
entry.setOriginalAmount(req.getOriginalAmount());
entry.setExchangeRate(req.getExchangeRate());
mapper.update(entry);
mapper.deleteEntryTagsByEntryId(id);
@@ -535,12 +529,6 @@ public class AccountService {
wallet.setOpeningBalance(req.getOpeningBalance() != null ? req.getOpeningBalance() : 0L);
wallet.setOpeningDate(req.getOpeningDate());
wallet.setCurrentValue("INVEST".equals(req.getType()) ? req.getCurrentValue() : null);
boolean isLoan = "LOAN".equals(req.getType());
wallet.setLoanAmount(isLoan ? req.getLoanAmount() : null);
wallet.setLoanRate(isLoan ? req.getLoanRate() : null);
wallet.setLoanMethod(isLoan ? blankToNull(req.getLoanMethod()) : null);
wallet.setLoanMonths(isLoan ? req.getLoanMonths() : null);
wallet.setLoanStart(isLoan ? req.getLoanStart() : null);
walletMapper.update(wallet);
return WalletResponse.from(wallet, balanceMap(memberId).getOrDefault(id, wallet.getOpeningBalance()));
}
@@ -570,7 +558,6 @@ public class AccountService {
}
private Wallet toWallet(WalletRequest req) {
boolean isLoan = "LOAN".equals(req.getType());
return Wallet.builder()
.type(req.getType())
.name(req.getName().trim())
@@ -580,11 +567,6 @@ public class AccountService {
.openingBalance(req.getOpeningBalance() != null ? req.getOpeningBalance() : 0L)
.openingDate(req.getOpeningDate())
.currentValue("INVEST".equals(req.getType()) ? req.getCurrentValue() : null)
.loanAmount(isLoan ? req.getLoanAmount() : null)
.loanRate(isLoan ? req.getLoanRate() : null)
.loanMethod(isLoan ? blankToNull(req.getLoanMethod()) : null)
.loanMonths(isLoan ? req.getLoanMonths() : null)
.loanStart(isLoan ? req.getLoanStart() : null)
.build();
}
@@ -156,12 +156,11 @@ public class BackupService {
}
}
// 7) 예산 — 복구 시 현재 월 예산으로 등록(백업엔 월 정보 없음)
// 7) 예산
if (req.getBudgets() != null) {
java.time.LocalDate today = java.time.LocalDate.now();
for (BudgetRequest b : req.getBudgets()) {
if (b == null || b.getCategory() == null) continue;
budgetService.create(b, memberId, today.getYear(), today.getMonthValue());
budgetService.create(b, memberId);
}
}
@@ -31,46 +31,36 @@ public class BudgetService {
private final BudgetMapper budgetMapper;
private final AccountEntryMapper entryMapper;
public List<BudgetResponse> list(Long memberId, int year, int month) {
return budgetMapper.findByMember(memberId, year, month).stream().map(BudgetResponse::from).toList();
public List<BudgetResponse> list(Long memberId) {
return budgetMapper.findByMember(memberId).stream().map(BudgetResponse::from).toList();
}
@Transactional
public BudgetResponse create(BudgetRequest req, Long memberId, int year, int month) {
public BudgetResponse create(BudgetRequest req, Long memberId) {
String category = req.getCategory().trim();
if (budgetMapper.countByCategory(memberId, category, null, year, month) > 0) {
if (budgetMapper.countByCategory(memberId, category, null) > 0) {
throw new ApiException(HttpStatus.CONFLICT, "이미 예산이 설정된 카테고리입니다.");
}
Budget budget = toBudget(req, category);
budget.setMemberId(memberId);
budget.setYear(year);
budget.setMonth(month);
budgetMapper.insert(budget);
return BudgetResponse.from(budget);
}
@Transactional
public BudgetResponse update(Long id, BudgetRequest req, Long memberId) {
Budget existing = mustFind(id, memberId);
mustFind(id, memberId);
String category = req.getCategory().trim();
if (budgetMapper.countByCategory(memberId, category, id, existing.getYear(), existing.getMonth()) > 0) {
if (budgetMapper.countByCategory(memberId, category, id) > 0) {
throw new ApiException(HttpStatus.CONFLICT, "이미 예산이 설정된 카테고리입니다.");
}
Budget budget = toBudget(req, category);
budget.setId(id);
budget.setMemberId(memberId);
budget.setYear(existing.getYear());
budget.setMonth(existing.getMonth());
budgetMapper.update(budget);
return BudgetResponse.from(budget);
}
/** fromYear/fromMonth 예산을 toYear/toMonth 로 복사(같은 분류 덮어씀). 복사된 건수 반환 */
@Transactional
public int copy(Long memberId, int fromYear, int fromMonth, int toYear, int toMonth) {
return budgetMapper.copyMonth(memberId, fromYear, fromMonth, toYear, toMonth);
}
@Transactional
public void delete(Long id, Long memberId) {
mustFind(id, memberId);
@@ -89,7 +79,7 @@ public class BudgetService {
}
List<BudgetStatus> result = new ArrayList<>();
for (Budget b : budgetMapper.findByMember(memberId, year, month)) {
for (Budget b : budgetMapper.findByMember(memberId)) {
long monthlyBudget = monthlyBudget(b, daysInMonth);
long spent = spentByCategory.getOrDefault(b.getCategory(), 0L);
result.add(BudgetStatus.builder()
@@ -113,8 +103,7 @@ public class BudgetService {
int y = year != null ? year : Year.now().getValue();
int m = month != null ? month : 1;
// 기간 차트는 선택 월의 예산을 기준으로 각 버킷에 적용(월별 예산 도입 전 동작과 동일)
List<Budget> budgets = budgetMapper.findByMember(memberId, y, m);
List<Budget> budgets = budgetMapper.findByMember(memberId);
Map<String, Long> expense = new HashMap<>();
for (Map<String, Object> row : entryMapper.statsByPeriod(memberId, unit, year, month)) {
expense.put(String.valueOf(((Number) row.get("bucket")).longValue()),
@@ -1,64 +0,0 @@
package com.sb.web.account.service;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 환율 조회. open.er-api.com(무료·무인증) 사용. 통화(base)별로 하루 1회만 호출하고 캐시.
* 실패해도 예외를 던지지 않고 null(또는 직전 캐시) 반환.
*/
@Slf4j
@Service
public class FxService {
private static final String URL = "https://open.er-api.com/v6/latest/{base}";
private final RestClient restClient = RestClient.builder().build();
private record Cached(LocalDate date, JsonNode rates) {}
private final Map<String, Cached> cache = new ConcurrentHashMap<>();
/** from 통화 1단위 → to 통화 환율. 조회 실패 시 null. */
public BigDecimal getRate(String from, String to) {
if (from == null || to == null) return null;
String base = from.trim().toUpperCase();
String target = to.trim().toUpperCase();
if (base.isEmpty() || target.isEmpty()) return null;
if (base.equals(target)) return BigDecimal.ONE;
JsonNode rates = ratesFor(base);
if (rates == null) return null;
JsonNode r = rates.get(target);
return (r != null && r.isNumber()) ? r.decimalValue() : null;
}
private JsonNode ratesFor(String base) {
Cached c = cache.get(base);
LocalDate today = LocalDate.now();
if (c != null && c.date().equals(today)) return c.rates();
try {
JsonNode root = restClient.get()
.uri(URL, base)
.header("User-Agent", "Mozilla/5.0")
.retrieve()
.body(JsonNode.class);
if (root == null || !"success".equals(root.path("result").asText())) {
return c != null ? c.rates() : null;
}
JsonNode rates = root.path("rates");
if (rates.isMissingNode() || !rates.isObject()) {
return c != null ? c.rates() : null;
}
cache.put(base, new Cached(today, rates));
return rates;
} catch (Exception e) {
log.warn("환율 조회 실패 base={}: {}", base, e.toString());
return c != null ? c.rates() : null; // 실패 시 직전 캐시라도
}
}
}
@@ -28,7 +28,7 @@ public class WebConfig implements WebMvcConfigurer {
.addPathPatterns("/api/auth/me", "/api/auth/points", "/api/auth/point-history",
"/api/auth/logout", "/api/auth/password",
"/api/auth/verify-password", "/api/auth/profile", "/api/auth/profile-image",
"/api/board/**", "/api/admin/**", "/api/account/**", "/api/billing/**", "/api/fx/**")
"/api/board/**", "/api/admin/**", "/api/account/**", "/api/billing/**")
// Google Play RTDN(서버 알림)은 비인증 — Google 이 호출
.excludePathPatterns("/api/billing/google/rtdn");
// 인가: 관리자 전용 경로 (authInterceptor 다음에 실행되어 role 검사)
-18
View File
@@ -28,11 +28,6 @@ ALTER TABLE wallet ADD COLUMN IF NOT EXISTS opening_balance BIGINT NOT NULL DEFA
ALTER TABLE wallet ADD COLUMN IF NOT EXISTS opening_date DATE NULL;
ALTER TABLE wallet ADD COLUMN IF NOT EXISTS current_value BIGINT NULL;
ALTER TABLE wallet ADD COLUMN IF NOT EXISTS sort_order INT NOT NULL DEFAULT 0;
ALTER TABLE wallet ADD COLUMN IF NOT EXISTS loan_rate DECIMAL(7,4) NULL COMMENT '연이자율(%) 예: 5.2500';
ALTER TABLE wallet ADD COLUMN IF NOT EXISTS loan_method VARCHAR(20) NULL COMMENT '상환방식: EQUAL_PAYMENT/EQUAL_PRINCIPAL/BULLET';
ALTER TABLE wallet ADD COLUMN IF NOT EXISTS loan_months INT NULL COMMENT '대출기간(개월)';
ALTER TABLE wallet ADD COLUMN IF NOT EXISTS loan_start DATE NULL COMMENT '대출시작일';
ALTER TABLE wallet ADD COLUMN IF NOT EXISTS loan_amount BIGINT NULL COMMENT '대출 실행 금액(원금, 처음 빌린 금액)';
-- 계좌번호 암호화 저장(AES-GCM)으로 평문보다 길어 VARCHAR(255) 필요 — 운영 적용 완료(CREATE TABLE 정의에 반영).
-- 매 기동 MODIFY 는 라이브 테이블 락 위험이 있어 제거. 기존 환경 확장이 필요하면 1회 수동 적용:
-- ALTER TABLE wallet MODIFY account_number VARCHAR(255) NULL;
@@ -64,10 +59,6 @@ ALTER TABLE account_entry ADD COLUMN IF NOT EXISTS installment_months INT NULL;
ALTER TABLE account_entry ADD COLUMN IF NOT EXISTS pending TINYINT(1) NOT NULL DEFAULT 0 COMMENT '확인 필요(미확정) 여부';
ALTER TABLE account_entry ADD COLUMN IF NOT EXISTS notif_key VARCHAR(160) NULL COMMENT '알림 자동인식 중복방지 키';
ALTER TABLE account_entry ADD INDEX IF NOT EXISTS idx_entry_notif (member_id, notif_key);
-- 외화 결제: amount 는 환산된 원화(표준값, 통계·예산은 이 값 사용). 아래는 외화 원본 보존용.
ALTER TABLE account_entry ADD COLUMN IF NOT EXISTS currency VARCHAR(3) NULL COMMENT '통화코드(USD/JPY 등). NULL/KRW=원화';
ALTER TABLE account_entry ADD COLUMN IF NOT EXISTS original_amount DECIMAL(18,2) NULL COMMENT '외화 원본 금액';
ALTER TABLE account_entry ADD COLUMN IF NOT EXISTS exchange_rate DECIMAL(18,6) NULL COMMENT '적용 환율(외화 1단위→원화)';
-- 가계부 태그 (사용자별 관리 — 게시판 태그와 분리)
CREATE TABLE IF NOT EXISTS account_tag (
@@ -170,15 +161,6 @@ CREATE TABLE IF NOT EXISTS budget (
UNIQUE KEY uk_budget_member_category (member_id, category)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 월별 예산: 예산을 연/월별로 분리(전월 복사로 다음 달에 동일 예산 저장 가능).
ALTER TABLE budget ADD COLUMN IF NOT EXISTS `year` INT NULL COMMENT '예산 연도';
ALTER TABLE budget ADD COLUMN IF NOT EXISTS `month` INT NULL COMMENT '예산 월(1~12)';
-- 기존 상시 예산을 현재 연/월로 1회 이관
UPDATE budget SET `year` = YEAR(NOW()), `month` = MONTH(NOW()) WHERE `year` IS NULL OR `month` IS NULL;
-- 분류 유니크를 (member,category) → (member,category,year,month) 로 교체
ALTER TABLE budget DROP INDEX IF EXISTS uk_budget_member_category;
ALTER TABLE budget ADD UNIQUE KEY IF NOT EXISTS uk_budget_member_cat_ym (member_id, category, `year`, `month`);
-- 가계부 항목 - 태그 (다대다, tag_id 는 account_tag.id 참조)
CREATE TABLE IF NOT EXISTS account_entry_tag (
entry_id BIGINT NOT NULL,
@@ -18,9 +18,6 @@
<result property="installmentMonths" column="installment_months"/>
<result property="pending" column="pending"/>
<result property="notifKey" column="notif_key"/>
<result property="currency" column="currency"/>
<result property="originalAmount" column="original_amount"/>
<result property="exchangeRate" column="exchange_rate"/>
<result property="createdAt" column="created_at"/>
<result property="updatedAt" column="updated_at"/>
</resultMap>
@@ -35,7 +32,6 @@
e.wallet_id, w.name AS wallet_name,
e.to_wallet_id, tw.name AS to_wallet_name,
e.installment_months, e.pending, e.notif_key,
e.currency, e.original_amount, e.exchange_rate,
e.created_at, e.updated_at
</sql>
<sql id="entryJoins">
@@ -145,20 +141,17 @@
useGeneratedKeys="true" keyProperty="id">
INSERT INTO account_entry (member_id, entry_date, type, category, amount, memo,
wallet_id, to_wallet_id, installment_months, pending, notif_key,
currency, original_amount, exchange_rate,
created_at, updated_at)
VALUES (#{memberId}, #{entryDate}, #{type}, #{category}, #{amount}, #{memo},
#{walletId}, #{toWalletId}, #{installmentMonths},
COALESCE(#{pending}, 0), #{notifKey},
#{currency}, #{originalAmount}, #{exchangeRate}, NOW(), NOW())
COALESCE(#{pending}, 0), #{notifKey}, NOW(), NOW())
</insert>
<update id="update" parameterType="com.sb.web.account.domain.AccountEntry">
UPDATE account_entry
SET entry_date = #{entryDate}, type = #{type}, category = #{category},
amount = #{amount}, memo = #{memo}, wallet_id = #{walletId}, to_wallet_id = #{toWalletId},
installment_months = #{installmentMonths},
currency = #{currency}, original_amount = #{originalAmount}, exchange_rate = #{exchangeRate}
installment_months = #{installmentMonths}
WHERE id = #{id} AND member_id = #{memberId}
</update>
+5 -24
View File
@@ -14,19 +14,15 @@
<result property="weekly" column="weekly"/>
<result property="monthly" column="monthly"/>
<result property="yearly" column="yearly"/>
<result property="year" column="year"/>
<result property="month" column="month"/>
<result property="createdAt" column="created_at"/>
<result property="updatedAt" column="updated_at"/>
</resultMap>
<sql id="cols">id, member_id, category, fixed, base_unit, base_amount,
daily, weekly, monthly, yearly, `year`, `month`, created_at, updated_at</sql>
daily, weekly, monthly, yearly, created_at, updated_at</sql>
<select id="findByMember" resultMap="budgetResultMap">
SELECT <include refid="cols"/> FROM budget
WHERE member_id = #{memberId} AND `year` = #{year} AND `month` = #{month}
ORDER BY category
<select id="findByMember" parameterType="long" resultMap="budgetResultMap">
SELECT <include refid="cols"/> FROM budget WHERE member_id = #{memberId} ORDER BY category
</select>
<select id="findByIdAndMember" resultMap="budgetResultMap">
@@ -36,30 +32,15 @@
<select id="countByCategory" resultType="int">
SELECT COUNT(*) FROM budget
WHERE member_id = #{memberId} AND category = #{category}
AND `year` = #{year} AND `month` = #{month}
<if test="excludeId != null">AND id != #{excludeId}</if>
</select>
<insert id="insert" parameterType="com.sb.web.account.domain.Budget"
useGeneratedKeys="true" keyProperty="id">
INSERT INTO budget (member_id, category, fixed, base_unit, base_amount,
daily, weekly, monthly, yearly, `year`, `month`, created_at, updated_at)
daily, weekly, monthly, yearly, created_at, updated_at)
VALUES (#{memberId}, #{category}, #{fixed}, #{baseUnit}, #{baseAmount},
#{daily}, #{weekly}, #{monthly}, #{yearly}, #{year}, #{month}, NOW(), NOW())
</insert>
<!-- 전월(또는 임의 월) 예산을 다른 월로 복사. 같은 분류는 덮어씀(ON DUPLICATE) -->
<insert id="copyMonth">
INSERT INTO budget (member_id, category, fixed, base_unit, base_amount,
daily, weekly, monthly, yearly, `year`, `month`, created_at, updated_at)
SELECT member_id, category, fixed, base_unit, base_amount,
daily, weekly, monthly, yearly, #{toYear}, #{toMonth}, NOW(), NOW()
FROM budget
WHERE member_id = #{memberId} AND `year` = #{fromYear} AND `month` = #{fromMonth}
ON DUPLICATE KEY UPDATE
fixed = VALUES(fixed), base_unit = VALUES(base_unit), base_amount = VALUES(base_amount),
daily = VALUES(daily), weekly = VALUES(weekly), monthly = VALUES(monthly),
yearly = VALUES(yearly), updated_at = NOW()
#{daily}, #{weekly}, #{monthly}, #{yearly}, NOW(), NOW())
</insert>
<update id="update" parameterType="com.sb.web.account.domain.Budget">
+4 -18
View File
@@ -15,20 +15,13 @@
<result property="openingBalance" column="opening_balance"/>
<result property="openingDate" column="opening_date"/>
<result property="currentValue" column="current_value"/>
<result property="loanAmount" column="loan_amount"/>
<result property="loanRate" column="loan_rate"/>
<result property="loanMethod" column="loan_method"/>
<result property="loanMonths" column="loan_months"/>
<result property="loanStart" column="loan_start"/>
<result property="sortOrder" column="sort_order"/>
<result property="createdAt" column="created_at"/>
<result property="updatedAt" column="updated_at"/>
</resultMap>
<sql id="cols">id, member_id, type, name, issuer, account_number, card_type,
opening_balance, opening_date, current_value,
loan_amount, loan_rate, loan_method, loan_months, loan_start,
sort_order, created_at, updated_at</sql>
opening_balance, opening_date, current_value, sort_order, created_at, updated_at</sql>
<select id="findByMember" parameterType="long" resultMap="walletResultMap">
SELECT <include refid="cols"/> FROM wallet
@@ -44,14 +37,10 @@
<insert id="insert" parameterType="com.sb.web.account.domain.Wallet"
useGeneratedKeys="true" keyProperty="id">
INSERT INTO wallet (member_id, type, name, issuer, account_number, card_type,
opening_balance, opening_date, current_value,
loan_amount, loan_rate, loan_method, loan_months, loan_start,
sort_order, created_at, updated_at)
opening_balance, opening_date, current_value, sort_order, created_at, updated_at)
VALUES (#{memberId}, #{type}, #{name}, #{issuer},
#{accountNumber, typeHandler=com.sb.web.common.crypto.EncryptedStringTypeHandler}, #{cardType},
#{openingBalance}, #{openingDate}, #{currentValue},
#{loanAmount}, #{loanRate}, #{loanMethod}, #{loanMonths}, #{loanStart},
#{sortOrder}, NOW(), NOW())
#{openingBalance}, #{openingDate}, #{currentValue}, #{sortOrder}, NOW(), NOW())
</insert>
<update id="update" parameterType="com.sb.web.account.domain.Wallet">
@@ -60,10 +49,7 @@
account_number = #{accountNumber, typeHandler=com.sb.web.common.crypto.EncryptedStringTypeHandler},
card_type = #{cardType},
opening_balance = #{openingBalance}, opening_date = #{openingDate},
current_value = #{currentValue},
loan_amount = #{loanAmount},
loan_rate = #{loanRate}, loan_method = #{loanMethod},
loan_months = #{loanMonths}, loan_start = #{loanStart}
current_value = #{currentValue}
WHERE id = #{id} AND member_id = #{memberId}
</update>