쿠폰 자동 입력 사이트 작성
## 적용된 변경사항
### 백엔드
- **`wos_coupons` 테이블에 `is_expired` 컬럼 추가** (TypeORM synchronize로 자동 생성)
- **`redeemAll` 로직 개선**:
- 이미 `성공` / `이미 수령` 로그 있는 쿠폰 → 자동 **스킵** (재지급 안 함)
- `err_code: 40008` (RECEIVED) → **성공** 처리 (`이미 수령` 메시지)
- `err_code: 40007` (TIME ERROR) → DB에서 `is_expired = true, is_active = false` 업데이트
- **매일 오전 9시 자동 실행** (`@Cron('0 0 9 * * *')`) - 저장된 모든 유저에게 미수령 쿠폰 자동 지급
### 프론트엔드
- **삭제 버튼 제거**
- 만료된 쿠폰은 **보라색 "만료" 배지** 표시, 토글 버튼 숨김
- 활성 쿠폰 카운트에서 만료 제외
This commit is contained in:
@@ -12,16 +12,26 @@
|
||||
</div>
|
||||
|
||||
<div class="coupon-list" v-if="store.coupons.length > 0">
|
||||
<div v-for="c in store.coupons" :key="c.id" class="coupon-item" :class="{ inactive: !c.is_active }">
|
||||
<div
|
||||
v-for="c in store.coupons"
|
||||
:key="c.id"
|
||||
class="coupon-item"
|
||||
:class="{ inactive: !c.is_active, expired: c.is_expired }"
|
||||
>
|
||||
<div class="coupon-info">
|
||||
<span class="coupon-name">{{ c.name }}</span>
|
||||
<span class="coupon-code">{{ c.code }}</span>
|
||||
</div>
|
||||
<div class="coupon-actions">
|
||||
<button class="btn-toggle" :class="c.is_active ? 'on' : 'off'" @click="store.toggleCoupon(c.id, !c.is_active)">
|
||||
<span v-if="c.is_expired" class="badge badge-expired">만료</span>
|
||||
<button
|
||||
v-else
|
||||
class="btn-toggle"
|
||||
:class="c.is_active ? 'on' : 'off'"
|
||||
@click="store.toggleCoupon(c.id, !c.is_active)"
|
||||
>
|
||||
{{ c.is_active ? '활성' : '비활성' }}
|
||||
</button>
|
||||
<button class="btn-delete" @click="remove(c.id)">삭제</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -37,7 +47,7 @@ const store = useWosStore();
|
||||
const newName = ref('');
|
||||
const newCode = ref('');
|
||||
|
||||
const activeCoupons = computed(() => store.coupons.filter((c) => c.is_active).length);
|
||||
const activeCoupons = computed(() => store.coupons.filter((c) => c.is_active && !c.is_expired).length);
|
||||
|
||||
async function add() {
|
||||
if (!newName.value.trim() || !newCode.value.trim()) return;
|
||||
@@ -45,11 +55,6 @@ async function add() {
|
||||
newName.value = '';
|
||||
newCode.value = '';
|
||||
}
|
||||
|
||||
async function remove(id: number) {
|
||||
if (!confirm('삭제하시겠습니까?')) return;
|
||||
await store.deleteCoupon(id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -95,6 +100,7 @@ async function remove(id: number) {
|
||||
gap: 8px;
|
||||
}
|
||||
.coupon-item.inactive { border-left-color: #334155; opacity: 0.6; }
|
||||
.coupon-item.expired { border-left-color: #7c3aed; opacity: 0.5; }
|
||||
.coupon-info { display: flex; flex-direction: column; gap: 2px; flex: 1; }
|
||||
.coupon-name { color: #f1f5f9; font-size: 0.9rem; font-weight: 500; }
|
||||
.coupon-code { color: #60a5fa; font-family: monospace; font-size: 0.82rem; }
|
||||
@@ -109,7 +115,7 @@ async function remove(id: number) {
|
||||
}
|
||||
.btn-toggle.on { background: #166534; color: #4ade80; }
|
||||
.btn-toggle.off { background: #374151; color: #9ca3af; }
|
||||
.btn-delete { background: none; border: none; color: #ef4444; cursor: pointer; font-size: 0.8rem; padding: 3px 6px; }
|
||||
.btn-delete:hover { color: #dc2626; }
|
||||
.badge { padding: 3px 10px; border-radius: 20px; font-size: 0.78rem; font-weight: 600; }
|
||||
.badge-expired { background: #3b0764; color: #c084fc; }
|
||||
.empty { color: #475569; font-size: 0.9rem; margin: 0; text-align: center; }
|
||||
</style>
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface Coupon {
|
||||
name: string;
|
||||
code: string;
|
||||
is_active: boolean;
|
||||
is_expired: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user