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

[Feature] 토스트 컴포넌트 구현 및 에러핸들링 로직 수정 #108

Closed
wants to merge 21 commits into from
Closed
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
7 changes: 5 additions & 2 deletions apps/admin/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import "wowds-ui/styles.css";
import "@wow-class/ui/styles.css";

import { JotaiProvider } from "components/JotaiProvider";
import ToastProvider from "components/ToastProvider";
import type { Metadata } from "next";
import type { ReactNode } from "react";

Expand Down Expand Up @@ -48,8 +49,10 @@ const RootLayout = ({
<html lang="ko">
<body>
<JotaiProvider>
{children}
{modal}
<ToastProvider>
{children}
{modal}
</ToastProvider>
</JotaiProvider>
</body>
</html>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Flex } from "@styled-system/jsx";
import { Text } from "@wow-class/ui";
import { studyApi } from "apis/study/studyApi";
import { tags } from "constants/tags";
import useToast from "hooks/useToast";
import { useState } from "react";
import type { StudyAnnouncementType } from "types/entities/study";
import { revalidateTagByName } from "utils/revalidateTagByName";
Expand All @@ -18,19 +19,24 @@ const CreateStudyAnnouncement = ({ studyId }: { studyId: string }) => {
link: "",
});

const { toast } = useToast();

const handlePublishAnnouncement = async (studyId: string) => {
const { success } = await studyApi.publishStudyAnnouncement(
parseInt(studyId, 10),
studyAnnouncement
);
if (success) {
try {
await studyApi.publishStudyAnnouncement(
parseInt(studyId, 10),
studyAnnouncement
);
toast({ type: "success", text: "공지 생성 성공" });
revalidateTagByName(tags.announcements);
setStudyAnnouncement({
title: "",
link: "",
});
} else {
console.log("공지 생성 실패");
} catch (error) {
if (error instanceof Error) {
toast({ type: "error", text: error.message });
}
}
};
Comment on lines 24 to 41
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3;
해당 api는 fetcher 클래스를 사용하는 것 같은데 response interceptor를 사용해보면 공통으로 에러 메시지 같은 부분을 관리할 수 있을 거 같아요!
불가피하게 fetch를 쓰는 경우에만 위 경우처럼 처리해주면 될 거 같은데 fetcher를 안 쓰고 fetch를 쓰는 경우가 있나요?

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { Flex } from "@styled-system/jsx";
import { studyApi } from "apis/study/studyApi";
import { routerPath } from "constants/router/routerPath";
import useToast from "hooks/useToast";
import Link from "next/link";
import type { StudyAssignmentStatusType } from "types/entities/study";
import Button from "wowds-ui/Button";
Expand All @@ -13,12 +14,16 @@ const AssignmentButtons = ({
studyDetailId: number;
assignmentStatus: StudyAssignmentStatusType;
}) => {
const { toast } = useToast();

const handleCancelAssignment = async () => {
const { success } = await studyApi.cancelAssignment(studyDetailId);
if (success) {
console.log("휴강 처리에 성공했어요.");
} else {
console.log("휴강 처리에 실패했어요.");
try {
await studyApi.cancelAssignment(studyDetailId);
toast({ type: "success", text: "휴강 처리에 성공했어요." });
} catch (error) {
if (error instanceof Error) {
toast({ type: "error", text: error.message });
}
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Flex } from "@styled-system/jsx";
import { Text } from "@wow-class/ui";
import { studyApi } from "apis/study/studyApi";
import { tags } from "constants/tags";
import useToast from "hooks/useToast";
import { useFormContext } from "react-hook-form";
import type {
AssignmentApiRequestDto,
Expand All @@ -25,9 +26,10 @@ const AssignmentHeader = ({ assignment, disabled }: AssignmentHeaderProps) => {
onOpen: () => void;
}
>();

const onOpen = methods.getValues("onOpen");

const { toast } = useToast();

const handleClickSubmit = async () => {
if (assignmentStatus === "CANCELLED") return;

Expand All @@ -37,14 +39,17 @@ const AssignmentHeader = ({ assignment, disabled }: AssignmentHeaderProps) => {
deadLine: methods.getValues("deadLine"),
};

const { success } =
try {
assignmentStatus === "NONE"
? await studyApi.createAssignment(studyDetailId, data)
: await studyApi.patchAssignment(studyDetailId, data);
if (success) {
revalidateTagByName(`${tags.assignments} ${studyDetailId}`);
revalidateTagByName(tags.assignments);
onOpen();
} catch (error) {
if (error instanceof Error) {
toast({ type: "error", text: error.message });
}
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useOpenState } from "@wow-class/ui/hooks";
import { studyApi } from "apis/study/studyApi";
import { assignmentStatusMap } from "constants/assignmentStatusMap";
import { assignmentStatusMap } from "constants/status/assignmentStatusMap";
import { useEffect, useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import type {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import ItemSeparator from "components/ItemSeparator";
import { routerPath } from "constants/router/routerPath";
import { tags } from "constants/tags";
import useParseSearchParams from "hooks/useParseSearchParams";
import useToast from "hooks/useToast";
import { useRouter, useSearchParams } from "next/navigation";
import type { CreateStudyApiRequestDto } from "types/dtos/createStudy";
import { revalidateTagByName } from "utils/revalidateTagByName";
Expand All @@ -26,15 +27,18 @@ const CreatedStudyCheckModal = () => {
const studyName = data.title;
const semester = `${data.academicYear}-${data.semesterType === "FIRST" ? "1" : "2"}`;

const handleClickSubmitButton = async () => {
const result = await createStudyApi.postCreateStudy(data);
const { toast } = useToast();

if (result.success) {
await revalidateTagByName(tags.studyList);
window.alert("스터디 생성에 성공했어요.");
const handleClickSubmitButton = async () => {
try {
await createStudyApi.postCreateStudy(data);
revalidateTagByName(tags.studyList);
toast({ type: "success", text: "스터디 생성에 성공했어요." });
router.push(`${routerPath.root.href}`);
} else {
window.alert("스터디 생성에 실패했어요.");
} catch (error) {
if (error instanceof Error) {
toast({ type: "error", text: error.message });
}
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,19 @@ const useSubmitStudyDetailInfo = (
const router = useRouter();

const handleSubmitDetailInfo = async () => {
const data = await createStudyApi.postStudyDetailInfo(
studyDetailData,
studyId
);
if (data.success) {
try {
await createStudyApi.postStudyDetailInfo(studyDetailData, studyId);
setIsSuccess(true);
const timerId = setTimeout(() => {
router.push(`${routerPath.root.href}/${studyId}`);
}, 500);
return () => clearTimeout(timerId);
} else {
setIsSuccess(false);
window.alert("스터디 상세 정보 저장에 실패했어요.");
router.push(`${routerPath.root.href}`);
} catch (error) {
if (error instanceof Error) {
setIsSuccess(false);
window.alert(error.message);
Comment on lines +24 to +25
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3;
이 부분도 window.alert를 toast로 변경해야 할 거 같네요
어드민에 기존에 사용되던 window.alert 부분을 변경해두면 좋을 거 같습니다!

router.push(`${routerPath.root.href}`);
}
}
};

Expand Down
46 changes: 46 additions & 0 deletions apps/admin/components/Toast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"use client";
import type { ToastProps } from "@wow-class/ui";
import { Toast as ToastUI } from "@wow-class/ui";
import { toastStatusMap } from "constants/status/toastStatusMap";
import { useSetAtom } from "jotai";
import { useEffect, useState } from "react";
import { removeToastAtom } from "store";

const TOAST_DURATION = 2000;
const ANIMATION_DURATION = 200;

const Toast = ({ id, type, text, ...rest }: ToastProps) => {
const [opacity, setOpacity] = useState<number>(0.2);
const removeToastItem = useSetAtom(removeToastAtom);

useEffect(() => {
setOpacity(1);
const timeoutForRemove = setTimeout(() => {
removeToastItem(id);
}, TOAST_DURATION);

const timeoutForVisible = setTimeout(() => {
setOpacity(0);
}, TOAST_DURATION - ANIMATION_DURATION);

return () => {
clearTimeout(timeoutForRemove);
clearTimeout(timeoutForVisible);
};
}, [id, removeToastItem]);

return (
<ToastUI
id={id}
style={{ opacity }}
text={text || toastStatusMap[type]}
transition="opacity"
transitionDelay="0.5"
transitionTimingFunction="ease-in-out"
type={type}
{...rest}
/>
);
};

export default Toast;
33 changes: 33 additions & 0 deletions apps/admin/components/ToastProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"use client";

import { Flex } from "@styled-system/jsx";
import type { ToastProps } from "@wow-class/ui";
import { useAtomValue } from "jotai";
import type { ReactNode } from "react";
import { toastsAtom } from "store";

import Toast from "./Toast";

const ToastProvider = ({ children }: { children: ReactNode }) => {
const toasts = useAtomValue(toastsAtom);

return (
<>
<Flex
direction="column"
gap="sm"
left="50%"
position="absolute"
top={24}
translate="-50%"
>
{toasts?.map((toast: ToastProps) => (
<Toast key={toast.id} {...toast} />
))}
</Flex>
{children}
</>
);
};

export default ToastProvider;
1 change: 1 addition & 0 deletions apps/admin/constants/messages/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const DEFAULT_ERROR_MESSAGE = "에러가 발생했어요.";
6 changes: 6 additions & 0 deletions apps/admin/constants/status/toastStatusMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { DEFAULT_ERROR_MESSAGE } from "constants/messages/error";

export const toastStatusMap: Record<"error" | "success", string> = {
error: DEFAULT_ERROR_MESSAGE,
success: "",
};
12 changes: 12 additions & 0 deletions apps/admin/hooks/useToast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useSetAtom } from "jotai";
import { toastAtom } from "store";

const useToast = () => {
const addToast = useSetAtom(toastAtom);

return {
toast: addToast(),
};
};

export default useToast;
1 change: 1 addition & 0 deletions apps/admin/store/actions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./toastAtoms";
26 changes: 26 additions & 0 deletions apps/admin/store/actions/toastAtoms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { ToastProps } from "@wow-class/ui";
import { atom } from "jotai";

import { toastsAtom } from "../atoms/toastAtoms";

export const toastAtom = atom(
null,
(get, set) =>
(props: Omit<ToastProps, "id"> & Partial<Pick<ToastProps, "id">>) => {
const prevAtom = get(toastsAtom);
const newToast = {
...props,
id: props.id || Date.now().toString(),
};

set(toastsAtom, [...prevAtom, newToast]);
}
);
Comment on lines +6 to +18
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3;
toastAtom보다는 addToastAtom이라는 네이밍이 적합해보입니다


export const removeToastAtom = atom(null, (get, set, id: string) => {
const prev = get(toastsAtom);
set(
toastsAtom,
prev.filter((toast) => toast.id !== id)
);
});
1 change: 1 addition & 0 deletions apps/admin/store/atoms/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./toastAtoms";
4 changes: 4 additions & 0 deletions apps/admin/store/atoms/toastAtoms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { ToastProps } from "@wow-class/ui";
import { atom } from "jotai";

export const toastsAtom = atom<ToastProps[]>([]);
2 changes: 2 additions & 0 deletions apps/admin/store/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./actions";
export * from "./atoms";
5 changes: 4 additions & 1 deletion apps/client/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import "./global.css";
import "wowds-ui/styles.css";
import "@wow-class/ui/styles.css";

import ToastProvider from "components/ToastProvider";
import type { Metadata } from "next";

import { JotaiProvider } from "../components/JotaiProvider";
Expand Down Expand Up @@ -53,7 +54,9 @@ const RootLayout = ({
return (
<html lang="ko">
<body>
<JotaiProvider>{children}</JotaiProvider>
<JotaiProvider>
<ToastProvider>{children}</ToastProvider>
</JotaiProvider>
</body>
</html>
);
Expand Down
Loading
Loading