diff --git a/android/app/src/main/java/kr/sblog/slimbudget/MainActivity.java b/android/app/src/main/java/kr/sblog/slimbudget/MainActivity.java index 77742bc..8d4cf54 100644 --- a/android/app/src/main/java/kr/sblog/slimbudget/MainActivity.java +++ b/android/app/src/main/java/kr/sblog/slimbudget/MainActivity.java @@ -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()); - v.setPadding(bars.left, bars.top, bars.right, bars.bottom); - return insets; - }); - } + // 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 WindowInsetsCompat.CONSUMED; + }); + ViewCompat.requestApplyInsets(content); } } diff --git a/src/api/accountApi.js b/src/api/accountApi.js index fefbe58..6d180d9 100644 --- a/src/api/accountApi.js +++ b/src/api/accountApi.js @@ -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}`) }, diff --git a/src/views/account/InvestPortfolio.vue b/src/views/account/InvestPortfolio.vue index 8ccaea7..37129e3 100644 --- a/src/views/account/InvestPortfolio.vue +++ b/src/views/account/InvestPortfolio.vue @@ -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 () => { - +
- +

매매 내역이 없습니다.

@@ -315,7 +367,7 @@ onMounted(async () => {