Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

검색 기능 구현 #21

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
Open
7 changes: 7 additions & 0 deletions src/apis/solutionList.js
Original file line number Diff line number Diff line change
@@ -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();
};
98 changes: 98 additions & 0 deletions src/components/QuestionInputAndList.js
Original file line number Diff line number Diff line change
@@ -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 (
<>
<TextInput
id="questionNameInput"
placeholder="문제 이름을 검색하세요."
defaultValue={questionName}
value={questionName}
onFocus={handleQuestionNameFocus}
onInput={handleQuestionNameInput}
/>
{isQuestionListVisible && (
<QuestionList id="questionsList">
{questionList.map((value, index) => (
<QuestionItem key={value.name + index}>
<QuestionBtn
onClick={handleQuestionClick}
data-value={value.name}
>
{value.name} / Level {value.level}
</QuestionBtn>
</QuestionItem>
))}
</QuestionList>
)}
</>
);
};
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};
}
`;
38 changes: 38 additions & 0 deletions src/components/utils/createFuzzyMatcher.js
Original file line number Diff line number Diff line change
@@ -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);
};
35 changes: 35 additions & 0 deletions src/hooks/solutionList/useFetchSolutionList.js
Original file line number Diff line number Diff line change
@@ -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", "");
}
7 changes: 7 additions & 0 deletions src/hooks/solutionList/useSetSolutionList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { useSetRecoilState } from "recoil";
import solutionListState from "../../state/solutionList";

const useSetSolutionList = () => {
return useSetRecoilState(solutionListState);
};
export default useSetSolutionList;
7 changes: 7 additions & 0 deletions src/hooks/solutionList/useSolutionListValue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { useRecoilValue } from "recoil";
import solutionListState from "../../state/solutionList";

const useSolutionListValue = () => {
return useRecoilValue(solutionListState);
};
export default useSolutionListValue;
82 changes: 42 additions & 40 deletions src/pages/errorReportPage/ErrorReport.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -51,7 +48,9 @@ export default function ErrorReport() {
function handleSubmitBtnClick() {
setSubmitted(true);
}

function handleQuestionNameChange(e) {
setQuestionName(e);
}
return (
<>
<Header />
Expand Down Expand Up @@ -110,38 +109,41 @@ export default function ErrorReport() {
{isQuestionNameVisible && (
<StepByStepInputItem>
<InputLabel>문제 이름</InputLabel>
<TextInput
id="questionNameInput"
placeholder="문제 이름을 검색하세요."
defaultValue={questionName}
onInput={handleQuestionNameInput}
<QuestionInputAndList
onQuestionNameChange={handleQuestionNameChange}
/>
<QuestionList id="questionsList">
<QuestionItem>
<QuestionBtn>1번문제</QuestionBtn>
</QuestionItem>
<QuestionItem>
<QuestionBtn>2번문제</QuestionBtn>
</QuestionItem>
<QuestionItem>
<QuestionBtn>3번문제</QuestionBtn>
</QuestionItem>
<QuestionItem>
<QuestionBtn>4번문제</QuestionBtn>
</QuestionItem>
<QuestionItem>
<QuestionBtn>5번문제</QuestionBtn>
</QuestionItem>
<QuestionItem>
<QuestionBtn>6번문제</QuestionBtn>
</QuestionItem>
<QuestionItem>
<QuestionBtn>7번문제</QuestionBtn>
</QuestionItem>
<QuestionItem>
<QuestionBtn>8번문제</QuestionBtn>
</QuestionItem>
</QuestionList>
{/*<TextInput*/}
{/* id="questionNameInput"*/}
{/* placeholder="문제 이름을 검색하세요."*/}
{/* defaultValue={questionName}*/}
{/* onInput={handleQuestionNameInput}*/}
{/*/>*/}
{/*<QuestionInputAndList id="questionsList">*/}
{/* <QuestionItem>*/}
{/* <QuestionBtn>1번문제</QuestionBtn>*/}
{/* </QuestionItem>*/}
{/* <QuestionItem>*/}
{/* <QuestionBtn>2번문제</QuestionBtn>*/}
{/* </QuestionItem>*/}
{/* <QuestionItem>*/}
{/* <QuestionBtn>3번문제</QuestionBtn>*/}
{/* </QuestionItem>*/}
{/* <QuestionItem>*/}
{/* <QuestionBtn>4번문제</QuestionBtn>*/}
{/* </QuestionItem>*/}
{/* <QuestionItem>*/}
{/* <QuestionBtn>5번문제</QuestionBtn>*/}
{/* </QuestionItem>*/}
{/* <QuestionItem>*/}
{/* <QuestionBtn>6번문제</QuestionBtn>*/}
{/* </QuestionItem>*/}
{/* <QuestionItem>*/}
{/* <QuestionBtn>7번문제</QuestionBtn>*/}
{/* </QuestionItem>*/}
{/* <QuestionItem>*/}
{/* <QuestionBtn>8번문제</QuestionBtn>*/}
{/* </QuestionItem>*/}
{/*</QuestionInputAndList>*/}
</StepByStepInputItem>
)}

Expand Down
Loading