diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx index c7de2160..5f601fc5 100644 --- a/apps/admin/src/App.tsx +++ b/apps/admin/src/App.tsx @@ -14,6 +14,7 @@ import OAuthKakaoPage from './pages/OAuth/OAuthKakaoPage'; import ShowAddCompletePage from './pages/ShowAddCompletePage/ShowAddCompletePage'; import ShowAddPage from './pages/ShowAddPage/ShowAddPage'; import ShowInfoPage from './pages/ShowInfoPage/ShowInfoPage'; +import ShowTicketPage from './pages/ShowTicketPage/ShowTicketPage'; import SignUpCompletePage from './pages/SignUpComplete/SignUpCompletePage'; const PublicRoute = () => { @@ -84,7 +85,7 @@ const privateRoutes = [ { path: PATH.SHOW_ADD, element: }, { path: PATH.SHOW_ADD_TICKET, element: }, { path: PATH.SHOW_INFO, element: }, - { path: PATH.SHOW_TICKET, element: null }, + { path: PATH.SHOW_TICKET, element: }, { path: PATH.SHOW_RESERVATION, element: null }, { path: PATH.SHOW_ENTRY, element: null }, { diff --git a/apps/admin/src/components/ShowDeleteForm/ShowDeleteForm.styles.ts b/apps/admin/src/components/ShowDeleteForm/ShowDeleteForm.styles.ts new file mode 100644 index 00000000..a82da10d --- /dev/null +++ b/apps/admin/src/components/ShowDeleteForm/ShowDeleteForm.styles.ts @@ -0,0 +1,30 @@ +import styled from '@emotion/styled'; + +const ShowDeleteForm = styled.form``; + +const Description = styled.p` + ${({ theme }) => theme.typo.b3}; + color: ${({ theme }) => theme.palette.grey.g70}; + margin-bottom: 28px; +`; + +const TextFieldContainer = styled.div` + margin-bottom: 32px; + + div { + width: 100%; + } +`; + +const ButtonContainer = styled.div` + display: flex; + justify-content: flex-end; + gap: 8px; +`; + +export default { + ShowDeleteForm, + Description, + TextFieldContainer, + ButtonContainer, +}; diff --git a/apps/admin/src/components/ShowDeleteForm/index.tsx b/apps/admin/src/components/ShowDeleteForm/index.tsx new file mode 100644 index 00000000..f2e0cbf7 --- /dev/null +++ b/apps/admin/src/components/ShowDeleteForm/index.tsx @@ -0,0 +1,69 @@ +import { Button, TextField } from '@boolti/ui'; +import { useState } from 'react'; +import { SubmitHandler, useForm } from 'react-hook-form'; + +import Styled from './ShowDeleteForm.styles'; + +export interface ShowDeleteFormInputs { + showName: string; +} + +interface ShowDeleteFormProps { + showName: string; + onSubmit: SubmitHandler; +} + +const ShowDeleteForm = ({ showName, onSubmit }: ShowDeleteFormProps) => { + const { register, handleSubmit, watch } = useForm(); + + const [error, setError] = useState(null); + + const onSubmitForm: SubmitHandler = (data) => { + if (data.showName === showName) { + onSubmit(data); + + return; + } + + setError('정확한 공연명을 입력해주세요.'); + }; + + return ( + + + 삭제한 공연은 다시 되돌릴 수 없습니다. +
+ 삭제하시려면 정확한 공연명을 입력해 주세요. +
+ + { + setError(null); + }, + })} + /> + + + + + +
+ ); +}; + +export default ShowDeleteForm; diff --git a/apps/admin/src/components/ShowDetailLayout/ShowDetailLayout.styles.ts b/apps/admin/src/components/ShowDetailLayout/ShowDetailLayout.styles.ts index 7f587b95..b4fbb5f2 100644 --- a/apps/admin/src/components/ShowDetailLayout/ShowDetailLayout.styles.ts +++ b/apps/admin/src/components/ShowDetailLayout/ShowDetailLayout.styles.ts @@ -39,7 +39,7 @@ const TopObserver = styled.div` const HeaderObserver = styled.div` position: absolute; - top: -130px; + top: calc(-197px + 68px - 40px); left: 0; width: 100%; height: 1px; diff --git a/apps/admin/src/components/ShowDetailLayout/index.tsx b/apps/admin/src/components/ShowDetailLayout/index.tsx index 0d66c5e0..37b3513e 100644 --- a/apps/admin/src/components/ShowDetailLayout/index.tsx +++ b/apps/admin/src/components/ShowDetailLayout/index.tsx @@ -14,14 +14,16 @@ import Styled from './ShowDetailLayout.styles.ts'; interface ShowDetailLayoutProps { showName: string; children?: React.ReactNode; + onClickMiddleware?: () => Promise; } -const ShowDetailLayout = ({ showName, children }: ShowDetailLayoutProps) => { +const ShowDetailLayout = ({ showName, children, onClickMiddleware }: ShowDetailLayoutProps) => { const { ref: topObserverRef, inView: topInView } = useInView({ threshold: 1, }); const { ref: headerObserverRef, inView: headerInView } = useInView({ - threshold: 0.01, + threshold: 1, + initialInView: true, }); const theme = useTheme(); @@ -45,7 +47,11 @@ const ShowDetailLayout = ({ showName, children }: ShowDetailLayoutProps) => { { + onClick={async () => { + if (onClickMiddleware && !(await onClickMiddleware())) { + return; + } + navigate(PATH.HOME); }} > @@ -57,8 +63,11 @@ const ShowDetailLayout = ({ showName, children }: ShowDetailLayoutProps) => { right={ { - await logoutMutation.mutateAsync(); + if (onClickMiddleware && !(await onClickMiddleware())) { + return; + } + await logoutMutation.mutateAsync(); navigate(PATH.LOGIN); }} > @@ -71,9 +80,13 @@ const ShowDetailLayout = ({ showName, children }: ShowDetailLayoutProps) => { { + onClick={async () => { if (!params.showId) return; + if (onClickMiddleware && !(await onClickMiddleware())) { + return; + } + navigate(HREF.SHOW_INFO(params.showId)); }} > @@ -81,9 +94,13 @@ const ShowDetailLayout = ({ showName, children }: ShowDetailLayoutProps) => { { + onClick={async () => { if (!params.showId) return; + if (onClickMiddleware && !(await onClickMiddleware())) { + return; + } + navigate(HREF.SHOW_TICKET(params.showId)); }} > @@ -91,9 +108,13 @@ const ShowDetailLayout = ({ showName, children }: ShowDetailLayoutProps) => { { + onClick={async () => { if (!params.showId) return; + if (onClickMiddleware && !(await onClickMiddleware())) { + return; + } + navigate(HREF.SHOW_RESERVATION(params.showId)); }} > @@ -101,9 +122,13 @@ const ShowDetailLayout = ({ showName, children }: ShowDetailLayoutProps) => { { + onClick={async () => { if (!params.showId) return; + if (onClickMiddleware && !(await onClickMiddleware())) { + return; + } + navigate(HREF.SHOW_ENTRY(params.showId)); }} > diff --git a/apps/admin/src/components/ShowInfoFormContent/ShowBasicInfoFormContent.tsx b/apps/admin/src/components/ShowInfoFormContent/ShowBasicInfoFormContent.tsx index e69de29b..63ae52a3 100644 --- a/apps/admin/src/components/ShowInfoFormContent/ShowBasicInfoFormContent.tsx +++ b/apps/admin/src/components/ShowInfoFormContent/ShowBasicInfoFormContent.tsx @@ -0,0 +1,183 @@ +import { ImageFile } from '@boolti/api'; +import { CloseIcon, FileUpIcon } from '@boolti/icon'; +import { TextField } from '@boolti/ui'; +import { useDropzone } from 'react-dropzone'; +import { Controller, UseFormReturn } from 'react-hook-form'; + +import Styled from './ShowInfoFormContent.styles'; +import { ShowInfoFormInputs } from './types'; + +const MAX_IMAGE_COUNT = 3; + +interface ShowBasicInfoFormContentProps { + form: UseFormReturn; + imageFiles: ImageFile[]; + disabled?: boolean; + onDropImage: (acceptedFiles: File[]) => void; + onDeleteImage: (file: ImageFile) => void; +} + +const ShowBasicInfoFormContent = ({ + form, + imageFiles, + disabled, + onDropImage, + onDeleteImage, +}: ShowBasicInfoFormContentProps) => { + const { register, watch, control } = form; + + const { getRootProps, getInputProps } = useDropzone({ + accept: { + 'image/*': [], + }, + maxFiles: MAX_IMAGE_COUNT, + onDrop: onDropImage, + }); + + return ( + + 기본 정보 + + + 공연 포스터 + + 원하시는 노출 순서대로 이미지를 업로드해주세요. (최소 1장, 최대{' '} + {MAX_IMAGE_COUNT}장 업로드 가능 / jpg, png 형식) + + + {imageFiles.map((file) => ( + + {!disabled && ( + onDeleteImage(file)} + > + + + )} + + ))} + {imageFiles.length < MAX_IMAGE_COUNT && !disabled && ( + + + + 이미지 업로드 + + )} + + + + + + 공연명 + + + + + + + + 공연일 + + ( + + )} + name="date" + /> + + + + + + 공연 시작 시간 + + + + + + 러닝타임 + + + + + + + + + 공연 장소 + + + + + + + + + + + + + + + ); +}; + +export default ShowBasicInfoFormContent; diff --git a/apps/admin/src/components/ShowInfoFormContent/ShowDetailInfoForm.tsx b/apps/admin/src/components/ShowInfoFormContent/ShowDetailInfoForm.tsx deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/admin/src/components/ShowInfoFormContent/ShowDetailInfoFormContent.tsx b/apps/admin/src/components/ShowInfoFormContent/ShowDetailInfoFormContent.tsx new file mode 100644 index 00000000..4eb3edbb --- /dev/null +++ b/apps/admin/src/components/ShowInfoFormContent/ShowDetailInfoFormContent.tsx @@ -0,0 +1,66 @@ +import { TextField } from '@boolti/ui'; +import { UseFormReturn } from 'react-hook-form'; + +import Styled from './ShowInfoFormContent.styles'; +import { ShowInfoFormInputs } from './types'; + +interface ShowDetailInfoFormContentProps { + form: UseFormReturn; + disabled?: boolean; +} + +const ShowDetailInfoFormContent = ({ form, disabled }: ShowDetailInfoFormContentProps) => { + const { register } = form; + + return ( + + 상세 정보 + + + 공연 내용 + + 예매자에게 알리고 싶은 공연 내용을 작성해주세요. + + + + + + + 대표자 이름 + + + + + + + + 대표 연락처 + + + + + + + ); +}; + +export default ShowDetailInfoFormContent; diff --git a/apps/admin/src/components/ShowInfoFormContent/ShowInfoForm.styles.ts b/apps/admin/src/components/ShowInfoFormContent/ShowInfoForm.styles.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/admin/src/components/ShowInfoFormContent/ShowInfoFormContent.styles.ts b/apps/admin/src/components/ShowInfoFormContent/ShowInfoFormContent.styles.ts new file mode 100644 index 00000000..640e9a75 --- /dev/null +++ b/apps/admin/src/components/ShowInfoFormContent/ShowInfoFormContent.styles.ts @@ -0,0 +1,338 @@ +import { Button } from '@boolti/ui'; +import styled from '@emotion/styled'; + +interface ShowInfoFormLabelProps { + required?: boolean; +} + +interface TextFieldProps { + flex?: string | number; +} + +interface ShowInfoFormButtonProps { + width?: string; +} + +interface FileUploadAreaProps { + imageCount: number; +} + +interface TicketGroupTitleProps { + required?: boolean; +} + +const ShowInfoFormGroup = styled.div``; + +const ShowInfoFormTitle = styled.h3` + ${({ theme }) => theme.typo.h1}; + color: ${({ theme }) => theme.palette.grey.g90}; + margin-bottom: 16px; +`; + +const ShowInfoFormRow = styled.div` + margin-bottom: 28px; + display: flex; + gap: 12px; + + &:last-of-type { + margin-bottom: 0; + } +`; + +const ShowInfoFormContent = styled.div` + flex: 1; +`; + +const ShowInfoFormLabel = styled.label` + display: block; + ${({ theme }) => theme.typo.b3}; + color: ${({ theme }) => theme.palette.grey.g90}; + + &::after { + content: '*'; + ${({ theme }) => theme.typo.b3}; + color: ${({ theme }) => theme.palette.status.error}; + display: ${({ required }) => (required ? 'inline' : 'none')}; + margin-left: 2px; + } +`; + +const ShowInfoFormDescription = styled.p` + ${({ theme }) => theme.typo.b1}; + color: ${({ theme }) => theme.palette.grey.g60}; + margin-top: 2px; + + strong { + font-weight: 600; + } +`; + +const ShowInfoFormButtonContainer = styled.div` + display: flex; + gap: 8px; +`; + +const ShowInfoFormButton = styled(Button)` + width: ${({ width }) => width}; +`; + +const PreviewImageContainer = styled.div` + display: grid; + grid-template-columns: repeat(3, 1fr); + height: 256px; + gap: 28px; + margin-top: 16px; +`; + +const PreviewImage = styled.div` + position: relative; + display: block; + max-width: 100%; + height: 100%; + border-radius: 4px; + border: 1px solid ${({ theme }) => theme.palette.grey.g20}; + background-size: contain; + background-repeat: no-repeat; + background-position: center; + + &:first-of-type::after { + content: '대표 사진'; + font-size: 14px; + font-weight: 600; + line-height: 18px; + background-color: ${({ theme }) => theme.palette.primary.o1}; + color: ${({ theme }) => theme.palette.grey.w}; + width: 100%; + height: 32px; + display: flex; + justify-content: center; + align-items: center; + position: absolute; + bottom: 0; + left: 0; + } +`; + +const PreviewImageDeleteButton = styled.button` + position: absolute; + top: -10px; + right: -10px; + background-color: ${({ theme }) => theme.palette.grey.g90}; + opacity: 0.8; + border: none; + border-radius: 50%; + width: 28px; + height: 28px; + display: inline-flex; + justify-content: center; + align-items: center; + cursor: pointer; + + path { + stroke: ${({ theme }) => theme.palette.grey.w}; + } +`; + +const FileUploadArea = styled.div` + grid-column: ${({ imageCount }) => { + switch (imageCount) { + case 1: + return 'span 2'; + case 2: + return 'span 1'; + case 3: + return 'span 0'; + default: + return 'span 3'; + } + }}; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + gap: 8px; + height: 100%; + border: 1px dashed ${({ theme }) => theme.palette.grey.g20}; + border-radius: 4px; + user-select: none; + cursor: pointer; + + path { + stroke: ${({ theme }) => theme.palette.grey.g40}; + } +`; + +const FileUploadAreaText = styled.span` + ${({ theme }) => theme.typo.sh2}; + color: ${({ theme }) => theme.palette.grey.g40}; +`; + +const TextField = styled.div` + margin-top: 8px; + display: flex; + align-items: center; + gap: 8px; + flex: ${({ flex }) => flex}; + + div { + width: auto; + flex: 1; + } +`; + +const TextFieldSuffix = styled.span` + ${({ theme }) => theme.typo.sh1}; + color: ${({ theme }) => theme.palette.grey.g50}; + flex: 0; +`; + +const TextFieldRow = styled.div` + display: flex; + gap: 8px; +`; + +const TextArea = styled.textarea` + width: 100%; + margin-top: 16px; + padding: 12px; + border: 1px solid ${({ theme }) => theme.palette.grey.g90}; + border-radius: 4px; + background-color: ${({ theme }) => theme.palette.grey.w}; + color: ${({ theme }) => theme.palette.grey.g90}; + ${({ theme }) => theme.typo.b3}; + + &:placeholder-shown { + border: 1px solid ${({ theme }) => theme.palette.grey.g20}; + color: ${({ theme }) => theme.palette.grey.g30}; + } + + &:disabled { + background: ${({ theme }) => theme.palette.grey.g10}; + border: 1px solid ${({ theme }) => theme.palette.grey.g20}; + color: ${({ theme }) => theme.palette.grey.g40}; + } +`; + +const TicketGroup = styled.div` + display: flex; + flex-direction: column; + gap: 32px; +`; + +const TicketGroupHeader = styled.div` + display: flex; + justify-content: space-between; +`; + +const TicketGroupInfo = styled.div` + display: flex; + flex-direction: column; +`; + +const TicketGroupTitle = styled.h3` + display: flex; + ${({ theme }) => theme.typo.h1}; + color: ${({ theme }) => theme.palette.grey.g90}; + + &::after { + content: '*'; + ${({ theme }) => theme.typo.b1}; + color: ${({ theme }) => theme.palette.status.error}; + display: ${({ required }) => (required ? 'inline' : 'none')}; + margin-left: 2px; + } +`; + +const TicketGroupDescription = styled.p` + ${({ theme }) => theme.typo.b1}; + color: ${({ theme }) => theme.palette.grey.g60}; + + strong { + font-weight: 600; + } +`; + +const TicketList = styled.div` + display: flex; + flex-direction: column; + gap: 20px; +`; + +const Ticket = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; + padding: 24px 28px; + border-radius: 8px; + box-shadow: 0px 8px 14px 0px ${({ theme }) => theme.palette.shadow}; + border: 1px solid ${({ theme }) => theme.palette.grey.g20}; + background-color: ${({ theme }) => theme.palette.grey.w}; +`; + +const TicketInfo = styled.div` + display: flex; + flex-direction: column; + gap: 6px; +`; + +const TicketTitle = styled.div` + display: flex; + align-items: center; + gap: 8px; +`; + +const TicketTitleText = styled.h4` + ${({ theme }) => theme.typo.sh2}; + color: ${({ theme }) => theme.palette.grey.g90}; +`; + +const TicketDescription = styled.p` + ${({ theme }) => theme.typo.b3}; + color: ${({ theme }) => theme.palette.grey.g70}; +`; + +const TicketAction = styled.div` + display: flex; + align-items: center; +`; + +const TicketAddButtonContainer = styled.div` + svg { + width: 20px; + height: 20px; + } +`; + +export default { + ShowInfoFormGroup, + ShowInfoFormTitle, + ShowInfoFormRow, + ShowInfoFormContent, + ShowInfoFormLabel, + ShowInfoFormDescription, + ShowInfoFormButtonContainer, + ShowInfoFormButton, + PreviewImageContainer, + PreviewImage, + PreviewImageDeleteButton, + FileUploadArea, + FileUploadAreaText, + TextField, + TextFieldSuffix, + TextFieldRow, + TextArea, + TicketGroup, + TicketGroupHeader, + TicketGroupInfo, + TicketGroupTitle, + TicketGroupDescription, + TicketList, + Ticket, + TicketInfo, + TicketTitle, + TicketTitleText, + TicketDescription, + TicketAction, + TicketAddButtonContainer, +}; diff --git a/apps/admin/src/components/ShowInfoFormContent/ShowInvitationTicketFormContent.tsx b/apps/admin/src/components/ShowInfoFormContent/ShowInvitationTicketFormContent.tsx new file mode 100644 index 00000000..3f900fbb --- /dev/null +++ b/apps/admin/src/components/ShowInfoFormContent/ShowInvitationTicketFormContent.tsx @@ -0,0 +1,88 @@ +import { PlusIcon } from '@boolti/icon'; +import { Badge, Button, useDialog } from '@boolti/ui'; +import { SubmitHandler } from 'react-hook-form'; + +import InvitationTicketForm, { + InvitationTicketFormInputs, +} from '../TicketForm/InvitationTicketForm'; +import Styled from './ShowInfoFormContent.styles'; + +interface ShowInvitationTicketFormContentProps { + invitationTicketList: InvitationTicketFormInputs[]; + onSubmitTicket: SubmitHandler; + onDeleteTicket: (ticket: InvitationTicketFormInputs) => void; +} + +const ShowInvitationTicketFormContent = ({ + invitationTicketList, + onSubmitTicket, + onDeleteTicket, +}: ShowInvitationTicketFormContentProps) => { + const invitationTicketDialog = useDialog(); + + const handleSubmitTicket: SubmitHandler = (data) => { + invitationTicketDialog.close(); + + onSubmitTicket(data); + }; + + return ( + + + + 초청 티켓 + + 초청 티켓 이용을 원하시면 티켓을 생성해주세요. +
* 초청 코드는 공연 등록 후 공연 관리 > 티켓 관리에서 확인할 수 + 있습니다. +
+
+ + + +
+ {invitationTicketList.length > 0 && ( + + {invitationTicketList.map((ticket) => ( + + + + {ticket.name} + + 재고 {ticket.quantity}/{ticket.quantity} + + + 1인당 1매 + + + + + + ))} + + )} +
+ ); +}; + +export default ShowInvitationTicketFormContent; diff --git a/apps/admin/src/components/ShowInfoFormContent/ShowSalesTicketFormContent.tsx b/apps/admin/src/components/ShowInfoFormContent/ShowSalesTicketFormContent.tsx new file mode 100644 index 00000000..0b418b8e --- /dev/null +++ b/apps/admin/src/components/ShowInfoFormContent/ShowSalesTicketFormContent.tsx @@ -0,0 +1,85 @@ +import { PlusIcon } from '@boolti/icon'; +import { Badge, Button, useDialog } from '@boolti/ui'; +import { SubmitHandler } from 'react-hook-form'; + +import SalesTicketForm, { SalesTicketFormInputs } from '../TicketForm/SalesTicketForm'; +import Styled from './ShowInfoFormContent.styles'; + +interface ShowSalesTicketFormContentProps { + salesTicketList: SalesTicketFormInputs[]; + onSubmitTicket: SubmitHandler; + onDeleteTicket: (ticket: SalesTicketFormInputs) => void; +} + +const ShowSalesTicketFormContent = ({ + salesTicketList, + onSubmitTicket, + onDeleteTicket, +}: ShowSalesTicketFormContentProps) => { + const salesTicketDialog = useDialog(); + + const handleSubmitTicket: SubmitHandler = (data) => { + salesTicketDialog.close(); + + onSubmitTicket(data); + }; + + return ( + + + + 일반 티켓 + + 티켓 판매를 위해서는 최소 1개 이상의 티켓이 필요해요. +
* 1매 이상 판매된 티켓은 삭제할 수 없습니다. +
+
+ + + +
+ {salesTicketList.length > 0 && ( + + {salesTicketList.map((ticket) => ( + + + + {ticket.name} + + 재고 {ticket.quantity}/{ticket.quantity} + + + {ticket.price}원 · 1인당 1매 + + + + + + ))} + + )} +
+ ); +}; + +export default ShowSalesTicketFormContent; diff --git a/apps/admin/src/components/ShowInfoFormContent/ShowTicketInfoFormContent.tsx b/apps/admin/src/components/ShowInfoFormContent/ShowTicketInfoFormContent.tsx new file mode 100644 index 00000000..6489ec26 --- /dev/null +++ b/apps/admin/src/components/ShowInfoFormContent/ShowTicketInfoFormContent.tsx @@ -0,0 +1,108 @@ +import { TextField } from '@boolti/ui'; +import { format, sub } from 'date-fns'; +import { Controller, UseFormReturn } from 'react-hook-form'; + +import Styled from './ShowInfoFormContent.styles'; +import { ShowTicketFormInputs } from './types'; + +interface ShowTicketInfoFormContentProps { + form: UseFormReturn; + showDate: string; + disabled?: boolean; +} + +const ShowTicketInfoFormContent = ({ + form, + showDate, + disabled, +}: ShowTicketInfoFormContentProps) => { + const { register, watch, control } = form; + + return ( + + 티켓 정보 + + + + + 판매 시작일 + + ( + + )} + name="startDate" + /> + + + + 판매 종료일 + + ( + + )} + name="endDate" + /> + + + + + + + + 티켓 구매 시 안내사항 + + (ex. 주류반입이 불가한 공연장입니다. 드시던 음료는 입구에 놓고 입장해주세요.) + + + + + + + + ); +}; + +export default ShowTicketInfoFormContent; diff --git a/apps/admin/src/components/ShowInfoFormContent/types.ts b/apps/admin/src/components/ShowInfoFormContent/types.ts new file mode 100644 index 00000000..c82df41d --- /dev/null +++ b/apps/admin/src/components/ShowInfoFormContent/types.ts @@ -0,0 +1,18 @@ +export interface ShowInfoFormInputs { + name: string; + date: string; + startTime: string; + runningTime: string; + placeName: string; + placeStreetAddress: string; + placeDetailAddress: string; + notice: string; + hostName: string; + hostPhoneNumber: string; +} + +export interface ShowTicketFormInputs { + startDate: string; + endDate: string; + ticketNotice: string; +} diff --git a/apps/admin/src/components/TicketForm/GeneralTicketForm.tsx b/apps/admin/src/components/TicketForm/SalesTicketForm.tsx similarity index 89% rename from apps/admin/src/components/TicketForm/GeneralTicketForm.tsx rename to apps/admin/src/components/TicketForm/SalesTicketForm.tsx index 2a4b0533..dc42f0de 100644 --- a/apps/admin/src/components/TicketForm/GeneralTicketForm.tsx +++ b/apps/admin/src/components/TicketForm/SalesTicketForm.tsx @@ -3,22 +3,22 @@ import { SubmitHandler, useForm } from 'react-hook-form'; import Styled from './TicketForm.styles'; -export interface GeneralTicketFormInputs { +export interface SalesTicketFormInputs { name: string; price: number; quantity: number; } -interface GeneralTicketFormProps { - onSubmit: SubmitHandler; +interface SalesTicketFormProps { + onSubmit: SubmitHandler; } -const GeneralTicketForm = ({ onSubmit }: GeneralTicketFormProps) => { +const SalesTicketForm = ({ onSubmit }: SalesTicketFormProps) => { const { handleSubmit, register, formState: { isDirty, isValid }, - } = useForm(); + } = useForm(); return ( @@ -76,4 +76,4 @@ const GeneralTicketForm = ({ onSubmit }: GeneralTicketFormProps) => { ); }; -export default GeneralTicketForm; +export default SalesTicketForm; diff --git a/apps/admin/src/pages/ShowAddPage/ShowAddPage.styles.ts b/apps/admin/src/pages/ShowAddPage/ShowAddPage.styles.ts index 7811aeda..fc4b6ab9 100644 --- a/apps/admin/src/pages/ShowAddPage/ShowAddPage.styles.ts +++ b/apps/admin/src/pages/ShowAddPage/ShowAddPage.styles.ts @@ -22,10 +22,6 @@ interface FileUploadAreaProps { imageCount: number; } -interface TicketGroupTitleProps { - required?: boolean; -} - const ShowAddPage = styled.div` background-color: ${({ theme }) => theme.palette.grey.g00}; `; @@ -149,6 +145,23 @@ const ShowAddForm = styled.form` gap: 68px; `; +const ShowInfoFormContent = styled.div``; + +const ShowInfoFormDivider = styled.hr` + border-top: 1px solid ${({ theme }) => theme.palette.grey.g20}; + margin: 52px 0; +`; + +const ShowInfoFormFooter = styled.div` + display: flex; + justify-content: space-between; + margin-top: 52px; + + button:first-of-type { + width: 128px; + } +`; + const ShowAddFormGroup = styled.div``; const ShowAddFormTitle = styled.h3` @@ -341,97 +354,6 @@ const TicketGroupContainer = styled.div` gap: 68px; `; -const TicketGroup = styled.div` - display: flex; - flex-direction: column; - gap: 32px; -`; - -const TicketGroupHeader = styled.div` - display: flex; - justify-content: space-between; -`; - -const TicketGroupInfo = styled.div` - display: flex; - flex-direction: column; -`; - -const TicketGroupTitle = styled.h3` - display: flex; - ${({ theme }) => theme.typo.h1}; - color: ${({ theme }) => theme.palette.grey.g90}; - - &::after { - content: '*'; - ${({ theme }) => theme.typo.b1}; - color: ${({ theme }) => theme.palette.status.error}; - display: ${({ required }) => (required ? 'inline' : 'none')}; - margin-left: 2px; - } -`; - -const TicketGroupDescription = styled.p` - ${({ theme }) => theme.typo.b1}; - color: ${({ theme }) => theme.palette.grey.g60}; - - strong { - font-weight: 600; - } -`; - -const TicketList = styled.div` - display: flex; - flex-direction: column; - gap: 20px; -`; - -const Ticket = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - gap: 16px; - padding: 24px 28px; - border-radius: 8px; - box-shadow: 0px 8px 14px 0px ${({ theme }) => theme.palette.shadow}; - border: 1px solid ${({ theme }) => theme.palette.grey.g20}; - background-color: ${({ theme }) => theme.palette.grey.w}; -`; - -const TicketInfo = styled.div` - display: flex; - flex-direction: column; - gap: 6px; -`; - -const TicketTitle = styled.div` - display: flex; - align-items: center; - gap: 8px; -`; - -const TicketTitleText = styled.h4` - ${({ theme }) => theme.typo.sh2}; - color: ${({ theme }) => theme.palette.grey.g90}; -`; - -const TicketDescription = styled.p` - ${({ theme }) => theme.typo.b3}; - color: ${({ theme }) => theme.palette.grey.g70}; -`; - -const TicketAction = styled.div` - display: flex; - align-items: center; -`; - -const TicketAddButtonContainer = styled.div` - svg { - width: 20px; - height: 20px; - } -`; - export default { ShowAddPage, HeaderContainer, @@ -449,6 +371,9 @@ export default { ProcessIndicatorText, CardDescription, ShowAddForm, + ShowInfoFormContent, + ShowInfoFormDivider, + ShowInfoFormFooter, ShowAddFormGroup, ShowAddFormTitle, ShowAddFormRow, @@ -467,17 +392,4 @@ export default { TextFieldRow, TextArea, TicketGroupContainer, - TicketGroup, - TicketGroupHeader, - TicketGroupInfo, - TicketGroupTitle, - TicketGroupDescription, - TicketList, - Ticket, - TicketInfo, - TicketTitle, - TicketTitleText, - TicketDescription, - TicketAction, - TicketAddButtonContainer, }; diff --git a/apps/admin/src/pages/ShowAddPage/ShowAddPage.tsx b/apps/admin/src/pages/ShowAddPage/ShowAddPage.tsx index 96e83b3c..644b9124 100644 --- a/apps/admin/src/pages/ShowAddPage/ShowAddPage.tsx +++ b/apps/admin/src/pages/ShowAddPage/ShowAddPage.tsx @@ -1,44 +1,22 @@ import { ImageFile, useAddShow, useUploadShowImage } from '@boolti/api'; -import { ArrowLeftIcon, CloseIcon, FileUpIcon, PlusIcon } from '@boolti/icon'; -import { Badge, Button, TextField, useDialog } from '@boolti/ui'; -import { format } from 'date-fns/format'; -import { sub } from 'date-fns/sub'; -import { useCallback, useState } from 'react'; -import { useDropzone } from 'react-dropzone'; -import { Controller, SubmitHandler, useForm } from 'react-hook-form'; +import { ArrowLeftIcon } from '@boolti/icon'; +import { Button } from '@boolti/ui'; +import { useState } from 'react'; +import { SubmitHandler, useForm } from 'react-hook-form'; import { Navigate, useNavigate } from 'react-router-dom'; -import GeneralTicketDialogContent, { - GeneralTicketFormInputs, -} from '~/components/TicketForm/GeneralTicketForm'; -import InvitationTicketForm, { - InvitationTicketFormInputs, -} from '~/components/TicketForm/InvitationTicketForm'; +import ShowBasicInfoFormContent from '~/components/ShowInfoFormContent/ShowBasicInfoFormContent'; +import ShowDetailInfoFormContent from '~/components/ShowInfoFormContent/ShowDetailInfoFormContent'; +import ShowInvitationTicketFormContent from '~/components/ShowInfoFormContent/ShowInvitationTicketFormContent'; +import ShowSalesTicketFormContent from '~/components/ShowInfoFormContent/ShowSalesTicketFormContent'; +import ShowTicketInfoFormContent from '~/components/ShowInfoFormContent/ShowTicketInfoFormContent'; +import { ShowInfoFormInputs, ShowTicketFormInputs } from '~/components/ShowInfoFormContent/types'; +import { InvitationTicketFormInputs } from '~/components/TicketForm/InvitationTicketForm'; +import { SalesTicketFormInputs } from '~/components/TicketForm/SalesTicketForm'; import { PATH } from '~/constants/routes'; import Styled from './ShowAddPage.styles'; -const MAX_IMAGE_COUNT = 3; - -interface ShowInfoFormInputs { - name: string; - date: string; - startTime: string; - runningTime: string; - placeName: string; - placeStreetAddress: string; - placeDetailAddress: string; - notice: string; - hostName: string; - hostPhoneNumber: string; -} - -interface ShowTicketFormInputs { - startDate: string; - endDate: string; - ticketNotice: string; -} - interface ShowAddPageProps { step: 'info' | 'ticket'; } @@ -47,7 +25,7 @@ const ShowAddPage = ({ step }: ShowAddPageProps) => { const navigate = useNavigate(); const [imageFiles, setImageFiles] = useState([]); - const [generalTicketList, setGeneralTicketList] = useState([]); + const [salesTicketList, setSalesTicketList] = useState([]); const [invitationTicketList, setInvitationTicketList] = useState( [], ); @@ -55,31 +33,10 @@ const ShowAddPage = ({ step }: ShowAddPageProps) => { const showInfoForm = useForm(); const showTicketForm = useForm(); - const generalTicketDialog = useDialog(); - const invitationTicketDialog = useDialog(); - const uploadShowImageMutation = useUploadShowImage(); const addShowMutation = useAddShow(); - const onDrop = useCallback((acceptedFiles: File[]) => { - setImageFiles((prevImageFiles) => [ - ...prevImageFiles, - ...acceptedFiles.map((file) => ({ - ...file, - preview: URL.createObjectURL(file), - })), - ]); - }, []); - - const { getRootProps, getInputProps } = useDropzone({ - accept: { - 'image/*': [], - }, - maxFiles: MAX_IMAGE_COUNT, - onDrop, - }); - - const onSubmitInfoForm: SubmitHandler = () => { + const onSubmitInfoForm: SubmitHandler = async () => { navigate(PATH.SHOW_ADD_TICKET); }; @@ -90,11 +47,7 @@ const ShowAddPage = ({ step }: ShowAddPageProps) => { // 공연 생성 await addShowMutation.mutateAsync({ name: showInfoForm.getValues('name'), - images: showImageInfo.map((info) => ({ - sequence: info.sequence, - thumbnailPath: info.thumbnailUrl, - path: info.imageUrl, - })), + images: showImageInfo, date: `${showInfoForm.getValues('date')}T${showInfoForm.getValues('startTime')}:00.000Z`, runningTime: Number(showInfoForm.getValues('runningTime')), place: { @@ -110,7 +63,7 @@ const ShowAddPage = ({ step }: ShowAddPageProps) => { salesStartTime: `${showTicketForm.getValues('startDate')}T00:00:00.000Z`, salesEndTime: `${showTicketForm.getValues('endDate')}T23:59:59.000Z`, ticketNotice: `${showTicketForm.getValues('ticketNotice')}`, - salesTickets: generalTicketList.map((ticket) => ({ + salesTickets: salesTicketList.map((ticket) => ({ ticketName: ticket.name, price: ticket.price, totalForSale: ticket.quantity, @@ -124,16 +77,6 @@ const ShowAddPage = ({ step }: ShowAddPageProps) => { navigate(PATH.SHOW_ADD_COMPLETE); }; - const onSubmitGeneralTicketForm: SubmitHandler = (data) => { - setGeneralTicketList((prevList) => [...prevList, data]); - generalTicketDialog.close(); - }; - - const onSubmitInvitationTicketForm: SubmitHandler = (data) => { - setInvitationTicketList((prevList) => [...prevList, data]); - invitationTicketDialog.close(); - }; - return ( @@ -173,201 +116,40 @@ const ShowAddPage = ({ step }: ShowAddPageProps) => { 공연 정보는 티켓 판매 시작 전까지 수정할 수 있어요. - - 기본 정보 - - - 공연 포스터 - - 원하시는 노출 순서대로 이미지를 업로드해주세요. (최소 - 1장, 최대 {MAX_IMAGE_COUNT}장 업로드 가능 / jpg, png 형식) - - - {imageFiles.map((file) => ( - - { - setImageFiles((prevImageFiles) => - prevImageFiles.filter((prevFile) => prevFile !== file), - ); - }} - > - - - - ))} - {imageFiles.length < MAX_IMAGE_COUNT && ( - - - - 이미지 업로드 - - )} - - - - - - 공연명 - - - - - - - - 공연일 - - ( - - )} - name="date" - /> - - - - - - 공연 시작 시간 - - - - - - 러닝타임 - - - - - - - - - 공연 장소 - - - - - - - - - - - - - - - - 상세 정보 - - - 공연 내용 - - 예매자에게 알리고 싶은 공연 내용을 작성해주세요. - - - - - - - 대표자 이름 - - - - - - - - 대표 연락처 - - - - - - + + { + setImageFiles((prevImageFiles) => [ + ...prevImageFiles, + ...acceptedFiles.map((file) => ({ + ...file, + preview: URL.createObjectURL(file), + })), + ]); + }} + onDeleteImage={(file) => { + setImageFiles((prevImageFiles) => + prevImageFiles.filter((prevFile) => prevFile !== file), + ); + }} + /> + + + + @@ -398,230 +180,33 @@ const ShowAddPage = ({ step }: ShowAddPageProps) => { 공연 정보는 티켓 판매 시작 전까지 수정할 수 있어요. - - 티켓 정보 - - - - - 판매 시작일 - - ( - - )} - name="startDate" - /> - - - - 판매 종료일 - - ( - - )} - name="endDate" - /> - - - - - - - - 티켓 구매 시 안내사항 - - (ex. 주류반입이 불가한 공연장입니다. 드시던 음료는 입구에 놓고 - 입장해주세요.) - - - - - - - + - - - - 일반 티켓 - - 티켓 판매를 위해서는 최소 1개 이상의 티켓이 필요해요. -
* 1매 이상 판매된 티켓은 삭제할 수 없습니다. -
-
- - - -
- {generalTicketList.length > 0 && ( - - {generalTicketList.map((ticket) => ( - - - - {ticket.name} - - 재고 {ticket.quantity}/{ticket.quantity} - - - - {ticket.price}원 · 1인당 1매 - - - - - - - ))} - - )} -
- - - - 초청 티켓 - - 초청 티켓 이용을 원하시면 티켓을 생성해주세요. -
* 초청 코드는 공연 등록 후{' '} - 공연 관리 > 티켓 관리에서 확인할 수 있습니다. -
-
- - - -
- {invitationTicketList.length > 0 && ( - - {invitationTicketList.map((ticket) => ( - - - - {ticket.name} - - 재고 {ticket.quantity}/{ticket.quantity} - - - 1인당 1매 - - - - - - ))} - - )} -
+ { + setSalesTicketList((prevList) => [...prevList, ticket]); + }} + onDeleteTicket={(ticket) => { + setSalesTicketList((prevList) => + prevList.filter((prevTicket) => prevTicket.name !== ticket.name), + ); + }} + /> + { + setInvitationTicketList((prevList) => [...prevList, ticket]); + }} + onDeleteTicket={(ticket) => { + setInvitationTicketList((prevList) => + prevList.filter((prevTicket) => prevTicket.name !== ticket.name), + ); + }} + />
{ disabled={ !showTicketForm.formState.isDirty || !showTicketForm.formState.isValid || - generalTicketList.length === 0 + salesTicketList.length === 0 } > 공연 등록 완료하기 diff --git a/apps/admin/src/pages/ShowInfoPage/ShowInfoPage.styles.ts b/apps/admin/src/pages/ShowInfoPage/ShowInfoPage.styles.ts index 6e9fb3ee..8cdf0da1 100644 --- a/apps/admin/src/pages/ShowInfoPage/ShowInfoPage.styles.ts +++ b/apps/admin/src/pages/ShowInfoPage/ShowInfoPage.styles.ts @@ -1,7 +1,35 @@ import styled from '@emotion/styled'; -const ShowInfoPage = styled.div``; +const ShowInfoPage = styled.div` + padding: 0 20px; + margin: 40px 0 68px; +`; + +const ShowInfoForm = styled.form``; + +const ShowInfoFormContent = styled.div` + max-width: 600px; +`; + +const ShowInfoFormDivider = styled.hr` + border-top: 1px solid ${({ theme }) => theme.palette.grey.g20}; + margin: 52px 0; +`; + +const ShowInfoFormFooter = styled.div` + display: flex; + justify-content: space-between; + margin-top: 52px; + + button:first-of-type { + width: 128px; + } +`; export default { ShowInfoPage, + ShowInfoForm, + ShowInfoFormContent, + ShowInfoFormDivider, + ShowInfoFormFooter, }; diff --git a/apps/admin/src/pages/ShowInfoPage/ShowInfoPage.tsx b/apps/admin/src/pages/ShowInfoPage/ShowInfoPage.tsx index f60b25bb..9c094037 100644 --- a/apps/admin/src/pages/ShowInfoPage/ShowInfoPage.tsx +++ b/apps/admin/src/pages/ShowInfoPage/ShowInfoPage.tsx @@ -1,14 +1,204 @@ +import { + ImageFile, + ShowImage, + useDeleteShow, + useEditShowInfo, + useShowDetail, + useShowSalesInfo, + useUploadShowImage, +} from '@boolti/api'; +import { Button, useConfirm, useDialog, useToast } from '@boolti/ui'; +import { compareAsc, format } from 'date-fns'; +import { useEffect, useState } from 'react'; +import { SubmitHandler, useForm } from 'react-hook-form'; +import { useNavigate, useParams } from 'react-router-dom'; + +import ShowDeleteForm from '~/components/ShowDeleteForm'; import ShowDetailLayout from '~/components/ShowDetailLayout'; +import ShowBasicInfoFormContent from '~/components/ShowInfoFormContent/ShowBasicInfoFormContent'; +import ShowDetailInfoFormContent from '~/components/ShowInfoFormContent/ShowDetailInfoFormContent'; +import { ShowInfoFormInputs } from '~/components/ShowInfoFormContent/types'; +import { PATH } from '~/constants/routes'; import Styled from './ShowInfoPage.styles'; const ShowInfoPage = () => { + const params = useParams<{ showId: string }>(); + const navigate = useNavigate(); + + const [imageFiles, setImageFiles] = useState([]); + const [showImages, setShowImages] = useState([]); + const isImageFilesDirty = imageFiles.some((file) => file.preview.startsWith('blob:')); + const showInfoForm = useForm(); + + const { data: show } = useShowDetail(Number(params!.showId)); + const { data: showSalesInfo } = useShowSalesInfo(Number(params!.showId)); + + const editShowInfoMutation = useEditShowInfo(); + const uploadShowImageMutation = useUploadShowImage(); + const deleteShowMutation = useDeleteShow(); + + const toast = useToast(); + const confirm = useConfirm(); + const deleteShowDialog = useDialog(); + + const onSubmit: SubmitHandler = async (data) => { + if (!show) return; + + const newImageFiles = imageFiles.filter((file) => file.preview.startsWith('blob:')); + const newShowImages = await (async () => { + if (newImageFiles.length === 0) return []; + + return await uploadShowImageMutation.mutateAsync(newImageFiles); + })(); + + await editShowInfoMutation.mutateAsync({ + showId: show.id, + body: { + name: data.name, + images: [...showImages, ...newShowImages].map((image, index) => ({ + sequence: index + 1, + thumbnailPath: image.thumbnailPath, + path: image.path, + })), + date: `${data.date}T${data.startTime}:00.000Z`, + runningTime: Number(data.runningTime), + place: { + name: data.placeName, + streetAddress: data.placeStreetAddress, + detailAddress: data.placeDetailAddress, + }, + notice: data.notice, + host: { + name: data.hostName, + phoneNumber: data.hostPhoneNumber, + }, + }, + }); + + toast.success('공연 정보를 저장했습니다.'); + }; + + const confirmSaveShowInfo = async () => { + if (!showInfoForm.formState.isDirty && !isImageFilesDirty) { + return true; + } + + const result = await confirm( + '저장하지 않고 이 페이지를 나가면 작성한 정보가 손실됩니다.\n변경된 정보를 저장할까요?', + { + cancel: '취소하기', + confirm: '저장하기', + }, + ); + + if (result) { + showInfoForm.handleSubmit(onSubmit)(); + } + + return true; + }; + + useEffect(() => { + if (!show) return; + + showInfoForm.reset({ + name: show.name, + date: format(show.date, 'yyyy-MM-dd'), + startTime: format(show.date, 'hh:mm'), + runningTime: `${show.runningTime}`, + placeName: show.place.name, + placeStreetAddress: show.place.streetAddress, + placeDetailAddress: show.place.detailAddress, + notice: show.notice, + hostName: show.host.name, + hostPhoneNumber: show.host.phoneNumber, + }); + + setImageFiles(show.images.map((image) => ({ preview: image.thumbnailPath }))); + setShowImages(show.images); + }, [show, showInfoForm]); + + if (!show || !showSalesInfo) return null; + + const salesStarted = compareAsc(new Date(showSalesInfo.salesStartTime), new Date()) === -1; + return ( - + - {Array.from({ length: 100 }).map(() => ( -

안녕

- ))} + + + { + setImageFiles((prevImageFiles) => [ + ...prevImageFiles, + ...acceptedFiles.map((file) => ({ + ...file, + preview: URL.createObjectURL(file), + })), + ]); + }} + onDeleteImage={(file) => { + setImageFiles((prevImageFiles) => + prevImageFiles.filter((prevFile) => prevFile !== file), + ); + setShowImages((prevShowImages) => + prevShowImages.filter((prevImage) => prevImage.thumbnailPath !== file.preview), + ); + }} + /> + + + + + + + + + +
); diff --git a/apps/admin/src/pages/ShowTicketPage/ShowTicketPage.styles.ts b/apps/admin/src/pages/ShowTicketPage/ShowTicketPage.styles.ts new file mode 100644 index 00000000..f8893e46 --- /dev/null +++ b/apps/admin/src/pages/ShowTicketPage/ShowTicketPage.styles.ts @@ -0,0 +1,24 @@ +import styled from '@emotion/styled'; + +const ShowTicketPage = styled.div` + padding: 0 20px; + margin: 40px 0 68px; +`; + +const ShowTicketForm = styled.form``; + +const ShowTicketFormContent = styled.div` + max-width: 600px; +`; + +const ShowTicketFormDivider = styled.hr` + border-top: 1px solid ${({ theme }) => theme.palette.grey.g20}; + margin: 52px 0; +`; + +export default { + ShowTicketPage, + ShowTicketForm, + ShowTicketFormContent, + ShowTicketFormDivider, +}; diff --git a/apps/admin/src/pages/ShowTicketPage/ShowTicketPage.tsx b/apps/admin/src/pages/ShowTicketPage/ShowTicketPage.tsx new file mode 100644 index 00000000..7d691cad --- /dev/null +++ b/apps/admin/src/pages/ShowTicketPage/ShowTicketPage.tsx @@ -0,0 +1,64 @@ +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; + +import ShowDetailLayout from '~/components/ShowDetailLayout'; +import ShowInvitationTicketFormContent from '~/components/ShowInfoFormContent/ShowInvitationTicketFormContent'; +import ShowSalesTicketFormContent from '~/components/ShowInfoFormContent/ShowSalesTicketFormContent'; +import ShowTicketInfoFormContent from '~/components/ShowInfoFormContent/ShowTicketInfoFormContent'; +import { ShowTicketFormInputs } from '~/components/ShowInfoFormContent/types'; +import { InvitationTicketFormInputs } from '~/components/TicketForm/InvitationTicketForm'; +import { SalesTicketFormInputs } from '~/components/TicketForm/SalesTicketForm'; + +import Styled from './ShowTicketPage.styles'; + +const ShowTicketPage = () => { + const showTicketForm = useForm(); + const [salesTicketList, setSalesTicketList] = useState([]); + const [invitationTicketList, setInvitationTicketList] = useState( + [], + ); + + return ( + + + + + + + + + { + setSalesTicketList((prevList) => + prevList.filter((prevTicket) => prevTicket.name !== ticket.name), + ); + }} + onDeleteTicket={(ticket) => { + setSalesTicketList((prevList) => + prevList.filter((prevTicket) => prevTicket.name !== ticket.name), + ); + }} + /> + + + + { + setInvitationTicketList((prevList) => [...prevList, ticket]); + }} + onDeleteTicket={(ticket) => { + setInvitationTicketList((prevList) => + prevList.filter((prevTicket) => prevTicket.name !== ticket.name), + ); + }} + /> + + + + + ); +}; + +export default ShowTicketPage; diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index b1daed96..70394db1 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -9,5 +9,6 @@ export * from './constants'; export * from './mutations'; export * from './queries'; export { queryKey } from './queryKey'; +export type * from './types'; export { useQueryClient }; diff --git a/packages/api/src/mutations/index.ts b/packages/api/src/mutations/index.ts index cc340e75..b57988e9 100644 --- a/packages/api/src/mutations/index.ts +++ b/packages/api/src/mutations/index.ts @@ -1,7 +1,9 @@ import useAddShow from './useAddShow'; import useCreateInvitationTickets from './useCreateInvitationTickets'; import useCreateSalesTickets from './useCreateSalesTickets'; +import useDeleteShow from './useDeleteShow'; import useEditSalesTicketInfo from './useEditSalesTicketInfo'; +import useEditShowInfo from './useEditShowInfo'; import useKakaoLogin from './useKakaoLogin'; import useKakaoToken from './useKakaoToken'; import useKakaoUserInfo from './useKakaoUserInfo'; @@ -14,7 +16,9 @@ export { useAddShow, useCreateInvitationTickets, useCreateSalesTickets, + useDeleteShow, useEditSalesTicketInfo, + useEditShowInfo, useKakaoLogin, useKakaoToken, useKakaoUserInfo, diff --git a/packages/api/src/mutations/useDeleteShow.ts b/packages/api/src/mutations/useDeleteShow.ts new file mode 100644 index 00000000..88efb462 --- /dev/null +++ b/packages/api/src/mutations/useDeleteShow.ts @@ -0,0 +1,9 @@ +import { useMutation } from '@tanstack/react-query'; + +import { fetcher } from '../fetcher'; + +const deleteShow = (showId: number) => fetcher.delete(`web/v1/host/shows/${showId}`); + +const useDeleteShow = () => useMutation((showId: number) => deleteShow(showId)); + +export default useDeleteShow; diff --git a/packages/api/src/mutations/useEditShowInfo.ts b/packages/api/src/mutations/useEditShowInfo.ts new file mode 100644 index 00000000..04b6ef99 --- /dev/null +++ b/packages/api/src/mutations/useEditShowInfo.ts @@ -0,0 +1,34 @@ +import { useMutation } from '@tanstack/react-query'; + +import { fetcher } from '../fetcher'; + +interface PutShowInfoRequest { + 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; + }; +} + +const putShowInfo = (showId: number, body: PutShowInfoRequest) => + fetcher.put(`web/v1/host/shows/${showId}`, { json: body }); + +const useEditShowInfo = () => + useMutation(({ showId, body }: { showId: number; body: PutShowInfoRequest }) => + putShowInfo(showId, body), + ); + +export default useEditShowInfo; diff --git a/packages/api/src/mutations/useUploadShowImage.ts b/packages/api/src/mutations/useUploadShowImage.ts index 1f7b7c53..a202b72a 100644 --- a/packages/api/src/mutations/useUploadShowImage.ts +++ b/packages/api/src/mutations/useUploadShowImage.ts @@ -9,8 +9,9 @@ import ImageResize from 'image-resize'; import ky from 'ky'; import { fetcher } from '../fetcher'; +import { ShowImage } from '../types'; -export interface ImageFile extends File { +export interface ImageFile extends Partial { preview: string; } @@ -52,7 +53,7 @@ const putS3Upload = (uploadUrl: string, file: File) => }); const useUploadShowImage = () => - useMutation(async (files: ImageFile[]) => { + useMutation(async (files: ImageFile[]) => { const resizedImages = await Promise.all( files.map(async (file) => { const imageFile = await imageResize.play(file.preview); @@ -64,7 +65,7 @@ const useUploadShowImage = () => return await Promise.all( resizedImages.map(async (image, index) => { - const [imageUrl, thumbnailUrl] = await Promise.all( + const [path, thumbnailPath] = await Promise.all( Object.entries(image).map(async ([type, dataUrl]) => { const fileName = type === 'imageFile' ? 'image.png' : 'thumbnail.png'; @@ -76,7 +77,7 @@ const useUploadShowImage = () => }), ); - return { sequence: index + 1, imageUrl, thumbnailUrl }; + return { sequence: index + 1, path, thumbnailPath }; }), ); }); diff --git a/packages/api/src/queries/index.ts b/packages/api/src/queries/index.ts index 7a38e9cd..2039b622 100644 --- a/packages/api/src/queries/index.ts +++ b/packages/api/src/queries/index.ts @@ -1,5 +1,6 @@ import useShowDetail from './useShowDetail'; +import useShowSalesInfo from './useShowSalesInfo'; import useUserAccountInfo from './useUserAccountInfo'; import useUserSummary from './useUserSummary'; -export { useShowDetail, useUserAccountInfo, useUserSummary }; +export { useShowDetail, useShowSalesInfo, useUserAccountInfo, useUserSummary }; diff --git a/packages/api/src/queries/useShowSalesInfo.ts b/packages/api/src/queries/useShowSalesInfo.ts new file mode 100644 index 00000000..f413c164 --- /dev/null +++ b/packages/api/src/queries/useShowSalesInfo.ts @@ -0,0 +1,7 @@ +import { useQuery } from '@tanstack/react-query'; + +import { queryKey } from '../queryKey'; + +const useShowSalesInfo = (showId: number) => useQuery(queryKey.showSalesInfo(showId)); + +export default useShowSalesInfo; diff --git a/packages/api/src/queryKey.ts b/packages/api/src/queryKey.ts index f8d16395..73bc2b00 100644 --- a/packages/api/src/queryKey.ts +++ b/packages/api/src/queryKey.ts @@ -1,7 +1,7 @@ import { createQueryKeys } from '@lukemorales/query-key-factory'; import { fetcher } from './fetcher'; -import { ShowResponse } from './types/show'; +import { ShowResponse, ShowSalesInfoResponse } from './types/show'; import { SettlementAccountInfoResponse, UserProfileSummaryResponse } from './types/users'; export interface Hello { @@ -13,6 +13,10 @@ export const queryKey = createQueryKeys('boolti', { queryKey: [showId], queryFn: () => fetcher.get(`web/v1/host/shows/${showId}`), }), + showSalesInfo: (showId: number) => ({ + queryKey: [showId], + queryFn: () => fetcher.get(`web/v1/host/shows/${showId}/sales-infos`), + }), userAccountInfo: { queryKey: null, queryFn: () => diff --git a/packages/api/src/types/index.ts b/packages/api/src/types/index.ts new file mode 100644 index 00000000..d50ebd1d --- /dev/null +++ b/packages/api/src/types/index.ts @@ -0,0 +1,2 @@ +export * from './show'; +export * from './users'; diff --git a/packages/api/src/types/show.ts b/packages/api/src/types/show.ts index d9ebe8e0..b5cfa382 100644 --- a/packages/api/src/types/show.ts +++ b/packages/api/src/types/show.ts @@ -26,3 +26,10 @@ export interface ShowResponse { host: ShowHost; isEnded: boolean; } + +export interface ShowSalesInfoResponse { + showId: number; + salesStartTime: string; + salesEndTime: string; + ticketNotice: string; +} diff --git a/packages/ui/src/components/Confirm/Confirm.styles.ts b/packages/ui/src/components/Confirm/Confirm.styles.ts index f7fbbf86..4fcc03ee 100644 --- a/packages/ui/src/components/Confirm/Confirm.styles.ts +++ b/packages/ui/src/components/Confirm/Confirm.styles.ts @@ -9,6 +9,7 @@ const DimmedArea = styled.div` display: flex; justify-content: center; align-items: center; + z-index: 999; `; const Confirm = styled.div` diff --git a/packages/ui/src/components/Dialog/Dialog.styles.ts b/packages/ui/src/components/Dialog/Dialog.styles.ts index d3b225b0..eac4b2e9 100644 --- a/packages/ui/src/components/Dialog/Dialog.styles.ts +++ b/packages/ui/src/components/Dialog/Dialog.styles.ts @@ -9,6 +9,7 @@ const DimmedArea = styled.div` display: flex; justify-content: center; align-items: center; + z-index: 999; `; const Dialog = styled.div` diff --git a/packages/ui/src/components/TextField/TextField.styles.ts b/packages/ui/src/components/TextField/TextField.styles.ts index 8992a6e9..f7fb18f4 100644 --- a/packages/ui/src/components/TextField/TextField.styles.ts +++ b/packages/ui/src/components/TextField/TextField.styles.ts @@ -38,7 +38,7 @@ const InputContainer = styled.div` } `; -const InputLabel = styled.label<{ hasError?: boolean }>` +const InputLabel = styled.label<{ hasError?: boolean; disabled?: boolean }>` display: block; width: 100%; height: 48px; @@ -49,6 +49,14 @@ const InputLabel = styled.label<{ hasError?: boolean }>` ${({ hasError, theme }) => (hasError ? theme.palette.status.error : theme.palette.grey.g90)}; background: ${({ theme }) => theme.palette.grey.w}; ${({ theme }) => theme.typo.b3}; + + ${({ disabled, theme }) => + disabled && + ` + border: 1px solid ${theme.palette.grey.g20}; + background: ${theme.palette.grey.g10}; + color: ${theme.palette.grey.g40}; + `} `; const Input = styled.input<{ hasError?: boolean }>` diff --git a/packages/ui/src/components/TextField/index.tsx b/packages/ui/src/components/TextField/index.tsx index 6a06811a..1f3c3586 100644 --- a/packages/ui/src/components/TextField/index.tsx +++ b/packages/ui/src/components/TextField/index.tsx @@ -17,12 +17,12 @@ const TextField = forwardRef(function TextField( {inputType === 'date' && ( - + {rest.value ? rest.value : placeholder} )} {inputType === 'file' && ( - + {rest.fileName ? rest.fileName : placeholder} )} @@ -36,8 +36,8 @@ const TextField = forwardRef(function TextField( type={inputType} {...rest} /> - {inputType === 'date' && } - {inputType === 'time' && } + {inputType === 'date' && !disabled && } + {inputType === 'time' && !disabled && } {buttonProps && ( diff --git a/packages/ui/src/hooks/useToast.tsx b/packages/ui/src/hooks/useToast.tsx index cac69d55..5e317bc4 100644 --- a/packages/ui/src/hooks/useToast.tsx +++ b/packages/ui/src/hooks/useToast.tsx @@ -71,7 +71,7 @@ const useToast = () => { }, }), {}, - ); + ) as Record; }; export default useToast;