쿠폰 자동 입력 사이트 작성
배포 완료됐습니다. 이제 FID 입력란 아래에 **최근 유저** 칩이 표시됩니다. 한 번 조회한 사람은 아바타 + 닉네임 칩으로 저장되어, 다음번엔 클릭 한 번으로 바로 쿠폰을 받을 수 있습니다.
This commit is contained in:
parent
1b43b874a1
commit
608a904768
@ -17,6 +17,11 @@ export class WosController {
|
|||||||
return this.wosService.redeemAll(body.fid);
|
return this.wosService.redeemAll(body.fid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('users')
|
||||||
|
async listUsers() {
|
||||||
|
return this.wosService.listUsers();
|
||||||
|
}
|
||||||
|
|
||||||
@Get('history/:fid')
|
@Get('history/:fid')
|
||||||
async getHistory(@Param('fid') fid: string) {
|
async getHistory(@Param('fid') fid: string) {
|
||||||
return this.wosService.getHistory(fid);
|
return this.wosService.getHistory(fid);
|
||||||
|
|||||||
@ -154,6 +154,10 @@ export class WosService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async listUsers() {
|
||||||
|
return this.wosUserRepo.find({ order: { created_at: 'DESC' } });
|
||||||
|
}
|
||||||
|
|
||||||
async listCoupons() {
|
async listCoupons() {
|
||||||
return this.wosCouponRepo.find({ order: { created_at: 'DESC' } });
|
return this.wosCouponRepo.find({ order: { created_at: 'DESC' } });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,6 +30,13 @@ export interface Coupon {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SavedUser {
|
||||||
|
fid: string;
|
||||||
|
nickname: string;
|
||||||
|
avatar_url: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const useWosStore = defineStore('wos', {
|
export const useWosStore = defineStore('wos', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
fid: '',
|
fid: '',
|
||||||
@ -37,6 +44,7 @@ export const useWosStore = defineStore('wos', {
|
|||||||
results: [] as RedeemResult[],
|
results: [] as RedeemResult[],
|
||||||
history: [] as HistoryItem[],
|
history: [] as HistoryItem[],
|
||||||
coupons: [] as Coupon[],
|
coupons: [] as Coupon[],
|
||||||
|
savedUsers: [] as SavedUser[],
|
||||||
status: 'idle' as 'idle' | 'loading' | 'success' | 'error',
|
status: 'idle' as 'idle' | 'loading' | 'success' | 'error',
|
||||||
errorMsg: '',
|
errorMsg: '',
|
||||||
}),
|
}),
|
||||||
@ -78,6 +86,13 @@ export const useWosStore = defineStore('wos', {
|
|||||||
} catch {}
|
} catch {}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async loadSavedUsers() {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get('/api/users');
|
||||||
|
this.savedUsers = data;
|
||||||
|
} catch {}
|
||||||
|
},
|
||||||
|
|
||||||
async loadCoupons() {
|
async loadCoupons() {
|
||||||
try {
|
try {
|
||||||
const { data } = await axios.get('/api/coupons');
|
const { data } = await axios.get('/api/coupons');
|
||||||
|
|||||||
@ -21,6 +21,22 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="error-msg" v-if="store.status === 'error'">{{ store.errorMsg }}</p>
|
<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>
|
</div>
|
||||||
|
|
||||||
<UserCard v-if="store.player" :player="store.player" :fid="store.fid" />
|
<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 store = useWosStore();
|
||||||
const fidInput = ref('');
|
const fidInput = ref('');
|
||||||
|
|
||||||
onMounted(() => store.loadCoupons());
|
onMounted(() => {
|
||||||
|
store.loadCoupons();
|
||||||
|
store.loadSavedUsers();
|
||||||
|
});
|
||||||
|
|
||||||
async function searchPlayer() {
|
async function searchPlayer() {
|
||||||
if (!fidInput.value.trim()) return;
|
if (!fidInput.value.trim()) return;
|
||||||
await store.searchPlayer(fidInput.value.trim());
|
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>
|
</script>
|
||||||
|
|
||||||
@ -107,4 +137,23 @@ header h1 { font-size: 1.9rem; color: #f1f5f9; margin: 0 0 8px; }
|
|||||||
.btn-primary:hover:not(:disabled) { background: #2563eb; }
|
.btn-primary:hover:not(:disabled) { background: #2563eb; }
|
||||||
.error-msg { color: #f87171; font-size: 0.9rem; margin: 8px 0 0; }
|
.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; }
|
.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>
|
</style>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user