해당 토이 프로젝트는 아래와 같은 기술을 사용할 것이다.

- Reactjs, Typescript, Tailwindcss
- Opentdb API(퀴즈 API)
- Google Cloud Translation API

먼저 프로젝트를 구성해보자.

 

1. 아래 명령어를 입력한다.

 

1
2
3
4
# 리액트 앱 설치
$ npx create-react-app react-ts-quiz-app --template typescript
$ cd react-ts-quiz-app
$ npm start
cs

 

 

2. 서버가 정상적으로 뜨는지 확인한다.

 

 

3. tailwind css를 적용해보자.

 

https://tailwindcss.com/docs/guides/create-react-app

 

Install Tailwind CSS with Create React App - Tailwind CSS

Setting up Tailwind CSS in a Create React App project.

tailwindcss.com

위 설명대로 따라하면 된다.

 

 

1
2
3
# tailwindcss 설치
$ npm install -D tailwindcss
$ npx tailwindcss init
cs

 

제대로 설치 되었다면 아래와 같이 css가 적용된 화면이 나타날것이다.

 

 

4. 퀴즈 API 데이터를 가져오자.

 

퀴즈 데이터는 오픈 API인 Open Trivia DB 에서 가져올 것이다.

 

https://opentdb.com/

 

Open Trivia DB

Free to use, user-contributed trivia questions!

opentdb.com

 

퀴즈 카테고리, 타입, 난이도, 정답, 오답 등의 json 데이터를 확인가능하다.

 

https://opentdb.com/api.php?amount=10 

 

그런데 문제가 생겼다. json 데이터가 모두 영어 데이터이다.

영어 json 데이터 를 한글로 번역하면 좋을 것 같다.

구글 번역 API를 사용할 것이다.

 

 

5. 구글 클라우드 콘솔에 접속한다.

 

 

프로젝트를 하나 생성 후 Cloud Translation API 를 사용설정한다.

사용자 인증 정보 > 사용자 인증 정보 만들기 > API 키 생성한다.

 

 

API Key 값을 복사해서 갖고 있는다.

API 키 수정을 통해 호출 제한 사항을 적절하게 설정한다.

 

 

 

 

6. 구글 번역 API를 사용해 데이터를 번역해서 가져오자.

 

1
2
# tailwindcss 설치
$ npm i @google-cloud/translate
cs

 

 

6.1 먼저 퀴즈 객체 생성 후 데이터를 가져오자.

 

export interface Quiz {
    category: string;
    type: string;
    difficulty: string;
    question: string;
    correct_answer: string;
    incorrect_answers: string[];
    all_answers: string[];
}
  const getQuizData = async() => {
    let problemCnt = '1'
    let url = `https://opentdb.com/api.php?amount=${problemCnt}&difficulty=easy&type=multiple`
    let response = await fetch(url, {
      method: 'GET',
    }); 
    let data = await response.json()
    return data;
  }

 

 

6.2 가져온 퀴즈 데이터를 인자로 받아 구글 번역 API를 불러 번역시킨다.

 

  const translate = async (quizzes: Quiz[]) => {
    let url = `https://translation.googleapis.com/language/translate/v2?key=${apiKey}`

    multiplePreTranslate = '';
    let preTranslate = '';

    for(let quizObj of quizzes){
      let icaListStr = '';
      for (let ica of quizObj.incorrect_answers) {
        if(ica === quizObj.incorrect_answers[quizObj.incorrect_answers.length - 1]) {
          icaListStr += `${ica}`;
        }else{
          icaListStr += `${ica}\n`;
        }
      }
      if(quizObj === quizzes[quizzes.length - 1]) {
        preTranslate += `${quizObj.category}\n${quizObj.difficulty}\n${quizObj.question}\n${quizObj.correct_answer}\n${icaListStr}`;      
      }else{
        preTranslate += `${quizObj.category}\n${quizObj.difficulty}\n${quizObj.question}\n${quizObj.correct_answer}\n${icaListStr}\n\n`;      
      }
    }

    multiplePreTranslate = dataStrReplace(preTranslate);

    let response = await fetch(url, {
      method: 'POST',
      headers: { 
        'Content-Type': 'application/json',
       },
       body: JSON.stringify({
         target: 'ko',
         format: "text",
         q: multiplePreTranslate
       }),
    }); 
    let data = await response.json()
    return data;
  }

 

원활한 번역을 위해 문자열과 구분은 한줄내림(\n) 으로 구분해준다.

 

퀴즈데이터를 받아와 구글 번역 API를 통해 아래와 같이 데이터가 적절하게 번역된것을 확인할수 있다.

 

// 번역 전 퀴즈 데이터
`
Entertainment: Musicals & Theatres
medium
When was the play "Macbeth" written?
1606
1605
1723
1628

Entertainment: Musicals & Theatres
easy
Which Shakespeare play inspired the musical 'West Side Story'?
Romeo  Juliet
Hamlet
Macbeth
Othello
`;

// 번역 후 퀴즈 데이터
`
엔터테인먼트: 뮤지컬 및 극장
중간
연극 "맥베스"는 언제 쓰여졌습니까?
1606년
1605년
1723년
1628년

엔터테인먼트: 뮤지컬 및 극장
쉬움
어떤 셰익스피어 연극이 뮤지컬 '웨스트 사이드 스토리'에 영감을 주었습니까?
로미오  줄리엣
작은 촌락
맥베스
오셀로
`;

 

7. 화면을 꾸며보자. 

 

App.tsx

import React, { useLayoutEffect, useState } from 'react';
import './App.css'; 
import { Quiz } from './quiz.interface';

function App() {
  const [translateQuizzArr, setTranslateQuizzArr] = useState<Quiz[]>([]);
  const [currentQuizIndex, setCurrentQuizIndex] = useState<number>(0);
  const [score, setScore] = useState<number>(0);
  const [selectedAnswer, setSelectedAnswer] = useState<string | null>(null);
  const [showAnswer, setShowAnswer] = useState<boolean>(false);
  const [hasIncorrectAttempt, setHasIncorrectAttempt] = useState(false);
  const [isCorrect, setIsCorrect] = useState(false);
  const [quizTrigger, setQuizTrigger] = useState(false);
  const [isLoading, setIsLoading] = useState(true);

  const { REACT_APP_GOOGLE_TRANSLATE_API_KEY } = process.env;
  const apiKey: string | undefined = REACT_APP_GOOGLE_TRANSLATE_API_KEY;
  if (!apiKey) {
    throw new Error('Missing Google API Key');
  }
  let multiplePreTranslate = '';

  useLayoutEffect(() => {
    getQuizData().then(response => {

      console.log(response.data);
      let quizzes: Quiz[] = response.results;
      let quizArr: Quiz[] = [];
  
      const translatePromises = translate(quizzes)
        .then(response => {
          let translatedData = response?.data?.translations[0]?.translatedText;
          let multipleArrPreTranslate = translatedData.split('\n\n');
          let multipleEngPreTranslate = multiplePreTranslate.split('\n\n');
          for(let i=0; i < multipleArrPreTranslate.length; i++){
            let data = multipleArrPreTranslate[i];
            let engData = multipleEngPreTranslate[i];

            let arrPreTranslate = data.split('\n');
            let engArrPreTranslate = engData.split('\n');
            let engRightAnswerStr = dataStrReplace(engArrPreTranslate[3]);
            let engWrongAnswerStr1 = dataStrReplace(engArrPreTranslate[4]);
            let engWrongAnswerStr2 = dataStrReplace(engArrPreTranslate[5]);
            let engWrongAnswerStr3 = dataStrReplace(engArrPreTranslate[6]);

            let incorrectStrArr: string[] = [];
            let questionStr = `${dataStrReplace(arrPreTranslate[2])}`;
            let rightAnswerStr = `${dataStrReplace(arrPreTranslate[3])} ( ${engRightAnswerStr} )`; 
            let wrongAnswerStr1 = `${dataStrReplace(arrPreTranslate[4])} ( ${engWrongAnswerStr1} )`;
            let wrongAnswerStr2 = `${dataStrReplace(arrPreTranslate[5])} ( ${engWrongAnswerStr2} )`;
            let wrongAnswerStr3 = `${dataStrReplace(arrPreTranslate[6])} ( ${engWrongAnswerStr3} )`;

            incorrectStrArr.push(wrongAnswerStr1, wrongAnswerStr2, wrongAnswerStr3);
            let allAnswersStrArr: string[] = [];
            allAnswersStrArr.push(...incorrectStrArr, rightAnswerStr);
            shuffleArray(allAnswersStrArr);
  
            let translateQuizObj: Quiz = {
              category: arrPreTranslate[0],
              difficulty: arrPreTranslate[1], 
              question: questionStr,
              correct_answer: rightAnswerStr,
              incorrect_answers: incorrectStrArr,
              all_answers: allAnswersStrArr,
              type: ''
            };
            quizArr.push(translateQuizObj);
          }
        })
        .catch(reason => {
          console.log(reason.message);
          return {
            category: '',
            difficulty: '',
            question: '',
            correct_answer: '',
            incorrect_answers: [''],
            type: ''
          } as Quiz;
        });
  
      Promise.all([translatePromises]).then(() => {
        setTranslateQuizzArr(quizArr);
        setIsLoading(false);
      });
  
    });
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [quizTrigger]);
  
  const handleSolveMore = () => {
    let quizArr: Quiz[] = [];
    setTranslateQuizzArr(quizArr);
    setIsLoading(true);
    setQuizTrigger(!quizTrigger); 
    setScore(0);
    setCurrentQuizIndex(0);
  };
  
  const handleAnswer = (answer: string) => {
    setSelectedAnswer(answer);
    const isAnswerCorrect = answer === translateQuizzArr[currentQuizIndex].correct_answer;
  
    if (isAnswerCorrect) {
      setIsCorrect(true);
      if (!hasIncorrectAttempt) {
        setScore(score + 1);
      }
      setShowAnswer(true);
  
      setTimeout(() => {
        setIsCorrect(false);
        setShowAnswer(false);
        setHasIncorrectAttempt(false);
        setCurrentQuizIndex(currentQuizIndex + 1);
      }, 800);  
    } else {
      setShowAnswer(true);
      setHasIncorrectAttempt(true);
    }
  };
  


  function dataStrReplace(arrPreTranslate: any) {
    return arrPreTranslate.replace(/&quot;/g, '"')
                     .replace(/&#039;/g, "'")
                     .replace(/&ldquo;/g, '"')
                     .replace(/&rdquo;/g, '"');
  }

  function shuffleArray(array: any[]) {
    for (let i = array.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [array[i], array[j]] = [array[j], array[i]];
    }
  }
  
  async function getQuizData() {
    let problemCnt = '3'
    let url = `https://opentdb.com/api.php?amount=${problemCnt}&difficulty=easy&type=multiple`
    let response = await fetch(url, {
      method: 'GET',
    }); 
    let data = await response.json()
    return data;
  }

  async function translate(quizzes: Quiz[]) {
    let url = `https://translation.googleapis.com/language/translate/v2?key=${apiKey}`

    multiplePreTranslate = '';
    let preTranslate = '';

    for(let quizObj of quizzes){
      let icaListStr = '';
      for (let ica of quizObj.incorrect_answers) {
        if(ica === quizObj.incorrect_answers[quizObj.incorrect_answers.length - 1]) {
          icaListStr += `${ica}`;
        }else{
          icaListStr += `${ica}\n`;
        }
      }
      if(quizObj === quizzes[quizzes.length - 1]) {
        preTranslate += `${quizObj.category}\n${quizObj.difficulty}\n${quizObj.question}\n${quizObj.correct_answer}\n${icaListStr}`;      
      }else{
        preTranslate += `${quizObj.category}\n${quizObj.difficulty}\n${quizObj.question}\n${quizObj.correct_answer}\n${icaListStr}\n\n`;      
      }
    }

    multiplePreTranslate = dataStrReplace(preTranslate);

    let response = await fetch(url, {
      method: 'POST',
      headers: { 
        'Content-Type': 'application/json',
       },
       body: JSON.stringify({
         target: 'ko',
         format: "text",
         q: multiplePreTranslate
       }),
    }); 
    let data = await response.json()
    return data;
  }

  return (
    <div className="App bg-blue-50 min-h-screen flex flex-col items-center justify-center font-nanum-gothic font-bold">
      <h1 className="text-4xl mb-4 font-bold">랜덤퀴즈 앱</h1>
      {
        isLoading ? 
        <div>Loading...</div> : 
        (
          <h2 className="text-2xl mb-8">점수 : {score}</h2>
        )
      }
      {translateQuizzArr.length > 0 && currentQuizIndex <= translateQuizzArr.length - 1 ? (
        <div className="w-full bg-white p-8 rounded shadow flex flex-col">
          <h2 className="text-xl mb-4">{translateQuizzArr[currentQuizIndex].question}</h2>

          {translateQuizzArr[currentQuizIndex].all_answers.map((answer, index) => (

          <button
            key={index}
            onClick={() => handleAnswer(answer)}
            className={`my-2 p-4 text-white 
              ${showAnswer && answer === translateQuizzArr[currentQuizIndex].correct_answer 
                ? 'bg-green-500 text-white animate-pulse'  
                : showAnswer && answer === selectedAnswer
                ? 'bg-red-500 text-white'
                : 'bg-sky-500/100 text-white'}`}
          >
            {answer}
          </button>

          ))}

          {isCorrect && (
            <div className="bg-green-500 text-white p-4 mt-4 rounded">
              정답입니다. 다음 문제로 이동 중...
            </div>
          )}
            
          {showAnswer && !isCorrect && (
            <div className="text-red-500 p-4 mt-4 rounded">
              정답은 : {translateQuizzArr[currentQuizIndex].correct_answer} 입니다.
            </div>
          )}
        </div>
      ) : isLoading ? <div>      
        <h2 className="text-2xl">랜덤 퀴즈 로딩 중입니다.</h2>
      </div> : 
      (
        <>
        <h2 className="text-2xl">퀴즈가 끝났습니다. <br/> 최종 점수는 {score} 점 입니다.</h2>
        <button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mt-4" onClick={handleSolveMore}>
          퀴즈 더 풀기
        </button>
      </>
      )
    }
    </div>
  );

}

export default App;

 

App.css

/* App.css */
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
@import url('https://fonts.googleapis.com/css2?family=Nanum+Gothic:wght@400;700;800;900&display=swap');

body {
  @apply font-sans m-0 p-0 bg-gray-200;
}

.App {
  @apply max-w-md mx-auto p-5 bg-white rounded-lg shadow-md;
}

h1 {
  @apply text-blue-600 text-center;
}

.input-container {
  @apply bg-blue-100 p-2 rounded mb-4;
  @apply w-full mx-2 box-border;
}

input {
  @apply w-full py-2 px-3 border-none outline-none text-lg rounded bg-blue-200 box-border;
}

.translated-text {
  @apply text-lg leading-loose;
}

@media screen and (max-width: 600px) {
  .App {
    @apply p-2;
  }
}

@media screen and (max-width: 400px) {
  .App {
    @apply p-1;
  }
}

 

 

7.1 결과 화면

 

- 퀴즈는 한 사이클당 30문제로 구성된다.

- 퀴즈를 푸는 사용자는 사지선다로 문제를 풀수 있다

- 최초 정답 선택 시 점수가 올라간다.

- 최초 틀린 문제를 선택 시 점수가 올라가지 않고 정답을 알려준다.

- 정답을 선택 시 다음 문제로 진입할 수 있다.

- 퀴즈 주제 바꾸기로 퀴즈 카테고리를 바꿀수 있다.

 

 

8. 문제가 생겼다. 구글 클라우드 번역 API는 공짜가 아니다.

 

하루에 요청가능한 글자수와 건수가 정해져있다.  ( 프로젝트가 결제설정 되어있다면 사용한 만큼 지불된다)

 

 

https://cloud.google.com/translate/pricing?hl=ko 

 

가격 책정  |  Cloud Translation  |  Google Cloud

Cloud Translation 가격 책정 검토

cloud.google.com

 

 

https://cloud.google.com/billing/docs/how-to/modify-project?hl=ko#how-to-disable-billing 

 

프로젝트의 결제 사용 설정, 사용 중지, 변경  |  Cloud Billing  |  Google Cloud

의견 보내기 프로젝트의 결제 사용 설정, 사용 중지, 변경 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 이 페이지에서는 각 Google Cloud 프로젝트와 Google Maps

cloud.google.com

 

 

사용량을 초과하면 아래와 같이 에러가 발생될 것이다.

 

User Rate Limit Exeeded !!!! (허허 ... 흙발자 무시하는겁니가... ㅠㅠ)

 

퀴즈 데이터는 카테고리가 약  24개이고 40문제씩 호출할 수 있었다.

그럼 총 문제수만 8~900 개 일 것이다.

 

퀴즈 오픈 API도 분명 데이터가 추가되거나 업데이트 될텐데...

 

1. 매번 앱 실행 시 구글 번역 API를 실행해야한다?

-> 번역 API 트래픽 다량 발생가능성 높음. 얼마나 번역할것인가?

새로운 퀴즈 데이터에 대한 업데이트나 추가 갱신이 빠름  

 

2. 처음 불러올때 데이터를 조금만 불러온다 ?

-> 퀴즈데이터 번역하는 시간 또한 고려해야하니 유저입장에서 다음화면으로 가기전까지 피곤할것이다.

번역 API 트래픽은 줄었지만 발생한다.

 

3. 대량의 데이터를 한번에 번역해서 파일형태의 static으로 갖고있는다? 

-> 퀴즈 데이터 최신화 업데이트에 대한 문제 

 

일단 비용이 발생하면 부담이 발생될것 같아 3번으로 고치겠다.

 

 

8.1 퀴즈 데이터 static 파일 만들기

 

quizData.tsx

export const ENG_QUIZ_CATE9 = 
`
General Knowledge
easy
What is the official language of Brazil?
Portugese
Brazilian
Spanish
English
`;

export const KOR_QUIZ_CATE9 = 
`
일반 지식
쉬움
브라질의 공식 언어는 무엇입니까?
포르투갈어
브라질
스페인의
영어
`;

 

영어 원문 데이터, 한글 번역된 데이터 2개를 갖고 static 데이터를 불러와 활용하고 있다.

 

App.tsx

  const getStaticQuizData = () => {
    let quizArr: Quiz[] = [];
    let quizCategory = [
        {"eng": ENG_QUIZ_CATE9, "kor" : KOR_QUIZ_CATE9}
      , {"eng": ENG_QUIZ_CATE10, "kor": KOR_QUIZ_CATE10}  
    ]

    shuffleArray(quizCategory);

    for(let data=0; data < quizCategory.length; data++){
      let quizCate = quizCategory[data];
      let multipleArrPreTranslate = quizCate.kor.split('\n\n');
      let multipleEngPreTranslate = quizCate.eng.split('\n\n');

      for(let i=0; i < multipleArrPreTranslate.length; i++){
        let data = multipleArrPreTranslate[i];
        let engData = multipleEngPreTranslate[i];
  
        let arrPreTranslate = data.split('\n');
        let engArrPreTranslate = engData.split('\n');
        let engRightAnswerStr = dataStrReplace(engArrPreTranslate[3]);
        let engWrongAnswerStr1 = dataStrReplace(engArrPreTranslate[4]);
        let engWrongAnswerStr2 = dataStrReplace(engArrPreTranslate[5]);
        let engWrongAnswerStr3 = dataStrReplace(engArrPreTranslate[6]);
  
        let incorrectStrArr: string[] = [];
        let questionStr = `${dataStrReplace(arrPreTranslate[2])}`;
        let rightAnswerStr = `${dataStrReplace(arrPreTranslate[3])} ( ${engRightAnswerStr} )`; 
        let wrongAnswerStr1 = `${dataStrReplace(arrPreTranslate[4])} ( ${engWrongAnswerStr1} )`;
        let wrongAnswerStr2 = `${dataStrReplace(arrPreTranslate[5])} ( ${engWrongAnswerStr2} )`;
        let wrongAnswerStr3 = `${dataStrReplace(arrPreTranslate[6])} ( ${engWrongAnswerStr3} )`;
  
        incorrectStrArr.push(wrongAnswerStr1, wrongAnswerStr2, wrongAnswerStr3);
        let allAnswersStrArr: string[] = [];
        allAnswersStrArr.push(...incorrectStrArr, rightAnswerStr);
        shuffleArray(allAnswersStrArr);
  
        let translateQuizObj: Quiz = {
          category: arrPreTranslate[0],
          difficulty: arrPreTranslate[1], 
          question: questionStr,
          correct_answer: rightAnswerStr,
          incorrect_answers: incorrectStrArr,
          all_answers: allAnswersStrArr,
          type: ''
        };
        quizArr.push(translateQuizObj);
      }
    }
    setTranslateQuizzArr(quizArr);
    setIsLoading(false);
  }

 

 

결국 API를 활용하지 않았으니 속도는 빨라졌다.

최신 데이터에 대한 업데이트는 주기적으로 API를 호출하여

기존 데이터와 비교하여 변경사항에 대한 정보를 받아올지 생각해봐야한다.

 

마지막으로 배포된 url과 github 주소이다.

 

https://shlee0882.github.io/react-ts-quiz-app/

 

랜덤퀴즈 앱

 

shlee0882.github.io

 

https://github.com/shlee0882/react-ts-quiz-app

 

GitHub - shlee0882/react-ts-quiz-app: :question: ReactJs, Ts, Tailwindcss 랜덤 퀴즈 앱 토이 프로젝트

:question: ReactJs, Ts, Tailwindcss 랜덤 퀴즈 앱 토이 프로젝트 - GitHub - shlee0882/react-ts-quiz-app: :question: ReactJs, Ts, Tailwindcss 랜덤 퀴즈 앱 토이 프로젝트

github.com

 

react-ts-quiz-app의

dev 브랜치에 퀴즈 데이터가 static으로 변경된 소스가 올라가있다.

master 브랜치에는 구글 번역 api가 적용된 소스가 올라가있다.

 

https://github1s.com/shlee0882/react-ts-quiz-app

 

GitHub1s

 

github1s.com

 

이상으로 포스팅을 마치겠다.

 

 

ReactJs, TypeScript, Vite를 사용해휴가(연차)를 효율적으로 사용할 수 있게 
추천해주는 [휴가 추천 토이 프로젝트]를 만들어보았다.

 

이번 프로젝트는 TypeScript와 React를 학습하는 것에 포커스를 두었다.

 

TypeScript는 (변수 : 타입 ) 형태로 구성되어 있다.

매개변수 Object의 타입을 알고있다면 Object안에 속한 속성을 빠르게 찾아 접근가능하며

타입을 제한하여 메소드의 진입을 막을수 있다.

 

문제는 API를 만드는 백엔드가 주도권을 갖고 있으므로

프론트는 받아서 사용하는 주체이므로 API가 구조적으로 변경되거나 

타입을 모를때 지정하고 싶지 않을때가 발생된다.

이때는 any로 받아버리면 된다.

 

아래는 DateType이라는 Object를 정의한것이다.

Java에서는 String year; String Month; ... 로 정의가 될것이다.

 

react-calendar (https://www.npmjs.com/package/react-calendar) 모듈을 사용했으며

오픈소스 이므로 가이드를 보며 필요한 props를 주입시켜

커스터마이징 한다.

 

export type DateType = {
  year: string,
  month: string,
  holidayArray: Array<RtnArrType>
  holidayRecArray: any,
  selectHoliday: string
}

const CalendarComponents = (dateType: DateType) => {
  const [value, onChange] = useState(new Date());
  return (
    <Calendar onChange={onChange} 
    className={["box"]}
    view="month"
    onClickMonth={(value, event) => (
      event.preventDefault(),
      event.stopPropagation()
    )}
    onClickDay={(value, event) => (
      event.preventDefault(),
      event.stopPropagation()
    )}
    defaultValue={[new Date(Number(dateType.year), Number(dateType.month))]}
    tileContent={({ activeStartDate, date, view }) => view === 'month'? <TitleContent activeStartDate={activeStartDate} date={date} holidayArray={dateType.holidayArray}/> : null}
    tileClassName={({activeStartDate, date, view }) => changeColorDate(activeStartDate, date, view, dateType)}
    calendarType="US" 
    formatDay={(locale, date) => moment(date).format("D")} />
  )
}

 

useState를 사용하여 상태관리가 가능하게 할수 있다.

{{ }} expression을 사용해 데이터 바인딩을 해준다.

 

fetchData를 통해 공공 휴일데이터 정보를 가져와 화면을 렌더링한다.

 

const App = () => {
  const [selectHoliday, setSelectHoliday] = useState("2");
  const [selectedOption, setSelectedOption] = useState(year);
  const [holidayArray, setHolidayArray] = useState<Array<RtnArrType>>([]);
  const [holidayRecArray, setHolidayRecArray] = useState([]);
  const [data, setData] = useState(null);
  const [error, setError] = useState(false);
  const [loading, setLoading] = useState(false);

  const fetchData = async () => {
    setError(false);
    setLoading(true);
    try {
      let reqUri = rtnReqUriAppend(selectedOption)
      const response = await axios.get(reqUri);
      let rtnArray = response?.data?.response?.body?.items?.item
      let holidayArray = rtnArray.filter((arr:RtnArrType) => arr.isHoliday == "Y");
      setHolidayArray(holidayArray)
      console.log(holidayArray)
      const arrDayArr:any = recHolidayCalc(holidayArray, selectHoliday)
      setHolidayRecArray(arrDayArr)
    } catch (err) {
      setError(true);
    }
    setLoading(false);
  };

  useEffect(() => {

  }, []);

  return (
    <div className="App">
        <p aria-hidden="true" style={{textAlign: "center"}}>
          <Placeholder.Button xs={6} aria-hidden="true">
            직장인 연차 추천 애플리케이션 - 연차 효율적으로 쓰고 여행 가기!<br/>
            연차 사용 추천일이 파란색으로 선택됩니다.
          </Placeholder.Button>
        </p>
        &nbsp;&nbsp;연도: <Form.Select aria-label="Default select example2" onChange={e => {
        setSelectedOption(e.target.value);}}
        value={selectedOption} className='selectBox1'>
        <option>휴가 갈 연도를 선택</option>
        {selectOptionData.map((item:number, index:number) =>(
          <option key={item} value={item}>{item}</option>
        ))}
      </Form.Select>
      사용가능 휴가일수: <Form.Select aria-label="Default select example1" onChange={e => {
        setSelectHoliday(e.target.value);}}
        value={selectHoliday} className='selectBox1'>
        <option>사용가능한 휴가 일수를 선택</option>
        {selectHolidayCnt.map((item:number, index:number) =>(
          <option key={item} value={item}>{item}</option>
        ))}
      </Form.Select>
      <Button variant="success" className='holidaySearch1' onClick={() => {
        fetchData();
      }
      }>휴가 검색!!</Button>
      <div>
        {loading ? (
          <div style={{ height: '100vh', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
            <BeatLoader size={70} color="green"/>
          </div>
        ) : 
          error? (
          <Alert variant="danger" onClose={() => setError(false)} dismissible>
            <Alert.Heading>Error</Alert.Heading>
            <p>휴일데이터를 갖고 오지 못했습니다.</p>
          </Alert>
          ) : (
          <>
            <div style={{ display: 'flex', flexWrap: 'nowrap' }}>
              {items1.map((item1: string, index: number) => (
                <CalendarComponents key={index} year={selectedOption} month={item1} holidayArray={holidayArray} holidayRecArray={holidayRecArray} selectHoliday={selectHoliday}></CalendarComponents>
              ))}
            </div>
            <div style={{ display: 'flex', flexWrap: 'nowrap' }}>
              {items2.map((item2: string, index: number) => (
                <CalendarComponents key={index} year={selectedOption} month={item2} holidayArray={holidayArray} holidayRecArray={holidayRecArray} selectHoliday={selectHoliday}></CalendarComponents>
              ))}
            </div>
            <div style={{ display: 'flex', flexWrap: 'nowrap' }}>
              {items3.map((item3: string, index: number) => (
                <CalendarComponents key={index} year={selectedOption} month={item3} holidayArray={holidayArray} holidayRecArray={holidayRecArray} selectHoliday={selectHoliday}></CalendarComponents>
              ))}
            </div>
          </>
        )}
      </div>
    </div>
  )
}

 

소스를 수정하며 발생되는 불필요한 기본 설정이나 스타일 등을 node_modules 안에 접근해서 적절하게 수정해준다.

오픈소스를 사용하며 발생되는 불가피한 영역인듯하다.

 

Vite 공식 가이드에 나온대로 배포 준비를 하고 (https://vitejs.dev/guide/static-deploy.html)

github 정적 페이지 배포를 통해 배포해준다.

 

배포된 Deploy URL

https://shlee0882.github.io/react-ts-calendar-app/

 

직장인 연차 추천 애플리케이션

 

shlee0882.github.io

 

repo

 

https://github.com/shlee0882/react-ts-calendar-app

 

GitHub - shlee0882/react-ts-calendar-app: TypeScript, ReactJs, Vite 직장인 휴가 추천 토이 프로젝트

TypeScript, ReactJs, Vite 직장인 휴가 추천 토이 프로젝트. Contribute to shlee0882/react-ts-calendar-app development by creating an account on GitHub.

github.com

 

package.json 

{
  "name": "react-ts-calendar-app",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@reduxjs/toolkit": "^1.9.2",
    "axios": "^1.3.2",
    "bootstrap": "^5.2.3",
    "react": "^18.2.0",
    "react-big-calendar": "^1.6.3",
    "react-bootstrap": "^2.7.0",
    "react-calendar": "^4.0.0",
    "react-dom": "^18.2.0",
    "react-redux": "^8.0.5",
    "react-spinners": "^0.13.8"
  },
  "devDependencies": {
    "@types/moment": "^2.13.0",
    "@types/react": "^18.0.26",
    "@types/react-big-calendar": "^0.38.4",
    "@types/react-calendar": "^3.9.0",
    "@types/react-dom": "^18.0.9",
    "@vitejs/plugin-react": "^3.0.0",
    "typescript": "^4.9.3",
    "vite": "^4.0.0"
  }
}

 

 

'전체 > 개인 프로젝트' 카테고리의 다른 글

Reactjs, Tailwindcss 토이 퀴즈 앱 만들기  (2) 2023.08.02

+ Recent posts