diff --git a/src/components/Common/Avatar/Avatar.stories.tsx b/src/components/Common/Avatar/Avatar.stories.tsx index 676f92fe9..a1148e085 100644 --- a/src/components/Common/Avatar/Avatar.stories.tsx +++ b/src/components/Common/Avatar/Avatar.stories.tsx @@ -1,53 +1,88 @@ import type { Meta, StoryObj } from '@storybook/react'; +import type { SingleAvatarProps } from '~/components/Common/Avatar/SingleAvatar'; + +import { userInfo } from '~/mocks/handlers/member/data'; import Avatar from './index'; -const meta: Meta = { +const disableArgs = ['userInfo']; +const disableArgsTypes = disableArgs.reduce((acc, cur) => { + (acc as Record)[cur] = { + table: { + disable: true, + }, + }; + return acc; +}, {}); + +const meta: Meta = { title: 'Avatar', component: Avatar, + argTypes: { + ...disableArgsTypes, + }, }; export default meta; -type AvatarStory = StoryObj; +interface AvatarStoryProps { + size: SingleAvatarProps['size']; + empty: boolean; +} + +type AvatarStory = StoryObj; + export const SingleAvatar: AvatarStory = { name: 'Avatar', args: { size: 'sm', - major: true, - nickName: '전공자', - isEmpty: false, + empty: false, + }, + render: (args: AvatarStoryProps) => { + const { size, empty } = args; + return ( + + ); }, }; -export const AvatarGroup = () => { - const data = [ - { - major: true, - nickName: '전공자', - }, - { - major: false, - nickName: '비전공자', - }, - { - major: true, - nickName: 'Eng', - }, - { - major: false, - nickName: 'Test', - }, - { - major: true, - nickName: 'Extra', - }, - ]; - return ( - - {data.map((d) => ( - - ))} - - ); +interface AvatarGroupStoryProps { + avatarCount: number; + maxCount: number; + visibleCount: number; + size: SingleAvatarProps['size']; +} +type AvatarGroupStory = StoryObj; + +export const AvatarGroup: AvatarGroupStory = { + name: 'AvatarGroup', + argTypes: {}, + args: { + avatarCount: 2, + maxCount: 4, + visibleCount: 4, + size: 'sm', + }, + + render: (args: AvatarGroupStoryProps) => { + const { avatarCount, maxCount, visibleCount, size } = args; + const data = Array(avatarCount) + .fill(undefined) + .map(() => userInfo.certifiedSsafyUserInfo); + + return ( + <> +
+ + {data.map((d) => ( + + ))} + +
+ + ); + }, }; diff --git a/src/components/Common/Avatar/AvatarGroup.tsx b/src/components/Common/Avatar/AvatarGroup.tsx index 1b09d2349..7b41f1ec1 100644 --- a/src/components/Common/Avatar/AvatarGroup.tsx +++ b/src/components/Common/Avatar/AvatarGroup.tsx @@ -1,46 +1,56 @@ -import type { ReactNode, ComponentPropsWithoutRef, ReactElement } from 'react'; +import type { SingleAvatarProps } from './SingleAvatar'; +import type { ReactNode, ComponentPropsWithoutRef } from 'react'; import { css } from '@emotion/react'; import { Children, isValidElement } from 'react'; -import { flex, fontCss } from '~/styles/utils'; +import { fontCss, inlineFlex } from '~/styles/utils'; import SingleAvatar from './SingleAvatar'; export interface AvatarGroupProps extends ComponentPropsWithoutRef<'div'> { children: ReactNode; visibleCount?: number; + maxCount: number; } const AvatarGroup = (props: AvatarGroupProps) => { - const { children, visibleCount = 4, ...rest } = props; + const { children, maxCount, visibleCount = 4, ...rest } = props; - const validAvatars = Children.toArray(children).filter(isValidElement); + const validAvatars = Children.toArray(children).filter( + isValidElement + ); const visibleAvatars = validAvatars.slice(0, visibleCount); - - const restAvatarsNumber = validAvatars.length - visibleCount; - const emptyAvatarsNumber = visibleCount - validAvatars.length; + const emptyAvatarsCount = visibleCount - validAvatars.length; + const restAvatarsCount = maxCount - visibleCount; + const avatarSize = validAvatars[0].props?.size || 'sm'; return (
{visibleAvatars} - {Array.from({ length: emptyAvatarsNumber }).map((_, i) => ( - + {Array.from({ length: emptyAvatarsCount }).map((_, i) => ( + ))} - {restAvatarsNumber > 0 && +{restAvatarsNumber}} + {restAvatarsCount > 0 && ( + + +{restAvatarsCount} + + )}
); }; const selfCss = css( - { - '> div': { - marginLeft: -2, - }, - }, - flex('center', 'center', 'row') + { '> div': { marginLeft: -4 } }, + inlineFlex('center', 'center', 'row'), + fontCss.family.auto ); -const textCss = css(fontCss.style.R12); +const textCss = css({ marginLeft: 4 }); +const textSizeCss = { + sm: css(fontCss.style.B12), + md: css(fontCss.style.B14), + lg: css(fontCss.style.B28), +}; export default AvatarGroup; diff --git a/src/components/Common/Avatar/SingleAvatar.tsx b/src/components/Common/Avatar/SingleAvatar.tsx index 7f64782d8..d6f226292 100644 --- a/src/components/Common/Avatar/SingleAvatar.tsx +++ b/src/components/Common/Avatar/SingleAvatar.tsx @@ -1,91 +1,78 @@ import type { SerializedStyles } from '@emotion/react'; import type { ComponentPropsWithoutRef } from 'react'; +import type { UserInfo } from '~/services/member'; import { css } from '@emotion/react'; import React from 'react'; -import { flex, fontCss } from '~/styles/utils'; +import { flex, fontCss, palettes } from '~/styles/utils'; -export interface AvatarProps extends ComponentPropsWithoutRef<'div'> { +export interface SingleAvatarProps extends ComponentPropsWithoutRef<'div'> { size?: AvatarSize; - major?: boolean; - nickName?: string; - isEmpty?: boolean; + userInfo?: UserInfo; } type AvatarSize = 'sm' | 'md' | 'lg'; -type BackgroundColor = 'major' | 'nonMajor'; -const SingleAvatar = (props: AvatarProps) => { - const { - major = false, - size = 'sm', - nickName = '샆사운드', - isEmpty = false, - ...rest - } = props; - // 현재 설계상 major라는 이름으로 전공여부를 가지고오게되어 그대로 사용하기 위해 major라는 명칭을 사용하게 됨. +const SingleAvatar = (props: SingleAvatarProps) => { + const { size = 'sm', userInfo, ...restProps } = props; return (
- {isEmpty || ( - - {getFirstText(nickName)} + {userInfo && ( + + {getFirstText(userInfo.nickname)} )}
); }; -const getFirstText = (str: string) => str.at(0); +const getFirstText = (str: string) => str.at(0) || ''; const selfCss = css( { borderRadius: 100, - color: '#000', - border: '0.6px solid #fff', + color: palettes.black, + border: `0.6px solid ${palettes.white}`, + backgroundColor: palettes.nonMajor, }, - flex('center', 'center', 'row') + flex('center', 'center', 'row'), + fontCss.family.auto ); -const fontStyleCss = fontCss.family.manrope; - const sizeCss: Record = { - sm: css({ width: 12, height: 12 }), - md: css({ width: 18, height: 18 }), + sm: css({ width: 16, height: 16 }), + md: css({ width: 20, height: 20 }), lg: css({ width: 40, height: 40 }), }; +const lineHeightCss = css({ lineHeight: 1 }); + const textCss: Record = { sm: css(fontCss.style.B12), md: css(fontCss.style.B14), - lg: css(fontCss.style.B24), + lg: css(fontCss.style.B28), }; const textCapitalizeCss = css({ textTransform: 'capitalize', }); -const backgroundCss: Record = { - major: css({ - backgroundColor: '#71E498', - // todo 팔레트로 이관 - }), - nonMajor: css({ - backgroundColor: '#FFBF75', - }), -}; +const majorCss = css({ + backgroundColor: palettes.major, +}); const emptyCss = css({ - backgroundColor: '#F0F0F0', - border: '1px dotted #292929', + backgroundColor: palettes.white, + border: `1px dotted ${palettes.grey0}`, }); export default SingleAvatar; diff --git a/src/components/Common/SsafyIcon/Track.stories.tsx b/src/components/Common/SsafyIcon/Track.stories.tsx index 67ba80537..8297c8da0 100644 --- a/src/components/Common/SsafyIcon/Track.stories.tsx +++ b/src/components/Common/SsafyIcon/Track.stories.tsx @@ -16,11 +16,6 @@ type TrackIconStory = StoryObj; export const TrackIcon: TrackIconStory = { args: { name: SsafyTrack.MOBILE, size: TrackSize.SM1 }, argTypes: { - label: { - table: { - disable: true, - }, - }, style: { table: { disable: true, @@ -43,7 +38,7 @@ export const AllTrackIcons: TrackIconStory = {
{sizes.map((size) => (
- +
))}
diff --git a/src/components/Common/SsafyIcon/Track.tsx b/src/components/Common/SsafyIcon/Track.tsx index f9074c741..622d9cd8c 100644 --- a/src/components/Common/SsafyIcon/Track.tsx +++ b/src/components/Common/SsafyIcon/Track.tsx @@ -12,36 +12,27 @@ import PythonTrack from '~/assets/images/track-python.svg'; import Uncertified from '~/assets/images/track-uncertified.svg'; import { SsafyTrack } from '~/services/member/utils'; import { inlineFlex } from '~/styles/utils'; +import { defaultify } from '~/utils'; -// MajorType을 ~/services/member 에서 가져오면 참조 오류가 발생하는데 이유를 잘 모르겠습니다. +// SsafyTrack을 ~/services/member 에서 가져오면 참조 오류가 발생하는데 이유를 잘 모르겠습니다. // import 구문 옆에다 주석 달면 린트 오류가 발생해서 여기 적어둡니다. export interface TrackProps { - /** - * API 명세에 `null`이 들어올 수 있다고 되어있는데 - * 만약 그렇다면 `fetcher`에서 `null`을 `undefined`로 변환할 예정입니다. - */ name?: Track; - label?: string; size?: TrackSize; style?: CSSProperties; theme?: Track extends 'fallback' | undefined ? FallbackTheme : undefined; } const Track = (props: TrackProps) => { - const { - name = 'fallback', - label = name, - size = TrackSize.SM1, - style = {}, - theme, - } = props; + const { name = 'fallback', size = TrackSize.SM1, style = {}, theme } = props; + const safeName = defaultify(name, [null]).to('fallback') as NonNullable; - const TrackComponent = tracks[name]; + const TrackComponent = tracks[safeName]; return (
- +
diff --git a/src/components/Name/Name.stories.tsx b/src/components/Name/Name.stories.tsx index e61c24f5e..f8aac97bd 100644 --- a/src/components/Name/Name.stories.tsx +++ b/src/components/Name/Name.stories.tsx @@ -1,6 +1,8 @@ +import type { NameProps } from './index'; import type { Meta, StoryObj } from '@storybook/react'; -import { SsafyTrack, CertificationState } from '~/services/member/utils'; +import { userInfo } from '~/mocks/handlers/member/data'; +import { CertificationState, SsafyTrack } from '~/services/member'; import Name from './index'; @@ -8,43 +10,85 @@ const meta: Meta = { title: 'Name', component: Name, tags: ['autodocs'], - argTypes: {}, + argTypes: { + userInfo: { + table: { + disable: true, + }, + }, + }, }; export default meta; -type NameStory = StoryObj; +type NameStory = StoryObj; +interface NameStoryArgs { + nickname: string; + isMajor: boolean; + certificationState: CertificationState; + track: SsafyTrack; + size: NameProps['size']; +} -export const NameStory: NameStory = { - name: 'Name', +export const Default: NameStory = { + args: { + nickname: 'Kimee', + isMajor: false, + certificationState: CertificationState.UNCERTIFIED, + track: SsafyTrack.EMBEDDED, + size: 'sm', + }, + argTypes: { + certificationState: { + control: 'radio', + options: Object.values(CertificationState), + }, + track: { + control: 'radio', + options: Object.values(SsafyTrack), + }, + }, + + render: (args: NameStoryArgs) => { + const { nickname, isMajor, certificationState, track, size, ...restArgs } = + args; + const ssafyInfo = + certificationState === CertificationState.CERTIFIED + ? { + certificationState, + majorTrack: track, + } + : { + certificationState, + majorTrack: null, + }; + + const user = { + ...userInfo.certifiedSsafyUserInfo, + nickname, + isMajor, + ssafyInfo: { + ...userInfo.certifiedSsafyUserInfo.ssafyInfo, + ...ssafyInfo, + }, + }; + + return ; + }, }; export const Certified = () => { - return ( - - ); + return ; }; export const UnCertified = () => { - return ( - - ); + return ; +}; + +export const LongName = () => { + const user = { + ...userInfo.certifiedSsafyUserInfo, + nickname: '한글한글한글한글한글한', + }; + return ; }; diff --git a/src/components/Name/index.tsx b/src/components/Name/index.tsx index 8e90a2026..3109bc52e 100644 --- a/src/components/Name/index.tsx +++ b/src/components/Name/index.tsx @@ -1,44 +1,44 @@ import type { SerializedStyles } from '@emotion/react'; -import type { UserSsafyInfo, UserBasicInfo } from '~/services/member/utils'; +import type { UserInfo } from '~/services/member/utils'; import { css } from '@emotion/react'; -import { SsafyTrack } from '~/services/member/utils'; +import { Avatar, SsafyIcon, TrackSize } from '~/components/Common'; +import { CertificationState } from '~/services/member/utils'; import { fontCss, inlineFlex } from '~/styles/utils'; -import { Avatar, SsafyIcon, TrackSize } from '../Common'; - -type BasicInfo = Pick; export type NameProps = { - userSsafyInfo: UserSsafyInfo; + userInfo: UserInfo; + withAvatar?: boolean; size?: NameSize; -} & BasicInfo; +}; type NameSize = 'sm' | 'md' | 'lg'; const Name = (props: NameProps) => { + const { size = 'sm', withAvatar = true, userInfo } = props; const { - size = 'lg', - nickname = '쌒사운드', - isMajor, - userSsafyInfo = { - ssafyMember: true, - ssafyInfo: { - majorTrack: SsafyTrack['MOBILE'], - certificationState: true, - }, - }, - } = props; + // basic info + nickname, + ssafyMember, - const { ssafyMember, ssafyInfo } = userSsafyInfo; + // ssafy info + ssafyInfo, + } = userInfo; + + const showBadge = + ssafyMember && + ssafyInfo?.certificationState === CertificationState.CERTIFIED; return ( - - - {nickname} - {ssafyMember && ( + + {withAvatar && } + + {nickname} + + {showBadge && ( )} @@ -46,19 +46,12 @@ const Name = (props: NameProps) => { ); }; -const selfCss = css(inlineFlex('center', '', 'row')); +const selfCss = css(inlineFlex('center', '', 'row', 2)); -const gapCss: Record = { - sm: css({ - gap: 2, - }), - md: css({ - gap: 4, - }), - lg: css({ - gap: 5, - }), -}; +const textBaseCss = css({ + maxWidth: 230, + wordBreak: 'break-word', +}); const textCss: Record = { sm: css(fontCss.style.B12), @@ -67,9 +60,9 @@ const textCss: Record = { }; const trackSize: Record = { - sm: TrackSize['SM1'], - md: TrackSize['SM2'], - lg: TrackSize['SM3'], + sm: TrackSize.SM1, + md: TrackSize.SM2, + lg: TrackSize.SM3, }; export default Name; diff --git a/src/mocks/handlers/member/data.ts b/src/mocks/handlers/member/data.ts index ce4a73c2c..a445ead99 100644 --- a/src/mocks/handlers/member/data.ts +++ b/src/mocks/handlers/member/data.ts @@ -1,6 +1,6 @@ import type { UserInfo } from '~/services/member'; -import { CertificationState } from '~/services/member'; +import { CertificationState, SsafyTrack } from '~/services/member'; const initialUserInfo: UserInfo = { memberId: 434, @@ -9,15 +9,27 @@ const initialUserInfo: UserInfo = { ssafyMember: null, isMajor: false, }; -const ssafyUserInfo: UserInfo = { +const certifiedSsafyUserInfo: UserInfo = { + ...initialUserInfo, + ssafyMember: true, + isMajor: false, + ssafyInfo: { + semester: 1, + campus: '구미', + certificationState: CertificationState.CERTIFIED, + majorTrack: SsafyTrack.EMBEDDED, + }, +}; + +const uncertifiedSsafyUserInfo: UserInfo = { ...initialUserInfo, ssafyMember: true, isMajor: false, ssafyInfo: { semester: 1, campus: '구미', - majorTrack: undefined, certificationState: CertificationState.UNCERTIFIED, + majorTrack: null, }, }; @@ -29,6 +41,7 @@ const nonSsafyUserInfo: UserInfo = { export const userInfo = { initialUserInfo, - ssafyUserInfo, + certifiedSsafyUserInfo, + uncertifiedSsafyUserInfo, nonSsafyUserInfo, }; diff --git a/src/mocks/handlers/member/index.ts b/src/mocks/handlers/member/index.ts index 10fc715c0..7fc7d50cd 100644 --- a/src/mocks/handlers/member/index.ts +++ b/src/mocks/handlers/member/index.ts @@ -2,7 +2,8 @@ import type { GetMyInfoApiData, UpdateMyInfoParams, UserInfo, - CertifyStudentApiData } from '~/services/member'; + CertifyStudentApiData, +} from '~/services/member'; import type { ApiErrorResponse } from '~/types'; import { rest } from 'msw'; @@ -18,7 +19,7 @@ const getMyInfo = rest.get( return res( ctx.delay(500), // ...mockSuccess(ctx, userInfo.initialUserInfo) - ...mockSuccess(ctx, userInfo.ssafyUserInfo) + ...mockSuccess(ctx, userInfo.certifiedSsafyUserInfo) // ...mockSuccess(ctx, userInfo.nonSsafyUserInfo) // ...mockError(ctx, 'code', 'message', 404), ); @@ -35,7 +36,7 @@ const updateMyInfo = rest.patch< let response; if (body.ssafyMember) { - response = userInfo.ssafyUserInfo; + response = userInfo.certifiedSsafyUserInfo; } else { response = userInfo.nonSsafyUserInfo; } diff --git a/src/services/member/utils/types.ts b/src/services/member/utils/types.ts index b30085ad2..d68ff5fc0 100644 --- a/src/services/member/utils/types.ts +++ b/src/services/member/utils/types.ts @@ -17,12 +17,12 @@ export interface SsafyBasicInfo { export type SsafyInfo = | (SsafyBasicInfo & { - majorTrack?: undefined; certificationState: CertificationState.UNCERTIFIED; + majorTrack?: null | undefined; }) | (SsafyBasicInfo & { - majorTrack?: SsafyTrack; certificationState: CertificationState.CERTIFIED; + majorTrack: SsafyTrack; }); export enum CertificationState {