Compare commits
56 Commits
dev
..
d028d957b6
| Author | SHA1 | Date | |
|---|---|---|---|
| d028d957b6 | |||
| f28ecd853e | |||
| 5fec464f90 | |||
| 483aaa6473 | |||
| 5bd2616de9 | |||
| ad40af18b2 | |||
| 95d84c8138 | |||
| 08338d516f | |||
| dc4e99a6db | |||
| f408715e17 | |||
| 74a2cae9ef | |||
| 6e269888c2 | |||
| 39a11d299c | |||
| f19f653715 | |||
| 16ddf88de3 | |||
| d794549f66 | |||
| 0e26237577 | |||
| 1646ec0a7f | |||
| fe0837cfd6 | |||
| 68f6c985bc | |||
| 47a01356a5 | |||
| 236332637a | |||
| 0e71e7b8cc | |||
| 9d4a415f2e | |||
| 5230ba1587 | |||
| 1f8b968588 | |||
| b90f9ea3b8 | |||
| a61700bdf6 | |||
| 484732c5ed | |||
| e3a24848bc | |||
| fd25968d0e | |||
| c61cec6dad | |||
| 2eef5c3b3e | |||
| 4f8ade5ff3 | |||
| 8bfe596f4e | |||
| 76bbf195df | |||
| b62b51b789 | |||
| 91cc5558e4 | |||
| 27b8829def | |||
| ad2ee4e75d | |||
| eb24a315d3 | |||
| 396e986792 | |||
| 191f1cfbe4 | |||
| 1d4dbc2183 | |||
| 478a75b6d8 | |||
| 41a5780e14 | |||
| 86c56f6c6f | |||
| bd1164866c | |||
| f884cb7b06 | |||
| e5fd31843a | |||
| a821cd8f89 | |||
| cf275fabf6 | |||
| d373e3e9e4 | |||
| f79c2412fd | |||
| f3230dcc5b | |||
| ef7d0f7f83 |
@@ -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; // 실패 시 직전 캐시라도
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,8 +27,7 @@ public interface WithdrawMapper {
|
||||
int deletePostsByAuthor(@Param("memberId") Long memberId);
|
||||
int deleteBoardImagesByMember(@Param("memberId") Long memberId);
|
||||
|
||||
// 신고 / 포인트 / 결제 / 세션
|
||||
int deleteReportsByMember(@Param("memberId") Long memberId);
|
||||
// 포인트 / 결제 / 세션
|
||||
int deletePointHistory(@Param("memberId") Long memberId);
|
||||
int deleteIapPurchases(@Param("memberId") Long memberId);
|
||||
int deleteAuthSessions(@Param("memberId") Long memberId);
|
||||
|
||||
@@ -324,8 +324,7 @@ public class AuthService {
|
||||
backupMapper.deleteCategories(memberId);
|
||||
backupMapper.deleteTags(memberId);
|
||||
backupMapper.deleteWallets(memberId);
|
||||
// 3) 신고 / 포인트 / 결제 / 세션
|
||||
withdrawMapper.deleteReportsByMember(memberId);
|
||||
// 3) 포인트 / 결제 / 세션
|
||||
withdrawMapper.deletePointHistory(memberId);
|
||||
withdrawMapper.deleteIapPurchases(memberId);
|
||||
withdrawMapper.deleteAuthSessions(memberId);
|
||||
|
||||
@@ -53,24 +53,6 @@ public class BoardController {
|
||||
return boardService.list(page, size, category, tag, keyword, searchType);
|
||||
}
|
||||
|
||||
/** 내 글 모아보기 */
|
||||
@GetMapping("/my/posts")
|
||||
public PageResponse<PostSummary> myPosts(
|
||||
@RequestParam(defaultValue = "1") int page,
|
||||
@RequestParam(defaultValue = "10") int size,
|
||||
@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) {
|
||||
return boardService.myPosts(current.getId(), page, size);
|
||||
}
|
||||
|
||||
/** 내 댓글 모아보기 */
|
||||
@GetMapping("/my/comments")
|
||||
public PageResponse<com.sb.web.board.dto.MyCommentResponse> myComments(
|
||||
@RequestParam(defaultValue = "1") int page,
|
||||
@RequestParam(defaultValue = "10") int size,
|
||||
@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) {
|
||||
return boardService.myComments(current.getId(), page, size);
|
||||
}
|
||||
|
||||
@GetMapping("/posts/{id}")
|
||||
public PostDetail get(
|
||||
@PathVariable Long id,
|
||||
@@ -196,31 +178,4 @@ public class BoardController {
|
||||
@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) {
|
||||
return boardService.voteComment(commentId, req.getType(), current);
|
||||
}
|
||||
|
||||
/** 게시글 신고 (누적 5건 시 블라인드) */
|
||||
@PostMapping("/posts/{id}/report")
|
||||
public ResponseEntity<Void> reportPost(
|
||||
@PathVariable Long id,
|
||||
@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) {
|
||||
boardService.reportPost(id, current);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
/** 댓글 신고 (누적 5건 시 블라인드) */
|
||||
@PostMapping("/comments/{commentId}/report")
|
||||
public ResponseEntity<Void> reportComment(
|
||||
@PathVariable Long commentId,
|
||||
@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) {
|
||||
boardService.reportComment(commentId, current);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
/** 댓글 블라인드 해제 (관리자) */
|
||||
@PostMapping("/comments/{commentId}/unblock")
|
||||
public ResponseEntity<Void> unblockComment(
|
||||
@PathVariable Long commentId,
|
||||
@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) {
|
||||
boardService.unblockComment(commentId, current);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,5 @@ public class Comment implements Serializable {
|
||||
private String authorGooglePicture; // 조회 시 member JOIN (저장 안 함)
|
||||
private String authorProfileImage; // 조회 시 member JOIN (저장 안 함)
|
||||
private String content;
|
||||
private Boolean blocked; // 신고 누적 블라인드
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ public class CommentResponse {
|
||||
private Integer downCount; // 비추천 수
|
||||
private String myVote; // 현재 사용자의 투표: UP / DOWN / null
|
||||
private Boolean hot; // 인기 댓글(추천 50+ & 1일 이내) 상단 노출
|
||||
private Boolean blocked; // 신고 누적 블라인드(관리자만 본문 열람)
|
||||
|
||||
public static CommentResponse from(Comment c) {
|
||||
return CommentResponse.builder()
|
||||
@@ -34,7 +33,6 @@ public class CommentResponse {
|
||||
.authorGooglePicture(c.getAuthorGooglePicture())
|
||||
.authorProfileImage(c.getAuthorProfileImage())
|
||||
.content(c.getContent())
|
||||
.blocked(Boolean.TRUE.equals(c.getBlocked()))
|
||||
.createdAt(c.getCreatedAt())
|
||||
.upCount(0)
|
||||
.downCount(0)
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
package com.sb.web.board.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 내 댓글 모아보기 항목 (어느 글의 댓글인지 함께).
|
||||
*/
|
||||
@Data
|
||||
public class MyCommentResponse {
|
||||
|
||||
private Long id;
|
||||
private String content;
|
||||
private LocalDateTime createdAt;
|
||||
private Long postId;
|
||||
private String postTitle;
|
||||
private String category;
|
||||
}
|
||||
@@ -13,7 +13,6 @@ public class PostSummary {
|
||||
|
||||
private Long id;
|
||||
private String title;
|
||||
private String category; // 내 글 모아보기 등 교차 게시판 링크용
|
||||
private Long authorId;
|
||||
private String authorName;
|
||||
private String authorGooglePicture;
|
||||
|
||||
@@ -18,13 +18,4 @@ public interface CommentMapper {
|
||||
int delete(@Param("id") Long id);
|
||||
|
||||
int deleteByPostId(@Param("postId") Long postId);
|
||||
|
||||
/** 신고 누적 블라인드 설정 */
|
||||
int updateBlocked(@Param("id") Long id, @Param("blocked") boolean blocked);
|
||||
|
||||
/** 내 댓글 모아보기 (글 제목·카테고리 포함) */
|
||||
java.util.List<com.sb.web.board.dto.MyCommentResponse> findMyPage(
|
||||
@Param("memberId") Long memberId, @Param("offset") int offset, @Param("size") int size);
|
||||
|
||||
long countMyComments(@Param("memberId") Long memberId);
|
||||
}
|
||||
|
||||
@@ -28,12 +28,6 @@ public interface PostMapper {
|
||||
@Param("minUp") int minUp,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/** 내 글 모아보기 */
|
||||
List<PostSummary> findMyPage(@Param("memberId") Long memberId,
|
||||
@Param("offset") int offset, @Param("size") int size);
|
||||
|
||||
long countMyPosts(@Param("memberId") Long memberId);
|
||||
|
||||
Post findById(@Param("id") Long id);
|
||||
|
||||
int insert(Post post);
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package com.sb.web.board.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* 신고(report) 매퍼 — 글/댓글 신고 누적 집계.
|
||||
*/
|
||||
@Mapper
|
||||
public interface ReportMapper {
|
||||
|
||||
/** 신고 추가 (회원당 대상별 1회 — 중복은 무시) */
|
||||
int insertIgnore(@Param("targetType") String targetType,
|
||||
@Param("targetId") Long targetId,
|
||||
@Param("memberId") Long memberId,
|
||||
@Param("reason") String reason);
|
||||
|
||||
int countByTarget(@Param("targetType") String targetType, @Param("targetId") Long targetId);
|
||||
|
||||
int deleteByTarget(@Param("targetType") String targetType, @Param("targetId") Long targetId);
|
||||
|
||||
int deleteByMember(@Param("memberId") Long memberId);
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import com.sb.web.board.dto.Recommender;
|
||||
import com.sb.web.board.dto.VoteResponse;
|
||||
import com.sb.web.board.mapper.CommentMapper;
|
||||
import com.sb.web.board.mapper.PostMapper;
|
||||
import com.sb.web.board.mapper.ReportMapper;
|
||||
import com.sb.web.board.mapper.TagMapper;
|
||||
import com.sb.web.board.mapper.VoteMapper;
|
||||
import com.sb.web.common.exception.ApiException;
|
||||
@@ -42,7 +41,6 @@ public class BoardService {
|
||||
private final TagMapper tagMapper;
|
||||
private final CommentMapper commentMapper;
|
||||
private final VoteMapper voteMapper;
|
||||
private final ReportMapper reportMapper;
|
||||
private final PointService pointService;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@@ -50,10 +48,6 @@ public class BoardService {
|
||||
private static final Duration VIEW_DEDUP_TTL = Duration.ofHours(24);
|
||||
private static final String UP = "UP";
|
||||
private static final String DOWN = "DOWN";
|
||||
private static final String T_POST = "POST";
|
||||
private static final String T_COMMENT = "COMMENT";
|
||||
/** 신고 누적 블라인드 임계 */
|
||||
private static final int REPORT_BLIND_THRESHOLD = 5;
|
||||
|
||||
// 인기 글/댓글: 등록 1일 이내 + 추천 50건 이상이면 상단에 최대 3건 노출
|
||||
private static final int HOT_WITHIN_HOURS = 24;
|
||||
@@ -89,22 +83,6 @@ public class BoardService {
|
||||
return PageResponse.of(content, p, s, total);
|
||||
}
|
||||
|
||||
/** 내 글 모아보기 */
|
||||
public PageResponse<PostSummary> myPosts(Long memberId, int page, int size) {
|
||||
int p = Math.max(page, 1);
|
||||
int s = Math.min(Math.max(size, 1), 100);
|
||||
List<PostSummary> content = postMapper.findMyPage(memberId, (p - 1) * s, s);
|
||||
return PageResponse.of(content, p, s, postMapper.countMyPosts(memberId));
|
||||
}
|
||||
|
||||
/** 내 댓글 모아보기 */
|
||||
public PageResponse<com.sb.web.board.dto.MyCommentResponse> myComments(Long memberId, int page, int size) {
|
||||
int p = Math.max(page, 1);
|
||||
int s = Math.min(Math.max(size, 1), 100);
|
||||
var content = commentMapper.findMyPage(memberId, (p - 1) * s, s);
|
||||
return PageResponse.of(content, p, s, commentMapper.countMyComments(memberId));
|
||||
}
|
||||
|
||||
private String normalizeSearchType(String t) {
|
||||
return ("content".equals(t) || "comment".equals(t)) ? t : "title";
|
||||
}
|
||||
@@ -141,22 +119,6 @@ public class BoardService {
|
||||
assertAdmin(user);
|
||||
mustFindPost(id);
|
||||
postMapper.updateBlocked(id, blocked, blocked ? reason : null);
|
||||
// 해제 시 신고 기록 초기화 → 다음 신고 1건에 곧바로 재블라인드되지 않게
|
||||
if (!blocked) {
|
||||
reportMapper.deleteByTarget(T_POST, id);
|
||||
}
|
||||
}
|
||||
|
||||
/** 댓글 블라인드 해제 (관리자 전용) — 신고 기록 초기화 */
|
||||
@Transactional
|
||||
public void unblockComment(Long commentId, SessionUser user) {
|
||||
assertAdmin(user);
|
||||
Comment comment = commentMapper.findById(commentId);
|
||||
if (comment == null) {
|
||||
throw new ApiException(HttpStatus.NOT_FOUND, "댓글을 찾을 수 없습니다.");
|
||||
}
|
||||
commentMapper.updateBlocked(commentId, false);
|
||||
reportMapper.deleteByTarget(T_COMMENT, commentId);
|
||||
}
|
||||
|
||||
/** 게시글 공지 설정/해제 (관리자 전용) — 목록 최상단 고정 */
|
||||
@@ -223,7 +185,6 @@ public class BoardService {
|
||||
// 투표 정리(댓글 투표는 댓글 삭제 전에 — 서브쿼리가 comment 참조)
|
||||
voteMapper.deleteCommentVotesByPost(id);
|
||||
voteMapper.deletePostVotesByPost(id);
|
||||
reportMapper.deleteByTarget(T_POST, id);
|
||||
tagMapper.deletePostTagsByPostId(id);
|
||||
commentMapper.deleteByPostId(id);
|
||||
postMapper.delete(id);
|
||||
@@ -256,43 +217,9 @@ public class BoardService {
|
||||
}
|
||||
assertCanModify(comment.getAuthorId(), user);
|
||||
voteMapper.deleteCommentVotesByComment(commentId);
|
||||
reportMapper.deleteByTarget(T_COMMENT, commentId);
|
||||
commentMapper.delete(commentId);
|
||||
}
|
||||
|
||||
/* ===================== 신고 ===================== */
|
||||
|
||||
/** 게시글 신고 — 회원당 1회, 누적 5건이면 블라인드(관리자만 열람) */
|
||||
@Transactional
|
||||
public void reportPost(Long postId, SessionUser user) {
|
||||
Post post = mustFindPost(postId);
|
||||
if (post.getAuthorId().equals(user.getId())) {
|
||||
throw new ApiException(HttpStatus.BAD_REQUEST, "본인 글은 신고할 수 없습니다.");
|
||||
}
|
||||
reportMapper.insertIgnore(T_POST, postId, user.getId(), null);
|
||||
if (!Boolean.TRUE.equals(post.getBlocked())
|
||||
&& reportMapper.countByTarget(T_POST, postId) >= REPORT_BLIND_THRESHOLD) {
|
||||
postMapper.updateBlocked(postId, true, "신고 누적으로 블라인드되었습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
/** 댓글 신고 — 회원당 1회, 누적 5건이면 블라인드(관리자만 열람) */
|
||||
@Transactional
|
||||
public void reportComment(Long commentId, SessionUser user) {
|
||||
Comment comment = commentMapper.findById(commentId);
|
||||
if (comment == null) {
|
||||
throw new ApiException(HttpStatus.NOT_FOUND, "댓글을 찾을 수 없습니다.");
|
||||
}
|
||||
if (comment.getAuthorId().equals(user.getId())) {
|
||||
throw new ApiException(HttpStatus.BAD_REQUEST, "본인 댓글은 신고할 수 없습니다.");
|
||||
}
|
||||
reportMapper.insertIgnore(T_COMMENT, commentId, user.getId(), null);
|
||||
if (!Boolean.TRUE.equals(comment.getBlocked())
|
||||
&& reportMapper.countByTarget(T_COMMENT, commentId) >= REPORT_BLIND_THRESHOLD) {
|
||||
commentMapper.updateBlocked(commentId, true);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================== 추천/비추천 ===================== */
|
||||
|
||||
/** 게시글 추천/비추천 토글. 같은 표 재요청 시 취소, 반대면 전환. */
|
||||
@@ -374,13 +301,6 @@ public class BoardService {
|
||||
}
|
||||
return cr;
|
||||
}).toList());
|
||||
// 신고 누적 블라인드 댓글: 관리자가 아니면 본문 숨김
|
||||
boolean viewerAdmin = isAdmin(viewer);
|
||||
if (!viewerAdmin) {
|
||||
comments.forEach(cr -> {
|
||||
if (Boolean.TRUE.equals(cr.getBlocked())) cr.setContent(null);
|
||||
});
|
||||
}
|
||||
comments = orderWithHot(comments);
|
||||
|
||||
PostDetail detail = PostDetail.of(post, tags, comments);
|
||||
|
||||
@@ -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 검사)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -95,22 +95,6 @@ CREATE TABLE IF NOT EXISTS comment (
|
||||
KEY idx_comment_post (post_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 댓글 신고 누적 블라인드 플래그 (멱등)
|
||||
ALTER TABLE comment ADD COLUMN IF NOT EXISTS blocked TINYINT(1) NOT NULL DEFAULT 0 COMMENT '신고 누적 블라인드';
|
||||
|
||||
-- 신고 (글/댓글). 회원당 대상별 1회. 5건 누적 시 블라인드(관리자만 열람).
|
||||
CREATE TABLE IF NOT EXISTS report (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
target_type VARCHAR(10) NOT NULL COMMENT 'POST / COMMENT',
|
||||
target_id BIGINT NOT NULL,
|
||||
member_id BIGINT NOT NULL COMMENT '신고자',
|
||||
reason VARCHAR(255) NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_report (target_type, target_id, member_id),
|
||||
KEY idx_report_target (target_type, target_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 게시글 추천/비추천 (회원당 1표, UP/DOWN). 같은 표 재요청 시 취소, 반대면 전환.
|
||||
CREATE TABLE IF NOT EXISTS post_vote (
|
||||
post_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>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -11,12 +11,11 @@
|
||||
<result property="authorGooglePicture" column="author_google_picture"/>
|
||||
<result property="authorProfileImage" column="author_profile_image"/>
|
||||
<result property="content" column="content"/>
|
||||
<result property="blocked" column="blocked"/>
|
||||
<result property="createdAt" column="created_at"/>
|
||||
</resultMap>
|
||||
|
||||
<select id="findByPostId" parameterType="long" resultMap="commentResultMap">
|
||||
SELECT c.id, c.post_id, c.author_id, c.author_name, c.content, c.blocked, c.created_at,
|
||||
SELECT c.id, c.post_id, c.author_id, c.author_name, c.content, c.created_at,
|
||||
m.google_picture AS author_google_picture, m.profile_image AS author_profile_image
|
||||
FROM comment c
|
||||
LEFT JOIN member m ON m.id = c.author_id
|
||||
@@ -25,30 +24,13 @@
|
||||
</select>
|
||||
|
||||
<select id="findById" parameterType="long" resultMap="commentResultMap">
|
||||
SELECT c.id, c.post_id, c.author_id, c.author_name, c.content, c.blocked, c.created_at,
|
||||
SELECT c.id, c.post_id, c.author_id, c.author_name, c.content, c.created_at,
|
||||
m.google_picture AS author_google_picture, m.profile_image AS author_profile_image
|
||||
FROM comment c
|
||||
LEFT JOIN member m ON m.id = c.author_id
|
||||
WHERE c.id = #{id}
|
||||
</select>
|
||||
|
||||
<update id="updateBlocked">
|
||||
UPDATE comment SET blocked = #{blocked} WHERE id = #{id}
|
||||
</update>
|
||||
|
||||
<select id="findMyPage" resultType="com.sb.web.board.dto.MyCommentResponse">
|
||||
SELECT c.id, c.content, c.created_at, p.id AS post_id, p.title AS post_title, p.category
|
||||
FROM comment c
|
||||
JOIN post p ON p.id = c.post_id
|
||||
WHERE c.author_id = #{memberId}
|
||||
ORDER BY c.id DESC
|
||||
LIMIT #{size} OFFSET #{offset}
|
||||
</select>
|
||||
|
||||
<select id="countMyComments" parameterType="long" resultType="long">
|
||||
SELECT COUNT(*) FROM comment WHERE author_id = #{memberId}
|
||||
</select>
|
||||
|
||||
<insert id="insert" parameterType="com.sb.web.board.domain.Comment"
|
||||
useGeneratedKeys="true" keyProperty="id">
|
||||
INSERT INTO comment (post_id, author_id, author_name, content, created_at)
|
||||
|
||||
@@ -71,25 +71,6 @@
|
||||
<include refid="filter"/>
|
||||
</select>
|
||||
|
||||
<!-- 내 글 모아보기 -->
|
||||
<select id="findMyPage" resultType="com.sb.web.board.dto.PostSummary">
|
||||
SELECT
|
||||
p.id, p.title, p.category, p.author_id, p.author_name, p.view_count, p.blocked, p.notice, p.created_at,
|
||||
m.google_picture AS author_google_picture, m.profile_image AS author_profile_image,
|
||||
(SELECT COUNT(*) FROM comment c WHERE c.post_id = p.id) AS comment_count,
|
||||
(SELECT COUNT(*) FROM post_vote v WHERE v.post_id = p.id AND v.vote_type = 'UP') AS up_count,
|
||||
(SELECT COUNT(*) FROM post_vote v WHERE v.post_id = p.id AND v.vote_type = 'DOWN') AS down_count
|
||||
FROM post p
|
||||
LEFT JOIN member m ON m.id = p.author_id
|
||||
WHERE p.author_id = #{memberId}
|
||||
ORDER BY p.id DESC
|
||||
LIMIT #{size} OFFSET #{offset}
|
||||
</select>
|
||||
|
||||
<select id="countMyPosts" parameterType="long" resultType="long">
|
||||
SELECT COUNT(*) FROM post WHERE author_id = #{memberId}
|
||||
</select>
|
||||
|
||||
<select id="findById" parameterType="long" resultType="com.sb.web.board.domain.Post">
|
||||
SELECT p.id, p.title, p.content, p.author_id, p.author_name, p.view_count,
|
||||
p.blocked, p.block_reason, p.notice, p.category, p.created_at, p.updated_at,
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.sb.web.board.mapper.ReportMapper">
|
||||
|
||||
<insert id="insertIgnore">
|
||||
INSERT IGNORE INTO report (target_type, target_id, member_id, reason, created_at)
|
||||
VALUES (#{targetType}, #{targetId}, #{memberId}, #{reason}, NOW())
|
||||
</insert>
|
||||
|
||||
<select id="countByTarget" resultType="int">
|
||||
SELECT COUNT(*) FROM report WHERE target_type = #{targetType} AND target_id = #{targetId}
|
||||
</select>
|
||||
|
||||
<delete id="deleteByTarget">
|
||||
DELETE FROM report WHERE target_type = #{targetType} AND target_id = #{targetId}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteByMember">
|
||||
DELETE FROM report WHERE member_id = #{memberId}
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -52,10 +52,7 @@
|
||||
DELETE FROM board_image WHERE member_id = #{memberId}
|
||||
</delete>
|
||||
|
||||
<!-- 신고 / 포인트 / 결제 / 세션 -->
|
||||
<delete id="deleteReportsByMember">
|
||||
DELETE FROM report WHERE member_id = #{memberId}
|
||||
</delete>
|
||||
<!-- 포인트 / 결제 / 세션 -->
|
||||
<delete id="deletePointHistory">
|
||||
DELETE FROM point_history WHERE member_id = #{memberId}
|
||||
</delete>
|
||||
|
||||
Reference in New Issue
Block a user