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

Feat/random select #160

Merged
merged 13 commits into from
Dec 6, 2023
33 changes: 33 additions & 0 deletions client/public/tags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Tag } from '../src/types/Tag';
export const constTags: Tag[] = [
{ id: 96, name: '수학' },
{ id: 12, name: '구현' },
{ id: 24, name: '다이나믹 프로그래밍' },
{ id: 134, name: '자료 구조' },
{ id: 13, name: '그래프 이론' },
{ id: 15, name: '그리디 알고리즘' },
{ id: 55, name: '문자열' },
{ id: 78, name: '브루트포스 알고리즘' },
{ id: 14, name: '그래프 탐색' },
{ id: 140, name: '정렬' },
{ id: 18, name: '기하학' },
{ id: 141, name: '정수론' },
{ id: 165, name: '트리' },
{ id: 109, name: '애드 혹' },
{ id: 92, name: '세그먼트 트리' },
{ id: 127, name: '이분 탐색' },
{ id: 83, name: '사칙연산' },
{ id: 104, name: '시뮬레이션' },
{ id: 20, name: '너비 우선 탐색' },
{ id: 184, name: '해 구성하기' },
{ id: 21, name: '누적 합' },
{ id: 143, name: '조합론' },
{ id: 47, name: '많은 조건 분기' },
{ id: 19, name: '깊이 우선 탐색' },
{ id: 149, name: '최단 경로' },
{ id: 81, name: '비트마스킹' },
{ id: 185, name: '해시를 사용한 집합과 맵' },
{ id: 31, name: '데이크스트라' },
{ id: 62, name: '백트래킹' },
{ id: 169, name: '트리를 사용한 집합과 맵' },
];
22 changes: 22 additions & 0 deletions client/src/apis/randomProblems.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { apiClient } from './apiClient';
import { ProblemResponse } from '../types/Problem';

export async function randomProblem(
tagIds: number[],
levels: number[],
count: number,
): Promise<ProblemResponse[]> {
const VITE_BASE_URL = import.meta.env.VITE_BASE_URL as string;
const tagIdsQuery = tagIds.join(',');
const levelIdsQuery = levels.join(',');
try {
const {data} = await apiClient
.get(
`${VITE_BASE_URL}/problem/random?tagIds=${tagIdsQuery}&levels=${levelIdsQuery}&count=${count}`,
);
return data;
} catch (error) {
console.log(error);
throw new Error('문제를 불러오는데 실패했습니다.');
}
}
glowisn marked this conversation as resolved.
Show resolved Hide resolved
21 changes: 11 additions & 10 deletions client/src/components/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { FaAngleDown, FaAngleUp } from 'react-icons/fa6';

export interface DropdownProps<T> {
options: Array<T>;
onOptionClick: (option: T) => void;
optionPostFix?: string;
selected: T;
setSelected: React.Dispatch<React.SetStateAction<T>>;
buttonClassName?: string;
itemBoxClassName?: string;
itemClassName?: string;
Expand All @@ -14,8 +15,9 @@ export interface DropdownProps<T> {
* Custom Dropdown Component
* @param {DropdownProps<T>} props
* @param {Array<T>} props.options dropdown items with value
* @param {Function} props.onOptionClick callback function when dropdown item is clicked
* @param {string} props.optionPostFix postfix of dropdown item
* @param {T} props.selected selected item
* @param {React.Dispatch<React.SetStateAction<T>>} props.setSelected set selected item
* @param {string} props.buttonClassName className of dropdown button for tailwindcss
* @param {string} props.itemBoxClassName className of dropdown item box for tailwindcss
* @param {string} props.itemClassName className of dropdown item for tailwindcss
Expand All @@ -24,17 +26,17 @@ export interface DropdownProps<T> {
*/
export default function Dropdown<T>({
options,
onOptionClick,
selected,
setSelected,
optionPostFix = '',
buttonClassName = '',
itemBoxClassName = '',
itemClassName = '',
}: DropdownProps<T>): JSX.Element {
const [isActive, setIsActive] = useState(false);
const [selected, setIsSelected] = useState<T>(options[0]);
const dropdownRef = useRef<HTMLDivElement>(null);

const dropdwonOutsiedClick = (event: MouseEvent) => {
const dropdownOutsideClick = (event: MouseEvent) => {
const targetElement = document.elementFromPoint(
event.clientX,
event.clientY,
Expand All @@ -46,12 +48,12 @@ export default function Dropdown<T>({
};

useEffect(() => {
document.addEventListener('click', dropdwonOutsiedClick);
document.addEventListener('click', dropdownOutsideClick);

return () => {
document.removeEventListener('click', dropdwonOutsiedClick);
document.removeEventListener('click', dropdownOutsideClick);
};
}, []);
}, [dropdownRef]);

return (
<div className="relative" ref={dropdownRef}>
Expand All @@ -72,9 +74,8 @@ export default function Dropdown<T>({
<li
key={option as Key}
onClick={() => {
setIsSelected(option);
setSelected(option);
setIsActive(!isActive);
onOptionClick(option);
}}
className={`${itemClassName} flex cursor-pointer justify-center`}>
{option + optionPostFix}
Expand Down
69 changes: 33 additions & 36 deletions client/src/components/MultipleDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
import { Key, useEffect, useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { FaAngleDown, FaAngleUp, FaCheck } from 'react-icons/fa6';

export interface MultipleChoiceDropdownProps<T> {
name: string;
options: Array<T>;
onOptionClick: (option: T[]) => void;
displayNames: string[];
selected: T[];
setSelected: React.Dispatch<React.SetStateAction<T[]>>;
optionPostFix?: string;
buttonClassName?: string;
itemBoxClassName?: string;
itemClassName?: string;
}

interface Identifiable {
id: string | number;
}

/**
* Custom MultipleChoiceDropdown Component
* @param {MultipleChoiceDropdownProps<T>} props
* @param {string} props.name name of dropdown
* @param {Array<T>} props.options dropdown items with value
* @param {Function} props.onOptionClick callback function when dropdown item is clicked
* @param {string[]} props.displayNames displayname of dropdown
* @param {string} props.optionPostFix postfix of dropdown item
* @param {string} props.buttonClassName className of dropdown button for tailwindcss
* @param {string} props.itemBoxClassName className of dropdown item box for tailwindcss
Expand All @@ -25,36 +31,29 @@ export interface MultipleChoiceDropdownProps<T> {
* @return {JSX.Element}
*/

export default function MultipleChoiceDropdown<T>({
export default function MultipleChoiceDropdown<T extends Identifiable>({
name,
options,
onOptionClick,
displayNames,
selected,
setSelected,
optionPostFix = '',
buttonClassName = '',
itemBoxClassName = '',
itemClassName = '',
}: MultipleChoiceDropdownProps<T>): JSX.Element {
const [isActive, setIsActive] = useState(false);
const [selected, setSelected] = useState<T[]>([]);
const dropdownRef = useRef<HTMLDivElement>(null);

const handleOptionClick = (option: T) => {
setSelected((prevSelected) => {
let newSelected;
if (prevSelected.includes(option)) {
newSelected = prevSelected.filter((o) => o !== option);
} else {
newSelected = [...prevSelected, option];
}

// Sort the selected options in the order they appear in the options array
newSelected.sort((a, b) => options.indexOf(a) - options.indexOf(b));

return newSelected;
});
if (selected.some((o) => o.id === option.id)) {
setSelected(selected.filter((o) => o.id !== option.id));
} else {
setSelected([...selected, option]);
}
};

const dropdwonOutsiedClick = (event: MouseEvent) => {
const dropdownOutsideClick = (event: MouseEvent) => {
const targetElement = document.elementFromPoint(
event.clientX,
event.clientY,
Expand All @@ -66,20 +65,12 @@ export default function MultipleChoiceDropdown<T>({
};

useEffect(() => {
document.addEventListener('click', dropdwonOutsiedClick);
document.addEventListener('click', dropdownOutsideClick);

return () => {
document.removeEventListener('click', dropdwonOutsiedClick);
};
}, []);

// Wrap onOptionClick with useEffect to prevent exhaustive-deps - https://github.com/facebook/react/issues/14920
useEffect(() => {
const callback = () => {
onOptionClick(selected);
document.removeEventListener('click', dropdownOutsideClick);
};
callback();
}, [onOptionClick, selected]);
}, [dropdownRef]);

return (
<div className="relative" ref={dropdownRef}>
Expand All @@ -89,7 +80,7 @@ export default function MultipleChoiceDropdown<T>({
}}
className={`${buttonClassName} cursor-pointer`}>
<div className="flex flex-row items-center gap-2">
<div>{`${name}`}</div>
{(selected.length > 0)? `${selected.length}개 선택됨` : `${name}`}
<div className="w-4">
{isActive ? <FaAngleUp /> : <FaAngleDown />}
</div>
Expand All @@ -98,16 +89,22 @@ export default function MultipleChoiceDropdown<T>({
<ol
className={`${itemBoxClassName} absolute z-10 flex max-h-[320px] w-full overflow-auto`}
style={{ display: isActive ? 'block' : 'none' }}>
{options.map((option) => (
{options.map((option, index) => (
<li
key={option as Key}
key={option.id}
onClick={() => handleOptionClick(option)}
className={`${itemClassName} flex cursor-pointer items-center justify-between`}>
<div className={`w-4 p-1`}>
<FaCheck color={selected.includes(option) ? 'green' : 'gray'} />
<FaCheck
color={
selected.some((o) => o.id === option.id) ? 'green' : 'gray'
}
/>
</div>
<div className="flex-grow overflow-hidden overflow-ellipsis whitespace-nowrap px-1 text-center hover:whitespace-normal">
{option + optionPostFix}
{displayNames === undefined
? option + optionPostFix
: displayNames[index] + optionPostFix}
</div>
</li>
))}
Expand Down
Loading