589 lines
16 KiB
Vue
589 lines
16 KiB
Vue
|
|
<script setup>
|
|||
|
|
import { 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 tForm = reactive({ tradeType: 'BUY', tradeDate: '', quantity: null, price: null, fee: null })
|
|||
|
|
const tSubmitting = ref(false)
|
|||
|
|
const tError = ref(null)
|
|||
|
|
|
|||
|
|
// 매매이력 드롭다운
|
|||
|
|
const openTradesId = ref(null)
|
|||
|
|
const tradesByHolding = reactive({})
|
|||
|
|
|
|||
|
|
function won(n) {
|
|||
|
|
return (n ?? 0).toLocaleString('ko-KR')
|
|||
|
|
}
|
|||
|
|
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')}`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function load() {
|
|||
|
|
loading.value = true
|
|||
|
|
try {
|
|||
|
|
holdings.value = await accountApi.investHoldings(props.walletId)
|
|||
|
|
} catch {
|
|||
|
|
holdings.value = []
|
|||
|
|
} finally {
|
|||
|
|
loading.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
|
|||
|
|
Object.assign(tForm, { tradeType: type, tradeDate: todayStr(), quantity: null, price: h.currentPrice ?? null, fee: null })
|
|||
|
|
tError.value = null
|
|||
|
|
tradeModal.value = true
|
|||
|
|
}
|
|||
|
|
async function submitTrade() {
|
|||
|
|
tError.value = null
|
|||
|
|
if (!tForm.quantity || tForm.quantity <= 0) {
|
|||
|
|
tError.value = '수량을 입력하세요.'
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
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: Number(tForm.price),
|
|||
|
|
fee: tForm.fee ? Number(tForm.fee) : 0,
|
|||
|
|
}
|
|||
|
|
try {
|
|||
|
|
await accountApi.addTrade(tradeHolding.value.id, payload)
|
|||
|
|
tradeModal.value = false
|
|||
|
|
if (openTradesId.value === tradeHolding.value.id) 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(load)
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<template>
|
|||
|
|
<div class="portfolio">
|
|||
|
|
<div class="pf-head">
|
|||
|
|
<span class="pf-title">보유 종목</span>
|
|||
|
|
<IconBtn icon="plus" title="종목 추가" variant="primary" size="sm" @click="openAddHolding" />
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<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">
|
|||
|
|
{{ won(h.quantity) }}주 · 평단 {{ won(h.avgPrice) }}
|
|||
|
|
<template v-if="h.currentPrice != null"> · 현재가 {{ won(h.currentPrice) }}</template>
|
|||
|
|
<span v-else class="noprice"> · 현재가 미입력</span>
|
|||
|
|
</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>
|
|||
|
|
|
|||
|
|
<!-- 매매이력 -->
|
|||
|
|
<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">{{ won(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>
|
|||
|
|
<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 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 }} {{ 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="1" :disabled="tSubmitting" /></label>
|
|||
|
|
<label>단가(원)<input v-model.number="tForm.price" type="number" min="0" :disabled="tSubmitting" /></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">보유 {{ won(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="등록" variant="primary" type="submit" :disabled="tSubmitting" />
|
|||
|
|
</div>
|
|||
|
|
</form>
|
|||
|
|
</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-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;
|
|||
|
|
}
|
|||
|
|
.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;
|
|||
|
|
}
|
|||
|
|
.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>
|