- invest_trade.quantity BIGINT→DECIMAL(18,6): 평단·원가·평가/실현손익 계산을 BigDecimal로 전환 (금액은 원단위 정수 유지) - account_entry.installment_months 추가: 카드 지출 2~24개월 할부 기록(일시불 null), 응답에 포함 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,7 @@ public class AccountEntry implements Serializable {
|
|||||||
private String walletName; // 조인 표시용
|
private String walletName; // 조인 표시용
|
||||||
private Long toWalletId; // 이체 입금 계좌
|
private Long toWalletId; // 이체 입금 계좌
|
||||||
private String toWalletName;
|
private String toWalletName;
|
||||||
|
private Integer installmentMonths; // 카드 할부 개월수(2~24, 일시불은 null)
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import lombok.Builder;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ public class InvestTrade {
|
|||||||
private Long holdingId;
|
private Long holdingId;
|
||||||
private String tradeType; // BUY / SELL
|
private String tradeType; // BUY / SELL
|
||||||
private LocalDate tradeDate;
|
private LocalDate tradeDate;
|
||||||
private Long quantity; // 수량(주)
|
private BigDecimal quantity; // 수량(주, 소수점 매매 지원)
|
||||||
private Long price; // 단가(원)
|
private Long price; // 단가(원)
|
||||||
private Long fee; // 수수료/세금(원)
|
private Long fee; // 수수료/세금(원)
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.sb.web.account.dto;
|
package com.sb.web.account.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Max;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import jakarta.validation.constraints.Pattern;
|
import jakarta.validation.constraints.Pattern;
|
||||||
@@ -39,6 +41,11 @@ public class AccountEntryRequest {
|
|||||||
/** 이체 입금 계좌 ID (TRANSFER 시 필수) */
|
/** 이체 입금 계좌 ID (TRANSFER 시 필수) */
|
||||||
private Long toWalletId;
|
private Long toWalletId;
|
||||||
|
|
||||||
|
/** 카드 할부 개월수 (2~24, 일시불은 null). EXPENSE + 카드 결제에만 의미 있음 */
|
||||||
|
@Min(value = 2, message = "할부는 2개월 이상이어야 합니다.")
|
||||||
|
@Max(value = 24, message = "할부는 최대 24개월까지 가능합니다.")
|
||||||
|
private Integer installmentMonths;
|
||||||
|
|
||||||
/** 선택한 태그 ID 목록 (태그 관리에 등록된 태그, 선택) */
|
/** 선택한 태그 ID 목록 (태그 관리에 등록된 태그, 선택) */
|
||||||
private List<Long> tagIds;
|
private List<Long> tagIds;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public class AccountEntryResponse {
|
|||||||
private String walletName;
|
private String walletName;
|
||||||
private Long toWalletId;
|
private Long toWalletId;
|
||||||
private String toWalletName;
|
private String toWalletName;
|
||||||
|
private Integer installmentMonths;
|
||||||
private List<String> tags;
|
private List<String> tags;
|
||||||
|
|
||||||
public static AccountEntryResponse from(AccountEntry e, List<String> tags) {
|
public static AccountEntryResponse from(AccountEntry e, List<String> tags) {
|
||||||
@@ -38,6 +39,7 @@ public class AccountEntryResponse {
|
|||||||
.walletName(e.getWalletName())
|
.walletName(e.getWalletName())
|
||||||
.toWalletId(e.getToWalletId())
|
.toWalletId(e.getToWalletId())
|
||||||
.toWalletName(e.getToWalletName())
|
.toWalletName(e.getToWalletName())
|
||||||
|
.installmentMonths(e.getInstallmentMonths())
|
||||||
.tags(tags)
|
.tags(tags)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package com.sb.web.account.dto;
|
|||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 보유 종목 응답 (매매이력으로 산출한 집계 포함).
|
* 보유 종목 응답 (매매이력으로 산출한 집계 포함).
|
||||||
*/
|
*/
|
||||||
@@ -16,7 +18,7 @@ public class HoldingResponse {
|
|||||||
private String ticker;
|
private String ticker;
|
||||||
private Long currentPrice; // 현재가(수동, null 가능)
|
private Long currentPrice; // 현재가(수동, null 가능)
|
||||||
|
|
||||||
private Long quantity; // 보유수량 (매수−매도)
|
private BigDecimal quantity; // 보유수량 (매수−매도, 소수점 가능)
|
||||||
private Long avgPrice; // 평균매입단가 (보유분, 반올림 표시값)
|
private Long avgPrice; // 평균매입단가 (보유분, 반올림 표시값)
|
||||||
private Long costBasis; // 보유분 매입원가 합(수수료 포함)
|
private Long costBasis; // 보유분 매입원가 합(수수료 포함)
|
||||||
private Long evalValue; // 평가금액 = 수량 × (현재가 ?? 평단)
|
private Long evalValue; // 평가금액 = 수량 × (현재가 ?? 평단)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import jakarta.validation.constraints.Positive;
|
|||||||
import jakarta.validation.constraints.PositiveOrZero;
|
import jakarta.validation.constraints.PositiveOrZero;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -20,8 +21,8 @@ public class TradeRequest {
|
|||||||
@NotNull(message = "거래일을 입력하세요.")
|
@NotNull(message = "거래일을 입력하세요.")
|
||||||
private LocalDate tradeDate;
|
private LocalDate tradeDate;
|
||||||
|
|
||||||
@Positive(message = "수량은 1 이상이어야 합니다.")
|
@Positive(message = "수량은 0보다 커야 합니다.")
|
||||||
private Long quantity;
|
private BigDecimal quantity;
|
||||||
|
|
||||||
@PositiveOrZero(message = "단가는 0 이상이어야 합니다.")
|
@PositiveOrZero(message = "단가는 0 이상이어야 합니다.")
|
||||||
private Long price;
|
private Long price;
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import com.sb.web.account.domain.InvestTrade;
|
|||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,12 +19,16 @@ public class TradeResponse {
|
|||||||
private Long holdingId;
|
private Long holdingId;
|
||||||
private String tradeType;
|
private String tradeType;
|
||||||
private LocalDate tradeDate;
|
private LocalDate tradeDate;
|
||||||
private Long quantity;
|
private BigDecimal quantity;
|
||||||
private Long price;
|
private Long price;
|
||||||
private Long fee;
|
private Long fee;
|
||||||
private Long amount; // 거래금액 = 수량 × 단가 (수수료 제외)
|
private Long amount; // 거래금액 = 수량 × 단가 (수수료 제외, 원 단위 반올림)
|
||||||
|
|
||||||
public static TradeResponse from(InvestTrade t) {
|
public static TradeResponse from(InvestTrade t) {
|
||||||
|
long amount = t.getQuantity()
|
||||||
|
.multiply(BigDecimal.valueOf(t.getPrice()))
|
||||||
|
.setScale(0, RoundingMode.HALF_UP)
|
||||||
|
.longValue();
|
||||||
return TradeResponse.builder()
|
return TradeResponse.builder()
|
||||||
.id(t.getId())
|
.id(t.getId())
|
||||||
.holdingId(t.getHoldingId())
|
.holdingId(t.getHoldingId())
|
||||||
@@ -31,7 +37,7 @@ public class TradeResponse {
|
|||||||
.quantity(t.getQuantity())
|
.quantity(t.getQuantity())
|
||||||
.price(t.getPrice())
|
.price(t.getPrice())
|
||||||
.fee(t.getFee())
|
.fee(t.getFee())
|
||||||
.amount(t.getQuantity() * t.getPrice())
|
.amount(amount)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ public class AccountService {
|
|||||||
.memo(req.getMemo())
|
.memo(req.getMemo())
|
||||||
.walletId(req.getWalletId())
|
.walletId(req.getWalletId())
|
||||||
.toWalletId(transfer ? req.getToWalletId() : null)
|
.toWalletId(transfer ? req.getToWalletId() : null)
|
||||||
|
.installmentMonths(installmentOf(req))
|
||||||
.build();
|
.build();
|
||||||
mapper.insert(entry);
|
mapper.insert(entry);
|
||||||
applyTags(entry.getId(), req.getTagIds(), memberId);
|
applyTags(entry.getId(), req.getTagIds(), memberId);
|
||||||
@@ -186,6 +187,7 @@ public class AccountService {
|
|||||||
entry.setMemo(req.getMemo());
|
entry.setMemo(req.getMemo());
|
||||||
entry.setWalletId(req.getWalletId());
|
entry.setWalletId(req.getWalletId());
|
||||||
entry.setToWalletId(transfer ? req.getToWalletId() : null);
|
entry.setToWalletId(transfer ? req.getToWalletId() : null);
|
||||||
|
entry.setInstallmentMonths(installmentOf(req));
|
||||||
mapper.update(entry);
|
mapper.update(entry);
|
||||||
|
|
||||||
mapper.deleteEntryTagsByEntryId(id);
|
mapper.deleteEntryTagsByEntryId(id);
|
||||||
@@ -448,6 +450,15 @@ public class AccountService {
|
|||||||
return AccountEntryResponse.from(entry, mapper.findTagNamesByEntryId(entry.getId()));
|
return AccountEntryResponse.from(entry, mapper.findTagNamesByEntryId(entry.getId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 할부 개월수 정규화: 지출(EXPENSE)이고 2개월 이상일 때만 저장, 그 외(일시불·수입·이체)는 null */
|
||||||
|
private Integer installmentOf(AccountEntryRequest req) {
|
||||||
|
Integer m = req.getInstallmentMonths();
|
||||||
|
if (!"EXPENSE".equals(req.getType()) || m == null || m < 2) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
/** 선택된 태그 ID 중 해당 사용자 소유로 존재하는 것만 연결 */
|
/** 선택된 태그 ID 중 해당 사용자 소유로 존재하는 것만 연결 */
|
||||||
private void applyTags(Long entryId, List<Long> tagIds, Long memberId) {
|
private void applyTags(Long entryId, List<Long> tagIds, Long memberId) {
|
||||||
if (tagIds == null || tagIds.isEmpty()) {
|
if (tagIds == null || tagIds.isEmpty()) {
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import org.springframework.http.HttpStatus;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -86,9 +88,9 @@ public class InvestService {
|
|||||||
String type = "SELL".equals(req.getTradeType()) ? "SELL" : "BUY";
|
String type = "SELL".equals(req.getTradeType()) ? "SELL" : "BUY";
|
||||||
if ("SELL".equals(type)) {
|
if ("SELL".equals(type)) {
|
||||||
Calc c = calc(investMapper.findTradesByHolding(holdingId, memberId));
|
Calc c = calc(investMapper.findTradesByHolding(holdingId, memberId));
|
||||||
if (req.getQuantity() > c.quantity()) {
|
if (req.getQuantity().compareTo(c.quantity()) > 0) {
|
||||||
throw new ApiException(HttpStatus.BAD_REQUEST,
|
throw new ApiException(HttpStatus.BAD_REQUEST,
|
||||||
"매도 수량이 보유수량(" + c.quantity() + ")을 초과합니다.");
|
"매도 수량이 보유수량(" + c.quantity().stripTrailingZeros().toPlainString() + ")을 초과합니다.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
InvestTrade t = InvestTrade.builder()
|
InvestTrade t = InvestTrade.builder()
|
||||||
@@ -124,7 +126,7 @@ public class InvestService {
|
|||||||
long evalPrice = h.getCurrentPrice() != null ? h.getCurrentPrice() : c.avgPrice();
|
long evalPrice = h.getCurrentPrice() != null ? h.getCurrentPrice() : c.avgPrice();
|
||||||
WalletInvest wi = result.computeIfAbsent(h.getWalletId(), k -> new WalletInvest());
|
WalletInvest wi = result.computeIfAbsent(h.getWalletId(), k -> new WalletInvest());
|
||||||
wi.cashDelta += cashDelta(trades);
|
wi.cashDelta += cashDelta(trades);
|
||||||
wi.stockEval += c.quantity() * evalPrice;
|
wi.stockEval += amountOf(c.quantity(), evalPrice);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -145,25 +147,28 @@ public class InvestService {
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 매매이력(시간순)으로 보유수량·원가·실현손익 산출 */
|
/** 매매이력(시간순)으로 보유수량·원가·실현손익 산출. 수량은 소수점, 금액은 원 단위 정수. */
|
||||||
private Calc calc(List<InvestTrade> trades) {
|
private Calc calc(List<InvestTrade> trades) {
|
||||||
long qty = 0, costBasis = 0, realized = 0;
|
BigDecimal qty = BigDecimal.ZERO;
|
||||||
|
long costBasis = 0, realized = 0;
|
||||||
for (InvestTrade t : trades) {
|
for (InvestTrade t : trades) {
|
||||||
long amount = t.getQuantity() * t.getPrice();
|
long amount = amountOf(t.getQuantity(), t.getPrice());
|
||||||
long fee = t.getFee() != null ? t.getFee() : 0L;
|
long fee = t.getFee() != null ? t.getFee() : 0L;
|
||||||
if ("BUY".equals(t.getTradeType())) {
|
if ("BUY".equals(t.getTradeType())) {
|
||||||
qty += t.getQuantity();
|
qty = qty.add(t.getQuantity());
|
||||||
costBasis += amount + fee;
|
costBasis += amount + fee;
|
||||||
} else { // SELL
|
} else { // SELL
|
||||||
long costRemoved = (qty > 0 && t.getQuantity() < qty)
|
// 부분 매도 시 보유원가를 수량 비율로 제거, 전량 매도 시 잔여 원가 전부 제거(반올림 잔차 방지)
|
||||||
? Math.round(costBasis * (double) t.getQuantity() / qty)
|
boolean partial = qty.signum() > 0 && t.getQuantity().compareTo(qty) < 0;
|
||||||
: costBasis; // 전량 매도 시 잔여 원가 전부 제거(반올림 잔차 방지)
|
long costRemoved = partial
|
||||||
|
? Math.round(costBasis * t.getQuantity().doubleValue() / qty.doubleValue())
|
||||||
|
: costBasis;
|
||||||
long proceeds = amount - fee;
|
long proceeds = amount - fee;
|
||||||
realized += proceeds - costRemoved;
|
realized += proceeds - costRemoved;
|
||||||
costBasis -= costRemoved;
|
costBasis -= costRemoved;
|
||||||
qty -= t.getQuantity();
|
qty = qty.subtract(t.getQuantity());
|
||||||
if (qty <= 0) {
|
if (qty.signum() <= 0) {
|
||||||
qty = 0;
|
qty = BigDecimal.ZERO;
|
||||||
costBasis = 0;
|
costBasis = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,17 +176,24 @@ public class InvestService {
|
|||||||
return new Calc(qty, costBasis, realized);
|
return new Calc(qty, costBasis, realized);
|
||||||
}
|
}
|
||||||
|
|
||||||
private record Calc(long quantity, long costBasis, long realized) {
|
private record Calc(BigDecimal quantity, long costBasis, long realized) {
|
||||||
long avgPrice() {
|
long avgPrice() {
|
||||||
return quantity > 0 ? Math.round(costBasis / (double) quantity) : 0;
|
return quantity.signum() > 0 ? Math.round(costBasis / quantity.doubleValue()) : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 거래금액(원) = 수량 × 단가, 원 단위 반올림 */
|
||||||
|
private static long amountOf(BigDecimal quantity, long price) {
|
||||||
|
return quantity.multiply(BigDecimal.valueOf(price))
|
||||||
|
.setScale(0, RoundingMode.HALF_UP)
|
||||||
|
.longValue();
|
||||||
|
}
|
||||||
|
|
||||||
/** 매매로 인한 예수금 증감 = −Σ매수(금액+수수료) + Σ매도(금액−수수료) */
|
/** 매매로 인한 예수금 증감 = −Σ매수(금액+수수료) + Σ매도(금액−수수료) */
|
||||||
private long cashDelta(List<InvestTrade> trades) {
|
private long cashDelta(List<InvestTrade> trades) {
|
||||||
long cashDelta = 0;
|
long cashDelta = 0;
|
||||||
for (InvestTrade t : trades) {
|
for (InvestTrade t : trades) {
|
||||||
long amount = t.getQuantity() * t.getPrice();
|
long amount = amountOf(t.getQuantity(), t.getPrice());
|
||||||
long fee = t.getFee() != null ? t.getFee() : 0L;
|
long fee = t.getFee() != null ? t.getFee() : 0L;
|
||||||
cashDelta += "BUY".equals(t.getTradeType()) ? -(amount + fee) : (amount - fee);
|
cashDelta += "BUY".equals(t.getTradeType()) ? -(amount + fee) : (amount - fee);
|
||||||
}
|
}
|
||||||
@@ -192,7 +204,7 @@ public class InvestService {
|
|||||||
Calc c = calc(trades);
|
Calc c = calc(trades);
|
||||||
long avgPrice = c.avgPrice();
|
long avgPrice = c.avgPrice();
|
||||||
long evalPrice = h.getCurrentPrice() != null ? h.getCurrentPrice() : avgPrice;
|
long evalPrice = h.getCurrentPrice() != null ? h.getCurrentPrice() : avgPrice;
|
||||||
long evalValue = c.quantity() * evalPrice;
|
long evalValue = amountOf(c.quantity(), evalPrice);
|
||||||
long evalGain = evalValue - c.costBasis();
|
long evalGain = evalValue - c.costBasis();
|
||||||
Double returnPct = c.costBasis() > 0 ? Math.round(evalGain * 1000.0 / c.costBasis()) / 10.0 : null;
|
Double returnPct = c.costBasis() > 0 ? Math.round(evalGain * 1000.0 / c.costBasis()) / 10.0 : null;
|
||||||
return HoldingResponse.builder()
|
return HoldingResponse.builder()
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ CREATE TABLE IF NOT EXISTS account_entry (
|
|||||||
memo VARCHAR(255) NULL,
|
memo VARCHAR(255) NULL,
|
||||||
wallet_id BIGINT NULL COMMENT '발생/출금 계좌(wallet.id)',
|
wallet_id BIGINT NULL COMMENT '발생/출금 계좌(wallet.id)',
|
||||||
to_wallet_id BIGINT NULL COMMENT '이체 입금 계좌(TRANSFER)',
|
to_wallet_id BIGINT NULL COMMENT '이체 입금 계좌(TRANSFER)',
|
||||||
|
installment_months INT NULL COMMENT '카드 할부 개월수(2~24, 일시불은 NULL)',
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
@@ -48,6 +49,7 @@ CREATE TABLE IF NOT EXISTS account_entry (
|
|||||||
-- 기존 account_entry 컬럼 추가 (멱등)
|
-- 기존 account_entry 컬럼 추가 (멱등)
|
||||||
ALTER TABLE account_entry ADD COLUMN IF NOT EXISTS wallet_id BIGINT NULL;
|
ALTER TABLE account_entry ADD COLUMN IF NOT EXISTS wallet_id BIGINT NULL;
|
||||||
ALTER TABLE account_entry ADD COLUMN IF NOT EXISTS to_wallet_id BIGINT NULL;
|
ALTER TABLE account_entry ADD COLUMN IF NOT EXISTS to_wallet_id BIGINT NULL;
|
||||||
|
ALTER TABLE account_entry ADD COLUMN IF NOT EXISTS installment_months INT NULL;
|
||||||
|
|
||||||
-- 가계부 태그 (사용자별 관리 — 게시판 태그와 분리)
|
-- 가계부 태그 (사용자별 관리 — 게시판 태그와 분리)
|
||||||
CREATE TABLE IF NOT EXISTS account_tag (
|
CREATE TABLE IF NOT EXISTS account_tag (
|
||||||
@@ -155,7 +157,7 @@ CREATE TABLE IF NOT EXISTS invest_trade (
|
|||||||
holding_id BIGINT NOT NULL COMMENT 'invest_holding.id',
|
holding_id BIGINT NOT NULL COMMENT 'invest_holding.id',
|
||||||
trade_type VARCHAR(10) NOT NULL COMMENT 'BUY / SELL',
|
trade_type VARCHAR(10) NOT NULL COMMENT 'BUY / SELL',
|
||||||
trade_date DATE NOT NULL,
|
trade_date DATE NOT NULL,
|
||||||
quantity BIGINT NOT NULL COMMENT '수량(주)',
|
quantity DECIMAL(18,6) NOT NULL COMMENT '수량(주, 소수점 매매 지원)',
|
||||||
price BIGINT NOT NULL COMMENT '단가(원)',
|
price BIGINT NOT NULL COMMENT '단가(원)',
|
||||||
fee BIGINT NOT NULL DEFAULT 0 COMMENT '수수료/세금(원)',
|
fee BIGINT NOT NULL DEFAULT 0 COMMENT '수수료/세금(원)',
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
@@ -163,3 +165,6 @@ CREATE TABLE IF NOT EXISTS invest_trade (
|
|||||||
KEY idx_trade_member_holding (member_id, holding_id),
|
KEY idx_trade_member_holding (member_id, holding_id),
|
||||||
KEY idx_trade_date (holding_id, trade_date, id)
|
KEY idx_trade_date (holding_id, trade_date, id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 소수점 매매(예: 해외주식 소수점 매수) 지원: 기존 BIGINT → DECIMAL
|
||||||
|
ALTER TABLE invest_trade MODIFY COLUMN quantity DECIMAL(18,6) NOT NULL COMMENT '수량(주, 소수점 매매 지원)';
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
<result property="walletName" column="wallet_name"/>
|
<result property="walletName" column="wallet_name"/>
|
||||||
<result property="toWalletId" column="to_wallet_id"/>
|
<result property="toWalletId" column="to_wallet_id"/>
|
||||||
<result property="toWalletName" column="to_wallet_name"/>
|
<result property="toWalletName" column="to_wallet_name"/>
|
||||||
|
<result property="installmentMonths" column="installment_months"/>
|
||||||
<result property="createdAt" column="created_at"/>
|
<result property="createdAt" column="created_at"/>
|
||||||
<result property="updatedAt" column="updated_at"/>
|
<result property="updatedAt" column="updated_at"/>
|
||||||
</resultMap>
|
</resultMap>
|
||||||
@@ -28,6 +29,7 @@
|
|||||||
e.id, e.member_id, e.entry_date, e.type, e.category, e.amount, e.memo,
|
e.id, e.member_id, e.entry_date, e.type, e.category, e.amount, e.memo,
|
||||||
e.wallet_id, w.name AS wallet_name,
|
e.wallet_id, w.name AS wallet_name,
|
||||||
e.to_wallet_id, tw.name AS to_wallet_name,
|
e.to_wallet_id, tw.name AS to_wallet_name,
|
||||||
|
e.installment_months,
|
||||||
e.created_at, e.updated_at
|
e.created_at, e.updated_at
|
||||||
</sql>
|
</sql>
|
||||||
<sql id="entryJoins">
|
<sql id="entryJoins">
|
||||||
@@ -125,15 +127,16 @@
|
|||||||
<insert id="insert" parameterType="com.sb.web.account.domain.AccountEntry"
|
<insert id="insert" parameterType="com.sb.web.account.domain.AccountEntry"
|
||||||
useGeneratedKeys="true" keyProperty="id">
|
useGeneratedKeys="true" keyProperty="id">
|
||||||
INSERT INTO account_entry (member_id, entry_date, type, category, amount, memo,
|
INSERT INTO account_entry (member_id, entry_date, type, category, amount, memo,
|
||||||
wallet_id, to_wallet_id, created_at, updated_at)
|
wallet_id, to_wallet_id, installment_months, created_at, updated_at)
|
||||||
VALUES (#{memberId}, #{entryDate}, #{type}, #{category}, #{amount}, #{memo},
|
VALUES (#{memberId}, #{entryDate}, #{type}, #{category}, #{amount}, #{memo},
|
||||||
#{walletId}, #{toWalletId}, NOW(), NOW())
|
#{walletId}, #{toWalletId}, #{installmentMonths}, NOW(), NOW())
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
<update id="update" parameterType="com.sb.web.account.domain.AccountEntry">
|
<update id="update" parameterType="com.sb.web.account.domain.AccountEntry">
|
||||||
UPDATE account_entry
|
UPDATE account_entry
|
||||||
SET entry_date = #{entryDate}, type = #{type}, category = #{category},
|
SET entry_date = #{entryDate}, type = #{type}, category = #{category},
|
||||||
amount = #{amount}, memo = #{memo}, wallet_id = #{walletId}, to_wallet_id = #{toWalletId}
|
amount = #{amount}, memo = #{memo}, wallet_id = #{walletId}, to_wallet_id = #{toWalletId},
|
||||||
|
installment_months = #{installmentMonths}
|
||||||
WHERE id = #{id} AND member_id = #{memberId}
|
WHERE id = #{id} AND member_id = #{memberId}
|
||||||
</update>
|
</update>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user