문제 페이지를 모두 작성하기는 했는데...
블로그를 작성하면서 동시에 만들어 보고 있어서 블로그에 차례 차례로 올리려다가 이것 저것 사소한 부분들을 하나 하나 고치다보니,
수정한 부분들이 너무 많아져서 그걸 블로그에 다시 재업로드 하기에 시간이 많이 들게 돼서.. 그냥 블로그는 잠시 올리지 않고 다 만들어 버렸습니다.
완성본
https://duljjii.github.io/
App.vue
<template>
<div id="app" class="container fw-bold">
<QuestionComponent
v-for="(question, index) in questions"
:key="index"
:question="question.question"
:answer="question.answer"
:index="index"
:total-count="questions.length"
v-show="index == showIndex"
:image="question.image"
@next-move="nextMove"
@prev-move="prevMove"
@save-answer="saveAnswer"
/>
<button
class="submit-button"
@click="submitAnswers"
v-show="showIndex != -1"
>
Submit
</button>
<ResultComponent
v-if="showIndex === -1"
:correct-answer-count="correctAnswerCount"
:total-count="questions.length"
:wrongAnswerList="getWrongtAnswerList"
:wrongQuestionList="getWrongQuestionList"
/>
</div>
</template>
<script>
import QuestionComponent from "@/components/QuestionComponent.vue";
import ResultComponent from "@/components/ResultComponent.vue";
import _ from 'lodash';
export default {
name: "App",
data() {
return {
questions: [
{
question:
"다음 중, Vue 인스턴스와 div를 연동하기 위해 빈칸에 넣어주어야 할 속성은?",
answer: {
a: "computed",
b: "methods",
c: "method",
d: "el",
},
image: require("@/assets/problem1.png"),
correctAnswer: "d",
},
{
question:
"html에 입력한 값이 달라지면 자동으로 Vue의 data 값도 바뀌도록 프로그램을 짜려고 할 때, input 태그 안에 넣어야 할 속성으로 알맞은 것은? ",
answer: {
a: "v-for",
b: "v-model",
c: "v-if",
d: "v-bind",
},
image: require("@/assets/problem2.png"),
correctAnswer: "b",
},
{
question:
"app 이라는 이름으로 만든 Vue 인스턴스 안에서 methods를 이용하여 message값을 바꾸려고 한다. 다음 중 methods를 잘못 정의한것은?",
answer: {
a: 'myFunction(){ this.message ="Hi"}',
b: 'myFunction : function(){ this.message = "hi"}',
c: 'myFunction : ()=>{ app.message = "hi" }',
d: 'myFunction : ()=>{ this.message = "hi"}',
},
image: require("@/assets/problem3.png"),
correctAnswer: "d",
},
{
question:
"다음 중, Vue 인스턴스의 watch 속성에서 오브젝트의 변화를 감지하기 위해 추가해야하는 속성은?",
answer: {
a: "handler",
b: "deep",
c: "set",
d: "on",
},
image: require("@/assets/problem4.png"),
correctAnswer: "b",
},
{
question:
"다음 중, Vue 인스턴스의 watch 속성에서 해당 data의 변화를 감지하면 실행되는 함수의 이름은?",
answer: {
a: "handler",
b: "deep",
c: "set",
d: "handle",
},
image: require("@/assets/problem5.png"),
correctAnswer: "a",
},
{
question:
"vue/cli 에서 style scoped를 주지 않고 스타일을 적용시켰을 때, 적용되는 범위로 알맞은것은?",
answer: {
a: "해당 컴포넌트에만 스타일이 적용된다.",
b: "해당 컴포넌트와 부모 컴포넌트에만 스타일 적용된다.",
c: "해당 컴포넌트와 자식 컴포넌트에만 스타일 적용된다.",
d: "모든 컴포넌트에 스타일이 적용된다.",
},
image: require("@/assets/problem6.png"),
correctAnswer: "d",
},
{
question: "style scoped의 설명으로 옳지 않은것은?",
answer: {
a: "전역 스타일과 겹치는 스타일이 있더라도 해당 스타일이 적용된다.",
b: "해당 컴포넌트에만 스타일이 적용된다 .",
c: "부모 태그에게도 해당 스타일이 적용된다.",
d: "스타일 태그의 적용 범위를 조절하기 위해 사용한다",
},
image: require("@/assets/problem7.png"),
correctAnswer: "c",
},
{
question: "다음 중 코드에 들어간 @는 어느 폴더를 가리키는가??",
answer: {
a: "dist",
b: "public",
c: "components",
d: "src",
},
image: require("@/assets/problem8.png"),
correctAnswer: "d",
},
{
question:
"특정 조건이 false일 때, display:none을 이용하여 화면 출력을 제어할 수 있는 속성은?",
answer: {
a: "v-if",
b: "v-else",
c: "v-bind",
d: "v-show",
},
image: require("@/assets/problem9.png"),
correctAnswer: "d",
},
{
question:
"특정 조건이 false일 때, 해당 요소를 HTML태그에서 제거하여 화면 출력을 제어할 수 있는 속성은?",
answer: {
a: "v-if",
b: "v-else",
c: "v-bind",
d: "v-show",
},
image: require("@/assets/problem10.png"),
correctAnswer: "a",
},
{
question:
"다음 코드와 같이 자식 컴포넌트에 props를 전달하고자 한다. html태그 안에 들어가야 할 속성의 case로 알맞은것은?",
answer: {
a: "camelCase",
b: "kebab-case",
c: "snake_case",
d: "PascalCase",
},
image: require("@/assets/problem11.png"),
correctAnswer: "b",
},
{
question:
"static prop을 이용하여 다음과 같이 값을 넘겨 주었을 때, 값의 type으로 알맞은것은?",
answer: {
a: "Object",
b: "Number",
c: "String",
d: "Array",
},
image: require("@/assets/problem12.png"),
correctAnswer: "c",
},
{
question:
"dynamic prop을 이용하여 다음과 같이 값을 넘겨 주었을 때, 자식 태그의 props 에서 받게되는 변수명의 이름은?",
answer: {
a: "passTotalCount",
b: "totalCount",
c: "total-count",
d: "pass_total_count",
},
image: require("@/assets/problem13.png"),
correctAnswer: "b",
},
{
question:
"dynamic prop으로 다음과 같이 값을 내려주었을 때, 해당 값의 타입으로 알맞은것은?",
answer: {
a: "Object",
b: "Number",
c: "String",
d: "Array",
},
image: require("@/assets/problem14.png"),
correctAnswer: "b",
},
{
question:
"버튼을 눌러 부모에게 받은 index값을 수정하려고 한다. 해당 메소드의 빈칸에 들어갈 값으로 알맞은것은?",
answer: {
a: "$store",
b: "$event",
c: "$on",
d: "$emit",
},
image: require("@/assets/problem15.png"),
correctAnswer: "d",
},
{
question:
"자식 객체에서 발생시킨 이벤트를 가져올 때, 컴포넌트의 html태그 안에서 @이후에 사용되는 case는?",
answer: {
a: "kebab-case",
b: "camelCase",
c: "PascalCase",
d: "snake_case",
},
image: require("@/assets/problem16.png"),
correctAnswer: "a",
},
{
question:
"다음 중 Vuex에서 Vue의 data와 비슷한 기능을 하는 속성으로, Vue 인스턴스의 상태를 정의하는 곳은?",
answer: {
a: "state",
b: "mutations",
c: "getters",
d: "actions",
},
image: require("@/assets/problem17.png"),
correctAnswer: "a",
},
{
question:
"다음 중 Vuex에서 state를 값을 변경하는 함수들을 정의하는 곳으로, 동기 함수들만 사용이 가능한 곳은?",
answer: {
a: "state",
b: "mutations",
c: "getters",
d: "actions",
},
image: require("@/assets/problem18.png"),
correctAnswer: "b",
},
{
question:
"다음 중 Vuex에서 state를 값을 변경하는 함수들을 정의하는 곳으로, 비동기 함수의 사용이 가능한 곳은?",
answer: {
a: "state",
b: "mutations",
c: "getters",
d: "actions",
},
image: require("@/assets/problem19.png"),
correctAnswer: "d",
},
{
question:
"다음 중 Vuex에서 state를 값을 변경하지 않고, state를 이용하여 계산된 식을 return해주는 함수를 정의하는 곳은?",
answer: {
a: "state",
b: "mutations",
c: "getters",
d: "actions",
},
image: require("@/assets/problem20.png"),
correctAnswer: "c",
},
{
question: "mutations의 첫번째 함수에 들어오는 값은?",
answer: {
a: "state",
b: "store",
c: "context",
d: "index",
},
image: require("@/assets/problem21.png"),
correctAnswer: "a",
},
{
question: "actions의 함수에 첫번째 인자로 들어오는 값은?",
answer: {
a: "state",
b: "store",
c: "context",
d: "index",
},
image: require("@/assets/problem22.png"),
correctAnswer: "c",
},
{
question:
"Vue 컴포넌트에서 Vue store에 있는 mutations를 사용하기 위해 사용해야하는 함수로 올바른것은?",
answer: {
a: "dispatch",
b: "emit",
c: "getters",
d: "commit",
},
image: require("@/assets/problem23.png"),
correctAnswer: "d",
},
{
question:
"Vue 컴포넌트에서 Vue store에 있는 actions를 사용하기 위해 사용해야하는 함수로 올바른것은?",
answer: {
a: "emit",
b: "dispatch",
c: "commit",
d: "store",
},
image: require("@/assets/problem24.png"),
correctAnswer: "b",
},
{
question:
"Vuex의 state 중 message를 Component에서 가져와 사용하려고 할 때, 올바르게 호출한 것은?",
answer: {
a: "this.store.state.message",
b: "this.store.$state.message",
c: "this.$store.$state.message",
d: "this.$store.state.message",
},
image: require("@/assets/problem25.png"),
correctAnswer: "d",
},
{
question:
"Vuex의 getters 중 messageLength라는 함수의 리턴값을 Component에서 가져와 사용하려고 할 때, 올바르게 호출한 것은?",
answer: {
a: "this.$store.getters.messageLength",
b: "this.$getters.messageLength()",
c: "this.$store.getters.messageLength()",
d: "this.$store.getters('messageLength')",
},
image: require("@/assets/problem26.png"),
correctAnswer: "a",
},
{
question:
"Vuex의 getters에서 해당 getters에 선언된 다른 함수를 사용하려고 한다. b 변수에 할당되는 값으로 알맞은것은?",
answer: {
a: "context",
b: "state",
c: "this",
d: "getters",
},
image: require("@/assets/problem27.png"),
correctAnswer: "d",
},
{
question: "해당 코드에서 a변수에 할당되는 값으로 알맞은것은?",
answer: {
a: "this",
b: "window",
c: "state",
d: "store",
},
image: require("@/assets/problem28.png"),
correctAnswer: "c",
},
{
question:
"dynamic prop을 이용하여 다음과 같이 값을 넘겨 주었을 때, 값의 type으로 알맞은것은?",
answer: {
a: "Object",
b: "Number",
c: "String",
d: "Array",
},
image: require("@/assets/problem29.png"),
correctAnswer: "a",
},
{
question:
"다음 중 vue/cli에서 data를 정의하는 방법으로 옳은 것은?",
answer: {
a: "data : { message:'duljji' }",
b: "data(){ {message : 'duljji'} }",
c: "data(){ return message : 'duljji'}",
d: "data(){ return { message : 'duljji'} }",
},
image: require("@/assets/problem30.png"),
correctAnswer: "d",
},
{
question:
"다음은 v-for를 활용하여 li를 반복하는 코드이다. a, b, 에 들어오는 값과 c에 넣어야 할 속성으로 알맞은것은?",
answer: {
a: "a : key(index), b:value, c : :key ",
b: "a : key(index), b:value, c : key",
c: "a : value, b : key(index), c : :key",
d: "a : value, b : key(index), c : key",
},
image: require("@/assets/problem31.png"),
correctAnswer: "c",
},
{
question:
"Life Cycle Hooks 중 Vue 인스턴스가 생성될 때 실행되며, HTML태그에 접근할 수 없는 Hook은?",
answer: {
a: "created",
b: "mounted",
c: "updated",
d: "destroyed",
},
image: require("@/assets/problem19.png"),
correctAnswer: "a",
},
{
question:
"Life Cycle Hooks 중 Vue 인스턴스가 HTML태그에 등록될 때, HTML태그에 접근이 가능한 Hook은?",
answer: {
a: "created",
b: "mounted",
c: "updated",
d: "destroyed",
},
image: require("@/assets/problem19.png"),
correctAnswer: "b",
},
{
question:
"Life Cycle Hooks 중 Vue 인스턴스에 변화가 발생할 때 실행되는 Hook은?",
answer: {
a: "created",
b: "mounted",
c: "updated",
d: "watch",
},
image: require("@/assets/problem19.png"),
correctAnswer: "c",
},
{
question:
"다음 코드의 설명으로 옳지 않은 것은?",
answer: {
a: "cute 클래스는 vue data와 관계없이 무조건 적용된다.",
b: "pretty 클래스는 isActive의 값이 true인지 false인지에 따라 적용 여부가 결정된다.",
c: "style color는 vue data의 color값이 true인지 false인지에 따라 적용 여부가 결정된다.",
d: "li 태그의 글자색은 빨간색이다",
},
image: require("@/assets/problem32.png"),
correctAnswer: "c",
},
],
showIndex: 0,
selectedAnswerList: [],
};
},
components: {
QuestionComponent,
ResultComponent,
},
methods: {
nextMove() {
this.showIndex =
this.showIndex + 1 > this.questions.length - 1
? this.questions.length - 1
: this.showIndex + 1;
},
prevMove() {
this.showIndex = this.showIndex - 1 < 0 ? 0 : this.showIndex - 1;
},
saveAnswer({ index, selectedAnswer }) {
this.selectedAnswerList[index] = selectedAnswer;
console.log(this.selectedAnswerList);
},
submitAnswers() {
this.showIndex = -1;
},
},
computed: {
correctAnswerCount() {
let correctAnswerCount = 0;
this.selectedAnswerList.forEach((selectedAnswer, idx) => {
console.log(selectedAnswer, this.questions[idx].correctAnswer);
if (selectedAnswer === this.questions[idx].correctAnswer) {
correctAnswerCount++;
}
});
return correctAnswerCount;
},
getWrongtAnswerList() {
const wrongAnswerList = this.selectedAnswerList.filter(
(selectedAnswer, idx) => {
return selectedAnswer != this.questions[idx].correctAnswer;
}
);
console.log(wrongAnswerList);
return wrongAnswerList;
},
getWrongQuestionList() {
const wrongQuestionList = this.questions.filter((question, idx) => {
return this.selectedAnswerList[idx] != question.correctAnswer;
});
console.log(wrongQuestionList);
return wrongQuestionList;
},
},
created() {
this.questions = _.shuffle(this.questions)
this.selectedAnswerList = new Array(this.questions.length).fill("");
},
};
</script>
<style scoped>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
margin-top: 60px;
}
.container {
background-color: #fce4ec;
border-radius: 10px;
box-shadow: 0 0 20px 0 rgba(255, 105, 180, 0.5);
padding: 40px;
}
.question-text {
font-size: 24px;
color: #333333;
margin-bottom: 20px;
}
.answer-label {
font-size: 20px;
color: #333333;
margin-bottom: 10px;
}
.answer-input {
margin-right: 10px;
}
.submit-button {
background-color: #ff69b4;
border: none;
border-radius: 50px;
color: white;
font-size: 18px;
padding: 10px 30px;
margin-top: 30px;
box-shadow: 0 0 20px 0 rgba(255, 105, 180, 0.5);
transition: all 0.3s ease-in-out;
}
.submit-button:hover {
background-color: #ff94c2;
box-shadow: 0 0 20px 0 rgba(255, 105, 180, 0.8);
cursor: pointer;
}
</style>
QuestionComponent.vue
<template>
<div>
<div class="text-start">
<h3 class="question-number">문제 {{ index + 1 }}</h3>
<p class="question-text">{{ question }}</p>
<div class="text-center">
<img :src="image" alt="" class="img-fluid" />
</div>
</div>
<div class="mt-3 container d-flex justify-content-center">
<div class="options">
<div
class="mt-3 answerBox d-flex flex-column justify-content-between align-items-start"
>
<div class="d-flex mt-3">
<p class="ms-2 mb-1">
<input
type="radio"
v-model="selectedAnswer"
:name="'answer' + index"
value="a"
:id="'a' + index"
/>
</p>
<div class="text-start me-2">
<label class="ps-4" :for="'a' + index">{{ answer.a }}</label>
</div>
</div>
<div class="d-flex">
<p class="ms-2 mb-1">
<input
type="radio"
v-model="selectedAnswer"
:name="'answer' + index"
value="b"
:id="'b' + index"
/>
</p>
<div class="text-start me-2">
<label class="ps-4" :for="'b' + index">{{ answer.b }}</label>
</div>
</div>
<div class="d-flex">
<p class="ms-2 mb-1">
<input
type="radio"
v-model="selectedAnswer"
:name="'answer' + index"
value="c"
:id="'c' + index"
/>
<div class="text-start me-2">
<label class="ps-4" :for="'c' + index">{{ answer.c }}</label>
</div>
</div>
<div class="d-flex mb-3">
<p class="ms-2 mb-1">
<input
type="radio"
v-model="selectedAnswer"
:name="'answer' + index"
value="d"
:id="'d' + index"
/>
<div class="text-start me-2">
<label class="ps-4" :for="'d' + index">{{ answer.d }}</label>
</div>
</div>
</div>
<div class="d-flex justify-content-between mt-3">
<button
@click="prevMove"
class="btn next-btn"
:class="{ invisible: isFirst }"
>
이전 문제
</button>
<button
@click="nextMove"
class="btn prev-btn"
:class="{ invisible: isLast }"
>
다음 문제
</button>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
selectedAnswer: null,
};
},
computed: {
isFirst() {
return this.index === 0;
},
isLast() {
console.log(this.index, this.totalCount)
return this.index === this.totalCount - 1;
},
},
props: {
question: String,
answer: Object,
index: Number,
totalCount: Number,
image:String,
},
methods: {
nextMove() {
this.$emit("next-move");
},
prevMove() {
this.$emit("prev-move");
},
saveAnswer() {
this.$emit("save-answer", {
index: this.index,
selectedAnswer: this.selectedAnswer,
});
},
},
watch: {
selectedAnswer() {
this.saveAnswer();
},
},
};
</script>
<style>
.question-number {
color: #ff69b4;
font-size: 24px;
margin-bottom: 10px;
}
.question-text {
color: #4b0082;
font-size: 20px;
margin-bottom: 20px;
text-shadow: 2px 2px 3px #ffb6c1;
}
.options {
background-color: #fffacd;
border-radius: 10px;
box-shadow: 2px 2px 10px #d2691e;
padding: 20px;
}
.answerBox {
border: 1px solid black;
width: 300px;
height: 300px;
text-align: center;
font-size: 1rem;
}
@media screen and (min-width: 768px) {
.answerBox {
width: 800px;
height: 200px;
}
}
.btn {
background-color: #ff69b4;
border: none;
border-radius: 10px;
color: #fff;
font-weight: bold;
margin-top: 20px;
padding: 10px 20px;
text-shadow: 2px 2px 3px #ffb6c1;
transition: all 0.3s ease-in-out;
}
.btn:hover {
box-shadow: 2px 2px 5px #d2691e;
cursor: pointer;
transform: scale(1.1);
}
.invisible {
display: hidden;
}
</style>
Result.vue
<template>
<div>
<p>
총 {{ totalCount }}개의 문제 중 {{ correctAnswerCount }}개의 문제를 맞췄습니다
</p>
<div class="d-flex flex-column align-items-start" v-if="totalCount!=correctAnswerCount">
<h1 class="fw-bold">틀린문제</h1>
<div>
<ul>
<li v-for="(wrongAnswer, index) in wrongAnswerList" :key="index">
<p>
{{index+1}}. {{ wrongQuestionList[index].question }}
</p>
<p>틀린 답 : {{ wrongQuestionList[index].answer[wrongAnswer] }} 정답 :
{{
wrongQuestionList[index].answer[
wrongQuestionList[index].correctAnswer
]
}}</p>
</li>
</ul>
</div>
</div>
<div v-else>
<h1 class="fw-bold">정답을 모두 맞추셨습니다 축하합니다!!!</h1>
<img :src="oneHundredItem" alt="">
<p>100점을 맞춘 뒤 나오는 귀여운 샴푸를 가장 먼저 보내신 분께 스타벅스 기프티콘을 드리겠습니다</p>
</div>
</div>
</template>
<script scoped>
export default {
name: "MypjtResultComponent",
data() {
return {
oneHundredItem : require("@/assets/oneHundredItem.png")
};
},
props: {
correctAnswerCount: Number,
totalCount: Number,
wrongAnswerList: Array,
wrongQuestionList: Array,
},
mounted() {},
methods: {},
};
</script>
<style>
ul{
text-align: left;
}
li {
list-style: none;
}
</style>
일단 image의 경로를 자식 컴포넌트에서 바로 넣어주지 않고 각 Question들이 image 경로 속성을 가지고 있게끔 만들었습니다.
그렇게 해야 문제를 섞을 때도, 해당 image들이 잘 들어갈 수 있기 때문입니다.
문제들을 섞게 되면 인덱스들의 값도 모두 바뀌기 때문에 더이상 QuestionComponent에서 인덱스에 해당하는 파일을 불러오는 것으로는 이미지를 매칭시킬 수 없었기에 문제들에 이미지경로를 직접 넣어줌으로써, 해당 문제의 이미지 속성을 가져오는 것으로 문제와 이미지를 매칭시켰습니다.
v-if 속성은 해당 조건이 만족되지 않으면 html태그에 나오지 않기 때문에 이스터에그를 넣기 아주 좋은 방법이라는 생각이 들었습니다.
100점을 맞추면 귀여운 강아지 사진이 나오도록 설정을 해두었습니다.
역시 직접 사이트를 만드는 묘미는 이런 이스터에그를 내가 마음대로 넣고 빼고 할 수 있다는 점인 것 같습니다
문제 사이트를 직접 만들어 보면서 느낀점이 많은데,
일단 개발을 하면서 재밌고 좋았던 것은 내가 '어.. 이 기능 있었으면 좋겠는데?' 라고 생각하는 는 것을 바로바로 넣어볼 수 있다는 점이었습니다.
SSAFY에서 배운 django와 DB들을 활용하여 문제집들을 추가적으로 활용하고 싶은 생각도 들었는데, 그렇게 서버까지 이용해서는 github으로 배포가 어려울 수도 있겠다는 생각이 들어, 잠시 접어두었지만요.
얼른 웹 서버를 이용한 배포를 하는 법도 익혀서 좀 더 재미있는 기능들을 넣은 웹페이지를 만들어보고 싶어 졌습니다.
계정을 만들어서, 각 계정들마다 문제를 풀면 경험치를 준다든지, 모든 사용자들에게 적용되는 공통 변수를 만들어서 가장 빨리 모든 문제를 푼 사람에게는 기프티콘이 나온다든지 하는 기능들을 구현할 수 있을테니까요
local storage를 활용하여 문제를 풀 때마다 경험치를 늘리는 기능도 만들어보긴 했지만, 30문제밖에 만들지 않아 의미가 없다 싶어 다시 뺐습니다.
아쉬운점은 Router를 활용하여 문제를 카테고리별로 나누는 것도 좀 더 공부하기 편한 사이트가 되지 않았을까 하는 마음이 들은 것입니다. 사실 컴포넌트별 props를 주어 출력되는 값들이 달라지게 만들었으니 , 그저 경로만 걸어주면 되는 간단한 기능인데, 문제 수가 없다보니 각 라우터 별로 4~5개의 작은 문제들이 나오는 것보다는 한번에 모든 문제가 나오는게 낫겠다는 판단이었는데,
직접 문제들을 만들어내다보니, 문제를 만드는 시간이 페이지를 구현하는 시간보다 더 오래 걸려, 다음 번에는 더미데이터를 활용하거나 API를 활용해서라도 좀 더 UX가 괜찮은 사이트를 만들어 보고 싶어졌습니다.
UI는... 좀 더 공부하도록 하겠습니다.
처음엔 모바일 화면에서 레이아웃이 모두 깨져, 어떻게 해야하나 고민이 있었는데, flexbox와 @media속성만 있으면 이제 반응형 웹페이지는 전혀 무섭지 않아졌습니다
아마 제가 혼자 공부하기 위해 만들어보자!! 라는 생각이었다면 이렇게 시간을 투자해서 페이지를 만들지 못했을텐데, 싸피 친구들이 시험을 잘 봤으면 좋겠다는 마음 덕에 의욕이 생겼던 것 같습니다.
그럼 다들 시험 잘 보기를 바라면서...!!
'프론트엔드 > Vue' 카테고리의 다른 글
Vue로 문제 풀이 페이지 만들어보기 4 (0) | 2023.05.05 |
---|---|
Vue로 문제 풀이 페이지 만들어보기 3 (0) | 2023.05.05 |
Vue로 문제 풀이 페이지 만들어보기 2 (1) | 2023.05.05 |
Vue로 문제 풀이 페이지 만들어보기 1 (0) | 2023.05.05 |