- 포인트 행 클릭 → 적립/차감 내역 모달(사유·일시·증감) - authApi.pointHistory(), 사유 라벨(BOARD_WRITE=게시판 작성) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,10 @@ export const authApi = {
|
|||||||
points() {
|
points() {
|
||||||
return http.get('/auth/points')
|
return http.get('/auth/points')
|
||||||
},
|
},
|
||||||
|
// 포인트 적립/차감 내역 (최신순)
|
||||||
|
pointHistory() {
|
||||||
|
return http.get('/auth/point-history')
|
||||||
|
},
|
||||||
// 현재 사용자 전체 프로필(아바타·포인트 포함) — 재로그인 없이 최신값 동기화
|
// 현재 사용자 전체 프로필(아바타·포인트 포함) — 재로그인 없이 최신값 동기화
|
||||||
profile() {
|
profile() {
|
||||||
return http.get('/auth/profile')
|
return http.get('/auth/profile')
|
||||||
|
|||||||
@@ -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 u = computed(() => auth.user || {})
|
||||||
const roleLabel = computed(() => (u.value.role === 'ADMIN' ? '관리자' : '일반회원'))
|
const roleLabel = computed(() => (u.value.role === 'ADMIN' ? '관리자' : '일반회원'))
|
||||||
const isPremium = computed(() => auth.isPremium)
|
const isPremium = computed(() => auth.isPremium)
|
||||||
@@ -128,12 +155,38 @@ function goEdit() {
|
|||||||
<RouterLink v-if="!isPremium" to="/upgrade" class="up-link">업그레이드 →</RouterLink>
|
<RouterLink v-if="!isPremium" to="/upgrade" class="up-link">업그레이드 →</RouterLink>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<button type="button" class="row row-point" @click="openHistory">
|
||||||
<span class="k">포인트</span>
|
<span class="k">포인트</span>
|
||||||
<span class="v"><span class="points">{{ points.toLocaleString('ko-KR') }} P</span></span>
|
<span class="v">
|
||||||
</div>
|
<span class="points">{{ points.toLocaleString('ko-KR') }} P</span>
|
||||||
|
<span class="point-more">내역 ›</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
</section>
|
</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>
|
<button type="button" class="primary-btn" @click="goEdit">이름 변경</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -201,6 +254,107 @@ function goEdit() {
|
|||||||
color: hsla(160, 100%, 37%, 1);
|
color: hsla(160, 100%, 37%, 1);
|
||||||
font-weight: 700;
|
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 {
|
.up-link {
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user