쿠폰 자동 입력 사이트 작성

배포 완료됐습니다.

이제 FID 입력란 아래에 **최근 유저** 칩이 표시됩니다. 한 번 조회한 사람은 아바타 + 닉네임 칩으로 저장되어, 다음번엔 클릭 한 번으로 바로 쿠폰을 받을 수 있습니다.
This commit is contained in:
hyoseung930 2026-04-16 18:06:00 +09:00
parent 1b43b874a1
commit 608a904768
4 changed files with 74 additions and 1 deletions

View File

@ -17,6 +17,11 @@ export class WosController {
return this.wosService.redeemAll(body.fid);
}
@Get('users')
async listUsers() {
return this.wosService.listUsers();
}
@Get('history/:fid')
async getHistory(@Param('fid') fid: string) {
return this.wosService.getHistory(fid);

View File

@ -154,6 +154,10 @@ export class WosService {
});
}
async listUsers() {
return this.wosUserRepo.find({ order: { created_at: 'DESC' } });
}
async listCoupons() {
return this.wosCouponRepo.find({ order: { created_at: 'DESC' } });
}

View File

@ -30,6 +30,13 @@ export interface Coupon {
created_at: string;
}
export interface SavedUser {
fid: string;
nickname: string;
avatar_url: string;
created_at: string;
}
export const useWosStore = defineStore('wos', {
state: () => ({
fid: '',
@ -37,6 +44,7 @@ export const useWosStore = defineStore('wos', {
results: [] as RedeemResult[],
history: [] as HistoryItem[],
coupons: [] as Coupon[],
savedUsers: [] as SavedUser[],
status: 'idle' as 'idle' | 'loading' | 'success' | 'error',
errorMsg: '',
}),
@ -78,6 +86,13 @@ export const useWosStore = defineStore('wos', {
} catch {}
},
async loadSavedUsers() {
try {
const { data } = await axios.get('/api/users');
this.savedUsers = data;
} catch {}
},
async loadCoupons() {
try {
const { data } = await axios.get('/api/coupons');

View File

@ -21,6 +21,22 @@
</button>
</div>
<p class="error-msg" v-if="store.status === 'error'">{{ store.errorMsg }}</p>
<div class="saved-users" v-if="store.savedUsers.length > 0">
<span class="saved-label">최근 유저</span>
<div class="chips">
<button
v-for="u in store.savedUsers"
:key="u.fid"
class="chip"
:class="{ active: store.fid === u.fid }"
@click="selectUser(u.fid)"
>
<img :src="u.avatar_url" class="chip-avatar" @error="onImgErr" />
<span>{{ u.nickname }}</span>
</button>
</div>
</div>
</div>
<UserCard v-if="store.player" :player="store.player" :fid="store.fid" />
@ -52,11 +68,25 @@ import CouponManager from '../components/CouponManager.vue';
const store = useWosStore();
const fidInput = ref('');
onMounted(() => store.loadCoupons());
onMounted(() => {
store.loadCoupons();
store.loadSavedUsers();
});
async function searchPlayer() {
if (!fidInput.value.trim()) return;
await store.searchPlayer(fidInput.value.trim());
store.loadSavedUsers();
}
async function selectUser(fid: string) {
fidInput.value = fid;
await store.searchPlayer(fid);
store.loadSavedUsers();
}
function onImgErr(e: Event) {
(e.target as HTMLImageElement).style.display = 'none';
}
</script>
@ -107,4 +137,23 @@ header h1 { font-size: 1.9rem; color: #f1f5f9; margin: 0 0 8px; }
.btn-primary:hover:not(:disabled) { background: #2563eb; }
.error-msg { color: #f87171; font-size: 0.9rem; margin: 8px 0 0; }
.empty-history { color: #475569; font-size: 0.9rem; margin: 0; text-align: center; }
.saved-users { margin-top: 14px; }
.saved-label { color: #64748b; font-size: 0.8rem; display: block; margin-bottom: 8px; }
.chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chip {
display: flex;
align-items: center;
gap: 6px;
padding: 5px 12px 5px 6px;
background: #0f172a;
border: 1px solid #334155;
border-radius: 20px;
color: #cbd5e1;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.15s;
}
.chip:hover { border-color: #60a5fa; color: #f1f5f9; }
.chip.active { border-color: #3b82f6; background: #1e3a5f; color: #93c5fd; }
.chip-avatar { width: 22px; height: 22px; border-radius: 50%; object-fit: cover; }
</style>