From 006fc87f70f00cf3ae4e52b76bccd746454f7baa Mon Sep 17 00:00:00 2001 From: ByungCheol Date: Sat, 6 Jun 2026 16:42:47 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=88=AC=EC=9E=90=20=EB=A7=A4=EB=A7=A4?= =?UTF-8?q?=EB=82=B4=EC=97=AD=20=EC=B5=9C=EA=B7=BCN+=EC=A0=84=EC=B2=B4?= =?UTF-8?q?=EB=B3=B4=EA=B8=B0=20=EB=AA=A8=EB=8B=AC=C2=B7=EB=A7=A4=EB=A7=A4?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20+=20=EC=95=88=EB=93=9C=EB=A1=9C?= =?UTF-8?q?=EC=9D=B4=EB=93=9C=20=EC=9D=B8=EC=85=8B=20=EB=B3=B4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - InvestPortfolio: 인라인은 최근 5건만, 초과분은 '전체 보기' 스크롤 모달 - 매매 행에 수정(✏)/삭제 버튼 — PUT /trades/{id} 로 매매 수정 - MainActivity: 콘텐츠 루트에 시스템 바 인셋 패딩(CONSUMED) — 에지투에지 소프트키/상태바 겹침 보정 Co-Authored-By: Claude Opus 4.8 --- .../kr/sblog/slimbudget/MainActivity.java | 23 +-- src/api/accountApi.js | 3 + src/views/account/InvestPortfolio.vue | 132 +++++++++++++++--- 3 files changed, 131 insertions(+), 27 deletions(-) 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 () => { - +
-
    -
  • - {{ tDate(t.tradeDate) }} - {{ t.tradeType === 'SELL' ? '매도' : '매수' }} - {{ shares(t.quantity) }}주 × {{ won(t.price) }} - 수수료 {{ won(t.fee) }} - {{ won(t.amount) }} - -
  • -
+

매매 내역이 없습니다.

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