diff --git a/src/apis/solutionList.js b/src/apis/solutionList.js new file mode 100644 index 0000000..8fc8011 --- /dev/null +++ b/src/apis/solutionList.js @@ -0,0 +1,7 @@ +const GET_SOLUTION_LIST_BASE_URL = `https://api.github.com/repos/codeisneverodd/programmers-coding-test/contents/`; + +export const requestSolutionListByLevelAPI = async (level) => { + const url = GET_SOLUTION_LIST_BASE_URL + `level-${level}`; + const response = await fetch(url); + return await response.json(); +}; diff --git a/src/components/QuestionInputAndList.js b/src/components/QuestionInputAndList.js new file mode 100644 index 0000000..917438a --- /dev/null +++ b/src/components/QuestionInputAndList.js @@ -0,0 +1,98 @@ +import styled from "styled-components"; +import { TextInput } from "../style/styledComponents"; +import { useEffect, useState } from "react"; +import useSolutionListValue from "../hooks/solutionList/useSolutionListValue"; +import { createFuzzyMatcher } from "./utils/createFuzzyMatcher"; + +const QuestionInputAndList = ({ onQuestionNameChange = () => {} }) => { + const [questionName, setQuestionName] = useState(""); + const [questionList, setQuestionList] = useState([]); + const [isQuestionListVisible, setIsQuestionListVisible] = useState(false); + const solutionList = useSolutionListValue(); + + useEffect(() => { + setQuestionList(solutionList); + }, []); + useEffect(() => { + onQuestionNameChange(questionName); + }, [questionName]); + + function handleQuestionNameInput(e) { + const inputValue = e.target.value; + setQuestionName(inputValue); + const findMatchedNameRegex = createFuzzyMatcher(inputValue); + setQuestionList( + solutionList.filter((solution) => + findMatchedNameRegex.test(solution.name) + ) + ); + if (!isQuestionListVisible) setIsQuestionListVisible(true); + } + function handleQuestionNameBlur(e) { + setIsQuestionListVisible(false); + } + function handleQuestionNameFocus(e) { + setIsQuestionListVisible(true); + } + function handleQuestionClick(e) { + setIsQuestionListVisible(false); + setQuestionName(e.target.dataset.value); + } + + return ( + <> + + {isQuestionListVisible && ( + + {questionList.map((value, index) => ( + + + {value.name} / Level {value.level} + + + ))} + + )} + + ); +}; +export default QuestionInputAndList; + +export const QuestionList = styled.ul` + //display: none; + position: absolute; + top: 20rem; + left: 0; + width: 100%; + height: 33.2rem; + background-color: ${(props) => props.theme.searchBg}; + overflow: scroll; + z-index: 10; +`; +export const QuestionItem = styled.li``; + +export const QuestionBtn = styled.div` + width: 100%; + height: 9rem; + text-align: left; + line-height: 9rem; + text-indent: 2rem; + background-color: transparent; + font-size: 3.1rem; + color: ${(props) => props.theme.basicWhite}; + border-bottom: 1px solid ${(props) => props.theme.notSelectedTab}; + cursor: pointer; + &:hover { + background-color: ${(props) => props.theme.programmersBlue}; + } +`; diff --git a/src/components/utils/createFuzzyMatcher.js b/src/components/utils/createFuzzyMatcher.js new file mode 100644 index 0000000..13a6f83 --- /dev/null +++ b/src/components/utils/createFuzzyMatcher.js @@ -0,0 +1,38 @@ +const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + +const ch2pattern = (ch) => { + const offset = 44032; + if (/[가-힣]/.test(ch)) { + const chCode = ch.charCodeAt(0) - offset; + if (chCode % 28 > 0) { + return ch; + } + const begin = Math.floor(chCode / 28) * 28 + offset; + const end = begin + 27; + return `[\\u${begin.toString(16)}-\\u${end.toString(16)}]`; + } + if (/[ㄱ-ㅎ]/.test(ch)) { + const con2syl = { + ㄱ: "가".charCodeAt(0), + ㄲ: "까".charCodeAt(0), + ㄴ: "나".charCodeAt(0), + ㄷ: "다".charCodeAt(0), + ㄸ: "따".charCodeAt(0), + ㄹ: "라".charCodeAt(0), + ㅁ: "마".charCodeAt(0), + ㅂ: "바".charCodeAt(0), + ㅃ: "빠".charCodeAt(0), + ㅅ: "사".charCodeAt(0), + }; + const begin = + con2syl[ch] || (ch.charCodeAt(0) - 12613) * 588 + con2syl["ㅅ"]; + const end = begin + 587; + return `[${ch}\\u${begin.toString(16)}-\\u${end.toString(16)}]`; + } + return escapeRegExp(ch); +}; + +export const createFuzzyMatcher = (input) => { + const pattern = input.split("").map(ch2pattern).join(".*?"); + return new RegExp(pattern); +}; diff --git a/src/hooks/solutionList/useFetchSolutionList.js b/src/hooks/solutionList/useFetchSolutionList.js new file mode 100644 index 0000000..a013f64 --- /dev/null +++ b/src/hooks/solutionList/useFetchSolutionList.js @@ -0,0 +1,35 @@ +import useSetSolutionList from "./useSetSolutionList"; +import { requestSolutionListByLevelAPI } from "../../apis/solutionList"; + +const useFetchSolutionList = () => { + const setSolutionList = useSetSolutionList(); + const POSSIBLE_LEVEL = [1, 2, 3, 4, 5]; + + const fetchSolutionList = async () => { + setSolutionList([]); + POSSIBLE_LEVEL.forEach((level) => { + requestSolutionListByLevelAPI(level).then((response) => { + const newSolutionList = response + .filter((solution) => solution.name !== "00-해답-예시.js") + .map((solution) => ({ + name: formattedFileName(solution.name), + level, + })); + + setSolutionList((solutionList) => [ + ...solutionList, + ...newSolutionList, + ]); + }); + }); + setSolutionList((solutionList) => + solutionList.sort((a, b) => a.level - b.level) + ); + }; + + return fetchSolutionList; +}; +export default useFetchSolutionList; +function formattedFileName(fileName) { + return fileName.replace(/-/g, " ").replace(".js", ""); +} diff --git a/src/hooks/solutionList/useSetSolutionList.js b/src/hooks/solutionList/useSetSolutionList.js new file mode 100644 index 0000000..8a0c233 --- /dev/null +++ b/src/hooks/solutionList/useSetSolutionList.js @@ -0,0 +1,7 @@ +import { useSetRecoilState } from "recoil"; +import solutionListState from "../../state/solutionList"; + +const useSetSolutionList = () => { + return useSetRecoilState(solutionListState); +}; +export default useSetSolutionList; diff --git a/src/hooks/solutionList/useSolutionListValue.js b/src/hooks/solutionList/useSolutionListValue.js new file mode 100644 index 0000000..f131f51 --- /dev/null +++ b/src/hooks/solutionList/useSolutionListValue.js @@ -0,0 +1,7 @@ +import { useRecoilValue } from "recoil"; +import solutionListState from "../../state/solutionList"; + +const useSolutionListValue = () => { + return useRecoilValue(solutionListState); +}; +export default useSolutionListValue; diff --git a/src/pages/errorReportPage/ErrorReport.js b/src/pages/errorReportPage/ErrorReport.js index 4fba4b6..e668ba1 100644 --- a/src/pages/errorReportPage/ErrorReport.js +++ b/src/pages/errorReportPage/ErrorReport.js @@ -2,18 +2,15 @@ import { useState } from "react"; import styled from "styled-components"; import Header from "../../components/Header"; import { - ThanksMsg, - OtherReportBtn, + InputLabel, MainContetnWrapper, + OtherReportBtn, StepByStepInputItem, - InputLabel, - TextInput, - QuestionList, - QuestionItem, - QuestionBtn, - TextArea, SubmitBtn, + TextArea, + ThanksMsg, } from "../../style/styledComponents"; +import QuestionInputAndList from "../../components/QuestionInputAndList"; export default function ErrorReport() { const [submitted, setSubmitted] = useState(false); @@ -51,7 +48,9 @@ export default function ErrorReport() { function handleSubmitBtnClick() { setSubmitted(true); } - + function handleQuestionNameChange(e) { + setQuestionName(e); + } return ( <>
@@ -110,38 +109,41 @@ export default function ErrorReport() { {isQuestionNameVisible && ( 문제 이름 - - - - 1번문제 - - - 2번문제 - - - 3번문제 - - - 4번문제 - - - 5번문제 - - - 6번문제 - - - 7번문제 - - - 8번문제 - - + {/**/} + {/**/} + {/* */} + {/* 1번문제*/} + {/* */} + {/* */} + {/* 2번문제*/} + {/* */} + {/* */} + {/* 3번문제*/} + {/* */} + {/* */} + {/* 4번문제*/} + {/* */} + {/* */} + {/* 5번문제*/} + {/* */} + {/* */} + {/* 6번문제*/} + {/* */} + {/* */} + {/* 7번문제*/} + {/* */} + {/* */} + {/* 8번문제*/} + {/* */} + {/**/} )} diff --git a/src/pages/solutionReportPage/SolutionReport.js b/src/pages/solutionReportPage/SolutionReport.js index ab6b177..a265644 100644 --- a/src/pages/solutionReportPage/SolutionReport.js +++ b/src/pages/solutionReportPage/SolutionReport.js @@ -8,7 +8,6 @@ import { StepByStepInputItem, SubmitBtn, TextArea, - TextInput, ThanksMsg, } from "../../style/styledComponents"; import gitHubLogoSrc from "../../images/github-logo-white.png"; @@ -16,14 +15,23 @@ import { useSearchParams } from "react-router-dom"; import { LOGIN_URL } from "./utils/gitHubLogin"; import useUserProfile from "../../hooks/user/useUserProfile"; import useUserLogin from "../../hooks/user/useUserLogin"; +import QuestionInputAndList from "../../components/QuestionInputAndList"; +import useFetchSolutionList from "../../hooks/solutionList/useFetchSolutionList"; +import useSetSolutionList from "../../hooks/solutionList/useSetSolutionList"; export default function SolutionReport() { const [submitted, setSubmitted] = useState(false); - const [questionName, setQuestionName] = useState(""); const [detailContent, setDetailContent] = useState(""); const [searchParams, setSearchParams] = useSearchParams(); const userInfo = useUserProfile(); const { isLoggedIn, requestLogin } = useUserLogin(); + const fetchSolutionList = useFetchSolutionList(); + const setSolutionList = useSetSolutionList(); + const [isFetched, setIsFetched] = useState(false); + useEffect(() => { + if (isFetched) fetchSolutionList(); + setIsFetched(true); + }, [isFetched, setIsFetched, fetchSolutionList]); useEffect(() => { if (searchParams.get("code")) { @@ -32,19 +40,15 @@ export default function SolutionReport() { } }, [searchParams]); const handleGitHubLogin = async () => {}; - const isDetailContentVisible = questionName !== ""; + // const isDetailContentVisible = questionName !== ""; const isSubmitBtnDisabled = detailContent === ""; function handleOtherSolutionBtnClick() { setSubmitted(false); - setQuestionName(""); + // setQuestionName(""); setDetailContent(""); } - function handleQuestionNameInput(e) { - setQuestionName(e.target.value); - } - function handleDetailContentInput(e) { setDetailContent(e.target.value); } @@ -52,7 +56,9 @@ export default function SolutionReport() { function handleSubmitBtnClick() { setSubmitted(true); } - + function handleQuestionNameChange(e) { + console.log(e); + } return ( <>
@@ -68,38 +74,9 @@ export default function SolutionReport() { 문제 이름 - - {/**/} - {/* */} - {/* 1번문제*/} - {/* */} - {/* */} - {/* 2번문제*/} - {/* */} - {/* */} - {/* 3번문제*/} - {/* */} - {/* */} - {/* 4번문제*/} - {/* */} - {/* */} - {/* 5번문제*/} - {/* */} - {/* */} - {/* 6번문제*/} - {/* */} - {/* */} - {/* 7번문제*/} - {/* */} - {/* */} - {/* 8번문제*/} - {/* */} - {/**/} {/*isDetailContentVisible &&*/} diff --git a/src/state/solutionList.js b/src/state/solutionList.js new file mode 100644 index 0000000..f60839b --- /dev/null +++ b/src/state/solutionList.js @@ -0,0 +1,9 @@ +import { atom } from "recoil"; +import { localStorageEffect } from "./utils/storageEffect"; + +const solutionListState = atom({ + key: "solutionListState", + default: [], + effects: [localStorageEffect("solutionListState")], +}); +export default solutionListState; diff --git a/src/state/user.js b/src/state/user.js index 71fd64d..44f8737 100644 --- a/src/state/user.js +++ b/src/state/user.js @@ -1,10 +1,10 @@ import { atom } from "recoil"; -import { sessionStorageEffect } from "./utils/sessionStorageEffect"; +import { storageEffect } from "./utils/storageEffect"; const userState = atom({ key: "userState", default: {}, - effects: [sessionStorageEffect("userState")], + effects: [storageEffect("userState")], }); export default userState; diff --git a/src/state/utils/sessionStorageEffect.js b/src/state/utils/sessionStorageEffect.js deleted file mode 100644 index f994c05..0000000 --- a/src/state/utils/sessionStorageEffect.js +++ /dev/null @@ -1,14 +0,0 @@ -export const sessionStorageEffect = - (key) => - ({ setSelf, onSet }) => { - const savedValue = sessionStorage.getItem(key); - if (savedValue != null) { - setSelf(JSON.parse(savedValue)); - } - - onSet((newValue, _, isReset) => { - isReset - ? sessionStorage.removeItem(key) - : sessionStorage.setItem(key, JSON.stringify(newValue)); - }); - }; diff --git a/src/state/utils/storageEffect.js b/src/state/utils/storageEffect.js new file mode 100644 index 0000000..5e53f19 --- /dev/null +++ b/src/state/utils/storageEffect.js @@ -0,0 +1,29 @@ +export const localStorageEffect = + (key) => + ({ setSelf, onSet }) => { + const savedValue = localStorage.getItem(key); + if (savedValue != null) { + setSelf(JSON.parse(savedValue)); + } + + onSet((newValue, _, isReset) => { + isReset + ? localStorage.removeItem(key) + : localStorage.setItem(key, JSON.stringify(newValue)); + }); + }; + +export const storageEffect = + (key) => + ({ setSelf, onSet }) => { + const savedValue = sessionStorage.getItem(key); + if (savedValue != null) { + setSelf(JSON.parse(savedValue)); + } + + onSet((newValue, _, isReset) => { + isReset + ? sessionStorage.removeItem(key) + : sessionStorage.setItem(key, JSON.stringify(newValue)); + }); + }; diff --git a/src/style/styledComponents.js b/src/style/styledComponents.js index d5048ee..e154534 100644 --- a/src/style/styledComponents.js +++ b/src/style/styledComponents.js @@ -48,36 +48,6 @@ export const TextInput = styled.input` border: 0; `; -export const QuestionList = styled.ul` - // display: none; - position: absolute; - top: 20rem; - left: 0; - width: 100%; - height: 33.2rem; - background-color: ${(props) => props.theme.searchBg}; - overflow: scroll; - z-index: 10; -`; - -export const QuestionItem = styled.li``; - -export const QuestionBtn = styled.button` - width: 100%; - height: 9rem; - text-align: left; - line-height: 9rem; - text-indent: 2rem; - background-color: transparent; - font-size: 3.1rem; - color: ${(props) => props.theme.basicWhite}; - border-bottom: 1px solid ${(props) => props.theme.notSelectedTab}; - cursor: pointer; - &:hover { - background-color: ${(props) => props.theme.programmersBlue}; - } -`; - export const SubmitBtn = styled.button` width: 100%; height: 13rem;