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 (
+
+
+
+ 초청 티켓
+
+ 초청 티켓 이용을 원하시면 티켓을 생성해주세요.
+
* 초청 코드는 공연 등록 후 공연 관리 > 티켓 관리에서 확인할 수
+ 있습니다.
+
+
+
+ }
+ onClick={() => {
+ invitationTicketDialog.open({
+ title: '초청 티켓 생성하기',
+ content: ,
+ });
+ }}
+ >
+ 생성하기
+
+
+
+ {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매 이상 판매된 티켓은 삭제할 수 없습니다.
+
+
+
+ }
+ onClick={() => {
+ salesTicketDialog.open({
+ title: '일반 티켓 생성하기',
+ content: ,
+ });
+ }}
+ >
+ 생성하기
+
+
+
+ {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매 이상 판매된 티켓은 삭제할 수 없습니다.
-
-
-
- }
- onClick={() => {
- generalTicketDialog.open({
- title: '일반 티켓 생성하기',
- content: (
-
- ),
- });
- }}
- >
- 생성하기
-
-
-
- {generalTicketList.length > 0 && (
-
- {generalTicketList.map((ticket) => (
-
-
-
- {ticket.name}
-
- 재고 {ticket.quantity}/{ticket.quantity}
-
-
-
- {ticket.price}원 · 1인당 1매
-
-
-
-
-
-
- ))}
-
- )}
-
-
-
-
- 초청 티켓
-
- 초청 티켓 이용을 원하시면 티켓을 생성해주세요.
-
* 초청 코드는 공연 등록 후{' '}
- 공연 관리 > 티켓 관리에서 확인할 수 있습니다.
-
-
-
- }
- onClick={() => {
- invitationTicketDialog.open({
- title: '초청 티켓 생성하기',
- content: (
-
- ),
- });
- }}
- >
- 생성하기
-
-
-
- {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;