Skip to content

Commit

Permalink
Merge pull request #140 from serafuku/yunochi/develop
Browse files Browse the repository at this point in the history
🎨 API 에러시 에러표시 개선
  • Loading branch information
yunochi authored Jan 2, 2025
2 parents a83da46 + ae4a832 commit 57b8cb3
Show file tree
Hide file tree
Showing 14 changed files with 229 additions and 145 deletions.
7 changes: 4 additions & 3 deletions src/app/_components/question.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SubmitHandler, useForm } from 'react-hook-form';
import { RefObject, useEffect, useLayoutEffect, useRef } from 'react';
import { CreateAnswerDto } from '@/app/_dto/answers/create-answer.dto';
import { questionDto } from '@/app/_dto/questions/question.dto';
import { onApiError } from '@/utils/api-error/onApiError';

interface formValue {
answer: string;
Expand Down Expand Up @@ -100,9 +101,8 @@ export default function Question({
visibility: e.visibility,
};
await postAnswer(req);
} catch (err) {
} catch {
answerRef.current?.close();
alert(err);
} finally {
setIsLoading(false);
}
Expand Down Expand Up @@ -254,6 +254,7 @@ async function postAnswer(req: CreateAnswerDto) {
headers: { 'Content-type': 'application/json' },
});
if (!res.ok) {
throw new Error(`답변을 작성하는데 실패했어요!, ${await res.text()}`);
onApiError(res.status, res);
throw new Error();
}
}
17 changes: 17 additions & 0 deletions src/app/main/_events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from '../_dto/websocket-event/websocket-event.dto';
import { NotificationPayloadTypes } from '../_dto/notification/notification.dto';
import { userProfileMeDto } from '@/app/_dto/fetch-profile/Profile.dto';
import { ApiErrorTypes } from '../_dto/api-error/apiErrorTypes';

const QuestionCreateEvent = 'QuestionCreateEvent';
const QuestionDeleteEvent = 'QuestionDeleteEvent';
Expand Down Expand Up @@ -144,3 +145,19 @@ export class MyProfileEv {
window.removeEventListener(ProfileUpdateReqEvent, onEvent as EventListener);
}
}

export type ApiErrorEventValues = { title: string; body: string; buttonText: string; errorType: ApiErrorTypes };
const ApiErrorEvent = 'ApiErrorEvent';
export class ApiErrorEv {
private constructor() {}
static async SendApiErrorEvent(data: ApiErrorEventValues) {
const ev = new CustomEvent<ApiErrorEventValues>(ApiErrorEvent, { bubbles: true, detail: data });
window.dispatchEvent(ev);
}
static addEventListener(onEvent: (ev: CustomEvent<ApiErrorEventValues>) => void) {
window.addEventListener(ApiErrorEvent, onEvent as EventListener);
}
static removeEventListener(onEvent: (ev: CustomEvent<ApiErrorEventValues>) => void) {
window.removeEventListener(ApiErrorEvent, onEvent as EventListener);
}
}
85 changes: 44 additions & 41 deletions src/app/main/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { userProfileMeDto } from '@/app/_dto/fetch-profile/Profile.dto';
import MainHeader from '@/app/main/_header';
import { createContext, useEffect, useRef, useState } from 'react';
import { AnswerWithProfileDto } from '../_dto/answers/Answers.dto';
import { AnswerEv, MyProfileEv, NotificationEv } from './_events';
import { AnswerEv, ApiErrorEv, ApiErrorEventValues, MyProfileEv, NotificationEv } from './_events';
import { NotificationDto, NotificationPayloadTypes } from '../_dto/notification/notification.dto';
import { AnswerCreatedPayload, AnswerDeletedEvPayload } from '@/app/_dto/websocket-event/websocket-event.dto';
import { Logger } from '@/utils/logger/Logger';
Expand All @@ -13,8 +13,8 @@ import DialogModalOneButton from '@/app/_components/modalOneButton';
import { logout } from '@/utils/logout/logout';
import { fetchAllAnswers } from '@/utils/answers/fetchAllAnswers';
import { fetchNoti } from '@/utils/notification/fetchNoti';
import { ApiErrorResponseDto } from '@/app/_dto/api-error/api-error.dto';
import { refreshJwt } from '@/utils/refreshJwt/refresh-jwt-token';
import { onApiError } from '@/utils/api-error/onApiError';

type MainPageContextType = {
answers: AnswerWithProfileDto[] | null;
Expand All @@ -33,45 +33,23 @@ export default function MainLayout({ modal, children }: { children: React.ReactN
const [noti, setNoti] = useState<NotificationDto>();
const [questionsNum, setQuestions_num] = useState<number>(0);
const [loginChecked, setLoginChecked] = useState<boolean>(false);
const [logoutModalText, setLogoutModalText] = useState<string>('');
const forceLogoutModalRef = useRef<HTMLDialogElement>(null);
const apiErrorModalRef = useRef<HTMLDialogElement>(null);
const [apiErrorModalValue, setApiErrorModalValue] = useState<ApiErrorEventValues>({
title: '',
body: '',
buttonText: '',
errorType: 'SERVER_ERROR',
});
const onApiErrorModalClose = useRef<undefined | (() => void)>(undefined);

const onResNotOk = async (code: number, res: Response) => {
try {
const errorRes = (await res.json()) as ApiErrorResponseDto;
switch (errorRes.error_type) {
case 'FORBIDDEN':
setLogoutModalText('요청이 거부 되었어요!');
break;
case 'JWT_EXPIRED':
setLogoutModalText('로그인 인증이 만료되었어요!');
break;
case 'REMOTE_ACCESS_TOKEN_REVOKED':
setLogoutModalText('원격 서버(Misskey/Mastodon...) 에서 인증이 해제 되었어요!');
break;
case 'JWT_REVOKED':
setLogoutModalText('로그인 인증이 해제되었어요!');
break;
case 'UNAUTHORIZED':
setLogoutModalText('인증에 실패했어요!');
break;
default:
setLogoutModalText('');
return;
}
forceLogoutModalRef.current?.showModal();
} catch (err) {
alert(`unknown error! code: ${code}, err: ${String(err)}`);
}
};
// ------------ Initial Fetch -----------------------------
useEffect(() => {
fetchMyProfile(onResNotOk).then((r) => {
fetchMyProfile(onApiError).then((r) => {
setUserProfileData(r);
setQuestions_num(r?.questions ?? 0);
setLoginChecked(true);
});
fetchAllAnswers({ sort: 'DESC', limit: 25 }, onResNotOk).then((r) => {
fetchAllAnswers({ sort: 'DESC', limit: 25 }, onApiError).then((r) => {
if (r.length === 0) {
setLoading(false);
setAnswers([]);
Expand All @@ -80,12 +58,12 @@ export default function MainLayout({ modal, children }: { children: React.ReactN
setAnswers(r);
setUntilId(r[r.length - 1].id);
});
fetchNoti(onResNotOk).then((v) => {
fetchNoti(onApiError).then((v) => {
setNoti(v);
});
const last_token_refresh = Number.parseInt(localStorage.getItem('last_token_refresh') ?? '0');
if (Date.now() / 1000 - last_token_refresh > 3600) {
refreshJwt(onResNotOk);
refreshJwt(onApiError);
}
}, []);

Expand All @@ -96,16 +74,41 @@ export default function MainLayout({ modal, children }: { children: React.ReactN
AnswerEv.addAnswerCreatedEventListener(onAnswerCreated);
AnswerEv.addAnswerDeletedEventListener(onAnswerDeleted);
NotificationEv.addNotificationEventListener(onNotiEv);
ApiErrorEv.addEventListener(onApiErrorEv);
return () => {
MyProfileEv.removeEventListener(onProfileUpdateEvent);
AnswerEv.removeFetchMoreRequestEventListener(onFetchMoreEv);
AnswerEv.removeAnswerCreatedEventListener(onAnswerCreated);
AnswerEv.removeAnswerDeletedEventListener(onAnswerDeleted);
NotificationEv.removeNotificationEventListener(onNotiEv);
ApiErrorEv.removeEventListener(onApiErrorEv);
};
}, []);

// ---------------- event callback functions -------------------
const onApiErrorEv = (ev: CustomEvent<ApiErrorEventValues>) => {
const data = ev.detail;
setApiErrorModalValue({
title: data.title,
body: data.body,
buttonText: data.buttonText,
errorType: data.errorType,
});
switch (data.errorType) {
case 'JWT_EXPIRED':
case 'JWT_REVOKED':
case 'REMOTE_ACCESS_TOKEN_REVOKED':
case 'UNAUTHORIZED':
if (localStorage.getItem('user_handle')) {
onApiErrorModalClose.current = logout;
}
break;
default:
onApiErrorModalClose.current = undefined;
break;
}
apiErrorModalRef.current?.showModal();
};
const onNotiEv = (ev: CustomEvent<NotificationPayloadTypes>) => {
const notiData = ev.detail;
switch (notiData.notification_name) {
Expand Down Expand Up @@ -201,11 +204,11 @@ export default function MainLayout({ modal, children }: { children: React.ReactN
</AnswersContext.Provider>
</MyProfileContext.Provider>
<DialogModalOneButton
title={'자동 로그아웃'}
body={logoutModalText}
buttonText={'확인'}
ref={forceLogoutModalRef}
onClose={logout}
title={apiErrorModalValue.title}
body={apiErrorModalValue.body}
buttonText={apiErrorModalValue.buttonText}
ref={apiErrorModalRef}
onClick={onApiErrorModalClose.current}
/>
</div>
);
Expand Down
38 changes: 8 additions & 30 deletions src/app/main/questions/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import { MyQuestionEv } from '../_events';
import { Logger } from '@/utils/logger/Logger';
import { QuestionDeletedPayload } from '@/app/_dto/websocket-event/websocket-event.dto';
import { MyProfileContext } from '@/app/main/layout';
import { createBlockByQuestionDto } from '@/app/_dto/blocking/blocking.dto';
import { deleteQuestion } from '@/utils/questions/deleteQuestion';
import { createBlock } from '@/utils/block/createBlock';
import { onApiError } from '@/utils/api-error/onApiError';

const fetchQuestions = async (): Promise<questionDto[] | null> => {
const res = await fetch('/api/db/questions');
Expand All @@ -18,39 +20,16 @@ const fetchQuestions = async (): Promise<questionDto[] | null> => {
if (res.status === 401) {
return null;
} else if (!res.ok) {
throw new Error(`내 질문을 불러오는데 실패했어요!: ${await res.text()}`);
onApiError(res.status, res);
return null;
} else {
return await res.json();
}
} catch (err) {
alert(err);
} catch {
return null;
}
};

async function deleteQuestion(id: number) {
const res = await fetch(`/api/db/questions/${id}`, {
method: 'DELETE',
cache: 'no-cache',
});
if (!res.ok) {
throw new Error(`질문을 삭제하는데 실패했어요! ${await res.text()}`);
}
}
async function createBlock(id: number) {
const body: createBlockByQuestionDto = {
questionId: id,
};
const res = await fetch(`/api/user/blocking/create-by-question`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!res.ok) {
throw new Error(`차단에 실패했어요! ${await res.text()}`);
}
}

export default function Questions() {
const [questions, setQuestions] = useState<questionDto[] | null>();
const profile = useContext(MyProfileContext);
Expand Down Expand Up @@ -144,8 +123,7 @@ export default function Questions() {
cancelButtonText={'취소'}
ref={deleteQuestionModalRef}
onClick={() => {
deleteQuestion(id);
setQuestions((prevQuestions) => (prevQuestions ? [...prevQuestions.filter((prev) => prev.id !== id)] : null));
deleteQuestion(id, onApiError);
}}
/>
<DialogModalTwoButton
Expand All @@ -155,7 +133,7 @@ export default function Questions() {
cancelButtonText={'취소'}
ref={createBlockModalRef}
onClick={() => {
createBlock(id);
createBlock(id, onApiError);
}}
/>
</div>
Expand Down
3 changes: 2 additions & 1 deletion src/app/main/settings/_table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import CollapseMenu from '@/app/_components/collapseMenu';
import DialogModalLoadingOneButton from '@/app/_components/modalLoadingOneButton';
import DialogModalTwoButton from '@/app/_components/modalTwoButton';
import { Block, DeleteBlockByIdDto, GetBlockListReqDto, GetBlockListResDto } from '@/app/_dto/blocking/blocking.dto';
import { onApiError } from '@/utils/api-error/onApiError';
import { useEffect, useRef, useState } from 'react';

export default function BlockList() {
Expand Down Expand Up @@ -49,10 +50,10 @@ export default function BlockList() {
const blocklist = ((await res.json()) as GetBlockListResDto).blockList;
return blocklist;
} else {
onApiError(res.status, res);
throw new Error('차단 리스트를 불러오는데 에러가 발생했어요!');
}
} catch (err) {
alert(err);
throw err;
}
};
Expand Down
24 changes: 6 additions & 18 deletions src/app/main/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { MdDeleteSweep, MdOutlineCleaningServices } from 'react-icons/md';
import { MyProfileContext } from '@/app/main/layout';
import { MyProfileEv } from '@/app/main/_events';
import { getProxyUrl } from '@/utils/getProxyUrl/getProxyUrl';
import { onApiError } from '@/utils/api-error/onApiError';

export type FormValue = {
stopAnonQuestion: boolean;
Expand Down Expand Up @@ -48,11 +49,11 @@ async function updateUserSettings(value: FormValue) {
},
});
if (!res.ok) {
throw await res.text();
onApiError(res.status, res);
return;
}
MyProfileEv.SendUpdateReq({ ...body });
} catch (err) {
alert(`설정 업데이트에 실패했어요 ${err}`);
throw err;
}
}
Expand Down Expand Up @@ -111,12 +112,8 @@ export default function Settings() {
if (res.ok) {
localStorage.removeItem('user_handle');
window.location.href = '/';
} else if (res.status === 429) {
alert('요청 제한을 초과했어요. 몇분 후 다시 시도해 주세요');
setButtonClicked(false);
return;
} else {
alert('오류가 발생했어요');
onApiError(res.status, res);
setButtonClicked(false);
return;
}
Expand All @@ -129,7 +126,6 @@ export default function Settings() {
setButtonClicked(true);
const user_handle = userInfo?.handle;
if (!user_handle) {
alert(`오류: 유저 정보를 알 수 없어요!`);
return;
}
const req: AccountCleanReqDto = {
Expand All @@ -142,12 +138,8 @@ export default function Settings() {
});
if (res.ok) {
console.log('계정청소 시작됨...');
} else if (res.status === 429) {
alert('요청 제한을 초과했어요. 잠시 후 다시 시도해 주세요');
setButtonClicked(false);
return;
} else {
alert('오류가 발생했어요');
onApiError(res.status, res);
}
setTimeout(() => {
setButtonClicked(false);
Expand All @@ -161,12 +153,8 @@ export default function Settings() {
});
if (res.ok) {
console.log('블락 리스트 가져오기 시작됨...');
} else if (res.status === 429) {
alert('요청 제한을 초과했어요. 잠시 후 다시 시도해 주세요');
setButtonClicked(false);
return;
} else {
alert(`오류가 발생했어요 ${await res.text()}`);
onApiError(res.status, res);
}
setTimeout(() => {
setButtonClicked(false);
Expand Down
8 changes: 4 additions & 4 deletions src/app/main/social/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import UsernameAndProfile from '@/app/_components/userProfile';
import { FollowingListResDto } from '@/app/_dto/following/following.dto';
import { MyProfileContext } from '@/app/main/layout';
import { onApiError } from '@/utils/api-error/onApiError';
import { useContext, useEffect, useState } from 'react';

export default function Social() {
Expand All @@ -18,13 +19,12 @@ export default function Social() {
body: JSON.stringify({}),
});
if (!res.ok) {
throw new Error('팔로잉 목록을 받아오는데 실패했어요!');
onApiError(res.status, res);
throw new Error();
}
const data = (await res.json()) as FollowingListResDto;
setFollowing(data);
} catch (err) {
alert(err);
}
} catch {}
};
if (profileContext) fn();
}, [profileContext]);
Expand Down
Loading

0 comments on commit 57b8cb3

Please sign in to comment.