-
Notifications
You must be signed in to change notification settings - Fork 2
✨Feat: 공통 사용자 프로필 구현 #40
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
Changes from all commits
174ce28
411d303
60b6185
02ea86f
6d398aa
8d3dc98
6e5ba0a
96adafc
b24b287
cc5e0e7
8e4cfdc
20d374d
4dba235
a16c2e8
56a273a
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 |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| 'use client' | ||
|
|
||
| import Image from 'next/image' | ||
|
|
||
| type ProfileProps = { | ||
| nickname: string | ||
| imageUrl?: string | ||
| size?: number | ||
| } | ||
|
|
||
| const customColors = [ | ||
| '#efaa8d', | ||
| '#FFC85A', | ||
| '#b9ef8d', | ||
| '#8eef8d', | ||
| '#8defd3', | ||
| '#8dcaef', | ||
| '#8d9def', | ||
| '#a58def', | ||
| '#e292e0', | ||
| ] | ||
|
|
||
| // 첫 글자로 사용자 프로필 생성하는 함수 | ||
| function getInitial(nickname: string): string { | ||
| const firstChar = nickname.trim().charAt(0) | ||
|
|
||
| if (/[a-zA-Z]/.test(firstChar)) { | ||
| return firstChar.toUpperCase() // 영어: 대문자 | ||
| } | ||
|
|
||
| if (/[가-힣]/.test(firstChar)) { | ||
| return firstChar // 한글은 그대로 반환 | ||
| } | ||
|
|
||
| return '?' // 기타문자: 물음표 | ||
| } | ||
|
|
||
| // 닉네임으로부터 배경색 생성 함수 | ||
| function getColor(nickname: string): string { | ||
| const hash = nickname | ||
| .split('') | ||
| .reduce((acc, char) => acc + char.charCodeAt(0), 0) | ||
| return customColors[hash % customColors.length] | ||
| } | ||
|
|
||
| export function Profile({ nickname, imageUrl, size = 36 }: ProfileProps) { | ||
| const initial = getInitial(nickname) | ||
| const bgColor = getColor(nickname) | ||
|
|
||
| return imageUrl ? ( | ||
| // 프로필 이미지가 있을 때 | ||
| <div className="flex items-center gap-4"> | ||
| <div className="relative size-48 overflow-hidden rounded-full"> | ||
| <Image | ||
| src="/images/profile.gif" | ||
| fill | ||
| alt="프로필 이미지" | ||
| className="size-full object-cover" | ||
| /> | ||
| </div> | ||
| <span className="text-sm font-semibold">사용자</span> | ||
| </div> | ||
| ) : ( | ||
| // 프로필 이미지가 없을 때 | ||
| <> | ||
| <div | ||
| className="ml-8 flex items-center justify-center rounded-full font-semibold text-white" | ||
| style={{ | ||
| width: size, | ||
| height: size, | ||
| fontSize: size * 0.4, // 글자 크기 조정 | ||
| backgroundColor: bgColor, | ||
| }} | ||
| > | ||
| {initial} | ||
| </div> | ||
| <div className="text-base font-medium">{nickname}</div> | ||
| </> | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,20 +1,20 @@ | ||
| 'use client' | ||
|
|
||
| import { useUserStore } from '@store/useUserStore' // Zustand 예시 | ||
| 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' | ||
|
|
||
| export default function Header() { | ||
| const pathname = usePathname() | ||
| const router = useRouter() | ||
| const goToMypage = () => { | ||
| router.push('/mypage') | ||
| } | ||
| const { user, logout } = useUserStore() // Zustand 상태 | ||
|
|
||
| return ( | ||
| <header className="flex items-center justify-between border-b border-gray-200 bg-white px-36 py-16 dark:border-gray-700 dark:bg-black"> | ||
| <header className="BG-White Border-section Text-black flex items-center justify-between border-b px-36 py-16"> | ||
| {/* 좌측 대시보드명 */} | ||
| <div className="flex items-center gap-8"> | ||
| <div className="font-bold">대시보드 명</div> | ||
|
|
@@ -24,12 +24,15 @@ export default function Header() { | |
| </div> | ||
|
|
||
| {/* 우측 사용자 정보/다크모드 */} | ||
| <div className="flex items-center gap-16"> | ||
| <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={`flex items-center gap-6 rounded-md border-2 border-solid px-8 py-4 ${pathname === '/dashboard' ? 'font-semibold' : ''}`} | ||
| 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="관리 버튼" /> | ||
|
|
@@ -38,7 +41,10 @@ export default function Header() { | |
| </Link> | ||
| <Link | ||
| href="/modal" | ||
| className={`flex items-center gap-6 rounded-6 border-2 border-solid px-8 py-4 ${pathname === '/modal' ? 'font-semibold' : ''}`} | ||
| 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="초대 버튼" /> | ||
|
|
@@ -47,15 +53,7 @@ export default function Header() { | |
| </Link> | ||
| </nav> | ||
| {/* 공동작업자 프로필 이미지 */} | ||
| <div className="relative size-48 overflow-hidden rounded-full"> | ||
| <Image | ||
| src="/images/collaborator.png" | ||
| fill | ||
| alt="초대된 사용자" | ||
| style={{ objectFit: 'cover' }} | ||
| /> | ||
| </div> | ||
| <div className="relative size-48 overflow-hidden rounded-full"> | ||
| <div className="relative mx-16 size-48 overflow-hidden rounded-full"> | ||
| <Image | ||
| src="/images/collaborator.png" | ||
| fill | ||
|
|
@@ -64,20 +62,12 @@ export default function Header() { | |
| /> | ||
| </div> | ||
| |{/* 내 프로필 이미지 */} | ||
| <div className="relative size-48 overflow-hidden rounded-full"> | ||
| <Image | ||
| src="/images/profile.gif" | ||
| fill | ||
| alt="프로필이미지" | ||
| style={{ objectFit: 'cover' }} | ||
| /> | ||
| </div> | ||
| <span className="text-sm">배유철 {user?.name}</span> | ||
| <Profile nickname="전유진" /> | ||
| {/* 드롭다운 메뉴 */} | ||
| <button onClick={goToMypage} className="text-xs"> | ||
| 마이페이지 | ||
| </button> | ||
| <button onClick={logout} className="text-xs"> | ||
| <button onClick={goToMypage} className="text-xs"> | ||
| 로그아웃 | ||
| </button> | ||
|
Comment on lines
67
to
72
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. 로그아웃 버튼이 실제 로그아웃을 수행하지 않음
🤖 Prompt for AI Agents |
||
| </> | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,6 @@ | ||||||||||
| import { clsx, type ClassValue } from 'clsx' | ||||||||||
| import { twMerge } from 'tailwind-merge' | ||||||||||
|
|
||||||||||
|
Comment on lines
+1
to
+3
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.
-import { clsx, type ClassValue } from 'clsx'
+import clsx, { type ClassValue } from 'clsx'📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
| export function cn(...inputs: ClassValue[]) { | ||||||||||
| return twMerge(clsx(inputs)) | ||||||||||
| } | ||||||||||
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.
imageUrl프롭이 실제로 사용되지 않음imageUrl을 받아 놓고 하드코딩된"/images/profile.gif"를 렌더링하고 있어 기능이 동작하지 않습니다.또한 표시되는 이름이
"사용자"로 고정되어 있어nickname과 불일치합니다.📝 Committable suggestion
🤖 Prompt for AI Agents