@@ -19,6 +19,7 @@ const hError = ref(null)
// 매매 모달
// 매매 모달
const tradeModal = ref ( false )
const tradeModal = ref ( false )
const tradeHolding = ref ( null )
const tradeHolding = ref ( null )
const tradeEditId = ref ( null ) // null=추가, 값=해당 매매 수정
// inputMode: 'PRICE'(수량×단가) | 'AMOUNT'(수량+총금액 → 단가 자동계산, 소수점 매수용)
// inputMode: 'PRICE'(수량×단가) | 'AMOUNT'(수량+총금액 → 단가 자동계산, 소수점 매수용)
const tForm = reactive ( { tradeType : 'BUY' , tradeDate : '' , quantity : null , price : null , amount : null , fee : null , inputMode : 'PRICE' } )
const tForm = reactive ( { tradeType : 'BUY' , tradeDate : '' , quantity : null , price : null , amount : null , fee : null , inputMode : 'PRICE' } )
const tSubmitting = ref ( false )
const tSubmitting = ref ( false )
@@ -38,9 +39,26 @@ const tradeAmount = computed(() => {
return Math . round ( tradeQty . value * ( Number ( tForm . price ) || 0 ) )
return Math . round ( tradeQty . value * ( Number ( tForm . price ) || 0 ) )
} )
} )
// 매매이력 드롭다운
// 매매이력 드롭다운 — 인라인은 최근 N건, 전체는 모달
const openTradesId = ref ( null )
const openTradesId = ref ( null )
const tradesByHolding = reactive ( { } )
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 ) {
function won ( n ) {
return ( n ? ? 0 ) . toLocaleString ( 'ko-KR' )
return ( n ? ? 0 ) . toLocaleString ( 'ko-KR' )
@@ -58,6 +76,13 @@ function tDate(v) {
const d = new Date ( 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' ) } `
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 ( ) {
async function load ( ) {
loading . value = true
loading . value = true
@@ -143,10 +168,27 @@ async function removeHolding(h) {
/* ===== 매매 ===== */
/* ===== 매매 ===== */
function openTrade ( h , type ) {
function openTrade ( h , type ) {
tradeHolding . value = h
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' } )
Object . assign ( tForm , { tradeType : type , tradeDate : todayStr ( ) , quantity : null , price : h . currentPrice ? ? null , amount : null , fee : null , inputMode : 'PRICE' } )
tError . value = null
tError . value = null
tradeModal . value = true
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 ( ) {
async function submitTrade ( ) {
tError . value = null
tError . value = null
if ( ! tForm . quantity || tForm . quantity <= 0 ) {
if ( ! tForm . quantity || tForm . quantity <= 0 ) {
@@ -171,9 +213,13 @@ async function submitTrade() {
fee : tForm . fee ? Number ( tForm . fee ) : 0 ,
fee : tForm . fee ? Number ( tForm . fee ) : 0 ,
}
}
try {
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
tradeModal . value = false
if ( openTradesId . value === tradeHolding . value . id ) await loadTrades ( tradeHolding . value . id )
await loadTrades ( tradeHolding . value . id ) // 인라인 최근목록·전체보기 모달 동시 갱신
await load ( )
await load ( )
emit ( 'changed' )
emit ( 'changed' )
} catch ( e ) {
} catch ( e ) {
@@ -269,18 +315,24 @@ onMounted(async () => {
< / div >
< / div >
< / div >
< / div >
<!-- 매매이력 -- >
<!-- 매매이력 ( 최근 N건 , 전체는 모달 ) -- >
< div v-if = "openTradesId === h.id" class="trade-drop" >
< div v-if = "openTradesId === h.id" class="trade-drop" >
< ul v-if = "( tradesByHolding[h.id] || []).length" class="trade-list " >
< template v-if = "tradesOf(h).length " >
< li v-for = "t in tradesByHolding[h.id]" :key="t.id" class="trade-row " >
< ul class = "trade-list " >
< span class = "t-date" > { { tDate ( t . tradeDate ) } } < / span >
< li v-for = "t in recentTrades(h)" :key="t.id" class="trade-row" >
< span class = "t-type" : class = "t.tradeType === 'SELL' ? 'sell' : 'buy'" > { { t . tradeType === 'SELL' ? '매도' : '매수' } } < / span >
< span class = "t-date" > { { tDate ( t . tradeDate ) } } < / span >
< span class = "t-q ty" > { { shares ( t . quantity ) } } 주 × { { won ( t . price ) } } < / span >
< span class = "t-type" : class = "t.tradeType === 'SELL' ? 'sell' : 'buy'" > { { t . tradeType === 'SELL' ? '매도' : '매수' } } < / span >
< span class = "t-fee" v-if = "t.fee "> 수수료 {{ won ( t.fee ) }} < / span >
< span class = "t-qty " > { { shares ( t . quantity ) } } 주 × { { won ( t . price ) } } < / span >
< span class = "t-amt " > { { won ( t . amount ) } } < / span >
< span class = "t-fee" v-if = "t.fee "> 수수료 {{ won ( t.fee ) }} < / span >
< IconBtn class = "t-del" icon = "trash" title = "매매 삭제" variant = "danger" size = "sm" @click ="removeTrade(t, h.id)" / >
< span class = "t-amt" > { { won ( t . amount ) } } < / span >
< / li >
< IconBtn icon = "edit" title = "매매 수정" size = "sm" @click ="openEditTrade(h, t)" / >
< / ul >
< 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 >
< p v-else class = "pf-msg sm" > 매매 내역이 없습니다 . < / p >
< / div >
< / div >
< / li >
< / li >
@@ -315,7 +367,7 @@ onMounted(async () => {
< div v-if = "tradeModal" class="modal-backdrop" @click.self="tradeModal = false" >
< div v-if = "tradeModal" class="modal-backdrop" @click.self="tradeModal = false" >
< div class = "modal" role = "dialog" aria -modal = " true " >
< div class = "modal" role = "dialog" aria -modal = " true " >
< button class = "close" type = "button" @click ="tradeModal = false" > × < / button >
< 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" >
< form class = "pf-form" @submit.prevent ="submitTrade" >
< label > 구분
< label > 구분
< select v-model = "tForm.tradeType" :disabled="tSubmitting" >
< select v-model = "tForm.tradeType" :disabled="tSubmitting" >
@@ -344,13 +396,36 @@ onMounted(async () => {
< p v-if = "tError" class="pf-msg err" > {{ tError }} < / p >
< p v-if = "tError" class="pf-msg err" > {{ tError }} < / p >
< div class = "pf-buttons" >
< div class = "pf-buttons" >
< IconBtn icon = "close" title = "취소" @click ="tradeModal = false" / >
< 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 >
< / div >
< / form >
< / form >
< / div >
< / div >
< / div >
< / div >
< / Transition >
< / Transition >
< / Teleport >
< / 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 >
< / div >
< / template >
< / template >
@@ -520,6 +595,31 @@ onMounted(async () => {
. trade - drop {
. trade - drop {
padding : 0.1 rem 0 0.5 rem 1.4 rem ;
padding : 0.1 rem 0 0.5 rem 1.4 rem ;
}
}
. more - trades {
margin - top : 0.2 rem ;
padding : 0.25 rem 0.6 rem ;
font - size : 0.76 rem ;
border : 1 px solid var ( -- color - border ) ;
border - radius : 4 px ;
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 : 420 px ;
}
. modal - trades {
max - height : 60 vh ;
overflow - y : auto ;
margin - top : 0.5 rem ;
}
. trade - list {
. trade - list {
list - style : none ;
list - style : none ;
}
}