feat: 자주 쓰는 내역(빠른등록) + 문자/푸시 텍스트 파싱 API

- quick_entry 테이블 + 도메인/매퍼/서비스/컨트롤러 (/account/quick-entries CRUD)
- /account/entries/parse: 문자·푸시 텍스트를 파서로 분석해 결과만 반환(저장X)
  → iPhone 등 알림 자동수집 불가 시 복사→붙여넣기 자동채움용

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ByungCheol
2026-06-27 14:47:00 +09:00
co-authored by Claude Opus 4.8
parent 63b3ffea24
commit a1697a7a22
11 changed files with 327 additions and 0 deletions
@@ -8,6 +8,7 @@ import com.sb.web.account.dto.AccountEntryResponse;
import com.sb.web.account.dto.AccountSummary;
import com.sb.web.account.dto.ConfirmRequest;
import com.sb.web.account.dto.NotificationRequest;
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;
@@ -277,6 +278,19 @@ public class AccountService {
* 카드 결제 푸시 알림 → 미확인(pending) 지출 내역 생성.
* 결제 알림이 아니거나 중복이면 null 반환(생성 안 함). 취소 알림도 일단 무시(v1).
*/
/** 문자/푸시 텍스트만 파싱해 폼 자동채움용 결과 반환 (저장 안 함). iPhone 등 알림 자동수집 불가 대비. */
public ParsedEntryResponse parseText(NotificationRequest req) {
CardNotificationParser.Parsed p =
notificationParser.parse(req.getTitle(), req.getText(), java.time.LocalDate.now());
return ParsedEntryResponse.builder()
.recognized(p.isRecognized())
.type(p.isIncome() ? "INCOME" : "EXPENSE")
.amount(p.getAmount())
.merchant(p.getMerchant())
.date(p.getDate())
.build();
}
@Transactional
public AccountEntryResponse createFromNotification(NotificationRequest req, Long memberId) {
CardNotificationParser.Parsed p =
@@ -0,0 +1,56 @@
package com.sb.web.account.service;
import com.sb.web.account.domain.QuickEntry;
import com.sb.web.account.dto.QuickEntryRequest;
import com.sb.web.account.dto.QuickEntryResponse;
import com.sb.web.account.mapper.QuickEntryMapper;
import com.sb.web.common.exception.ApiException;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* 자주 쓰는 내역(빠른 등록 템플릿) 서비스 — 사용자별.
*/
@Service
@RequiredArgsConstructor
public class QuickEntryService {
private final QuickEntryMapper mapper;
public List<QuickEntryResponse> list(Long memberId) {
return mapper.findByMember(memberId).stream().map(QuickEntryResponse::from).toList();
}
@Transactional
public QuickEntryResponse create(QuickEntryRequest req, Long memberId) {
Integer max = mapper.maxSortOrder(memberId);
QuickEntry e = QuickEntry.builder()
.memberId(memberId)
.label(blankToNull(req.getLabel()))
.type(req.getType())
.category("EXPENSE".equals(req.getType()) || "INCOME".equals(req.getType()) ? blankToNull(req.getCategory()) : null)
.amount(req.getAmount())
.memo(blankToNull(req.getMemo()))
.walletId(req.getWalletId())
.sortOrder(max == null ? 0 : max + 1)
.build();
mapper.insert(e);
return QuickEntryResponse.from(mapper.findByIdAndMember(e.getId(), memberId));
}
@Transactional
public void delete(Long id, Long memberId) {
if (mapper.findByIdAndMember(id, memberId) == null) {
throw new ApiException(HttpStatus.NOT_FOUND, "자주 쓰는 내역을 찾을 수 없습니다.");
}
mapper.delete(id, memberId);
}
private String blankToNull(String s) {
return (s == null || s.isBlank()) ? null : s.trim();
}
}