From 9780bc5a1c68b283c64b20039247fc020522abae Mon Sep 17 00:00:00 2001 From: hyeonjiroh Date: Wed, 16 Apr 2025 20:40:38 +0900 Subject: [PATCH 1/2] refactor: Add HTTP error handling for fetch requests --- src/lib/apis/cardsApi.ts | 22 ++++++++++++++++++++- src/lib/apis/columnsApi.ts | 18 ++++++++++++++++- src/lib/apis/commentsApi.ts | 18 ++++++++++++++++- src/lib/apis/dashboardsApi.ts | 36 ++++++++++++++++++++++++++++++++-- src/lib/apis/invitationsApi.ts | 8 ++++++++ src/lib/apis/membersApi.ts | 10 +++++++++- src/lib/apis/usersApi.ts | 4 ++++ 7 files changed, 110 insertions(+), 6 deletions(-) diff --git a/src/lib/apis/cardsApi.ts b/src/lib/apis/cardsApi.ts index 8903a56..fb7ae47 100644 --- a/src/lib/apis/cardsApi.ts +++ b/src/lib/apis/cardsApi.ts @@ -24,6 +24,10 @@ export async function fetchTaskCardList({ cache: "no-store", }); + if (!res.ok) { + throw new Error(`Error ${res.status}: ${res.statusText}`); + } + return res.json(); } @@ -42,6 +46,10 @@ export async function fetchTaskCardDetail({ cache: "no-store", }); + if (!res.ok) { + throw new Error(`Error ${res.status}: ${res.statusText}`); + } + return res.json(); } @@ -84,6 +92,10 @@ export async function putCard({ }), }); + if (!res.ok) { + throw new Error(`Error ${res.status}: ${res.statusText}`); + } + return res.json(); } @@ -94,7 +106,7 @@ export async function deleteCard({ token: string; cardId: number; }) { - await fetch(`${BASE_URL}/cards/${cardId}`, { + const res = await fetch(`${BASE_URL}/cards/${cardId}`, { method: "DELETE", headers: { Accept: "application/json", @@ -102,6 +114,10 @@ export async function deleteCard({ }, }); + if (!res.ok) { + throw new Error(`Error ${res.status}: ${res.statusText}`); + } + return null; } @@ -161,5 +177,9 @@ export async function createCard({ body: JSON.stringify(payload), }); + if (!res.ok) { + throw new Error(`Error ${res.status}: ${res.statusText}`); + } + return await res.json(); } diff --git a/src/lib/apis/columnsApi.ts b/src/lib/apis/columnsApi.ts index af10b62..61ab024 100644 --- a/src/lib/apis/columnsApi.ts +++ b/src/lib/apis/columnsApi.ts @@ -15,6 +15,10 @@ export async function fetchColumnList({ cache: "no-store", }); + if (!res.ok) { + throw new Error(`Error ${res.status}: ${res.statusText}`); + } + return res.json(); } @@ -40,6 +44,10 @@ export async function postColumn({ }), }); + if (!res.ok) { + throw new Error(`Error ${res.status}: ${res.statusText}`); + } + return res.json(); } @@ -64,6 +72,10 @@ export async function putColumn({ }), }); + if (!res.ok) { + throw new Error(`Error ${res.status}: ${res.statusText}`); + } + return res.json(); } @@ -74,7 +86,7 @@ export async function deleteColumn({ token: string; columnId: number; }) { - await fetch(`${BASE_URL}/columns/${columnId}`, { + const res = await fetch(`${BASE_URL}/columns/${columnId}`, { method: "DELETE", headers: { Accept: "application/json", @@ -82,5 +94,9 @@ export async function deleteColumn({ }, }); + if (!res.ok) { + throw new Error(`Error ${res.status}: ${res.statusText}`); + } + return null; } diff --git a/src/lib/apis/commentsApi.ts b/src/lib/apis/commentsApi.ts index 123e495..88d282e 100644 --- a/src/lib/apis/commentsApi.ts +++ b/src/lib/apis/commentsApi.ts @@ -24,6 +24,10 @@ export async function fetchCommentList({ cache: "no-store", }); + if (!res.ok) { + throw new Error(`Error ${res.status}: ${res.statusText}`); + } + return res.json(); } @@ -55,6 +59,10 @@ export async function postComment({ }), }); + if (!res.ok) { + throw new Error(`Error ${res.status}: ${res.statusText}`); + } + return res.json(); } @@ -79,6 +87,10 @@ export async function putComment({ }), }); + if (!res.ok) { + throw new Error(`Error ${res.status}: ${res.statusText}`); + } + return res.json(); } @@ -89,7 +101,7 @@ export async function deleteComment({ token: string; commentId: number; }) { - await fetch(`${BASE_URL}/comments/${commentId}`, { + const res = await fetch(`${BASE_URL}/comments/${commentId}`, { method: "DELETE", headers: { Accept: "application/json", @@ -97,5 +109,9 @@ export async function deleteComment({ }, }); + if (!res.ok) { + throw new Error(`Error ${res.status}: ${res.statusText}`); + } + return null; } diff --git a/src/lib/apis/dashboardsApi.ts b/src/lib/apis/dashboardsApi.ts index b18f5d0..2e38148 100644 --- a/src/lib/apis/dashboardsApi.ts +++ b/src/lib/apis/dashboardsApi.ts @@ -20,6 +20,10 @@ export async function fetchDashboardList({ } ); + if (!res.ok) { + throw new Error(`Error ${res.status}: ${res.statusText}`); + } + return res.json(); } @@ -38,6 +42,10 @@ export async function fetchDashboard({ cache: "no-store", }); + if (!res.ok) { + throw new Error(`Error ${res.status}: ${res.statusText}`); + } + return res.json(); } @@ -63,6 +71,10 @@ export async function postDashboard({ }), }); + if (!res.ok) { + throw new Error(`Error ${res.status}: ${res.statusText}`); + } + return res.json(); } @@ -90,6 +102,10 @@ export async function putDashboard({ }), }); + if (!res.ok) { + throw new Error(`Error ${res.status}: ${res.statusText}`); + } + return res.json(); } @@ -100,7 +116,7 @@ export async function deleteDashboard({ token: string; id: number; }) { - await fetch(`${BASE_URL}/dashboards/${id}`, { + const res = await fetch(`${BASE_URL}/dashboards/${id}`, { method: "DELETE", headers: { Accept: "application/json", @@ -108,6 +124,10 @@ export async function deleteDashboard({ }, }); + if (!res.ok) { + throw new Error(`Error ${res.status}: ${res.statusText}`); + } + return null; } @@ -133,6 +153,10 @@ export async function fetchInvitationList({ } ); + if (!res.ok) { + throw new Error(`Error ${res.status}: ${res.statusText}`); + } + return res.json(); } @@ -157,6 +181,10 @@ export async function postInvitation({ }), }); + if (!res.ok) { + throw new Error(`Error ${res.status}: ${res.statusText}`); + } + return res.json(); } @@ -169,7 +197,7 @@ export async function deleteInvitation({ dashboardId: number; invitationId: number; }) { - await fetch( + const res = await fetch( `${BASE_URL}/dashboards/${dashboardId}/invitations/${invitationId}`, { method: "DELETE", @@ -180,5 +208,9 @@ export async function deleteInvitation({ } ); + if (!res.ok) { + throw new Error(`Error ${res.status}: ${res.statusText}`); + } + return null; } diff --git a/src/lib/apis/invitationsApi.ts b/src/lib/apis/invitationsApi.ts index a93330b..04b643f 100644 --- a/src/lib/apis/invitationsApi.ts +++ b/src/lib/apis/invitationsApi.ts @@ -26,6 +26,10 @@ export async function fetchInvitationList({ cache: "no-store", }); + if (!res.ok) { + throw new Error(`Error ${res.status}: ${res.statusText}`); + } + return res.json(); } @@ -50,5 +54,9 @@ export async function putInvitation({ }), }); + if (!res.ok) { + throw new Error(`Error ${res.status}: ${res.statusText}`); + } + return res.json(); } diff --git a/src/lib/apis/membersApi.ts b/src/lib/apis/membersApi.ts index 345715d..71f8397 100644 --- a/src/lib/apis/membersApi.ts +++ b/src/lib/apis/membersApi.ts @@ -24,6 +24,10 @@ export async function fetchDashboardMember({ cache: "no-store", }); + if (!res.ok) { + throw new Error(`Error ${res.status}: ${res.statusText}`); + } + return res.json(); } @@ -34,7 +38,7 @@ export async function deleteDashboardMember({ token: string; memberId: number; }) { - await fetch(`${BASE_URL}/members/${memberId}`, { + const res = await fetch(`${BASE_URL}/members/${memberId}`, { method: "DELETE", headers: { Accept: "application/json", @@ -42,5 +46,9 @@ export async function deleteDashboardMember({ }, }); + if (!res.ok) { + throw new Error(`Error ${res.status}: ${res.statusText}`); + } + return null; } diff --git a/src/lib/apis/usersApi.ts b/src/lib/apis/usersApi.ts index 62b2276..20739b0 100644 --- a/src/lib/apis/usersApi.ts +++ b/src/lib/apis/usersApi.ts @@ -9,5 +9,9 @@ export async function fetchUser({ token }: { token: string }) { cache: "no-store", }); + if (!res.ok) { + throw new Error(`Error ${res.status}: ${res.statusText}`); + } + return res.json(); } From f89af8119a8834f2df316fac1618e2e53d06752f Mon Sep 17 00:00:00 2001 From: hyeonjiroh Date: Wed, 16 Apr 2025 22:00:55 +0900 Subject: [PATCH 2/2] refactor: add loading and error handling to data fetching --- .../[dashboardid]/_components/Column.tsx | 2 + .../edit/_components/DashboardEditSection.tsx | 56 ++++++++++++------- .../edit/_components/DeleteButton.tsx | 25 ++++++--- .../edit/_components/InvitationCard.tsx | 25 ++++++--- .../edit/_components/InvitationSection.tsx | 27 ++++++--- .../_components/InvitationCard.tsx | 53 +++++++++++++----- src/components/common/alert/AlertProvider.tsx | 15 +++-- src/components/common/modal/MenuButton.tsx | 22 +++++--- src/components/layout/navbar/UserMenu.tsx | 13 ++++- .../layout/sidebar/SideMenuList.tsx | 2 + .../modal/add-column/AddColumnModal.tsx | 44 ++++++++++----- .../create-dashboard/CreateDashboardModal.tsx | 50 +++++++++++------ .../modal/edit-task/AssigneeDropdown.tsx | 25 ++++++--- .../modal/edit-task/ColumnDropdown.tsx | 26 ++++++--- .../modal/editColumn/EditColumnModal.tsx | 44 ++++++++++----- src/components/modal/invite/InviteModal.tsx | 48 ++++++++++------ .../modal/task-detail/CommentCard.tsx | 45 ++++++++++----- .../modal/task-detail/CommentList.tsx | 2 + .../modal/task-detail/TaskCommentSection.tsx | 27 ++++++--- .../modal/task-detail/TaskDetailModal.tsx | 2 + 20 files changed, 384 insertions(+), 169 deletions(-) diff --git a/src/app/(after-login)/dashboard/[dashboardid]/_components/Column.tsx b/src/app/(after-login)/dashboard/[dashboardid]/_components/Column.tsx index 9505002..fd181ca 100644 --- a/src/app/(after-login)/dashboard/[dashboardid]/_components/Column.tsx +++ b/src/app/(after-login)/dashboard/[dashboardid]/_components/Column.tsx @@ -42,6 +42,8 @@ export default function Column({ id, title }: DashboardColumn) { if (newCards.length < PAGE_SIZE || nextCursorId === null) { setIsLast(true); } + } catch (error) { + console.error("Failed to load card list :", error); } finally { setIsLoading(false); } diff --git a/src/app/(after-login)/dashboard/[dashboardid]/edit/_components/DashboardEditSection.tsx b/src/app/(after-login)/dashboard/[dashboardid]/edit/_components/DashboardEditSection.tsx index 40d5547..7e7897b 100644 --- a/src/app/(after-login)/dashboard/[dashboardid]/edit/_components/DashboardEditSection.tsx +++ b/src/app/(after-login)/dashboard/[dashboardid]/edit/_components/DashboardEditSection.tsx @@ -18,23 +18,32 @@ export default function DashboardEditSection({ token: string; }) { const [data, setData] = useState(null); + const [loading, setLoading] = useState(false); const [dashboardName, setDashboardName] = useState(""); const [selectedColor, setSelectedColor] = useState(""); const [isFormValid, setIsFormValid] = useState(false); const setDashboardId = useDashboardStore((state) => state.setDashboardId); useEffect(() => { - const getData = async () => { - const data = await fetchDashboard({ - token, - id: String(id), - }); - setData(data); - setDashboardName(data.title); - setSelectedColor(data.color); - }; + setLoading(true); - getData(); + try { + const getData = async () => { + const data = await fetchDashboard({ + token, + id: String(id), + }); + setData(data); + setDashboardName(data.title); + setSelectedColor(data.color); + }; + + getData(); + } catch (error) { + console.error("Failed to load dashboard:", error); + } finally { + setLoading(false); + } }, []); const onColorSelect = (color: ColorCode | "") => { @@ -54,18 +63,27 @@ export default function DashboardEditSection({ }; const editDashboard = async () => { - await putDashboard({ - token, - title: dashboardName, - color: selectedColor, - id, - }); + setLoading(true); + + try { + await putDashboard({ + token, + title: dashboardName, + color: selectedColor, + id, + }); - window.location.replace(`/dashboard/${id}`); - setDashboardId(String(id)); + window.location.replace(`/dashboard/${id}`); + setDashboardId(String(id)); + } catch (error) { + console.error("Failed to edit dashboard :", error); + } finally { + setLoading(false); + } }; if (!data) return; + if (loading) return

Loading...

; return (
@@ -91,7 +109,7 @@ export default function DashboardEditSection({ onClick={editDashboard} disabled={!isFormValid} > - 변경 + {!loading && "변경"}
diff --git a/src/app/(after-login)/dashboard/[dashboardid]/edit/_components/DeleteButton.tsx b/src/app/(after-login)/dashboard/[dashboardid]/edit/_components/DeleteButton.tsx index a1f94b5..79bb00b 100644 --- a/src/app/(after-login)/dashboard/[dashboardid]/edit/_components/DeleteButton.tsx +++ b/src/app/(after-login)/dashboard/[dashboardid]/edit/_components/DeleteButton.tsx @@ -1,5 +1,6 @@ "use client"; +import { useState } from "react"; import { useRouter } from "next/navigation"; import { useDashboardStore } from "@/lib/store/useDashboardStore"; import { deleteDashboard } from "@/lib/apis/dashboardsApi"; @@ -12,17 +13,27 @@ export default function DeleteButton({ id: number; token: string; }) { + const [loading, setLoading] = useState(false); + const router = useRouter(); const setDashboardId = useDashboardStore((state) => state.setDashboardId); const handleDeleteClick = async () => { - await deleteDashboard({ - token, - id, - }); + setLoading(true); + + try { + await deleteDashboard({ + token, + id, + }); - router.push(ROUTE.MYDASHBOARD); - setDashboardId(null); + router.push(ROUTE.MYDASHBOARD); + setDashboardId(null); + } catch (error) { + console.error("Failed to delete dashboard :", error); + } finally { + setLoading(false); + } }; return ( @@ -31,7 +42,7 @@ export default function DeleteButton({ onClick={handleDeleteClick} className="max-w-[320px] h-[52px] rounded-lg border border-gray-400 bg-gray-200 font-medium text-lg text-gray-800 tablet:h-[62px] tablet:text-2lg hover:bg-gray-300" > - 대시보드 삭제하기 + {!loading && "대시보드 삭제하기"} ); } diff --git a/src/app/(after-login)/dashboard/[dashboardid]/edit/_components/InvitationCard.tsx b/src/app/(after-login)/dashboard/[dashboardid]/edit/_components/InvitationCard.tsx index b61963a..0023f6d 100644 --- a/src/app/(after-login)/dashboard/[dashboardid]/edit/_components/InvitationCard.tsx +++ b/src/app/(after-login)/dashboard/[dashboardid]/edit/_components/InvitationCard.tsx @@ -1,3 +1,4 @@ +import { useState } from "react"; import { Invitation } from "@/lib/types"; import { deleteInvitation } from "@/lib/apis/dashboardsApi"; import Button from "@/components/common/button/Button"; @@ -12,14 +13,24 @@ export default function InvitationCard({ invitee, token, }: InvitationCardProps) { + const [loading, setLoading] = useState(false); + const handleDeleteClick = async () => { - await deleteInvitation({ - token, - dashboardId: dashboard.id, - invitationId: id, - }); + setLoading(true); + + try { + await deleteInvitation({ + token, + dashboardId: dashboard.id, + invitationId: id, + }); - window.location.reload(); + window.location.reload(); + } catch (error) { + console.error("Failed to delete invitation :", error); + } finally { + setLoading(false); + } }; return ( @@ -31,7 +42,7 @@ export default function InvitationCard({ onClick={handleDeleteClick} className="w-[52px] max-h-[32px] tablet:w-[84px]" > - 취소 + {!loading && "취소"} diff --git a/src/app/(after-login)/dashboard/[dashboardid]/edit/_components/InvitationSection.tsx b/src/app/(after-login)/dashboard/[dashboardid]/edit/_components/InvitationSection.tsx index 051c730..3956117 100644 --- a/src/app/(after-login)/dashboard/[dashboardid]/edit/_components/InvitationSection.tsx +++ b/src/app/(after-login)/dashboard/[dashboardid]/edit/_components/InvitationSection.tsx @@ -17,19 +17,28 @@ export default function InvitationSection({ token: string; }) { const [items, setItems] = useState([]); + const [loading, setLoading] = useState(false); const [page, setPage] = useState(1); const [totalCount, setTotalCount] = useState(0); const totalPage = Math.ceil(totalCount / PAGE_SIZE); const handleLoad = async () => { - const { invitations, totalCount } = await fetchInvitationList({ - token, - id, - page, - size: PAGE_SIZE, - }); - setItems(invitations); - setTotalCount(totalCount); + setLoading(true); + + try { + const { invitations, totalCount } = await fetchInvitationList({ + token, + id, + page, + size: PAGE_SIZE, + }); + setItems(invitations); + setTotalCount(totalCount); + } catch (error) { + console.error("Failed to load invitation:", error); + } finally { + setLoading(false); + } }; useEffect(() => { @@ -48,6 +57,8 @@ export default function InvitationSection({ } }; + if (loading) return

Loading...

; + return (
diff --git a/src/app/(after-login)/mydashboard/_components/InvitationCard.tsx b/src/app/(after-login)/mydashboard/_components/InvitationCard.tsx index edd884c..3974075 100644 --- a/src/app/(after-login)/mydashboard/_components/InvitationCard.tsx +++ b/src/app/(after-login)/mydashboard/_components/InvitationCard.tsx @@ -1,3 +1,4 @@ +import { useState } from "react"; import { useRouter } from "next/navigation"; import { useDashboardStore } from "@/lib/store/useDashboardStore"; import { Invitation } from "@/lib/types"; @@ -14,29 +15,47 @@ export default function InvitationCard({ dashboard, token, }: InvitationCardProps) { + const [loading, setLoading] = useState(false); + const newDashboardId = String(dashboard.id); const router = useRouter(); const setDashboardId = useDashboardStore((state) => state.setDashboardId); const handleApproveInvite = async () => { - await putInvitation({ - token, - invitationId: id, - inviteAccepted: true, - }); + setLoading(true); + + try { + await putInvitation({ + token, + invitationId: id, + inviteAccepted: true, + }); - router.push(`/dashboard/${newDashboardId}`); - setDashboardId(newDashboardId); + router.push(`/dashboard/${newDashboardId}`); + setDashboardId(newDashboardId); + } catch (error) { + console.error("Failed to approve invitation :", error); + } finally { + setLoading(false); + } }; const handleRejectInvite = async () => { - await putInvitation({ - token, - invitationId: id, - inviteAccepted: false, - }); + setLoading(true); + + try { + await putInvitation({ + token, + invitationId: id, + inviteAccepted: false, + }); - window.location.reload(); + window.location.reload(); + } catch (error) { + console.error("Failed to reject invitation :", error); + } finally { + setLoading(false); + } }; return ( @@ -66,7 +85,9 @@ export default function InvitationCard({ radius="sm" className="flex-1 max-h-[32px] tablet:max-w-[72px] pc:max-w-[84px]" > -
수락
+
+ {!loading && "수락"} +
diff --git a/src/components/common/alert/AlertProvider.tsx b/src/components/common/alert/AlertProvider.tsx index fbd37ce..5738d05 100644 --- a/src/components/common/alert/AlertProvider.tsx +++ b/src/components/common/alert/AlertProvider.tsx @@ -15,11 +15,16 @@ export default function AlertProvider() { const handleDeleteClick = async () => { if (!accessToken || !selectedColumnId) return; - await deleteColumn({ - token: accessToken, - columnId: Number(selectedColumnId), - }); - router.refresh(); + + try { + await deleteColumn({ + token: accessToken, + columnId: Number(selectedColumnId), + }); + router.refresh(); + } catch (error) { + console.error("Failed to delete column :", error); + } }; return ( diff --git a/src/components/common/modal/MenuButton.tsx b/src/components/common/modal/MenuButton.tsx index 6bff456..57b9c9a 100644 --- a/src/components/common/modal/MenuButton.tsx +++ b/src/components/common/modal/MenuButton.tsx @@ -6,6 +6,7 @@ import MenuButtonIcon from "../../../../public/icon/menu_icon.svg"; import { useTaskStore } from "@/lib/store/useTaskStore"; export default function MenuButton() { + const [loading, setLoading] = useState(false); const [isOpen, setIsOpen] = useState(false); const { openModal, closeModal } = useModalStore(); const { selectedTaskId } = useTaskStore(); @@ -18,14 +19,21 @@ export default function MenuButton() { const handleDelete = async () => { if (!selectedTaskId) return; + setLoading(true); - await deleteCard({ - token: accessToken, - cardId: selectedTaskId, - }); + try { + await deleteCard({ + token: accessToken, + cardId: selectedTaskId, + }); - closeModal(); - window.location.reload(); + closeModal(); + window.location.reload(); + } catch (error) { + console.error("Failed to delete card :", error); + } finally { + setLoading(false); + } }; return ( @@ -50,7 +58,7 @@ export default function MenuButton() { onClick={handleDelete} className="w-[81px] h-8 rounded font-normal text-md text-gray-800 hover:text-violet hover:bg-violet-8" > - 삭제하기 + {!loading && "삭제하기"} )} diff --git a/src/components/layout/navbar/UserMenu.tsx b/src/components/layout/navbar/UserMenu.tsx index d283326..bc9d828 100644 --- a/src/components/layout/navbar/UserMenu.tsx +++ b/src/components/layout/navbar/UserMenu.tsx @@ -1,4 +1,5 @@ "use client"; + import Cookies from "js-cookie"; import { useEffect, useState } from "react"; import { UserInfo } from "@/lib/types"; @@ -11,6 +12,7 @@ import ROUTE from "@/lib/constants/route"; export default function UserMenu() { const [data, setData] = useState(null); + const [loading, setLoading] = useState(false); const [isOpen, setIsOpen] = useState(false); const isMobile = useIsMobile(); const router = useRouter(); @@ -20,16 +22,25 @@ export default function UserMenu() { useEffect(() => { const getData = async () => { - if (accessToken) { + if (!accessToken) return; + setLoading(true); + + try { const res = await fetchUser({ token: accessToken, }); setData(res); + } catch (error) { + console.error("Failed to load user:", error); + } finally { + setLoading(false); } }; + getData(); }, [accessToken]); + if (loading) return

Loading...

; if (!data) return null; const { nickname, profileImageUrl } = data; diff --git a/src/components/layout/sidebar/SideMenuList.tsx b/src/components/layout/sidebar/SideMenuList.tsx index 44fa695..c046405 100644 --- a/src/components/layout/sidebar/SideMenuList.tsx +++ b/src/components/layout/sidebar/SideMenuList.tsx @@ -31,6 +31,8 @@ export default function SideMenuList() { setItems((prev) => [...prev, ...newDashboards]); setPage((prev) => prev + 1); } + } catch (error) { + console.error("Failed to load dashboard list :", error); } finally { setIsLoading(false); } diff --git a/src/components/modal/add-column/AddColumnModal.tsx b/src/components/modal/add-column/AddColumnModal.tsx index 40ee732..0bd0b51 100644 --- a/src/components/modal/add-column/AddColumnModal.tsx +++ b/src/components/modal/add-column/AddColumnModal.tsx @@ -13,6 +13,7 @@ export interface ColumnListResponse { export default function CreateDashboardModal() { const [columnList, setColumnList] = useState([]); + const [loading, setLoading] = useState(false); const [inputValue, setInputValue] = useState(""); const [isDuplicate, setIsDuplicate] = useState(false); const [isFormValid, setIsFormValid] = useState(false); @@ -22,16 +23,23 @@ export default function CreateDashboardModal() { useEffect(() => { if (!dashboardId) return; + setLoading(true); - const getData = async () => { - const res = await fetchColumnList({ - token: accessToken, - id: dashboardId, - }); - setColumnList(res.data); - }; + try { + const getData = async () => { + const res = await fetchColumnList({ + token: accessToken, + id: dashboardId, + }); + setColumnList(res.data); + }; - getData(); + getData(); + } catch (error) { + console.error("Failed to load column list :", error); + } finally { + setLoading(false); + } }, [dashboardId]); useEffect(() => { @@ -51,17 +59,25 @@ export default function CreateDashboardModal() { const buttonClick = async () => { if (!dashboardId) return; + setLoading(true); - await postColumn({ - token: accessToken, - title: inputValue, - dashboardId: Number(dashboardId), - }); + try { + await postColumn({ + token: accessToken, + title: inputValue, + dashboardId: Number(dashboardId), + }); - router.refresh(); + router.refresh(); + } catch (error) { + console.error("Failed to post column :", error); + } finally { + setLoading(false); + } }; if (!dashboardId) return; + if (loading) return

Loading...

; return ( (""); const [isFormValid, setIsFormValid] = useState(false); + const [loading, setLoading] = useState(false); const router = useRouter(); const setDashboardId = useDashboardStore((state) => state.setDashboardId); const accessToken = localStorage.getItem("accessToken") ?? ""; @@ -33,16 +34,24 @@ export default function CreateDashboardModal() { }; const createDashboard = async () => { - const res = await postDashboard({ - token: accessToken, - title: dashboardName, - color: selectedColor, - }); + setLoading(true); + + try { + const res = await postDashboard({ + token: accessToken, + title: dashboardName, + color: selectedColor, + }); - const newDashboardId = res.id; + const newDashboardId = res.id; - router.push(`/dashboard/${newDashboardId}`); - setDashboardId(newDashboardId); + router.push(`/dashboard/${newDashboardId}`); + setDashboardId(newDashboardId); + } catch (error) { + console.error("Failed to create dashboard :", error); + } finally { + setLoading(false); + } }; return ( @@ -52,15 +61,22 @@ export default function CreateDashboardModal() { disabled: !isFormValid, }} > -
- - -
+ {loading ? ( +

Loading...

+ ) : ( +
+ + +
+ )}
); } diff --git a/src/components/modal/edit-task/AssigneeDropdown.tsx b/src/components/modal/edit-task/AssigneeDropdown.tsx index 7e62b3f..cd2ede6 100644 --- a/src/components/modal/edit-task/AssigneeDropdown.tsx +++ b/src/components/modal/edit-task/AssigneeDropdown.tsx @@ -21,22 +21,33 @@ export default function AssigneeDropdown({ const [members, setMembers] = useState< { userId: number; nickname: string; profileImageUrl: string | null }[] >([]); + const [loading, setLoading] = useState(false); const [isOpen, setIsOpen] = useState(false); useEffect(() => { const getData = async () => { - const res = await fetchDashboardMember({ - token, - page: 1, - size: 20, - id: dashboardId, - }); - setMembers(res.members); + setLoading(true); + + try { + const res = await fetchDashboardMember({ + token, + page: 1, + size: 20, + id: dashboardId, + }); + setMembers(res.members); + } catch (error) { + console.error("Failed to load members:", error); + } finally { + setLoading(false); + } }; getData(); }, []); + if (loading) return

Loading...

; + const selected = members.find((member) => member.userId === memberId); return ( diff --git a/src/components/modal/edit-task/ColumnDropdown.tsx b/src/components/modal/edit-task/ColumnDropdown.tsx index e72021a..a4e8078 100644 --- a/src/components/modal/edit-task/ColumnDropdown.tsx +++ b/src/components/modal/edit-task/ColumnDropdown.tsx @@ -18,21 +18,31 @@ export default function ColumnDropdown({ onChange, }: ColumnDropdownProps) { const [columns, setColumns] = useState<{ id: number; title: string }[]>([]); + const [loading, setLoading] = useState(false); const [isOpen, setIsOpen] = useState(false); useEffect(() => { - const getData = async () => { - const res = await fetchColumnList({ - token, - id: dashboardId, - }); - setColumns(res.data); - }; + setLoading(true); - getData(); + try { + const getData = async () => { + const res = await fetchColumnList({ + token, + id: dashboardId, + }); + setColumns(res.data); + }; + + getData(); + } catch (error) { + console.error("Failed to load column list :", error); + } finally { + setLoading(false); + } }, []); const selected = columns.find((col) => col.id === columnId); + if (loading) return

Loading...

; return (
diff --git a/src/components/modal/editColumn/EditColumnModal.tsx b/src/components/modal/editColumn/EditColumnModal.tsx index 8fdf3f9..931fbcd 100644 --- a/src/components/modal/editColumn/EditColumnModal.tsx +++ b/src/components/modal/editColumn/EditColumnModal.tsx @@ -15,6 +15,7 @@ export interface ColumnListResponse { export default function CreateDashboardModal() { const { selectedColumnId, selectedColumnTitle } = useColumnStore(); const [columnList, setColumnList] = useState([]); + const [loading, setLoading] = useState(false); const [inputValue, setInputValue] = useState(selectedColumnTitle ?? ""); const [isDuplicate, setIsDuplicate] = useState(false); const [isFormValid, setIsFormValid] = useState(false); @@ -24,16 +25,23 @@ export default function CreateDashboardModal() { useEffect(() => { if (!dashboardId) return; + setLoading(true); - const getData = async () => { - const res = await fetchColumnList({ - token: accessToken, - id: dashboardId, - }); - setColumnList(res.data); - }; + try { + const getData = async () => { + const res = await fetchColumnList({ + token: accessToken, + id: dashboardId, + }); + setColumnList(res.data); + }; - getData(); + getData(); + } catch (error) { + console.error("Failed to load column list :", error); + } finally { + setLoading(false); + } }, [dashboardId]); useEffect(() => { @@ -55,17 +63,25 @@ export default function CreateDashboardModal() { const handleEditClick = async () => { if (!selectedColumnId) return; + setLoading(true); - await putColumn({ - token: accessToken, - title: inputValue, - columnId: Number(selectedColumnId), - }); + try { + await putColumn({ + token: accessToken, + title: inputValue, + columnId: Number(selectedColumnId), + }); - router.refresh(); + router.refresh(); + } catch (error) { + console.error("Failed to edit column :", error); + } finally { + setLoading(false); + } }; if (!dashboardId) return; + if (loading) return

Loading...

; return ( { if (!dashboardId) return; + setLoading(true); - postInvitation({ - token: accessToken, - id: Number(dashboardId), - email: inputValue, - }); + try { + postInvitation({ + token: accessToken, + id: Number(dashboardId), + email: inputValue, + }); - router.refresh(); + router.refresh(); + } catch (error) { + console.error("Failed to invite :", error); + } finally { + setLoading(false); + } }; if (!dashboardId) return; @@ -50,16 +58,22 @@ export default function CreateDashboardModal() { disabled: !isFormValid, }} > -
- -
+ {loading ? ( +

Loading...

+ ) : ( +
+ +
+ )}
); } diff --git a/src/components/modal/task-detail/CommentCard.tsx b/src/components/modal/task-detail/CommentCard.tsx index 56c7abf..4cf1550 100644 --- a/src/components/modal/task-detail/CommentCard.tsx +++ b/src/components/modal/task-detail/CommentCard.tsx @@ -21,6 +21,7 @@ export default function CommentCard({ const [isEditMode, setIsEditMode] = useState(false); const [inputValue, setInputValue] = useState(content); const [isFormValid, setIsFormValid] = useState(false); + const [loading, setLoading] = useState(false); const isMobile = useIsMobile(); const date = formatDate(createdAt, true); const accessToken = localStorage.getItem("accessToken") ?? ""; @@ -39,23 +40,39 @@ export default function CommentCard({ }; const handleEditComment = async () => { - await putComment({ - token: accessToken, - content: inputValue.trim(), - commentId: id, - }); + setLoading(true); - setInputValue(""); - onChange(); + try { + await putComment({ + token: accessToken, + content: inputValue.trim(), + commentId: id, + }); + + setInputValue(""); + onChange(); + } catch (error) { + console.error("Failed to edit comment :", error); + } finally { + setLoading(false); + } }; const handleDeleteComment = async () => { - await deleteComment({ - token: accessToken, - commentId: id, - }); + setLoading(true); + + try { + await deleteComment({ + token: accessToken, + commentId: id, + }); - onChange(); + onChange(); + } catch (error) { + console.error("Failed to delete comment :", error); + } finally { + setLoading(false); + } }; return ( @@ -95,7 +112,7 @@ export default function CommentCard({ className="w-[84px] max-h-[28px] absolute bottom-3 right-3 font-medium text-xs leading-[18px] tablet:w-[78px] tablet:h-[32px]" disabled={!isFormValid} > - 등록 + {!loading && "등록"}
) : ( @@ -115,7 +132,7 @@ export default function CommentCard({ onClick={handleDeleteComment} className="font-normal text-[10px] text-gray-500 underline cursor-pointer tablet:text-xs" > - 삭제 + {!loading && "삭제"} diff --git a/src/components/modal/task-detail/CommentList.tsx b/src/components/modal/task-detail/CommentList.tsx index 76558a4..ef74a5f 100644 --- a/src/components/modal/task-detail/CommentList.tsx +++ b/src/components/modal/task-detail/CommentList.tsx @@ -39,6 +39,8 @@ export default function CommentList({ if (newComments.length < PAGE_SIZE || nextCursorId === null) { setIsLast(true); } + } catch (error) { + console.error("Failed to load comments:", error); } finally { setIsLoading(false); } diff --git a/src/components/modal/task-detail/TaskCommentSection.tsx b/src/components/modal/task-detail/TaskCommentSection.tsx index 3397b22..abc7efc 100644 --- a/src/components/modal/task-detail/TaskCommentSection.tsx +++ b/src/components/modal/task-detail/TaskCommentSection.tsx @@ -14,6 +14,7 @@ export default function TaskCommentSection({ }) { const [inputValue, setInputValue] = useState(""); const [isFormValid, setIsFormValid] = useState(false); + const [loading, setLoading] = useState(false); const [commentListKey, setCommentListKey] = useState(0); const { dashboardId } = useDashboardStore(); const accessToken = localStorage.getItem("accessToken") ?? ""; @@ -29,20 +30,28 @@ export default function TaskCommentSection({ const buttonClick = async () => { if (!dashboardId) return; + setLoading(true); - await postComment({ - token: accessToken, - content: inputValue.trim(), - cardId: cardId, - columnId: columnId, - dashboardId: Number(dashboardId), - }); + try { + await postComment({ + token: accessToken, + content: inputValue.trim(), + cardId: cardId, + columnId: columnId, + dashboardId: Number(dashboardId), + }); - setInputValue(""); - setCommentListKey((prev) => prev + 1); // key 값 변경해서 CommentList 다시 마운트 + setInputValue(""); + setCommentListKey((prev) => prev + 1); // key 값 변경해서 CommentList 다시 마운트 + } catch (error) { + console.error("Failed to post comment :", error); + } finally { + setLoading(false); + } }; if (!dashboardId) return; + if (loading) return

Loading...

; return (
diff --git a/src/components/modal/task-detail/TaskDetailModal.tsx b/src/components/modal/task-detail/TaskDetailModal.tsx index ec524cd..e0fdc57 100644 --- a/src/components/modal/task-detail/TaskDetailModal.tsx +++ b/src/components/modal/task-detail/TaskDetailModal.tsx @@ -24,6 +24,8 @@ export default function TaskDetailModal() { id: selectedTaskId, }); setData(res); + } catch (error) { + console.error("Failed to load card detail :", error); } finally { setIsLoading(false); }