diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/components/Card.tsx b/src/app/(with-header-sidebar)/dashboard/[id]/components/Card.tsx
index 8d35693..a201fcf 100644
--- a/src/app/(with-header-sidebar)/dashboard/[id]/components/Card.tsx
+++ b/src/app/(with-header-sidebar)/dashboard/[id]/components/Card.tsx
@@ -4,14 +4,16 @@ import { Cards } from '@/types/dashboardView';
import { useModal } from '@/app/(with-header-sidebar)/mydashboard/_hooks/useModal';
import Modal from '@/app/(with-header-sidebar)/mydashboard/_components/modal/Modal';
import CardInfo from './card-detail/CardInfo';
+import HeaderMenu from './card-detail/HeaderMenu';
import styles from './Card.module.css';
interface Props {
item: Cards;
index: number;
+ columnTitle: string;
}
-function Card({ item, index }: Props) {
+function Card({ item, index, columnTitle }: Props) {
const { isOpen, openModal, isClosing, closeModal } = useModal();
if (!item || !item.id) {
@@ -39,8 +41,11 @@ function Card({ item, index }: Props) {
onClose={closeModal}
title={item.title}
hasCloseButton={true}
+ headerComponent={() => (
+
+ )}
>
-
+
)}
>
diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/components/Column.tsx b/src/app/(with-header-sidebar)/dashboard/[id]/components/Column.tsx
index 6d62384..71962c3 100644
--- a/src/app/(with-header-sidebar)/dashboard/[id]/components/Column.tsx
+++ b/src/app/(with-header-sidebar)/dashboard/[id]/components/Column.tsx
@@ -99,7 +99,6 @@ function Column({
/>
-
{items.map((item, index) =>
- item ? : null
+ item ? (
+
+ ) : null
)}
-
{provided.placeholder}
-
)}
diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/Assignment.module.css b/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/Assignment.module.css
index 0ed9468..c0024b6 100644
--- a/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/Assignment.module.css
+++ b/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/Assignment.module.css
@@ -17,6 +17,7 @@
display: flex;
flex-direction: column;
justify-content: flex-start;
+ gap: 2px;
}
.label {
@@ -26,13 +27,14 @@
line-height: 20px;
}
-.description {
+.description,
+.avatarContainer {
color: var(--black-100);
font-size: 12px;
font-weight: 400;
}
-.assignee .description {
+.avatarContainer {
display: flex;
align-items: center;
gap: 8px;
@@ -43,7 +45,7 @@
height: 26px;
}
-.dueDate .description {
+.description {
margin-top: 8px;
}
@@ -57,6 +59,10 @@
gap: 16px;
}
+ .assignee {
+ gap: 5px;
+ }
+
.avatar {
width: 34px;
height: 34px;
diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/Assignment.tsx b/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/Assignment.tsx
index 309ff37..b722abf 100644
--- a/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/Assignment.tsx
+++ b/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/Assignment.tsx
@@ -10,11 +10,6 @@ interface AssignmentProps {
export default function Assignment({ card }: AssignmentProps) {
if (!card) return null;
- // const { assignee, dueDate } = {
- // dueDate: '2024-11-30 12:00',
- // assignee: { profileImageUrl: null, nickname: 'manta', id: 1 },
- // };
-
const { assignee, dueDate } = card;
return (
@@ -23,7 +18,7 @@ export default function Assignment({ card }: AssignmentProps) {
{assignee && (
담당자
-
+
- 카드 상세내용 + 댓글 영역
+
+
+
+
+
+
+
+ {tags.map((tag, index) => (
+
+ ))}
+
+
+
{description}
+ {imageUrl && (
+
+
+
+ )}
+
);
}
diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/HeaderMenu.module.css b/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/HeaderMenu.module.css
new file mode 100644
index 0000000..36db845
--- /dev/null
+++ b/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/HeaderMenu.module.css
@@ -0,0 +1,18 @@
+.headerMenu {
+ display: flex;
+ align-items: center;
+ margin-right: 16px;
+ position: relative;
+}
+
+.moreButton {
+ display: inline-flex;
+ align-items: center;
+}
+
+.menuContainer {
+ position: absolute;
+ z-index: 999;
+ top: 40px;
+ right: 0;
+}
diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/HeaderMenu.tsx b/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/HeaderMenu.tsx
new file mode 100644
index 0000000..1187440
--- /dev/null
+++ b/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/HeaderMenu.tsx
@@ -0,0 +1,43 @@
+import More from '@/components/svg/More';
+import type { Menu } from '@/types/menu';
+import MenuDropdown from '@/components/MenuDropdown';
+import { useMenu } from '@/hooks/useMenu';
+import styles from './HeaderMenu.module.css';
+import { deleteCard } from '@/lib/cardService';
+import { useRouter } from 'next/navigation';
+import useDashboardStore from '@/store/dashboardStore';
+
+interface HeaderMenuProps {
+ cardId: number;
+ closeModal: () => void;
+}
+
+export default function HeaderMenu({ cardId, closeModal }: HeaderMenuProps) {
+ const router = useRouter();
+ const { dashboard } = useDashboardStore();
+ const { isMenuVisible, toggleMenu } = useMenu();
+
+ const handelDeleteClick = async () => {
+ await deleteCard(cardId);
+ closeModal();
+ router.replace(`/dashboard/${dashboard?.id}`);
+ };
+
+ const cardMenus: Menu[] = [
+ { name: '수정하기', handleOnClick: () => console.log('수정하기 클릭') },
+ { name: '삭제하기', handleOnClick: handelDeleteClick },
+ ];
+
+ return (
+
+
+ {isMenuVisible && (
+
+
+
+ )}
+
+ );
+}
diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/edit/_hooks/useMember.ts b/src/app/(with-header-sidebar)/dashboard/[id]/edit/_hooks/useMember.ts
index 1f4c3a3..a06fbd3 100644
--- a/src/app/(with-header-sidebar)/dashboard/[id]/edit/_hooks/useMember.ts
+++ b/src/app/(with-header-sidebar)/dashboard/[id]/edit/_hooks/useMember.ts
@@ -15,7 +15,7 @@ const DEFAULT_MEMBERS_STATE: MemberState = {
members: [],
};
-const useMember = (dashboardId: string, pageSize = 4) => {
+const useMember = (dashboardId: string | null, pageSize = 4) => {
const [memberState, setMemberState] = useState(
DEFAULT_MEMBERS_STATE
);
diff --git a/src/app/(with-header-sidebar)/mydashboard/_components/modal/Modal.module.css b/src/app/(with-header-sidebar)/mydashboard/_components/modal/Modal.module.css
index a87c115..fad776c 100644
--- a/src/app/(with-header-sidebar)/mydashboard/_components/modal/Modal.module.css
+++ b/src/app/(with-header-sidebar)/mydashboard/_components/modal/Modal.module.css
@@ -44,15 +44,15 @@
color: var(--black-100);
}
-.close {
+.closeButtonWrapper {
+ position: relative;
width: 24px;
height: 24px;
- transition: 200ms;
}
-.close:hover {
- filter: brightness(2);
- transform: rotate(90deg);
+.closeButton {
+ display: inline-flex;
+ align-items: center;
}
@keyframes fadeIn {
@@ -107,4 +107,9 @@
flex: 1;
color: var(--black-100);
}
+
+ .closeButtonWrapper {
+ width: 32px;
+ height: 32px;
+ }
}
diff --git a/src/app/(with-header-sidebar)/mydashboard/_components/modal/Modal.tsx b/src/app/(with-header-sidebar)/mydashboard/_components/modal/Modal.tsx
index 5787627..f3040c8 100644
--- a/src/app/(with-header-sidebar)/mydashboard/_components/modal/Modal.tsx
+++ b/src/app/(with-header-sidebar)/mydashboard/_components/modal/Modal.tsx
@@ -12,7 +12,7 @@ interface ModalProps {
allowDimClose?: boolean;
title?: string;
hasCloseButton?: boolean;
- headerComponent?: React.ComponentType;
+ headerComponent?: React.ComponentType;
children: ReactNode;
}
@@ -59,18 +59,16 @@ export default function Modal({
{title && {title}
}
{Component && }
{hasCloseButton && (
-
+
+
+
)}
{children}
diff --git a/src/app/(with-header-sidebar)/mydashboard/_hooks/useApi.ts b/src/app/(with-header-sidebar)/mydashboard/_hooks/useApi.ts
index b6f92ee..c01a32e 100644
--- a/src/app/(with-header-sidebar)/mydashboard/_hooks/useApi.ts
+++ b/src/app/(with-header-sidebar)/mydashboard/_hooks/useApi.ts
@@ -31,7 +31,6 @@ export default function useApi<
const [error, setError] = useState(null);
const fetchData = useCallback(async () => {
- // TODO if (loading) return; add?
setIsLoading(true);
setError(null);
diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx
index 7f17ee2..34b73b6 100644
--- a/src/components/Avatar.tsx
+++ b/src/components/Avatar.tsx
@@ -1,5 +1,6 @@
import Image from 'next/image';
import styles from './Avatar.module.css';
+import { getRandomColor } from '@/utils/colorUtils';
interface AvatarProps {
name: string | number;
@@ -36,20 +37,3 @@ export default function Avatar({
>
);
}
-
-const getRandomColor = (name: string) => {
- const hash = [...name].reduce(
- (acc, char) => acc + char.charCodeAt(0) * 31,
- 0
- );
- const getValue = (offset: number) => {
- const baseValue = (((hash >> offset) % 0xff) % 76) + 180;
- const maxValue = 225;
- return Math.min(baseValue, maxValue);
- };
- const red = getValue(0);
- const green = getValue(8);
- const blue = getValue(16);
-
- return `rgb(${red}, ${green}, ${blue})`;
-};
diff --git a/src/components/MenuDropdown.module.css b/src/components/MenuDropdown.module.css
new file mode 100644
index 0000000..d803ccc
--- /dev/null
+++ b/src/components/MenuDropdown.module.css
@@ -0,0 +1,40 @@
+@keyframes slideDown {
+ from {
+ opacity: 0;
+ transform: translateY(-10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.menuDropdown {
+ border-radius: 6px;
+ border: 1px solid var(--gray-300);
+ background: var(--white);
+ box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.08);
+ padding: 6px;
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+ animation: slideDown 0.3s ease-out;
+}
+
+.button {
+ border-radius: 4px;
+ color: var(--black);
+ font-size: 14px;
+ font-weight: 400;
+ transition:
+ background-color 0.3s ease,
+ color 0.3s ease,
+ transform 0.3s ease;
+ white-space: nowrap;
+ padding: 5px 16px;
+}
+
+.button:hover {
+ background-color: var(--violet-light);
+ color: var(--violet);
+}
diff --git a/src/components/MenuDropdown.tsx b/src/components/MenuDropdown.tsx
new file mode 100644
index 0000000..6ef0688
--- /dev/null
+++ b/src/components/MenuDropdown.tsx
@@ -0,0 +1,23 @@
+import type { Menu } from '@/types/menu';
+import styles from './MenuDropdown.module.css';
+
+interface MenuDropdownProps {
+ menus: Menu[];
+}
+
+export default function MenuDropdown({ menus }: MenuDropdownProps) {
+ return (
+
+ {menus.map((menu, index) => (
+
+ ))}
+
+ );
+}
diff --git a/src/components/card/ColumnLabel.module.css b/src/components/card/ColumnLabel.module.css
new file mode 100644
index 0000000..d3b51b3
--- /dev/null
+++ b/src/components/card/ColumnLabel.module.css
@@ -0,0 +1,11 @@
+.columnLabel {
+ border-radius: 16px;
+ background: var(--violet-light);
+ color: var(--violet);
+ font-size: 12px;
+ font-weight: 400;
+ line-height: 18px;
+ display: flex;
+ gap: 6px;
+ padding: 4px 10px;
+}
diff --git a/src/components/card/ColumnLabel.tsx b/src/components/card/ColumnLabel.tsx
new file mode 100644
index 0000000..54ddede
--- /dev/null
+++ b/src/components/card/ColumnLabel.tsx
@@ -0,0 +1,17 @@
+import styles from './ColumnLabel.module.css';
+import Dot from '../svg/Dot';
+
+interface ColumnLabelProps {
+ name: string;
+}
+
+export default function ColumnLabel({ name }: ColumnLabelProps) {
+ return (
+
+ );
+}
diff --git a/src/components/card/Tag.module.css b/src/components/card/Tag.module.css
new file mode 100644
index 0000000..d46a9d9
--- /dev/null
+++ b/src/components/card/Tag.module.css
@@ -0,0 +1,7 @@
+.tag {
+ font-size: 12px;
+ font-weight: 400;
+ line-height: 18px;
+ padding: 2px 6px;
+ border-radius: 4px;
+}
diff --git a/src/components/card/Tag.tsx b/src/components/card/Tag.tsx
new file mode 100644
index 0000000..d0b1762
--- /dev/null
+++ b/src/components/card/Tag.tsx
@@ -0,0 +1,20 @@
+import { getDarkerColor, getRandomColor } from '@/utils/colorUtils';
+import styles from './Tag.module.css';
+
+interface TagProps {
+ name: string;
+}
+
+export default function Tag({ name }: TagProps) {
+ const mainColor = getRandomColor(name);
+ const style = {
+ backgroundColor: mainColor,
+ color: getDarkerColor(mainColor),
+ };
+
+ return (
+
+ {name}
+
+ );
+}
diff --git a/src/components/header/DashboardMembers.tsx b/src/components/header/DashboardMembers.tsx
index 4a05b7d..025d8ce 100644
--- a/src/components/header/DashboardMembers.tsx
+++ b/src/components/header/DashboardMembers.tsx
@@ -16,7 +16,7 @@ export default function DashboardMembers() {
const { isMobile } = useWindowSize();
const { members, totalPages } = useMember(
- dashboard?.id.toString() || '0',
+ dashboard?.id.toString() || null,
MEMBERS_VIEW_COUNT.desktop
);
diff --git a/src/components/header/Header.module.css b/src/components/header/Header.module.css
index d791386..717acf7 100644
--- a/src/components/header/Header.module.css
+++ b/src/components/header/Header.module.css
@@ -44,50 +44,6 @@
display: none;
}
-.userInfoContainer {
- display: flex;
- align-items: center;
-}
-
-.userInfoContainer::before {
- content: '';
- border: 0.5px solid var(--gray-300);
- height: 34px;
-}
-
-.userInfoButton.userInfoButton {
- margin-left: 16px;
- display: flex;
- align-items: center;
- background-color: transparent;
-}
-
-.myMenu {
- position: absolute;
- top: 75px;
- right: 10px;
- padding: 6px 0;
- border: 1px solid var(--gray-300);
- border-radius: 6px;
- color: var(--gray-500);
- font-size: 14px;
- font-weight: 500;
- display: flex;
- flex-direction: column;
- gap: 5px;
- background: var(--white);
-}
-
-.myMenu div {
- padding: 3px 12px;
- transition: background-color 0.3s ease;
-}
-
-.myMenu div:hover {
- cursor: pointer;
- background: var(--violet-light);
-}
-
@media screen and (min-width: 768px) {
.header {
padding-left: 40px;
@@ -111,17 +67,6 @@
.icon {
display: block;
}
-
- .userInfoWrapper {
- margin-left: 36px;
- }
-
- .myMenu {
- min-width: 120px;
- text-align: center;
- top: 80px;
- right: 50px;
- }
}
@media screen and (min-width: 1199px) {
diff --git a/src/components/header/Header.tsx b/src/components/header/Header.tsx
index 87b3dca..af68f89 100644
--- a/src/components/header/Header.tsx
+++ b/src/components/header/Header.tsx
@@ -1,13 +1,12 @@
'use client';
import { usePathname, useRouter } from 'next/navigation';
-import { useState } from 'react';
import Image from 'next/image';
import Button from '../Button';
-import UserInfo from './UserInfo';
import Title from './Title';
-import styles from './Header.module.css';
import useDashboardStore from '@/store/dashboardStore';
+import UserSection from './UserSection';
+import styles from './Header.module.css';
interface HeaderProps {
component?: React.ComponentType;
@@ -16,21 +15,11 @@ interface HeaderProps {
export default function Header({ component: Component }: HeaderProps) {
const router = useRouter();
const dashboard = useDashboardStore((state) => state.dashboard);
- const [isMenuVisible, setIsMenuVisible] = useState(false);
-
- const handleUserInfoClick = () => {
- setIsMenuVisible(!isMenuVisible);
- };
const handleSettingsClick = () => {
router.push(`/dashboard/${dashboard?.id}/edit`);
};
- const navigateTo = (href: string) => {
- router.push(href);
- handleUserInfoClick();
- };
-
return (
@@ -63,18 +52,7 @@ export default function Header({ component: Component }: HeaderProps) {
)}
-
-
- {isMenuVisible && (
-
-
navigateTo('/mydashboard')}>내 대시보드
-
navigateTo('/mypage')}>내 정보
-
navigateTo('/')}>로그아웃
-
- )}
-
+
);
}
diff --git a/src/components/header/UserSection.module.css b/src/components/header/UserSection.module.css
new file mode 100644
index 0000000..b012e5b
--- /dev/null
+++ b/src/components/header/UserSection.module.css
@@ -0,0 +1,30 @@
+.userInfoContainer {
+ position: relative;
+ display: flex;
+ align-items: center;
+}
+
+.userInfoContainer::before {
+ content: '';
+ border: 0.5px solid var(--gray-300);
+ height: 34px;
+}
+
+.userInfoButton.userInfoButton {
+ margin-left: 16px;
+ display: flex;
+ align-items: center;
+ background-color: transparent;
+}
+
+.myMenu {
+ position: absolute;
+ top: 75px;
+ right: 0;
+}
+
+@media screen and (min-width: 768px) {
+ .myMenu {
+ top: 60px;
+ }
+}
diff --git a/src/components/header/UserSection.tsx b/src/components/header/UserSection.tsx
new file mode 100644
index 0000000..61d163b
--- /dev/null
+++ b/src/components/header/UserSection.tsx
@@ -0,0 +1,36 @@
+import UserInfo from './UserInfo';
+import Button from '../Button';
+import type { Menu } from '@/types/menu';
+import { useMenu } from '@/hooks/useMenu';
+import { useRouter } from 'next/navigation';
+import MenuDropdown from '../MenuDropdown';
+import styles from './UserSection.module.css';
+
+export default function UserSection() {
+ const router = useRouter();
+ const { isMenuVisible, toggleMenu, closeMenu } = useMenu();
+
+ const navigateTo = (href: string) => {
+ router.push(href);
+ closeMenu();
+ };
+
+ const myInfoMenus: Menu[] = [
+ { name: '내 대시보드', handleOnClick: () => navigateTo('/mydashboard') },
+ { name: '내 정보', handleOnClick: () => navigateTo('/mypage') },
+ { name: '로그아웃', handleOnClick: () => navigateTo('/') },
+ ];
+
+ return (
+
+
+ {isMenuVisible && (
+
+
+
+ )}
+
+ );
+}
diff --git a/src/components/svg/Dot.tsx b/src/components/svg/Dot.tsx
new file mode 100644
index 0000000..c9275aa
--- /dev/null
+++ b/src/components/svg/Dot.tsx
@@ -0,0 +1,17 @@
+export default function Dot({
+ width = '1em',
+ height = '1em',
+ fill = '#5534da',
+}) {
+ return (
+
+ );
+}
diff --git a/src/components/svg/More.tsx b/src/components/svg/More.tsx
new file mode 100644
index 0000000..3e7b202
--- /dev/null
+++ b/src/components/svg/More.tsx
@@ -0,0 +1,20 @@
+export default function More({
+ width = '28',
+ height = '28',
+ fill = '#000000',
+}) {
+ return (
+
+ );
+}
diff --git a/src/components/svg/Pipe.tsx b/src/components/svg/Pipe.tsx
new file mode 100644
index 0000000..7616977
--- /dev/null
+++ b/src/components/svg/Pipe.tsx
@@ -0,0 +1,13 @@
+export default function Pipe({ width = '2', height = '20', fill = '#D9D9D9' }) {
+ return (
+
+ );
+}
diff --git a/src/hooks/useMenu.ts b/src/hooks/useMenu.ts
new file mode 100644
index 0000000..3b22b19
--- /dev/null
+++ b/src/hooks/useMenu.ts
@@ -0,0 +1,15 @@
+import { useState } from 'react';
+
+export function useMenu() {
+ const [isMenuVisible, setIsMenuVisible] = useState(false);
+
+ const toggleMenu = () => {
+ setIsMenuVisible((prev) => !prev);
+ };
+
+ const closeMenu = () => {
+ setIsMenuVisible(false);
+ };
+
+ return { isMenuVisible, toggleMenu, closeMenu };
+}
diff --git a/src/lib/cardService.ts b/src/lib/cardService.ts
new file mode 100644
index 0000000..c20832d
--- /dev/null
+++ b/src/lib/cardService.ts
@@ -0,0 +1,9 @@
+import axiosInstance from '@/lib/axiosInstance';
+
+export const deleteCard = async (cardId: number) => {
+ try {
+ await axiosInstance.delete(`/cards/${cardId}`);
+ } catch (error) {
+ throw error;
+ }
+};
diff --git a/src/types/dashboardView.ts b/src/types/dashboardView.ts
index 616d62d..c4121d3 100644
--- a/src/types/dashboardView.ts
+++ b/src/types/dashboardView.ts
@@ -1,7 +1,7 @@
import { DebouncedFunc } from 'lodash';
export interface CardAssignee {
- profileImageUrl: string;
+ profileImageUrl: string | null;
nickname: string;
id: number;
}
diff --git a/src/types/menu.ts b/src/types/menu.ts
new file mode 100644
index 0000000..4af6062
--- /dev/null
+++ b/src/types/menu.ts
@@ -0,0 +1,4 @@
+export interface Menu {
+ name: string;
+ handleOnClick: (event: React.MouseEvent) => void;
+}
diff --git a/src/utils/colorUtils.ts b/src/utils/colorUtils.ts
new file mode 100644
index 0000000..86c2def
--- /dev/null
+++ b/src/utils/colorUtils.ts
@@ -0,0 +1,29 @@
+export const getRandomColor = (name: string) => {
+ const hash = [...name].reduce(
+ (acc, char) => acc + char.charCodeAt(0) * 31,
+ 0
+ );
+ const getValue = (offset: number) => {
+ const baseValue = (((hash >> offset) % 0xff) % 76) + 180;
+ const maxValue = 225;
+ return Math.min(baseValue, maxValue);
+ };
+ const red = getValue(0);
+ const green = getValue(8);
+ const blue = getValue(16);
+
+ return `rgb(${red}, ${green}, ${blue})`;
+};
+
+export const getDarkerColor = (rgbStr: string, adjustValue = 100) => {
+ const [red, green, blue] = extractRGBNumbers(rgbStr).map((color) =>
+ Math.max(color - adjustValue, 0)
+ );
+
+ return `rgb(${red}, ${green}, ${blue})`;
+};
+
+export const extractRGBNumbers = (rgbStr: string): number[] => {
+ const match = rgbStr.match(/\d+/g);
+ return match ? match.map(Number) : [];
+};