From 231af0c27d19028994077c58f40ec54c6b242c99 Mon Sep 17 00:00:00 2001 From: Leeseojeong Date: Sun, 5 Jul 2026 15:56:00 +0900 Subject: [PATCH 1/7] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/user/index.ts | 2 ++ src/entities/user/model/types.ts | 4 +++ src/entities/user/ui/Profile.stories.tsx | 18 ++++++++++ src/entities/user/ui/Profile.tsx | 44 ++++++++++++++++++++++++ 4 files changed, 68 insertions(+) create mode 100644 src/entities/user/index.ts create mode 100644 src/entities/user/model/types.ts create mode 100644 src/entities/user/ui/Profile.stories.tsx create mode 100644 src/entities/user/ui/Profile.tsx 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..3184eec --- /dev/null +++ b/src/entities/user/ui/Profile.stories.tsx @@ -0,0 +1,18 @@ +import { Profile } from './Profile'; + +import type { Meta, StoryObj } from '@storybook/nextjs'; + +const meta = { + title: 'Entities/User/Profile', + component: Profile, + args: { + name: '이서정', + profileImageUrl: '/images/profile.png', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/src/entities/user/ui/Profile.tsx b/src/entities/user/ui/Profile.tsx new file mode 100644 index 0000000..cf93202 --- /dev/null +++ b/src/entities/user/ui/Profile.tsx @@ -0,0 +1,44 @@ +import type { ComponentPropsWithoutRef } from 'react'; + +import Image from 'next/image'; + +import { cn } from '@/shared/styles/utils/cn'; + +import type { UserProfile } from '../model/types'; + +interface ProfileProps extends ComponentPropsWithoutRef<'div'>, UserProfile { + imageAlt?: string; +} + +/** + * ## Profile + * + * @description + * 로그인 사용자 프로필을 이름과 원형 프로필 이미지로 표시하는 도메인 UI입니다. + * + * @param name - 사용자 이름 + * @param profileImageUrl - 백엔드에서 전달받은 프로필 이미지 URL + * @param imageAlt - 이미지 대체 텍스트. 전달하지 않으면 사용자 이름 기반으로 생성합니다. + */ +export function Profile({ + name, + profileImageUrl, + imageAlt = `${name} 프로필 이미지`, + className, + ...props +}: ProfileProps) { + return ( +
+ {imageAlt} + {name} +
+ ); +} From f7a4fc966f8844c6f38e9c89877244bc0450d0e5 Mon Sep 17 00:00:00 2001 From: Leeseojeong Date: Sun, 5 Jul 2026 15:56:35 +0900 Subject: [PATCH 2/7] =?UTF-8?q?=F0=9F=93=9D=20Docs:=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=ED=98=B8=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20TODO=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- next.config.ts | 1 + 1 file changed, 1 insertion(+) 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': [ From 568ba5464674e7b8ea3b1d0ec4301772959d3626 Mon Sep 17 00:00:00 2001 From: Leeseojeong Date: Sun, 5 Jul 2026 16:08:30 +0900 Subject: [PATCH 3/7] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EC=8A=A4=ED=86=A0=EB=A6=AC=20mock=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/user/ui/Profile.stories.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/entities/user/ui/Profile.stories.tsx b/src/entities/user/ui/Profile.stories.tsx index 3184eec..e90ccf0 100644 --- a/src/entities/user/ui/Profile.stories.tsx +++ b/src/entities/user/ui/Profile.stories.tsx @@ -2,12 +2,15 @@ import { Profile } from './Profile'; import type { Meta, StoryObj } from '@storybook/nextjs'; +const MOCK_PROFILE_IMAGE_URL = + 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"%3E%3Ccircle cx="16" cy="16" r="15.5" fill="%23b7d6b5" stroke="white"/%3E%3C/svg%3E'; + const meta = { title: 'Entities/User/Profile', component: Profile, args: { name: '이서정', - profileImageUrl: '/images/profile.png', + profileImageUrl: MOCK_PROFILE_IMAGE_URL, }, } satisfies Meta; From 9915bd9fc6255d2494b1ff3daa8a94e6f8154f67 Mon Sep 17 00:00:00 2001 From: Leeseojeong Date: Sun, 5 Jul 2026 16:43:57 +0900 Subject: [PATCH 4/7] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EA=B8=B0=EB=B3=B8=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?fallback=20=EC=A7=80=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/user/ui/Profile.stories.tsx | 13 ++++-- src/entities/user/ui/Profile.tsx | 18 ++++----- src/entities/user/ui/ProfileImage.tsx | 38 ++++++++++++++++++ .../assets/images/img-default-profile.png | Bin 0 -> 2512 bytes src/shared/assets/images/index.ts | 3 ++ 5 files changed, 58 insertions(+), 14 deletions(-) create mode 100644 src/entities/user/ui/ProfileImage.tsx create mode 100644 src/shared/assets/images/img-default-profile.png diff --git a/src/entities/user/ui/Profile.stories.tsx b/src/entities/user/ui/Profile.stories.tsx index e90ccf0..7e0c146 100644 --- a/src/entities/user/ui/Profile.stories.tsx +++ b/src/entities/user/ui/Profile.stories.tsx @@ -1,16 +1,15 @@ +import { DefaultProfileImage } from '@/shared/assets/images'; + import { Profile } from './Profile'; import type { Meta, StoryObj } from '@storybook/nextjs'; -const MOCK_PROFILE_IMAGE_URL = - 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"%3E%3Ccircle cx="16" cy="16" r="15.5" fill="%23b7d6b5" stroke="white"/%3E%3C/svg%3E'; - const meta = { title: 'Entities/User/Profile', component: Profile, args: { name: '이서정', - profileImageUrl: MOCK_PROFILE_IMAGE_URL, + profileImageUrl: DefaultProfileImage.src, }, } satisfies Meta; @@ -19,3 +18,9 @@ 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 index cf93202..bebcfcf 100644 --- a/src/entities/user/ui/Profile.tsx +++ b/src/entities/user/ui/Profile.tsx @@ -1,12 +1,16 @@ import type { ComponentPropsWithoutRef } from 'react'; -import Image from 'next/image'; +import type { ImageProps } from 'next/image'; +import { DefaultProfileImage } from '@/shared/assets/images'; import { cn } from '@/shared/styles/utils/cn'; +import { ProfileImage } from './ProfileImage'; + import type { UserProfile } from '../model/types'; interface ProfileProps extends ComponentPropsWithoutRef<'div'>, UserProfile { + fallbackImageSrc?: ImageProps['src']; imageAlt?: string; } @@ -18,26 +22,20 @@ interface ProfileProps extends ComponentPropsWithoutRef<'div'>, UserProfile { * * @param name - 사용자 이름 * @param profileImageUrl - 백엔드에서 전달받은 프로필 이미지 URL + * @param fallbackImageSrc - 프로필 이미지 로딩 실패 시 표시할 대체 이미지 * @param imageAlt - 이미지 대체 텍스트. 전달하지 않으면 사용자 이름 기반으로 생성합니다. */ export function Profile({ name, profileImageUrl, + fallbackImageSrc = DefaultProfileImage, imageAlt = `${name} 프로필 이미지`, className, ...props }: ProfileProps) { return (
- {imageAlt} + {name}
); diff --git a/src/entities/user/ui/ProfileImage.tsx b/src/entities/user/ui/ProfileImage.tsx new file mode 100644 index 0000000..77fc374 --- /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 [failedSrc, setFailedSrc] = useState(null); + const imageSrc = failedSrc === src ? fallbackSrc : src; + + const handleError = () => { + if (imageSrc === fallbackSrc) { + return; + } + + setFailedSrc(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 0000000000000000000000000000000000000000..b1eb98bd4bd43596a73b9304d75874680f0e0e05 GIT binary patch literal 2512 zcmV;>2`~1EP)R*geMkU&~8ZAFtt zCnL3!TE+k^puMaEY0{*O!14^=^GKv1=jTAa2? zoA4(NO+rF!pZ>nr_d53dV_)09d+j9tNRI96lk4lRf9L$p`JHoq)~E^i<3F#&v$ z9XxX-L7IYyCrHQ=Yk{rzJr?$@poI-gP*cb^!?Q+z1TCz%pm_(Q-eG5QgqoP3gcTL^ z*x8R>Qz|<|4XoN#qv=D#r(S-Ggb)!FR+-7F|1?|m_6vFkY^?_*tW~EvfmLR*xzARr zn1H)(aeRisw=e&l>ZC3U{Fg!$yQk&Et$zI9=$MhG)+}f zwV?U-GRI=l0IhCDq7nUQwe4zEf|^~Tt1jc~RY@(*%C}QOHHT%c6)}P7tj^=J`~4r^ zg67-IRbS2B7%)G(FMJ6)c=k%u4I#rtCGtI*%2aGa&C$;}YQbo)51)RetQKVjH9b0a z+E>s}GFZ~=`F~!{NtLbg@aWVMvzSLgwMDq(v z{5(U`vkUb7{W+TA;~^45%$M(v+Fx*3XjsDu!;zJG#4Z}JQ?u*m$7y3%9uo^nnucM9~>nOfs z6QzE5SP(Y8eUouZO+E}qSSiK(SeAiHQVhor#}4ODA92Q?pf;0~Wh)_jcp{Hd`iG7uBhM=T_tWM!vdBL7_;; z+*#6kUa-(0trJ!J1dAC4cen|-Hab~yEIqeK$(!F9RvQ56Oy+Tq6+12TL$MTw0sxj7 zmYAT2+tq>IZ6(KI9c}dMJzW$K)&dTLf}l!iex4i(P0z z4qFclK~dd#Gut#2;${F)h%Z z+S$eJtF)Dx%Q8tncJ{*o>8R4)?x(3)h;3|hG(8Zrr*jht6N4|(bGdAs1lhNHN5k7r zJI`!&I-;(v8%Y}YB3+}1Xmx+SualaM9*$z!!Iv&x?Q;~@HC^i`LF`VT8Gvx@?2PQ# zyH)jq{Q){RrlKy-OV9-l{9GR=x;;fSy+9TD`hjfOEgPS39upQ6n=PW)ZNZ|7;MI2G zgZ}$E-!J~o2JaI1tfpI4eBe<%QGNaVzj54U*5mshsbOI0j;NcPUvQ6*ce(UXysB`oOYGUL z3Rhb;EJOvJHRpHiyx2>$zkB&PKJ@Q*yPxA1UCy#E?Q}0|yw+R#{PekpF>dOm*cFxTi)mrvNF1P==%4O$W=9hv9IP03GrAUxFlQW*LLFY;W z1yy!>cF}okZjppZi^s82lMVC=9;8`JyEWqZUE#TWZ01GBK1G6D8~uTrjj5S=$1%*| zOT!)e6fAU%1hG%&HUp#M&Sq#L06|&O8J%TKkuaZrIZ4gJRC?aw&EfIK@6VAqibgm% zB3tFb-wZ7$Hi9rOHu>}VyI;=^$izEnE)HNEc9`PCpNyk9}_m*zp-m-nsF>=_YU*LZ@!%Nxo1+ucJfvJ6 z0pa|(4lE6_&wApEkO+<-8O#T#4xTJLFMe_G%%ud+gnJ zk2eJ1xv6$@Qf+>5iLE+gST=4D4%YQJo240kj1U|a)K-dZLR@R*-55v2(n6uTXhiQR z-d^L_1v2>d<*RFZqpW}I)(M50DPO03xwASHv6IxU%<~tGA+HOy^}b)=RgtyRyb;tevH#|NY${LKl{T|TZU z<86HjSq5b?%;YX;9WHI6W{15-Rh&2bbq)@Nv9jO6W;+m;BBoiQ{ zv1p6Gv>RcHSWH6%wS%k@NQ8jB``B)d9n$$ty~aM&lC6{0HlX}ZsHv37(HK@?H|iI@ a?EC~b=_uT9&ldv#0000 Date: Sun, 5 Jul 2026 16:46:33 +0900 Subject: [PATCH 5/7] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20pro?= =?UTF-8?q?ps=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/user/ui/Profile.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/entities/user/ui/Profile.tsx b/src/entities/user/ui/Profile.tsx index bebcfcf..cc23fa9 100644 --- a/src/entities/user/ui/Profile.tsx +++ b/src/entities/user/ui/Profile.tsx @@ -1,15 +1,12 @@ -import type { ComponentPropsWithoutRef } from 'react'; - import type { ImageProps } from 'next/image'; import { DefaultProfileImage } from '@/shared/assets/images'; -import { cn } from '@/shared/styles/utils/cn'; import { ProfileImage } from './ProfileImage'; import type { UserProfile } from '../model/types'; -interface ProfileProps extends ComponentPropsWithoutRef<'div'>, UserProfile { +interface ProfileProps extends UserProfile { fallbackImageSrc?: ImageProps['src']; imageAlt?: string; } @@ -30,11 +27,9 @@ export function Profile({ profileImageUrl, fallbackImageSrc = DefaultProfileImage, imageAlt = `${name} 프로필 이미지`, - className, - ...props }: ProfileProps) { return ( -
+
{name}
From a0b9cbab1e9e944e6c280f4544f7622108b5c090 Mon Sep 17 00:00:00 2001 From: Leeseojeong Date: Sun, 5 Jul 2026 16:47:44 +0900 Subject: [PATCH 6/7] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=20=EC=83=81=ED=83=9C=20=EB=B3=80=EC=88=98=EB=AA=85=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/user/ui/ProfileImage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/entities/user/ui/ProfileImage.tsx b/src/entities/user/ui/ProfileImage.tsx index 77fc374..4c39604 100644 --- a/src/entities/user/ui/ProfileImage.tsx +++ b/src/entities/user/ui/ProfileImage.tsx @@ -12,15 +12,15 @@ interface ProfileImageProps { } export function ProfileImage({ alt, fallbackSrc, src }: ProfileImageProps) { - const [failedSrc, setFailedSrc] = useState(null); - const imageSrc = failedSrc === src ? fallbackSrc : src; + const [failedImageSrc, setFailedImageSrc] = useState(null); + const imageSrc = failedImageSrc === src ? fallbackSrc : src; const handleError = () => { if (imageSrc === fallbackSrc) { return; } - setFailedSrc(src); + setFailedImageSrc(src); }; return ( From b12c4153ba6f388a110d625ebdfae7f23010e994 Mon Sep 17 00:00:00 2001 From: Leeseojeong Date: Sun, 5 Jul 2026 16:52:22 +0900 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20PNG=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=ED=83=80=EC=9E=85=20=EC=84=A0=EC=96=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/types/images.d.ts | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/shared/types/images.d.ts 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; +}