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
Binary file added public/favicon.ico
Binary file not shown.
Binary file removed public/favicon.png
Binary file not shown.
3 changes: 3 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ body {
.Border-column {
@apply border-r border-[#EEEEEE] dark:border-[#262626];
}
.Border-bottom {
@apply border-b border-[#D9D9D9] dark:border-[#707070];
}
.BG-addPhoto {
@apply bg-[#F5F5F5] dark:bg-[#2E2E2E];
}
Expand Down
2 changes: 1 addition & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const metadata: Metadata = {
title: 'Coplan',
description: 'Generated by create next app',
icons: {
icon: '/favicon.png',
icon: '/favicon.ico',
},
}

Expand Down
59 changes: 59 additions & 0 deletions src/app/shared/components/common/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
'use client'

import { cn } from '@lib/cn' // 클래스 이름 병합 유틸리티
import { ReactNode, useEffect, useRef, useState } from 'react'

type DropdownProps = {
trigger: ReactNode
children: ReactNode
align?: 'left' | 'right' | 'center'
width?: string
}

export default function Dropdown({
trigger,
children,
align = 'center',
width = 'w-40',
}: DropdownProps) {
const [isOpen, setIsOpen] = useState(false)
const ref = useRef<HTMLDivElement>(null)

useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) {
setIsOpen(false)
}
}

document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])

return (
<div ref={ref} className="relative inline-block">
<div
onClick={() => setIsOpen((prev) => !prev)}
className="cursor-pointer"
>
{trigger}
</div>

{isOpen && (
<div
className={cn(
'BG-gray absolute z-50 mt-4 rounded border shadow-md',
width,
{
'left-0': align === 'left',
'right-0': align === 'right',
'left-1/2 -translate-x-1/2': align === 'center',
},
Comment on lines +46 to +51

Choose a reason for hiding this comment

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

이렇게 작성하면 어떻게 적용이 되는 건가요?

Copy link
Contributor Author

@yuj2n yuj2n Jun 12, 2025

Choose a reason for hiding this comment

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

드롭다운 컴포넌트 사용 시에 와 같이 사용해주시면 좌측 정렬이 가능해집니다!!
이후에 사용해보고 너무 치우친다 싶으면 'leftt-0' 부분을 'left-4'와 같이 조정해주는 식으로 변경하면 될 것 같습니당

)}
>
{children}
</div>
)}
</div>
)
}
2 changes: 1 addition & 1 deletion src/app/shared/components/common/Profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export function Profile({ nickname, imageUrl, size = 36 }: ProfileProps) {
className="size-full object-cover"
/>
</div>
<span className="text-sm font-semibold">사용자</span>
<span className="text-sm font-semibold">{nickname}</span>
</div>
) : (
// 프로필 이미지가 없을 때
Expand Down
100 changes: 47 additions & 53 deletions src/app/shared/components/common/header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
'use client'

import { usePathname, useRouter } from 'next/navigation'
import { cn } from '@lib/cn' // 클래스 이름 병합 유틸리티
import Link from 'next/link'
import Image from 'next/image'
import { Profile } from '@components/common/Profile'
import Link from 'next/link'
import { usePathname } from 'next/navigation'

import ThemeToggle from '../../ThemeToggle'
import UserDropdown from './UserDropdown'

export default function Header() {
const pathname = usePathname()
const router = useRouter()
const goToMypage = () => {
router.push('/mypage')
}

return (
<header className="BG-White Border-section Text-black flex items-center justify-between border-b px-36 py-16">
<header className="Border-bottom Text-black flex items-center justify-between border-b px-18 py-12">
{/* 좌측 대시보드명 */}
<div className="flex items-center gap-8">
<div className="font-bold">대시보드 명</div>
Expand All @@ -25,52 +23,48 @@ export default function Header() {

{/* 우측 사용자 정보/다크모드 */}
<div className="flex items-center gap-8">
<>
<nav className="hidden gap-8 text-sm text-gray-600 dark:text-gray-300 md:flex">
<Link
href="/dashboard"
className={cn(
'Border-btn flex items-center gap-6 rounded-md border-solid px-12 py-6',
pathname === '/dashboard' && 'font-semibold',
)}
>
<div className="relative flex size-12">
<Image src="/images/management.png" fill alt="관리 버튼" />
</div>
관리
</Link>
<Link
href="/modal"
className={cn(
'Border-btn mr-16 flex items-center gap-6 rounded-6 border-solid px-12 py-6',
pathname === '/modal' && 'font-semibold',
)}
>
<div className="relative flex size-12">
<Image src="/images/invitation.png" fill alt="초대 버튼" />
</div>
초대하기
</Link>
</nav>
{/* 공동작업자 프로필 이미지 */}
<div className="relative mx-16 size-48 overflow-hidden rounded-full">
<Image
src="/images/collaborator.png"
fill
alt="초대된 사용자"
style={{ objectFit: 'cover' }}
/>
</div>
|{/* 내 프로필 이미지 */}
<Profile nickname="전유진" />
<nav className="Text-gray hidden gap-8 text-sm md:flex">
<Link
href="/dashboard"
className={cn(
'Border-btn flex items-center gap-6 rounded-md border-solid px-12 py-6',
pathname === '/dashboard' && 'font-semibold',
)}
>
<div className="relative flex size-12">
<Image src="/images/management.png" fill alt="관리 버튼" />
</div>
관리
</Link>
<Link
href="/modal"
className={cn(
'Border-btn mr-16 flex items-center gap-6 rounded-6 border-solid px-12 py-6',
pathname === '/modal' && 'font-semibold',
)}
>
<div className="relative flex size-12">
<Image src="/images/invitation.png" fill alt="초대 버튼" />
</div>
초대하기
</Link>
</nav>
{/* 공동작업자 프로필 이미지 */}
<div className="relative mx-16 size-48 overflow-hidden rounded-full">
<Image
src="/images/collaborator.png"
fill
alt="초대된 사용자"
style={{ objectFit: 'cover' }}
/>
</div>
|
<div className="flex items-center gap-16">
{/* 드롭다운 메뉴 */}
<button onClick={goToMypage} className="text-xs">
마이페이지
</button>
<button onClick={goToMypage} className="text-xs">
로그아웃
</button>
</>
<UserDropdown />
{/* 다크모드 토글 버튼 */}
<ThemeToggle />
</div>
</div>
</header>
)
Expand Down
42 changes: 42 additions & 0 deletions src/app/shared/components/common/header/UserDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use client'

import Dropdown from '@components/common/Dropdown/Dropdown'
import { Profile } from '@components/common/Profile'
import { useRouter } from 'next/navigation'

export default function UserDropdown() {
const router = useRouter()

const goToMypage = () => {
router.push('/mypage')
}

const handleLogout = () => {
console.log('로그아웃 처리')
}

return (
<Dropdown
trigger={
<div className="flex cursor-pointer items-center gap-8">
<Profile nickname="닉네임" size={36} />
</div>
Comment on lines +22 to +23
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

닉네임 하드코딩

<Profile>"닉네임"을 하드코딩하면 실제 사용자 정보를 표시하지 못합니다. session 또는 user 컨텍스트에서 닉네임을 받아와 전달하도록 수정해 주세요.

-          <Profile nickname="닉네임" size={36} />
+          <Profile nickname={user.nickname} imageUrl={user.imageUrl} size={36} />

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/app/shared/components/common/header/UserDropdown.tsx at lines 22-23, the
Profile component currently has the nickname prop hardcoded as "닉네임". Replace
this hardcoded string by retrieving the actual user's nickname from the session
or user context and pass it dynamically to the Profile component to display the
correct user information.

Copy link
Contributor

Choose a reason for hiding this comment

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

오... 프롭으로 아예 jsx 덩어리를 전달할수도 있군요

Copy link
Contributor Author

@yuj2n yuj2n Jun 12, 2025

Choose a reason for hiding this comment

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

맞아용~ JSX 덩어리 전체를 prop으로 전달하는 게 가능하다고 합니다.
이는 실제로 레고 블럭처럼 조립 가능하고 재사용성과 유연성 증가하는 동시에 역할 분리도 확실해서 React에서 매우 일반적이고 강력한 패턴이라고 합니다!!

}
width="w-80"
align="right"
>
<button
onClick={goToMypage}
className="w-full p-5 text-xs hover:bg-gray-100 hover:text-black"
>
마이페이지
</button>
<button
onClick={handleLogout}
className="w-full p-5 text-xs hover:bg-gray-100 hover:text-black"
>
로그아웃
</button>
</Dropdown>
)
}