feat: 시세 자동조회·퇴직연금 평가액·게시판 카테고리·태그 정렬·계좌번호 암호화

- 투자: 종목코드로 현재가 시세 자동조회(StockQuoteService, 투자탭/포트폴리오 진입 시 갱신)
- 투자: 퇴직연금 등 평가액 직접입력형 계좌(currentValue 활성화, manualValuation)
- 게시판: community/saving/tips 카테고리 분리(post.category + 목록 필터)
- 태그: 순서 변경(account_tag.sort_order, /tags/reorder)
- 보안: 계좌번호 AES-256-GCM 암호화 저장/복호화(EncryptedStringTypeHandler, account_number VARCHAR(255))
  키는 서버 .env ACCOUNT_CRYPTO_KEY 로 주입(미설정 시 평문 통과)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ByungCheol
2026-06-04 05:00:18 +09:00
co-authored by Claude Opus 4.8
parent 8fec057752
commit fb94df8112
24 changed files with 401 additions and 29 deletions
@@ -31,6 +31,7 @@ public class InvestService {
private final InvestMapper investMapper;
private final WalletMapper walletMapper;
private final StockQuoteService stockQuoteService;
/* ===================== 보유 종목 ===================== */
@@ -73,6 +74,37 @@ public class InvestService {
investMapper.deleteHolding(id, memberId);
}
/** 종목코드가 있는 보유종목의 현재가를 시세 조회로 갱신. 조회 성공한 종목만 반영 */
@Transactional
public List<HoldingResponse> refreshPrices(Long memberId, Long walletId) {
requireInvestWallet(walletId, memberId);
Map<String, Long> cache = new HashMap<>();
for (InvestHolding h : investMapper.findHoldingsByWallet(memberId, walletId)) {
applyQuote(h, cache);
}
return listHoldings(memberId, walletId);
}
/** 회원의 모든 투자계좌 보유종목 현재가를 일괄 갱신(계좌 목록 평가액 반영용) */
@Transactional
public void refreshAllPrices(Long memberId) {
Map<String, Long> cache = new HashMap<>();
for (InvestHolding h : investMapper.findHoldingsByMember(memberId)) {
applyQuote(h, cache);
}
}
/** 종목코드로 시세를 받아 현재가 갱신(동일 종목코드는 호출당 1회만 조회) */
private void applyQuote(InvestHolding h, Map<String, Long> cache) {
String ticker = h.getTicker();
if (ticker == null || ticker.isBlank()) return;
Long price = cache.computeIfAbsent(ticker.trim(), stockQuoteService::getDomesticPrice);
if (price != null && price > 0) {
h.setCurrentPrice(price);
investMapper.updateHolding(h);
}
}
/* ===================== 매매 ===================== */
public List<TradeResponse> listTrades(Long holdingId, Long memberId) {