coupon/front/src/views/HomeView.vue
hyoseung930 2300c59f43 쿠폰 자동 입력 사이트 작성
배포 완료. 이제 **등록된 유저** 섹션 상단에 검색창이 생겨서 닉네임/FID로 필터링됩니다.
2026-04-17 10:32:18 +09:00

204 lines
5.8 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="layout">
<header>
<h1> WOS 쿠폰 자동 입력</h1>
<p class="subtitle">FID를 입력하면 등록된 모든 쿠폰을 자동으로 지급합니다</p>
</header>
<div class="columns">
<div class="col-left">
<div class="card">
<h3 class="section-title">등록된 유저 ({{ filteredUsers.length }})</h3>
<input
v-model="userSearchQuery"
type="text"
class="input user-search"
placeholder="닉네임 / FID 검색"
/>
<div class="user-list" v-if="filteredUsers.length > 0">
<button
v-for="u in filteredUsers"
:key="u.fid"
class="user-item"
:class="{ active: store.fid === u.fid }"
@click="selectUser(u.fid)"
>
<img :src="u.avatar_url" class="user-avatar" @error="onImgErr" />
<span class="user-name">{{ u.nickname }}</span>
</button>
</div>
<p class="empty" v-else>결과가 없습니다.</p>
</div>
</div>
<div class="col-right">
<div class="card">
<div class="search-row">
<input
v-model="fidInput"
type="text"
placeholder="FID (플레이어 ID) 입력"
class="input"
@keydown.enter="searchPlayer"
:disabled="store.status === 'loading'"
/>
<button class="btn btn-primary" @click="searchPlayer" :disabled="store.status === 'loading'">
<span v-if="store.status === 'loading'">⏳ 처리 중...</span>
<span v-else>🎁 쿠폰 받기</span>
</button>
</div>
<p class="error-msg" v-if="store.status === 'error'">{{ store.errorMsg }}</p>
</div>
<UserCard v-if="store.player" :player="store.player" :fid="store.fid" />
<div class="card" v-if="store.player">
<h3 class="section-title">지급 결과</h3>
<CouponInput />
</div>
<div class="card admin-card">
<CouponManager />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { useWosStore } from '../stores/wos.store';
import UserCard from '../components/UserCard.vue';
import CouponInput from '../components/CouponInput.vue';
import CouponManager from '../components/CouponManager.vue';
const store = useWosStore();
const fidInput = ref('');
const userSearchQuery = ref('');
const filteredUsers = computed(() => {
const q = userSearchQuery.value.trim().toLowerCase();
if (!q) return store.savedUsers;
return store.savedUsers.filter(
(u) => u.nickname?.toLowerCase().includes(q) || u.fid.includes(q)
);
});
onMounted(async () => {
store.loadCoupons();
store.loadSavedUsers();
const savedFid = localStorage.getItem('wos_fid');
if (savedFid) {
fidInput.value = savedFid;
await store.searchPlayer(savedFid);
store.loadSavedUsers();
}
});
async function searchPlayer() {
if (!fidInput.value.trim()) return;
const fid = fidInput.value.trim();
localStorage.setItem('wos_fid', fid);
await store.searchPlayer(fid);
store.loadSavedUsers();
}
async function selectUser(fid: string) {
fidInput.value = fid;
localStorage.setItem('wos_fid', fid);
await store.searchPlayer(fid);
store.loadSavedUsers();
}
function onImgErr(e: Event) {
(e.target as HTMLImageElement).style.display = 'none';
}
</script>
<style scoped>
.layout {
max-width: 1100px;
margin: 0 auto;
padding: 32px 16px 60px;
}
header {
text-align: center;
margin-bottom: 28px;
}
header h1 { font-size: 1.9rem; color: #f1f5f9; margin: 0 0 8px; }
.subtitle { color: #64748b; margin: 0; font-size: 0.9rem; }
.columns {
display: flex;
gap: 16px;
align-items: flex-start;
}
.col-left {
width: 220px;
flex-shrink: 0;
}
.col-right {
flex: 1;
min-width: 0;
}
.card {
background: #1e293b;
border: 1px solid #334155;
border-radius: 12px;
padding: 20px;
margin-bottom: 16px;
}
.admin-card { border-style: dashed; border-color: #475569; }
.section-title { margin: 0 0 10px; color: #94a3b8; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em; }
.user-search { width: 100%; box-sizing: border-box; margin-bottom: 10px; font-size: 0.85rem; padding: 7px 10px; }
.user-list { display: flex; flex-direction: column; gap: 6px; max-height: calc(100vh - 320px); overflow-y: auto; }
.user-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
background: #0f172a;
border: 1px solid #334155;
border-radius: 8px;
color: #cbd5e1;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.15s;
text-align: left;
width: 100%;
}
.user-item:hover { border-color: #60a5fa; color: #f1f5f9; }
.user-item.active { border-color: #3b82f6; background: #1e3a5f; color: #93c5fd; }
.user-avatar { width: 28px; height: 28px; border-radius: 50%; object-fit: cover; flex-shrink: 0; }
.user-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.search-row { display: flex; gap: 10px; }
.input {
flex: 1;
padding: 10px 14px;
background: #0f172a;
border: 1px solid #334155;
border-radius: 8px;
color: #f1f5f9;
font-size: 0.95rem;
}
.input:focus { outline: none; border-color: #60a5fa; }
.input:disabled { opacity: 0.5; }
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 0.95rem;
font-weight: 700;
white-space: nowrap;
}
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-primary { background: #3b82f6; color: #fff; }
.btn-primary:hover:not(:disabled) { background: #2563eb; }
.error-msg { color: #f87171; font-size: 0.9rem; margin: 8px 0 0; }
.empty { color: #475569; font-size: 0.9rem; margin: 0; text-align: center; padding: 12px 0; }
</style>