Files
sb-front/src/views/account/InvestPortfolio.vue
T
ByungCheol 006fc87f70
CI / build (push) Successful in 26s
feat: 투자 매매내역 최근N+전체보기 모달·매매 수정 + 안드로이드 인셋 보정
- InvestPortfolio: 인라인은 최근 5건만, 초과분은 '전체 보기' 스크롤 모달
- 매매 행에 수정(✏)/삭제 버튼 — PUT /trades/{id} 로 매매 수정
- MainActivity: 콘텐츠 루트에 시스템 바 인셋 패딩(CONSUMED) — 에지투에지 소프트키/상태바 겹침 보정

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 16:42:47 +09:00

800 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { accountApi } from '@/api/accountApi'
import IconBtn from '@/components/ui/IconBtn.vue'
const props = defineProps({ walletId: { type: [Number, String], required: true } })
const emit = defineEmits(['changed'])
const holdings = ref([])
const loading = ref(false)
// 종목 추가/수정 모달
const holdingModal = ref(false)
const holdingEditId = ref(null)
const hForm = reactive({ name: '', ticker: '', currentPrice: null })
const hSubmitting = ref(false)
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)
const tError = ref(null)
const tradeQty = computed(() => Number(tForm.quantity) || 0)
// 실제 저장에 쓰는 단가(원/주). 금액 모드면 총금액÷수량을 반올림.
const tradePrice = computed(() => {
if (tForm.inputMode === 'AMOUNT') {
return tradeQty.value > 0 ? Math.round((Number(tForm.amount) || 0) / tradeQty.value) : 0
}
return Number(tForm.price) || 0
})
// 표시용 총 거래금액
const tradeAmount = computed(() => {
if (tForm.inputMode === 'AMOUNT') return Number(tForm.amount) || 0
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')
}
// 수량(소수점 매매 지원): 최대 6자리, 불필요한 0 제거
function shares(n) {
return Number(n ?? 0).toLocaleString('ko-KR', { maximumFractionDigits: 6 })
}
function todayStr() {
const d = new Date()
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
}
function tDate(v) {
if (!v) return '-'
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
try {
holdings.value = await accountApi.investHoldings(props.walletId)
} catch {
holdings.value = []
} finally {
loading.value = false
}
}
// 종목코드로 현재가 시세 일괄 갱신
const refreshing = ref(false)
const refreshMsg = ref('')
async function refreshPrices() {
if (refreshing.value) return
refreshing.value = true
refreshMsg.value = ''
try {
holdings.value = await accountApi.refreshPrices(props.walletId)
emit('changed')
const noTicker = holdings.value.filter((h) => !h.ticker).length
refreshMsg.value = noTicker
? `시세 갱신 완료 · 종목코드 없는 ${noTicker}개는 제외`
: '시세 갱신 완료'
} catch {
refreshMsg.value = '시세 갱신에 실패했습니다.'
} finally {
refreshing.value = false
}
}
/* ===== 종목 ===== */
function openAddHolding() {
holdingEditId.value = null
Object.assign(hForm, { name: '', ticker: '', currentPrice: null })
hError.value = null
holdingModal.value = true
}
function openEditHolding(h) {
holdingEditId.value = h.id
Object.assign(hForm, { name: h.name, ticker: h.ticker || '', currentPrice: h.currentPrice ?? null })
hError.value = null
holdingModal.value = true
}
async function submitHolding() {
hError.value = null
if (!hForm.name.trim()) {
hError.value = '종목명을 입력하세요.'
return
}
hSubmitting.value = true
const payload = {
walletId: Number(props.walletId),
name: hForm.name.trim(),
ticker: hForm.ticker || null,
currentPrice: hForm.currentPrice !== '' && hForm.currentPrice != null ? Number(hForm.currentPrice) : null,
}
try {
if (holdingEditId.value) await accountApi.updateHolding(holdingEditId.value, payload)
else await accountApi.createHolding(payload)
holdingModal.value = false
await load()
emit('changed')
} catch (e) {
hError.value = e.response?.data?.message || '저장에 실패했습니다.'
} finally {
hSubmitting.value = false
}
}
async function removeHolding(h) {
if (!confirm(`'${h.name}' 종목과 매매이력을 모두 삭제할까요?`)) return
try {
await accountApi.removeHolding(h.id)
await load()
emit('changed')
} catch (e) {
alert(e.response?.data?.message || '삭제에 실패했습니다.')
}
}
/* ===== 매매 ===== */
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) {
tError.value = '수량을 입력하세요.'
return
}
if (tForm.inputMode === 'AMOUNT') {
if (!tForm.amount || tForm.amount <= 0) {
tError.value = '금액을 입력하세요.'
return
}
} else if (tForm.price == null || tForm.price < 0) {
tError.value = '단가를 입력하세요.'
return
}
tSubmitting.value = true
const payload = {
tradeType: tForm.tradeType,
tradeDate: tForm.tradeDate,
quantity: Number(tForm.quantity),
price: tradePrice.value,
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
await loadTrades(tradeHolding.value.id) // 인라인 최근목록·전체보기 모달 동시 갱신
await load()
emit('changed')
} catch (e) {
tError.value = e.response?.data?.message || '저장에 실패했습니다.'
} finally {
tSubmitting.value = false
}
}
async function loadTrades(holdingId) {
try {
tradesByHolding[holdingId] = await accountApi.holdingTrades(holdingId)
} catch {
tradesByHolding[holdingId] = []
}
}
async function toggleTrades(h) {
if (openTradesId.value === h.id) {
openTradesId.value = null
return
}
openTradesId.value = h.id
await loadTrades(h.id)
}
async function removeTrade(t, holdingId) {
if (!confirm('이 매매 내역을 삭제할까요?')) return
try {
await accountApi.removeTrade(t.id)
await loadTrades(holdingId)
await load()
emit('changed')
} catch (e) {
alert(e.response?.data?.message || '삭제에 실패했습니다.')
}
}
onMounted(async () => {
await load()
// 투자 계좌를 펼치면 종목코드 있는 종목 현재가를 자동으로 시세 갱신
if (holdings.value.some((h) => h.ticker)) {
refreshPrices()
}
})
</script>
<template>
<div class="portfolio">
<div class="pf-head">
<span class="pf-title">보유 종목</span>
<div class="pf-head-btns">
<button
type="button" class="refresh-btn"
:disabled="refreshing || !holdings.length"
@click="refreshPrices"
>{{ refreshing ? '갱신 중…' : '↻ 시세 갱신' }}</button>
<IconBtn icon="plus" title="종목 추가" variant="primary" size="sm" @click="openAddHolding" />
</div>
</div>
<p v-if="refreshMsg" class="pf-refresh-msg">{{ refreshMsg }}</p>
<p v-if="loading" class="pf-msg">불러오는 중...</p>
<p v-else-if="!holdings.length" class="pf-msg">보유 종목이 없습니다. 종목을 추가하고 매수를 기록하세요.</p>
<ul v-else class="hold-list">
<li v-for="h in holdings" :key="h.id" class="hold-item">
<div class="hold-row">
<div class="h-info" @click="toggleTrades(h)">
<span class="chev">{{ openTradesId === h.id ? '▾' : '▸' }}</span>
<div>
<div class="h-name">{{ h.name }}<span v-if="h.ticker" class="h-ticker">{{ h.ticker }}</span></div>
<div class="h-sub">
<div class="h-sub-qty">{{ shares(h.quantity) }}</div>
<div class="h-sub-price">
평단 {{ won(h.avgPrice) }}
<template v-if="h.currentPrice != null"> · 현재가 {{ won(h.currentPrice) }}</template>
<span v-else class="noprice"> · 현재가 미입력</span>
</div>
</div>
</div>
</div>
<div class="h-val">
<div class="h-eval">{{ won(h.evalValue) }}</div>
<div class="h-gain" :class="h.evalGain < 0 ? 'neg' : 'pos'">
{{ h.evalGain >= 0 ? '+' : '' }}{{ won(h.evalGain) }}<span v-if="h.returnPct != null"> ({{ h.returnPct }}%)</span>
</div>
<div v-if="h.realizedPL" class="h-realized">실현 {{ h.realizedPL >= 0 ? '+' : '' }}{{ won(h.realizedPL) }}</div>
</div>
<div class="h-actions">
<IconBtn icon="plus" title="매수" variant="buy" size="sm" @click="openTrade(h, 'BUY')" />
<IconBtn icon="minus" title="매도" variant="sell" size="sm" :disabled="h.quantity <= 0" @click="openTrade(h, 'SELL')" />
<IconBtn icon="edit" title="현재가 수정" size="sm" @click="openEditHolding(h)" />
<IconBtn icon="trash" title="삭제" variant="danger" size="sm" @click="removeHolding(h)" />
</div>
</div>
<!-- 매매이력 (최근 N건, 전체는 모달) -->
<div v-if="openTradesId === h.id" class="trade-drop">
<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>
</ul>
<!-- 종목 추가/수정 모달 -->
<Teleport to="body">
<Transition name="fade">
<div v-if="holdingModal" class="modal-backdrop" @click.self="holdingModal = false">
<div class="modal" role="dialog" aria-modal="true">
<button class="close" type="button" @click="holdingModal = false">×</button>
<h2>종목 {{ holdingEditId ? '수정' : '추가' }}</h2>
<form class="pf-form" @submit.prevent="submitHolding">
<label>종목명<input v-model="hForm.name" type="text" placeholder="예: 삼성전자" :disabled="hSubmitting" /></label>
<label>종목코드<input v-model="hForm.ticker" type="text" placeholder="예: 005930 (국내 상장)" :disabled="hSubmitting" /></label>
<label>현재가<input v-model.number="hForm.currentPrice" type="number" min="0" placeholder="(비우면 시세 갱신으로 자동)" :disabled="hSubmitting" /></label>
<p class="pf-form-hint">종목코드(6자리) 입력하면 <b>시세 갱신</b> 버튼으로 현재가를 자동으로 불러옵니다.</p>
<p v-if="hError" class="pf-msg err">{{ hError }}</p>
<div class="pf-buttons">
<IconBtn icon="close" title="취소" @click="holdingModal = false" />
<IconBtn icon="save" title="저장" variant="primary" type="submit" :disabled="hSubmitting" />
</div>
</form>
</div>
</div>
</Transition>
</Teleport>
<!-- 매매 모달 -->
<Teleport to="body">
<Transition name="fade">
<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 }} {{ tradeEditId ? '매매 수정' : (tForm.tradeType === 'SELL' ? '매도' : '매수') }}</h2>
<form class="pf-form" @submit.prevent="submitTrade">
<label>구분
<select v-model="tForm.tradeType" :disabled="tSubmitting">
<option value="BUY">매수</option>
<option value="SELL" :disabled="(tradeHolding?.quantity || 0) <= 0">매도</option>
</select>
</label>
<label>거래일<input v-model="tForm.tradeDate" type="date" :disabled="tSubmitting" /></label>
<label>수량()<input v-model.number="tForm.quantity" type="number" min="0" step="any" inputmode="decimal" placeholder="예: 0.5 / 1.25 (소수점 가능)" :disabled="tSubmitting" /></label>
<label>입력 방식
<select v-model="tForm.inputMode" :disabled="tSubmitting">
<option value="PRICE">단가로 입력</option>
<option value="AMOUNT">금액으로 입력 (소수점 매수)</option>
</select>
</label>
<label v-if="tForm.inputMode === 'PRICE'">단가()
<input v-model.number="tForm.price" type="number" min="0" :disabled="tSubmitting" />
<small v-if="tradeQty > 0 && tForm.price" class="pf-hint"> {{ won(tradeAmount) }}</small>
</label>
<label v-else>금액(, )
<input v-model.number="tForm.amount" type="number" min="0" placeholder="이 금액어치 매매" :disabled="tSubmitting" />
<small v-if="tradeQty > 0 && tForm.amount" class="pf-hint">단가 {{ won(tradePrice) }}</small>
</label>
<label>수수료/세금<input v-model.number="tForm.fee" type="number" min="0" placeholder="(선택)" :disabled="tSubmitting" /></label>
<p v-if="tForm.tradeType === 'SELL'" class="pf-hint">보유 {{ shares(tradeHolding?.quantity) }} · 평단 {{ won(tradeHolding?.avgPrice) }}</p>
<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="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>
<style scoped>
.portfolio {
padding: 0.25rem 0 0.5rem;
}
.pf-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.pf-title {
font-size: 0.85rem;
font-weight: 600;
opacity: 0.8;
}
.pf-head-btns {
display: flex;
align-items: center;
gap: 0.4rem;
}
.refresh-btn {
padding: 0.25rem 0.6rem;
font-size: 0.78rem;
border: 1px solid var(--color-border);
border-radius: 4px;
background: var(--color-background-soft);
color: var(--color-text);
cursor: pointer;
white-space: nowrap;
}
.refresh-btn:disabled {
opacity: 0.5;
cursor: default;
}
.pf-refresh-msg {
font-size: 0.78rem;
opacity: 0.7;
margin: 0 0 0.4rem;
}
.pf-form-hint {
font-size: 0.76rem;
opacity: 0.6;
margin: -0.2rem 0 0;
}
.pf-add {
padding: 0.25rem 0.6rem;
font-size: 0.8rem;
border: 1px solid var(--color-border);
border-radius: 4px;
background: var(--color-background-soft);
color: var(--color-text);
cursor: pointer;
}
.pf-msg {
font-size: 0.85rem;
opacity: 0.65;
padding: 0.5rem 0;
}
.pf-msg.sm {
padding: 0.3rem 0;
}
.pf-msg.err {
color: #c0392b;
opacity: 1;
}
.hold-list {
list-style: none;
}
.hold-item {
border-top: 1px dashed var(--color-border);
}
.hold-row {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0;
flex-wrap: wrap;
}
.h-info {
display: flex;
align-items: center;
gap: 0.4rem;
flex: 1;
min-width: 9rem;
cursor: pointer;
}
.chev {
opacity: 0.5;
font-size: 0.75rem;
}
.h-name {
font-weight: 600;
font-size: 0.9rem;
}
.h-ticker {
margin-left: 0.35rem;
font-size: 0.72rem;
opacity: 0.6;
}
.h-sub {
font-size: 0.76rem;
opacity: 0.7;
}
.h-sub-qty {
font-weight: 600;
}
.h-sub-price {
margin-top: 1px;
}
.noprice {
color: #e67e22;
}
.h-val {
text-align: right;
white-space: nowrap;
}
.h-eval {
font-weight: 700;
font-size: 0.9rem;
}
.h-gain {
font-size: 0.76rem;
font-weight: 600;
}
.h-gain.pos {
color: #2e7d32;
}
.h-gain.neg {
color: #c0392b;
}
.h-realized {
font-size: 0.7rem;
opacity: 0.7;
}
.h-actions {
display: flex;
gap: 0.25rem;
}
.h-actions button {
padding: 0.18rem 0.45rem;
font-size: 0.76rem;
border: 1px solid var(--color-border);
border-radius: 4px;
background: var(--color-background-soft);
color: var(--color-text);
cursor: pointer;
}
.h-actions button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.h-actions .buy {
border-color: #2e7d32;
color: #2e7d32;
}
.h-actions .sell {
border-color: #c0392b;
color: #c0392b;
}
.h-actions .danger {
border-color: #c0392b;
color: #c0392b;
}
.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;
}
.trade-row {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.25rem 0;
font-size: 0.78rem;
}
.t-date {
opacity: 0.7;
width: 3rem;
}
.t-type {
font-weight: 600;
}
.t-type.buy {
color: #2e7d32;
}
.t-type.sell {
color: #c0392b;
}
.t-qty {
flex: 1;
}
.t-fee {
opacity: 0.6;
font-size: 0.72rem;
}
.t-amt {
font-weight: 600;
}
.t-del {
border: 0;
background: transparent;
color: #c0392b;
cursor: pointer;
font-size: 1rem;
line-height: 1;
}
/* 모달 (계좌 관리와 동일 톤) */
.modal-backdrop {
position: fixed;
inset: 0;
z-index: 1100;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.5);
padding: 1rem;
}
.modal {
position: relative;
width: 100%;
max-width: 340px;
padding: 1.75rem 1.5rem 1.5rem;
background: var(--color-background);
border: 1px solid var(--color-border);
border-radius: 8px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
}
.modal .close {
position: absolute;
top: 0.5rem;
right: 0.6rem;
width: 2rem;
height: 2rem;
border: 0;
background: transparent;
font-size: 1.5rem;
line-height: 1;
cursor: pointer;
}
.modal h2 {
margin-bottom: 1rem;
font-size: 1.1rem;
text-align: center;
}
.pf-form {
display: flex;
flex-direction: column;
gap: 0.55rem;
}
.pf-form label {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.85rem;
}
.pf-form input,
.pf-form select {
padding: 0.5rem 0.7rem;
border: 1px solid var(--color-border);
border-radius: 4px;
background: var(--color-background-soft);
color: var(--color-text);
}
.pf-hint {
font-size: 0.78rem;
opacity: 0.7;
}
.pf-buttons {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 0.5rem;
}
.pf-buttons button {
padding: 0.45rem 0.9rem;
border: 1px solid var(--color-border);
border-radius: 4px;
background: var(--color-background-soft);
color: var(--color-text);
cursor: pointer;
}
.pf-buttons .primary {
border-color: hsla(160, 100%, 37%, 1);
color: hsla(160, 100%, 37%, 1);
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.18s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
/* ===== 모바일: 매매이력 2줄, 글자 세로쪼개짐 방지 ===== */
@media (max-width: 768px) {
.hold-row {
column-gap: 0.4rem;
}
.h-actions {
flex-wrap: wrap;
justify-content: flex-end;
}
.trade-drop {
padding-left: 0.8rem;
}
.trade-row {
flex-wrap: wrap;
column-gap: 0.4rem;
row-gap: 0.1rem;
}
.t-date,
.t-type,
.t-qty,
.t-fee,
.t-amt {
white-space: nowrap;
}
.t-date {
order: 0;
width: auto;
}
.t-type {
order: 1;
}
.t-amt {
order: 2;
margin-left: auto;
}
.t-del {
order: 3;
}
.t-qty {
order: 4;
flex: 0 1 auto;
}
.t-fee {
order: 5;
}
}
</style>