Skip to content

Commit

Permalink
Merge pull request #45 from Invincible-Backend-Study/fe/dev
Browse files Browse the repository at this point in the history
refactor: 로딩 및 최소 지연시간 추가
  • Loading branch information
JaeHongDev authored Jul 4, 2024
2 parents 8be7292 + 1d9c159 commit 0beacd0
Show file tree
Hide file tree
Showing 15 changed files with 105 additions and 46 deletions.
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()}일`;

0 comments on commit 0beacd0

Please sign in to comment.