- 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:
@@ -3,6 +3,7 @@ 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;
|
||||
@@ -241,6 +242,14 @@ public class AccountController {
|
||||
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(
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.sb.web.account.controller;
|
||||
|
||||
import com.sb.web.account.dto.QuickEntryRequest;
|
||||
import com.sb.web.account.dto.QuickEntryResponse;
|
||||
import com.sb.web.account.service.QuickEntryService;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 자주 쓰는 내역(빠른 등록 템플릿) API. (/api/account/quick-entries — 로그인 필요, 본인 데이터만)
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/account/quick-entries")
|
||||
@RequiredArgsConstructor
|
||||
public class QuickEntryController {
|
||||
|
||||
private final QuickEntryService service;
|
||||
|
||||
@GetMapping
|
||||
public List<QuickEntryResponse> list(
|
||||
@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) {
|
||||
return service.list(current.getId());
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<QuickEntryResponse> create(
|
||||
@Valid @RequestBody QuickEntryRequest req,
|
||||
@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) {
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(service.create(req, current.getId()));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> delete(
|
||||
@PathVariable Long id,
|
||||
@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) {
|
||||
service.delete(id, current.getId());
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.sb.web.account.domain;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 자주 쓰는 내역(빠른 등록 템플릿) — 사용자별.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class QuickEntry implements Serializable {
|
||||
|
||||
private Long id;
|
||||
private Long memberId;
|
||||
private String label;
|
||||
private String type; // INCOME / EXPENSE
|
||||
private String category;
|
||||
private Long amount;
|
||||
private String memo;
|
||||
private Long walletId;
|
||||
private Integer sortOrder;
|
||||
private LocalDateTime createdAt;
|
||||
private String walletName; // 조회 시 조인(표시용)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.sb.web.account.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 문자/푸시 텍스트 파싱 결과 (내역 추가 폼 자동 채움용). 저장하지 않음.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public class ParsedEntryResponse {
|
||||
|
||||
private boolean recognized; // 거래로 인식되었는가
|
||||
private String type; // INCOME / EXPENSE
|
||||
private long amount;
|
||||
private String merchant; // 가맹점/적요(메모 후보)
|
||||
private LocalDate date;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.sb.web.account.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 자주 쓰는 내역 생성 요청.
|
||||
*/
|
||||
@Data
|
||||
public class QuickEntryRequest {
|
||||
|
||||
@Size(max = 50, message = "이름은 50자 이내여야 합니다.")
|
||||
private String label;
|
||||
|
||||
@Pattern(regexp = "INCOME|EXPENSE", message = "구분이 올바르지 않습니다.")
|
||||
private String type;
|
||||
|
||||
@Size(max = 50)
|
||||
private String category;
|
||||
|
||||
@NotNull(message = "금액을 입력하세요.")
|
||||
@Positive(message = "금액은 0보다 커야 합니다.")
|
||||
private Long amount;
|
||||
|
||||
@Size(max = 255)
|
||||
private String memo;
|
||||
|
||||
private Long walletId;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.sb.web.account.dto;
|
||||
|
||||
import com.sb.web.account.domain.QuickEntry;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 자주 쓰는 내역 응답.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public class QuickEntryResponse {
|
||||
|
||||
private Long id;
|
||||
private String label;
|
||||
private String type;
|
||||
private String category;
|
||||
private Long amount;
|
||||
private String memo;
|
||||
private Long walletId;
|
||||
private String walletName;
|
||||
|
||||
public static QuickEntryResponse from(QuickEntry q) {
|
||||
return QuickEntryResponse.builder()
|
||||
.id(q.getId()).label(q.getLabel()).type(q.getType()).category(q.getCategory())
|
||||
.amount(q.getAmount()).memo(q.getMemo()).walletId(q.getWalletId())
|
||||
.walletName(q.getWalletName())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.sb.web.account.mapper;
|
||||
|
||||
import com.sb.web.account.domain.QuickEntry;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 자주 쓰는 내역 매퍼 — 소유자(member_id) 격리.
|
||||
*/
|
||||
@Mapper
|
||||
public interface QuickEntryMapper {
|
||||
|
||||
List<QuickEntry> findByMember(@Param("memberId") Long memberId);
|
||||
|
||||
QuickEntry findByIdAndMember(@Param("id") Long id, @Param("memberId") Long memberId);
|
||||
|
||||
int insert(QuickEntry e);
|
||||
|
||||
int delete(@Param("id") Long id, @Param("memberId") Long memberId);
|
||||
|
||||
Integer maxSortOrder(@Param("memberId") Long memberId);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user