Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: 학습을 위해 카드 뭉치의 카드들을 조회할 수 있다. (#12)
* 🧪 getters test 추가 - id로 필터링 * ✨ Vuex getters 추가 - id에 해당되는 Learnset 반환 * ✅ Vuex getter 수정 및 추가 - 기준날짜 이전에 학습해야 하는 데이터 조회 * ✨ /learn route 추가 - 학습 카드 데이터를 확인할 수 있는 경로 * 🧪 LearnsetCard Component test 추가 * 🐛 중간 질문에 응답이 없는 경우 처리 추가 - 테스트 수정 - 기존 코드는 마지막 항목을 처리하지 않았음 * ♻️ vuex getters 수정 - id 파라미터전달 * ✨ /learnset route에서 /learn 로 이동하는 버튼추가 * 🧪 LearnsetCard test 수정 - showBack, submitted data 전달 * ✨ Vuex getters 추가 * 📦 highlight.js, swiper 설치 * 💄 highlight, swiper 관련 style 추가 * ✨ LearnsetCard 렌더링 * 💄 markdown rendering css 추가 * ♻️ markdown-it plugin 생성 및 적용 - provide, inject로 불러와서 사용하기 - 테스트 코드에도 plugin 적용 * ♻️ BaseButton Component를 공통으로 사용하도록 수정 * ♻️ 점수 데이터 상수로 분리 * ✨ Vuex getters 수정 - 매개변수로 id, baseDate 전달 * 🧪 LearnsetCard Component router 설정 * 📦 supermemo 설치 * ✨ Card 점수 매기기 추가 - supermemo로 dueDate 업데이트 - dueDate가 오늘 이전인 항목만 활성화하는 switch 추가 * 🔥 필요없는 코드 삭제 * ✅ 로직 변경으로 인한 테스트 수정
- Loading branch information
Showing
22 changed files
with
1,422 additions
and
278 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
@import 'highlight.js/scss/base16/atelier-sulphurpool-light.scss'; | ||
@import 'github-markdown-css/github-markdown.css'; | ||
|
||
:root { | ||
--swiper-navigation-sides-offset: -60px; | ||
--swiper-navigation-size: 32px; /* To edit the size of the arrows */ | ||
--swiper-navigation-color: #4527a0; /* To edit the color of the arrows */ | ||
} | ||
|
||
.markdown-body { | ||
padding: 10px; | ||
background-color: rgba(0, 0, 0, 0); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
<template> | ||
<div class="d-flex flex-column pa-3 w-100"> | ||
<h2 class="text-h5 font-weight-black">{{ card.title }}</h2> | ||
<v-textarea | ||
label="내 답변" | ||
:disabled="showBack" | ||
variant="outlined" | ||
clearable | ||
auto-grow | ||
class="pa-3" | ||
rows="2" | ||
></v-textarea> | ||
<div class="d-flex justify-center" v-if="!showBack"> | ||
<BaseButton | ||
text="정답 확인하기" | ||
variant="outlined" | ||
data-testid="show-answer-btn" | ||
@click="showBack = !showBack" | ||
/> | ||
</div> | ||
<div class="d-flex flex-column pa-3" v-else> | ||
<v-card variant="outlined"> | ||
<v-card-title class="font-weight-bold">정답</v-card-title> | ||
<v-card-text class="text-left"> | ||
<p v-html="backHtml" class="rounded-pill markdown-body"></p> | ||
</v-card-text> | ||
</v-card> | ||
|
||
<template v-if="review"> | ||
<p class="mt-6 mb-2 text-body-2 text-left"> | ||
정답과 유사한 정도에 따라 점수를 매기세요. | ||
</p> | ||
<div class="d-flex flex-wrap justify-space-around btn-wrapper"> | ||
<BaseButton | ||
v-for="score in SCORES" | ||
:key="score.score" | ||
:text="score.score" | ||
:size="size" | ||
:disabled="submitted" | ||
@click="() => submit(Number(score.score))" | ||
> | ||
<v-tooltip | ||
activator="parent" | ||
location="bottom" | ||
:disabled="submitted" | ||
> | ||
{{ score.tooltip }} | ||
</v-tooltip> | ||
</BaseButton> | ||
</div> | ||
</template> | ||
</div> | ||
</div> | ||
</template> | ||
|
||
<script setup lang="ts"> | ||
import BaseButton from './BaseButton.vue'; | ||
import type { Card } from '@/types/interfaces'; | ||
import { ref, computed, type PropType } from 'vue'; | ||
import { useStore } from 'vuex'; | ||
import { useDisplay } from 'vuetify'; | ||
import { useMarkdownIt } from '@/plugins/markdownit'; | ||
import { useRoute } from 'vue-router'; | ||
import { SCORES } from '@/constants'; | ||
import type { MyStore } from '@/store/types'; | ||
import { MutationTypes } from '@/store/mutations'; | ||
import type { SuperMemoGrade } from 'supermemo'; | ||
import { practice } from '@/utils/supermemo'; | ||
const showBack = ref(false); | ||
const submitted = ref(false); | ||
const store: MyStore = useStore(); | ||
const learnsets = computed(() => store.state.learnsets); | ||
const route = useRoute(); | ||
const { id: learnsetId } = route.params; | ||
const submit = (score: number) => { | ||
const reviewCard = practice(prop.card, score as SuperMemoGrade); | ||
const learnsetIdx = learnsets.value.findIndex( | ||
(learnset) => learnset.id === learnsetId | ||
); | ||
const cardIdx = learnsets.value[learnsetIdx].cards.findIndex( | ||
(card) => card.id === prop.card.id | ||
); | ||
learnsets.value[learnsetIdx].cards[cardIdx] = reviewCard; | ||
store.commit(MutationTypes.SET_LEARNSETS, learnsets.value); | ||
submitted.value = true; | ||
}; | ||
const prop = defineProps({ | ||
card: { | ||
type: Object as PropType<Card>, | ||
required: true, | ||
}, | ||
review: { | ||
type: Boolean, | ||
required: true, | ||
}, | ||
}); | ||
const md = useMarkdownIt(); | ||
const backHtml = md.renderer.render(prop.card.back, md.options, {}); | ||
const { name } = useDisplay(); | ||
const size = computed(() => { | ||
switch (name.value) { | ||
case 'xs': | ||
case 'sm': | ||
return 'x-small'; | ||
default: | ||
return 'default'; | ||
} | ||
}); | ||
</script> | ||
|
||
<style scoped> | ||
.btn-wrapper { | ||
row-gap: 8px; | ||
column-count: 3; | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import LearnsetCard from '@/components/LearnsetCard.vue'; | ||
import type { Card } from '@/types/interfaces'; | ||
import { | ||
findAllByTestId, | ||
fireEvent, | ||
render, | ||
waitFor, | ||
} from '@testing-library/vue'; | ||
import vuetify from '@/utils/setupVuetify'; | ||
import markdownit from 'markdown-it'; | ||
import { markdownItPlugin } from '@/plugins/markdownit'; | ||
import { createRouter, createWebHistory } from 'vue-router'; | ||
|
||
const router = createRouter({ | ||
history: createWebHistory(), | ||
routes: [], | ||
}); | ||
|
||
describe('LearnsetCard Component', () => { | ||
const md = markdownit({}); | ||
const FRONT_TOKEN = md.parse(`## 안녕하세요\n\n`, {}); | ||
const BACK_TOKEN = md.parse(`- 반갑습니다\n- 하하하하\n\n\n`, {}); | ||
|
||
const CARD_DATA: Card = { | ||
id: '1', | ||
front: FRONT_TOKEN, | ||
back: BACK_TOKEN, | ||
title: '안녕하세요', | ||
efactor: 2.5, | ||
dueDate: '', | ||
repetition: 0, | ||
interval: 0, | ||
reviewDate: '', | ||
}; | ||
|
||
it('카드의 질문이 렌더링 된다.', () => { | ||
const { getByText } = render(LearnsetCard, { | ||
global: { | ||
plugins: [router, vuetify, markdownItPlugin], | ||
}, | ||
props: { | ||
card: CARD_DATA, | ||
}, | ||
}); | ||
|
||
expect(getByText('안녕하세요')).toBeInTheDocument(); | ||
}); | ||
|
||
it('정답 확인 버튼을 클릭하면 카드의 정답과 점수를 매길 수 있는 버튼이 렌더링 된다.', async () => { | ||
const { getByText, getAllByRole, getByRole, getByTestId } = render( | ||
LearnsetCard, | ||
{ | ||
global: { | ||
plugins: [router, vuetify, markdownItPlugin], | ||
}, | ||
props: { | ||
card: CARD_DATA, | ||
review: true, | ||
}, | ||
data() { | ||
return { | ||
showBack: false, | ||
submitted: false, | ||
}; | ||
}, | ||
} | ||
); | ||
await fireEvent.click(getByTestId('show-answer-btn')); | ||
|
||
expect(getByText('반갑습니다')).toBeInTheDocument(); | ||
expect(getByText('하하하하')).toBeInTheDocument(); | ||
|
||
const scores = Array.from({ length: 6 }, (v, i) => i); | ||
const scoreButtons: HTMLButtonElement[] = getAllByRole('button', { | ||
name: /^[0-9]$/, | ||
}); | ||
scoreButtons.forEach(({ name }) => { | ||
expect(scores.includes(Number(name))).toBeTruthy(); | ||
}); | ||
}); | ||
|
||
it('점수를 매길 수 있는 버튼을 클릭하면 더 이상 버튼을 클릭할 수 없다.', async () => { | ||
const { queryByRole, getAllByRole, getByRole, getByTestId } = render( | ||
LearnsetCard, | ||
{ | ||
global: { | ||
plugins: [router, vuetify, markdownItPlugin], | ||
}, | ||
props: { | ||
card: CARD_DATA, | ||
review: true, | ||
}, | ||
data() { | ||
return { | ||
showBack: false, | ||
submitted: false, | ||
}; | ||
}, | ||
} | ||
); | ||
|
||
await fireEvent.click(getByTestId('show-answer-btn')); | ||
|
||
await fireEvent.click(getByRole('button', { name: '5' })); | ||
|
||
waitFor(() => { | ||
const scoreButtons: HTMLButtonElement[] = getAllByRole('button', { | ||
name: /^[0-9]$/, | ||
}); | ||
|
||
scoreButtons.forEach((button) => { | ||
expect(button).toBeDisabled(); | ||
}); | ||
}); | ||
}); | ||
|
||
it('리뷰가 필요하지 않는 항목은 점수를 매길 수 없다.', () => { | ||
const { queryByRole } = render(LearnsetCard, { | ||
global: { | ||
plugins: [router, vuetify, markdownItPlugin], | ||
}, | ||
props: { | ||
card: CARD_DATA, | ||
review: false, | ||
}, | ||
data() { | ||
return { | ||
showBack: false, | ||
submitted: false, | ||
}; | ||
}, | ||
}); | ||
|
||
expect(queryByRole('button', { name: '5' })).not.toBeInTheDocument(); | ||
}); | ||
}); |
Oops, something went wrong.