Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
6901e9a
feat: 과외 선생 정보 페이지 debounce search 추가
sterdsterd Aug 14, 2025
58f4742
feat: 개념 태그 페이지 debounce search 추가
sterdsterd Aug 14, 2025
0f85591
feat: 문제 페이지 debounce search 추가
sterdsterd Aug 14, 2025
8ddf987
feat: 문제 세트 페이지 debounce search 추가
sterdsterd Aug 14, 2025
bec85ad
feat: ProblemSearchModal에 debounce search 추가
sterdsterd Aug 14, 2025
21f6a3a
feat: 발행 세트 검색 페이지 debounce search 추가
sterdsterd Aug 14, 2025
cee4002
refactor: @component export 구조 수정
sterdsterd Aug 14, 2025
6264f1d
feat: 개념 태그 대분류 수정 Modal 추가
sterdsterd Aug 14, 2025
5004821
feat: 문제 추가/수정 시 form validation 추가
sterdsterd Aug 14, 2025
438fa1c
feat: 문제 권장 시간 분/초 표시
sterdsterd Aug 14, 2025
f8e6f6e
feat: 반응형 디자인 수정
iameuni Aug 14, 2025
e2741b2
Merge pull request #102 from iameuni/feat/admin/problem-#100
sterdsterd Aug 14, 2025
63e740e
feat: Add ProblemID component with toggleable guidance table in Probl…
sterdsterd Aug 15, 2025
6a2f89b
feat: Update PracticeTestSelect component for improved styling and in…
sterdsterd Aug 15, 2025
1e6998a
refactor: Update terminology from '문항' to '문제' across various compone…
sterdsterd Aug 15, 2025
ba51b8e
feat: 메인 문제 답안 상태 관리 개선
sterdsterd Aug 15, 2025
057515a
feat: 개념 태그 Validation 추가
sterdsterd Aug 15, 2025
0c58dcb
fix: Improve error handling in problem creation and update flows to d…
sterdsterd Aug 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 28 additions & 11 deletions apps/admin/src/components/common/Inputs/AnswerInput.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { Input } from '@components';
import { ProblemAnswerType } from '@types';
import { forwardRef } from 'react';
import { forwardRef, InputHTMLAttributes } from 'react';

const AnswerTypeList = ['MULTIPLE_CHOICE', 'SHORT_ANSWER'];
const AnswerTypeName = {
MULTIPLE_CHOICE: '객',
SHORT_ANSWER: '주',
};

interface AnswerTypeSectionProps {
interface AnswerTypeSectionProps extends InputHTMLAttributes<HTMLInputElement> {
selectedAnswerType: ProblemAnswerType | undefined;
}

interface AnswerInputSectionProps {
interface AnswerInputSectionProps extends InputHTMLAttributes<HTMLInputElement> {
selectedAnswerType: ProblemAnswerType | undefined;
selectedAnswer: number | undefined;
isError: boolean;
}

const AnswerInput = ({ children }: { children: React.ReactNode }) => {
Expand All @@ -27,10 +28,17 @@ const AnswerTypeSection = forwardRef<HTMLInputElement, AnswerTypeSectionProps>(
<div className='flex gap-200'>
{AnswerTypeList.map((answerType) => (
<label key={answerType}>
<input type='radio' className='hidden' ref={ref} value={answerType} {...props} />
<input
type='radio'
className='hidden'
ref={ref}
value={answerType}
checked={answerType === selectedAnswerType}
{...props}
/>
<div
className={`rounded-100 flex h-[5.6rem] w-[5.6rem] cursor-pointer items-center justify-center ${answerType === selectedAnswerType ? 'bg-midgray200 text-white' : 'bg-lightgray300 text-lightgray500'} `}>
<span className='font-medium-24'>
className={`rounded-200 flex h-[5.6rem] w-[5.6rem] cursor-pointer items-center justify-center ${answerType === selectedAnswerType ? 'bg-midgray200 text-white' : 'bg-lightgray300 text-lightgray500'} `}>
<span className='font-medium-18'>
{AnswerTypeName[answerType as ProblemAnswerType]}
</span>
</div>
Expand All @@ -42,21 +50,30 @@ const AnswerTypeSection = forwardRef<HTMLInputElement, AnswerTypeSectionProps>(
);

const AnswerInputSection = forwardRef<HTMLInputElement, AnswerInputSectionProps>(
({ selectedAnswerType, selectedAnswer, ...props }, ref) => {
({ selectedAnswerType, selectedAnswer, isError, ...props }, ref) => {
return (
<>
{selectedAnswerType === 'SHORT_ANSWER' && <Input ref={ref} {...props} />}
{selectedAnswerType === 'SHORT_ANSWER' && (
<Input ref={ref} {...props} className={`${isError ? 'border-red' : ''}`} type='number' />
)}
{selectedAnswerType === 'MULTIPLE_CHOICE' && (
<div className='flex items-center justify-between gap-400'>
<div className='flex items-center justify-between gap-200'>
{Array.from({ length: 5 }, (_, i) => i + 1).map((num) => (
<label key={num}>
<input type='radio' className='hidden' value={num} ref={ref} {...props} />
<input
type='radio'
className='hidden'
value={num}
ref={ref}
checked={Number(selectedAnswer) === num}
{...props}
/>
<div
className={`flex h-[5.6rem] w-[5.6rem] cursor-pointer items-center justify-center rounded-full ${
Number(selectedAnswer) === num
? 'bg-darkgray100 text-white'
: 'border-lightgray500 border bg-white text-black'
}`}>
} ${isError ? 'border-red' : ''}`}>
<span className='font-bold-18'>{num}</span>
</div>
</label>
Expand Down
6 changes: 3 additions & 3 deletions apps/admin/src/components/common/Inputs/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { InputHTMLAttributes, forwardRef } from 'react';

const Input = forwardRef<HTMLInputElement, InputHTMLAttributes<HTMLInputElement>>(
({ ...props }, ref) => {
({ className, ...rest }, ref) => {
return (
<input
ref={ref}
className={`placeholder:text-lightgray500 disabled:text-lightgray500 font-bold-18 border-lightgray500 rounded-400 h-[5.2rem] w-full border bg-white px-400 text-black ${props.className}`}
{...props}
{...rest}
className={`placeholder:text-lightgray500 disabled:text-lightgray500 font-bold-18 border-lightgray500 rounded-400 h-[5.2rem] w-full border bg-white px-400 text-black ${className ?? ''}`}
/>
);
}
Expand Down
37 changes: 37 additions & 0 deletions apps/admin/src/components/common/Modals/EditCategoryModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Button, SearchInput } from '@components';
import { useForm } from 'react-hook-form';

interface Props {
onClose: () => void;
onSubmit: (data: { name: string }) => void;
defaultName?: string;
}

const EditCategoryModal = ({ onClose, onSubmit, defaultName }: Props) => {
const { register, handleSubmit } = useForm<{ name: string }>({
defaultValues: { name: defaultName ?? '' },
});
return (
<div className='w-4xl px-1600 py-1200'>
<h2 className='font-bold-24 text-black'>대분류 제목 수정</h2>
<form className='mt-16 flex flex-col gap-800' onSubmit={handleSubmit(onSubmit)}>
<SearchInput
sizeType='full'
label='제목'
placeholder='입력해주세요'
{...register('name', { required: true })}
/>
<div className='mt-[5.6rem] flex justify-end gap-400'>
<Button type='button' variant='light' onClick={onClose}>
취소
</Button>
<Button type='submit' variant='dark'>
수정
</Button>
</div>
</form>
</div>
);
};

export default EditCategoryModal;
46 changes: 37 additions & 9 deletions apps/admin/src/components/common/Modals/ProblemSearchModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useModal } from '@hooks';
import { components } from '@schema';
import { IcDown } from '@svg';
import { GetProblemsSearchParams } from '@types';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import PulseLoader from 'react-spinners/PulseLoader';

Expand All @@ -20,7 +20,35 @@ const ProblemSearchModal = ({ onClickCard }: ProblemSearchModalProps) => {
const [searchQuery, setSearchQuery] = useState<GetProblemsSearchParams>({});
const [selectedTagList, setSelectedTagList] = useState<number[]>([]);

const { register, handleSubmit, reset } = useForm<GetProblemsSearchParams>();
const { register, handleSubmit, reset, watch } = useForm<GetProblemsSearchParams>();

const watchedCustomId = watch('customId');
const watchedTitle = watch('title');

useEffect(() => {
const debounceTimer = setTimeout(() => {
const trimmedCustomId = (watchedCustomId ?? '').toString().trim();
const trimmedTitle = (watchedTitle ?? '').toString().trim();

setSearchQuery((prev) => {
const nextQuery: GetProblemsSearchParams = {
...prev,
customId: trimmedCustomId || undefined,
title: trimmedTitle || undefined,
};

const cleaned = Object.fromEntries(
Object.entries(nextQuery).filter(([, value]) =>
Array.isArray(value) ? value.length > 0 : Boolean(value)
)
) as GetProblemsSearchParams;

return cleaned;
});
}, 300);

return () => clearTimeout(debounceTimer);
}, [watchedCustomId, watchedTitle]);

const { data: problemList, isLoading } = getProblemsSearch(searchQuery);
const { data: tagsData } = getConcept();
Expand Down Expand Up @@ -63,24 +91,24 @@ const ProblemSearchModal = ({ onClickCard }: ProblemSearchModalProps) => {
return (
<>
<div className='h-[90dvh] w-[90dvw] px-1600 py-1200'>
<h2 className='font-bold-24 text-black'>문항 검색</h2>
<h2 className='font-bold-24 text-black'>문제 검색</h2>
<form
className='mt-800 flex items-end justify-between'
onSubmit={handleSubmit(handleClickSearch)}>
<div className='flex gap-600'>
<SearchInput
label='문항 ID'
label='문제 ID'
placeholder='입력해주세요.'
{...register('customId', { required: false })}
/>
<SearchInput
label='문항 타이틀'
label='문제 타이틀'
sizeType='long'
placeholder='입력해주세요.'
{...register('title', { required: false })}
/>
<div className='flex flex-col gap-300'>
<span className='font-medium-18 text-black'>문항 개념 태그</span>
<span className='font-medium-18 text-black'>문제 개념 태그</span>
<div
className='border-lightgray500 rounded-400 flex h-[5.6rem] w-[42.4rem] cursor-pointer items-center justify-between border bg-white px-400 py-200'
onClick={() => {
Expand Down Expand Up @@ -130,9 +158,9 @@ const ProblemSearchModal = ({ onClickCard }: ProblemSearchModalProps) => {
onClick={() => onClickCard(problem)}>
<ProblemCard>
<ProblemCard.TextSection>
<ProblemCard.Info label='문항 ID' content={customId} />
<ProblemCard.Info label='문항 타이틀' content={title} />
<ProblemCard.Info label='문항 메모' content={memo} />
<ProblemCard.Info label='문제 ID' content={customId} />
<ProblemCard.Info label='문제 타이틀' content={title} />
<ProblemCard.Info label='문제 메모' content={memo} />
</ProblemCard.TextSection>

<ProblemCard.CardImage src={mainProblemImageUrl ?? ''} height={'34.4rem'} />
Expand Down
2 changes: 1 addition & 1 deletion apps/admin/src/components/common/Modals/TagSelectModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const TagSelectModal = ({ onClose, selectedTagList, handleChangeTagList }: TagSe

return (
<div className='flex w-[70dvw] flex-col gap-800 px-1600 py-1200'>
<h2 className='font-bold-24 text-black'>문항 개념 태그 검색</h2>
<h2 className='font-bold-24 text-black'>문제 개념 태그 검색</h2>
<div className='flex w-full items-center justify-between'>
<div className='w-[42.4rem]'>
<Input {...register('searchInput')} placeholder='개념 태그를 입력해주세요.' />
Expand Down
20 changes: 16 additions & 4 deletions apps/admin/src/components/common/Modals/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,33 @@ import BaseModalTemplate from './templates/BaseModalTemplate';
import ErrorModalTemplate from './templates/ErrorModalTemplate';
import OneButtonModalTemplate from './templates/OneButtonModalTemplate';
import TwoButtonModalTemplate from './templates/TwoButtonModalTemplate';
import TagSelectModal from './TagSelectModal';
import ProblemSearchModal from './ProblemSearchModal';
import CreateNoticeModal from './CreateNoticeModal';
import CreatePracticeTestModal from './CreatePracticeTestModal';
import EditCategoryModal from './EditCategoryModal';
import EditConceptModal from './EditConceptModal';
import EditTeacherModal from './EditTeacherModal';
import NoticeListModal from './NoticeListModal';
import ProblemSearchModal from './ProblemSearchModal';
import ProgressModal from './ProgressModal';
import SelectStudentModal from './SelectStudentModal';
import StudentSearchModal from './StudentSearchModal';
import TagSelectModal from './TagSelectModal';

export {
Modal,
BaseModalTemplate,
ErrorModalTemplate,
OneButtonModalTemplate,
TwoButtonModalTemplate,
TagSelectModal,
ProblemSearchModal,
CreateNoticeModal,
CreatePracticeTestModal,
EditCategoryModal,
EditConceptModal,
EditTeacherModal,
NoticeListModal,
ProblemSearchModal,
ProgressModal,
SelectStudentModal,
StudentSearchModal,
TagSelectModal,
};
2 changes: 1 addition & 1 deletion apps/admin/src/components/common/ProblemCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const CardEmptyView = ({ onClick }: { onClick: () => void }) => {
className='flex h-full w-full cursor-pointer items-center justify-center'
onClick={onClick}
onPointerDown={(e) => e.stopPropagation()}>
<span className='font-bold-24 text-lightgray500 text-center whitespace-pre-line'>{`여기를 클릭해\n문항을 추가해주세요.`}</span>
<span className='font-bold-24 text-lightgray500 text-center whitespace-pre-line'>{`여기를 클릭해\n문제을 추가해주세요.`}</span>
</div>
);
};
Expand Down
4 changes: 2 additions & 2 deletions apps/admin/src/components/common/ProblemPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ const ProblemPreview = ({ title, memo, imgSrc }: ProblemPreviewProps) => {
return (
<div className='flex w-[48rem] min-w-[28rem] flex-col gap-400'>
<div className='flex flex-col gap-200'>
<LabelAndText label='문항 타이틀' text={title} />
<LabelAndText label='문항 메모' text={memo} />
<LabelAndText label='문제 타이틀' text={title} />
<LabelAndText label='문제 메모' text={memo} />
</div>
<div>
<img
Expand Down
4 changes: 2 additions & 2 deletions apps/admin/src/components/problem/LevelSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ interface LevelSelectProps {

const LevelSelect = ({ selectedLevel, onChange }: LevelSelectProps) => {
return (
<div className='flex items-center gap-400'>
<div className='flex items-center gap-200'>
{Array.from({ length: 10 }, (_, i) => (i + 1) as LevelType).map((num) => (
<label key={num} className='flex cursor-pointer items-center'>
<input
Expand All @@ -19,7 +19,7 @@ const LevelSelect = ({ selectedLevel, onChange }: LevelSelectProps) => {
/>
<div
className={`rounded-200 flex h-[5.6rem] w-[5.6rem] cursor-pointer items-center justify-center ${selectedLevel === num ? 'bg-midgray200 text-white' : 'bg-lightgray300 text-lightgray500'}`}>
<span className='font-medium-24'>{num}</span>
<span className='font-medium-18'>{num}</span>
</div>
</label>
))}
Expand Down
Loading
Loading