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;