- InvestPortfolio: 인라인은 최근 5건만, 초과분은 '전체 보기' 스크롤 모달
- 매매 행에 수정(✏)/삭제 버튼 — PUT /trades/{id} 로 매매 수정
- MainActivity: 콘텐츠 루트에 시스템 바 인셋 패딩(CONSUMED) — 에지투에지 소프트키/상태바 겹침 보정
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import android.view.View;
|
||||
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
@@ -15,16 +16,16 @@ public class MainActivity extends BridgeActivity {
|
||||
registerPlugin(CardNotifPlugin.class);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// targetSdk 35+ 의 에지투에지 강제 환경에서 웹뷰가 상태바/소프트키(시스템 내비) 뒤로
|
||||
// 깔리는 것을 방지한다. 시스템 바 인셋만큼 웹뷰에 패딩을 줘서 콘텐츠(하단 고정 내비 포함)가
|
||||
// 항상 안전 영역 안에 오도록 한다. (CSS env(safe-area-*) 가 기기별로 0 을 반환하는 문제 회피)
|
||||
final View webView = getBridge().getWebView();
|
||||
if (webView != null) {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(webView, (v, insets) -> {
|
||||
Insets bars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||
// targetSdk 35+ 의 에지투에지 강제 환경에서 콘텐츠가 상태바/소프트키 뒤로 깔리는 것을 방지.
|
||||
// 구글 권장 패턴: 콘텐츠 루트에 시스템 바 인셋만큼 패딩을 적용하고 인셋을 소비(CONSUMED)한다.
|
||||
// → 헤더는 상태바 아래, 하단 고정 내비는 소프트키 위에 위치.
|
||||
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||
final View content = findViewById(android.R.id.content);
|
||||
ViewCompat.setOnApplyWindowInsetsListener(content, (v, windowInsets) -> {
|
||||
Insets bars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||
v.setPadding(bars.left, bars.top, bars.right, bars.bottom);
|
||||
return insets;
|
||||
return WindowInsetsCompat.CONSUMED;
|
||||
});
|
||||
}
|
||||
ViewCompat.requestApplyInsets(content);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`)
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
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">
|
||||
<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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user