feat: 투자 매매내역 최근N+전체보기 모달·매매 수정 + 안드로이드 인셋 보정
CI / build (push) Successful in 26s

- InvestPortfolio: 인라인은 최근 5건만, 초과분은 '전체 보기' 스크롤 모달
- 매매 행에 수정(✏)/삭제 버튼 — PUT /trades/{id} 로 매매 수정
- MainActivity: 콘텐츠 루트에 시스템 바 인셋 패딩(CONSUMED) — 에지투에지 소프트키/상태바 겹침 보정

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ByungCheol
2026-06-06 16:42:47 +09:00
parent 16b4bce136
commit 006fc87f70
3 changed files with 131 additions and 27 deletions
+3
View File
@@ -121,6 +121,9 @@ export const accountApi = {
addTrade(holdingId, payload) {
return http.post(`/account/invest/holdings/${holdingId}/trades`, payload)
},
updateTrade(id, payload) {
return http.put(`/account/invest/trades/${id}`, payload)
},
removeTrade(id) {
return http.delete(`/account/invest/trades/${id}`)
},
+116 -16
View File
@@ -19,6 +19,7 @@ const hError = ref(null)
// 매매 모달
const tradeModal = ref(false)
const tradeHolding = ref(null)
const tradeEditId = ref(null) // null=추가, 값=해당 매매 수정
// inputMode: 'PRICE'(수량×단가) | 'AMOUNT'(수량+총금액 → 단가 자동계산, 소수점 매수용)
const tForm = reactive({ tradeType: 'BUY', tradeDate: '', quantity: null, price: null, amount: null, fee: null, inputMode: 'PRICE' })
const tSubmitting = ref(false)
@@ -38,9 +39,26 @@ const tradeAmount = computed(() => {
return Math.round(tradeQty.value * (Number(tForm.price) || 0))
})
// 매매이력 드롭다운
// 매매이력 드롭다운 — 인라인은 최근 N건, 전체는 모달
const openTradesId = ref(null)
const tradesByHolding = reactive({})
const RECENT_N = 5
const allTradesModal = ref(false)
const allTradesHolding = ref(null)
function tradesOf(h) {
return tradesByHolding[h.id] || []
}
function recentTrades(h) {
return [...tradesOf(h)].reverse().slice(0, RECENT_N) // 최신순 N건
}
const allTradesList = computed(() => {
const id = allTradesHolding.value?.id
return id ? [...(tradesByHolding[id] || [])].reverse() : []
})
function openAllTrades(h) {
allTradesHolding.value = h
allTradesModal.value = true
}
function won(n) {
return (n ?? 0).toLocaleString('ko-KR')
@@ -58,6 +76,13 @@ function tDate(v) {
const d = new Date(v)
return Number.isNaN(d.getTime()) ? v : `${String(d.getMonth() + 1).padStart(2, '0')}.${String(d.getDate()).padStart(2, '0')}`
}
// 날짜 input(yyyy-mm-dd) 용 변환 (수정 진입 시 프리필)
function toDateInput(v) {
if (!v) return todayStr()
const d = new Date(v)
if (Number.isNaN(d.getTime())) return String(v).slice(0, 10)
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
}
async function load() {
loading.value = true
@@ -143,10 +168,27 @@ async function removeHolding(h) {
/* ===== 매매 ===== */
function openTrade(h, type) {
tradeHolding.value = h
tradeEditId.value = null
Object.assign(tForm, { tradeType: type, tradeDate: todayStr(), quantity: null, price: h.currentPrice ?? null, amount: null, fee: null, inputMode: 'PRICE' })
tError.value = null
tradeModal.value = true
}
// 기존 매매 수정 진입 — 단가 모드로 프리필
function openEditTrade(h, t) {
tradeHolding.value = h
tradeEditId.value = t.id
Object.assign(tForm, {
tradeType: t.tradeType,
tradeDate: toDateInput(t.tradeDate),
quantity: Number(t.quantity),
price: t.price,
amount: null,
fee: t.fee || null,
inputMode: 'PRICE',
})
tError.value = null
tradeModal.value = true
}
async function submitTrade() {
tError.value = null
if (!tForm.quantity || tForm.quantity <= 0) {
@@ -171,9 +213,13 @@ async function submitTrade() {
fee: tForm.fee ? Number(tForm.fee) : 0,
}
try {
await accountApi.addTrade(tradeHolding.value.id, payload)
if (tradeEditId.value) {
await accountApi.updateTrade(tradeEditId.value, payload)
} else {
await accountApi.addTrade(tradeHolding.value.id, payload)
}
tradeModal.value = false
if (openTradesId.value === tradeHolding.value.id) await loadTrades(tradeHolding.value.id)
await loadTrades(tradeHolding.value.id) // 인라인 최근목록·전체보기 모달 동시 갱신
await load()
emit('changed')
} catch (e) {
@@ -269,18 +315,24 @@ onMounted(async () => {
</div>
</div>
<!-- 매매이력 -->
<!-- 매매이력 (최근 N건, 전체는 모달) -->
<div v-if="openTradesId === h.id" class="trade-drop">
<ul v-if="(tradesByHolding[h.id] || []).length" class="trade-list">
<li v-for="t in tradesByHolding[h.id]" :key="t.id" class="trade-row">
<span class="t-date">{{ tDate(t.tradeDate) }}</span>
<span class="t-type" :class="t.tradeType === 'SELL' ? 'sell' : 'buy'">{{ t.tradeType === 'SELL' ? '매도' : '매수' }}</span>
<span class="t-qty">{{ shares(t.quantity) }} × {{ won(t.price) }}</span>
<span class="t-fee" v-if="t.fee">수수료 {{ won(t.fee) }}</span>
<span class="t-amt">{{ won(t.amount) }}</span>
<IconBtn class="t-del" icon="trash" title="매매 삭제" variant="danger" size="sm" @click="removeTrade(t, h.id)" />
</li>
</ul>
<template v-if="tradesOf(h).length">
<ul class="trade-list">
<li v-for="t in recentTrades(h)" :key="t.id" class="trade-row">
<span class="t-date">{{ tDate(t.tradeDate) }}</span>
<span class="t-type" :class="t.tradeType === 'SELL' ? 'sell' : 'buy'">{{ t.tradeType === 'SELL' ? '매도' : '매수' }}</span>
<span class="t-qty">{{ shares(t.quantity) }} × {{ won(t.price) }}</span>
<span class="t-fee" v-if="t.fee">수수료 {{ won(t.fee) }}</span>
<span class="t-amt">{{ won(t.amount) }}</span>
<IconBtn icon="edit" title="매매 수정" size="sm" @click="openEditTrade(h, t)" />
<IconBtn class="t-del" icon="trash" title="매매 삭제" variant="danger" size="sm" @click="removeTrade(t, h.id)" />
</li>
</ul>
<button v-if="tradesOf(h).length > RECENT_N" type="button" class="more-trades" @click="openAllTrades(h)">
전체 {{ tradesOf(h).length }} 보기
</button>
</template>
<p v-else class="pf-msg sm">매매 내역이 없습니다.</p>
</div>
</li>
@@ -315,7 +367,7 @@ onMounted(async () => {
<div v-if="tradeModal" class="modal-backdrop" @click.self="tradeModal = false">
<div class="modal" role="dialog" aria-modal="true">
<button class="close" type="button" @click="tradeModal = false">×</button>
<h2>{{ tradeHolding?.name }} {{ tForm.tradeType === 'SELL' ? '매도' : '매수' }}</h2>
<h2>{{ tradeHolding?.name }} {{ tradeEditId ? '매매 수정' : (tForm.tradeType === 'SELL' ? '매도' : '매수') }}</h2>
<form class="pf-form" @submit.prevent="submitTrade">
<label>구분
<select v-model="tForm.tradeType" :disabled="tSubmitting">
@@ -344,13 +396,36 @@ onMounted(async () => {
<p v-if="tError" class="pf-msg err">{{ tError }}</p>
<div class="pf-buttons">
<IconBtn icon="close" title="취소" @click="tradeModal = false" />
<IconBtn icon="save" title="등록" variant="primary" type="submit" :disabled="tSubmitting" />
<IconBtn icon="save" :title="tradeEditId ? '수정' : '등록'" variant="primary" type="submit" :disabled="tSubmitting" />
</div>
</form>
</div>
</div>
</Transition>
</Teleport>
<!-- 전체 매매내역 모달 (스크롤) -->
<Teleport to="body">
<Transition name="fade">
<div v-if="allTradesModal" class="modal-backdrop trades-backdrop" @click.self="allTradesModal = false">
<div class="modal trades-modal" role="dialog" aria-modal="true">
<button class="close" type="button" @click="allTradesModal = false">×</button>
<h2>{{ allTradesHolding?.name }} 매매내역</h2>
<ul class="trade-list modal-trades">
<li v-for="t in allTradesList" :key="t.id" class="trade-row">
<span class="t-date">{{ tDate(t.tradeDate) }}</span>
<span class="t-type" :class="t.tradeType === 'SELL' ? 'sell' : 'buy'">{{ t.tradeType === 'SELL' ? '매도' : '매수' }}</span>
<span class="t-qty">{{ shares(t.quantity) }} × {{ won(t.price) }}</span>
<span class="t-fee" v-if="t.fee">수수료 {{ won(t.fee) }}</span>
<span class="t-amt">{{ won(t.amount) }}</span>
<IconBtn icon="edit" title="매매 수정" size="sm" @click="openEditTrade(allTradesHolding, t)" />
<IconBtn class="t-del" icon="trash" title="매매 삭제" variant="danger" size="sm" @click="removeTrade(t, allTradesHolding.id)" />
</li>
</ul>
</div>
</div>
</Transition>
</Teleport>
</div>
</template>
@@ -520,6 +595,31 @@ onMounted(async () => {
.trade-drop {
padding: 0.1rem 0 0.5rem 1.4rem;
}
.more-trades {
margin-top: 0.2rem;
padding: 0.25rem 0.6rem;
font-size: 0.76rem;
border: 1px solid var(--color-border);
border-radius: 4px;
background: var(--color-background-soft);
color: var(--color-text);
cursor: pointer;
}
.more-trades:hover {
border-color: var(--color-border-hover);
}
/* 전체보기 모달: 매매목록 아래 매수 모달이 위에 오도록 한 단계 낮게 */
.modal-backdrop.trades-backdrop {
z-index: 1090;
}
.trades-modal {
max-width: 420px;
}
.modal-trades {
max-height: 60vh;
overflow-y: auto;
margin-top: 0.5rem;
}
.trade-list {
list-style: none;
}