-
Notifications
You must be signed in to change notification settings - Fork 3
Fix/kanban url #77
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
Fix/kanban url #77
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| import { useState } from 'react'; | ||
| import type React from 'react'; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While import { useState } from 'react';
import type { PointerEvent } from 'react'; |
||
| import { | ||
| PointerSensor, | ||
| useSensor, | ||
|
|
@@ -12,6 +13,21 @@ import type { KanbanTask, KanbanStatus } from '../interfaces/team'; | |
| // 드래그 시작으로 인식하는 최소 이동 거리(px) | ||
| const DRAG_ACTIVATION_DISTANCE = 8; | ||
|
|
||
| // input, label 등 인터랙티브 요소 클릭 시 드래그를 시작하지 않는 커스텀 센서 | ||
| class SmartPointerSensor extends PointerSensor { | ||
| static activators = [ | ||
| { | ||
| eventName: 'onPointerDown' as const, | ||
| handler: ({ nativeEvent: event }: React.PointerEvent): boolean => { | ||
| if (!event.isPrimary || event.button !== 0) return false; | ||
| const target = event.target as Element; | ||
| if (target.closest('input, button, a, label, textarea, select')) return false; | ||
| return true; | ||
| }, | ||
| }, | ||
| ]; | ||
| } | ||
|
|
||
| export function useKanbanDnd( | ||
| tasks: KanbanTask[], | ||
| setTasks: React.Dispatch<React.SetStateAction<KanbanTask[]>>, | ||
|
|
@@ -21,7 +37,7 @@ export function useKanbanDnd( | |
| const [activeTask, setActiveTask] = useState<KanbanTask | null>(null); | ||
|
|
||
| const sensors = useSensors( | ||
| useSensor(PointerSensor, { | ||
| useSensor(SmartPointerSensor, { | ||
| activationConstraint: { distance: DRAG_ACTIVATION_DISTANCE }, | ||
| }), | ||
| ); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -108,8 +108,38 @@ export function useKanbanTasks( | |||||||||
| [deleteTaskListMutation], | ||||||||||
| ); | ||||||||||
|
|
||||||||||
| // 태스크 아이템 체크 상태 변경은 할 일 목록 상세 페이지에서 처리하므로 빈 함수 | ||||||||||
| const handleItemCheckedChange = useCallback(() => {}, []); | ||||||||||
| // 체크박스 클릭 시 낙관적 업데이트 후 서버에 완료 상태 반영 | ||||||||||
| // 컬럼 이동은 발생하지 않음 (드래그앤 드롭으로만 이동 가능) | ||||||||||
| const handleItemCheckedChange = useCallback( | ||||||||||
| async (taskId: string, itemId: string, checked: boolean) => { | ||||||||||
| const taskListId = Number(taskId); | ||||||||||
| const queryKey = taskListKeys.detail(groupId, taskListId, today); | ||||||||||
|
|
||||||||||
| // 진행 중인 백그라운드 리패치 취소 (낙관적 업데이트가 덮어씌워지는 것을 방지) | ||||||||||
| await queryClient.cancelQueries({ queryKey }); | ||||||||||
|
|
||||||||||
| // 낙관적 업데이트: items만 변경하고 status(컬럼 위치)는 유지 | ||||||||||
| setTasks((prev) => | ||||||||||
| prev.map((task) => { | ||||||||||
| if (task.id !== taskId) return task; | ||||||||||
| const updatedItems = task.items.map((item) => | ||||||||||
| item.id === itemId ? { ...item, checked } : item, | ||||||||||
| ); | ||||||||||
| // 현재 컬럼 위치를 localStorage에 고정 (deriveStatus 재계산으로 인한 이동 방지) | ||||||||||
| setStoredStatus(groupId, taskListId, task.status); | ||||||||||
| return { ...task, items: updatedItems }; | ||||||||||
| }), | ||||||||||
| ); | ||||||||||
|
|
||||||||||
| try { | ||||||||||
| await updateTask(groupId, taskListId, Number(itemId), { done: checked }); | ||||||||||
| } finally { | ||||||||||
| // 성공/실패 관계없이 서버 상태와 동기화 | ||||||||||
| await queryClient.invalidateQueries({ queryKey }); | ||||||||||
| } | ||||||||||
| }, | ||||||||||
| [groupId, today, queryClient], | ||||||||||
| ); | ||||||||||
|
Comment on lines
+141
to
+142
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The dependency array for
Suggested change
|
||||||||||
|
|
||||||||||
| // 할 일 목록 추가 모달 열기 | ||||||||||
| const handleAddTask = useCallback((status: KanbanStatus) => { | ||||||||||
|
|
@@ -140,44 +170,13 @@ export function useKanbanTasks( | |||||||||
| // 수정 기능은 할 일 목록 상세 페이지에서 처리 | ||||||||||
| const handleUpdateTask = useCallback(() => {}, []); | ||||||||||
|
|
||||||||||
| // 드래그로 컬럼 이동 시 컬럼 위치를 저장하고, 완료/할 일 이동 시 API로 완료 상태 동기화 | ||||||||||
| // 드래그로 컬럼 이동 시 컬럼 위치만 localStorage에 저장 (체크박스 상태는 변경하지 않음) | ||||||||||
| const handleStatusChange = useCallback( | ||||||||||
| async (taskId: string, fromStatus: KanbanStatus, toStatus: KanbanStatus) => { | ||||||||||
| (taskId: string, fromStatus: KanbanStatus, toStatus: KanbanStatus) => { | ||||||||||
| if (fromStatus === toStatus) return; | ||||||||||
|
|
||||||||||
| const task = tasks.find((t) => t.id === taskId); | ||||||||||
| const taskListId = Number(taskId); | ||||||||||
|
|
||||||||||
| // 항목 유무와 관계없이 컬럼 위치를 localStorage에 저장 | ||||||||||
| setStoredStatus(groupId, taskListId, toStatus); | ||||||||||
|
|
||||||||||
| // 진행중으로 이동하거나 항목이 없으면 API 호출 없이 종료 (위치는 이미 저장됨) | ||||||||||
| if (!task || task.items.length === 0 || toStatus === 'inProgress') return; | ||||||||||
|
|
||||||||||
| try { | ||||||||||
| if (toStatus === 'done') { | ||||||||||
| // 모든 항목 완료 처리 | ||||||||||
| await Promise.all( | ||||||||||
| task.items.map((item) => | ||||||||||
| updateTask(groupId, taskListId, Number(item.id), { done: true }), | ||||||||||
| ), | ||||||||||
| ); | ||||||||||
| } else if (toStatus === 'todo') { | ||||||||||
| // 모든 항목 미완료 처리 | ||||||||||
| await Promise.all( | ||||||||||
| task.items.map((item) => | ||||||||||
| updateTask(groupId, taskListId, Number(item.id), { done: false }), | ||||||||||
| ), | ||||||||||
| ); | ||||||||||
| } | ||||||||||
| } finally { | ||||||||||
| // 성공/실패 관계없이 쿼리를 무효화하여 실제 서버 상태로 동기화 | ||||||||||
| await queryClient.invalidateQueries({ | ||||||||||
| queryKey: taskListKeys.detail(groupId, taskListId, today), | ||||||||||
| }); | ||||||||||
| } | ||||||||||
| setStoredStatus(groupId, Number(taskId), toStatus); | ||||||||||
| }, | ||||||||||
|
Comment on lines
+173
to
178
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The previous implementation of const handleStatusChange = useCallback(
async (taskId: string, fromStatus: KanbanStatus, toStatus: KanbanStatus) => {
if (fromStatus === toStatus) return;
setStoredStatus(groupId, Number(taskId), toStatus);
// Re-introduce API calls if backend state needs to reflect column changes
const task = tasks.find((t) => t.id === taskId);
const taskListId = Number(taskId);
if (!task || task.items.length === 0 || toStatus === 'inProgress') return;
try {
if (toStatus === 'done') {
await Promise.all(
task.items.map((item) =>
updateTask(groupId, taskListId, Number(item.id), { done: true }),
),
);
} else if (toStatus === 'todo') {
await Promise.all(
task.items.map((item) =>
updateTask(groupId, taskListId, Number(item.id), { done: false }),
),
);
}
} finally {
await queryClient.invalidateQueries({
queryKey: taskListKeys.detail(groupId, taskListId, today),
});
}
},
[groupId, tasks, today, queryClient]
); |
||||||||||
| [tasks, groupId, today, queryClient], | ||||||||||
| [groupId], | ||||||||||
| ); | ||||||||||
|
|
||||||||||
| return { | ||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| 'use client'; | ||
|
|
||
| import { useRouter } from 'next/navigation'; | ||
| import { Sidebar } from '@/components/sidebar'; | ||
|
|
||
| export default function AddTeamSidebarWrapper() { | ||
| const router = useRouter(); | ||
|
|
||
| return <Sidebar onProfileClick={() => router.push('/mypage')} />; | ||
| } | ||
|
Comment on lines
+1
to
+10
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's generally a good practice to define font sizes and weights in a more centralized theme or utility file rather than directly in component-specific CSS modules. This helps maintain consistency across the application and makes it easier to manage design tokens.