Skip to content

Commit

Permalink
feat: 학습을 위해 카드 뭉치의 카드들을 조회할 수 있다. (#12)
Browse files Browse the repository at this point in the history
* 🧪 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
padosum committed Feb 28, 2023
1 parent 475c555 commit 028a40f
Show file tree
Hide file tree
Showing 22 changed files with 1,422 additions and 278 deletions.
981 changes: 724 additions & 257 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions package.json
Expand Up @@ -14,7 +14,11 @@
"dependencies": {
"@vueuse/core": "^9.13.0",
"file-dialog": "^0.0.8",
"github-markdown-css": "^5.2.0",
"highlight.js": "^11.7.0",
"markdown-it": "^13.0.1",
"supermemo": "^2.0.17",
"swiper": "^9.0.5",
"uuidv4": "^6.2.13",
"vue": "^3.2.45",
"vue-router": "^4.1.6",
Expand All @@ -39,6 +43,7 @@
"jsdom": "^20.0.3",
"npm-run-all": "^4.1.5",
"prettier": "^2.7.1",
"sass": "^1.58.3",
"typescript": "~4.7.4",
"vite": "^4.0.0",
"vitest": "^0.25.6",
Expand Down
13 changes: 13 additions & 0 deletions src/assets/style.scss
@@ -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);
}
47 changes: 34 additions & 13 deletions src/components/AddLearnsetModal.vue
@@ -1,6 +1,11 @@
<template>
<v-btn class="d-none" @click="openModal = true" role="button"> open </v-btn>
<v-dialog v-model="openModal" width="70%" persistent v-if="openModal">
<BaseButton
text="open"
class="d-none"
role="button"
@click="openModal = true"
/>
<v-dialog v-model="openModal" persistent v-if="openModal" :class="modalSize">
<v-card>
<v-card-title>
<span class="text-h5">{{ title }}</span>
Expand All @@ -16,29 +21,31 @@
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="blue-darken-1"
<BaseButton
text="취소"
color="primary"
variant="text"
data-testid="close"
@click="openModal = false"
>
취소
</v-btn>
<v-btn
color="blue-darken-1"
/>
<BaseButton
text="추가"
color="primary"
variant="text"
data-testid="save"
@click="addLearnset"
>
추가
</v-btn>
/>
</v-card-actions>
</v-card>
</v-dialog>
</template>

<script setup lang="ts">
import { ref, watchEffect } from 'vue';
import BaseButton from './BaseButton.vue';
import { ref, computed, watchEffect } from 'vue';
import { useDisplay } from 'vuetify';
const emit = defineEmits(['addLearnset']);
const props = defineProps({
title: {
Expand All @@ -63,6 +70,20 @@ const addLearnset = () => {
emit('addLearnset', learnsetTitle.value);
};
const { name } = useDisplay();
const modalSize = computed(() => {
switch (name.value) {
case 'xs':
case 'sm':
return 'w-100';
case 'md':
return 'w-75';
default:
return 'w-25';
}
});
defineExpose({
toggleModal,
});
Expand Down
3 changes: 2 additions & 1 deletion src/components/BaseButton.vue
@@ -1,6 +1,7 @@
<template>
<v-btn variant="outlined" role="button">
<v-btn role="button" variant="outlined">
{{ text }}
<slot></slot>
</v-btn>
</template>

Expand Down
130 changes: 130 additions & 0 deletions src/components/LearnsetCard.vue
@@ -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>
136 changes: 136 additions & 0 deletions src/components/__tests__/LearnsetCard.spec.ts
@@ -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();
});
});

0 comments on commit 028a40f

Please sign in to comment.