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
/>
-
- )
-}
+ );
+};
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 링크
- } type="button" onClick={() => {
- linkDialog.open({
- title: '링크 추가',
- content: (
- {
- setLinks((prev) => [...prev, { title: data.title, link: data.link }]);
- linkDialog.close();
- }}
- />
- )
- })
- }}>
+ }
+ type="button"
+ onClick={() => {
+ linkDialog.open({
+ title: '링크 추가',
+ content: (
+ {
+ setLinks((prev) => [...prev, { title: data.title, link: data.link }]);
+ linkDialog.close();
+ }}
+ />
+ ),
+ });
+ }}
+ >
링크 추가
@@ -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일 이내 재 로그인 시 삭제를 취소할 수 있어요.
-
-
- {
- accountDeleteDialog.open({
- content: (
- {
- onDeleteAccount?.();
- accountDeleteDialog.close();
- }}
- />
- ),
- mobileType: 'centerPopup',
- });
+ {currentMenu === 'account' && (
+
+
+ 계정
+
+
+ 식별 코드
+ {
+ event.preventDefault();
}}
- >
- 삭제하기
-
-
- )
- }
-
-
+ style={{ caretColor: 'transparent' }}
+ />
+
+
+ 연결 서비스
+
+ {userProfile?.oauthType === 'KAKAO' && (
+
+ 카카오
+
+ )}
+ {userProfile?.oauthType === 'APPLE' && (
+
+ Apple
+
+ )}
+
+
+
+ 계정 삭제
+
+
+ 주최한 공연 정보는 사라지지 않아요.
+
+
+ 예매한 티켓은 전부 사라지며 복구할 수 없어요.
+
+
+ 삭제일로 부터 30일 이내 재 로그인 시 삭제를 취소할 수 있어요.
+
+
+ {
+ accountDeleteDialog.open({
+ content: (
+ {
+ onDeleteAccount?.();
+ accountDeleteDialog.close();
+ }}
+ />
+ ),
+ mobileType: 'centerPopup',
+ });
+ }}
+ >
+ 삭제하기
+
+
+ )}
+
);
};
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 (
+
+
+
+ 출연진 정보
+
+ 출연진 정보를 팀 단위로 등록해 주세요.
+
+ 정보는 공연 등록 이후에도 수정 및 추가할 수 있어요.
+
+
+ }
+ onClick={() => {
+ dialog.open({
+ isAuto: true,
+ title: '출연진 정보 등록',
+ content: (
+ {
+ try {
+ await onSave(value);
+ dialog.close();
+ } catch {
+ return new Promise((_, reject) => reject('저장 중 오류가 발생하였습니다.'));
+ }
+ }}
+ />
+ ),
+ });
+ }}
+ >
+ 등록하기
+
+
+
+ );
+};
+
+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 = () => {
-
+ >
);
};
diff --git a/apps/admin/src/pages/ShowAddPage/index.tsx b/apps/admin/src/pages/ShowAddPage/index.tsx
index d55a2bb4..3bf48dfc 100644
--- a/apps/admin/src/pages/ShowAddPage/index.tsx
+++ b/apps/admin/src/pages/ShowAddPage/index.tsx
@@ -1,4 +1,9 @@
-import { ImageFile, useAddShow, useUploadShowImage } from '@boolti/api';
+import {
+ ImageFile,
+ ShowCastTeamCreateOrUpdateRequest,
+ useAddShow,
+ useUploadShowImage,
+} from '@boolti/api';
import { ArrowLeftIcon } from '@boolti/icon';
import { Button, useToast } from '@boolti/ui';
import { useState } from 'react';
@@ -18,6 +23,9 @@ import { ShowInfoFormInputs, ShowTicketFormInputs } from '~/components/ShowInfoF
import { PATH } from '~/constants/routes';
import Styled from './ShowAddPage.styles';
+import ShowCastInfoFormContent from '~/components/ShowInfoFormContent/ShowCastInfoFormContent';
+import ShowCastInfo from '~/components/ShowCastInfo';
+import { TempShowCastInfoFormInput } from '~/components/ShowCastInfoFormDialogContent';
interface ShowAddPageProps {
step: 'info' | 'ticket';
@@ -32,6 +40,7 @@ const ShowAddPage = ({ step }: ShowAddPageProps) => {
const showInfoForm = useForm();
const showTicketForm = useForm();
+ const [showCastInfo, setShowCastInfo] = useState([]);
const uploadShowImageMutation = useUploadShowImage();
const addShowMutation = useAddShow();
@@ -77,6 +86,16 @@ const ShowAddPage = ({ step }: ShowAddPageProps) => {
ticketName: ticket.name,
totalForSale: ticket.quantity,
})),
+ castTeams: showCastInfo.map(({ name, members }) => ({
+ name,
+ members: members
+ ?.filter(({ id, userCode, roleName }) => id && userCode && roleName)
+ .map(({ id, userCode, roleName }) => ({
+ id,
+ userCode,
+ roleName,
+ })),
+ })) as ShowCastTeamCreateOrUpdateRequest[],
});
navigate(PATH.SHOW_ADD_COMPLETE);
@@ -145,6 +164,34 @@ const ShowAddPage = ({ step }: ShowAddPageProps) => {
+
+ {
+ setShowCastInfo((prev) => [...prev, showCastInfoFormInput]);
+ return new Promise((reslve) => reslve());
+ }}
+ />
+ {showCastInfo.map((info, index) => (
+ {
+ setShowCastInfo((prev) =>
+ prev.map((prevCastInfo, currentIndex) =>
+ index === currentIndex ? showCastInfoFormInput : prevCastInfo,
+ ),
+ );
+ return new Promise((reslve) => reslve());
+ }}
+ onDelete={() => {
+ setShowCastInfo((prev) =>
+ prev.filter((_, currentIndex) => index !== currentIndex),
+ );
+ return new Promise((reslve) => reslve());
+ }}
+ />
+ ))}
+
{
+ const queryClient = useQueryClient();
const params = useParams<{ showId: string }>();
const navigate = useNavigate();
const [myHostInfo] = useAtom(myHostInfoAtom);
@@ -40,10 +51,14 @@ const ShowInfoPage = () => {
const showId = Number(params!.showId);
const { data: show } = useShowDetail(showId);
const { data: showSalesInfo } = useShowSalesInfo(showId);
+ const { data: castTeamList } = useCastTeamList(showId);
const editShowInfoMutation = useEditShowInfo();
const uploadShowImageMutation = useUploadShowImage();
const deleteShowMutation = useDeleteShow();
+ const putCastTeams = usePutCastTeams();
+ const postCastTeams = usePostCastTeams();
+ const deleteCastTeams = useDeleteCastTeams();
const toast = useToast();
const confirm = useConfirm();
@@ -129,7 +144,9 @@ const ShowInfoPage = () => {
setShowImages(show.images);
}, [show, showInfoForm]);
- if (!show || !showSalesInfo) return null;
+ if (!show || !showSalesInfo || !castTeamList) {
+ return null;
+ }
const salesStarted = compareAsc(new Date(showSalesInfo.salesStartTime), new Date()) === -1;
@@ -188,6 +205,66 @@ const ShowInfoPage = () => {
저장하기
+
+
+
+ {
+ await postCastTeams.mutateAsync(
+ {
+ showId,
+ name,
+ members: members
+ ?.filter(({ id, userCode, roleName }) => id && userCode && roleName)
+ .map(({ id, userCode, roleName }) => ({
+ id,
+ userCode,
+ roleName,
+ })) as ShowCastTeamCreateOrUpdateRequest['members'],
+ },
+ {
+ onSuccess: () => {
+ queryClient.invalidateQueries(queryKeys.castTeams.list(showId));
+ },
+ },
+ );
+ }}
+ />
+ {castTeamList.map((info, index) => (
+ {
+ await putCastTeams.mutateAsync(
+ {
+ name,
+ members: members
+ ?.filter(({ id, userCode, roleName }) => id && userCode && roleName)
+ .map(({ id, userCode, roleName }) => ({
+ id,
+ userCode,
+ roleName,
+ })) as ShowCastTeamCreateOrUpdateRequest['members'],
+ castTeamId: info.id,
+ },
+ {
+ onSuccess: () => {
+ queryClient.invalidateQueries(queryKeys.castTeams.list(showId));
+ },
+ },
+ );
+ }}
+ onDelete={async () => {
+ await deleteCastTeams.mutateAsync(info.id, {
+ onSuccess: () => {
+ queryClient.invalidateQueries(queryKeys.castTeams.list(showId));
+ },
+ });
+ }}
+ />
+ ))}
+
+
{
+ return text
+ .replace(/[^a-z0-9]/gi, '')
+ .toUpperCase()
+ .slice(0, 8);
+};
diff --git a/packages/api/src/mutations/index.ts b/packages/api/src/mutations/index.ts
index 1e3490f2..a2cb8c1e 100644
--- a/packages/api/src/mutations/index.ts
+++ b/packages/api/src/mutations/index.ts
@@ -33,8 +33,14 @@ import useDeleteHost from './useDeleteHost';
import useDeleteMe from './useDeleteMe';
import useEditUserProfile from './useEditUserProfile';
import useUploadProfileImage from './useUploadProfileImage';
+import usePutCastTeams from './usePutCastTeams';
+import useDeleteCastTeams from './useDeleteCastTeams';
+import usePostCastTeams from './usePostCastTeams';
export {
+ usePostCastTeams,
+ usePutCastTeams,
+ useDeleteCastTeams,
useAddBankAccount,
useAddShow,
useAdminCreateSettlementStatement,
diff --git a/packages/api/src/mutations/useAddShow.ts b/packages/api/src/mutations/useAddShow.ts
index c7c4d980..08c9de6c 100644
--- a/packages/api/src/mutations/useAddShow.ts
+++ b/packages/api/src/mutations/useAddShow.ts
@@ -1,47 +1,15 @@
import { useMutation } from '@tanstack/react-query';
import { fetcher } from '../fetcher';
-
-interface PostAddShowRequest {
- name: string;
- images: {
- sequence: number;
- thumbnailPath: string;
- path: string;
- }[];
- date: string;
- runningTime: number;
- place: {
- name: string;
- streetAddress: string;
- detailAddress: string;
- };
- notice: string;
- host: {
- name: string;
- phoneNumber: string;
- };
- salesStartTime: string;
- salesEndTime: string;
- ticketNotice: string;
- salesTickets: {
- ticketName: string;
- price: number;
- totalForSale: number;
- }[];
- invitationTickets: {
- ticketName: string;
- totalForSale: number;
- }[];
-}
+import { ShowCreateRequest } from '../types';
type PostAddShowResponse = number;
-const postAddShow = (body: PostAddShowRequest) =>
+const postAddShow = (body: ShowCreateRequest) =>
fetcher.post('web/v1/host/shows', {
json: body,
});
-const useAddShow = () => useMutation((body: PostAddShowRequest) => postAddShow(body));
+const useAddShow = () => useMutation((body: ShowCreateRequest) => postAddShow(body));
export default useAddShow;
diff --git a/packages/api/src/mutations/useDeleteCastTeams.ts b/packages/api/src/mutations/useDeleteCastTeams.ts
new file mode 100644
index 00000000..953d833d
--- /dev/null
+++ b/packages/api/src/mutations/useDeleteCastTeams.ts
@@ -0,0 +1,10 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { fetcher } from '../fetcher';
+
+const deleteCastTeams = (castTeamId: number) =>
+ fetcher.delete(`web/v1/cast-teams/${castTeamId}`, {});
+
+const useDeleteCastTeams = () => useMutation({ mutationFn: deleteCastTeams });
+
+export default useDeleteCastTeams;
diff --git a/packages/api/src/mutations/useDeleteMe.ts b/packages/api/src/mutations/useDeleteMe.ts
index c38dc66e..126ec0fc 100644
--- a/packages/api/src/mutations/useDeleteMe.ts
+++ b/packages/api/src/mutations/useDeleteMe.ts
@@ -7,10 +7,8 @@ interface DeleteMeRequestBody {
appleIdAuthorizationCode?: string;
}
-const deleteMe = (body: DeleteMeRequestBody) =>
- fetcher.delete('web/v1/users/me', { json: body });
+const deleteMe = (body: DeleteMeRequestBody) => fetcher.delete('web/v1/users/me', { json: body });
-const useDeleteMe = () =>
- useMutation((body: DeleteMeRequestBody) => deleteMe(body));
+const useDeleteMe = () => useMutation((body: DeleteMeRequestBody) => deleteMe(body));
export default useDeleteMe;
diff --git a/packages/api/src/mutations/useEditUserProfile.ts b/packages/api/src/mutations/useEditUserProfile.ts
index 399f9c81..603b5a72 100644
--- a/packages/api/src/mutations/useEditUserProfile.ts
+++ b/packages/api/src/mutations/useEditUserProfile.ts
@@ -1,12 +1,12 @@
import { useMutation } from '@tanstack/react-query';
import { fetcher } from '../fetcher';
-import { UserProfileLink } from '../types';
+import { UserLink } from '../types';
interface PutUserProfileRequestBody {
nickname: string;
profileImagePath?: string;
introduction?: string;
- link?: UserProfileLink[];
+ link?: UserLink[];
}
const putUserProfile = (body: PutUserProfileRequestBody) =>
diff --git a/packages/api/src/mutations/usePostCastTeams.ts b/packages/api/src/mutations/usePostCastTeams.ts
new file mode 100644
index 00000000..45888915
--- /dev/null
+++ b/packages/api/src/mutations/usePostCastTeams.ts
@@ -0,0 +1,16 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { fetcher } from '../fetcher';
+import { ShowCastTeamCreateOrUpdateRequest } from '../types';
+
+const postCastTeams = ({
+ showId,
+ ...json
+}: ShowCastTeamCreateOrUpdateRequest & { showId: number }) =>
+ fetcher.post(`web/v1/shows/${showId}/cast-teams`, {
+ json,
+ });
+
+const usePostCastTeams = () => useMutation({ mutationFn: postCastTeams });
+
+export default usePostCastTeams;
diff --git a/packages/api/src/mutations/usePutCastTeams.ts b/packages/api/src/mutations/usePutCastTeams.ts
new file mode 100644
index 00000000..652e8fb3
--- /dev/null
+++ b/packages/api/src/mutations/usePutCastTeams.ts
@@ -0,0 +1,16 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { fetcher } from '../fetcher';
+import { ShowCastTeamCreateOrUpdateRequest } from '../types';
+
+const putCastTeams = ({
+ castTeamId,
+ ...json
+}: ShowCastTeamCreateOrUpdateRequest & { castTeamId: number }) =>
+ fetcher.put(`web/v1/cast-teams/${castTeamId}`, {
+ json,
+ });
+
+const usePutCastTeams = () => useMutation({ mutationFn: putCastTeams });
+
+export default usePutCastTeams;
diff --git a/packages/api/src/queries/index.ts b/packages/api/src/queries/index.ts
index 4e55406d..235ea3c8 100644
--- a/packages/api/src/queries/index.ts
+++ b/packages/api/src/queries/index.ts
@@ -30,8 +30,12 @@ import useAdminShowInfo from './useAdminShowInfo';
import useAdminReservations from './useAdminReservations';
import useAdminReservationSummary from './useAdminReservationSummary';
import useUserProfile from './useUserProfile';
+import useUserByUserCode from './useUserByUserCode';
+import useCastTeamList from './useCastTeamList';
export {
+ useCastTeamList,
+ useUserByUserCode,
useAdminSettlementEvent,
useAdminSettlementInfo,
useGift,
diff --git a/packages/api/src/queries/useCastTeamList.ts b/packages/api/src/queries/useCastTeamList.ts
new file mode 100644
index 00000000..f130bd9f
--- /dev/null
+++ b/packages/api/src/queries/useCastTeamList.ts
@@ -0,0 +1,7 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { queryKeys } from '../queryKey';
+
+const useCastTeamList = (showId: number) => useQuery(queryKeys.castTeams.list(showId));
+
+export default useCastTeamList;
diff --git a/packages/api/src/queries/useUserByUserCode.ts b/packages/api/src/queries/useUserByUserCode.ts
new file mode 100644
index 00000000..5dffff93
--- /dev/null
+++ b/packages/api/src/queries/useUserByUserCode.ts
@@ -0,0 +1,9 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { queryKeys } from '../queryKey';
+
+const useUserByUserCode = (userCode: string) => {
+ return useQuery({ ...queryKeys.user.userCode(userCode) });
+};
+
+export default useUserByUserCode;
diff --git a/packages/api/src/queryKey.ts b/packages/api/src/queryKey.ts
index 7040ba91..6cd03f45 100644
--- a/packages/api/src/queryKey.ts
+++ b/packages/api/src/queryKey.ts
@@ -9,6 +9,7 @@ import {
PageReservationResponse,
ReservationSummaryResponse,
SettlementBannersResponse,
+ ShowCastTeamReadResponse,
ShowInvitationCodeListResponse,
ShowInvitationTicketResponse,
ShowPreviewResponse,
@@ -30,7 +31,11 @@ import {
SuperAdminShowStatus,
TicketSalesInfoResponse,
} from './types/adminShow';
-import { BankAccountListResponse, UserProfileResponse, UserProfileSummaryResponse } from './types/users';
+import {
+ BankAccountListResponse,
+ UserProfileResponse,
+ UserProfileSummaryResponse,
+} from './types/users';
import { GiftInfoResponse } from './types/gift';
import { HostListItem, HostListResponse } from './types/host';
import {
@@ -295,7 +300,7 @@ export const showQueryKeys = createQueryKeys('show', {
export const userQueryKeys = createQueryKeys('user', {
profile: {
queryKey: null,
- queryFn: () => fetcher.get('web/v1/users/me')
+ queryFn: () => fetcher.get('web/v1/users/me'),
},
summary: {
queryKey: null,
@@ -305,6 +310,10 @@ export const userQueryKeys = createQueryKeys('user', {
queryKey: null,
queryFn: () => fetcher.get(`web/v1/host/users/me/bank-accounts`),
},
+ userCode: (userCode: string) => ({
+ queryKey: [userCode],
+ queryFn: () => fetcher.get(`web/papi/v1/users/${userCode}`),
+ }),
});
export const giftQueryKeys = createQueryKeys('gift', {
@@ -325,6 +334,14 @@ export const hostQueryKeys = createQueryKeys('host', {
}),
});
+export const castTeamQueryKeys = createQueryKeys('castTeams', {
+ list: (showId: number) => ({
+ queryKey: [showId],
+ queryFn: () =>
+ fetcher.get(`web/papi/v1/shows/${showId}/cast-teams`),
+ }),
+});
+
export const queryKeys = mergeQueryKeys(
adminShowQueryKeys,
adminEntranceQueryKeys,
@@ -334,4 +351,5 @@ export const queryKeys = mergeQueryKeys(
entranceQueryKeys,
giftQueryKeys,
hostQueryKeys,
+ castTeamQueryKeys,
);
diff --git a/packages/api/src/types/cast.ts b/packages/api/src/types/cast.ts
new file mode 100644
index 00000000..73ea7376
--- /dev/null
+++ b/packages/api/src/types/cast.ts
@@ -0,0 +1,36 @@
+export interface Member {
+ /** 공연 출연진 팀원 ID */
+ id?: number;
+ /** 유저 식별 코드 */
+ userCode: string;
+ /** 역할 이름 (1~100자. 빈 문자열 불가) */
+ roleName: string;
+ /** 유저 닉네임 */
+ userNickname: string;
+ /** 유저 프로필 이미지 경로 */
+ userImgPath: string;
+ /** 유저 생성 일시 */
+ createdAt: string;
+ /** 유저 수정 일시 */
+ modifedAt: string;
+}
+
+export interface ShowCastTeamReadResponse {
+ /** 출연진 팀 식별자 */
+ id: number;
+ /** 출연진 팀 이름 */
+ name: string;
+ /** 출연진 팀 멤버 목록 */
+ members?: Member[];
+ /** 팀 생성 일시 */
+ createdAt: string;
+ /** 팀 수정 일시 */
+ modifiedAt: string;
+}
+
+export interface ShowCastTeamCreateOrUpdateRequest {
+ /** 팀 이름 */
+ name: ShowCastTeamReadResponse['name'];
+ /** 팀원 목록 */
+ members?: Pick[];
+}
diff --git a/packages/api/src/types/index.ts b/packages/api/src/types/index.ts
index 42d25db0..d4c560d2 100644
--- a/packages/api/src/types/index.ts
+++ b/packages/api/src/types/index.ts
@@ -2,3 +2,4 @@ export * from './common';
export * from './entrance';
export * from './show';
export * from './users';
+export * from './cast';
diff --git a/packages/api/src/types/show.ts b/packages/api/src/types/show.ts
index a9fc9079..c58b1f6d 100644
--- a/packages/api/src/types/show.ts
+++ b/packages/api/src/types/show.ts
@@ -1,20 +1,29 @@
+import { ShowCastTeamCreateOrUpdateRequest } from './cast';
import { PageResponse, ReservationStatus, TicketStatus, TicketType } from './common';
import { HostType } from './host';
export interface ShowImage {
+ /** 공연 포스터 이미지 순서. 1부터 시작. 최대 3개까지 가능. */
sequence: number;
+ /** 공연 포스터 이미지 썸네일 url */
thumbnailPath: string;
+ /** 공연 포스터 이미지 원본 url */
path: string;
}
-export interface ShowPlace {
+export interface Place {
+ /** 장소 이름 */
name: string;
+ /** 장소 도로명 주소 */
streetAddress: string;
+ /** 장소 상세 주소 */
detailAddress: string;
}
-export interface ShowHost {
+export interface Host {
+ /** 호스트 이름 */
name: string;
+ /** 호스트 전화번호 (하이픈 없음) */
phoneNumber: string;
}
@@ -24,9 +33,9 @@ export interface ShowResponse {
images: ShowImage[];
date: string;
runningTime: number;
- place: ShowPlace;
+ place: Place;
notice: string;
- host: ShowHost;
+ host: Host;
isEnded: boolean;
settlementStatus: 'SETTLEMENT_REQUIRED' | 'SETTLEMENT_REQUEST' | 'SETTLEMENT_DONE' | null;
}
@@ -213,3 +222,44 @@ export type SettlementBannersResponse = {
showName: string;
bannerType: 'REQUIRED' | 'DONE';
}[];
+
+export interface ShowCreateRequest {
+ /** 공연 이름 */
+ name: string;
+ /** 공연 포스터 이미지 목록 */
+ images: ShowImage[];
+ /** 공연 시작 날짜, 시간. ISO8601. */
+ date: string;
+ /** 러닝 타임. 분 */
+ runningTime: number;
+ /** 장소 정보 */
+ place: Place;
+ /** 공지사항 (공연 상세) */
+ notice: string;
+ /** 호스트 정보 */
+ host: Host;
+ /** 판매 시작 시간. required. 시간은 0시 0분 0초 */
+ salesStartTime: string;
+ /** 판매 종료 시간. required. 시간은 23시 59분 59초 */
+ salesEndTime: string;
+ /** 티켓 구매시 안내사항. optional. */
+ ticketNotice?: string;
+ /** 생성할 판매 티켓 목록, invitationTickets 와 둘 중 1개는 필수 */
+ salesTickets: {
+ /** 티켓 이름. required. 20자 이내 */
+ ticketName: string;
+ /** 티켓 가격(장당), required. 1 이상 */
+ price: number;
+ /** 티켓 수량, required. 1 이상 */
+ totalForSale: number;
+ }[];
+ /** 생성할 초대 티켓 목록, salesTickets 와 둘 중 1개는 필수 */
+ invitationTickets: {
+ /** 티켓 이름. required. 20자 이내 */
+ ticketName: string;
+ /** 티켓 수량, required. 1 이상 */
+ totalForSale: number;
+ }[];
+ /** 출연진 팀 */
+ castTeams?: Array;
+}
diff --git a/packages/api/src/types/users.ts b/packages/api/src/types/users.ts
index 98198c95..d7ea31f3 100644
--- a/packages/api/src/types/users.ts
+++ b/packages/api/src/types/users.ts
@@ -24,19 +24,27 @@ export interface UserProfileSummaryResponse {
oauthType: 'KAKAO' | 'APPLE';
}
-export interface UserProfileLink {
+export interface UserLink {
title: string;
link: string;
}
export interface UserProfileResponse {
- id: number
- nickname: string
- email: string
- userCode: string
- imgPath: string
- introduction: string
- link: UserProfileLink[]
+ /** 유저 ID */
+ id: number;
+ /** 유저 닉네임 */
+ nickname: string;
+ /** 유저 이메일 */
+ email: string;
+ /** 유저코드 (권한 부여 시) */
+ userCode: string;
+ /** 유저 프로필 이미지 경로 */
+ imgPath: string;
+ /** 유저 소개 */
+ introduction: string;
+ /** 유저 링크 */
+ link: UserLink[];
+ /** 가입 시 사용한 oauth 제공자 */
oauthType: 'KAKAO' | 'APPLE';
}
diff --git a/packages/icon/src/components/BooltiLightGrey.tsx b/packages/icon/src/components/BooltiLightGrey.tsx
index 189d42ea..1fea3a94 100644
--- a/packages/icon/src/components/BooltiLightGrey.tsx
+++ b/packages/icon/src/components/BooltiLightGrey.tsx
@@ -2,12 +2,38 @@ export const BooltiLightGrey = () => (
-)
+);
diff --git a/packages/icon/src/components/Call.tsx b/packages/icon/src/components/Call.tsx
index 40063a80..38be7d8f 100644
--- a/packages/icon/src/components/Call.tsx
+++ b/packages/icon/src/components/Call.tsx
@@ -1,5 +1,12 @@
export const Call = () => (
-