diff --git a/next.config.ts b/next.config.ts index 730dc55..7d7c912 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,6 +1,7 @@ import type { NextConfig } from 'next'; const nextConfig: NextConfig = { + // TODO: 백엔드 프로필 이미지 호스트가 확정되면 images.remotePatterns에 허용 도메인 추가 필요 turbopack: { rules: { '*.svg': [ diff --git a/src/entities/user/index.ts b/src/entities/user/index.ts new file mode 100644 index 0000000..c01da8f --- /dev/null +++ b/src/entities/user/index.ts @@ -0,0 +1,2 @@ +export { Profile } from './ui/Profile'; +export type { UserProfile } from './model/types'; diff --git a/src/entities/user/model/types.ts b/src/entities/user/model/types.ts new file mode 100644 index 0000000..2658953 --- /dev/null +++ b/src/entities/user/model/types.ts @@ -0,0 +1,4 @@ +export interface UserProfile { + name: string; + profileImageUrl: string; +} diff --git a/src/entities/user/ui/Profile.stories.tsx b/src/entities/user/ui/Profile.stories.tsx new file mode 100644 index 0000000..7e0c146 --- /dev/null +++ b/src/entities/user/ui/Profile.stories.tsx @@ -0,0 +1,26 @@ +import { DefaultProfileImage } from '@/shared/assets/images'; + +import { Profile } from './Profile'; + +import type { Meta, StoryObj } from '@storybook/nextjs'; + +const meta = { + title: 'Entities/User/Profile', + component: Profile, + args: { + name: '이서정', + profileImageUrl: DefaultProfileImage.src, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Fallback: Story = { + args: { + profileImageUrl: '/invalid-profile-image.png', + }, +}; diff --git a/src/entities/user/ui/Profile.tsx b/src/entities/user/ui/Profile.tsx new file mode 100644 index 0000000..cc23fa9 --- /dev/null +++ b/src/entities/user/ui/Profile.tsx @@ -0,0 +1,37 @@ +import type { ImageProps } from 'next/image'; + +import { DefaultProfileImage } from '@/shared/assets/images'; + +import { ProfileImage } from './ProfileImage'; + +import type { UserProfile } from '../model/types'; + +interface ProfileProps extends UserProfile { + fallbackImageSrc?: ImageProps['src']; + imageAlt?: string; +} + +/** + * ## Profile + * + * @description + * 로그인 사용자 프로필을 이름과 원형 프로필 이미지로 표시하는 도메인 UI입니다. + * + * @param name - 사용자 이름 + * @param profileImageUrl - 백엔드에서 전달받은 프로필 이미지 URL + * @param fallbackImageSrc - 프로필 이미지 로딩 실패 시 표시할 대체 이미지 + * @param imageAlt - 이미지 대체 텍스트. 전달하지 않으면 사용자 이름 기반으로 생성합니다. + */ +export function Profile({ + name, + profileImageUrl, + fallbackImageSrc = DefaultProfileImage, + imageAlt = `${name} 프로필 이미지`, +}: ProfileProps) { + return ( +
+ + {name} +
+ ); +} diff --git a/src/entities/user/ui/ProfileImage.tsx b/src/entities/user/ui/ProfileImage.tsx new file mode 100644 index 0000000..4c39604 --- /dev/null +++ b/src/entities/user/ui/ProfileImage.tsx @@ -0,0 +1,38 @@ +'use client'; + +import { useState } from 'react'; + +import Image from 'next/image'; +import type { ImageProps } from 'next/image'; + +interface ProfileImageProps { + alt: string; + fallbackSrc: ImageProps['src']; + src: string; +} + +export function ProfileImage({ alt, fallbackSrc, src }: ProfileImageProps) { + const [failedImageSrc, setFailedImageSrc] = useState(null); + const imageSrc = failedImageSrc === src ? fallbackSrc : src; + + const handleError = () => { + if (imageSrc === fallbackSrc) { + return; + } + + setFailedImageSrc(src); + }; + + return ( + {alt} + ); +} diff --git a/src/shared/assets/images/img-default-profile.png b/src/shared/assets/images/img-default-profile.png new file mode 100644 index 0000000..b1eb98b Binary files /dev/null and b/src/shared/assets/images/img-default-profile.png differ diff --git a/src/shared/assets/images/index.ts b/src/shared/assets/images/index.ts index e69de29..7b80019 100644 --- a/src/shared/assets/images/index.ts +++ b/src/shared/assets/images/index.ts @@ -0,0 +1,3 @@ +import DefaultProfileImage from './img-default-profile.png'; + +export { DefaultProfileImage }; diff --git a/src/shared/types/images.d.ts b/src/shared/types/images.d.ts new file mode 100644 index 0000000..0d65352 --- /dev/null +++ b/src/shared/types/images.d.ts @@ -0,0 +1,5 @@ +declare module '*.png' { + const content: import('next/image').StaticImageData; + + export default content; +}