diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx index ad17151f..99c2b9df 100644 --- a/apps/admin/src/App.tsx +++ b/apps/admin/src/App.tsx @@ -38,6 +38,7 @@ import { } from './pages'; import ShowAddPage from './pages/ShowAddPage'; import { Suspense } from 'react'; +import { domAnimation, LazyMotion } from 'framer-motion'; setDefaultOptions({ locale: ko }); @@ -142,7 +143,9 @@ const routes: RouteObject[] = [ element: ( - + + + ), diff --git a/apps/admin/src/components/LinkFormDialogContent/LinkFormDialogContent.styles.ts b/apps/admin/src/components/LinkFormDialogContent/LinkFormDialogContent.styles.ts index d72debe3..e0234826 100644 --- a/apps/admin/src/components/LinkFormDialogContent/LinkFormDialogContent.styles.ts +++ b/apps/admin/src/components/LinkFormDialogContent/LinkFormDialogContent.styles.ts @@ -1,5 +1,5 @@ -import styled from '@emotion/styled' -import { mq_lg } from '@boolti/ui' +import styled from '@emotion/styled'; +import { mq_lg } from '@boolti/ui'; interface LabelProps { required?: boolean; @@ -17,7 +17,7 @@ const LinkForm = styled.form` ${mq_lg} { padding: 0; } -` +`; const LinkFormControl = styled.div` margin-bottom: 28px; @@ -25,7 +25,7 @@ const LinkFormControl = styled.div` & > div { width: 100%; } -` +`; const LinkFormButtonWrapper = styled.div` display: flex; @@ -35,7 +35,7 @@ const LinkFormButtonWrapper = styled.div` margin-top: 4px; button { - width: ${({ isEditMode }) => isEditMode ? 'auto' : '100%'}; + width: ${({ isEditMode }) => (isEditMode ? 'auto' : '100%')}; } ${mq_lg} { @@ -43,7 +43,7 @@ const LinkFormButtonWrapper = styled.div` width: auto; } } -` +`; const Label = styled.label` ${({ theme }) => theme.typo.b3}; @@ -53,7 +53,7 @@ const Label = styled.label` position: relative; &::after { - content: ${({ required }) => (required ? "'*'" : "none")}; + content: ${({ required }) => (required ? "'*'" : 'none')}; color: ${({ theme }) => theme.palette.status.error}; ${({ theme }) => theme.typo.b1}; line-height: 22px; @@ -66,12 +66,12 @@ const LinkDeleteButton = styled.button` line-height: 22px; text-decoration: underline; cursor: pointer; -` +`; export default { LinkForm, LinkFormControl, LinkFormButtonWrapper, Label, - LinkDeleteButton -} + LinkDeleteButton, +}; diff --git a/apps/admin/src/components/LinkFormDialogContent/index.tsx b/apps/admin/src/components/LinkFormDialogContent/index.tsx index eca6822b..89b3a722 100644 --- a/apps/admin/src/components/LinkFormDialogContent/index.tsx +++ b/apps/admin/src/components/LinkFormDialogContent/index.tsx @@ -1,9 +1,9 @@ import { useForm } from 'react-hook-form'; import Styled from './LinkFormDialogContent.styles'; import { Button, TextField } from '@boolti/ui'; -import { UserProfileLink } from '@boolti/api'; +import { UserLink } from '@boolti/api'; -export type LinkFormInputs = UserProfileLink; +export type LinkFormInputs = UserLink; interface LinkFormDialogContentProps { defaultValues?: LinkFormInputs; @@ -11,9 +11,13 @@ interface LinkFormDialogContentProps { onDelete?: () => void; } -const LinkFormDialogContent = ({ defaultValues, onSubmit, onDelete }: LinkFormDialogContentProps) => { +const LinkFormDialogContent = ({ + defaultValues, + onSubmit, + onDelete, +}: LinkFormDialogContentProps) => { const linkForm = useForm({ - defaultValues + defaultValues, }); const isEditMode = !!defaultValues; @@ -23,7 +27,7 @@ const LinkFormDialogContent = ({ defaultValues, onSubmit, onDelete }: LinkFormDi title: data.title.trim(), link: data.link.trim(), }); - } + }; return ( @@ -47,7 +51,12 @@ const LinkFormDialogContent = ({ defaultValues, onSubmit, onDelete }: LinkFormDi /> - {isEditMode && onDelete && ( @@ -57,7 +66,7 @@ const LinkFormDialogContent = ({ defaultValues, onSubmit, onDelete }: LinkFormDi )} - ) -} + ); +}; export default LinkFormDialogContent; diff --git a/apps/admin/src/components/SettingDialogContent/SettingDialogContent.styles.ts b/apps/admin/src/components/SettingDialogContent/SettingDialogContent.styles.ts index 52922741..6fe6a179 100644 --- a/apps/admin/src/components/SettingDialogContent/SettingDialogContent.styles.ts +++ b/apps/admin/src/components/SettingDialogContent/SettingDialogContent.styles.ts @@ -116,7 +116,7 @@ const SettingContentHeader = styled.div` display: flex; justify-content: space-between; } -` +`; const SettingContentTitle = styled.h3` ${({ theme }) => theme.typo.h1}; @@ -134,7 +134,7 @@ const SettingContentSubmitWrapper = styled.div` ${mq_lg} { display: block; } -` +`; const SettingContentSubmitWrapperMobile = styled.div` position: fixed; @@ -154,9 +154,9 @@ const SettingContentSubmitWrapperMobile = styled.div` ${mq_lg} { display: none; } -` +`; -const SettingContentForm = styled.form`` +const SettingContentForm = styled.form``; const SettingContentFormControl = styled.div` margin-bottom: 24px; @@ -173,14 +173,14 @@ const ProfileImageWrapper = styled.div` width: 100px; height: 100px; } -` +`; const ProfileImage = styled.img` width: 100px; height: 100px; border-radius: 100px; object-fit: cover; -` +`; const DefaultProfileImage = styled(DefaultUserProfileIcon)` border-radius: 100px; @@ -200,7 +200,7 @@ const ProfileImageEditButton = styled.label` bottom: 0; left: calc(100px - 42px + 8px); cursor: pointer; -` +`; const Label = styled.label` ${({ theme }) => theme.typo.b3}; @@ -210,7 +210,7 @@ const Label = styled.label` position: relative; &::after { - content: ${({ required }) => (required ? "'*'" : "none")}; + content: ${({ required }) => (required ? "'*'" : 'none')}; color: ${({ theme }) => theme.palette.status.error}; ${({ theme }) => theme.typo.b1}; line-height: 22px; @@ -231,19 +231,19 @@ const LinkItem = styled.div` align-items: center; gap: 16px; height: 56px; -` +`; const LinkInfo = styled.div` display: flex; flex-direction: column; gap: 2px; min-width: 0; -` +`; const LinkTitle = styled.p` ${({ theme }) => theme.typo.sh1}; color: ${({ theme }) => theme.palette.grey.g90}; -` +`; const LinkDescription = styled.p` ${({ theme }) => theme.typo.b1}; @@ -251,7 +251,7 @@ const LinkDescription = styled.p` overflow: hidden; white-space: nowrap; text-overflow: ellipsis; -` +`; const LinkEditButton = styled.button` width: 24px; @@ -260,7 +260,7 @@ const LinkEditButton = styled.button` justify-content: center; align-items: center; cursor: pointer; -` +`; const ConnectedServiceList = styled.div` display: flex; @@ -314,12 +314,12 @@ const SettingDescriptionItem = styled.li``; const TextAreaWrapper = styled.div` position: relative; height: 122px; -` +`; const TextAreaBox = styled.div` border: 1px solid ${({ theme, hasError }) => - hasError ? `${theme.palette.status.error} !important` : theme.palette.grey.g20}; + hasError ? `${theme.palette.status.error} !important` : theme.palette.grey.g20}; border-radius: 4px; background-color: ${({ theme }) => theme.palette.grey.w}; position: absolute; @@ -341,13 +341,13 @@ const TextAreaBox = styled.div` border: 1px solid ${({ theme }) => theme.palette.grey.g20}; background: ${({ theme }) => theme.palette.grey.g10}; } -` +`; const TextArea = styled.textarea` position: absolute; top: 0; left: 0; - width: calc(100% - 12px - 12px); + width: calc(100% - 12px - 12px); height: 72px; margin: 12px 12px 38px; color: ${({ theme }) => theme.palette.grey.g90}; @@ -368,7 +368,7 @@ const TextAreaCount = styled.span` bottom: 12px; right: 12px; z-index: 3; -` +`; export default { SettingDialogContent, diff --git a/apps/admin/src/components/SettingDialogContent/index.tsx b/apps/admin/src/components/SettingDialogContent/index.tsx index bae743aa..12b1c0ff 100644 --- a/apps/admin/src/components/SettingDialogContent/index.tsx +++ b/apps/admin/src/components/SettingDialogContent/index.tsx @@ -36,7 +36,7 @@ const AppleIcon = () => { type ProfileFormInputs = { nickname: string; introduction: string; -} +}; interface SettingDialogContentProps { onDeleteAccount?: () => void; @@ -51,7 +51,7 @@ const NICKNAME_ERROR_MESSAGE = { required: '1자 이상 입력해 주세요.', minLength: '1자 이상 입력해 주세요.', maxLength: `${MAX_NICKNAME_LENGTH}자 이내로 입력해 주세요.`, -} +}; const SettingDialogContent = ({ onDeleteAccount }: SettingDialogContentProps) => { const theme = useTheme(); @@ -64,7 +64,8 @@ const SettingDialogContent = ({ onDeleteAccount }: SettingDialogContentProps) => const uploadProfileImageMutation = useUploadProfileImage(); const editProfileMutation = useEditUserProfile(); - const { register, handleSubmit, setValue, watch, setError, clearErrors, formState } = useForm(); + const { register, handleSubmit, setValue, watch, setError, clearErrors, formState } = + useForm(); const [profileImageFile, setProfileImageFile] = useState(null); const [profileImagePreview, setProfileImagePreview] = useState(null); @@ -78,7 +79,7 @@ const SettingDialogContent = ({ onDeleteAccount }: SettingDialogContentProps) => if (files && files.length > 0) { setProfileImageFile(files[0]); } - } + }; const submitHandler = async (data: ProfileFormInputs) => { if (editProfileMutation.isLoading) return; @@ -107,7 +108,7 @@ const SettingDialogContent = ({ onDeleteAccount }: SettingDialogContentProps) => } catch (error) { toast.error('프로필 정보를 저장하는 중 문제가 발생했습니다.'); } - } + }; useEffect(() => { if (!userProfile) return; @@ -116,29 +117,37 @@ const SettingDialogContent = ({ onDeleteAccount }: SettingDialogContentProps) => setValue('introduction', userProfile.introduction ?? ''); setLinks(userProfile.link); - }, [setValue, userProfile]) + }, [setValue, userProfile]); useEffect(() => { - if (!profileImageFile) return + if (!profileImageFile) return; - const url = URL.createObjectURL(profileImageFile) - setProfileImagePreview(url) + const url = URL.createObjectURL(profileImageFile); + setProfileImagePreview(url); - return () => URL.revokeObjectURL(url) - }, [profileImageFile]) + return () => URL.revokeObjectURL(url); + }, [profileImageFile]); return ( - { - setCurrentMenu('profile'); - }}> + { + setCurrentMenu('profile'); + }} + > 프로필 - { - setCurrentMenu('account'); - }}> + { + setCurrentMenu('account'); + }} + > 계정 @@ -160,19 +169,29 @@ const SettingDialogContent = ({ onDeleteAccount }: SettingDialogContentProps) => {profileImagePreview ?? userProfile?.imgPath ? ( - + ) : ( )} - + - 닉네임 + + 닉네임 + // 문자열이 0자일 때 에러 메시지 출력 if (event.target.value.trim().length === 0) { - setError('nickname', { type: 'minLength', message: NICKNAME_ERROR_MESSAGE.minLength }); + setError('nickname', { + type: 'minLength', + message: NICKNAME_ERROR_MESSAGE.minLength, + }); - return + return; } // 문자열 20자 초과 시 에러 메시지 출력 if (event.target.value.trim().length > MAX_NICKNAME_LENGTH) { - setError('nickname', { type: 'maxLength', message: NICKNAME_ERROR_MESSAGE.maxLength }); + setError('nickname', { + type: 'maxLength', + message: NICKNAME_ERROR_MESSAGE.maxLength, + }); - return + return; } // 이외의 경우에는 에러 메시지 미출력 - clearErrors('nickname') - } + clearErrors('nickname'); + }, })} /> @@ -225,7 +250,8 @@ const SettingDialogContent = ({ onDeleteAccount }: SettingDialogContentProps) => placeholder="예) 재즈와 펑크락을 좋아해요" maxLength={MAX_INTRODUCTION_LENGTH} {...register('introduction', { - maxLength: MAX_INTRODUCTION_LENGTH, onChange(event) { + maxLength: MAX_INTRODUCTION_LENGTH, + onChange(event) { if (event.target.value.length > MAX_INTRODUCTION_LENGTH) { event.target.value = event.target.value.slice(0, MAX_INTRODUCTION_LENGTH); } @@ -242,19 +268,25 @@ const SettingDialogContent = ({ onDeleteAccount }: SettingDialogContentProps) => SNS 링크 - @@ -265,30 +297,40 @@ const SettingDialogContent = ({ onDeleteAccount }: SettingDialogContentProps) => {link.title} {link.link} - { - linkDialog.open({ - title: '링크 편집', - content: ( - { - setLinks((prev) => prev.map((item) => { - if (item.title === link.title && item.link === link.link) { - return { title: data.title, link: data.link }; - } - - return item; - })); - linkDialog.close(); - }} - onDelete={() => { - setLinks((prev) => prev.filter((item) => item.title !== link.title && item.link !== link.link)); - linkDialog.close(); - }} - /> - ), - }) - }}> + { + linkDialog.open({ + title: '링크 편집', + content: ( + { + setLinks((prev) => + prev.map((item) => { + if (item.title === link.title && item.link === link.link) { + return { title: data.title, link: data.link }; + } + + return item; + }), + ); + linkDialog.close(); + }} + onDelete={() => { + setLinks((prev) => + prev.filter( + (item) => + item.title !== link.title && item.link !== link.link, + ), + ); + linkDialog.close(); + }} + /> + ), + }); + }} + > @@ -297,85 +339,84 @@ const SettingDialogContent = ({ onDeleteAccount }: SettingDialogContentProps) => - + )} - { - currentMenu === 'account' && ( - - - 계정 - - - 식별 코드 - { - event.preventDefault(); - }} - style={{ caretColor: 'transparent' }} - /> - - - 연결 서비스 - - {userProfile?.oauthType === 'KAKAO' && ( - - 카카오 - - )} - {userProfile?.oauthType === 'APPLE' && ( - - Apple - - )} - - - - 계정 삭제 - - - 주최한 공연 정보는 사라지지 않아요. - - - 예매한 티켓은 전부 사라지며 복구할 수 없어요. - - - 삭제일로 부터 30일 이내 재 로그인 시 삭제를 취소할 수 있어요. - - - - - ) - } - - + style={{ caretColor: 'transparent' }} + /> + + + 연결 서비스 + + {userProfile?.oauthType === 'KAKAO' && ( + + 카카오 + + )} + {userProfile?.oauthType === 'APPLE' && ( + + Apple + + )} + + + + 계정 삭제 + + + 주최한 공연 정보는 사라지지 않아요. + + + 예매한 티켓은 전부 사라지며 복구할 수 없어요. + + + 삭제일로 부터 30일 이내 재 로그인 시 삭제를 취소할 수 있어요. + + + + + )} + ); }; diff --git a/apps/admin/src/components/ShowCastInfo/ShowCastInfo.styles.ts b/apps/admin/src/components/ShowCastInfo/ShowCastInfo.styles.ts new file mode 100644 index 00000000..323d4b52 --- /dev/null +++ b/apps/admin/src/components/ShowCastInfo/ShowCastInfo.styles.ts @@ -0,0 +1,123 @@ +import { Button } from '@boolti/ui'; +import styled from '@emotion/styled'; +import { m } from 'framer-motion'; + +const Container = styled.div` + border-radius: 8px; + background: ${({ theme }) => theme.palette.grey.w}; + box-shadow: 0px 8px 14px 0px ${({ theme }) => theme.palette.shadow}; + + &:not(:first-of-type) { + margin-top: 20px; + } +`; + +const Header = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + border-radius: 8px 8px 0px 0px; + border: 1px solid ${({ theme }) => theme.palette.grey.g20}; + color: ${({ theme }) => theme.palette.grey.g90}; + ${({ theme }) => theme.typo.sh2}; + padding: 24px 28px; +`; + +const EditButton = styled(Button)` + padding: 13px 18px; + & > svg { + margin-right: 8px; + } +`; + +const Cast = styled(m.div)` + display: flex; + flex-wrap: wrap; + border: 1px solid ${({ theme }) => theme.palette.grey.g20}; + border-top: none; + border-bottom: none; + overflow: hidden; + max-height: 574px; + overflow: scroll; + ::-webkit-scrollbar { + display: none; + } +`; + +const CastItem = styled.div` + display: flex; + justify-content: flex-start; + align-items: center; + padding: 14px 28px; + flex: 1 0 50%; + max-width: 50%; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + + &:first-of-type, + &:nth-of-type(2) { + padding-top: 18px; + } +`; + +const UserImage = styled.div` + box-sizing: border-box; + width: 32px; + height: 32px; + border-radius: 32px; + background-repeat: no-repeat; + background-size: cover; + background-position: center; + background-image: var(--imgPath); + margin-right: 6px; + flex: 0 0 auto; +`; + +const Username = styled.span` + color: ${({ theme }) => theme.palette.grey.g90}; + ${({ theme }) => theme.typo.b3}; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + flex: 0 1 auto; +`; + +const Rolename = styled.span` + color: ${({ theme }) => theme.palette.grey.g50}; + ${({ theme }) => theme.typo.b3}; + margin-left: 4px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + flex: 0 1 auto; +`; + +const CollapseButton = styled.button` + display: flex; + justify-content: center; + align-items: center; + width: 100%; + border-radius: 0px 0px 8px 8px; + border: 1px solid ${({ theme }) => theme.palette.grey.g20}; + border-top: none; + ${({ theme }) => theme.typo.sh1}; + color: ${({ theme }) => theme.palette.grey.g70}; + padding: 19px 32px; + + & > svg { + margin-left: 8px; + } +`; + +export default { + Container, + Header, + Cast, + CollapseButton, + EditButton, + CastItem, + UserImage, + Username, + Rolename, +}; diff --git a/apps/admin/src/components/ShowCastInfo/index.tsx b/apps/admin/src/components/ShowCastInfo/index.tsx new file mode 100644 index 00000000..a5c59dc1 --- /dev/null +++ b/apps/admin/src/components/ShowCastInfo/index.tsx @@ -0,0 +1,95 @@ +import { useDialog } from '@boolti/ui'; + +import Styled from './ShowCastInfo.styles'; +import { EditIcon, ChevronDownIcon, ChevronUpIcon } from '@boolti/icon'; +import { useState } from 'react'; +import ShowCastInfoFormDialogContent, { + TempShowCastInfoFormInput, +} from '../ShowCastInfoFormDialogContent'; + +interface Props { + showCastInfo: TempShowCastInfoFormInput; + onSave: (value: TempShowCastInfoFormInput) => Promise; + onDelete?: () => Promise; +} + +const ShowCastInfo = ({ showCastInfo, onSave, onDelete }: Props) => { + const { members = [] } = showCastInfo; + const memberLength = members.length ?? 0; + const dialog = useDialog(); + const [isOpen, setIsOpen] = useState(false); + + const toggle = () => setIsOpen((prev) => !prev); + + return ( + + + {showCastInfo.name} + { + e.preventDefault(); + dialog.open({ + title: '출연진 정보 편집', + isAuto: true, + content: ( + { + try { + await onSave(castInfo); + dialog.close(); + } catch { + return new Promise((_, reject) => reject('저장 중 오류가 발생하였습니다.')); + } + }} + prevShowCastInfo={showCastInfo} + onDelete={async () => { + try { + await onDelete?.(); + dialog.close(); + } catch { + return new Promise((_, reject) => reject('삭제 중 오류가 발생하였습니다.')); + } + }} + /> + ), + }); + }} + > + + 편집하기 + + + + {members.map((member) => ( + + + {member.userNickname} + ({member.roleName}) + + ))} + + {memberLength > 0 && ( + { + e.preventDefault(); + toggle(); + }} + > + {isOpen ? '팀원 리스트 접기' : '팀원 리스트 펼쳐보기'} + {isOpen ? : } + + )} + + ); +}; + +export default ShowCastInfo; diff --git a/apps/admin/src/components/ShowCastInfoFormDialogContent/ShowCastInfoFormDialogContent.styles.ts b/apps/admin/src/components/ShowCastInfoFormDialogContent/ShowCastInfoFormDialogContent.styles.ts new file mode 100644 index 00000000..58ef49f8 --- /dev/null +++ b/apps/admin/src/components/ShowCastInfoFormDialogContent/ShowCastInfoFormDialogContent.styles.ts @@ -0,0 +1,160 @@ +import { Button } from '@boolti/ui'; +import styled from '@emotion/styled'; + +interface ShowInfoFormLabelProps { + required?: boolean; +} + +interface InputWrapperProps { + text: string; +} + +const ShowInfoFormLabel = styled.span` + display: block; + ${({ theme }) => theme.typo.b3}; + color: ${({ theme }) => theme.palette.grey.g90}; + margin-bottom: 8px; + + &::after { + content: '*'; + ${({ theme }) => theme.typo.b3}; + color: ${({ theme }) => theme.palette.status.error}; + display: ${({ required }) => (required ? 'inline' : 'none')}; + margin-left: 2px; + } +`; + +const MemberList = styled.div` + max-height: 364px; + overflow-y: scroll; + ::-webkit-scrollbar { + display: none; + } +`; + +const InputWrapper = styled.div` + ${({ theme }) => theme.typo.b3}; + border: 1px solid ${({ text, theme }) => (text ? theme.palette.grey.g90 : theme.palette.grey.g20)}; + border-radius: 4px; + background-color: ${({ theme }) => theme.palette.grey.w}; + padding: 8px 12px; + height: 48px; + margin-right: 8px; + flex: auto; + position: relative; + display: flex; + align-items: center; + width: calc(50% - 32px); +`; + +const TextFieldWrap = styled.div` + margin-bottom: 28px; + + & > div { + width: auto; + } +`; + +const HashTag = styled.span` + color: ${({ theme }) => theme.palette.grey.g90}; + line-height: 24px; + padding-right: 4px; +`; + +const Input = styled.input` + width: ${({ value }) => (value ? 'calc(100% - 80px)' : '100%')}; + line-height: 24px; + + &::placeholder { + color: ${({ theme }) => theme.palette.grey.g30}; + } +`; + +const Row = styled.div` + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 20px; +`; + +const TrashCanButton = styled.button` + cursor: pointer; + height: 100%; +`; + +const MemberAddButton = styled.button` + display: flex; + justify-content: center; + align-items: center; + width: 100%; + border-radius: 4px; + padding: 11px 0; + border: 1px dashed ${({ theme }) => theme.palette.grey.g20}; + background: var(--W-White, #fff); + ${({ theme }) => theme.typo.sh1}; + color: ${({ theme }) => theme.palette.grey.g40}; + width: 536px; + + & > svg { + margin-right: 8px; + } +`; + +const RegisterButton = styled(Button)` + margin-left: auto; +`; + +const UserImage = styled.div` + box-sizing: border-box; + width: 32px; + height: 32px; + border-radius: 32px; + background-repeat: no-repeat; + background-size: cover; + background-position: center; + background-image: var(--imgPath); +`; + +const Username = styled.span` + ${({ theme }) => theme.typo.b3}; + color: ${({ theme }) => theme.palette.grey.g90}; + margin: 0 8px; + flex: 1 1 auto; +`; + +const RemoveButton = styled.button` + width: 24px; + height: 24px; + cursor: pointer; +`; + +const DeleteButton = styled.button` + cursor: pointer; + ${({ theme }) => theme.typo.sh1}; + text-decoration: underline; +`; + +const ButtonWrap = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 32px; +`; + +export default { + ShowInfoFormLabel, + InputWrapper, + HashTag, + Input, + Row, + TrashCanButton, + MemberAddButton, + RegisterButton, + MemberList, + UserImage, + Username, + RemoveButton, + TextFieldWrap, + ButtonWrap, + DeleteButton, +}; diff --git a/apps/admin/src/components/ShowCastInfoFormDialogContent/index.tsx b/apps/admin/src/components/ShowCastInfoFormDialogContent/index.tsx new file mode 100644 index 00000000..4b49adc0 --- /dev/null +++ b/apps/admin/src/components/ShowCastInfoFormDialogContent/index.tsx @@ -0,0 +1,251 @@ +import { TextField, useConfirm, useToast } from '@boolti/ui'; +import { Controller, useFieldArray, useForm } from 'react-hook-form'; +import Styled from './ShowCastInfoFormDialogContent.styles'; +import { useState } from 'react'; +import { useBodyScrollLock } from '~/hooks/useBodyScrollLock'; +import { ClearIcon, PlusIcon, TrashIcon } from '@boolti/icon'; +import { Member, ShowCastTeamCreateOrUpdateRequest, queryKeys, useQueryClient } from '@boolti/api'; +import { replaceUserCode } from '~/utils/replace'; + +export interface TempShowCastInfoFormInput { + name: string; + members?: Array>; +} + +interface Props { + prevShowCastInfo?: TempShowCastInfoFormInput; + onDelete?: () => Promise; + onSave: (value: TempShowCastInfoFormInput) => Promise; +} + +const ShowCastInfoFormDialogContent = ({ onDelete, prevShowCastInfo, onSave }: Props) => { + const queryClient = useQueryClient(); + + const previousShowCastInfoMemberLength = prevShowCastInfo?.members?.length ?? 0; + const defaultValues = { + name: prevShowCastInfo?.name, + members: previousShowCastInfoMemberLength > 0 ? prevShowCastInfo?.members : [{}], + }; + const { control, getValues, watch } = useForm({ + defaultValues, + }); + const { fields, append, remove, update } = useFieldArray({ + control, + name: 'members', + }); + const watchMemberFields = watch('members') ?? []; + const controlledFields = fields.map((field, index) => { + return { + ...field, + ...watchMemberFields[index], + }; + }); + + const disabled = !getValues('name'); + + const toast = useToast(); + const confirm = useConfirm(); + + useBodyScrollLock(true); + + const [hasBlurred, setHasBlurred] = useState< + Record + >({ + name: false, + members: [], + }); + + return ( + <> + 팀명 + ( + + { + onBlur(); + setHasBlurred((prev) => ({ ...prev, name: true })); + }} + value={value ?? ''} + errorMessage={hasBlurred.name && !value ? '필수 입력사항입니다.' : undefined} + /> + + )} + name="name" + /> + 팀원 + + {controlledFields.map((controlledField, index) => ( + + ( + + {controlledField.userImgPath && controlledField.userNickname ? ( + <> + + {controlledField.userNickname} + { + update(index, { roleName: controlledField.roleName }); + }} + > + + + + ) : ( + <> + # + { + const nextValue = replaceUserCode(e.target.value); + onChange(nextValue); + }} + onBlur={async (event) => { + onBlur(); + const userCode = event.target.value; + if (userCode !== '') { + try { + const { imgPath, nickname } = await queryClient.fetchQuery( + queryKeys.user.userCode(event.target.value), + ); + update(index, { + ...controlledField, + userImgPath: imgPath, + userNickname: nickname, + }); + } catch { + toast.error( + '불티에 회원으로 등록된 식별 코드로만 등록이 가능합니다.' + + '\n' + + '식별 코드를 확인 후 다시 시도해 주세요.', + ); + } + } + }} + value={controlledField.userCode ?? ''} + /> + + )} + + )} + name={`members.${index}.userCode`} + /> + ( + + { + onBlur(); + }} + value={controlledField.roleName ?? ''} + /> + + )} + name={`members.${index}.roleName`} + /> + { + const isConfirm = await confirm('팀원 정보를 삭제하시겠어요?', { + confirm: '삭제하기', + cancel: '취소하기', + }); + + if (isConfirm) { + toast.success('팀원 정보를 삭제했습니다.'); + remove(index); + } + }} + > + + + + ))} + { + append({}); + }} + > + + 팀원 추가 + + + + {onDelete && ( + { + const isConfirm = await confirm('팀 정보를 삭제하시겠어요?', { + confirm: '삭제하기', + cancel: '취소하기', + }); + + if (isConfirm) { + try { + onDelete(); + toast.success('팀 정보를 삭제했습니다.'); + } catch { + toast.error('알 수 없는 오류가 발생했습니다.'); + } + } + }} + > + 팀 삭제 + + )} + { + e.preventDefault(); + + const name = getValues('name'); + const members = (getValues('members') ?? []).filter( + (member) => + member.userImgPath && member.userNickname && member.roleName && member.userCode, + ); + + try { + await onSave({ name, members }); + toast.success( + onDelete ? '출연진 정보를 수정했습니다.' : '출연진 정보를 생성했습니다.', + ); + } catch { + toast.error('알 수 없는 오류가 발생했습니다.'); + } + }} + > + 등록하기 + + + + ); +}; + +export default ShowCastInfoFormDialogContent; diff --git a/apps/admin/src/components/ShowInfoFormContent/ShowBasicInfoFormContent.tsx b/apps/admin/src/components/ShowInfoFormContent/ShowBasicInfoFormContent.tsx index fcea10c6..dc31121f 100644 --- a/apps/admin/src/components/ShowInfoFormContent/ShowBasicInfoFormContent.tsx +++ b/apps/admin/src/components/ShowInfoFormContent/ShowBasicInfoFormContent.tsx @@ -78,7 +78,11 @@ const ShowBasicInfoFormContent = ({ return ( - 기본 정보 + + + 기본 정보 + + 공연 포스터 diff --git a/apps/admin/src/components/ShowInfoFormContent/ShowCastInfoFormContent.tsx b/apps/admin/src/components/ShowInfoFormContent/ShowCastInfoFormContent.tsx new file mode 100644 index 00000000..9693028d --- /dev/null +++ b/apps/admin/src/components/ShowInfoFormContent/ShowCastInfoFormContent.tsx @@ -0,0 +1,58 @@ +import { Button, useDialog } from '@boolti/ui'; + +import Styled from './ShowInfoFormContent.styles'; +import { PlusIcon } from '@boolti/icon'; +import ShowCastInfoFormDialogContent, { + TempShowCastInfoFormInput, +} from '../ShowCastInfoFormDialogContent'; + +interface Props { + onSave: (value: TempShowCastInfoFormInput) => Promise; +} + +const ShowCastInfoFormContent = ({ onSave }: Props) => { + const dialog = useDialog(); + + return ( + + + + 출연진 정보 + + 출연진 정보를 팀 단위로 등록해 주세요. +
+ 정보는 공연 등록 이후에도 수정 및 추가할 수 있어요. +
+
+ +
+
+ ); +}; + +export default ShowCastInfoFormContent; diff --git a/apps/admin/src/components/ShowInfoFormContent/ShowDetailInfoFormContent.tsx b/apps/admin/src/components/ShowInfoFormContent/ShowDetailInfoFormContent.tsx index f6e3dd25..36774ed4 100644 --- a/apps/admin/src/components/ShowInfoFormContent/ShowDetailInfoFormContent.tsx +++ b/apps/admin/src/components/ShowInfoFormContent/ShowDetailInfoFormContent.tsx @@ -23,7 +23,11 @@ const ShowDetailInfoFormContent = ({ form, disabled }: ShowDetailInfoFormContent return ( - 상세 정보 + + + 상세 정보 + + 공연 내용 diff --git a/apps/admin/src/components/ShowInfoFormContent/ShowInfoFormContent.styles.ts b/apps/admin/src/components/ShowInfoFormContent/ShowInfoFormContent.styles.ts index 79be6da0..19b414c9 100644 --- a/apps/admin/src/components/ShowInfoFormContent/ShowInfoFormContent.styles.ts +++ b/apps/admin/src/components/ShowInfoFormContent/ShowInfoFormContent.styles.ts @@ -31,16 +31,37 @@ interface MobileTicketActionProps { const ShowInfoFormGroup = styled.div``; +const ShowInfoFormGroupHeader = styled.div` + display: flex; + justify-content: space-between; +`; + +const ShowInfoFormGroupInfo = styled.div` + flex: 1; + margin-bottom: 16px; + display: flex; + flex-direction: column; + gap: 2px; +`; + const ShowInfoFormTitle = styled.h3` ${({ theme }) => theme.typo.sh2}; color: ${({ theme }) => theme.palette.grey.g90}; - margin-bottom: 16px; ${mq_lg} { ${({ theme }) => theme.typo.h1}; } `; +const ShowInfoFormSubtitle = styled.p` + ${({ theme }) => theme.typo.b1}; + color: ${({ theme }) => theme.palette.grey.g60}; + + strong { + font-weight: 600; + } +`; + const ShowInfoFormRow = styled.div` margin-bottom: 28px; display: flex; @@ -515,7 +536,10 @@ const MobileTicketAction = styled.div` export default { ShowInfoFormGroup, + ShowInfoFormGroupHeader, + ShowInfoFormGroupInfo, ShowInfoFormTitle, + ShowInfoFormSubtitle, ShowInfoFormRow, ShowInfoFormContent, ShowInfoFormLabel, diff --git a/apps/admin/src/components/ShowInfoFormContent/types.ts b/apps/admin/src/components/ShowInfoFormContent/types.ts index c82df41d..53503244 100644 --- a/apps/admin/src/components/ShowInfoFormContent/types.ts +++ b/apps/admin/src/components/ShowInfoFormContent/types.ts @@ -16,3 +16,12 @@ export interface ShowTicketFormInputs { endDate: string; ticketNotice: string; } + +export interface ShowTeamFromInputs { + name: string; + members?: Array<{ + id?: number; + userCode: string; + roleName: string; + }>; +} diff --git a/apps/admin/src/index.css b/apps/admin/src/index.css index 0d1aff96..14578df4 100644 --- a/apps/admin/src/index.css +++ b/apps/admin/src/index.css @@ -1,4 +1,4 @@ -@import url("https://cdnjs.cloudflare.com/ajax/libs/pretendard/1.3.9/static/pretendard-dynamic-subset.min.css"); +@import url('https://cdnjs.cloudflare.com/ajax/libs/pretendard/1.3.9/static/pretendard-dynamic-subset.min.css'); @font-face { font-family: 'SB Aggro'; @@ -8,5 +8,26 @@ } * { - font-family: "Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont, system-ui, Roboto, "Helvetica Neue", "Segoe UI", "Apple SD Gothic Neo", "Noto Sans KR", "Malgun Gothic", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", sans-serif; + font-family: + 'Pretendard Variable', + Pretendard, + -apple-system, + BlinkMacSystemFont, + system-ui, + Roboto, + 'Helvetica Neue', + 'Segoe UI', + 'Apple SD Gothic Neo', + 'Noto Sans KR', + 'Malgun Gothic', + 'Apple Color Emoji', + 'Segoe UI Emoji', + 'Segoe UI Symbol', + sans-serif; +} + +body { + &::-webkit-scrollbar { + display: none; + } } diff --git a/apps/admin/src/pages/Landing/index.tsx b/apps/admin/src/pages/Landing/index.tsx index b3e20255..a50160a7 100644 --- a/apps/admin/src/pages/Landing/index.tsx +++ b/apps/admin/src/pages/Landing/index.tsx @@ -1,12 +1,11 @@ import { Footer } from '@boolti/ui'; -import { domAnimation, LazyMotion } from 'framer-motion'; import { Header, KeyVisual, MoreInformation, OrganizerSection, UserSection } from './components'; import Styled from './LandingPage.styles'; const LandingPage = () => { return ( - + <>
@@ -22,7 +21,7 @@ const LandingPage = () => {