From 2542c0ab3b6f5c71c3e1cce7d55a3cb6d932c14f Mon Sep 17 00:00:00 2001 From: jieunsse Date: Wed, 25 Feb 2026 01:53:45 +0900 Subject: [PATCH 1/5] =?UTF-8?q?refactor:=20soft=20optimistic=20ui=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_domain/components/Kanban/KanbanItem.module.css | 7 +++++++ .../[teamid]/_domain/components/Kanban/KanbanItem.tsx | 2 +- src/app/(root)/[teamid]/_domain/hooks/useKanbanTasks.ts | 9 +++++++++ src/app/(root)/[teamid]/_domain/interfaces/team.ts | 2 ++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/app/(root)/[teamid]/_domain/components/Kanban/KanbanItem.module.css b/src/app/(root)/[teamid]/_domain/components/Kanban/KanbanItem.module.css index b13f7cb..62b370d 100644 --- a/src/app/(root)/[teamid]/_domain/components/Kanban/KanbanItem.module.css +++ b/src/app/(root)/[teamid]/_domain/components/Kanban/KanbanItem.module.css @@ -8,6 +8,13 @@ .cardWrapper { position: relative; + transition: opacity 0.15s; +} + +/* 서버 요청 진행 중: 카드를 흐리게 표시하고 추가 인터랙션 차단 */ +.cardPending { + opacity: 0.5; + pointer-events: none; } /* 드래그 중인 아이템: TodoCard와 동일한 border-radius(12px)로 테두리 플레이스홀더 표시 */ diff --git a/src/app/(root)/[teamid]/_domain/components/Kanban/KanbanItem.tsx b/src/app/(root)/[teamid]/_domain/components/Kanban/KanbanItem.tsx index 01a9809..c320184 100644 --- a/src/app/(root)/[teamid]/_domain/components/Kanban/KanbanItem.tsx +++ b/src/app/(root)/[teamid]/_domain/components/Kanban/KanbanItem.tsx @@ -105,7 +105,7 @@ function KanbanItem({ >
{isEditing ? (
diff --git a/src/app/(root)/[teamid]/_domain/hooks/useKanbanTasks.ts b/src/app/(root)/[teamid]/_domain/hooks/useKanbanTasks.ts index 6ec06b6..227e88c 100644 --- a/src/app/(root)/[teamid]/_domain/hooks/useKanbanTasks.ts +++ b/src/app/(root)/[teamid]/_domain/hooks/useKanbanTasks.ts @@ -131,9 +131,18 @@ export function useKanbanTasks( }), ); + // 2초 이상 응답이 없을 때만 pending 표시 (빠른 응답 시 깜빡임 방지) + const pendingTimer = window.setTimeout(() => { + setTasks((prev) => + prev.map((task) => (task.id === taskId ? { ...task, pending: true } : task)), + ); + }, 2000); + try { await updateTask(groupId, taskListId, Number(itemId), { done: checked }); } finally { + // 타이머가 남아있으면 취소 (pending 노출 전에 완료된 경우) + clearTimeout(pendingTimer); // 성공/실패 관계없이 서버 상태와 동기화 await queryClient.invalidateQueries({ queryKey }); } diff --git a/src/app/(root)/[teamid]/_domain/interfaces/team.ts b/src/app/(root)/[teamid]/_domain/interfaces/team.ts index 8e04562..a9c847d 100644 --- a/src/app/(root)/[teamid]/_domain/interfaces/team.ts +++ b/src/app/(root)/[teamid]/_domain/interfaces/team.ts @@ -18,6 +18,8 @@ export interface KanbanTask { title: string; items: TaskItem[]; status: KanbanStatus; + /** 서버 요청 진행 중 여부 (Soft Optimistic UI용) */ + pending?: boolean; } export interface MockTeam { From 1dae57632a350fc8cea1c385cb84905b1ed4f3c1 Mon Sep 17 00:00:00 2001 From: jieunsse Date: Wed, 25 Feb 2026 02:06:15 +0900 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20=EC=82=AC=EC=9D=B4=EB=93=9C=EB=B0=94?= =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20=EC=97=B0=EA=B2=B0=20?= =?UTF-8?q?addteam,=20[teamid]=20=ED=8E=98=EC=9D=B4=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[teamid]/_domain/components/Team/SidebarWrapper.tsx | 6 ++++++ .../[teamid]/_domain/components/Team/TeamNavClient.tsx | 5 +++++ .../addteam/_domain/components/AddTeamSidebarWrapper.tsx | 6 ++++++ 3 files changed, 17 insertions(+) diff --git a/src/app/(root)/[teamid]/_domain/components/Team/SidebarWrapper.tsx b/src/app/(root)/[teamid]/_domain/components/Team/SidebarWrapper.tsx index 0156e21..e11174d 100644 --- a/src/app/(root)/[teamid]/_domain/components/Team/SidebarWrapper.tsx +++ b/src/app/(root)/[teamid]/_domain/components/Team/SidebarWrapper.tsx @@ -10,6 +10,11 @@ export default function SidebarWrapper() { const { data: currentUser } = useCurrentUserQuery(); const router = useRouter(); + const handleLogout = async () => { + await fetch('/api/auth/logout', { method: 'POST' }); + router.push('/login'); + }; + return ( } @@ -20,6 +25,7 @@ export default function SidebarWrapper() { profileName={currentUser?.nickname} profileTeam={currentUser?.email} onProfileClick={() => router.push('/mypage')} + onLogout={handleLogout} /> ); } diff --git a/src/app/(root)/[teamid]/_domain/components/Team/TeamNavClient.tsx b/src/app/(root)/[teamid]/_domain/components/Team/TeamNavClient.tsx index 96cf5eb..b27f7c0 100644 --- a/src/app/(root)/[teamid]/_domain/components/Team/TeamNavClient.tsx +++ b/src/app/(root)/[teamid]/_domain/components/Team/TeamNavClient.tsx @@ -18,6 +18,10 @@ export default function TeamNavClient() { const openDrawer = () => setIsDrawerOpen(true); const closeDrawer = () => setIsDrawerOpen(false); const handleProfileClick = () => router.push('/mypage'); + const handleLogout = async () => { + await fetch('/api/auth/logout', { method: 'POST' }); + router.push('/login'); + }; return ( <> @@ -35,6 +39,7 @@ export default function TeamNavClient() { } onMenuClick={openDrawer} onProfileClick={handleProfileClick} + onLogout={handleLogout} />
diff --git a/src/app/(root)/addteam/_domain/components/AddTeamSidebarWrapper.tsx b/src/app/(root)/addteam/_domain/components/AddTeamSidebarWrapper.tsx index 35227aa..5ec4639 100644 --- a/src/app/(root)/addteam/_domain/components/AddTeamSidebarWrapper.tsx +++ b/src/app/(root)/addteam/_domain/components/AddTeamSidebarWrapper.tsx @@ -9,6 +9,11 @@ export default function AddTeamSidebarWrapper() { const { data: currentUser } = useCurrentUserQuery(); const router = useRouter(); + const handleLogout = async () => { + await fetch('/api/auth/logout', { method: 'POST' }); + router.push('/login'); + }; + return ( router.push('/mypage')} + onLogout={handleLogout} /> ); } From 98375a174a3878a90a3374ae8ea554d2f6516ac8 Mon Sep 17 00:00:00 2001 From: jieunsse Date: Wed, 25 Feb 2026 12:27:05 +0900 Subject: [PATCH 3/5] =?UTF-8?q?style:=20teamid=20=EC=BD=98=ED=85=90?= =?UTF-8?q?=EC=B8=A0=20=EC=A2=8C=EC=9A=B0=EB=8C=80=EC=B9=AD=20=EC=A1=B0?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(root)/[teamid]/page.module.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/(root)/[teamid]/page.module.css b/src/app/(root)/[teamid]/page.module.css index 6ea7c63..6cb330a 100644 --- a/src/app/(root)/[teamid]/page.module.css +++ b/src/app/(root)/[teamid]/page.module.css @@ -13,7 +13,6 @@ min-width: 0; width: 100%; /* Explicitly set width to 100% */ box-sizing: border-box; /* Ensure padding is included in the width */ - margin-left: 24px; } /* Apply max-width to direct children of mainContents to prevent overflow */ @@ -26,8 +25,9 @@ .desktopSidebar { display: block; /* Show desktop sidebar */ /* fixed 포지션 사이드바가 flex 흐름에서 공간을 차지하지 않으므로, spacer 역할을 위해 너비 명시 */ - width: 270px; - min-width: 270px; + /* 루트 레이아웃 main의 margin-left: 72px를 이미 반영하므로 270 - 72 = 198px */ + width: 198px; + min-width: 198px; flex-shrink: 0; } /* Tablet styles */ From 5691f9a7f2e365b464d7ad2a316052e3db0a717b Mon Sep 17 00:00:00 2001 From: Jieunsse Date: Wed, 25 Feb 2026 12:56:19 +0900 Subject: [PATCH 4/5] =?UTF-8?q?chore:=20=EC=A4=91=EB=B3=B5=EC=BB=A4?= =?UTF-8?q?=EB=B0=8B=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/Team/SidebarWrapper.tsx | 31 ------------------- 1 file changed, 31 deletions(-) delete mode 100644 src/app/(root)/[teamid]/_domain/components/Team/SidebarWrapper.tsx diff --git a/src/app/(root)/[teamid]/_domain/components/Team/SidebarWrapper.tsx b/src/app/(root)/[teamid]/_domain/components/Team/SidebarWrapper.tsx deleted file mode 100644 index e11174d..0000000 --- a/src/app/(root)/[teamid]/_domain/components/Team/SidebarWrapper.tsx +++ /dev/null @@ -1,31 +0,0 @@ -'use client'; - -import { useRouter } from 'next/navigation'; -import { Sidebar } from '@/components/sidebar'; -import ProfileImage from '@/components/profile-img/ProfileImage'; -import { useCurrentUserQuery } from '@/shared/queries/user/useCurrentUserQuery'; -import TeamSidebarDropdown from './TeamSidebarDropdown'; - -export default function SidebarWrapper() { - const { data: currentUser } = useCurrentUserQuery(); - const router = useRouter(); - - const handleLogout = async () => { - await fetch('/api/auth/logout', { method: 'POST' }); - router.push('/login'); - }; - - return ( - } - isLoggedIn={!!currentUser} - profileImage={ - - } - profileName={currentUser?.nickname} - profileTeam={currentUser?.email} - onProfileClick={() => router.push('/mypage')} - onLogout={handleLogout} - /> - ); -} From a21c359bcbd966e908a6e0b41bad8e9c7f6da478 Mon Sep 17 00:00:00 2001 From: Jieunsse Date: Wed, 25 Feb 2026 12:56:49 +0900 Subject: [PATCH 5/5] =?UTF-8?q?chore:=20=EC=A4=91=EB=B3=B5=EC=BB=A4?= =?UTF-8?q?=EB=B0=8B=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_domain/components/Team/TeamNavClient.tsx | 52 ------------------- 1 file changed, 52 deletions(-) delete mode 100644 src/app/(root)/[teamid]/_domain/components/Team/TeamNavClient.tsx diff --git a/src/app/(root)/[teamid]/_domain/components/Team/TeamNavClient.tsx b/src/app/(root)/[teamid]/_domain/components/Team/TeamNavClient.tsx deleted file mode 100644 index b27f7c0..0000000 --- a/src/app/(root)/[teamid]/_domain/components/Team/TeamNavClient.tsx +++ /dev/null @@ -1,52 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { useRouter } from 'next/navigation'; - -import { MobileHeader, MobileDrawer } from '@/components/sidebar'; -import ProfileImage from '@/components/profile-img/ProfileImage'; -import { useCurrentUserQuery } from '@/shared/queries/user/useCurrentUserQuery'; -import TeamTabletHeader from './TeamTabletHeader'; -import TeamSidebarDropdown from './TeamSidebarDropdown'; -import styles from './TeamNavClient.module.css'; - -export default function TeamNavClient() { - const [isDrawerOpen, setIsDrawerOpen] = useState(false); - const { data: currentUser } = useCurrentUserQuery(); - const router = useRouter(); - - const openDrawer = () => setIsDrawerOpen(true); - const closeDrawer = () => setIsDrawerOpen(false); - const handleProfileClick = () => router.push('/mypage'); - const handleLogout = async () => { - await fetch('/api/auth/logout', { method: 'POST' }); - router.push('/login'); - }; - - return ( - <> - {/* 태블릿 헤더 */} -
- -
- - {/* 모바일 헤더 */} -
- - } - onMenuClick={openDrawer} - onProfileClick={handleProfileClick} - onLogout={handleLogout} - /> -
- - {/* 태블릿/모바일 공통 사이드바 드로어 */} - - - - - ); -}