- AppModal.vue: 전체화면 슬라이드업 공통 모달 (Teleport+Transition) - CategoryPicker.vue: 대/소분류 아코디언 칩 공통 컴포넌트 admin 모드(일반: 행 단위+수정/삭제, 순서변경: flat+SortableJS) - AccountView: 계좌종류·구분 셀렉트→뱃지, 분류→CategoryPicker - RecurringView: 구분·주기 셀렉트→뱃지, 계좌/카드→뱃지, 분류→CategoryPicker - AccountWalletView: 카드종류·상환방식 셀렉트→뱃지 - BudgetView: 비고정/고정 라디오→뱃지, 카테고리→CategoryPicker - CategoryView: 리스트 UI → CategoryPicker 칩 그리드 관리 모드 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { accountApi } from '@/api/accountApi'
|
||||
import IconBtn from '@/components/ui/IconBtn.vue'
|
||||
import AppModal from '@/components/ui/AppModal.vue'
|
||||
|
||||
const props = defineProps({ walletId: { type: [Number, String], required: true } })
|
||||
const emit = defineEmits(['changed'])
|
||||
@@ -339,93 +340,69 @@ onMounted(async () => {
|
||||
</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>
|
||||
<AppModal v-model="holdingModal" :title="`종목 ${holdingEditId ? '수정' : '추가'}`">
|
||||
<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>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</form>
|
||||
</AppModal>
|
||||
|
||||
<!-- 매매 모달 -->
|
||||
<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>
|
||||
<!-- 매매 모달 (매매내역 위에 표시되도록 z-index 높게) -->
|
||||
<AppModal v-model="tradeModal" :title="`${tradeHolding?.name} ${tradeEditId ? '매매 수정' : (tForm.tradeType === 'SELL' ? '매도' : '매수')}`" :zIndex="1100">
|
||||
<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>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</form>
|
||||
</AppModal>
|
||||
|
||||
<!-- 전체 매매내역 모달 (스크롤) -->
|
||||
<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>
|
||||
<!-- 전체 매매내역 모달 -->
|
||||
<AppModal v-model="allTradesModal" :title="`${allTradesHolding?.name} 매매내역`" :zIndex="1050">
|
||||
<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>
|
||||
</AppModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -608,16 +585,7 @@ onMounted(async () => {
|
||||
.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 {
|
||||
@@ -662,44 +630,6 @@ onMounted(async () => {
|
||||
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;
|
||||
@@ -741,15 +671,6 @@ onMounted(async () => {
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user