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
11 changes: 10 additions & 1 deletion design-system/stories/buttons/TertiaryButton.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const meta = {
},
size: {
control: 'radio',
options: ['small', 'medium', 'large'], // medium 추가
options: ['small', 'medium', 'large', 'full'],
description: '버튼 크기',
defaultValue: 'large',
},
Expand Down Expand Up @@ -88,3 +88,12 @@ export const Medium: Story = {
size: 'medium',
},
};

export const Full: Story = {
args: {
label: '가장 큰 버튼',
type: 'button',
color: 'pink',
size: 'full',
},
};
3 changes: 2 additions & 1 deletion design-system/ui/buttons/TertiaryButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ interface TertiaryButtonProps {
label: string;
type: 'button' | 'submit';
color: 'pink' | 'black';
size: 'small' | 'medium' | 'large';
size: 'small' | 'medium' | 'large' | 'full';
disabled?: boolean;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
className?: string;
Expand All @@ -16,6 +16,7 @@ const TertiaryButton = ({ label, type, color, size, disabled, onClick, className
medium: 'px-4 py-1 text-sm',
large:
'text-sm sm:px-2.5 sm:py-2 sm:text-xs sm:rounded md:px-3 md:py-2.5 md:text-sm md:rounded-md lg:px-3 lg:py-2.5 lg:text-base lg:rounded-md',
full: 'w-full py-1 text-sm',
};

const colorStyle =
Expand Down
58 changes: 38 additions & 20 deletions design-system/ui/textFields/DefaultTextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ interface DefaultTextFieldProps {
onBlur?: (e: FocusEvent<HTMLInputElement>) => void;
placeholder?: string;
errorMessage?: string;
errorPosition?: 'bottom' | 'right';
className?: string;
labelClassName?: string;
detailClassName?:string;
detailClassName?: string;
disabled?: boolean;
maxLength?: number;
}
Expand All @@ -37,7 +38,8 @@ const DefaultTextField = forwardRef<HTMLInputElement, DefaultTextFieldProps>(
className = '',
labelClassName = '',
errorMessage,
detailClassName ='',
errorPosition = 'bottom',
detailClassName = '',
disabled = false,
maxLength,
...rest
Expand All @@ -47,27 +49,43 @@ const DefaultTextField = forwardRef<HTMLInputElement, DefaultTextFieldProps>(
return (
<div>
<label className={`block px-1 text-sm font-semibold text-gray-700 ${labelClassName}`}>{label}</label>
<label className={`block px-1 mb-1 font-medium text-placeholderText sm:text-10 md:text-13 ${detailClassName}`}>{detail}</label>
<div className={`flex items-center justify-center `}>
<label className={`block px-1 mb-1 font-medium text-placeholderText sm:text-10 md:text-13 ${detailClassName}`}>
{detail}
</label>
<div className="flex items-center justify-center relative">
{leftText && <div className="w-24 text-base font-bold whitespace-nowrap">{leftText}</div>}
<input
ref={ref}
value={value}
onChange={onChange}
onKeyDown={onKeyDown}
onFocus={onFocus}
onBlur={onBlur}
placeholder={placeholder}
disabled={disabled}
maxLength={maxLength}
{...rest}
className={`w-full border border-placeholderText rounded-[3px] px-2 py-1 outline-none placeholder:text-placeholderText text-xs font-light resize-none ${className} ${
errorMessage ? 'border-red-500' : ''
}`}
/>

<div className="relative w-full">
<input
ref={ref}
value={value}
onChange={onChange}
onKeyDown={onKeyDown}
onFocus={onFocus}
onBlur={onBlur}
placeholder={placeholder}
disabled={disabled}
{...rest}
className={`w-full h-11 px-3 py-2 border ${
errorMessage ? 'border-red-500' : 'border-placeholderText'
} rounded-[6px] text-sm placeholder:text-placeholderText outline-none ${className}`}
/>
{errorMessage && errorPosition === 'right' && (
<>
{/* 데스크탑일 때만 오른쪽에 보이기 */}
<p className="hidden md:block absolute left-full top-1/2 -translate-y-1/2 ml-2 text-xs text-red-500 whitespace-nowrap">
{errorMessage}
</p>
Comment on lines +76 to +78
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

오른쪽 에러 메시지 위치 조정이 필요합니다.

현재 left-fullml-2 조합은 에러 메시지가 입력 필드를 벗어나 화면 밖으로 나갈 수 있습니다.

-                <p className="hidden md:block absolute left-full top-1/2 -translate-y-1/2 ml-2 text-xs text-red-500 whitespace-nowrap">
+                <p className="hidden md:block absolute left-full top-1/2 -translate-y-1/2 ml-2 text-xs text-red-500 whitespace-nowrap z-10 bg-white px-1 rounded shadow-sm">
                  {errorMessage}
                </p>

또한 컨테이너에 오버플로우 처리를 추가해주세요:

-          <div className="relative w-full">
+          <div className="relative w-full overflow-visible">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<p className="hidden md:block absolute left-full top-1/2 -translate-y-1/2 ml-2 text-xs text-red-500 whitespace-nowrap">
{errorMessage}
</p>
<div className="relative w-full overflow-visible">
{/* ...your input markup... */}
{errorMessage && (
<p className="hidden md:block absolute left-full top-1/2 -translate-y-1/2 ml-2 text-xs text-red-500 whitespace-nowrap z-10 bg-white px-1 rounded shadow-sm">
{errorMessage}
</p>
)}
</div>
🤖 Prompt for AI Agents
In design-system/ui/textFields/DefaultTextField.tsx around lines 74 to 76, the
error message positioning uses 'left-full' and 'ml-2', which can cause the
message to overflow outside the viewport. Adjust the positioning to keep the
error message visible within the container, for example by using relative
positioning or adjusting left/right values. Additionally, add overflow handling
(like 'overflow-auto' or 'overflow-hidden') to the container element to prevent
content from spilling out visually.

{/* 모바일에서는 아래에 표시 */}
<p className="md:hidden mt-1 text-xs text-red-500">{errorMessage}</p>
</>
)}
</div>

{rightContent && <div className="ml-3">{rightContent}</div>}
</div>
{errorMessage && <p className="absolute px-1 text-10 md:text-xs text-red-500">{errorMessage}</p>}

{errorMessage && errorPosition !== 'right' && <p className="mt-1 text-xs text-red-500">{errorMessage}</p>}
</div>
);
}
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"@storybook/react": "^8.4.6",
"@storybook/react-vite": "^8.4.6",
"@storybook/test": "^8.4.6",
"@types/classnames": "^2.3.4",
"@types/js-cookie": "^3.0.6",
"@types/lodash": "^4.17.13",
"@types/node": "^22.10.5",
Expand Down
3 changes: 3 additions & 0 deletions public/assets/bottomBar/Arrow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions public/assets/bottomBar/Bookmark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions public/assets/bottomBar/Logout.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/app/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div className="w-full h-full min-h-screen bg-gray-50">
<div className="mx-auto w-full max-w-lg min-h-screen bg-white">
<div className="mx-auto w-full max-w-lg min-h-screen bg-white border border-gray-200">
<main>{children}</main>
<div id="portal" />
</div>
Expand Down
44 changes: 44 additions & 0 deletions src/entities/user/ui/BookmarkList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useNavigate } from 'react-router-dom';
import bookmark from '../../../../public/assets/bottomBar/Bookmark.svg';
import { useBookmarks } from '../../../features/bookmark/hook/useBookmarkHook';
import { formatDate } from '../../../shared/lib/date';
import EventInfo from './EventInfo';

const BookmarkList = () => {
const { data } = useBookmarks();
const navigate = useNavigate();

const sortedData = data ? [...data].sort((a, b) => a.id - b.id) : [];
const visibleEvents = sortedData.slice(0, 2);

return (
<div className="w-full h-full max-h-84 bg-white border-[0.5px] rounded-[10px] px-5 md:px-7 py-4 md:py-5">
<div className="flex flex-col gap-6 h-full">
<div className="flex items-center gap-3">
<img src={bookmark} alt="북마크 아이콘" className="w-5 h-5 md:w-6 md:h-6" />
<h1 className="text-20 md:text-22 font-bold">관심 이벤트</h1>
</div>
{visibleEvents.length > 0 ? (
visibleEvents.map(event => (
<EventInfo
key={event.id}
eventImageUrl={event.bannerImageUrl}
title={event.title}
date={formatDate(event.startDate)}
onClick={() => navigate(`/event-details/${event.id}`)}
/>
))
) : (
<p className="text-center text-sm md:text-base text-gray-500">관심 있는 이벤트가 없습니다.</p>
)}
<span
onClick={() => navigate('/bookmark')}
className="text-center text-main cursor-pointer font-semibold text-xs md:text-sm"
>
전체 관심 이벤트 보기
</span>
</div>
</div>
);
};
export default BookmarkList;
21 changes: 21 additions & 0 deletions src/entities/user/ui/EventInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
interface EventInfoProps {
eventImageUrl: string;
title: string;
date?: string;
onClick?: () => void;
}

const EventInfo = ({ eventImageUrl, title, date, onClick }: EventInfoProps) => {
return (
<div onClick={onClick} className="flex items-center gap-4 md:gap-6 cursor-pointer">
<img src={eventImageUrl} alt={`${title} 이미지`} className="w-16 h-16 md:w-20 md:h-20 rounded-[5px]" />
<div className="flex flex-col gap-2">
<span className="font-semibold text-base md:text-lg w-52 md:w-80 truncate overflow-hidden whitespace-nowrap">
{title}
</span>
<div className="text-gray-500 text-sm">{date}</div>
</div>
</div>
);
};
export default EventInfo;
148 changes: 148 additions & 0 deletions src/entities/user/ui/ProfileInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useEffect, useState } from 'react';
import { SubmitHandler, useForm } from 'react-hook-form';
import { myPageSchema } from '../../../shared/lib/formValidation';
import { formatPhoneNumber } from '../../../shared/utils/phoneFormatter';
import ProfileCircle from '../../../../design-system/ui/Profile';
import TertiaryButton from '../../../../design-system/ui/buttons/TertiaryButton';
import DefaultTextField from '../../../../design-system/ui/textFields/DefaultTextField';
import useAuthStore from '../../../app/provider/authStore';
import { useUserInfo, useUserUpdate } from '../../../features/join/hooks/useUserHook';
import { formatProfilName } from '../../../shared/lib/formatProfileName';

const ProfileInfo = () => {
const isLoggedIn = useAuthStore(state => state.isLoggedIn);
const { setName } = useAuthStore();
const { data, isLoading, error, refetch } = useUserInfo(isLoggedIn);
const { mutate: updateUser } = useUserUpdate();
const [isEditing, setIsEditing] = useState(false);

const {
register,
handleSubmit,
formState: { errors },
setValue,
} = useForm<{ name: string; phone: string }>({
defaultValues: { name: data?.name || '', phone: data?.phoneNumber || '' },
resolver: zodResolver(myPageSchema),
});

useEffect(() => {
if (data) {
setValue('name', data.name);
setValue('phone', data.phoneNumber);
}
}, [data, setValue]);
const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const formatted = formatPhoneNumber(e.target.value);
setValue('phone', formatted, { shouldValidate: true });
};

const onSubmit: SubmitHandler<{ name: string; phone: string }> = formData => {
const { name, phone } = formData;

if (!data?.id) {
alert('사용자 정보를 불러오는 데 실패했습니다. 다시 시도해주세요.');
return;
}

const updatedData = {
id: data.id,
name: name,
email: data.email,
phoneNumber: phone,
};

updateUser(updatedData, {
onSuccess: () => {
setName(name);
refetch();
setIsEditing(false);
},
onError: () => {
alert('정보 업데이트에 실패했습니다. 다시 시도해주세요.');
},
});
};

if (isLoading) {
return <div>로딩 중...</div>;
}
if (error) {
return <div>정보를 불러오는데 실패했습니다. 다시 시도해주세요.</div>;
}
Comment on lines +68 to +73
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

로딩 및 에러 상태 UI가 너무 단순합니다.

현재 로딩과 에러 상태가 단순한 텍스트로만 표시되어 있어 사용자 경험이 좋지 않습니다. 디자인 시스템의 컴포넌트를 활용하여 개선해주세요.

  if (isLoading) {
-    return <div>로딩 중...</div>;
+    return (
+      <div className="relative w-full h-52 md:h-56">
+        <div className="absolute inset-0 bg-main rounded-[10px]" />
+        <div className="relative z-10 bg-dashboardBg rounded-[10px] p-5 ml-2 h-full flex items-center justify-center">
+          <div className="text-center">
+            <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-main mx-auto mb-2"></div>
+            <p className="text-sm text-gray-600">프로필 정보를 불러오는 중...</p>
+          </div>
+        </div>
+      </div>
+    );
  }
  if (error) {
-    return <div>정보를 불러오는데 실패했습니다. 다시 시도해주세요.</div>;
+    return (
+      <div className="relative w-full h-52 md:h-56">
+        <div className="absolute inset-0 bg-main rounded-[10px]" />
+        <div className="relative z-10 bg-dashboardBg rounded-[10px] p-5 ml-2 h-full flex items-center justify-center">
+          <div className="text-center">
+            <p className="text-sm text-red-500 mb-2">프로필 정보를 불러오는데 실패했습니다.</p>
+            <TertiaryButton
+              label="다시 시도"
+              type="button"
+              color="pink"
+              size="small"
+              onClick={() => refetch()}
+            />
+          </div>
+        </div>
+      </div>
+    );
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (isLoading) {
return <div>로딩 중...</div>;
}
if (error) {
return <div>정보를 불러오는데 실패했습니다. 다시 시도해주세요.</div>;
}
if (isLoading) {
return (
<div className="relative w-full h-52 md:h-56">
<div className="absolute inset-0 bg-main rounded-[10px]" />
<div className="relative z-10 bg-dashboardBg rounded-[10px] p-5 ml-2 h-full flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-main mx-auto mb-2"></div>
<p className="text-sm text-gray-600">프로필 정보를 불러오는 중...</p>
</div>
</div>
</div>
);
}
if (error) {
return (
<div className="relative w-full h-52 md:h-56">
<div className="absolute inset-0 bg-main rounded-[10px]" />
<div className="relative z-10 bg-dashboardBg rounded-[10px] p-5 ml-2 h-full flex items-center justify-center">
<div className="text-center">
<p className="text-sm text-red-500 mb-2">프로필 정보를 불러오는데 실패했습니다.</p>
<TertiaryButton
label="다시 시도"
type="button"
color="pink"
size="small"
onClick={() => refetch()}
/>
</div>
</div>
</div>
);
}
🤖 Prompt for AI Agents
In src/entities/user/ui/ProfileInfo.tsx around lines 61 to 66, the loading and
error UI states are currently rendered as simple text, which results in poor
user experience. Replace these plain div elements with appropriate components
from the design system, such as a spinner or loader component for the loading
state and a styled alert or error message component for the error state, to
enhance visual feedback and consistency.


return (
<div className="relative w-full h-52 md:h-56">
<div className="absolute inset-0 bg-main rounded-[10px]" />
<div className="relative z-10 bg-dashboardBg rounded-[10px] p-5 ml-2 h-full">
<div className="flex flex-col items-start h-full">
<h1 className="text-20 md:text-22 font-bold">프로필 정보</h1>
{!isEditing ? (
<>
<div className="flex items-center gap-3">
<ProfileCircle
profile="userProfile"
name={formatProfilName(data?.name || '')}
className="w-16 h-16 md:w-18 md:h-18 text-xl md:text-2xl"
/>
<div className="flex flex-col gap-1 py-5 md:py-7 mb-2">
<span className="text-17 md:text-19 font-bold">{data?.name}</span>
<span className="text-14 md:text-16 text-gray-500">{data?.phoneNumber}</span>
</div>
</div>
<TertiaryButton
label="수정하기"
type="button"
color="pink"
size="full"
onClick={() => setIsEditing(true)}
/>
</>
) : (
<form onSubmit={handleSubmit(onSubmit)} className="w-full">
<div className="flex py-2 md:py-3 gap-3">
<ProfileCircle
profile="userProfile"
name={formatProfilName(data?.name || '')}
className="w-16 h-16 md:w-18 md:h-18 text-xl md:text-2xl"
/>
<div className="flex flex-col gap-1">
<DefaultTextField
{...register('name')}
errorPosition="right"
errorMessage={errors.name?.message}
className="h-9"
/>
<DefaultTextField
{...register('phone')}
onChange={handlePhoneChange}
errorPosition="right"
errorMessage={errors.phone?.message}
className="h-9"
/>
</div>
</div>
<div className="flex gap-2">
<TertiaryButton
label="취소하기"
type="button"
color="pink"
size="full"
onClick={() => {
setIsEditing(false);
setValue('name', data?.name || '');
setValue('phone', data?.phoneNumber || '');
}}
/>
<TertiaryButton label="수정하기" type="submit" color="pink" size="full" />
</div>
</form>
)}
</div>
</div>
</div>
);
};

export default ProfileInfo;
4 changes: 2 additions & 2 deletions src/features/event/ui/ShareEventModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import link from '../../../../public/assets/event-manage/details/Link.svg';
import kakao from '../../../../public/assets/event-manage/details/KaKao.svg';
import { shareToKakao } from '../../../shared/lib/kakaoShare';
import stripHtml from '../lib/stripHtml';
import EventInfo from '../../../entities/user/ui/EventInfo';

interface ShareEventModalProps {
closeModal: () => void;
Expand Down Expand Up @@ -71,8 +72,7 @@ const ShareEventModal = ({
</div>
<h1 className="font-semibold text-xl text-center mb-6">공유하기</h1>
<div className="flex items-center gap-5 mb-8">
<img src={eventImageUrl} alt="프로필 사진" className="w-20 h-20 rounded-[5px]" />
<span className="font-semibold text-lg">{title}</span>
<EventInfo eventImageUrl={eventImageUrl} title={title} />
</div>
<div className="flex flex-col gap-4 py-3">
<div onClick={handleCopyLink} className="flex items-center gap-4 cursor-pointer">
Expand Down
Loading