feat: 계정정보 포인트 클릭 시 적립내역 모달
CI / build (push) Failing after 13m30s

- 포인트 행 클릭 → 적립/차감 내역 모달(사유·일시·증감)
- authApi.pointHistory(), 사유 라벨(BOARD_WRITE=게시판 작성)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ByungCheol
2026-06-28 14:44:59 +09:00
parent 7e096d5e7b
commit 56d2ad6a8a
2 changed files with 161 additions and 3 deletions
+4
View File
@@ -43,6 +43,10 @@ export const authApi = {
points() {
return http.get('/auth/points')
},
// 포인트 적립/차감 내역 (최신순)
pointHistory() {
return http.get('/auth/point-history')
},
// 현재 사용자 전체 프로필(아바타·포인트 포함) — 재로그인 없이 최신값 동기화
profile() {
return http.get('/auth/profile')
+157 -3
View File
@@ -22,6 +22,33 @@ onMounted(async () => {
}
})
// 포인트 적립내역 모달
const POINT_REASONS = { BOARD_WRITE: '게시판 작성' }
function reasonLabel(r) {
return POINT_REASONS[r] || r
}
const historyOpen = ref(false)
const history = ref([])
const historyLoading = ref(false)
async function openHistory() {
historyOpen.value = true
historyLoading.value = true
try {
history.value = await authApi.pointHistory()
} catch {
history.value = []
} finally {
historyLoading.value = false
}
}
function fmtDateTime(s) {
if (!s) return ''
const d = new Date(s)
if (Number.isNaN(d.getTime())) return s
const p = (n) => String(n).padStart(2, '0')
return `${d.getFullYear()}.${p(d.getMonth() + 1)}.${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
}
const u = computed(() => auth.user || {})
const roleLabel = computed(() => (u.value.role === 'ADMIN' ? '관리자' : '일반회원'))
const isPremium = computed(() => auth.isPremium)
@@ -128,12 +155,38 @@ function goEdit() {
<RouterLink v-if="!isPremium" to="/upgrade" class="up-link">업그레이드 </RouterLink>
</span>
</div>
<div class="row">
<button type="button" class="row row-point" @click="openHistory">
<span class="k">포인트</span>
<span class="v"><span class="points">{{ points.toLocaleString('ko-KR') }} P</span></span>
</div>
<span class="v">
<span class="points">{{ points.toLocaleString('ko-KR') }} P</span>
<span class="point-more">내역 </span>
</span>
</button>
</section>
<!-- 포인트 적립내역 모달 -->
<Teleport to="body">
<div v-if="historyOpen" class="ph-backdrop" @click.self="historyOpen = false">
<div class="ph-modal" role="dialog" aria-modal="true" aria-label="포인트 적립내역">
<div class="ph-head">
<strong>포인트 적립내역</strong>
<button type="button" class="ph-close" aria-label="닫기" @click="historyOpen = false">×</button>
</div>
<p v-if="historyLoading" class="ph-msg">불러오는 </p>
<p v-else-if="!history.length" class="ph-msg">적립 내역이 없습니다.</p>
<ul v-else class="ph-list">
<li v-for="(h, i) in history" :key="i">
<div class="ph-main">
<span class="ph-reason">{{ reasonLabel(h.reason) }}</span>
<span class="ph-date">{{ fmtDateTime(h.createdAt) }}</span>
</div>
<span class="ph-amount" :class="{ minus: h.amount < 0 }">{{ h.amount > 0 ? '+' : '' }}{{ h.amount }} P</span>
</li>
</ul>
</div>
</div>
</Teleport>
<button type="button" class="primary-btn" @click="goEdit">이름 변경</button>
</div>
</template>
@@ -201,6 +254,107 @@ function goEdit() {
color: hsla(160, 100%, 37%, 1);
font-weight: 700;
}
.row-point {
width: 100%;
border: 0;
border-bottom: 1px solid var(--color-border);
background: transparent;
cursor: pointer;
font: inherit;
text-align: left;
}
.row-point:last-child {
border-bottom: 0;
}
.row-point:hover {
background: var(--color-background-mute);
}
.point-more {
margin-left: 0.5rem;
font-size: 0.82rem;
font-weight: 600;
color: var(--color-text);
opacity: 0.55;
}
/* 포인트 내역 모달 */
.ph-backdrop {
position: fixed;
inset: 0;
z-index: 2000;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
}
.ph-modal {
width: 100%;
max-width: 380px;
max-height: 75vh;
display: flex;
flex-direction: column;
background: var(--color-background);
border: 1px solid var(--color-border);
border-radius: 10px;
overflow: hidden;
}
.ph-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.85rem 1rem;
border-bottom: 1px solid var(--color-border);
}
.ph-close {
border: 0;
background: transparent;
color: var(--color-text);
font-size: 1.5rem;
line-height: 1;
cursor: pointer;
}
.ph-msg {
padding: 2rem 1rem;
text-align: center;
opacity: 0.7;
}
.ph-list {
list-style: none;
margin: 0;
padding: 0.25rem 0;
overflow-y: auto;
}
.ph-list li {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.65rem 1rem;
border-bottom: 1px solid var(--color-border);
}
.ph-list li:last-child {
border-bottom: 0;
}
.ph-main {
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.ph-reason {
font-size: 0.92rem;
}
.ph-date {
font-size: 0.78rem;
opacity: 0.6;
}
.ph-amount {
font-weight: 700;
color: hsla(160, 100%, 37%, 1);
white-space: nowrap;
}
.ph-amount.minus {
color: #c0392b;
}
.up-link {
margin-left: 0.5rem;
font-size: 0.82rem;