feat: level selector (beginner/intermediate/advanced) + filter daily words by level
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 10s

This commit is contained in:
deploy 2026-05-18 01:11:47 +00:00
parent 95ee186b52
commit 8ffa2c969a
4 changed files with 71 additions and 7 deletions

View File

@ -4,7 +4,7 @@ const api = axios.create({ baseURL: '/api' })
export const wordsApi = { export const wordsApi = {
getAll: (category, page = 1, limit = 50) => api.get('/words', { params: { category, page, limit } }), getAll: (category, page = 1, limit = 50) => api.get('/words', { params: { category, page, limit } }),
getDaily: () => api.get('/words/daily'), getDaily: (difficulty) => api.get('/words/daily', { params: { difficulty } }),
getQuiz: (count = 5) => api.get('/words/quiz', { params: { count } }), getQuiz: (count = 5) => api.get('/words/quiz', { params: { count } }),
getCategories: () => api.get('/words/categories'), getCategories: () => api.get('/words/categories'),
} }

View File

@ -7,5 +7,12 @@ export const useSessionStore = defineStore('session', {
localStorage.setItem('session_id', id) localStorage.setItem('session_id', id)
return id return id
})(), })(),
level: localStorage.getItem('user_level') || 'beginner',
}), }),
actions: {
setLevel(level) {
this.level = level
localStorage.setItem('user_level', level)
},
},
}) })

View File

@ -12,6 +12,23 @@
</div> </div>
</div> </div>
<!-- 단어 난이도 선택 -->
<div class="card" style="margin-bottom:1rem;padding:1rem 1.2rem">
<div style="font-size:0.82rem;color:#718096;margin-bottom:0.6rem;font-weight:600">📚 단어 수준</div>
<div style="display:flex;gap:0.5rem">
<button
v-for="lvl in diffLevels" :key="lvl.value"
class="level-btn"
:class="{ active: session.level === lvl.value }"
@click="changeLevel(lvl.value)"
>
<span>{{ lvl.emoji }}</span>
<span style="font-size:0.82rem;font-weight:600">{{ lvl.label }}</span>
<span style="font-size:0.7rem;opacity:0.75">{{ lvl.sub }}</span>
</button>
</div>
</div>
<!-- 스트릭 + 콤보 --> <!-- 스트릭 + 콤보 -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:1rem"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:1rem">
<div class="streak-box" style="margin-bottom:0"> <div class="streak-box" style="margin-bottom:0">
@ -108,6 +125,16 @@ const levelNames = ['입문자','초보자','학습자','중급자','숙련자',
const xpPct = computed(() => Math.min(100, Math.round((progress.value.xpProgress / progress.value.xpRange) * 100)) || 0) const xpPct = computed(() => Math.min(100, Math.round((progress.value.xpProgress / progress.value.xpRange) * 100)) || 0)
const earnedBadges = computed(() => (progress.value.allBadges || []).filter(b => b.earned)) const earnedBadges = computed(() => (progress.value.allBadges || []).filter(b => b.earned))
const diffLevels = [
{ value: 'beginner', emoji: '🌱', label: '초급', sub: 'Beginner' },
{ value: 'intermediate', emoji: '📘', label: '중급', sub: 'Intermediate' },
{ value: 'advanced', emoji: '🔥', label: '고급', sub: 'Advanced' },
]
const changeLevel = (lvl) => {
session.setLevel(lvl)
}
onMounted(async () => { onMounted(async () => {
const [q, s, p] = await Promise.all([ const [q, s, p] = await Promise.all([
questApi.getToday(session.id), questApi.getToday(session.id),
@ -125,6 +152,17 @@ onMounted(async () => {
background: linear-gradient(135deg, #667eea, #764ba2); background: linear-gradient(135deg, #667eea, #764ba2);
color: white; border-radius: 12px; padding: 1.2rem 1.5rem; color: white; border-radius: 12px; padding: 1.2rem 1.5rem;
} }
.level-btn {
flex: 1; display: flex; flex-direction: column; align-items: center; gap: 0.15rem;
padding: 0.6rem 0.4rem; border-radius: 10px; border: 2px solid #e2e8f0;
background: #f7fafc; cursor: pointer; transition: all 0.18s;
color: #4a5568;
}
.level-btn.active {
border-color: #667eea; background: linear-gradient(135deg,#ebf4ff,#f0ecff);
color: #553c9a; box-shadow: 0 2px 8px rgba(102,126,234,0.2);
}
.level-btn:hover:not(.active) { border-color: #a0aec0; background: #edf2f7; }
.quick-card { .quick-card {
background: white; border-radius: 12px; padding: 1.2rem; background: white; border-radius: 12px; padding: 1.2rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.06); box-shadow: 0 2px 8px rgba(0,0,0,0.06);

View File

@ -8,8 +8,13 @@
<div class="card start-card"> <div class="card start-card">
<div style="font-size:3rem;margin-bottom:0.75rem">📖</div> <div style="font-size:3rem;margin-bottom:0.75rem">📖</div>
<h2 style="margin-bottom:0.3rem">오늘의 단어 학습</h2> <h2 style="margin-bottom:0.3rem">오늘의 단어 학습</h2>
<p style="color:#718096;margin-bottom:1.5rem;font-size:0.9rem">오늘의 단어 <strong>{{ words.length }}</strong> 학습합니다</p> <div class="level-badge-row">
<button class="btn btn-primary" style="font-size:1rem;padding:0.8rem 2.5rem;width:100%" @click="start">학습 시작</button> <span class="level-badge" :class="`level-${session.level}`">
{{ levelMap[session.level]?.emoji }} {{ levelMap[session.level]?.label }}
</span>
<span style="font-size:0.85rem;color:#718096">수준 · <strong>{{ words.length }}</strong></span>
</div>
<button class="btn btn-primary" style="font-size:1rem;padding:0.8rem 2.5rem;width:100%;margin-top:1.2rem" @click="start">학습 시작</button>
</div> </div>
<div class="card" style="padding:1.2rem"> <div class="card" style="padding:1.2rem">
@ -115,11 +120,17 @@ const xpGained = ref(0)
const totalXp = ref(0) const totalXp = ref(0)
const newBadges = ref([]) const newBadges = ref([])
const levelMap = {
beginner: { emoji: '🌱', label: '초급' },
intermediate: { emoji: '📘', label: '중급' },
advanced: { emoji: '🔥', label: '고급' },
}
const badgeMap = { const badgeMap = {
first_study: { emoji: '🌱', name: '첫 걸음' }, first_study: { emoji: '🌱', name: '첫 걸음' },
words_50: { emoji: '📚', name: '단어 50' }, words_50: { emoji: '📚', name: '단어 50' },
words_100: { emoji: '🎓', name: '단어 100' }, words_100: { emoji: '🎓', name: '단어 100' },
words_500: { emoji: '👑', name: '단어 500' }, words_500: { emoji: '👑', name: '단어 500' },
} }
const diffBadge = (d) => { const diffBadge = (d) => {
@ -131,7 +142,7 @@ const diffBadge = (d) => {
let xpTimer = null let xpTimer = null
onMounted(async () => { onMounted(async () => {
const res = await wordsApi.getDaily() const res = await wordsApi.getDaily(session.level)
words.value = res.data words.value = res.data
loading.value = false loading.value = false
}) })
@ -176,6 +187,14 @@ const prev = () => {
<style scoped> <style scoped>
.start-wrap { display: flex; flex-direction: column; gap: 1rem; } .start-wrap { display: flex; flex-direction: column; gap: 1rem; }
.start-card { text-align: center; padding: 2rem; } .start-card { text-align: center; padding: 2rem; }
.level-badge-row { display: flex; align-items: center; justify-content: center; gap: 0.6rem; margin-top: 0.5rem; }
.level-badge {
display: inline-flex; align-items: center; gap: 0.3rem;
padding: 0.3rem 0.8rem; border-radius: 99px; font-weight: 700; font-size: 0.85rem;
}
.level-beginner { background: #f0fff4; color: #276749; border: 1px solid #9ae6b4; }
.level-intermediate { background: #ebf8ff; color: #2b6cb0; border: 1px solid #90cdf4; }
.level-advanced { background: #fff5f5; color: #9b2c2c; border: 1px solid #feb2b2; }
.today-word-list { display: flex; flex-direction: column; gap: 0.6rem; } .today-word-list { display: flex; flex-direction: column; gap: 0.6rem; }
.today-word-item { .today-word-item {