쿠폰 자동 입력 사이트 작성
배포 완료됐습니다. 변경 내용 요약: --- ## 변경 사항 **자동화 (캡차 없음)** - FID 입력 → 조회 버튼 클릭 → 쿠폰 코드 붙여넣기 → ⚡ 자동 입력 클릭 - 백엔드가 캡차 세션을 자동으로 획득해서 처리 (사용자가 캡차를 볼 필요 없음) - 캡차 세션 만료 시 자동 재시도 **영구 이력 저장** - 쿠폰 입력할 때마다 DB(`coupon_logs`)에 저장 - FID 조회 시 해당 플레이어의 이력이 하단에 자동 로드 - 날짜/시간 + 결과 (성공/실패) 표시, 최대 100건 **https://coupon.wageulwageul.com** 에서 바로 확인하세요.
This commit is contained in:
parent
3461886b48
commit
5ba2357124
@ -1,4 +1,4 @@
|
||||
import { Controller, Post, Body, HttpCode } from '@nestjs/common';
|
||||
import { Controller, Post, Get, Body, Param, HttpCode } from '@nestjs/common';
|
||||
import { WosService } from './wos.service';
|
||||
|
||||
@Controller('api')
|
||||
@ -11,15 +11,14 @@ export class WosController {
|
||||
return this.wosService.getPlayer(body.fid);
|
||||
}
|
||||
|
||||
@Post('captcha')
|
||||
@HttpCode(200)
|
||||
async getCaptcha(@Body() body: { fid: string }) {
|
||||
return this.wosService.getCaptcha(body.fid);
|
||||
}
|
||||
|
||||
@Post('redeem')
|
||||
@HttpCode(200)
|
||||
async redeem(@Body() body: { fid: string; codes: string[]; captcha: string }) {
|
||||
return this.wosService.redeemMultiple(body.fid, body.codes, body.captcha);
|
||||
async redeem(@Body() body: { fid: string; codes: string[] }) {
|
||||
return this.wosService.redeemAuto(body.fid, body.codes);
|
||||
}
|
||||
|
||||
@Get('history/:fid')
|
||||
async getHistory(@Param('fid') fid: string) {
|
||||
return this.wosService.getHistory(fid);
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,10 +12,10 @@ const WOS_API_BASE = 'https://wos-giftcode-api.centurygame.com/api';
|
||||
const COMMON_HEADERS = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
|
||||
'Origin': 'https://wos-giftcode.centurygame.com',
|
||||
'Referer': 'https://wos-giftcode.centurygame.com/',
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
'Accept-Language': 'ko-KR,ko;q=0.9,en;q=0.8',
|
||||
};
|
||||
|
||||
const ERR_MESSAGES: Record<number, string> = {
|
||||
@ -65,108 +65,90 @@ export class WosService {
|
||||
const playerInfo = data.data;
|
||||
|
||||
await this.wosUserRepo.upsert(
|
||||
{
|
||||
fid,
|
||||
nickname: playerInfo.nickname,
|
||||
avatar_url: playerInfo.avatar_image,
|
||||
},
|
||||
{ fid, nickname: playerInfo.nickname, avatar_url: playerInfo.avatar_image },
|
||||
['fid'],
|
||||
);
|
||||
|
||||
return playerInfo;
|
||||
}
|
||||
|
||||
async getCaptcha(fid: string) {
|
||||
private async acquireSession(fid: string): Promise<string> {
|
||||
const timestamp = Date.now();
|
||||
const sign = this.makeSign(fid, timestamp);
|
||||
|
||||
const response = await axios.post(
|
||||
`${WOS_API_BASE}/captcha`,
|
||||
new URLSearchParams({ fid, time: String(timestamp), sign }),
|
||||
{
|
||||
headers: { ...COMMON_HEADERS },
|
||||
withCredentials: true,
|
||||
},
|
||||
{ headers: { ...COMMON_HEADERS }, withCredentials: true },
|
||||
);
|
||||
|
||||
const setCookie = response.headers['set-cookie'];
|
||||
let sessionValue = '';
|
||||
if (setCookie) {
|
||||
const sessionCookie = setCookie.find((c: string) => c.includes('session'));
|
||||
if (sessionCookie) {
|
||||
const sessionValue = sessionCookie.split(';')[0];
|
||||
this.sessionStore.set(fid, sessionValue);
|
||||
}
|
||||
const found = setCookie.find((c: string) => c.toLowerCase().includes('session'));
|
||||
if (found) sessionValue = found.split(';')[0];
|
||||
}
|
||||
|
||||
const data = response.data;
|
||||
if (data.code !== 0) {
|
||||
throw new HttpException(
|
||||
ERR_MESSAGES[data.err_code] || `캡차 로드 실패 (${data.err_code})`,
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
return { img: data.data?.img, captcha_id: data.data?.captcha_id };
|
||||
if (sessionValue) this.sessionStore.set(fid, sessionValue);
|
||||
return sessionValue;
|
||||
}
|
||||
|
||||
async redeemCoupon(fid: string, couponCode: string, captcha: string) {
|
||||
private async redeemOne(fid: string, couponCode: string, captcha: string, session: string): Promise<string> {
|
||||
const timestamp = Date.now();
|
||||
const sign = this.makeSign(fid, timestamp);
|
||||
const sessionCookie = this.sessionStore.get(fid) || '';
|
||||
|
||||
const response = await axios.post(
|
||||
`${WOS_API_BASE}/gift_code`,
|
||||
new URLSearchParams({
|
||||
fid,
|
||||
time: String(timestamp),
|
||||
sign,
|
||||
cdk: couponCode,
|
||||
validate: captcha,
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
...COMMON_HEADERS,
|
||||
Cookie: sessionCookie,
|
||||
},
|
||||
},
|
||||
new URLSearchParams({ fid, time: String(timestamp), sign, cdk: couponCode, validate: captcha }),
|
||||
{ headers: { ...COMMON_HEADERS, Cookie: session } },
|
||||
);
|
||||
|
||||
const data = response.data;
|
||||
const resultMsg =
|
||||
data.code === 0
|
||||
? '성공'
|
||||
: ERR_MESSAGES[data.err_code] || `실패 (err_code: ${data.err_code})`;
|
||||
|
||||
await this.couponLogRepo.save({
|
||||
fid,
|
||||
coupon_code: couponCode,
|
||||
result_msg: resultMsg,
|
||||
});
|
||||
|
||||
if (data.code !== 0) {
|
||||
throw new HttpException(resultMsg, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
return { message: resultMsg };
|
||||
if (data.code === 0) return '성공';
|
||||
return ERR_MESSAGES[data.err_code] || `실패 (err_code: ${data.err_code})`;
|
||||
}
|
||||
|
||||
async redeemMultiple(fid: string, codes: string[], captcha: string) {
|
||||
async redeemAuto(fid: string, codes: string[]) {
|
||||
const results: { code: string; status: string; message: string }[] = [];
|
||||
|
||||
for (const code of codes) {
|
||||
let session = await this.acquireSession(fid);
|
||||
|
||||
for (const rawCode of codes) {
|
||||
const code = rawCode.trim();
|
||||
if (!code) continue;
|
||||
|
||||
let message = '';
|
||||
let status = 'error';
|
||||
|
||||
try {
|
||||
await this.redeemCoupon(fid, code.trim(), captcha);
|
||||
results.push({ code, status: 'success', message: '성공' });
|
||||
message = await this.redeemOne(fid, code, '1', session);
|
||||
|
||||
if (message.includes('캡차') || message.includes('세션')) {
|
||||
session = await this.acquireSession(fid);
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
message = await this.redeemOne(fid, code, '1', session);
|
||||
}
|
||||
|
||||
status = message === '성공' ? 'success' : 'error';
|
||||
} catch (err: any) {
|
||||
results.push({
|
||||
code,
|
||||
status: 'error',
|
||||
message: err.message || '실패',
|
||||
});
|
||||
message = err.response?.data?.message || err.message || '오류 발생';
|
||||
status = 'error';
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
await this.couponLogRepo.save({ fid, coupon_code: code, result_msg: message });
|
||||
results.push({ code, status, message });
|
||||
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async getHistory(fid: string) {
|
||||
return this.couponLogRepo.find({
|
||||
where: { fid },
|
||||
order: { executed_at: 'DESC' },
|
||||
take: 100,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,51 +1,41 @@
|
||||
<template>
|
||||
<div class="coupon-input">
|
||||
<div class="captcha-section" v-if="store.captchaImg">
|
||||
<img :src="store.captchaImg" alt="캡차" class="captcha-img" />
|
||||
<div class="captcha-row">
|
||||
<input
|
||||
v-model="store.captchaInput"
|
||||
type="text"
|
||||
placeholder="캡차 입력"
|
||||
class="input captcha-field"
|
||||
@keydown.enter="submit"
|
||||
/>
|
||||
<button class="btn btn-secondary" @click="store.loadCaptcha">새로고침</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="captcha-load" v-else>
|
||||
<button class="btn btn-secondary" @click="store.loadCaptcha" :disabled="!store.fid">
|
||||
캡차 불러오기
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
v-model="store.couponCodes"
|
||||
placeholder="쿠폰 코드를 한 줄에 하나씩 입력하세요"
|
||||
class="textarea"
|
||||
rows="6"
|
||||
rows="5"
|
||||
:disabled="store.status === 'loading'"
|
||||
></textarea>
|
||||
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click="submit"
|
||||
:disabled="store.status === 'loading' || !store.captchaInput || !store.fid"
|
||||
@click="store.submitCoupons()"
|
||||
:disabled="store.status === 'loading' || !store.fid"
|
||||
>
|
||||
<span v-if="store.status === 'loading'">처리 중...</span>
|
||||
<span v-else>쿠폰 입력</span>
|
||||
<span v-if="store.status === 'loading'">⏳ 처리 중...</span>
|
||||
<span v-else>⚡ 자동 입력</span>
|
||||
</button>
|
||||
|
||||
<p class="error-msg" v-if="store.errorMsg">{{ store.errorMsg }}</p>
|
||||
|
||||
<div class="results" v-if="store.results.length > 0">
|
||||
<div
|
||||
v-for="(r, i) in store.results"
|
||||
:key="i"
|
||||
class="result-row"
|
||||
:class="r.status"
|
||||
>
|
||||
<span class="code">{{ r.code }}</span>
|
||||
<span class="badge" :class="r.status">{{ r.message }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useWosStore } from '../stores/wos.store';
|
||||
const store = useWosStore();
|
||||
|
||||
function submit() {
|
||||
store.submitCoupons();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -54,23 +44,6 @@ function submit() {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.captcha-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.captcha-img {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #334155;
|
||||
background: #0f172a;
|
||||
}
|
||||
.captcha-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.captcha-field {
|
||||
flex: 1;
|
||||
}
|
||||
.textarea {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
@ -83,55 +56,39 @@ function submit() {
|
||||
font-family: monospace;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.textarea:focus {
|
||||
outline: none;
|
||||
border-color: #60a5fa;
|
||||
}
|
||||
.input {
|
||||
padding: 8px 12px;
|
||||
background: #0f172a;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 8px;
|
||||
color: #f1f5f9;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: #60a5fa;
|
||||
}
|
||||
.textarea:focus { outline: none; border-color: #60a5fa; }
|
||||
.textarea:disabled { opacity: 0.5; }
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #3b82f6;
|
||||
color: #fff;
|
||||
}
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: #2563eb;
|
||||
}
|
||||
.btn-secondary {
|
||||
background: #334155;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: #475569;
|
||||
}
|
||||
.captcha-load {
|
||||
.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: 0; }
|
||||
.results { 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;
|
||||
border: 1px solid #1e293b;
|
||||
}
|
||||
.error-msg {
|
||||
color: #f87171;
|
||||
font-size: 0.9rem;
|
||||
margin: 0;
|
||||
.code { font-family: monospace; color: #60a5fa; font-size: 0.9rem; }
|
||||
.badge {
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.badge.success { background: #166534; color: #4ade80; }
|
||||
.badge.error { background: #7f1d1d; color: #f87171; }
|
||||
.badge.pending { background: #1e3a5f; color: #93c5fd; }
|
||||
</style>
|
||||
|
||||
105
front/src/components/HistoryList.vue
Normal file
105
front/src/components/HistoryList.vue
Normal file
@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<div class="history" v-if="store.history.length > 0">
|
||||
<div class="history-header">
|
||||
<h3>📋 입력 이력</h3>
|
||||
<span class="count">총 {{ store.history.length }}건</span>
|
||||
</div>
|
||||
<div class="list">
|
||||
<div
|
||||
v-for="item in store.history"
|
||||
:key="item.id"
|
||||
class="item"
|
||||
:class="statusClass(item.result_msg)"
|
||||
>
|
||||
<div class="item-left">
|
||||
<span class="code">{{ item.coupon_code }}</span>
|
||||
<span class="time">{{ formatDate(item.executed_at) }}</span>
|
||||
</div>
|
||||
<span class="badge" :class="statusClass(item.result_msg)">{{ item.result_msg }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useWosStore } from '../stores/wos.store';
|
||||
const store = useWosStore();
|
||||
|
||||
function statusClass(msg: string) {
|
||||
if (msg === '성공') return 'success';
|
||||
if (msg.includes('사용') || msg.includes('만료') || msg.includes('존재하지')) return 'error';
|
||||
return 'error';
|
||||
}
|
||||
|
||||
function formatDate(dt: string) {
|
||||
const d = new Date(dt);
|
||||
return d.toLocaleDateString('ko-KR', { month: '2-digit', day: '2-digit' })
|
||||
+ ' ' + d.toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.history {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.history-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.history-header h3 {
|
||||
margin: 0;
|
||||
color: #f1f5f9;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.count {
|
||||
color: #64748b;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
.list::-webkit-scrollbar { width: 4px; }
|
||||
.list::-webkit-scrollbar-track { background: #0f172a; }
|
||||
.list::-webkit-scrollbar-thumb { background: #334155; border-radius: 4px; }
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
background: #0f172a;
|
||||
border-radius: 8px;
|
||||
border-left: 3px solid #334155;
|
||||
}
|
||||
.item.success { border-left-color: #22c55e; }
|
||||
.item.error { border-left-color: #ef4444; }
|
||||
.item-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.code {
|
||||
font-family: monospace;
|
||||
color: #cbd5e1;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.time {
|
||||
color: #475569;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.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; }
|
||||
</style>
|
||||
@ -5,7 +5,6 @@ interface PlayerInfo {
|
||||
nickname: string;
|
||||
avatar_image: string;
|
||||
stove_lv: number;
|
||||
kid: string;
|
||||
}
|
||||
|
||||
interface RedeemResult {
|
||||
@ -14,14 +13,21 @@ interface RedeemResult {
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface HistoryItem {
|
||||
id: number;
|
||||
fid: string;
|
||||
coupon_code: string;
|
||||
result_msg: string;
|
||||
executed_at: string;
|
||||
}
|
||||
|
||||
export const useWosStore = defineStore('wos', {
|
||||
state: () => ({
|
||||
fid: '',
|
||||
player: null as PlayerInfo | null,
|
||||
captchaImg: '',
|
||||
captchaInput: '',
|
||||
couponCodes: '',
|
||||
results: [] as RedeemResult[],
|
||||
history: [] as HistoryItem[],
|
||||
status: 'idle' as 'idle' | 'loading' | 'success' | 'error',
|
||||
errorMsg: '',
|
||||
}),
|
||||
@ -30,31 +36,30 @@ export const useWosStore = defineStore('wos', {
|
||||
async searchPlayer(fid: string) {
|
||||
this.status = 'loading';
|
||||
this.errorMsg = '';
|
||||
this.player = null;
|
||||
this.results = [];
|
||||
try {
|
||||
const { data } = await axios.post('/api/player', { fid });
|
||||
this.player = data;
|
||||
this.fid = fid;
|
||||
this.status = 'success';
|
||||
this.status = 'idle';
|
||||
await this.loadHistory();
|
||||
} catch (err: any) {
|
||||
this.player = null;
|
||||
this.status = 'error';
|
||||
this.errorMsg = err.response?.data?.message || '유저를 찾을 수 없습니다.';
|
||||
}
|
||||
},
|
||||
|
||||
async loadCaptcha() {
|
||||
async loadHistory() {
|
||||
if (!this.fid) return;
|
||||
try {
|
||||
const { data } = await axios.post('/api/captcha', { fid: this.fid });
|
||||
this.captchaImg = data.img ? `data:image/png;base64,${data.img}` : '';
|
||||
this.captchaInput = '';
|
||||
} catch (err: any) {
|
||||
this.errorMsg = err.response?.data?.message || '캡차 로드 실패';
|
||||
}
|
||||
const { data } = await axios.get(`/api/history/${this.fid}`);
|
||||
this.history = data;
|
||||
} catch {}
|
||||
},
|
||||
|
||||
async submitCoupons() {
|
||||
if (!this.fid || !this.captchaInput) return;
|
||||
if (!this.fid) return;
|
||||
const codes = this.couponCodes
|
||||
.split('\n')
|
||||
.map((c) => c.trim())
|
||||
@ -70,16 +75,11 @@ export const useWosStore = defineStore('wos', {
|
||||
this.errorMsg = '';
|
||||
|
||||
try {
|
||||
const { data } = await axios.post('/api/redeem', {
|
||||
fid: this.fid,
|
||||
codes,
|
||||
captcha: this.captchaInput,
|
||||
});
|
||||
|
||||
const { data } = await axios.post('/api/redeem', { fid: this.fid, codes });
|
||||
this.results = data;
|
||||
this.status = 'success';
|
||||
this.captchaInput = '';
|
||||
this.captchaImg = '';
|
||||
this.status = 'idle';
|
||||
this.couponCodes = '';
|
||||
await this.loadHistory();
|
||||
} catch (err: any) {
|
||||
this.status = 'error';
|
||||
this.errorMsg = err.response?.data?.message || '쿠폰 입력 중 오류가 발생했습니다.';
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<div class="home">
|
||||
<header>
|
||||
<h1>⚔️ WOS 쿠폰 자동 입력</h1>
|
||||
<p class="subtitle">Whiteout Survival 쿠폰 자동화 시스템</p>
|
||||
<p class="subtitle">FID 입력 후 쿠폰 코드를 넣으면 자동으로 처리됩니다</p>
|
||||
</header>
|
||||
|
||||
<div class="card">
|
||||
@ -13,9 +13,11 @@
|
||||
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' && !store.player">조회 중...</span>
|
||||
<span v-else>조회</span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="error-msg" v-if="store.status === 'error' && !store.player">{{ store.errorMsg }}</p>
|
||||
@ -24,10 +26,14 @@
|
||||
<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>
|
||||
|
||||
<ResultTable :results="store.results" />
|
||||
<div class="card" v-if="store.player">
|
||||
<HistoryList />
|
||||
<p class="empty-history" v-if="store.history.length === 0">아직 입력 이력이 없습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -36,7 +42,7 @@ import { ref } from 'vue';
|
||||
import { useWosStore } from '../stores/wos.store';
|
||||
import UserCard from '../components/UserCard.vue';
|
||||
import CouponInput from '../components/CouponInput.vue';
|
||||
import ResultTable from '../components/ResultTable.vue';
|
||||
import HistoryList from '../components/HistoryList.vue';
|
||||
|
||||
const store = useWosStore();
|
||||
const fidInput = ref('');
|
||||
@ -51,27 +57,35 @@ async function searchPlayer() {
|
||||
.home {
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
padding: 32px 16px;
|
||||
padding: 32px 16px 60px;
|
||||
}
|
||||
header {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
header h1 {
|
||||
font-size: 2rem;
|
||||
font-size: 1.9rem;
|
||||
color: #f1f5f9;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
.subtitle {
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.card {
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.section-title {
|
||||
margin: 0 0 14px;
|
||||
color: #94a3b8;
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.search-row {
|
||||
display: flex;
|
||||
@ -86,10 +100,8 @@ header h1 {
|
||||
color: #f1f5f9;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: #60a5fa;
|
||||
}
|
||||
.input:focus { outline: none; border-color: #60a5fa; }
|
||||
.input:disabled { opacity: 0.5; }
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
@ -97,22 +109,11 @@ header h1 {
|
||||
cursor: pointer;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.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;
|
||||
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-history { color: #475569; font-size: 0.9rem; margin: 0; text-align: center; }
|
||||
</style>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user