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
3 changes: 3 additions & 0 deletions public/images/arrow-dropdown.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions public/images/close.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions public/images/drop-more.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 15 additions & 3 deletions src/app/dashboard/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

import Image from 'next/image'
import { useParams } from 'next/navigation'
import { useRef } from 'react'
import { useEffect, useRef } from 'react'

import { useCardMutation } from '@/app/features/dashboard_Id/api/useCardMutation'
import useColumns from '@/app/features/dashboard_Id/api/useColumns'
import Column from '@/app/features/dashboard_Id/Column/Column'
import { useColumnsStore } from '@/app/features/dashboard_Id/store/useColumnsStore'
import { useDragStore } from '@/app/features/dashboard_Id/store/useDragStore'
import { Card } from '@/app/features/dashboard_Id/type/Card.type'

Expand All @@ -17,12 +18,23 @@ export default function DashboardID() {
// const { data: columns, isLoading, error } = useColumns(id)

const { draggingCard, setDraggingCard } = useDragStore()
const { setColumns } = useColumnsStore()
const cardMutation = useCardMutation()
const touchPos = useRef({ x: 0, y: 0 })
const prevColumn = useRef<HTMLElement | null>(null)
const longPressTimer = useRef<number | null>(null)
const isLongPressActive = useRef(false)

useEffect(() => {
if (columns) {
const transformed = columns.map((column) => ({
columnId: column.id,
columnTitle: column.title,
}))
setColumns(transformed)
}
}, [columns, setColumns])

const handleTouchStart = (e: React.TouchEvent<HTMLDivElement>) => {
// 1. 터치 대상 찾기
const target = e.target as HTMLElement
Expand Down Expand Up @@ -127,9 +139,9 @@ export default function DashboardID() {
if (error) return <p>error...{error.message}</p>
return (
<>
<div className="ml-300 select-none">
<div className="select-none">
<div
className="flex min-h-[calc(100vh-100px)] tablet:flex-col"
className="flex min-h-[calc(100vh-100px)] mobile:flex-row tablet:flex-col"
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
Expand Down
51 changes: 34 additions & 17 deletions src/app/features/dashboard_Id/Card/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
import Image from 'next/image'
import { useState } from 'react'

import { Avatar } from '@/app/shared/components/common/Avatar'

import { useDragStore } from '../store/useDragStore'
import type { Card as CardType } from '../type/Card.type'
import type { Column as ColumnType } from '../type/Column.type'
import CardContent from './cardModal/CardContent'
import CardModal from './cardModal/CardModal'
import Tags from './Tags'

export default function Card({
card,
columnId,
column,
}: {
card: CardType
columnId: number
column: ColumnType
}) {
const { id, imageUrl, title, tags, dueDate, assignee } = card
const { setDraggingCard } = useDragStore()
const [openCard, setOpenCard] = useState(false) //card.tsx

return (
<div
data-card-id={id}
Expand All @@ -23,6 +29,7 @@ export default function Card({
onDragStart={() => setDraggingCard({ cardData: card })}
onContextMenu={(e: React.MouseEvent) => e.preventDefault()}
className="BG-white Border-section relative rounded-6 border-solid px-20 py-16"
onClick={() => setOpenCard(true)}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

클릭 가능한 카드에 커서 스타일 추가

카드가 클릭 가능하다는 것을 사용자에게 명확히 알려주기 위해 커서 스타일을 추가하세요.

-      className="BG-white Border-section relative rounded-6 border-solid px-20 py-16"
+      className="BG-white Border-section relative cursor-pointer rounded-6 border-solid px-20 py-16"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onClick={() => setOpenCard(true)}
className="BG-white Border-section relative cursor-pointer rounded-6 border-solid px-20 py-16"
onClick={() => setOpenCard(true)}
🤖 Prompt for AI Agents
In src/app/features/dashboard_Id/Card/Card.tsx at line 32, the onClick handler
is set but the card element lacks a cursor style indicating it is clickable. Add
a CSS style or class to the card element to set the cursor to 'pointer' so users
clearly see it is clickable.

>
{imageUrl && (
<Image
Expand All @@ -42,36 +49,46 @@ export default function Card({
</h3>

{/* 태그 */}
<Tags tags={tags} />
{tags.length !== 0 && <Tags tags={tags} />}

{/* 마감일 & 담당자 */}
<div className="mt-8 flex content-around items-center">
{/* :마감일 */}
{dueDate && (
<div className="flex size-full items-center gap-6">
<Image
src={'/images/calendar.svg'}
alt="마감일"
width={18}
height={18}
/>
<div className="flex size-full items-center gap-6">
<Image
src={'/images/calendar.svg'}
alt="마감일"
width={18}
height={18}
/>
{dueDate && (
<div className="Text-gray mt-4 text-12 font-medium leading-none">
{dueDate}
{dueDate.split(' ')[0]}
</div>
</div>
)}

)}
</div>
Comment on lines +57 to +69
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

마감일이 없을 때 캘린더 아이콘 조건부 렌더링

마감일이 없어도 캘린더 아이콘이 항상 표시됩니다. 사용자 혼란을 방지하기 위해 마감일이 있을 때만 아이콘을 표시하세요.

-        <div className="flex size-full items-center gap-6">
-          <Image
-            src={'/images/calendar.svg'}
-            alt="마감일"
-            width={18}
-            height={18}
-          />
-          {dueDate && (
-            <div className="Text-gray mt-4 text-12 font-medium leading-none">
-              {dueDate.split(' ')[0]}
-            </div>
-          )}
-        </div>
+        {dueDate && (
+          <div className="flex size-full items-center gap-6">
+            <Image
+              src={'/images/calendar.svg'}
+              alt="마감일"
+              width={18}
+              height={18}
+            />
+            <div className="Text-gray mt-4 text-12 font-medium leading-none">
+              {dueDate.split(' ')[0]}
+            </div>
+          </div>
+        )}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="flex size-full items-center gap-6">
<Image
src={'/images/calendar.svg'}
alt="마감일"
width={18}
height={18}
/>
{dueDate && (
<div className="Text-gray mt-4 text-12 font-medium leading-none">
{dueDate}
{dueDate.split(' ')[0]}
</div>
</div>
)}
)}
</div>
{dueDate && (
<div className="flex size-full items-center gap-6">
<Image
src={'/images/calendar.svg'}
alt="마감일"
width={18}
height={18}
/>
<div className="Text-gray mt-4 text-12 font-medium leading-none">
{dueDate.split(' ')[0]}
</div>
</div>
)}
🤖 Prompt for AI Agents
In src/app/features/dashboard_Id/Card/Card.tsx around lines 57 to 69, the
calendar icon is always rendered regardless of whether dueDate exists, which can
confuse users. Modify the code to conditionally render the entire div containing
the calendar icon and dueDate only when dueDate is present, so the icon and date
appear together only if dueDate is defined.

{/* :담당자 */}
{assignee && (
<div className="shrink-0">
<Avatar
nickname={assignee.nickname}
imageUrl={assignee.profileImageUrl}
size={24}
name={assignee.nickname}
imageUrl={assignee.profileImageUrl}
/>
</div>
)}
</div>

{/* 카드 모달 */}
{openCard && (
<CardModal>
<CardContent
onClose={() => setOpenCard(false)}
card={card}
column={column}
/>
</CardModal>
)}
</div>
)
}
10 changes: 10 additions & 0 deletions src/app/features/dashboard_Id/Card/ColumnTitle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export default function ColumnTitle({ title }: { title: string }) {
return (
<div className="BG-lightblue flex w-fit items-center gap-6 rounded-16 px-10 py-4">
<div className="size-6 rounded-25 bg-[#228DFF]"></div>
<div className="Text-deepblue pb-4 pt-6 text-14 font-medium leading-none">
{title}
</div>
</div>
)
}
18 changes: 18 additions & 0 deletions src/app/features/dashboard_Id/Card/MyAssignee.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Avatar } from '@/app/shared/components/common/Avatar'

import { Assignee } from '../type/Card.type'

export default function MyAssignee({ assignee }: { assignee: Assignee }) {
return (
<div className="flex items-center gap-6">
<Avatar
size={26}
name={assignee.nickname}
imageUrl={assignee.profileImageUrl}
/>
<span className="regular Text-black block pt-1 text-16 leading-none">
{assignee.nickname}
</span>
</div>
)
}
22 changes: 19 additions & 3 deletions src/app/features/dashboard_Id/Card/Tags.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,39 @@
import { useTheme } from 'next-themes'

import { getColor } from '@/app/shared/lib/getColor'

export default function Tags({ tags }: { tags: string[] }) {
//태그 컬러 - 랜덤 배정
//카드 생성 시 - 동일 태그 입력 불가하도록
const bgColors = ['#F9EEE3', '#E7F7DB', '#F7DBF0', '#DBE6F7']
const bgColorsDark = ['#774212', '#366712', '#711E5C', '#0F3167']
const textColors = ['#D58D49', '#86D549', '#D549B6', '#4981D5']

const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark'

return (
<div className="flex flex-wrap gap-6">
{tags.map((tag) => {
const colorIndex = getColor(tag, bgColors.length)
// const colorIndex = getColor(tag, bgColors.length)
// getColors함수 사용하면 NaN값이 떠서 작동을 안함.. 원래 문제 없었는데 이유를 모르겠음.
const hash = tag
.split('')
.reduce((acc, char) => acc + char.charCodeAt(0), 0)
const colorIndex = hash % 4
Comment on lines +18 to +23
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

코드 중복 및 getColor 함수 문제 해결 필요

수동 해시 계산 로직이 TagsCanDelete.tsx와 중복됩니다. getColor 함수에서 NaN 문제가 발생한다면 해당 함수를 수정하는 것이 더 나은 접근입니다.

다음과 같이 개선할 수 있습니다:

-        // const colorIndex = getColor(tag, bgColors.length)
-        // getColors함수 사용하면 NaN값이 떠서 작동을 안함.. 원래 문제 없었는데 이유를 모르겠음.
-        const hash = tag
-          .split('')
-          .reduce((acc, char) => acc + char.charCodeAt(0), 0)
-        const colorIndex = hash % 4
+        const colorIndex = getColor(tag, bgColors.length)

getColor 함수의 NaN 문제를 먼저 해결하고, 공통 해시 함수를 만들어 중복을 제거하세요.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// const colorIndex = getColor(tag, bgColors.length)
// getColors함수 사용하면 NaN값이 떠서 작동을 안함.. 원래 문제 없었는데 이유를 모르겠음.
const hash = tag
.split('')
.reduce((acc, char) => acc + char.charCodeAt(0), 0)
const colorIndex = hash % 4
const colorIndex = getColor(tag, bgColors.length)
🤖 Prompt for AI Agents
In src/app/features/dashboard_Id/Card/Tags.tsx around lines 18 to 23, the manual
hash calculation duplicates logic found in TagsCanDelete.tsx and the getColor
function currently causes NaN errors. To fix this, first debug and correct the
getColor function to prevent NaN results, then extract the hash calculation into
a shared utility function used by both components to eliminate code duplication
and ensure consistent color indexing.


const backgroundColor = isDark
? bgColorsDark[colorIndex]
: bgColors[colorIndex]
const textColor = isDark ? bgColors[colorIndex] : textColors[colorIndex]

return (
<span
key={tag}
className="inline-block whitespace-nowrap rounded-4 px-9 pb-3 pt-5"
style={{
backgroundColor: bgColors[colorIndex],
color: textColors[colorIndex],
backgroundColor: backgroundColor,
color: textColor,
}}
>
{tag}
Expand Down
49 changes: 49 additions & 0 deletions src/app/features/dashboard_Id/Card/TagsCanDelete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useTheme } from 'next-themes'

import { getColor } from '@/app/shared/lib/getColor'

export default function TagsCanDelete({
tags,
setTags,
}: {
tags: string[]
setTags: React.Dispatch<React.SetStateAction<string[]>>
}) {
//태그 컬러 - 랜덤 배정
//카드 생성 시 - 동일 태그 입력 불가하도록
const bgColors = ['#F9EEE3', '#E7F7DB', '#F7DBF0', '#DBE6F7']
const textColors = ['#D58D49', '#86D549', '#D549B6', '#4981D5']

const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark'

return (
<div className="flex flex-wrap gap-6">
{tags.map((tag) => {
const hash = tag
.split('')
.reduce((acc, char) => acc + char.charCodeAt(0), 0)
const colorIndex = hash % 4
Comment on lines +23 to +26
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

코드 중복 제거 필요

Tags.tsx와 동일한 해시 계산 로직이 중복됩니다. 공통 유틸리티 함수로 추출하세요.

공통 함수를 만들어 중복을 제거하세요:

// utils/tagColorUtils.ts
export function getTagColorIndex(tag: string): number {
  const hash = tag
    .split('')
    .reduce((acc, char) => acc + char.charCodeAt(0), 0)
  return hash % 4
}

그 후 두 컴포넌트에서 사용:

-        const hash = tag
-          .split('')
-          .reduce((acc, char) => acc + char.charCodeAt(0), 0)
-        const colorIndex = hash % 4
+        const colorIndex = getTagColorIndex(tag)
🤖 Prompt for AI Agents
In src/app/features/dashboard_Id/Card/TagsCanDelete.tsx around lines 23 to 26,
the hash calculation logic for determining colorIndex is duplicated from
Tags.tsx. Extract this logic into a shared utility function, for example
getTagColorIndex in a new file like utils/tagColorUtils.ts, which takes a tag
string and returns the color index. Then replace the duplicated code in both
components by importing and using this utility function to remove redundancy.


const backgroundColor = isDark
? textColors[colorIndex]
: bgColors[colorIndex]
const textColor = isDark ? bgColors[colorIndex] : textColors[colorIndex]
Comment on lines +28 to +31
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Tags.tsx와 다크 모드 색상 로직 불일치

Tags.tsx와 다른 다크 모드 색상 로직을 사용하고 있어 일관성이 없습니다. Tags.tsx는 bgColorsDark를 배경색으로, 여기서는 textColors를 배경색으로 사용합니다.

일관성을 위해 동일한 색상 로직을 사용하세요:

         const backgroundColor = isDark
-          ? textColors[colorIndex]
+          ? ['#774212', '#366712', '#711E5C', '#0F3167'][colorIndex]
           : bgColors[colorIndex]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const backgroundColor = isDark
? textColors[colorIndex]
: bgColors[colorIndex]
const textColor = isDark ? bgColors[colorIndex] : textColors[colorIndex]
const backgroundColor = isDark
? ['#774212', '#366712', '#711E5C', '#0F3167'][colorIndex]
: bgColors[colorIndex]
const textColor = isDark ? bgColors[colorIndex] : textColors[colorIndex]
🤖 Prompt for AI Agents
In src/app/features/dashboard_Id/Card/TagsCanDelete.tsx around lines 28 to 31,
the dark mode color logic is inconsistent with Tags.tsx, which uses bgColorsDark
for background colors instead of textColors. To fix this, update the
backgroundColor assignment to use bgColorsDark when isDark is true, matching the
logic in Tags.tsx, ensuring consistent color usage across components.


return (
<span
key={tag}
className="inline-block whitespace-nowrap rounded-4 px-9 pb-3 pt-5"
style={{
backgroundColor: backgroundColor,
color: textColor,
}}
onClick={() => setTags(tags.filter((t) => t !== tag))}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

키보드 접근성 개선 필요

태그 삭제가 마우스 클릭으로만 가능합니다. 키보드 접근성을 위해 Enter/Space 키 지원을 추가하세요.

           <span
             key={tag}
             className="inline-block whitespace-nowrap rounded-4 px-9 pb-3 pt-5"
+            role="button"
+            tabIndex={0}
             style={{
               backgroundColor: backgroundColor,
               color: textColor,
             }}
             onClick={() => setTags(tags.filter((t) => t !== tag))}
+            onKeyDown={(e) => {
+              if (e.key === 'Enter' || e.key === ' ') {
+                e.preventDefault()
+                setTags(tags.filter((t) => t !== tag))
+              }
+            }}
           >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onClick={() => setTags(tags.filter((t) => t !== tag))}
<span
key={tag}
className="inline-block whitespace-nowrap rounded-4 px-9 pb-3 pt-5"
role="button"
tabIndex={0}
style={{
backgroundColor: backgroundColor,
color: textColor,
}}
onClick={() => setTags(tags.filter((t) => t !== tag))}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
setTags(tags.filter((t) => t !== tag))
}
}}
>
{tag}
</span>
🤖 Prompt for AI Agents
In src/app/features/dashboard_Id/Card/TagsCanDelete.tsx at line 41, the tag
deletion is currently triggered only by mouse clicks via the onClick handler. To
improve keyboard accessibility, add keyboard event handling to support tag
deletion when the Enter or Space key is pressed. Implement an onKeyDown handler
that checks for these keys and triggers the same tag removal logic as the
onClick event.

>
{tag}
</span>
)
})}
</div>
)
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { ControllerRenderProps } from 'react-hook-form'

import { Avatar } from '@/app/shared/components/common/Avatar'
import { cn } from '@/app/shared/lib/cn'

import getDashboardMembers from '../../lib/getDashboardMembers'
import { Assignee } from '../../type/Card.type'
import { CardFormData } from '../../type/CardFormData.type'
import { Member } from '../../type/Member.type'
import MyAssignee from '../MyAssignee'

export interface Assignee {
userId: number
nickname: string
}
interface AssigneeListProps {
members: Member[] | undefined
setAssignee: (assignee: Assignee) => void
Expand All @@ -36,13 +35,13 @@ export default function AssigneeList({
'BG-Input-hovered w-full cursor-pointer px-16 py-11 pt-14 placeholder-gray-400 caret-transparent',
index !== 0 && 'border-t',
)}
key={assignee.userId}
key={assignee.id}
onClick={() => {
setAssignee(assignee) // 담당자 업데이트
controlField.onChange(assignee.userId) // 리액트 훅에는 .userId 값 연결
controlField.onChange(assignee.id) // 리액트 훅에는 .userId 값 연결
}}
>
{assignee.nickname}
<MyAssignee assignee={assignee} />
</div>
))}
</div>
Expand Down
44 changes: 44 additions & 0 deletions src/app/features/dashboard_Id/Card/cardFormModals/ColumnList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { ControllerRenderProps } from 'react-hook-form'

import { cn } from '@/app/shared/lib/cn'

import { SimpleColumn, useColumnsStore } from '../../store/useColumnsStore'
import { CardFormData } from '../../type/CardFormData.type'
import ColumnTitle from '../ColumnTitle'

interface ColumnListProps {
setColumn: (selectedColumn: SimpleColumn) => void
controlField: ControllerRenderProps<CardFormData, 'columnId'>
}

// ✅ ColumnList 컴포넌트: 컬럼 목록을 보여주는 드롭다운 리스트
// 1. 컬럼 목록을 클릭하면:
// - setColumn(column) 실행 → 선택된 담당자 객체를 부모 컴포넌트 하에 관리 (ex. UI에서 닉네임 표시용)
// - controlField.onChange(column.columnId) 실행 → react-hook-form에 columnId 값을 전달 (form 제출에는 Id 데이터만 전달함)

export default function ColumnList({
setColumn,
controlField,
}: ColumnListProps) {
const { ColumnsInDashboard } = useColumnsStore() // 컬럼 목록 데이터는 store에서 불러옴

return (
<div className="BG-white Border-btn absolute left-0 top-full z-10 mt-4 w-full rounded-6">
{ColumnsInDashboard.map((column, index) => (
<div
className={cn(
'BG-Input-hovered w-full cursor-pointer px-16 py-11 pt-14 placeholder-gray-400 caret-transparent',
index !== 0 && 'border-t',
)}
key={column.columnId}
onClick={() => {
setColumn(column) // 담당자 업데이트
controlField.onChange(column.columnId) // controlField: 폼의 'columnId' 필드와 연결되어 있음. .columnId 값으로 업데이트
}}
>
<ColumnTitle title={column.columnTitle} />
</div>
))}
</div>
)
}
Loading