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

refactor: 로딩 및 최소 지연시간 추가 #45

Merged
merged 7 commits into from
Jul 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 3 additions & 2 deletions frontend/src/api/AxiosInstance.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import axios from "axios";
import {NETWORK} from "@/constants/api";
import {checkAndSetToken, handleTokenError} from "@/api/Interceptors";
import {checkAndSetToken, delayFulfilled, handleTokenError, waitingFulfilled} from "@/api/Interceptors";

export const axiosInstance = axios.create({
baseURL: `${import.meta.env.VITE_API}/api`,
Expand All @@ -10,4 +10,5 @@ export const axiosInstance = axios.create({
})

axiosInstance.interceptors.request.use(checkAndSetToken);
axiosInstance.interceptors.response.use(response => response, handleTokenError);
axiosInstance.interceptors.request.use(delayFulfilled);
axiosInstance.interceptors.response.use(waitingFulfilled, handleTokenError);
26 changes: 25 additions & 1 deletion frontend/src/api/Interceptors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {AxiosError, InternalAxiosRequestConfig} from "axios";
import {AxiosError, AxiosResponse, InternalAxiosRequestConfig} from "axios";
import {TOKEN} from "@/constants/api";
import {PATH} from "@/constants/path";
import {reIssueToken} from "@/api/auth/ReIssueToken";
Expand Down Expand Up @@ -54,3 +54,27 @@ export const handleTokenError = async(error: AxiosError<ErrorResponseData>) => {
throw new HTTPError(status, data.message, data.code);

}


export const delayFulfilled = (config: InternalAxiosRequestConfig )=> ({
...config,
p0: performance.now(),
});

export const waitingFulfilled = async (response: AxiosResponse) => {
const minimumDelay = 1000;
const latency = performance.now() - response.config.p0;
const shouldNotDelay = minimumDelay < latency;

if (shouldNotDelay) {
return response;
}

const remainder = minimumDelay - latency;
const [responseWithDelay] = await Promise.all([
response,
new Promise((resolve) => setTimeout(resolve, remainder)),
]);
return responseWithDelay;
}

4 changes: 4 additions & 0 deletions frontend/src/api/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@ declare module 'axios' {
export interface AxiosRequestConfig {
useAuth: boolean;
}

export interface InternalAxiosRequestConfig {
p0: number;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {Button, ModalBody, ModalContent, ModalHeader, Slider} from "@nextui-org/
import {problemCount, tailQuestionCount} from "@/components/InterviewCreatorForm/InterviewCreateForm.constant";
import {InterviewSettings} from "@/types/interview";
import {useInterviewCreateMutation} from "@/hooks/api/interview/useInterviewCreateMutation";
import {useCallback} from "react";
import {useInterviewCreateForm} from "@/components/InterviewCreatorForm/useInterviewCreateForm";


Expand Down Expand Up @@ -46,13 +45,9 @@ interface InterviewCreateFormProps {
}

const InterviewCreateForm = ({ interviewSettings: {questionSetId, count, tailQuestionDepth}}: InterviewCreateFormProps) => {
const {mutate} = useInterviewCreateMutation();
const {mutate, isPending,isSuccess} = useInterviewCreateMutation();
const {interviewCreateForm, handleOnChange} = useInterviewCreateForm({tailQuestionDepth, totalProblemCount:count})

const handleInterviewCreate = useCallback(() => {
mutate({questionSetId, ...interviewCreateForm});
}, [interviewCreateForm]);

return (
<ModalContent>
{() => (
Expand All @@ -78,7 +73,9 @@ const InterviewCreateForm = ({ interviewSettings: {questionSetId, count, tailQue
<p>*아직 지원하지 않습니다.</p>
<InterviewSettingsSlider label={"문항 별 대기 시간"} disabled={true}/>
<InterviewSettingsSlider label={"문항 별 제한 시간"} disabled={true}/>
<Button onClick={handleInterviewCreate}>면접 시작</Button>
<Button onClick={() => {
mutate({questionSetId, ...interviewCreateForm});
}} isLoading={isPending || isSuccess}>면접 시작</Button>
</ModalBody>
</>
)}
Expand Down
29 changes: 15 additions & 14 deletions frontend/src/components/InterviewForm/InterviewForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const border = "1px solid rgb(54, 54, 54)";
const InterviewForm = ({interviewId}: InterviewFormProps) => {
const {
interview,
interviewLoading,
handleSubmit,
handlePass,
chatList,
Expand Down Expand Up @@ -51,25 +52,25 @@ const InterviewForm = ({interviewId}: InterviewFormProps) => {
borderLeft: border
}}>
<div className='p-3'>
<InterviewQuestionBoard question={interview.question}
chatList={chatList}
remainTailQuestionCount={remainTailQuestionCount}/>
<InterviewQuestionBoard
isLoading={interviewLoading}
question={interview.question}
chatList={chatList}
remainTailQuestionCount={remainTailQuestionCount}/>
</div>

<div className="gap-1 h-full flex flex-col max-h-[310px] p-4" style={{
borderTop:border
}}>
<div>
<Textarea
placeholder="여기에 답을 적어주세요"
value={answer}
onChange={(e) => handleAnswerChange(e.target.value)}
minRows={10}
rows={10}
maxRows={10}
disabled={feedbackWaiting}
/>
</div>
<Textarea
placeholder="여기에 답을 적어주세요"
value={answer}
onChange={(e) => handleAnswerChange(e.target.value)}
minRows={10}
rows={10}
maxRows={10}
disabled={feedbackWaiting}
/>
<div className="row-span-1 flex flex-col-reverse" >
<InterviewController
onQuit={quit}
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/components/InterviewForm/useInterviewForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ interface InterviewForm {
export const useInterviewForm = (interviewId: number) => {

const navigate = useNavigate();
const {interview, refetch, error} = useInterviewQuestionLoadQuery(interviewId);
const {interview, refetch, error, isLoading: interviewLoading} = useInterviewQuestionLoadQuery(interviewId);
const interviewSubmitMutation = useInterviewSubmitMutation();

const answerFeedbackMutation = useAnswerFeedbackMutation();
Expand Down Expand Up @@ -228,9 +228,10 @@ export const useInterviewForm = (interviewId: number) => {

return {
interview,
interviewLoading,
handleSubmit,
handlePass,
feedbackWaiting: answerFeedbackMutation.isPending,
feedbackWaiting: answerFeedbackMutation.isPending || (interviewSubmitMutation.isPending || tailQuestionSubmitMutation.isPending),
remainTailQuestionCount: interviewForm.remainTailQuestionCount,
chatList: interviewForm.chatList,
answer: interviewForm.answer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
Dropdown,
DropdownItem,
DropdownMenu,
DropdownTrigger,
DropdownTrigger, Spinner,
Table,
TableBody,
TableCell,
Expand Down Expand Up @@ -38,9 +38,8 @@ const statusDisplay: Record<InterviewState, string> = {

const InterviewHistoryTable = () => {


const [page, setPage] = useState(1);
const {data, totalPages, refetch} = useMyInterviewQuery(page);
const {data, totalPages, refetch, isLoading} = useMyInterviewQuery(page);
const navigate = useNavigate();


Expand Down Expand Up @@ -117,6 +116,7 @@ const InterviewHistoryTable = () => {
<Table
isCompact
removeWrapper

classNames={tableClassNames}
aria-label="Example table with custom cells, pagination and sorting"
bottomContent={bottomContent}
Expand All @@ -136,7 +136,10 @@ const InterviewHistoryTable = () => {
</TableColumn>
)}
</TableHeader>
<TableBody items={data === undefined ? [] : data} emptyContent={"참여한 면접 이력이 없습니다."}>
<TableBody
isLoading={isLoading}
loadingContent={<Spinner label="Loading..." />}
items={data === undefined ? [] : data} emptyContent={"참여한 면접 이력이 없습니다."}>
{(row) => (
<TableRow key={row.interviewId}>
{(columnKey) => <TableCell>{renderCell(row, columnKey)}</TableCell>}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@ import UserChat from "@/components/Chat/UserChat";
import {Chat} from "@/types/question";
import {useCallback, useEffect, useRef} from "react";


interface InterviewQuestionBoardProps {
question: string;
remainTailQuestionCount: number;
chatList: Chat[]
chatList: Chat[],
isLoading: boolean;
}

const InterviewQuestionBoard = ({question, remainTailQuestionCount, chatList}: InterviewQuestionBoardProps) => {

const InterviewQuestionBoard = ({question, remainTailQuestionCount, chatList, isLoading}: InterviewQuestionBoardProps) => {
const scrollRef = useRef<HTMLDivElement>(null);

useEffect(() => {
Expand All @@ -38,7 +37,7 @@ const InterviewQuestionBoard = ({question, remainTailQuestionCount, chatList}: I
return (
<div className="max-w-full min-w-full ">
<div className='row-span-4 col-auto'>
<p className='text-3xl'>{question}</p>
<p className='text-3xl'>{isLoading ? "질문을 가져옵니다....": question}</p>
<TailQuestionMessage/>


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const QuestionSetItem = ({questionSet:{questionSetId, title, description, tailQu
<CardBody>
<Image
isBlurred
width={300}
width={270}
height={200}
src="https://velog.velcdn.com/images/pak4184/post/98ba8b4f-7b89-4d28-8376-0dc8d1be805a/image.png"
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {Button, Card, CardBody, CardFooter, CardHeader, Divider, Skeleton, Spacer} from "@nextui-org/react";

const QuestionSetItemListSkeleton = () => {
return new Array(5).fill(0).map((_,index) => <Card key={index} className="max-w-[270px] ">
<CardHeader className="flex flex-col items-start ">
<span className="text-blue-500">NEW</span>
<Skeleton className='w-3/5 rounded-lg'>
<div className='h-4 w-3/5 rounded-lg bg-default-200'></div>
</Skeleton>
<Spacer y={2}/>
<Skeleton className='w-full rounded-lg'>
<div className='h-4 w-4/5 rounded-lg bg-default-200'></div>
</Skeleton>
</CardHeader>
<CardBody className='p-3'>
<Skeleton className="w-[240px] h-[200px] rounded-lg"></Skeleton>
</CardBody>
<Divider/>
<CardFooter className="flex flex-row-reverse">
<Skeleton className="rounded-lg">
<Button />
</Skeleton>
</CardFooter>
</Card>)
}

export default QuestionSetItemListSkeleton;
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {PATH} from "@/constants/path";
import {toast} from "sonner";



export const useInterviewCreateMutation = () => {
const navigate = useNavigate();
return useMutation({
Expand All @@ -14,9 +15,7 @@ export const useInterviewCreateMutation = () => {
navigate(PATH.INTERVIEW(interviewId));
},
onError:(error) => {
console.log(error.message);
toast.error(error.message)
}

})
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import {loadByCurrentInterviewQuestion} from "@/api/interview/LoadByCurrentInter

export const useInterviewQuestionLoadQuery = (interviewId: number) => {
// TODO: 변수명 바꾸기
const {data: interview, refetch, error} = useSuspenseQuery({
const {data: interview, refetch, error, isFetching} = useSuspenseQuery({
queryKey: ['loadInterviewQuestion', interviewId],
queryFn: () => loadByCurrentInterviewQuestion(interviewId),
gcTime: 60 * 60 * 10,
staleTime: 60* 60 * 10
});

return {interview, refetch, error}
return {interview, refetch, error, isLoading: isFetching}
}


Expand Down
5 changes: 3 additions & 2 deletions frontend/src/hooks/api/interview/useMyInterviewQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const size = 10;

export const useMyInterviewQuery = (page: number) => {

const {data, refetch} = useQuery({
const {data, refetch, isLoading, isRefetching} = useQuery({
queryKey: ['my interview'],
queryFn: () => myInterview({page: page - 1, size}),
placeholderData: keepPreviousData,
Expand All @@ -18,6 +18,7 @@ export const useMyInterviewQuery = (page: number) => {
return {
totalPages: data?.totalPages,
data: data?.content,
refetch
refetch,
isLoading: isLoading || isRefetching
}
}
8 changes: 5 additions & 3 deletions frontend/src/pages/MainPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import {InterviewSettings} from "@/types/interview";
import InterviewCreateForm from "@/components/InterviewCreatorForm/InterviewCreateForm";
import QuestionSetItemList from "@/components/QuestionSetItem/QuestionSetItemList";
import {useIntersectionObserver} from "@/hooks/useIntersectionObserver";
import QuestionSetItemListSkeleton from "@/components/QuestionSetItem/QuestionSetItemListSkeleton";

export default function(){
const {data, fetchNextPage} = useQuestionSetQuery();
const {data, fetchNextPage, isLoading} = useQuestionSetQuery();
const {isOpen, onClose, onOpen} = useDisclosure();

const ref = useRef<HTMLDivElement>(null)
Expand Down Expand Up @@ -38,10 +39,11 @@ export default function(){

<div className="p-5 flex flex-wrap gap-4">
{data?.pages.map((data, index)=> <QuestionSetItemList key={index} questionSetItems={data.content} openInterviewSetting={handleOpenInterviewSettings}/>)}
{isLoading ? <QuestionSetItemListSkeleton/> : <></>}
<div ref={ref}/>
</div>
<Modal backdrop="blur" isDismissable={false} isKeyboardDismissDisabled={true} isOpen={isOpen} placement={"top"} className={`text-foreground bg-background dark`} onClose={onClose}>
<InterviewCreateForm interviewSettings={settings} />
<Modal backdrop="opaque" isDismissable={false} isKeyboardDismissDisabled={true} isOpen={isOpen} placement={"top"} onClose={onClose}>
<InterviewCreateForm interviewSettings={settings}/>
</Modal>
</div>
)
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/utils/Date.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@


export const dateToString = (date: Date) => `${date.getFullYear()}년 ${date.getMonth()}월 ${date.getDay()}일`;
export const dateToString = (date: Date) => `${date.getFullYear()}년 ${date.getMonth() + 1}월 ${date.getDay()}일`;
Loading