THE DEVLOG

scribbly.

Next.js+형태소분석 CS퀴즈앱 만들기

2023.03.02 16:22:36

pages/api/quiz.tsx 에서 게시글을 읽어들이는 로직을 작성하는데,
이미 데이터를 읽는 로직은 handler의 최상위에 있기에 아래의 부분만 작성하면 데이터를 반환하게 된다.

  } else if (req.method === "GET") {
    res.status(200).json({
      message: "퀴즈를 불러왔습니다.",
      result: {
        quizes: quizes,
        keywords: keywords,
      },
    });
  } else {
    res.status(405).json({ message: "메서드가 잘못 되었습니다." });
  }

이렇게 데이터를 반환했을 때,
json으로 작성한 부분이 'data'에 담겨서 반환된다.

즉 응답객체는 아래와 같다.

res : {
  status : 200,
  data : {
    message: "퀴즈를 불러왔습니다.",
    result: {
      quizes : Array<quiz>,
      keywords ; Array<string>,
    },
  },
}

버튼을 누르면 응답을 받아 콘솔을 찍는 코드로 콘솔을 찍어보면 아래와 같다.

import axios from "axios";
import React from "react";

const QuizPage = () => {
  const onClick = async (e) => {
    e.preventDefault();
    try {
      const res = await axios.get("/api/quiz");
      if (res.status === 200) {
        alert(res.data.message);
        console.log(res);
      }
    } catch (e) {
      alert(e.message);
    }
  };
  return (
    <div>
      <button type="button" onClick={onClick}>
        {" "}
        전송보내기{" "}
      </button>
    </div>
  );
};

export default QuizPage;

이제 useEffect로 퀴즈를 불러오고,
무작위로 한 문제를 선정하도록 컴포넌트를 수정한다.

import axios from "axios";
import React, { useEffect, useState } from "react";
import { Quiz } from "../api/quiz";

const QuizPage = () => {
  const [quizes, setQuizes] = useState([]);
  const [keywords, setKeywords] = useState([]);
  const [quizNumber, setQuizNumber] = useState(0);
  
  const fetchData = async () => {
    try {
      const res = await axios.get("/api/quiz");
      if (res.status === 200) {
        const quizes = res.data.result.quizes as Quiz[];
        const keywords = res.data.result.keywords as string[];
        return { quizes, keywords };
      }
    } catch (e) {
      alert(e.message);
    }
  };

  useEffect(() => {
    fetchData().then((res) => {
      const { quizes, keywords } = res;
      setQuizes(quizes);
      setKeywords(keywords);
      setQuizNumber(Math.floor(Math.random() * quizes.length));
    });
  }, []);

  return (
    <div>
      <div>{JSON.stringify(quizes[quizNumber])}</div>
      <div>{JSON.stringify(keywords)}</div>
    </div>
  );
};

export default QuizPage;

나머지 로직은 테스트의 용이성 및 로직 관리의 용이성을 위해 자식 컴포넌트를 만들어서 진행해야 할 것 같다.

문제풀이 컴포넌트

components/QuizContainer.tsx 이다.
퀴즈앱3 의 로직을 대부분 따라가지만,

'n개의 정답 키워드에 무작위 키워드를 추가해서 10개의 보기 키워드를 만드는 로직'과
'n개의 정답 키워드 각각이 정답 상태인지 아닌지(isCorrected)'
'isCorrected 상태를 바탕으로 현재 풀고 있는 문제가 몇 번 문제인지' 가 추가되었다.

(이때 퀴즈앱 3의 로직은 useEffect 안에 넣어 한 번만 실행 되도록 하며,
Quiz 프롭스가 로딩되지 않을 경우에 대비하여 빠른 반환 및 의존자 배열에 추가해준다.)

import React, { useEffect, useState } from "react";
import { Quiz } from "../pages/api/quiz";

const QuizContainer = ({
  quiz,
  keywords,
}: {
  quiz: Quiz;
  keywords: string[];
}) => {
  const [slicedTextArray, setSlicedTextArray] = useState([] as string[]);
  const [replacedTextArray, setReplacedTextArray] = useState([] as string[]);
  const [answersKeyword, setAnswersKeyword] = useState([] as string[]);
  const [allAnswersKeyword, setAllAnswersKeyword] = useState([] as string[]);
  const [isCorrect, setIsCorrect] = useState([] as boolean[]);

  useEffect(() => {
    const randomIndexes = [];
    if (!quiz) return;
    const { keywordArray, originalText, category, title } = quiz;
    while (randomIndexes.length < 5) {
      // 배열 인덱스 중 무작위 5개의 인덱스를 선택한다.
      const randomIndex = Math.floor(Math.random() * keywordArray.length);
      if (!randomIndexes.includes(randomIndex)) {
        randomIndexes.push(randomIndex);
      }
    }

    // 인덱스들을 오름차순으로 정렬한다.
    randomIndexes.sort((a, b) => a - b);
    // 해당하는 인덱스의 키워드들을 배열로 반환한다.
    const sortedKeyword = randomIndexes.map((index) => {
      return keywordArray[index];
    });
    console.log(JSON.stringify(sortedKeyword));

    let fromIndex = 0;
    const answersKeyword = [];
    const answersIndex = [];
    sortedKeyword.forEach((value) => {
      let target = value;
      let indexResult = originalText.indexOf(target, fromIndex);
      if (indexResult < 0) target = value.split(" ").join("");
      indexResult = originalText.indexOf(target, fromIndex);
      if (indexResult >= 0) {
        answersKeyword.push(target);
        answersIndex.push(indexResult);
        fromIndex = indexResult + 1;
      }
    });

    const slicedTextArray = [];
    const replacedTextArray = [];
    answersIndex.forEach((answerIndex, idx) => {
      //만약 slicedTextArray가 없다면, 최초 키워드의 인덱스 전까지의 모든 문자열을 배열에 삽입한다.
      if (!slicedTextArray.length) {
        slicedTextArray.push(originalText.slice(0, answerIndex));
      }
      //현재 키워드의 인덱스부터 다음 키워드의 인덱스까지를 slice한다. 만일 다음 키워드가 없다면 끝까지 슬라이스 된다.
      const slicedText = originalText.slice(answerIndex, answersIndex[idx + 1]);
      //슬라이스한 문자열에서 키워드를 찾아 키워드를 '{0번 문제}'로 바꾼다.
      const replacedText = slicedText.replace(
        answersKeyword[idx],
        `\{문제 ${idx + 1}번\}`
      );
      //바꾼 문자열을 배열에 추가한다.
      slicedTextArray.push(slicedText);
      replacedTextArray.push(replacedText);
    });

    //배열을 모두 합치면 원하는 문자열이 된다.
    setIsCorrect(Array(replacedTextArray.length).fill(false));
    setSlicedTextArray(slicedTextArray);
    setReplacedTextArray(replacedTextArray);
    setAnswersKeyword(answersKeyword);
    const allAnswersKeyword = [...answersKeyword];
    while (allAnswersKeyword.length < 10) {
      const idx = Math.floor(Math.random() * keywords.length);
      const keyword = keywords[idx];
      if (!allAnswersKeyword.includes(keyword)) allAnswersKeyword.push(keyword);
    }
    setAllAnswersKeyword(allAnswersKeyword.sort(() => Math.random() - 0.5));
  }, [quiz]);

  const [answerIndex, setAnswerIndex] = useState(0);
  useEffect(() => {
    if (!isCorrect.length) return;
    let allCorrected = true;
    for (let i = 0; i < 5; i++) {
      if (!isCorrect[i]) {
        setAnswerIndex(i);
        allCorrected = false;
        break;
      }
    }
    if (allCorrected) setTimeout(() => alert("모두 맞췄습니다!"), 50);
  }, [isCorrect]);

  const onClickKeyword = (e) => {
    if (e.target.value === answersKeyword[answerIndex]) {
      const newIsCorrect = [...isCorrect];
      newIsCorrect[answerIndex] = true;
      e.target.disabled = true;
      setIsCorrect(newIsCorrect);
    } else {
      alert("틀렸습니당");
    }
  };
  return (
    <div>
      <div style={{ whiteSpace: "pre" }}>
        {[
          slicedTextArray[0],
          ...isCorrect.map((boolean, idx) => {
            if (boolean) return slicedTextArray[idx + 1];
            else return replacedTextArray[idx];
          }),
        ].join("")}
      </div>
      <div>
        {allAnswersKeyword.map((keyword) => {
          return (
            <button type="button" value={keyword} onClick={onClickKeyword}>
              {keyword}
            </button>
          );
        })}
      </div>
    </div>
  );
};

export default QuizContainer;

결과물은 아래와 같다.
지금도 이미 문제풀이앱이 된 것 같다..(걍 배포할까?)