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
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 10s
This commit is contained in:
parent
95ee186b52
commit
8ffa2c969a
@ -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'),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user