coupon/front/src/views/HomeView.vue
hyoseung930 9d133ab21e New task
푸시 완료됐습니다. CI/CD가 자동으로 배포합니다.

**변경 내용:**
- 모바일에서 2단 컬럼 유지 (column 전환 제거)
- 왼쪽 유저 리스트: 너비 72px의 좁은 사이드바로 고정
- 닉네임 텍스트 숨김, 아바타 이미지만 표시 (36×36px)
- 검색창 숨김 (공간 절약)
- 아바타 탭으로 유저 선택 가능
- 오른쪽 메인 영역이 나머지 공간 전부 차지
2026-05-19 17:51:37 +09:00

229 lines
6.7 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
ref="fidInputRef"
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, nextTick } 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 fidInputRef = ref<HTMLInputElement | null>(null);
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();
}
await nextTick();
fidInputRef.value?.focus();
});
async function searchPlayer() {
if (!fidInput.value.trim()) return;
const fid = fidInput.value.trim();
localStorage.setItem('wos_fid', fid);
await store.searchPlayer(fid);
store.loadSavedUsers();
await nextTick();
fidInputRef.value?.focus();
}
async function selectUser(fid: string) {
fidInput.value = fid;
localStorage.setItem('wos_fid', fid);
await store.searchPlayer(fid);
store.loadSavedUsers();
await nextTick();
fidInputRef.value?.focus();
}
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: #1e293b; 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: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 20px;
margin-bottom: 16px;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
}
.admin-card { border-style: dashed; border-color: #cbd5e1; }
.section-title { margin: 0 0 10px; color: #64748b; 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: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
color: #334155;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.15s;
text-align: left;
width: 100%;
}
.user-item:hover { border-color: #3b82f6; color: #1e293b; background: #f0f7ff; }
.user-item.active { border-color: #3b82f6; background: #eff6ff; color: #2563eb; }
.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: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
color: #1e293b;
font-size: 0.95rem;
}
.input:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59,130,246,0.1); }
.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: #dc2626; font-size: 0.9rem; margin: 8px 0 0; }
.empty { color: #94a3b8; font-size: 0.9rem; margin: 0; text-align: center; padding: 12px 0; }
@media (max-width: 640px) {
.layout { padding: 16px 8px 40px; }
header h1 { font-size: 1.4rem; }
.columns { gap: 8px; align-items: stretch; }
.col-left { width: 72px; flex-shrink: 0; }
.col-left .card { padding: 10px 6px; }
.user-search { display: none; }
.user-list { max-height: calc(100vh - 180px); gap: 4px; }
.user-item { padding: 6px 4px; justify-content: center; }
.user-name { display: none; }
.user-avatar { width: 36px; height: 36px; }
.search-row { flex-direction: column; }
.search-row .btn { width: 100%; }
.card { padding: 12px; }
}
</style>