Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,4 @@ scripts/refresh-dev-token.mjs
coworkers-swagger.json
.gitignore
GEMINI.md
src/app/api/dev/inject-session/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
/* 접힘/펼침 전환 시 타이틀 수평 시프팅 방지: folded 상태와 패딩·보더 두께 통일 */
padding-left: 20px !important;
border: 1px solid transparent !important;
font-size: 12px;
font-weight: 400;
Comment on lines +30 to +31

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

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.

}

/* 피그마 fold=True 상태: 54px 높이, 좌측 패딩 20px, 테두리만 표시 */
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
'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();

return (
<Sidebar
Expand All @@ -17,6 +19,7 @@ export default function SidebarWrapper() {
}
profileName={currentUser?.nickname}
profileTeam={currentUser?.email}
onProfileClick={() => router.push('/mypage')}
/>
);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState } from 'react';
import type React from 'react';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

While type React from 'react' is technically correct, the more common and idiomatic way to import types from React is import type { PointerEvent } from 'react'; or import type { ReactNode } from 'react'; if you need specific types. Importing the entire React namespace as a type is less common.

import { useState } from 'react';
import type { PointerEvent } from 'react';

import {
PointerSensor,
useSensor,
Expand All @@ -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[]>>,
Expand All @@ -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 },
}),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The dependency array for handleItemCheckedChange includes groupId, today, and queryClient. While groupId and queryClient are stable, today is derived from new Date().toISOString().split('T')[0] and will change on every render, causing handleItemCheckedChange to be recreated unnecessarily. This can lead to performance issues or unexpected behavior if handleItemCheckedChange is used as a dependency in other hooks. Consider memoizing today or passing it as an argument if it's meant to be dynamic.

Suggested change
[groupId, today, queryClient],
);
[groupId, queryClient]
);


// 할 일 목록 추가 모달 열기
const handleAddTask = useCallback((status: KanbanStatus) => {
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

critical

The previous implementation of handleStatusChange included logic to update task items' done status via API calls when moving tasks between 'done' and 'todo' columns. The current change removes this logic, making handleStatusChange solely responsible for updating the local storage status. This means that moving a task to 'done' or 'todo' will no longer automatically update the done status of its items on the backend. This could lead to a discrepancy between the UI's representation of task completion and the actual backend state, potentially causing data inconsistencies or unexpected behavior for users. If the intention is to only update the local storage, this should be clearly documented or the API calls should be re-introduced if the backend state needs to reflect the column change.

  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 {
Expand Down
File renamed without changes.
File renamed without changes.
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The AddTeamSidebarWrapper component is a very thin wrapper around Sidebar that only passes onProfileClick. If this is the only difference, consider if this wrapper is truly necessary. It might be more straightforward to pass the onProfileClick prop directly where Sidebar is used in AddTeamLayout, or to abstract the onProfileClick logic into a custom hook if it's reused across multiple sidebar instances with different behaviors.

File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { MobileHeader, Sidebar } from '@/components/sidebar';
import { MobileHeader } from '@/components/sidebar';
import AddTeamSidebarWrapper from './_domain/components/AddTeamSidebarWrapper';
import styles from './page.module.css';

export default function AddTeamLayout({ children }: { children: React.ReactNode }) {
return (
<main className={styles.page}>
<Sidebar />
<AddTeamSidebarWrapper />
<div className={styles.mobileOnlyHeader}>
<MobileHeader />
</div>
Expand Down
File renamed without changes.
File renamed without changes.
Loading