완료됐습니다. 변경 내용 요약: --- ## 변경 사항 ### 버그 수정 - TypeORM `upsert` → MySQL 호환 `findOne + update/save` 방식으로 수정 → 500 에러 해결 ### 신규 기능 **쿠폰 관리 (관리자)** - **https://coupon.wageulwageul.com/admin** 에서 쿠폰 추가/삭제/활성화 토글 - 쿠폰 코드 + 이름 저장 (예: `ABC123` / `신년 이벤트`) **자동 전체 지급 (사용자)** - FID 입력 → 조회 → **⚡ 전체 쿠폰 자동 지급** 버튼 클릭 - DB에 등록된 활성 쿠폰을 전부 자동으로 시도 - 결과(쿠폰명 + 코드 + 성공/실패)가 바로 표시되고 DB에 영구 저장 - 이력은 FID별로 조회 시 자동 로드 **사용 흐름:** 1. `/admin` 에서 쿠폰 등록 2. 사용자는 `/` 에서 FID 입력 → 버튼 한 번으로 모든 쿠폰 자동 지급
62 lines
2.1 KiB
Vue
62 lines
2.1 KiB
Vue
<template>
|
|
<div class="redeem-results" v-if="store.results.length > 0">
|
|
<div class="results-header">
|
|
<span class="label">이번 지급 결과</span>
|
|
<span class="summary">
|
|
성공 {{ successCount }}개 / 전체 {{ store.results.length }}개
|
|
</span>
|
|
</div>
|
|
<div class="result-list">
|
|
<div v-for="(r, i) in store.results" :key="i" class="result-row">
|
|
<div class="result-info">
|
|
<span class="result-name">{{ r.name }}</span>
|
|
<span class="result-code">{{ r.code }}</span>
|
|
</div>
|
|
<span class="badge" :class="r.status">{{ r.message }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="empty-result" v-else-if="store.status === 'idle' && store.player">
|
|
<p>쿠폰이 없거나 모두 지급됐습니다.</p>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed } from 'vue';
|
|
import { useWosStore } from '../stores/wos.store';
|
|
|
|
const store = useWosStore();
|
|
const successCount = computed(() => store.results.filter((r) => r.status === 'success').length);
|
|
</script>
|
|
|
|
<style scoped>
|
|
.redeem-results { display: flex; flex-direction: column; gap: 10px; }
|
|
.results-header { display: flex; justify-content: space-between; align-items: center; }
|
|
.label { color: #94a3b8; font-size: 0.85rem; }
|
|
.summary { color: #60a5fa; font-size: 0.85rem; font-weight: 600; }
|
|
.result-list { display: flex; flex-direction: column; gap: 6px; }
|
|
.result-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 8px 12px;
|
|
background: #0f172a;
|
|
border-radius: 8px;
|
|
gap: 8px;
|
|
}
|
|
.result-info { display: flex; flex-direction: column; gap: 2px; }
|
|
.result-name { color: #cbd5e1; font-size: 0.9rem; }
|
|
.result-code { color: #475569; font-family: monospace; font-size: 0.78rem; }
|
|
.badge {
|
|
padding: 3px 10px;
|
|
border-radius: 20px;
|
|
font-size: 0.78rem;
|
|
font-weight: 600;
|
|
white-space: nowrap;
|
|
}
|
|
.badge.success { background: #166534; color: #4ade80; }
|
|
.badge.error { background: #7f1d1d; color: #f87171; }
|
|
.badge.pending { background: #1e3a5f; color: #93c5fd; }
|
|
.empty-result p { color: #475569; font-size: 0.9rem; margin: 0; text-align: center; }
|
|
</style>
|