From a4e2a22893ce13501669740f6f08906502f4c913 Mon Sep 17 00:00:00 2001 From: Minsu Kim Date: Wed, 6 Nov 2024 23:44:21 +0900 Subject: [PATCH 01/62] =?UTF-8?q?fix:=20=EC=9B=B9=EB=B7=B0=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20=ED=97=A4=EB=8D=94=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/ShowAddCompletePage/index.tsx | 24 ++++++++++--------- apps/admin/src/pages/ShowAddPage/index.tsx | 24 ++++++++++--------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/apps/admin/src/pages/ShowAddCompletePage/index.tsx b/apps/admin/src/pages/ShowAddCompletePage/index.tsx index d06df61e..c4887f74 100644 --- a/apps/admin/src/pages/ShowAddCompletePage/index.tsx +++ b/apps/admin/src/pages/ShowAddCompletePage/index.tsx @@ -60,17 +60,19 @@ const ShowAddCompletePage = () => { - - { - navigate(PATH.HOME); - }} - > - - - 공연 등록 - + {!isWebView && ( + + { + navigate(PATH.HOME); + }} + > + + + 공연 등록 + + )} 공연 등록이 완료되었습니다. diff --git a/apps/admin/src/pages/ShowAddPage/index.tsx b/apps/admin/src/pages/ShowAddPage/index.tsx index ef8c1fef..df7dc4ac 100644 --- a/apps/admin/src/pages/ShowAddPage/index.tsx +++ b/apps/admin/src/pages/ShowAddPage/index.tsx @@ -326,17 +326,19 @@ const ShowAddPage = ({ step }: ShowAddPageProps) => { - - { - navigate(PATH.HOME); - }} - > - - - 공연 등록 - + {!isWebView && ( + + { + navigate(PATH.HOME); + }} + > + + + 공연 등록 + + )} {step === 'info' && ( From df8beffdcb837f7e80e180c4c463d76e4017cc80 Mon Sep 17 00:00:00 2001 From: Minsu Kim Date: Sat, 9 Nov 2024 16:34:50 +0900 Subject: [PATCH 02/62] =?UTF-8?q?feat:=20=EC=96=B4=EB=93=9C=EB=AF=BC=20?= =?UTF-8?q?=ED=83=AD=20=EB=A0=88=EC=9D=B4=EB=B8=94=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/admin/src/components/ShowDetailLayout/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/admin/src/components/ShowDetailLayout/index.tsx b/apps/admin/src/components/ShowDetailLayout/index.tsx index 99c9db48..f3b99cb5 100644 --- a/apps/admin/src/components/ShowDetailLayout/index.tsx +++ b/apps/admin/src/components/ShowDetailLayout/index.tsx @@ -62,8 +62,8 @@ const toTargets = { const label = { INFO: '공연 기본 정보', TICKET: '티켓 관리', - RESERVATION: '방문자 관리', - ENTRANCE: '입장 관리', + RESERVATION: '결제 관리', + ENTRANCE: '방문자 관리', SETTLEMENT: '정산 관리', }; From ceb92799661cbd41f52c6723f8994003202275fa Mon Sep 17 00:00:00 2001 From: Minsu Kim Date: Sat, 9 Nov 2024 16:37:31 +0900 Subject: [PATCH 03/62] =?UTF-8?q?chore:=20=EB=B0=9C=EA=B6=8C=20->=20?= =?UTF-8?q?=EA=B2=B0=EC=A0=9C=20=EC=9B=8C=EB=94=A9=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/admin/src/pages/ShowReservationPage/index.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/admin/src/pages/ShowReservationPage/index.tsx b/apps/admin/src/pages/ShowReservationPage/index.tsx index 6250ba69..88d92411 100644 --- a/apps/admin/src/pages/ShowReservationPage/index.tsx +++ b/apps/admin/src/pages/ShowReservationPage/index.tsx @@ -19,9 +19,9 @@ import { useTheme } from '@emotion/react'; import { BooltiGreyIcon } from '@boolti/icon/src/components/BooltiGreyIcon'; const emptyLabel: Record = { - COMPLETE: '발권 완료된 티켓이 없어요.', - WAIT: '발권 대기 중인 티켓이 없어요.', - CANCEL: '발권 취소된 티켓이 없어요.', + COMPLETE: '결제 완료된 티켓이 없어요.', + WAIT: '결제 대기 중인 티켓이 없어요.', + CANCEL: '결제 취소된 티켓이 없어요.', }; const ShowReservationPage = () => { @@ -125,7 +125,7 @@ const ShowReservationPage = () => { }} isSelected={selectedTicketStatus === 'COMPLETE'} > - 발권 완료 {completeCount} + 결제 완료 {completeCount} { @@ -134,7 +134,7 @@ const ShowReservationPage = () => { }} isSelected={selectedTicketStatus === 'WAIT'} > - 발권 대기 {waitCount} + 결제 대기 {waitCount} { @@ -143,7 +143,7 @@ const ShowReservationPage = () => { }} isSelected={selectedTicketStatus === 'CANCEL'} > - 발권 취소 {cancelCount} + 결제 취소 {cancelCount} From 8503ecfb4613be704065f329e65d12920e483e6a Mon Sep 17 00:00:00 2001 From: Minsu Kim Date: Mon, 11 Nov 2024 20:29:43 +0900 Subject: [PATCH 04/62] =?UTF-8?q?feat:=20=EC=98=88=EB=A7=A4=EC=9E=90=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20v2=20=ED=83=80=EC=9E=85=20=EB=B0=8F=20API?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/api/src/queryKey.ts | 30 +++++++++++++ packages/api/src/types/common.ts | 2 + packages/api/src/types/show.ts | 73 ++++++++++++++++++++++++++++++-- 3 files changed, 102 insertions(+), 3 deletions(-) diff --git a/packages/api/src/queryKey.ts b/packages/api/src/queryKey.ts index 6fefe46a..6728c8b3 100644 --- a/packages/api/src/queryKey.ts +++ b/packages/api/src/queryKey.ts @@ -7,6 +7,7 @@ import { EntranceSummaryResponse, PageEntranceResponse, PageReservationResponse, + PageReservationWithTicketsResponse, ReservationSummaryResponse, SettlementBannersResponse, ShowCastTeamReadResponse, @@ -286,6 +287,35 @@ export const showQueryKeys = createQueryKeys('show', { }); }, }), + reservationV2: ( + showId: number, + page: number, + ticketType: TicketType | undefined = undefined, + paymentManagementStatus: TicketStatus | undefined = undefined, + reservationNameOrPhoneNumber?: string, + ) => ({ + queryKey: [showId, page, reservationNameOrPhoneNumber, ticketType, paymentManagementStatus], + queryFn: () => { + const searchParams: SearchParamsOption = { + page, + }; + if (ticketType) { + searchParams.ticketType = ticketType; + } + if (paymentManagementStatus) { + searchParams.paymentManagementStatus = paymentManagementStatus; + } + if (reservationNameOrPhoneNumber) { + searchParams.reservationNameOrPhoneNumber = reservationNameOrPhoneNumber; + } + return fetcher.get( + `web/v2/shows/${showId}/reservations`, + { + searchParams, + }, + ); + }, + }), salesTicketList: (showId: number) => ({ queryKey: [showId], queryFn: () => diff --git a/packages/api/src/types/common.ts b/packages/api/src/types/common.ts index 1ad377ff..c74bb3cd 100644 --- a/packages/api/src/types/common.ts +++ b/packages/api/src/types/common.ts @@ -11,6 +11,8 @@ export type ReservationStatus = export type TicketStatus = 'COMPLETE' | 'WAIT' | 'CANCEL'; +export type PayementMeans = 'ACCOUNT_TRANSFER' | 'CARD' | 'FREE' | 'SIMPLE_PAYMENT'; + export interface PageResponse { totalPages: number; totalElements: number; diff --git a/packages/api/src/types/show.ts b/packages/api/src/types/show.ts index 86723c89..12b08ca8 100644 --- a/packages/api/src/types/show.ts +++ b/packages/api/src/types/show.ts @@ -1,5 +1,5 @@ import { ShowCastTeamCreateOrUpdateRequest } from './cast'; -import { PageResponse, ReservationStatus, TicketStatus, TicketType } from './common'; +import { PageResponse, PayementMeans, ReservationStatus, TicketStatus, TicketType } from './common'; import { HostType } from './host'; export interface ShowImage { @@ -75,6 +75,75 @@ export interface ShowSalesInfoResponse { ticketNotice: string; } +export interface SalesTicketTypeResponseV2 { + /** 판매 티켓 ID */ + id: number; + /** 판매 티켓 타입 */ + ticketType: TicketType; + /** 티켓 이름 */ + ticketName: string; + /** 티켓 가격(장당) */ + price: number; +} + +export interface TicketResponse { + /** 티켓 ID */ + ticketId: number; + /** cs용(유저용) 티켓 ID */ + csTicketId: string; + /** 판매 티켓 타입 정보. 만약 삭제된 경우 null */ + salesTicketType?: SalesTicketTypeResponseV2; + /** 티켓 생성일시 */ + createdAt: string; + /** 티켓 사용일시 */ + usedAt?: string; +} + +export interface PaymentInfoResponse { + /** 결제자 이름 초청일때는 없음 */ + payerName?: string; + /** 결제자 전화번호 초청일때는 없음 */ + payerPhoneNumber?: string; + /** 결제 수단 */ + means: PayementMeans; +} + +export interface CancelInfoResponse { + /** 취소 요청 사유 */ + cancelReason: string; + /** 취소 요청 일시 */ + cancelRequestAt: string; + /** 취소 완료 일시 */ + canceledAt: string; +} + +export interface ReservationWithTicketsResponse { + /** 예매 ID */ + reservationId: number; + /** cs용(유저용) 예매 ID */ + csReservationId: number; + /** 예매 결제 관리 상태 */ + paymentManagementStatus: TicketStatus; + /** 선물 여부 */ + gift: boolean; + /** 판매 티켓 타입 정보. 만약 삭제된 경우 null */ + salesTicketType?: SalesTicketTypeResponseV2; + /** 티켓 정보 목록. */ + tickets: TicketResponse[]; + /** 결제 정보. 초청 티켓일 경우 없음 */ + paymentInfo?: PaymentInfoResponse; + /** 환불 정보 */ + cancelInfo?: CancelInfoResponse; + /** 예매 생성 일시 */ + createdAt: string; + /** 예매 수정 일시 */ + modifiedAt: string; +} + +export type PageReservationWithTicketsResponse = PageResponse; + +export type PageReservationResponse = PageResponse; + export interface ReservationResponse { /** 티켓 ID */ ticketId: number; @@ -112,8 +181,6 @@ export interface ReservationResponse { csTicketId: string; } -export type PageReservationResponse = PageResponse; - export interface ReservationSummaryResponse { /** 판매 티켓 발권 수량 */ salesTicketSoldCount: number; From 76f64891391dbcb6a66e1f753c5d8b9748006e18 Mon Sep 17 00:00:00 2001 From: Minsu Kim Date: Mon, 11 Nov 2024 20:32:50 +0900 Subject: [PATCH 05/62] =?UTF-8?q?feat:=20=EC=BF=BC=EB=A6=AC=20=ED=9B=85=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/api/src/queries/index.ts | 2 ++ .../queries/useShowReservationWithTickets.ts | 23 +++++++++++++++++++ packages/api/src/queryKey.ts | 2 +- 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 packages/api/src/queries/useShowReservationWithTickets.ts diff --git a/packages/api/src/queries/index.ts b/packages/api/src/queries/index.ts index da2b6620..bc59a69f 100644 --- a/packages/api/src/queries/index.ts +++ b/packages/api/src/queries/index.ts @@ -35,6 +35,7 @@ import useSuperAdminSalesTicketList from './useSuperAdminSalesTicketList'; import useSuperAdminInvitationTicketList from './useSuperAdminInvitationTicketList'; import useSuperAdminInvitationCodeList from './useSuperAdminInvitationCodeList'; import useUserByUserCode from './useUserByUserCode'; +import useShowReservationWithTickets from './useShowReservationWithTickets'; import useCastTeamList from './useCastTeamList'; export { @@ -43,6 +44,7 @@ export { useAdminSettlementEvent, useAdminSettlementInfo, useGift, + useShowReservationWithTickets, useAdminShowDetail, useAdminShowList, useAdminTicketSalesInfo, diff --git a/packages/api/src/queries/useShowReservationWithTickets.ts b/packages/api/src/queries/useShowReservationWithTickets.ts new file mode 100644 index 00000000..06efc150 --- /dev/null +++ b/packages/api/src/queries/useShowReservationWithTickets.ts @@ -0,0 +1,23 @@ +import { useQuery } from '@tanstack/react-query'; + +import { queryKeys } from '../queryKey'; +import { TicketStatus, TicketType } from '../types'; + +const useShowReservationWithTickets = ( + showId: number, + page: number, + ticketType: TicketType | undefined = undefined, + paymentManagementStatus: TicketStatus | undefined = undefined, + reservationNameOrPhoneNumber: string | undefined = undefined, +) => + useQuery({ + ...queryKeys.show.reservationWithTickets( + showId, + page, + ticketType, + paymentManagementStatus, + reservationNameOrPhoneNumber, + ), + }); + +export default useShowReservationWithTickets; diff --git a/packages/api/src/queryKey.ts b/packages/api/src/queryKey.ts index 6728c8b3..a953aa44 100644 --- a/packages/api/src/queryKey.ts +++ b/packages/api/src/queryKey.ts @@ -287,7 +287,7 @@ export const showQueryKeys = createQueryKeys('show', { }); }, }), - reservationV2: ( + reservationWithTickets: ( showId: number, page: number, ticketType: TicketType | undefined = undefined, From 6b791ce002b61aa05a39b821995ca5fbc50228a3 Mon Sep 17 00:00:00 2001 From: Minsu Kim Date: Mon, 11 Nov 2024 21:22:18 +0900 Subject: [PATCH 06/62] =?UTF-8?q?feat:=20=EA=B2=B0=EC=A0=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MobileCardList/MobileCardList.style.ts | 26 +++- .../src/components/MobileCardList/index.tsx | 10 +- .../ReservationTable.styles.ts | 33 +++--- .../src/components/ReservationTable/index.tsx | 111 ++++++++++-------- .../src/pages/ShowReservationPage/index.tsx | 38 +++--- 5 files changed, 127 insertions(+), 91 deletions(-) diff --git a/apps/admin/src/components/MobileCardList/MobileCardList.style.ts b/apps/admin/src/components/MobileCardList/MobileCardList.style.ts index 5c9f4aff..92781754 100644 --- a/apps/admin/src/components/MobileCardList/MobileCardList.style.ts +++ b/apps/admin/src/components/MobileCardList/MobileCardList.style.ts @@ -48,18 +48,42 @@ const DateText = styled.div` `; const UserInfoText = styled.div` + width: 100%; + text-align: left; ${({ theme }) => theme.typo.sh1}; color: ${({ theme }) => theme.palette.grey.g90}; `; +const TicketDetailTextWrap = styled.div` + display: flex; + width: 100%; + justify-content: space-between; + align-items: center; +`; + const TicketInfoText = styled.div` ${({ theme }) => theme.typo.b1}; color: ${({ theme }) => theme.palette.grey.g70}; `; +const TicketPriceText = styled.div` + ${({ theme }) => theme.typo.b1}; + color: ${({ theme }) => theme.palette.grey.g90}; +`; + const ResetButton = styled.button` font-weight: 600; text-decoration: underline; `; -export default { Container, CardItem, Row, DateText, UserInfoText, TicketInfoText, ResetButton }; +export default { + Container, + CardItem, + Row, + DateText, + UserInfoText, + TicketInfoText, + ResetButton, + TicketDetailTextWrap, + TicketPriceText, +}; diff --git a/apps/admin/src/components/MobileCardList/index.tsx b/apps/admin/src/components/MobileCardList/index.tsx index dd9af206..22e7aaa7 100644 --- a/apps/admin/src/components/MobileCardList/index.tsx +++ b/apps/admin/src/components/MobileCardList/index.tsx @@ -12,6 +12,7 @@ type Item = { name: string; phoneNumber: string; ticketName: string; + price: number; date?: string; count: number; }; @@ -39,9 +40,12 @@ function MobileCardList({ searchText, items, emptyText, onClickReset }: Props) { __html: boldText(`${item.name} (${formatPhoneNumber(item.phoneNumber)})`, searchText), }} > - - {item.ticketName} · {item.count}매 - + + + {item.ticketName} · {item.count}매 + + {item.price.toLocaleString()}원 + ); diff --git a/apps/admin/src/components/ReservationTable/ReservationTable.styles.ts b/apps/admin/src/components/ReservationTable/ReservationTable.styles.ts index 9e860eea..18ef2cfb 100644 --- a/apps/admin/src/components/ReservationTable/ReservationTable.styles.ts +++ b/apps/admin/src/components/ReservationTable/ReservationTable.styles.ts @@ -29,29 +29,28 @@ const HeaderItem = styled.span` margin-right: 12px; } &:nth-of-type(1) { - width: 88px; + width: 108px; } &:nth-of-type(2) { - width: 80px; + width: 100px; } &:nth-of-type(3) { - min-width: 100px; + min-width: 140px; } &:nth-of-type(4) { - width: 100px; + width: 80px; } &:nth-of-type(5) { - width: 140px; + width: 180px; } &:nth-of-type(6) { - width: 96px; + width: 50px; } &:nth-of-type(7) { - text-align: right; width: 92px; } &:nth-of-type(8) { - width: 92px; + width: 143px; } `; @@ -76,32 +75,28 @@ const Item = styled.span` margin-right: 12px; } &:nth-of-type(1) { - width: 88px; + width: 108px; } &:nth-of-type(2) { - width: 80px; + width: 100px; } &:nth-of-type(3) { - min-width: 100px; + min-width: 140px; } &:nth-of-type(4) { - width: 100px; + width: 80px; } &:nth-of-type(5) { - width: 140px; + width: 180px; } &:nth-of-type(6) { - width: 96px; + width: 50px; } &:nth-of-type(7) { - text-align: right; width: 92px; } &:nth-of-type(8) { - width: 92px; - } - &:nth-of-type(9) { - width: 100px; + width: 143px; } `; diff --git a/apps/admin/src/components/ReservationTable/index.tsx b/apps/admin/src/components/ReservationTable/index.tsx index 30a64471..193e4893 100644 --- a/apps/admin/src/components/ReservationTable/index.tsx +++ b/apps/admin/src/components/ReservationTable/index.tsx @@ -1,5 +1,6 @@ -import { ReservationResponse, TicketStatus } from '@boolti/api'; +import { ReservationWithTicketsResponse, TicketStatus } from '@boolti/api'; import { + ColumnDef, createColumnHelper, flexRender, getCoreRowModel, @@ -12,86 +13,95 @@ import { formatPhoneNumber } from '~/utils/format'; import Styled from './ReservationTable.styles'; -const columnHelper = createColumnHelper(); +const columnHelper = createColumnHelper(); -const columns = [ - columnHelper.accessor('csTicketId', { - header: '티켓 번호', - }), - columnHelper.accessor('ticketType', { - header: '티켓 종류', - cell: (props) => `${props.getValue() === 'INVITE' ? '초청' : '일반'}티켓`, - }), - columnHelper.accessor('ticketName', { - header: '티켓 이름', +const getColumns = (ticketStatus: TicketStatus) => [ + columnHelper.accessor('csReservationId', { + header: '주문 번호', }), - columnHelper.accessor('reservationName', { - header: '방문자 이름', + columnHelper.accessor('paymentInfo.payerName', { + header: '결제자명', cell: (props) => { const { searchText = '' } = (props.table.options.meta ?? {}) as { searchText: string }; return ( - + ); }, }), - columnHelper.accessor('reservationPhoneNumber', { + columnHelper.accessor('paymentInfo.payerPhoneNumber', { header: '연락처', cell: (props) => { const { searchText = '' } = (props.table.options.meta ?? {}) as { searchText: string }; return ( ); }, }), - columnHelper.accessor('csReservationId', { - header: '주문 번호', - }), - columnHelper.accessor('ticketPrice', { - header: (props) => - (props.table.options.meta as { ticketStatus: TicketStatus }).ticketStatus === 'CANCEL' - ? '환불 금액' - : '결제 금액', - cell: (props) => `${props.getValue().toLocaleString()}원`, - }), - columnHelper.accessor('means', { - header: (props) => - (props.table.options.meta as { ticketStatus: TicketStatus }).ticketStatus === 'CANCEL' - ? '환불 방법' - : '결제 방법', + columnHelper.accessor('salesTicketType.ticketType', { + header: '티켓종류', cell: (props) => { switch (props.getValue()) { - case 'ACCOUNT_TRANSFER': - return '계좌이체'; - case 'CARD': - return '카드'; - case 'FREE': - return '-'; - case 'SIMPLE_PAYMENT': - return '간편결제'; + case 'INVITE': + return '초청 티켓'; + case 'SALE': + return '일반 티켓'; } }, }), + columnHelper.accessor('salesTicketType.ticketName', { + header: '티켓명', + }), + columnHelper.accessor('tickets', { + header: '매수', + cell: (props) => `${props.getValue().length}매`, + }), + columnHelper.accessor((row) => (row.salesTicketType?.price ?? 0) * row.tickets.length, { + header: ticketStatus === 'CANCEL' ? '취소 금액' : '결제 금액', + cell: (props) => `${props.getValue().toLocaleString()}원`, + }), + ...(ticketStatus === 'COMPLETE' + ? [ + columnHelper.accessor( + (row) => + row.tickets.length > 1 + ? `${row.tickets[0].csTicketId} 외 ${row.tickets.length - 1}개` + : row.tickets[0]?.csTicketId, + { + header: '티켓 번호', + }, + ), + ] + : []), columnHelper.accessor( - (props) => - props.ticketStatus === 'CANCEL' && props.canceledAt ? props.canceledAt : props.ticketIssuedAt, + (row) => { + if (ticketStatus === 'CANCEL') { + return row.cancelInfo?.canceledAt; + } + + return row.createdAt; + }, { - id: 'at', - header: (props) => - (props.table.options.meta as { ticketStatus: TicketStatus }).ticketStatus === 'CANCEL' - ? '환불일시' - : '발권일시', - cell: (props) => (props.getValue() ? format(props.getValue(), 'yyyy/MM/dd HH:mm') : '-'), + header: ticketStatus === 'CANCEL' ? '취소 일시' : '결제 일시', + cell: (props) => { + const value = props.getValue(); + if (value) { + return format(value, 'yyyy/MM/dd HH:mm'); + } + return '-'; + }, }, ), ]; interface Props { emptyText: string; - data: ReservationResponse[]; + data: ReservationWithTicketsResponse[]; selectedTicketStatus: TicketStatus; searchText: string; onClickReset?: VoidFunction; @@ -106,11 +116,10 @@ const ReservationTable = ({ }: Props) => { const isSearchResult = searchText !== ''; const table = useReactTable({ - columns, + columns: getColumns(selectedTicketStatus), data, getCoreRowModel: getCoreRowModel(), meta: { - ticketStatus: selectedTicketStatus, searchText, }, }); diff --git a/apps/admin/src/pages/ShowReservationPage/index.tsx b/apps/admin/src/pages/ShowReservationPage/index.tsx index 88d92411..24cee5c3 100644 --- a/apps/admin/src/pages/ShowReservationPage/index.tsx +++ b/apps/admin/src/pages/ShowReservationPage/index.tsx @@ -1,8 +1,8 @@ import { TicketStatus, useShowDetail, - useShowReservations, useShowReservationSummary, + useShowReservationWithTickets, } from '@boolti/api'; import { ClearIcon, SearchIcon } from '@boolti/icon'; import { useEffect, useState } from 'react'; @@ -42,18 +42,20 @@ const ShowReservationPage = () => { const { data: show } = useShowDetail(showId); const { data: reservationSummary } = useShowReservationSummary(showId); - const { data: reservationData, isLoading: isReservationPagesLoading } = useShowReservations( - showId, - currentPage, - selectedTicketType.value === 'ALL' ? undefined : selectedTicketType.value, - selectedTicketStatus, - debouncedSearchText, - ); + const { data: reservationData, isLoading: isReservationPagesLoading } = + useShowReservationWithTickets( + showId, + currentPage, + selectedTicketType.value === 'ALL' ? undefined : selectedTicketType.value, + selectedTicketStatus, + debouncedSearchText, + ); const totalPages = reservationData?.totalPages ?? 0; const reservations = (reservationData?.content ?? []).filter( - ({ ticketStatus, ticketType }) => - ticketStatus === selectedTicketStatus && - (selectedTicketType.value === 'ALL' || ticketType === selectedTicketType.value), + ({ paymentManagementStatus, salesTicketType }) => + paymentManagementStatus === selectedTicketStatus && + (selectedTicketType.value === 'ALL' || + salesTicketType?.ticketType === selectedTicketType.value), ); const onClickReset = () => { @@ -185,12 +187,14 @@ const ShowReservationPage = () => { ({ - id: reservation.ticketId, - badgeText: reservation.ticketType === 'INVITE' ? '초청티켓' : '일반티켓', - name: reservation.reservationName, - phoneNumber: reservation.reservationPhoneNumber, - ticketName: reservation.ticketName, - count: 1, + id: reservation.csReservationId, + badgeText: + reservation.salesTicketType?.ticketType === 'INVITE' ? '초청티켓' : '일반티켓', + name: reservation.paymentInfo?.payerName ?? '', + phoneNumber: reservation.paymentInfo?.payerPhoneNumber ?? '', + ticketName: reservation.salesTicketType?.ticketName ?? '', + count: reservation.tickets.length, + price: (reservation.salesTicketType?.price ?? 0) * reservation.tickets.length, }))} searchText={debouncedSearchText} emptyText={emptyLabel[selectedTicketStatus]} From 7f00428e243a6a5e65bb5f2216d89244ffa46a23 Mon Sep 17 00:00:00 2001 From: Minsu Kim Date: Mon, 11 Nov 2024 22:00:48 +0900 Subject: [PATCH 07/62] =?UTF-8?q?feat:=20=ED=88=B4=ED=8C=81=20=EB=85=B8?= =?UTF-8?q?=EC=B6=9C=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReservationTable.styles.ts | 26 ++++ .../src/components/ReservationTable/index.tsx | 128 ++++++++++++------ 2 files changed, 115 insertions(+), 39 deletions(-) diff --git a/apps/admin/src/components/ReservationTable/ReservationTable.styles.ts b/apps/admin/src/components/ReservationTable/ReservationTable.styles.ts index 18ef2cfb..509435ef 100644 --- a/apps/admin/src/components/ReservationTable/ReservationTable.styles.ts +++ b/apps/admin/src/components/ReservationTable/ReservationTable.styles.ts @@ -117,6 +117,29 @@ const ResetButton = styled(Button)` margin-top: 18px; `; +const TooltipItemColumn = styled.ul` + display: block; +`; + +const TooltipItemRow = styled.li` + display: flex; + justify-content: space-between; + align-items: center; + & > span { + ${({ theme }) => theme.typo.c1}; + color: ${({ theme }) => theme.palette.grey.w}; + } + & > span:first-of-type { + margin-right: 16px; + } +`; + +const TooltipAnchor = styled.a` + &:hover { + color: ${({ theme }) => theme.palette.grey.g50}; + } +`; + export default { Container, HeaderItem, @@ -125,4 +148,7 @@ export default { Item, Empty, ResetButton, + TooltipItemColumn, + TooltipItemRow, + TooltipAnchor, }; diff --git a/apps/admin/src/components/ReservationTable/index.tsx b/apps/admin/src/components/ReservationTable/index.tsx index 193e4893..1e0ff72b 100644 --- a/apps/admin/src/components/ReservationTable/index.tsx +++ b/apps/admin/src/components/ReservationTable/index.tsx @@ -1,6 +1,5 @@ import { ReservationWithTicketsResponse, TicketStatus } from '@boolti/api'; import { - ColumnDef, createColumnHelper, flexRender, getCoreRowModel, @@ -12,6 +11,8 @@ import { boldText } from '~/utils/boldText'; import { formatPhoneNumber } from '~/utils/format'; import Styled from './ReservationTable.styles'; +import { Tooltip } from 'react-tooltip'; +import { palette } from '@boolti/ui'; const columnHelper = createColumnHelper(); @@ -74,6 +75,27 @@ const getColumns = (ticketStatus: TicketStatus) => [ : row.tickets[0]?.csTicketId, { header: '티켓 번호', + cell: (props) => { + const useTooltip = props.row.original.tickets.length > 1; + if (useTooltip) { + return ( + content + ' ' + ticket.csTicketId, '') + .trim() + : '' + } + > + {props.getValue()} + + ); + } + return props.getValue(); + }, }, ), ] @@ -124,45 +146,73 @@ const ReservationTable = ({ }, }); return ( - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender(header.column.columnDef.header, header.getContext())} - - ))} - - ))} - {data.length === 0 ? ( - - {isSearchResult ? ( - <> - 검색 결과가 없어요.{'\n'}방문자 이름 또는 연락처를 변경해보세요. - - 검색 초기화 - - - ) : ( - emptyText - )} - - ) : ( - <> - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - + <> + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} + + ))} + + ))} + {data.length === 0 ? ( + + {isSearchResult ? ( + <> + 검색 결과가 없어요.{'\n'}방문자 이름 또는 연락처를 변경해보세요. + + 검색 초기화 + + + ) : ( + emptyText + )} + + ) : ( + <> + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + + )} + + { + const ticketIds = (content ?? '').split(' '); + return ( + + {ticketIds.map((id, index) => ( + + No.{index + 1} + {id} + ))} - - ))} - - )} - + + ); + }} + /> + ); }; From ef792475b87fd24ffa3f172f775d2e018025a36a Mon Sep 17 00:00:00 2001 From: Minsu Kim Date: Mon, 11 Nov 2024 23:05:33 +0900 Subject: [PATCH 08/62] =?UTF-8?q?feat:=20=EC=95=84=EC=A7=81=20=EC=84=A0?= =?UTF-8?q?=EB=AC=BC=20=EB=93=B1=EB=A1=9D=EB=90=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EC=9D=80=20=EA=B2=BD=EC=9A=B0=20=EB=8C=80=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReservationTable/ReservationTable.styles.ts | 6 ++++++ .../src/components/ReservationTable/index.tsx | 17 +++++++++++------ packages/api/src/types/show.ts | 13 +++++++++++-- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/apps/admin/src/components/ReservationTable/ReservationTable.styles.ts b/apps/admin/src/components/ReservationTable/ReservationTable.styles.ts index 509435ef..380ae840 100644 --- a/apps/admin/src/components/ReservationTable/ReservationTable.styles.ts +++ b/apps/admin/src/components/ReservationTable/ReservationTable.styles.ts @@ -100,6 +100,11 @@ const Item = styled.span` } `; +const DisabledText = styled.span` + ${({ theme }) => theme.typo.b2}; + color: ${({ theme }) => theme.palette.grey.g30}; +`; + const Empty = styled.div` display: flex; flex-direction: column; @@ -146,6 +151,7 @@ export default { HeaderRow, Row, Item, + DisabledText, Empty, ResetButton, TooltipItemColumn, diff --git a/apps/admin/src/components/ReservationTable/index.tsx b/apps/admin/src/components/ReservationTable/index.tsx index 1e0ff72b..8d1c2585 100644 --- a/apps/admin/src/components/ReservationTable/index.tsx +++ b/apps/admin/src/components/ReservationTable/index.tsx @@ -102,18 +102,23 @@ const getColumns = (ticketStatus: TicketStatus) => [ : []), columnHelper.accessor( (row) => { - if (ticketStatus === 'CANCEL') { - return row.cancelInfo?.canceledAt; + if (ticketStatus === 'CANCEL' && row.cancelInfo) { + return { type: 'date', value: row.cancelInfo.canceledAt }; } - return row.createdAt; + return row.gift && !row.gift.done + ? { type: 'text', value: '아직 선물이 등록되지 않았습니다.' } + : { type: 'date', value: row.createdAt }; }, { header: ticketStatus === 'CANCEL' ? '취소 일시' : '결제 일시', cell: (props) => { - const value = props.getValue(); - if (value) { - return format(value, 'yyyy/MM/dd HH:mm'); + const { type, value } = props.getValue(); + switch (type) { + case 'date': + return format(value, 'yyyy/MM/dd HH:mm'); + case 'text': + return {value}; } return '-'; }, diff --git a/packages/api/src/types/show.ts b/packages/api/src/types/show.ts index 12b08ca8..64914cb1 100644 --- a/packages/api/src/types/show.ts +++ b/packages/api/src/types/show.ts @@ -117,6 +117,15 @@ export interface CancelInfoResponse { canceledAt: string; } +export interface GiftResponseV2 { + /** 선물 ID */ + id: number; + /** 선물 수신 완료 여부 */ + done: boolean; + /** 선물 생성 일시 */ + createdAt: string; +} + export interface ReservationWithTicketsResponse { /** 예매 ID */ reservationId: number; @@ -124,8 +133,8 @@ export interface ReservationWithTicketsResponse { csReservationId: number; /** 예매 결제 관리 상태 */ paymentManagementStatus: TicketStatus; - /** 선물 여부 */ - gift: boolean; + /** 선물 정보 선물이 아니면 null */ + gift?: GiftResponseV2; /** 판매 티켓 타입 정보. 만약 삭제된 경우 null */ salesTicketType?: SalesTicketTypeResponseV2; /** 티켓 정보 목록. */ From aa03572d17ecd1521329ac2a5e0c58bd4731bd8b Mon Sep 17 00:00:00 2001 From: Minsu Kim Date: Mon, 11 Nov 2024 23:22:25 +0900 Subject: [PATCH 09/62] =?UTF-8?q?feat:=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=ED=83=80=EC=9E=85=EB=B3=84=20=EC=8A=A4?= =?UTF-8?q?=ED=83=80=EC=9D=BC=20=EB=8C=80=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/MobileCardList/MobileCardList.style.ts | 6 ++++-- apps/admin/src/components/MobileCardList/index.tsx | 10 +++++++++- apps/admin/src/components/ReservationTable/index.tsx | 4 ++-- apps/admin/src/pages/ShowReservationPage/index.tsx | 8 +++++--- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/apps/admin/src/components/MobileCardList/MobileCardList.style.ts b/apps/admin/src/components/MobileCardList/MobileCardList.style.ts index 92781754..26157d43 100644 --- a/apps/admin/src/components/MobileCardList/MobileCardList.style.ts +++ b/apps/admin/src/components/MobileCardList/MobileCardList.style.ts @@ -66,9 +66,11 @@ const TicketInfoText = styled.div` color: ${({ theme }) => theme.palette.grey.g70}; `; -const TicketPriceText = styled.div` +const TicketPriceText = styled.div<{ type: 'PRICE' | 'CANCELED' | 'NOT_REGISTERED' }>` ${({ theme }) => theme.typo.b1}; - color: ${({ theme }) => theme.palette.grey.g90}; + color: ${({ theme, type }) => + type === 'PRICE' ? theme.palette.grey.g90 : theme.palette.grey.g30}; + text-decoration: ${({ type }) => (type === 'CANCELED' ? 'line-through' : undefined)}; `; const ResetButton = styled.button` diff --git a/apps/admin/src/components/MobileCardList/index.tsx b/apps/admin/src/components/MobileCardList/index.tsx index 22e7aaa7..ec5c4752 100644 --- a/apps/admin/src/components/MobileCardList/index.tsx +++ b/apps/admin/src/components/MobileCardList/index.tsx @@ -13,6 +13,8 @@ type Item = { phoneNumber: string; ticketName: string; price: number; + isCanceled: boolean; + isNotGiftRegister: boolean; date?: string; count: number; }; @@ -44,7 +46,13 @@ function MobileCardList({ searchText, items, emptyText, onClickReset }: Props) { {item.ticketName} · {item.count}매 - {item.price.toLocaleString()}원 + + {item.isNotGiftRegister ? '선물 미등록' : `${item.price.toLocaleString()}원`} + diff --git a/apps/admin/src/components/ReservationTable/index.tsx b/apps/admin/src/components/ReservationTable/index.tsx index 8d1c2585..d9ff111a 100644 --- a/apps/admin/src/components/ReservationTable/index.tsx +++ b/apps/admin/src/components/ReservationTable/index.tsx @@ -193,13 +193,13 @@ const ReservationTable = ({ {ticketIds.map((id, index) => ( - + No.{index + 1} {id} diff --git a/apps/admin/src/pages/ShowReservationPage/index.tsx b/apps/admin/src/pages/ShowReservationPage/index.tsx index 24cee5c3..7f57c2ad 100644 --- a/apps/admin/src/pages/ShowReservationPage/index.tsx +++ b/apps/admin/src/pages/ShowReservationPage/index.tsx @@ -127,7 +127,7 @@ const ShowReservationPage = () => { }} isSelected={selectedTicketStatus === 'COMPLETE'} > - 결제 완료 {completeCount} + {isMobile ? '완료' : '결제 완료'} {completeCount} { @@ -136,7 +136,7 @@ const ShowReservationPage = () => { }} isSelected={selectedTicketStatus === 'WAIT'} > - 결제 대기 {waitCount} + {isMobile ? '대기' : '결제 대기'} {waitCount} { @@ -145,7 +145,7 @@ const ShowReservationPage = () => { }} isSelected={selectedTicketStatus === 'CANCEL'} > - 결제 취소 {cancelCount} + {isMobile ? '취소' : '결제 취소'} {cancelCount} @@ -195,6 +195,8 @@ const ShowReservationPage = () => { ticketName: reservation.salesTicketType?.ticketName ?? '', count: reservation.tickets.length, price: (reservation.salesTicketType?.price ?? 0) * reservation.tickets.length, + isCanceled: !!reservation.cancelInfo, + isNotGiftRegister: !!reservation.gift && !reservation.gift?.done, }))} searchText={debouncedSearchText} emptyText={emptyLabel[selectedTicketStatus]} From 2a29347c02d6954e52ff7f2bc96ef98ea8a77e46 Mon Sep 17 00:00:00 2001 From: Minsu Kim Date: Mon, 11 Nov 2024 23:38:46 +0900 Subject: [PATCH 10/62] =?UTF-8?q?feat:=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20?= =?UTF-8?q?=EB=B0=94=ED=85=80=EC=8B=9C=ED=8A=B8=20=ED=95=84=ED=84=B0=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20=EB=B0=8F=20=ED=83=80=EC=9E=85=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TicketTypeSelect.styles.ts | 23 ++++++++++ .../src/components/TicketTypeSelect/index.tsx | 44 ++++++++++++++++--- .../src/pages/ShowEnterancePage/index.tsx | 3 ++ 3 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 apps/admin/src/components/TicketTypeSelect/TicketTypeSelect.styles.ts diff --git a/apps/admin/src/components/TicketTypeSelect/TicketTypeSelect.styles.ts b/apps/admin/src/components/TicketTypeSelect/TicketTypeSelect.styles.ts new file mode 100644 index 00000000..2f396410 --- /dev/null +++ b/apps/admin/src/components/TicketTypeSelect/TicketTypeSelect.styles.ts @@ -0,0 +1,23 @@ +import styled from '@emotion/styled'; + +const MobileMenu = styled.div` + padding-bottom: 8px; +`; + +const Item = styled.button<{ isSelected: boolean }>` + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + height: 48px; + padding: 12px 0; + ${({ theme, isSelected }) => (isSelected ? theme.typo.sh1 : theme.typo.b3)}; + color: ${({ theme, isSelected }) => + isSelected ? theme.palette.grey.g90 : theme.palette.grey.g70}; + cursor: pointer; +`; + +export default { + MobileMenu, + Item, +}; diff --git a/apps/admin/src/components/TicketTypeSelect/index.tsx b/apps/admin/src/components/TicketTypeSelect/index.tsx index f313dd0a..dd0c2da5 100644 --- a/apps/admin/src/components/TicketTypeSelect/index.tsx +++ b/apps/admin/src/components/TicketTypeSelect/index.tsx @@ -1,6 +1,8 @@ -import { ChevronRightIcon } from '@boolti/icon'; -import { breakpoint } from '@boolti/ui'; +import { CheckIcon, ChevronRightIcon } from '@boolti/icon'; +import { breakpoint, useDialog } from '@boolti/ui'; import { useTheme } from '@emotion/react'; +import Styled from './TicketTypeSelect.styles'; + import Select from 'react-select'; import { useDeviceWidth } from '~/hooks/useDeviceWidth'; @@ -21,17 +23,44 @@ export const options = [ const TicketTypeSelect = ({ onChange, value }: Props) => { const theme = useTheme(); const width = useDeviceWidth(); + const isMobile = width < parseInt(theme.breakpoint.mobile, 10); + + const { open, close } = useDialog(); return ( - + {title && onClose && ( From 9e304160896a7d55eadfe171de78fda8c2055343 Mon Sep 17 00:00:00 2001 From: Shim MunSeong Date: Tue, 12 Nov 2024 22:15:09 +0900 Subject: [PATCH 13/62] =?UTF-8?q?feat:=20=EC=B6=9C=EC=97=B0=EC=A7=84=20?= =?UTF-8?q?=ED=8C=80=20=EC=88=9C=EC=84=9C=20=EB=B3=80=EA=B2=BD=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pnp.cjs | 92 ++++++++ apps/admin/package.json | 2 + apps/admin/src/App.tsx | 6 +- .../ShowCastInfo/ShowCastInfo.styles.ts | 24 +- .../src/components/ShowCastInfo/index.tsx | 218 +++++++++++------- .../ShowCastInfoFormDialogContent/index.tsx | 5 +- apps/admin/src/pages/ShowInfoPage/index.tsx | 54 ++++- packages/api/src/mutations/index.ts | 2 + .../src/mutations/useChangeCastTeamOrder.ts | 15 ++ packages/api/src/queries/useCastTeamList.ts | 11 +- packages/icon/src/components/Menu.tsx | 6 +- yarn.lock | 79 ++++++- 12 files changed, 423 insertions(+), 91 deletions(-) create mode 100644 packages/api/src/mutations/useChangeCastTeamOrder.ts diff --git a/.pnp.cjs b/.pnp.cjs index 23326a13..94112257 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -5654,6 +5654,33 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@react-dnd/asap", [\ + ["npm:5.0.2", {\ + "packageLocation": "./.yarn/cache/@react-dnd-asap-npm-5.0.2-66021d3d61-0063db616d.zip/node_modules/@react-dnd/asap/",\ + "packageDependencies": [\ + ["@react-dnd/asap", "npm:5.0.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@react-dnd/invariant", [\ + ["npm:4.0.2", {\ + "packageLocation": "./.yarn/cache/@react-dnd-invariant-npm-4.0.2-826eacc1ea-b303cc53fc.zip/node_modules/@react-dnd/invariant/",\ + "packageDependencies": [\ + ["@react-dnd/invariant", "npm:4.0.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@react-dnd/shallowequal", [\ + ["npm:4.0.2", {\ + "packageLocation": "./.yarn/cache/@react-dnd-shallowequal-npm-4.0.2-f944714335-9a352fd176.zip/node_modules/@react-dnd/shallowequal/",\ + "packageDependencies": [\ + ["@react-dnd/shallowequal", "npm:4.0.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@react-pdf/fns", [\ ["npm:2.2.1", {\ "packageLocation": "./.yarn/cache/@react-pdf-fns-npm-2.2.1-77536ed89f-457bdff57e.zip/node_modules/@react-pdf/fns/",\ @@ -8560,6 +8587,8 @@ const RAW_RUNTIME_STATE = ["qrcode.react", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:3.1.0"],\ ["react", "npm:18.2.0"],\ ["react-daum-postcode", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:3.1.3"],\ + ["react-dnd", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:16.0.1"],\ + ["react-dnd-html5-backend", "npm:16.0.1"],\ ["react-dom", "virtual:de80dc576383b2386358abc0e9fe49c00e3397fe355a0337462b73ab3115c2e557eb85784ee0fe776394cc11dd020b4e84dbbd75acf72ee6d54415d82d21f5c5#npm:18.2.0"],\ ["react-dropzone", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:14.2.3"],\ ["react-hook-form", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:7.50.0"],\ @@ -10326,6 +10355,18 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["dnd-core", [\ + ["npm:16.0.1", {\ + "packageLocation": "./.yarn/cache/dnd-core-npm-16.0.1-552224cee0-6b852c576c.zip/node_modules/dnd-core/",\ + "packageDependencies": [\ + ["dnd-core", "npm:16.0.1"],\ + ["@react-dnd/asap", "npm:5.0.2"],\ + ["@react-dnd/invariant", "npm:4.0.2"],\ + ["redux", "npm:4.2.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["doctrine", [\ ["npm:2.1.0", {\ "packageLocation": "./.yarn/cache/doctrine-npm-2.1.0-ac15d049b7-b6416aaff1.zip/node_modules/doctrine/",\ @@ -16234,6 +16275,47 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["react-dnd", [\ + ["npm:16.0.1", {\ + "packageLocation": "./.yarn/cache/react-dnd-npm-16.0.1-974f047d7b-d069435750.zip/node_modules/react-dnd/",\ + "packageDependencies": [\ + ["react-dnd", "npm:16.0.1"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:16.0.1", {\ + "packageLocation": "./.yarn/__virtual__/react-dnd-virtual-4b292e52c3/0/cache/react-dnd-npm-16.0.1-974f047d7b-d069435750.zip/node_modules/react-dnd/",\ + "packageDependencies": [\ + ["react-dnd", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:16.0.1"],\ + ["@react-dnd/invariant", "npm:4.0.2"],\ + ["@react-dnd/shallowequal", "npm:4.0.2"],\ + ["@types/hoist-non-react-statics", null],\ + ["@types/node", null],\ + ["@types/react", "npm:18.2.48"],\ + ["dnd-core", "npm:16.0.1"],\ + ["fast-deep-equal", "npm:3.1.3"],\ + ["hoist-non-react-statics", "npm:3.3.2"],\ + ["react", "npm:18.2.0"]\ + ],\ + "packagePeers": [\ + "@types/hoist-non-react-statics",\ + "@types/node",\ + "@types/react",\ + "react"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["react-dnd-html5-backend", [\ + ["npm:16.0.1", {\ + "packageLocation": "./.yarn/cache/react-dnd-html5-backend-npm-16.0.1-754940d855-6e4b632a11.zip/node_modules/react-dnd-html5-backend/",\ + "packageDependencies": [\ + ["react-dnd-html5-backend", "npm:16.0.1"],\ + ["dnd-core", "npm:16.0.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["react-docgen", [\ ["npm:7.0.3", {\ "packageLocation": "./.yarn/cache/react-docgen-npm-7.0.3-ea0f679a0f-74622750e6.zip/node_modules/react-docgen/",\ @@ -16964,6 +17046,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["redux", [\ + ["npm:4.2.1", {\ + "packageLocation": "./.yarn/cache/redux-npm-4.2.1-e7e2cf2e37-136d98b3d5.zip/node_modules/redux/",\ + "packageDependencies": [\ + ["redux", "npm:4.2.1"],\ + ["@babel/runtime", "npm:7.23.9"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["regenerate", [\ ["npm:1.4.2", {\ "packageLocation": "./.yarn/cache/regenerate-npm-1.4.2-b296c5b63a-f73c9eba5d.zip/node_modules/regenerate/",\ diff --git a/apps/admin/package.json b/apps/admin/package.json index 9432d0e0..f49ff77f 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -29,6 +29,8 @@ "qrcode.react": "^3.1.0", "react": "^18.2.0", "react-daum-postcode": "^3.1.3", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", "react-hook-form": "^7.50.0", diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx index 2ed23ccf..6e602774 100644 --- a/apps/admin/src/App.tsx +++ b/apps/admin/src/App.tsx @@ -3,6 +3,8 @@ import './index.css'; import { QueryClientProvider } from '@boolti/api'; import { BooltiUIProvider } from '@boolti/ui'; +import { DndProvider } from 'react-dnd' +import { HTML5Backend } from 'react-dnd-html5-backend' import { setDefaultOptions } from 'date-fns'; import { ko } from 'date-fns/locale'; import { @@ -155,7 +157,9 @@ const routes: RouteObject[] = [ - + + + diff --git a/apps/admin/src/components/ShowCastInfo/ShowCastInfo.styles.ts b/apps/admin/src/components/ShowCastInfo/ShowCastInfo.styles.ts index da058c2d..d04a1082 100644 --- a/apps/admin/src/components/ShowCastInfo/ShowCastInfo.styles.ts +++ b/apps/admin/src/components/ShowCastInfo/ShowCastInfo.styles.ts @@ -18,8 +18,6 @@ const Header = styled.div` align-items: center; border-radius: 8px 8px 0px 0px; border: 1px solid ${({ theme }) => theme.palette.grey.g20}; - color: ${({ theme }) => theme.palette.grey.g90}; - ${({ theme }) => theme.typo.sh2}; padding: 24px 28px; &:last-child { @@ -27,6 +25,25 @@ const Header = styled.div` } `; +const HeaderNameWrapper = styled.div` + display: flex; + align-items: center; + gap: 12px; +` + +const Handle = styled.div` + display: inline-flex; + align-items: center; + justify-content: center; + color: ${({ theme }) => theme.palette.grey.g40}; + cursor: move; +` + +const Name = styled.span` + color: ${({ theme }) => theme.palette.grey.g90}; + ${({ theme }) => theme.typo.sh2}; +`; + const EditButton = styled(Button)` padding: 13px 18px; & > svg { @@ -118,6 +135,9 @@ const CollapseButton = styled.button` export default { Container, Header, + HeaderNameWrapper, + Handle, + Name, Cast, CollapseButton, EditButton, diff --git a/apps/admin/src/components/ShowCastInfo/index.tsx b/apps/admin/src/components/ShowCastInfo/index.tsx index 05d7ae62..9ffcbd4e 100644 --- a/apps/admin/src/components/ShowCastInfo/index.tsx +++ b/apps/admin/src/components/ShowCastInfo/index.tsx @@ -1,19 +1,72 @@ import { useDialog } from '@boolti/ui'; +import { useDrag, useDrop } from 'react-dnd' import Styled from './ShowCastInfo.styles'; -import { EditIcon, ChevronDownIcon, ChevronUpIcon, UserIcon } from '@boolti/icon'; -import { useState } from 'react'; +import { EditIcon, ChevronDownIcon, ChevronUpIcon, UserIcon, MenuIcon } from '@boolti/icon'; +import { useRef, useState } from 'react'; import ShowCastInfoFormDialogContent, { TempShowCastInfoFormInput, } from '../ShowCastInfoFormDialogContent'; +import { ShowCastTeamReadResponse } from '@boolti/api'; + +export interface CastTeamListDraft extends ShowCastTeamReadResponse { + index: number; +} interface Props { - showCastInfo: TempShowCastInfoFormInput; + showCastInfo: CastTeamListDraft; + index: number; onSave: (value: TempShowCastInfoFormInput) => Promise; + onDropHover: (draggedItemId: number, hoverIndex: number) => void; + onDrop: () => void; onDelete?: () => Promise; } -const ShowCastInfo = ({ showCastInfo, onSave, onDelete }: Props) => { +interface DragItem { + id: number + index: number +} + +const ShowCastInfo = ({ showCastInfo, index, onSave, onDropHover, onDrop, onDelete }: Props) => { + const ref = useRef(null) + const [{ isDragging }, drag, preview] = useDrag(() => ({ + type: 'castTeam', + previewOptions: { + captureDraggingState: true, + }, + item: { id: showCastInfo.id, index: showCastInfo.index }, + collect: (monitor) => ({ + isDragging: monitor.isDragging() + }), + })) + const [, drop] = useDrop({ + accept: 'castTeam', + hover(item: DragItem, monitor) { + if (!ref.current) return + if (!monitor.canDrop()) return + if (item.id === showCastInfo.id) return + + const dragIndex = item.index + const hoverIndex = index + + const hoverBoundingRect = ref.current.getBoundingClientRect() + const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2 + const clientOffset = monitor.getClientOffset() + if (!clientOffset) return + + const hoverClientY = clientOffset.y - hoverBoundingRect.top + if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) return + if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) return + + item.index = hoverIndex + + onDropHover(item.id, index) + }, + drop() { + onDrop() + } + }) + const { members = [] } = showCastInfo; const memberLength = members.length ?? 0; const dialog = useDialog(); @@ -22,79 +75,90 @@ const ShowCastInfo = ({ showCastInfo, onSave, onDelete }: Props) => { const toggle = () => setIsOpen((prev) => !prev); return ( - - - {showCastInfo.name} - { - e.preventDefault(); - dialog.open({ - title: '출연진 정보 편집', - isAuto: true, - content: ( - { - try { - await onSave(castInfo); - dialog.close(); - } catch { - return new Promise((_, reject) => reject('저장 중 오류가 발생하였습니다.')); - } - }} - prevShowCastInfo={showCastInfo} - onDelete={async () => { - try { - await onDelete?.(); - dialog.close(); - } catch { - return new Promise((_, reject) => reject('삭제 중 오류가 발생하였습니다.')); - } - }} - /> - ), - }); - }} - > - - 편집하기 - - - {memberLength > 0 && ( - <> - - {members.map((member) => ( - - {member.userImgPath ? ( - - ) : ( - - )} - {member.userNickname} - ({member.roleName}) - - ))} - - { - e.preventDefault(); - toggle(); - }} - > - {isOpen ? '팀원 리스트 접기' : '팀원 리스트 펼쳐보기'} - {isOpen ? : } - - - )} - + +
+
+ + + + + + + {showCastInfo.name} + + + { + e.preventDefault(); + dialog.open({ + title: '출연진 정보 편집', + isAuto: true, + content: ( + { + try { + await onSave(castInfo); + dialog.close(); + } catch { + return new Promise((_, reject) => reject('저장 중 오류가 발생하였습니다.')); + } + }} + prevShowCastInfo={showCastInfo} + onDelete={async () => { + try { + await onDelete?.(); + dialog.close(); + } catch { + return new Promise((_, reject) => reject('삭제 중 오류가 발생하였습니다.')); + } + }} + /> + ), + }); + }} + > + + 편집하기 + + + {memberLength > 0 && ( + <> + + {members.map((member) => ( + + {member.userImgPath ? ( + + ) : ( + + )} + {member.userNickname} + ({member.roleName}) + + ))} + + { + e.preventDefault(); + toggle(); + }} + > + {isOpen ? '팀원 리스트 접기' : '팀원 리스트 펼쳐보기'} + {isOpen ? : } + + + )} +
+
+
); }; diff --git a/apps/admin/src/components/ShowCastInfoFormDialogContent/index.tsx b/apps/admin/src/components/ShowCastInfoFormDialogContent/index.tsx index ace1e584..836e94c5 100644 --- a/apps/admin/src/components/ShowCastInfoFormDialogContent/index.tsx +++ b/apps/admin/src/components/ShowCastInfoFormDialogContent/index.tsx @@ -10,6 +10,7 @@ import { replaceUserCode } from '~/utils/replace'; export interface TempShowCastInfoFormInput { name: string; members?: Array>; + order?: number; } interface Props { @@ -163,8 +164,8 @@ const ShowCastInfoFormDialogContent = ({ onDelete, prevShowCastInfo, onSave }: P } catch { toast.error( '불티에 회원으로 등록된 식별 코드로만 등록이 가능합니다.' + - '\n' + - '식별 코드를 확인 후 다시 시도해 주세요.', + '\n' + + '식별 코드를 확인 후 다시 시도해 주세요.', ); } finally { setIsMemberFieldBlurred((prev) => { diff --git a/apps/admin/src/pages/ShowInfoPage/index.tsx b/apps/admin/src/pages/ShowInfoPage/index.tsx index e9196acd..bd2ee527 100644 --- a/apps/admin/src/pages/ShowInfoPage/index.tsx +++ b/apps/admin/src/pages/ShowInfoPage/index.tsx @@ -4,6 +4,7 @@ import { ShowImage, queryKeys, useCastTeamList, + useChangeCastTeamOrder, useDeleteCastTeams, useDeleteShow, useEditShowInfo, @@ -34,7 +35,7 @@ import { HostType } from '@boolti/api/src/types/host'; import ShowDetailUnauthorized from '~/components/ShowDetailUnauthorized'; import Portal from '@boolti/ui/src/components/Portal'; import ShowCastInfoFormContent from '~/components/ShowInfoFormContent/ShowCastInfoFormContent'; -import ShowCastInfo from '~/components/ShowCastInfo'; +import ShowCastInfo, { CastTeamListDraft } from '~/components/ShowCastInfo'; import { TempShowCastInfoFormInput } from '~/components/ShowCastInfoFormDialogContent'; import { useBodyScrollLock } from '~/hooks/useBodyScrollLock'; @@ -54,7 +55,9 @@ const ShowInfoPage = () => { const showId = Number(params!.showId); const { data: show } = useShowDetail(showId); const { data: showSalesInfo } = useShowSalesInfo(showId); - const { data: castTeamList } = useCastTeamList(showId); + const { data: castTeamList, refetch: refetchCastTeamList } = useCastTeamList(showId); + + const [castTeamListDraft, setCastTeamListDraft] = useState(null); const editShowInfoMutation = useEditShowInfo(); const uploadShowImageMutation = useUploadShowImage(); @@ -62,6 +65,7 @@ const ShowInfoPage = () => { const putCastTeams = usePutCastTeams(); const postCastTeams = usePostCastTeams(); const deleteCastTeams = useDeleteCastTeams(); + const changeCastTeamOrder = useChangeCastTeamOrder(); const toast = useToast(); const confirm = useConfirm(); @@ -134,6 +138,39 @@ const ShowInfoPage = () => { return true; }, [confirm, isImageFilesDirty, onSubmit, showInfoForm]); + const changeCastTeamIndex = useCallback((draggedItemId: number, targetIndex: number) => { + setCastTeamListDraft((prevDraft) => { + if (prevDraft === null) return prevDraft; + + const draggedItem = prevDraft.find(({ id }) => id === draggedItemId); + if (!draggedItem) return prevDraft; + + const nextDraft = [...prevDraft]; + + nextDraft.splice(nextDraft.indexOf(draggedItem), 1); + nextDraft.splice(targetIndex, 0, draggedItem); + + return nextDraft; + }) + }, []) + + const castTeamDropHoverHandler = useCallback((draggedItemId: number, hoverIndex: number) => { + changeCastTeamIndex(draggedItemId, hoverIndex); + }, [changeCastTeamIndex]); + + const castTeamDropHandler = useCallback(async () => { + if (!castTeamListDraft) return; + + await changeCastTeamOrder.mutateAsync({ + showId, + body: { + castTeamIds: castTeamListDraft.map(({ id }) => id), + }, + }); + + refetchCastTeamList(); + }, [castTeamListDraft, changeCastTeamOrder, refetchCastTeamList, showId]) + useEffect(() => { if (!show) return; @@ -154,6 +191,12 @@ const ShowInfoPage = () => { setShowImages(show.images); }, [show, showInfoForm]); + useEffect(() => { + if (!castTeamList) return; + + setCastTeamListDraft(castTeamList); + }, [castTeamList]) + useEffect(() => { setMiddleware(() => confirmSaveShowInfo); return () => { @@ -247,10 +290,11 @@ const ShowInfoPage = () => { ); }} /> - {castTeamList.map((info, index) => ( + {castTeamListDraft?.map((info, index) => ( { await putCastTeams.mutateAsync( { @@ -271,6 +315,8 @@ const ShowInfoPage = () => { }, ); }} + onDropHover={castTeamDropHoverHandler} + onDrop={castTeamDropHandler} onDelete={async () => { await deleteCastTeams.mutateAsync(info.id, { onSuccess: () => { diff --git a/packages/api/src/mutations/index.ts b/packages/api/src/mutations/index.ts index 9c1a8bc5..830ff458 100644 --- a/packages/api/src/mutations/index.ts +++ b/packages/api/src/mutations/index.ts @@ -41,6 +41,7 @@ import useSuperAdminEditSalesInfo from './useSuperAdminEditSalesInfo'; import usePutCastTeams from './usePutCastTeams'; import useDeleteCastTeams from './useDeleteCastTeams'; import usePostCastTeams from './usePostCastTeams'; +import useChangeCastTeamOrder from './useChangeCastTeamOrder'; export { usePostCastTeams, @@ -86,6 +87,7 @@ export { useSuperAdminCreateSalesTicket, useSuperAdminCreateInvitationTicket, useSuperAdminEditSalesInfo, + useChangeCastTeamOrder }; export type { ImageFile }; diff --git a/packages/api/src/mutations/useChangeCastTeamOrder.ts b/packages/api/src/mutations/useChangeCastTeamOrder.ts new file mode 100644 index 00000000..cd64e5ee --- /dev/null +++ b/packages/api/src/mutations/useChangeCastTeamOrder.ts @@ -0,0 +1,15 @@ +import { useMutation } from '@tanstack/react-query'; + +import { fetcher } from '../fetcher'; + +interface PostChangeCastTeamOrderRequest { + castTeamIds: number[]; +} + +const postChangeCastTeamOrder = (showId: number, body: PostChangeCastTeamOrderRequest) => + fetcher.post(`web/v1/shows/${showId}/cast-teams/change-sequence`, { json: body }); + +const useChangeCastTeamOrder = () => + useMutation(({ showId, body }: { showId: number, body: PostChangeCastTeamOrderRequest }) => postChangeCastTeamOrder(showId, body)); + +export default useChangeCastTeamOrder; diff --git a/packages/api/src/queries/useCastTeamList.ts b/packages/api/src/queries/useCastTeamList.ts index f130bd9f..0fdb6aee 100644 --- a/packages/api/src/queries/useCastTeamList.ts +++ b/packages/api/src/queries/useCastTeamList.ts @@ -1,7 +1,16 @@ import { useQuery } from '@tanstack/react-query'; import { queryKeys } from '../queryKey'; +import { ShowCastTeamReadResponse } from '../types'; -const useCastTeamList = (showId: number) => useQuery(queryKeys.castTeams.list(showId)); +const useCastTeamList = (showId: number) => useQuery({ + ...queryKeys.castTeams.list(showId), + select: (data: ShowCastTeamReadResponse[]) => { + return data.map((team, index) => ({ + ...team, + index + })); + } +}); export default useCastTeamList; diff --git a/packages/icon/src/components/Menu.tsx b/packages/icon/src/components/Menu.tsx index dfb928d1..1a42ff3a 100644 --- a/packages/icon/src/components/Menu.tsx +++ b/packages/icon/src/components/Menu.tsx @@ -3,21 +3,21 @@ export const Menu = () => { = 3.3.1" + "@types/node": ">= 12" + "@types/react": ">= 16" + react: ">= 16.14" + peerDependenciesMeta: + "@types/hoist-non-react-statics": + optional: true + "@types/node": + optional: true + "@types/react": + optional: true + checksum: 10c0/d069435750f0d6653cfa2b951cac8abb3583fb144ff134a20176608877d9c5964c63384ebbacaa0fdeef819b592a103de0d8e06f3b742311d64a029ffed0baa3 + languageName: node + linkType: hard + "react-docgen-typescript@npm:^2.2.2": version: 2.2.2 resolution: "react-docgen-typescript@npm:2.2.2" @@ -12075,6 +12143,15 @@ __metadata: languageName: node linkType: hard +"redux@npm:^4.2.0": + version: 4.2.1 + resolution: "redux@npm:4.2.1" + dependencies: + "@babel/runtime": "npm:^7.9.2" + checksum: 10c0/136d98b3d5dbed1cd6279c8c18a6a74c416db98b8a432a46836bdd668475de6279a2d4fd9d1363f63904e00f0678a8a3e7fa532c897163340baf1e71bb42c742 + languageName: node + linkType: hard + "regenerate-unicode-properties@npm:^10.1.0": version: 10.1.1 resolution: "regenerate-unicode-properties@npm:10.1.1" From faa362e0535b0a3c5e16c0a9bb6f8001bff9009a Mon Sep 17 00:00:00 2001 From: Shim MunSeong Date: Tue, 12 Nov 2024 22:22:07 +0900 Subject: [PATCH 14/62] =?UTF-8?q?fix:=20=ED=8F=AC=EC=8A=A4=ED=84=B0=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=EA=B0=80=20=EC=A0=95=EC=83=81?= =?UTF-8?q?=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EC=9D=B4=EB=A3=A8=EC=96=B4?= =?UTF-8?q?=EC=A7=80=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useDropzone에서 잘못 입력된 accept 입력 수정 --- .../ShowInfoFormContent/ShowBasicInfoFormContent.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/admin/src/components/ShowInfoFormContent/ShowBasicInfoFormContent.tsx b/apps/admin/src/components/ShowInfoFormContent/ShowBasicInfoFormContent.tsx index 1c03fa55..03f5653d 100644 --- a/apps/admin/src/components/ShowInfoFormContent/ShowBasicInfoFormContent.tsx +++ b/apps/admin/src/components/ShowInfoFormContent/ShowBasicInfoFormContent.tsx @@ -36,7 +36,8 @@ const ShowBasicInfoFormContent = ({ const { getRootProps, getInputProps } = useDropzone({ accept: { - 'image/jpeg, image/png': [], + 'image/jpeg': [], + 'image/png': [], }, maxFiles: MAX_IMAGE_COUNT, onDrop: onDropImage, From 6e654d57e61e94a25b123dd896f5b48ba151c51c Mon Sep 17 00:00:00 2001 From: Minsu Kim Date: Fri, 15 Nov 2024 22:45:35 +0900 Subject: [PATCH 15/62] =?UTF-8?q?feat:=20=EB=B0=A9=EB=AC=B8=EC=9E=90=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EB=B0=94=EB=80=90=20=EA=B2=83=20=EA=B0=84?= =?UTF-8?q?=EB=8B=A8=ED=95=98=EA=B2=8C=20=EC=9D=BC=EB=8B=A8=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EnteranceTable/EnteranceTable.styles.ts | 34 ++++++++++--------- .../src/components/EnteranceTable/index.tsx | 31 +++++++++-------- .../src/pages/ShowEnterancePage/index.tsx | 9 +++-- 3 files changed, 38 insertions(+), 36 deletions(-) diff --git a/apps/admin/src/components/EnteranceTable/EnteranceTable.styles.ts b/apps/admin/src/components/EnteranceTable/EnteranceTable.styles.ts index 796262a2..7c6b89c4 100644 --- a/apps/admin/src/components/EnteranceTable/EnteranceTable.styles.ts +++ b/apps/admin/src/components/EnteranceTable/EnteranceTable.styles.ts @@ -30,25 +30,23 @@ const HeaderItem = styled.span` margin-right: 12px; } &:nth-of-type(1) { - width: 108px; + width: 120px; } &:nth-of-type(2) { - width: 80px; + width: 100px; } &:nth-of-type(3) { - width: 180px; + width: 140px; } &:nth-of-type(4) { - width: 100px; + width: 80px; } &:nth-of-type(5) { width: 140px; } &:nth-of-type(6) { - width: 80px; - } - &:nth-of-type(7) { - width: 148px; + width: 180px; + flex: 1 0 auto; } `; @@ -73,25 +71,23 @@ const Item = styled.span` margin-right: 12px; } &:nth-of-type(1) { - width: 108px; + width: 120px; } &:nth-of-type(2) { - width: 80px; + width: 100px; } &:nth-of-type(3) { - width: 180px; + width: 140px; } &:nth-of-type(4) { - width: 100px; + width: 80px; } &:nth-of-type(5) { width: 140px; } &:nth-of-type(6) { - width: 80px; - } - &:nth-of-type(7) { - width: 148px; + width: 180px; + flex: 1 0 auto; } `; @@ -112,6 +108,11 @@ const ResetButton = styled(Button)` margin-top: 18px; `; +const DisabledText = styled.span` + ${({ theme }) => theme.typo.b2}; + color: ${({ theme }) => theme.palette.grey.g30}; +`; + export default { Container, HeaderItem, @@ -120,4 +121,5 @@ export default { Item, Empty, ResetButton, + DisabledText, }; diff --git a/apps/admin/src/components/EnteranceTable/index.tsx b/apps/admin/src/components/EnteranceTable/index.tsx index 8a134eda..de86f7c4 100644 --- a/apps/admin/src/components/EnteranceTable/index.tsx +++ b/apps/admin/src/components/EnteranceTable/index.tsx @@ -18,15 +18,8 @@ const columns = [ columnHelper.accessor('csTicketId', { header: '티켓 번호', }), - columnHelper.accessor('ticketType', { - header: '티켓 종류', - cell: (props) => `${props.getValue() === 'INVITE' ? '초청' : '일반'}티켓`, - }), - columnHelper.accessor('ticketName', { - header: '티켓 이름', - }), columnHelper.accessor('reservationName', { - header: '방문자 이름', + header: '방문자명', cell: (props) => { const { searchText = '' } = (props.table.options.meta ?? {}) as { searchText: string }; return ( @@ -47,13 +40,21 @@ const columns = [ ); }, }), - columnHelper.accessor('entered', { - header: '상태', - cell: (props) => (props.getValue() ? '입장 확인' : '미입장'), + columnHelper.accessor('ticketType', { + header: '티켓 종류', + cell: (props) => `${props.getValue() === 'INVITE' ? '초청' : '일반'}티켓`, + }), + columnHelper.accessor('ticketName', { + header: '티켓명', }), columnHelper.accessor('enteredAt', { - header: '입장 일시', - cell: (props) => (props.getValue() ? format(props.getValue(), 'yyyy/MM/dd HH:mm') : '-'), + header: '방문 일시', + cell: (props) => + props.getValue() ? ( + format(props.getValue(), 'yyyy/MM/dd HH:mm') + ) : ( + 아직 방문하지 않았습니다. + ), }), ]; @@ -97,9 +98,9 @@ const EnteranceTable = ({ searchText, data, isEnteredTicket, onClickReset }: Pro ) : isEnteredTicket ? ( - '입장 관객이 없어요.' + '아직 방문자가 없어요.' ) : ( - '미입장 관객이 없어요.' + '미방문자가 없어요.' )} ) : ( diff --git a/apps/admin/src/pages/ShowEnterancePage/index.tsx b/apps/admin/src/pages/ShowEnterancePage/index.tsx index 29edcbb5..99fe98b3 100644 --- a/apps/admin/src/pages/ShowEnterancePage/index.tsx +++ b/apps/admin/src/pages/ShowEnterancePage/index.tsx @@ -86,8 +86,7 @@ const ShowEnterancePage = () => { - 아직 판매한 티켓이 없어요.{'\n'} - 티켓을 판매하고 관객 입장을 관리해 보세요. + 아직 판매한 티켓이 없어요.{'\n'}티켓을 판매하고 방문자 명단을 관리해 보세요. ) : ( @@ -129,7 +128,7 @@ const ShowEnterancePage = () => { }} isSelected={!isEnteredTicket} > - 미입장 {notEnteredTicketCount} + 미방문자 {notEnteredTicketCount} { @@ -138,7 +137,7 @@ const ShowEnterancePage = () => { }} isSelected={isEnteredTicket} > - 입장 확인 {enteredTicketCount} + 방문자 {enteredTicketCount} @@ -188,7 +187,7 @@ const ShowEnterancePage = () => { status: reservation.entered ? reservation.enteredAt : '미방문', }))} searchText={debouncedSearchText} - emptyText={isEnteredTicket ? '입장 관객이 없어요.' : '미입장 관객이 없어요.'} + emptyText={isEnteredTicket ? '아직 방문자가 없어요.' : '미방문자가 없어요.'} onClickReset={onClickReset} /> From b7c9e6d20bbfd6803fa472bac23203b01a22d282 Mon Sep 17 00:00:00 2001 From: Minsu Kim Date: Fri, 15 Nov 2024 22:59:56 +0900 Subject: [PATCH 16/62] =?UTF-8?q?feat:=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EB=8F=99=EC=9E=91=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/admin/src/components/Pagination/index.tsx | 16 ++++++++++------ apps/admin/src/pages/ShowEnterancePage/index.tsx | 1 + .../src/pages/ShowReservationPage/index.tsx | 1 + 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/apps/admin/src/components/Pagination/index.tsx b/apps/admin/src/components/Pagination/index.tsx index 8822a283..9b559cdb 100644 --- a/apps/admin/src/components/Pagination/index.tsx +++ b/apps/admin/src/components/Pagination/index.tsx @@ -1,6 +1,8 @@ import { ChevronLeftIcon, ChevronRightIcon } from '@boolti/icon'; import Styled from './Pagination.styles'; +import { useDeviceWidth } from '~/hooks/useDeviceWidth'; +import { useTheme } from '@emotion/react'; interface Props { totalPages: number; @@ -8,18 +10,20 @@ interface Props { onClickPage?: (page: number) => void; } -const SIZE_PER_PAGE = 10; - const Pagination = ({ totalPages, currentPage, onClickPage }: Props) => { - const start = Math.floor(currentPage / SIZE_PER_PAGE) * SIZE_PER_PAGE; - const end = start + SIZE_PER_PAGE; + const deviceWidth = useDeviceWidth(); + const theme = useTheme(); + const isMobile = deviceWidth < parseInt(theme.breakpoint.mobile, 10); + const sizePerPage = isMobile ? 5 : 10; + const start = Math.floor(currentPage / sizePerPage) * sizePerPage; + const end = start + sizePerPage; const pages = Array.from({ length: totalPages }, (_, i) => i).slice(start, end); return ( { - onClickPage?.(Math.max(currentPage - SIZE_PER_PAGE, 0)); + onClickPage?.(Math.max(start - sizePerPage, 0)); }} > @@ -38,7 +42,7 @@ const Pagination = ({ totalPages, currentPage, onClickPage }: Props) => { { - onClickPage?.(Math.min(currentPage + SIZE_PER_PAGE, totalPages - 1)); + onClickPage?.(Math.min(end, totalPages - 1)); }} > diff --git a/apps/admin/src/pages/ShowEnterancePage/index.tsx b/apps/admin/src/pages/ShowEnterancePage/index.tsx index 99fe98b3..62937b33 100644 --- a/apps/admin/src/pages/ShowEnterancePage/index.tsx +++ b/apps/admin/src/pages/ShowEnterancePage/index.tsx @@ -194,6 +194,7 @@ const ShowEnterancePage = () => { )} {totalPages > 1 && ( { )} {totalPages > 1 && ( Date: Fri, 15 Nov 2024 23:02:05 +0900 Subject: [PATCH 17/62] =?UTF-8?q?fix:=20=ED=83=80=EC=9E=85=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/admin/src/pages/ShowReservationPage/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/admin/src/pages/ShowReservationPage/index.tsx b/apps/admin/src/pages/ShowReservationPage/index.tsx index 126f7090..e2dbd543 100644 --- a/apps/admin/src/pages/ShowReservationPage/index.tsx +++ b/apps/admin/src/pages/ShowReservationPage/index.tsx @@ -212,7 +212,6 @@ const ShowReservationPage = () => { )} {totalPages > 1 && ( Date: Fri, 15 Nov 2024 23:13:02 +0900 Subject: [PATCH 18/62] =?UTF-8?q?fix:=20=ED=83=80=EC=9E=85=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/admin/src/pages/ShowEnterancePage/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/admin/src/pages/ShowEnterancePage/index.tsx b/apps/admin/src/pages/ShowEnterancePage/index.tsx index 62937b33..99fe98b3 100644 --- a/apps/admin/src/pages/ShowEnterancePage/index.tsx +++ b/apps/admin/src/pages/ShowEnterancePage/index.tsx @@ -194,7 +194,6 @@ const ShowEnterancePage = () => { )} {totalPages > 1 && ( Date: Sat, 16 Nov 2024 10:40:50 +0900 Subject: [PATCH 19/62] =?UTF-8?q?fix:=20=EA=B3=B5=EC=97=B0=EC=9D=84=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=A0=20=EB=95=8C=20=EC=B6=9C=EC=97=B0?= =?UTF-8?q?=EC=A7=84=20=EC=A0=95=EB=B3=B4=20=EC=88=9C=EC=84=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EC=9D=B4=20=EC=98=AC=EB=B0=94=EB=A5=B4=EA=B2=8C=20?= =?UTF-8?q?=EB=8F=99=EC=9E=91=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/ShowCastInfo/index.tsx | 207 +++++++++--------- .../ShowCastInfoFormDialogContent/index.tsx | 7 +- apps/admin/src/hooks/useCastTeamListOrder.ts | 70 ++++++ apps/admin/src/pages/ShowAddPage/index.tsx | 42 ++-- apps/admin/src/pages/ShowInfoPage/index.tsx | 48 +--- packages/api/src/queries/useCastTeamList.ts | 11 +- 6 files changed, 203 insertions(+), 182 deletions(-) create mode 100644 apps/admin/src/hooks/useCastTeamListOrder.ts diff --git a/apps/admin/src/components/ShowCastInfo/index.tsx b/apps/admin/src/components/ShowCastInfo/index.tsx index 9ffcbd4e..2fabbe67 100644 --- a/apps/admin/src/components/ShowCastInfo/index.tsx +++ b/apps/admin/src/components/ShowCastInfo/index.tsx @@ -7,18 +7,13 @@ import { useRef, useState } from 'react'; import ShowCastInfoFormDialogContent, { TempShowCastInfoFormInput, } from '../ShowCastInfoFormDialogContent'; -import { ShowCastTeamReadResponse } from '@boolti/api'; - -export interface CastTeamListDraft extends ShowCastTeamReadResponse { - index: number; -} interface Props { - showCastInfo: CastTeamListDraft; + showCastInfo: TempShowCastInfoFormInput; index: number; onSave: (value: TempShowCastInfoFormInput) => Promise; onDropHover: (draggedItemId: number, hoverIndex: number) => void; - onDrop: () => void; + onDrop?: () => void; onDelete?: () => Promise; } @@ -29,12 +24,12 @@ interface DragItem { const ShowCastInfo = ({ showCastInfo, index, onSave, onDropHover, onDrop, onDelete }: Props) => { const ref = useRef(null) - const [{ isDragging }, drag, preview] = useDrag(() => ({ + const [{ isDragging }, drag, preview] = useDrag(() => ({ type: 'castTeam', previewOptions: { captureDraggingState: true, }, - item: { id: showCastInfo.id, index: showCastInfo.index }, + item: { id: showCastInfo.id, index }, collect: (monitor) => ({ isDragging: monitor.isDragging() }), @@ -42,28 +37,28 @@ const ShowCastInfo = ({ showCastInfo, index, onSave, onDropHover, onDrop, onDele const [, drop] = useDrop({ accept: 'castTeam', hover(item: DragItem, monitor) { - if (!ref.current) return - if (!monitor.canDrop()) return - if (item.id === showCastInfo.id) return + if (!ref.current) return; + if (!monitor.canDrop()) return; + if (item.id === showCastInfo.id) return; - const dragIndex = item.index - const hoverIndex = index + const dragIndex = item.index; + const hoverIndex = index; - const hoverBoundingRect = ref.current.getBoundingClientRect() - const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2 - const clientOffset = monitor.getClientOffset() - if (!clientOffset) return + const hoverBoundingRect = ref.current.getBoundingClientRect(); + const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2; + const clientOffset = monitor.getClientOffset(); + if (!clientOffset) return; - const hoverClientY = clientOffset.y - hoverBoundingRect.top - if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) return - if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) return + const hoverClientY = clientOffset.y - hoverBoundingRect.top; + if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) return; + if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) return; - item.index = hoverIndex + item.index = hoverIndex; - onDropHover(item.id, index) + onDropHover(item.id, index); }, drop() { - onDrop() + onDrop?.() } }) @@ -74,90 +69,88 @@ const ShowCastInfo = ({ showCastInfo, index, onSave, onDropHover, onDrop, onDele const toggle = () => setIsOpen((prev) => !prev); + preview(drop(ref)) + return ( - -
-
- - - - - - - {showCastInfo.name} - - - { - e.preventDefault(); - dialog.open({ - title: '출연진 정보 편집', - isAuto: true, - content: ( - { - try { - await onSave(castInfo); - dialog.close(); - } catch { - return new Promise((_, reject) => reject('저장 중 오류가 발생하였습니다.')); - } - }} - prevShowCastInfo={showCastInfo} - onDelete={async () => { - try { - await onDelete?.(); - dialog.close(); - } catch { - return new Promise((_, reject) => reject('삭제 중 오류가 발생하였습니다.')); - } - }} - /> - ), - }); - }} - > - - 편집하기 - - - {memberLength > 0 && ( - <> - - {members.map((member) => ( - - {member.userImgPath ? ( - - ) : ( - - )} - {member.userNickname} - ({member.roleName}) - - ))} - - { - e.preventDefault(); - toggle(); - }} - > - {isOpen ? '팀원 리스트 접기' : '팀원 리스트 펼쳐보기'} - {isOpen ? : } - - - )} -
-
+ + + + + + + + {showCastInfo.name} + + + { + e.preventDefault(); + dialog.open({ + title: '출연진 정보 편집', + isAuto: true, + content: ( + { + try { + await onSave(castInfo); + dialog.close(); + } catch { + return new Promise((_, reject) => reject('저장 중 오류가 발생하였습니다.')); + } + }} + prevShowCastInfo={showCastInfo} + onDelete={async () => { + try { + await onDelete?.(); + dialog.close(); + } catch { + return new Promise((_, reject) => reject('삭제 중 오류가 발생하였습니다.')); + } + }} + /> + ), + }); + }} + > + + 편집하기 + + + {memberLength > 0 && ( + <> + + {members.map((member) => ( + + {member.userImgPath ? ( + + ) : ( + + )} + {member.userNickname} + ({member.roleName}) + + ))} + + { + e.preventDefault(); + toggle(); + }} + > + {isOpen ? '팀원 리스트 접기' : '팀원 리스트 펼쳐보기'} + {isOpen ? : } + + + )} ); }; diff --git a/apps/admin/src/components/ShowCastInfoFormDialogContent/index.tsx b/apps/admin/src/components/ShowCastInfoFormDialogContent/index.tsx index 836e94c5..f2b168c3 100644 --- a/apps/admin/src/components/ShowCastInfoFormDialogContent/index.tsx +++ b/apps/admin/src/components/ShowCastInfoFormDialogContent/index.tsx @@ -8,9 +8,9 @@ import { Member, queryKeys, useQueryClient } from '@boolti/api'; import { replaceUserCode } from '~/utils/replace'; export interface TempShowCastInfoFormInput { + id: number; name: string; members?: Array>; - order?: number; } interface Props { @@ -19,7 +19,7 @@ interface Props { onSave: (value: TempShowCastInfoFormInput) => Promise; } -const ShowCastInfoFormDialogContent = ({ onDelete, prevShowCastInfo, onSave }: Props) => { +const ShowCastInfoFormDialogContent = ({ prevShowCastInfo, onDelete, onSave }: Props) => { const queryClient = useQueryClient(); const previousShowCastInfoMemberLength = prevShowCastInfo?.members?.length ?? 0; @@ -279,13 +279,14 @@ const ShowCastInfoFormDialogContent = ({ onDelete, prevShowCastInfo, onSave }: P onClick={async (e) => { e.preventDefault(); + const id = prevShowCastInfo?.id ?? -Math.floor(Math.random() * 1000000); const name = getValues('name'); const members = (getValues('members') ?? []).filter( (member) => member.userNickname && member.roleName && member.userCode, ); try { - await onSave({ name, members }); + await onSave({ id, name, members }); toast.success( onDelete ? '출연진 정보를 수정했습니다.' : '출연진 정보를 생성했습니다.', ); diff --git a/apps/admin/src/hooks/useCastTeamListOrder.ts b/apps/admin/src/hooks/useCastTeamListOrder.ts new file mode 100644 index 00000000..1d1f3af6 --- /dev/null +++ b/apps/admin/src/hooks/useCastTeamListOrder.ts @@ -0,0 +1,70 @@ +import { useChangeCastTeamOrder } from "@boolti/api"; +import { useCallback, useEffect, useState } from "react"; +import { TempShowCastInfoFormInput } from "~/components/ShowCastInfoFormDialogContent"; + +interface UseCastTeamListOrderParams { + showId?: number; + castTeamList?: TempShowCastInfoFormInput[]; + onChange?: () => void; +} + +const useCastTeamListOrder = (params?: UseCastTeamListOrderParams) => { + const showId = params?.showId; + const castTeamList = params?.castTeamList; + const onChange = params?.onChange; + + const [castTeamListDraft, setCastTeamListDraft] = useState([]); + + const changeCastTeamOrder = useChangeCastTeamOrder(); + + const changeCastTeamIndex = useCallback((draggedItemId: number, targetIndex: number) => { + setCastTeamListDraft((prevDraft) => { + if (!prevDraft) return prevDraft; + + const draggedItemIndex = prevDraft.findIndex(({ id }) => id === draggedItemId); + if (draggedItemIndex === -1 || targetIndex < 0 || targetIndex >= prevDraft.length) { + return prevDraft; + } + + const nextDraft = [...prevDraft]; + const [draggedItem] = nextDraft.splice(draggedItemIndex, 1); + nextDraft.splice(targetIndex, 0, draggedItem); + + return nextDraft; + }) + }, []) + + const castTeamDropHoverHandler = useCallback((draggedItemId: number, hoverIndex: number) => { + changeCastTeamIndex(draggedItemId, hoverIndex); + }, [changeCastTeamIndex]); + + const castTeamDropHandler = useCallback(async () => { + if (!castTeamListDraft) return; + + if (showId !== undefined) { + await changeCastTeamOrder.mutateAsync({ + showId, + body: { + castTeamIds: castTeamListDraft.map(({ id }) => id), + }, + }); + } + + onChange?.(); + }, [castTeamListDraft, changeCastTeamOrder, onChange, showId]) + + useEffect(() => { + if (!castTeamList) return; + + setCastTeamListDraft(castTeamList); + }, [castTeamList]) + + return { + castTeamListDraft, + setCastTeamListDraft, + castTeamDropHoverHandler, + castTeamDropHandler, + } +} + +export default useCastTeamListOrder diff --git a/apps/admin/src/pages/ShowAddPage/index.tsx b/apps/admin/src/pages/ShowAddPage/index.tsx index df7dc4ac..7c9ed9a5 100644 --- a/apps/admin/src/pages/ShowAddPage/index.tsx +++ b/apps/admin/src/pages/ShowAddPage/index.tsx @@ -27,6 +27,7 @@ import ShowCastInfoFormContent from '~/components/ShowInfoFormContent/ShowCastIn import ShowCastInfo from '~/components/ShowCastInfo'; import { TempShowCastInfoFormInput } from '~/components/ShowCastInfoFormDialogContent'; import { checkIsWebView } from '~/utils/webview'; +import useCastTeamListOrder from '~/hooks/useCastTeamListOrder'; interface ShowAddPageProps { step: 'info' | 'ticket'; @@ -42,10 +43,10 @@ const ShowAddPage = ({ step }: ShowAddPageProps) => { const showInfoForm = useForm(); const showTicketForm = useForm(); - const [showCastInfo, setShowCastInfo] = useState([]); const uploadShowImageMutation = useUploadShowImage(); const addShowMutation = useAddShow(); + const { castTeamListDraft, setCastTeamListDraft, castTeamDropHoverHandler, castTeamDropHandler } = useCastTeamListOrder(); const toast = useToast(); @@ -88,12 +89,11 @@ const ShowAddPage = ({ step }: ShowAddPageProps) => { ticketName: ticket.name, totalForSale: ticket.quantity, })), - castTeams: showCastInfo.map(({ name, members }) => ({ + castTeams: castTeamListDraft.map(({ name, members }) => ({ name, members: members - ?.filter(({ id, userCode, roleName }) => id && userCode && roleName) - .map(({ id, userCode, roleName }) => ({ - id, + ?.filter(({ userCode, roleName }) => userCode && roleName) + .map(({ userCode, roleName }) => ({ userCode, roleName, })), @@ -171,28 +171,31 @@ const ShowAddPage = ({ step }: ShowAddPageProps) => { { - setShowCastInfo((prev) => [...prev, showCastInfoFormInput]); + setCastTeamListDraft((prev) => [...prev, showCastInfoFormInput]); return new Promise((reslve) => reslve()); }} /> - {showCastInfo.map((info, index) => ( + {castTeamListDraft.map((info, index) => ( { - setShowCastInfo((prev) => + setCastTeamListDraft((prev) => prev.map((prevCastInfo, currentIndex) => index === currentIndex ? showCastInfoFormInput : prevCastInfo, - ), + ) ); return new Promise((reslve) => reslve()); }} onDelete={() => { - setShowCastInfo((prev) => - prev.filter((_, currentIndex) => index !== currentIndex), + setCastTeamListDraft((prev) => + prev.filter((_, currentIndex) => index !== currentIndex) ); return new Promise((reslve) => reslve()); }} + onDrop={castTeamDropHandler} + onDropHover={castTeamDropHoverHandler} /> ))} @@ -383,28 +386,31 @@ const ShowAddPage = ({ step }: ShowAddPageProps) => { { - setShowCastInfo((prev) => [...prev, showCastInfoFormInput]); + setCastTeamListDraft((prev) => [...prev, showCastInfoFormInput]); return new Promise((reslve) => reslve()); }} /> - {showCastInfo.map((info, index) => ( + {castTeamListDraft.map((info, index) => ( { - setShowCastInfo((prev) => + setCastTeamListDraft((prev) => prev.map((prevCastInfo, currentIndex) => index === currentIndex ? showCastInfoFormInput : prevCastInfo, - ), + ) ); return new Promise((reslve) => reslve()); }} onDelete={() => { - setShowCastInfo((prev) => - prev.filter((_, currentIndex) => index !== currentIndex), + setCastTeamListDraft((prev) => + prev.filter((_, currentIndex) => index !== currentIndex) ); return new Promise((reslve) => reslve()); }} + onDropHover={castTeamDropHoverHandler} + onDrop={castTeamDropHandler} /> ))} diff --git a/apps/admin/src/pages/ShowInfoPage/index.tsx b/apps/admin/src/pages/ShowInfoPage/index.tsx index bd2ee527..d110171f 100644 --- a/apps/admin/src/pages/ShowInfoPage/index.tsx +++ b/apps/admin/src/pages/ShowInfoPage/index.tsx @@ -4,7 +4,6 @@ import { ShowImage, queryKeys, useCastTeamList, - useChangeCastTeamOrder, useDeleteCastTeams, useDeleteShow, useEditShowInfo, @@ -35,9 +34,11 @@ import { HostType } from '@boolti/api/src/types/host'; import ShowDetailUnauthorized from '~/components/ShowDetailUnauthorized'; import Portal from '@boolti/ui/src/components/Portal'; import ShowCastInfoFormContent from '~/components/ShowInfoFormContent/ShowCastInfoFormContent'; -import ShowCastInfo, { CastTeamListDraft } from '~/components/ShowCastInfo'; +import ShowCastInfo from '~/components/ShowCastInfo'; import { TempShowCastInfoFormInput } from '~/components/ShowCastInfoFormDialogContent'; import { useBodyScrollLock } from '~/hooks/useBodyScrollLock'; +import useCastTeamListOrder from '~/hooks/useCastTeamListOrder'; + const ShowInfoPage = () => { const queryClient = useQueryClient(); @@ -56,8 +57,7 @@ const ShowInfoPage = () => { const { data: show } = useShowDetail(showId); const { data: showSalesInfo } = useShowSalesInfo(showId); const { data: castTeamList, refetch: refetchCastTeamList } = useCastTeamList(showId); - - const [castTeamListDraft, setCastTeamListDraft] = useState(null); + const { castTeamListDraft, castTeamDropHoverHandler, castTeamDropHandler } = useCastTeamListOrder({ showId, castTeamList, onChange: refetchCastTeamList }); const editShowInfoMutation = useEditShowInfo(); const uploadShowImageMutation = useUploadShowImage(); @@ -65,7 +65,6 @@ const ShowInfoPage = () => { const putCastTeams = usePutCastTeams(); const postCastTeams = usePostCastTeams(); const deleteCastTeams = useDeleteCastTeams(); - const changeCastTeamOrder = useChangeCastTeamOrder(); const toast = useToast(); const confirm = useConfirm(); @@ -138,39 +137,6 @@ const ShowInfoPage = () => { return true; }, [confirm, isImageFilesDirty, onSubmit, showInfoForm]); - const changeCastTeamIndex = useCallback((draggedItemId: number, targetIndex: number) => { - setCastTeamListDraft((prevDraft) => { - if (prevDraft === null) return prevDraft; - - const draggedItem = prevDraft.find(({ id }) => id === draggedItemId); - if (!draggedItem) return prevDraft; - - const nextDraft = [...prevDraft]; - - nextDraft.splice(nextDraft.indexOf(draggedItem), 1); - nextDraft.splice(targetIndex, 0, draggedItem); - - return nextDraft; - }) - }, []) - - const castTeamDropHoverHandler = useCallback((draggedItemId: number, hoverIndex: number) => { - changeCastTeamIndex(draggedItemId, hoverIndex); - }, [changeCastTeamIndex]); - - const castTeamDropHandler = useCallback(async () => { - if (!castTeamListDraft) return; - - await changeCastTeamOrder.mutateAsync({ - showId, - body: { - castTeamIds: castTeamListDraft.map(({ id }) => id), - }, - }); - - refetchCastTeamList(); - }, [castTeamListDraft, changeCastTeamOrder, refetchCastTeamList, showId]) - useEffect(() => { if (!show) return; @@ -191,12 +157,6 @@ const ShowInfoPage = () => { setShowImages(show.images); }, [show, showInfoForm]); - useEffect(() => { - if (!castTeamList) return; - - setCastTeamListDraft(castTeamList); - }, [castTeamList]) - useEffect(() => { setMiddleware(() => confirmSaveShowInfo); return () => { diff --git a/packages/api/src/queries/useCastTeamList.ts b/packages/api/src/queries/useCastTeamList.ts index 0fdb6aee..f130bd9f 100644 --- a/packages/api/src/queries/useCastTeamList.ts +++ b/packages/api/src/queries/useCastTeamList.ts @@ -1,16 +1,7 @@ import { useQuery } from '@tanstack/react-query'; import { queryKeys } from '../queryKey'; -import { ShowCastTeamReadResponse } from '../types'; -const useCastTeamList = (showId: number) => useQuery({ - ...queryKeys.castTeams.list(showId), - select: (data: ShowCastTeamReadResponse[]) => { - return data.map((team, index) => ({ - ...team, - index - })); - } -}); +const useCastTeamList = (showId: number) => useQuery(queryKeys.castTeams.list(showId)); export default useCastTeamList; From 3e7ea3bcab51279669ecc0b4d94bde73150f69fd Mon Sep 17 00:00:00 2001 From: Shim MunSeong Date: Sat, 16 Nov 2024 15:40:49 +0900 Subject: [PATCH 20/62] =?UTF-8?q?feat:=20=EC=B6=9C=EC=97=B0=EC=A7=84=20?= =?UTF-8?q?=ED=8C=80=20=EB=82=B4=20=ED=8C=80=EC=9B=90=20=EC=88=9C=EC=84=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pnp.cjs | 10 +- apps/admin/src/App.tsx | 6 +- .../ShowCastInfoFormDialogContent.styles.ts | 15 +- .../ShowCastInfoMemberRow.tsx | 175 +++++++++++++ .../ShowCastInfoFormDialogContent/index.tsx | 238 +++++++----------- apps/admin/src/pages/ShowInfoPage/index.tsx | 8 +- packages/api/src/types/cast.ts | 2 +- packages/ui/package.json | 2 + .../src/components/BooltiUIProvider/index.tsx | 20 +- yarn.lock | 2 + 10 files changed, 313 insertions(+), 165 deletions(-) create mode 100644 apps/admin/src/components/ShowCastInfoFormDialogContent/ShowCastInfoMemberRow.tsx diff --git a/.pnp.cjs b/.pnp.cjs index 94112257..10cf54e8 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -3005,6 +3005,8 @@ const RAW_RUNTIME_STATE = ["linkifyjs", "npm:4.1.3"],\ ["nanoid", "npm:5.0.4"],\ ["react", "npm:18.2.0"],\ + ["react-dnd", "virtual:9ef42ff9c873460955cc48cd9b15127324f3d1f83a4bea8e6327df0101bb993bef095b175f8d10a3f0d23ee47f702ca3ef7272cba815f708e8609d03d84b96a2#npm:16.0.1"],\ + ["react-dnd-html5-backend", "npm:16.0.1"],\ ["react-dom", "virtual:de80dc576383b2386358abc0e9fe49c00e3397fe355a0337462b73ab3115c2e557eb85784ee0fe776394cc11dd020b4e84dbbd75acf72ee6d54415d82d21f5c5#npm:18.2.0"],\ ["react-hot-toast", "virtual:9ef42ff9c873460955cc48cd9b15127324f3d1f83a4bea8e6327df0101bb993bef095b175f8d10a3f0d23ee47f702ca3ef7272cba815f708e8609d03d84b96a2#npm:2.4.1"],\ ["swiper", "npm:11.0.7"],\ @@ -8587,7 +8589,7 @@ const RAW_RUNTIME_STATE = ["qrcode.react", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:3.1.0"],\ ["react", "npm:18.2.0"],\ ["react-daum-postcode", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:3.1.3"],\ - ["react-dnd", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:16.0.1"],\ + ["react-dnd", "virtual:9ef42ff9c873460955cc48cd9b15127324f3d1f83a4bea8e6327df0101bb993bef095b175f8d10a3f0d23ee47f702ca3ef7272cba815f708e8609d03d84b96a2#npm:16.0.1"],\ ["react-dnd-html5-backend", "npm:16.0.1"],\ ["react-dom", "virtual:de80dc576383b2386358abc0e9fe49c00e3397fe355a0337462b73ab3115c2e557eb85784ee0fe776394cc11dd020b4e84dbbd75acf72ee6d54415d82d21f5c5#npm:18.2.0"],\ ["react-dropzone", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:14.2.3"],\ @@ -16283,10 +16285,10 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:16.0.1", {\ - "packageLocation": "./.yarn/__virtual__/react-dnd-virtual-4b292e52c3/0/cache/react-dnd-npm-16.0.1-974f047d7b-d069435750.zip/node_modules/react-dnd/",\ + ["virtual:9ef42ff9c873460955cc48cd9b15127324f3d1f83a4bea8e6327df0101bb993bef095b175f8d10a3f0d23ee47f702ca3ef7272cba815f708e8609d03d84b96a2#npm:16.0.1", {\ + "packageLocation": "./.yarn/__virtual__/react-dnd-virtual-42d9d58644/0/cache/react-dnd-npm-16.0.1-974f047d7b-d069435750.zip/node_modules/react-dnd/",\ "packageDependencies": [\ - ["react-dnd", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:16.0.1"],\ + ["react-dnd", "virtual:9ef42ff9c873460955cc48cd9b15127324f3d1f83a4bea8e6327df0101bb993bef095b175f8d10a3f0d23ee47f702ca3ef7272cba815f708e8609d03d84b96a2#npm:16.0.1"],\ ["@react-dnd/invariant", "npm:4.0.2"],\ ["@react-dnd/shallowequal", "npm:4.0.2"],\ ["@types/hoist-non-react-statics", null],\ diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx index 6e602774..2ed23ccf 100644 --- a/apps/admin/src/App.tsx +++ b/apps/admin/src/App.tsx @@ -3,8 +3,6 @@ import './index.css'; import { QueryClientProvider } from '@boolti/api'; import { BooltiUIProvider } from '@boolti/ui'; -import { DndProvider } from 'react-dnd' -import { HTML5Backend } from 'react-dnd-html5-backend' import { setDefaultOptions } from 'date-fns'; import { ko } from 'date-fns/locale'; import { @@ -157,9 +155,7 @@ const routes: RouteObject[] = [ - - - + diff --git a/apps/admin/src/components/ShowCastInfoFormDialogContent/ShowCastInfoFormDialogContent.styles.ts b/apps/admin/src/components/ShowCastInfoFormDialogContent/ShowCastInfoFormDialogContent.styles.ts index 1f95eee0..476f14d1 100644 --- a/apps/admin/src/components/ShowCastInfoFormDialogContent/ShowCastInfoFormDialogContent.styles.ts +++ b/apps/admin/src/components/ShowCastInfoFormDialogContent/ShowCastInfoFormDialogContent.styles.ts @@ -59,10 +59,20 @@ const InputWrapper = styled.div` &:focus-within { border-color: ${({ theme, isError }) => - isError ? theme.palette.status.error : theme.palette.grey.g70}; + isError ? theme.palette.status.error : theme.palette.grey.g70}; } `; +const Handle = styled.div` + display: inline-flex; + align-items: center; + justify-content: center; + color: ${({ theme }) => theme.palette.grey.g40}; + margin-top: 12px; + margin-right: 8px; + cursor: move; +` + const TextFieldWrap = styled.div` margin-bottom: 28px; @@ -91,6 +101,8 @@ const Row = styled.div` justify-content: center; align-items: flex-start; margin-bottom: 20px; + background-color: ${({ theme }) => theme.palette.grey.w}; + border-radius: 4px; `; const TrashCanButton = styled.button` @@ -174,6 +186,7 @@ const ErrorMessage = styled.span` export default { ShowInfoFormLabel, InputWrapper, + Handle, HashTag, Input, Row, diff --git a/apps/admin/src/components/ShowCastInfoFormDialogContent/ShowCastInfoMemberRow.tsx b/apps/admin/src/components/ShowCastInfoFormDialogContent/ShowCastInfoMemberRow.tsx new file mode 100644 index 00000000..b763b863 --- /dev/null +++ b/apps/admin/src/components/ShowCastInfoFormDialogContent/ShowCastInfoMemberRow.tsx @@ -0,0 +1,175 @@ +import { useDrag, useDrop } from "react-dnd"; +import { ClearIcon, MenuIcon, TrashIcon, UserIcon } from '@boolti/icon'; +import { Member } from '@boolti/api'; +import { replaceUserCode } from '~/utils/replace'; +import Styled from './ShowCastInfoFormDialogContent.styles'; +import { TempShowCastInfoFormInput } from "."; +import { Control, Controller } from "react-hook-form"; +import { useRef } from "react"; + +interface DragItem { + id: number + index: number +} + +interface ShowCastInfoMemberRowProps { + control: Control; + field: Partial & { id: number }; + index: number; + isFieldBlurred: { userCode: boolean; roleName: boolean }; + onSetUser: (userCode: string) => void; + onResetUser: () => void; + onBlurRoleName: () => void; + onDelete: () => void + onDropHover: (draggedItemId: number, hoverIndex: number) => void; + onDrop?: () => void; +} + +const ShowCastInfoMemberRow = ({ control, field, index, isFieldBlurred, onSetUser, onResetUser, onBlurRoleName, onDelete, onDropHover, onDrop }: ShowCastInfoMemberRowProps) => { + const ref = useRef(null) + const [{ isDragging }, drag, preview] = useDrag(() => ({ + type: 'castMember', + previewOptions: { + captureDraggingState: true, + }, + item: { id: field.id, index }, + collect: (monitor) => ({ + isDragging: monitor.isDragging() + }), + })) + const [, drop] = useDrop({ + accept: 'castMember', + hover(item: DragItem, monitor) { + if (!ref.current) return; + if (!monitor.canDrop()) return; + if (item.id === field.id) return; + + const dragIndex = item.index; + const hoverIndex = index; + + const hoverBoundingRect = ref.current.getBoundingClientRect(); + const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2; + const clientOffset = monitor.getClientOffset(); + if (!clientOffset) return; + + const hoverClientY = clientOffset.y - hoverBoundingRect.top; + if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) return; + if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) return; + + item.index = hoverIndex; + + onDropHover(item.id, index); + }, + drop() { + onDrop?.() + } + }) + + preview(drop(ref)) + + return ( + + + + + { + const value = field.userCode; + const isError = Boolean( + isFieldBlurred.userCode ? !value || !field.userNickname : false, + ); + return ( + + + {field.userNickname ? ( + <> + {field.userImgPath ? ( + + ) : ( + + )} + {field.userNickname} + { + onChange(undefined); + onResetUser(); + }} + > + + + + ) : ( + <> + # + { + const nextValue = replaceUserCode(e.target.value); + onChange(nextValue); + }} + onBlur={async (event) => { + onBlur(); + onSetUser(event.target.value); + }} + value={value ?? ''} + /> + + )} + + {isError && 필수 입력사항입니다.} + + ); + }} + name={`members.${index}.userCode`} + /> + { + const value = field.roleName; + const isError = isFieldBlurred.roleName && !value; + return ( + + + { + onBlur(); + onBlurRoleName(); + }} + value={value ?? ''} + /> + + {isError && 필수 입력사항입니다.} + + ); + }} + name={`members.${index}.roleName`} + /> + + + + + ); +} + +export default ShowCastInfoMemberRow diff --git a/apps/admin/src/components/ShowCastInfoFormDialogContent/index.tsx b/apps/admin/src/components/ShowCastInfoFormDialogContent/index.tsx index f2b168c3..e7dbf9ee 100644 --- a/apps/admin/src/components/ShowCastInfoFormDialogContent/index.tsx +++ b/apps/admin/src/components/ShowCastInfoFormDialogContent/index.tsx @@ -1,16 +1,16 @@ import { TextField, useConfirm, useToast } from '@boolti/ui'; import { Controller, useFieldArray, useForm } from 'react-hook-form'; import Styled from './ShowCastInfoFormDialogContent.styles'; -import { useState } from 'react'; +import { useCallback, useRef, useState } from 'react'; import { useBodyScrollLock } from '~/hooks/useBodyScrollLock'; -import { ClearIcon, PlusIcon, TrashIcon, UserIcon } from '@boolti/icon'; +import { PlusIcon } from '@boolti/icon'; import { Member, queryKeys, useQueryClient } from '@boolti/api'; -import { replaceUserCode } from '~/utils/replace'; +import ShowCastInfoMemberRow from './ShowCastInfoMemberRow'; export interface TempShowCastInfoFormInput { id: number; name: string; - members?: Array>; + members?: Array & { id: number }>; } interface Props { @@ -30,7 +30,7 @@ const ShowCastInfoFormDialogContent = ({ prevShowCastInfo, onDelete, onSave }: P const { control, getValues, watch, getFieldState } = useForm({ defaultValues, }); - const { fields, append, remove, update } = useFieldArray({ + const { fields, append, remove, update, replace } = useFieldArray({ control, name: 'members', keyName: '_id', @@ -42,6 +42,7 @@ const ShowCastInfoFormDialogContent = ({ prevShowCastInfo, onDelete, onSave }: P ...watchMemberFields[index], }; }); + const prevControlledFields = useRef(controlledFields); const toast = useToast(); const confirm = useConfirm(); @@ -67,6 +68,28 @@ const ShowCastInfoFormDialogContent = ({ prevShowCastInfo, onDelete, onSave }: P (!userNickname || !roleName), )); + const dragHoverHandler = useCallback((draggedItemId: number, hoverIndex: number) => { + const draggedItemIndex = controlledFields.findIndex(({ id }) => id === draggedItemId); + if (draggedItemIndex === -1 || hoverIndex < 0 || hoverIndex >= controlledFields.length) { + return; + } + + prevControlledFields.current = { ...controlledFields }; + + const nextFields = [...fields]; + const [draggedItem] = nextFields.splice(draggedItemIndex, 1); + nextFields.splice(hoverIndex, 0, draggedItem); + + replace(nextFields); + + setIsMemberFieldBlurred((prev) => { + const nextMemberFieldBlurred = [...prev]; + nextMemberFieldBlurred.splice(draggedItemIndex, 1); + nextMemberFieldBlurred.splice(hoverIndex, 0, prev[draggedItemIndex]); + return nextMemberFieldBlurred; + }) + }, [fields, controlledFields, replace]) + return ( <> 팀명 @@ -97,151 +120,76 @@ const ShowCastInfoFormDialogContent = ({ prevShowCastInfo, onDelete, onSave }: P 팀원 {controlledFields.map((field, index) => ( - - { - const value = field.userCode; - const isError = Boolean( - isMemberFieldBlurred[index].userCode ? !value || !field.userNickname : false, - ); - return ( - - - {field.userNickname ? ( - <> - {field.userImgPath ? ( - - ) : ( - - )} - {field.userNickname} - { - onChange(undefined); - setIsMemberFieldBlurred((prev) => { - const nextMemberFieldBlurred = [...prev]; - nextMemberFieldBlurred[index].userCode = true; - return nextMemberFieldBlurred; - }); - update(index, { - roleName: field.roleName, - }); - }} - > - - - - ) : ( - <> - # - { - const nextValue = replaceUserCode(e.target.value); - onChange(nextValue); - }} - onBlur={async (event) => { - onBlur(); - const userCode = event.target.value; - if (userCode !== '') { - try { - const { imgPath, nickname } = await queryClient.fetchQuery( - queryKeys.user.userCode(event.target.value), - ); - update(index, { - ...controlledFields[index], - userImgPath: imgPath, - userNickname: nickname, - }); - } catch { - toast.error( - '불티에 회원으로 등록된 식별 코드로만 등록이 가능합니다.' + - '\n' + - '식별 코드를 확인 후 다시 시도해 주세요.', - ); - } finally { - setIsMemberFieldBlurred((prev) => { - const nextMemberFieldBlurred = [...prev]; - nextMemberFieldBlurred[index].userCode = true; - return nextMemberFieldBlurred; - }); - } - } - }} - value={value ?? ''} - /> - - )} - - {isError && 필수 입력사항입니다.} - - ); - }} - name={`members.${index}.userCode`} - /> - { - const value = field.roleName; - const isError = isMemberFieldBlurred[index].roleName && !value; - return ( - - - { - onBlur(); - setIsMemberFieldBlurred((prev) => { - const nextMemberFieldBlurred = [...prev]; - nextMemberFieldBlurred[index].roleName = true; - return nextMemberFieldBlurred; - }); - }} - value={value ?? ''} - /> - - {isError && 필수 입력사항입니다.} - - ); - }} - name={`members.${index}.roleName`} - /> - { - const isConfirm = await confirm('팀원 정보를 삭제하시겠어요?', { - confirm: '삭제하기', - cancel: '취소하기', - }); - - if (isConfirm) { - toast.success('팀원 정보를 삭제했습니다.'); - setIsMemberFieldBlurred((prev) => - prev.filter((_, blurredIndex) => blurredIndex !== index), + { + if (userCode !== '') { + try { + const { imgPath, nickname } = await queryClient.fetchQuery( + queryKeys.user.userCode(userCode), + ); + update(index, { + ...controlledFields[index], + userImgPath: imgPath, + userNickname: nickname, + }); + } catch { + toast.error( + '불티에 회원으로 등록된 식별 코드로만 등록이 가능합니다.' + + '\n' + + '식별 코드를 확인 후 다시 시도해 주세요.', ); - remove(index); + } finally { + setIsMemberFieldBlurred((prev) => { + const nextMemberFieldBlurred = [...prev]; + nextMemberFieldBlurred[index].userCode = true; + return nextMemberFieldBlurred; + }); } - }} - > - - - + } + }} + onResetUser={() => { + setIsMemberFieldBlurred((prev) => { + const nextMemberFieldBlurred = [...prev]; + nextMemberFieldBlurred[index].userCode = true; + return nextMemberFieldBlurred; + }); + update(index, { + id: field.id, + roleName: field.roleName, + }); + }} + onBlurRoleName={() => { + setIsMemberFieldBlurred((prev) => { + const nextMemberFieldBlurred = [...prev]; + nextMemberFieldBlurred[index].roleName = true; + return nextMemberFieldBlurred; + }); + }} + onDelete={async () => { + const isConfirm = await confirm('팀원 정보를 삭제하시겠어요?', { + confirm: '삭제하기', + cancel: '취소하기', + }); + + if (isConfirm) { + toast.success('팀원 정보를 삭제했습니다.'); + setIsMemberFieldBlurred((prev) => + prev.filter((_, blurredIndex) => blurredIndex !== index), + ); + remove(index); + } + }} + onDropHover={dragHoverHandler} + /> ))} { - append({}); + append({ id: -Math.floor(Math.random() * 1000000) }); setIsMemberFieldBlurred((prev) => [...prev, { userCode: false, roleName: false }]); }} > diff --git a/apps/admin/src/pages/ShowInfoPage/index.tsx b/apps/admin/src/pages/ShowInfoPage/index.tsx index d110171f..38b09c91 100644 --- a/apps/admin/src/pages/ShowInfoPage/index.tsx +++ b/apps/admin/src/pages/ShowInfoPage/index.tsx @@ -237,7 +237,7 @@ const ShowInfoPage = () => { members: members ?.filter(({ userCode, roleName }) => userCode && roleName) .map(({ id, userCode, roleName }) => ({ - id, + id: id < 0 ? undefined : id, userCode, roleName, })) as ShowCastTeamCreateOrUpdateRequest['members'], @@ -256,13 +256,15 @@ const ShowInfoPage = () => { showCastInfo={info} index={index} onSave={async ({ name, members }: TempShowCastInfoFormInput) => { + if (info.id === undefined) return; + await putCastTeams.mutateAsync( { name, members: members ?.filter(({ userCode, roleName }) => userCode && roleName) .map(({ id, userCode, roleName }) => ({ - id, + id: id < 0 ? undefined : id, userCode, roleName, })) as ShowCastTeamCreateOrUpdateRequest['members'], @@ -278,6 +280,8 @@ const ShowInfoPage = () => { onDropHover={castTeamDropHoverHandler} onDrop={castTeamDropHandler} onDelete={async () => { + if (info.id === undefined) return; + await deleteCastTeams.mutateAsync(info.id, { onSuccess: () => { queryClient.invalidateQueries(queryKeys.castTeams.list(showId)); diff --git a/packages/api/src/types/cast.ts b/packages/api/src/types/cast.ts index 73ea7376..4d49ff2e 100644 --- a/packages/api/src/types/cast.ts +++ b/packages/api/src/types/cast.ts @@ -1,6 +1,6 @@ export interface Member { /** 공연 출연진 팀원 ID */ - id?: number; + id: number; /** 유저 식별 코드 */ userCode: string; /** 역할 이름 (1~100자. 빈 문자열 불가) */ diff --git a/packages/ui/package.json b/packages/ui/package.json index 8a1d0a4e..09f781d6 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -18,6 +18,8 @@ "linkifyjs": "^4.1.3", "nanoid": "^5.0.4", "react": "^18.2.0", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", "react-hot-toast": "^2.4.1", "swiper": "^11.0.7" diff --git a/packages/ui/src/components/BooltiUIProvider/index.tsx b/packages/ui/src/components/BooltiUIProvider/index.tsx index 5f93e23c..84c89b1a 100644 --- a/packages/ui/src/components/BooltiUIProvider/index.tsx +++ b/packages/ui/src/components/BooltiUIProvider/index.tsx @@ -1,9 +1,11 @@ +import { DndProvider } from 'react-dnd'; import '../../index.css'; import AlertProvider from '../AlertProvider'; import ConfirmProvider from '../ConfirmProvider'; import DialogProvider from '../DialogProvider'; import ThemeProvider from '../ThemeProvider'; +import { HTML5Backend } from 'react-dnd-html5-backend'; interface BooltiUIProviderProps { children: React.ReactNode; @@ -11,13 +13,17 @@ interface BooltiUIProviderProps { const BooltiUIProvider = ({ children }: BooltiUIProviderProps) => { return ( - - - - {children} - - - + + + + + + {children} + + + + + ); }; diff --git a/yarn.lock b/yarn.lock index 719cc4fe..e0d8dcc5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1731,6 +1731,8 @@ __metadata: linkifyjs: "npm:^4.1.3" nanoid: "npm:^5.0.4" react: "npm:^18.2.0" + react-dnd: "npm:^16.0.1" + react-dnd-html5-backend: "npm:^16.0.1" react-dom: "npm:^18.2.0" react-hot-toast: "npm:^2.4.1" swiper: "npm:^11.0.7" From 9a1acb4956e3027635e68f8e10d9676fba8e81de Mon Sep 17 00:00:00 2001 From: Minsu Kim Date: Sun, 17 Nov 2024 00:36:49 +0900 Subject: [PATCH 21/62] =?UTF-8?q?feat:=20=EB=B0=A9=EB=AC=B8=EC=9E=90=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EA=B0=9C=EC=84=A0=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20API=20=EA=B4=80=EB=A0=A8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/api/src/queries/index.ts | 4 ++ .../src/queries/useAdminSalesTicketList.ts | 8 ++++ .../api/src/queries/useAdminTicketList.ts | 20 ++++++++ packages/api/src/queryKey.ts | 17 +++++++ packages/api/src/types/adminTicket.ts | 46 +++++++++++++++++++ 5 files changed, 95 insertions(+) create mode 100644 packages/api/src/queries/useAdminSalesTicketList.ts create mode 100644 packages/api/src/queries/useAdminTicketList.ts diff --git a/packages/api/src/queries/index.ts b/packages/api/src/queries/index.ts index bc59a69f..31dee323 100644 --- a/packages/api/src/queries/index.ts +++ b/packages/api/src/queries/index.ts @@ -37,11 +37,15 @@ import useSuperAdminInvitationCodeList from './useSuperAdminInvitationCodeList'; import useUserByUserCode from './useUserByUserCode'; import useShowReservationWithTickets from './useShowReservationWithTickets'; import useCastTeamList from './useCastTeamList'; +import useAdminTicketList from './useAdminTicketList'; +import useAdminSalesTicketList from './useAdminSalesTicketList'; export { useCastTeamList, useUserByUserCode, useAdminSettlementEvent, + useAdminTicketList, + useAdminSalesTicketList, useAdminSettlementInfo, useGift, useShowReservationWithTickets, diff --git a/packages/api/src/queries/useAdminSalesTicketList.ts b/packages/api/src/queries/useAdminSalesTicketList.ts new file mode 100644 index 00000000..fba7b7f6 --- /dev/null +++ b/packages/api/src/queries/useAdminSalesTicketList.ts @@ -0,0 +1,8 @@ +import { useQuery } from '@tanstack/react-query'; + +import { queryKeys } from '../queryKey'; + +const useAdminSalesTicketList = (showId: number) => + useQuery(queryKeys.adminTicket.salesTicketList(showId)); + +export default useAdminSalesTicketList; diff --git a/packages/api/src/queries/useAdminTicketList.ts b/packages/api/src/queries/useAdminTicketList.ts new file mode 100644 index 00000000..773e0199 --- /dev/null +++ b/packages/api/src/queries/useAdminTicketList.ts @@ -0,0 +1,20 @@ +import { useQuery } from '@tanstack/react-query'; + +import { queryKeys } from '../queryKey'; + +const useAdminTicketList = ( + showId: number, + reservationNameOrPhoneNumber: string, + salesTicketTypeId: string[], + isUsed?: boolean, +) => + useQuery( + queryKeys.adminTicket.ticketList( + showId, + reservationNameOrPhoneNumber, + salesTicketTypeId, + isUsed, + ), + ); + +export default useAdminTicketList; diff --git a/packages/api/src/queryKey.ts b/packages/api/src/queryKey.ts index a953aa44..1d6fd35d 100644 --- a/packages/api/src/queryKey.ts +++ b/packages/api/src/queryKey.ts @@ -9,6 +9,7 @@ import { PageReservationResponse, PageReservationWithTicketsResponse, ReservationSummaryResponse, + SalesTicketTypeResponseV2, SettlementBannersResponse, ShowCastTeamReadResponse, ShowInvitationCodeListResponse, @@ -50,6 +51,7 @@ import { } from './types/adminReservation'; import { AdminTicketSalesInfoResponse, + PageTicketWithReservationResponse, SuperAdminInvitationCodeListResponse, SuperAdminInvitationTicketListResponse, SuperAdminSalesTicketListResponse, @@ -241,6 +243,21 @@ export const adminTicketQueryKeys = createQueryKeys('adminTicket', { `sa-api/v1/invitation-tickets/${ticketId}/invitation-codes`, ), }), + salesTicketList: (showId: number) => ({ + queryKey: [showId], + queryFn: () => + fetcher.get(`/web/v1/host/shows/${showId}/sales-tickets`), + }), + ticketList: ( + showId: number, + reservationNameOrPhoneNumber: string, + salesTicketTypeId: string[], + isUsed?: boolean, + ) => ({ + queryKey: [showId, reservationNameOrPhoneNumber, salesTicketTypeId, isUsed], + queryFn: () => + fetcher.get(`/web/v1/shows/${showId}/tickets`), + }), }); export const showQueryKeys = createQueryKeys('show', { diff --git a/packages/api/src/types/adminTicket.ts b/packages/api/src/types/adminTicket.ts index cc78ad38..f8b40b36 100644 --- a/packages/api/src/types/adminTicket.ts +++ b/packages/api/src/types/adminTicket.ts @@ -1,3 +1,5 @@ +import { PageResponse, TicketType } from './common'; + export interface AdminTicketSalesInfoResponse { /** 공연 ID */ showId: number; @@ -66,3 +68,47 @@ export interface SuperAdminEditSalesInfoRequest { salesEndTime: string; ticketNotice: string; } + +export interface SalesTicketTypeResponseV2 { + /** 판매 티켓 ID */ + id: number; + /** 판매 티켓 타입 */ + ticketType: TicketType; + /** 티켓 이름 */ + ticketName: string; + /** 티켓 가격(장당) */ + price: number; +} + +export interface ReservationHolderDetailResponse { + /** 예매자 이름 */ + name: string; + /** 예매자 전화번호 */ + phoneNumber: string; +} + +export interface ReservationResponseV2 { + /** 예매 ID */ + id: number; + /** CS용 예매 ID */ + csReservationId: number; + /** 예매자 정보 */ + reservationHolder: ReservationHolderDetailResponse; +} + +export interface TicketWithReservationResponse { + /** 티켓 ID */ + id: number; + /** CS용 티켓 ID */ + csTicketId: string; + /** 예매 정보 */ + reservation: ReservationResponseV2; + /** 판매 티켓 타입 정보 */ + salesTicketType: SalesTicketTypeResponseV2; + /** 입장 일시 */ + usedAt?: string; + /** 티켓 생성 일시 */ + createdAt: string; +} + +export type PageTicketWithReservationResponse = PageResponse; From 5c1c5fc695477192d9e8cca5bb08d14cf7911cfb Mon Sep 17 00:00:00 2001 From: Minsu Kim Date: Sun, 17 Nov 2024 00:54:56 +0900 Subject: [PATCH 22/62] =?UTF-8?q?feat:=20=ED=85=8C=EC=9D=B4=EB=B8=94?= =?UTF-8?q?=EC=97=90=20=EB=B0=94=EB=80=90=20API=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/EnteranceTable/index.tsx | 28 ++++--- .../src/pages/ShowEnterancePage/index.tsx | 79 +++++++++++-------- packages/api/src/queryKey.ts | 5 +- 3 files changed, 65 insertions(+), 47 deletions(-) diff --git a/apps/admin/src/components/EnteranceTable/index.tsx b/apps/admin/src/components/EnteranceTable/index.tsx index de86f7c4..ee588af2 100644 --- a/apps/admin/src/components/EnteranceTable/index.tsx +++ b/apps/admin/src/components/EnteranceTable/index.tsx @@ -1,4 +1,3 @@ -import { EntranceResponse } from '@boolti/api'; import { createColumnHelper, flexRender, @@ -11,14 +10,15 @@ import { boldText } from '~/utils/boldText'; import { formatPhoneNumber } from '~/utils/format'; import Styled from './EnteranceTable.styles'; +import { TicketWithReservationResponse } from '@boolti/api/src/types/adminTicket'; -const columnHelper = createColumnHelper(); +const columnHelper = createColumnHelper(); const columns = [ columnHelper.accessor('csTicketId', { header: '티켓 번호', }), - columnHelper.accessor('reservationName', { + columnHelper.accessor('reservation.reservationHolder.name', { header: '방문자명', cell: (props) => { const { searchText = '' } = (props.table.options.meta ?? {}) as { searchText: string }; @@ -27,7 +27,7 @@ const columns = [ ); }, }), - columnHelper.accessor('reservationPhoneNumber', { + columnHelper.accessor('reservation.reservationHolder.phoneNumber', { header: '연락처', cell: (props) => { const { searchText = '' } = (props.table.options.meta ?? {}) as { searchText: string }; @@ -40,27 +40,29 @@ const columns = [ ); }, }), - columnHelper.accessor('ticketType', { + columnHelper.accessor('salesTicketType.ticketType', { header: '티켓 종류', cell: (props) => `${props.getValue() === 'INVITE' ? '초청' : '일반'}티켓`, }), - columnHelper.accessor('ticketName', { + columnHelper.accessor('salesTicketType.ticketName', { header: '티켓명', }), - columnHelper.accessor('enteredAt', { + columnHelper.accessor('usedAt', { header: '방문 일시', - cell: (props) => - props.getValue() ? ( - format(props.getValue(), 'yyyy/MM/dd HH:mm') + cell: (props) => { + const value = props.getValue(); + return value ? ( + format(value, 'yyyy/MM/dd HH:mm') ) : ( 아직 방문하지 않았습니다. - ), + ); + }, }), ]; interface Props { - data: EntranceResponse[]; - isEnteredTicket: boolean; + data: TicketWithReservationResponse[]; + isEnteredTicket?: boolean; searchText: string; onClickReset?: VoidFunction; } diff --git a/apps/admin/src/pages/ShowEnterancePage/index.tsx b/apps/admin/src/pages/ShowEnterancePage/index.tsx index 99fe98b3..8fe0f02d 100644 --- a/apps/admin/src/pages/ShowEnterancePage/index.tsx +++ b/apps/admin/src/pages/ShowEnterancePage/index.tsx @@ -1,7 +1,8 @@ import { + useAdminSalesTicketList, + useAdminTicketList, useShowDetail, useShowEnteranceInfo, - useShowEnterances, useShowEnteranceSummary, } from '@boolti/api'; import { ClearIcon, SearchIcon } from '@boolti/icon'; @@ -20,6 +21,8 @@ import { useDeviceWidth } from '~/hooks/useDeviceWidth'; import { useTheme } from '@emotion/react'; import { BooltiGreyIcon } from '@boolti/icon/src/components/BooltiGreyIcon'; +type TicketType = 'ALL' | 'USED' | 'UNUSED'; + const ShowEnterancePage = () => { const params = useParams<{ showId: string }>(); const { open, close } = useDialog(); @@ -27,7 +30,7 @@ const ShowEnterancePage = () => { const [selectedTicketType, setSelectedTicketType] = useState< React.ComponentProps['value'] >({ value: 'ALL', label: '티켓 전체' }); - const [isEnteredTicket, setIsEnteredTicket] = useState(false); + const [enteranceTicketType, setEnteranceTicetType] = useState('ALL'); const [searchText, setSearchText] = useState(''); const [debouncedSearchText, setDebouncedSearchText] = useState(''); const [currentPage, setCurrentPage] = useState(0); @@ -36,25 +39,30 @@ const ShowEnterancePage = () => { const { data: show } = useShowDetail(showId); const { data: entranceSummary } = useShowEnteranceSummary(showId); const { data: enteranceInfo } = useShowEnteranceInfo(showId); - const { data: enteranceData, isLoading: isEntranceListLoading } = useShowEnterances( + + const useTicketUsedFilter = + enteranceTicketType === 'ALL' ? undefined : enteranceTicketType === 'USED'; + const { data: salesTicketList } = useAdminSalesTicketList(showId); + const { data: ticketList, isLoading: isTicketListLoading } = useAdminTicketList( showId, - currentPage, - isEnteredTicket, - selectedTicketType.value === 'ALL' ? undefined : selectedTicketType.value, - debouncedSearchText, + searchText, + [], + useTicketUsedFilter, ); const deviceWidth = useDeviceWidth(); const theme = useTheme(); const isMobile = deviceWidth < parseInt(theme.breakpoint.mobile, 10); - const totalPages = enteranceData?.totalPages ?? 0; - const reservations = (enteranceData?.content ?? []).filter( - ({ entered, ticketType }) => - entered === isEnteredTicket && - (selectedTicketType.value === 'ALL' || ticketType === selectedTicketType.value), + const totalPages = ticketList?.totalPages ?? 0; + const tickets = (ticketList?.content ?? []).filter( + ({ usedAt, salesTicketType }) => + useTicketUsedFilter === undefined || + (!!usedAt === useTicketUsedFilter && + (selectedTicketType.value === 'ALL' || + salesTicketType.ticketType === selectedTicketType.value)), ); - + console.log(ticketList); const onClickReset = () => { setSelectedTicketType({ value: 'ALL', label: '티켓 전체' }); setSearchText(''); @@ -69,9 +77,9 @@ const ShowEnterancePage = () => { useEffect(() => { setCurrentPage(0); - }, [selectedTicketType, isEnteredTicket, debouncedSearchText]); + }, [selectedTicketType, useTicketUsedFilter, debouncedSearchText]); - if (!show || !entranceSummary || !enteranceInfo) return null; + if (!show || !entranceSummary || !enteranceInfo || !ticketList) return null; const { totalTicketCount = 0, @@ -123,19 +131,28 @@ const ShowEnterancePage = () => { { - setIsEnteredTicket(false); + setEnteranceTicetType('ALL'); + onClickReset(); + }} + isSelected={enteranceTicketType === 'ALL'} + > + 전체 + + { + setEnteranceTicetType('UNUSED'); onClickReset(); }} - isSelected={!isEnteredTicket} + isSelected={enteranceTicketType === 'UNUSED'} > 미방문자 {notEnteredTicketCount} { - setIsEnteredTicket(true); + setEnteranceTicetType('USED'); onClickReset(); }} - isSelected={isEnteredTicket} + isSelected={enteranceTicketType === 'USED'} > 방문자 {enteredTicketCount} @@ -166,28 +183,28 @@ const ShowEnterancePage = () => {
- {!isEntranceListLoading && ( + {!isTicketListLoading && ( <> ({ - id: reservation.ticketId, - name: reservation.reservationName, - date: reservation.enteredAt, - phoneNumber: reservation.reservationPhoneNumber, - ticketName: reservation.ticketName, - type: reservation.entered ? 'NORMAL' : 'DISABLED', - status: reservation.entered ? reservation.enteredAt : '미방문', + items={tickets.map((ticket) => ({ + id: ticket.id, + name: ticket.reservation.reservationHolder.name, + date: ticket.usedAt, + phoneNumber: ticket.reservation.reservationHolder.phoneNumber, + ticketName: ticket.salesTicketType.ticketName, + type: ticket?.usedAt ? 'NORMAL' : 'DISABLED', + status: ticket?.usedAt ? ticket.usedAt : '미방문', }))} searchText={debouncedSearchText} - emptyText={isEnteredTicket ? '아직 방문자가 없어요.' : '미방문자가 없어요.'} + emptyText={useTicketUsedFilter ? '아직 방문자가 없어요.' : '미방문자가 없어요.'} onClickReset={onClickReset} /> diff --git a/packages/api/src/queryKey.ts b/packages/api/src/queryKey.ts index 1d6fd35d..a41ea8e7 100644 --- a/packages/api/src/queryKey.ts +++ b/packages/api/src/queryKey.ts @@ -246,7 +246,7 @@ export const adminTicketQueryKeys = createQueryKeys('adminTicket', { salesTicketList: (showId: number) => ({ queryKey: [showId], queryFn: () => - fetcher.get(`/web/v1/host/shows/${showId}/sales-tickets`), + fetcher.get(`web/v1/host/shows/${showId}/sales-tickets`), }), ticketList: ( showId: number, @@ -255,8 +255,7 @@ export const adminTicketQueryKeys = createQueryKeys('adminTicket', { isUsed?: boolean, ) => ({ queryKey: [showId, reservationNameOrPhoneNumber, salesTicketTypeId, isUsed], - queryFn: () => - fetcher.get(`/web/v1/shows/${showId}/tickets`), + queryFn: () => fetcher.get(`web/v1/shows/${showId}/tickets`), }), }); From 8e9031dd77fb5b31f03ea397d683be40e9c9c54f Mon Sep 17 00:00:00 2001 From: Minsu Kim Date: Sun, 17 Nov 2024 02:01:57 +0900 Subject: [PATCH 23/62] =?UTF-8?q?feat:=20=EB=8D=B0=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=ED=83=91=EC=97=90=EC=84=9C=20=ED=95=84=ED=84=B0=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20=EA=B0=80=EB=8A=A5=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TicketNameFilter.styles.ts | 92 +++++++++++++++++++ .../src/components/TicketNameFilter/index.tsx | 90 ++++++++++++++++++ .../src/pages/ShowEnterancePage/index.tsx | 35 +++---- packages/icon/src/components/Filter.tsx | 54 +++++++++++ .../icon/src/components/SquareCheckIcon.tsx | 22 +++++ packages/icon/src/components/index.ts | 4 + 6 files changed, 280 insertions(+), 17 deletions(-) create mode 100644 apps/admin/src/components/TicketNameFilter/TicketNameFilter.styles.ts create mode 100644 apps/admin/src/components/TicketNameFilter/index.tsx create mode 100644 packages/icon/src/components/Filter.tsx create mode 100644 packages/icon/src/components/SquareCheckIcon.tsx diff --git a/apps/admin/src/components/TicketNameFilter/TicketNameFilter.styles.ts b/apps/admin/src/components/TicketNameFilter/TicketNameFilter.styles.ts new file mode 100644 index 00000000..c3e79dd5 --- /dev/null +++ b/apps/admin/src/components/TicketNameFilter/TicketNameFilter.styles.ts @@ -0,0 +1,92 @@ +import styled from '@emotion/styled'; + +const Container = styled.div` + position: relative; +`; + +const TicketFilterButton = styled.button<{ isActive?: boolean }>` + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + color: ${({ theme, isActive }) => (isActive ? theme.palette.primary.o1 : theme.palette.grey.g90)}; + border-radius: 4px; + ${({ theme }) => theme.typo.b3}; + padding: 9px 16px; + + & > svg { + margin-right: 8px; + } + + ${({ isActive, theme }) => + !isActive + ? ` + &:hover { + color: ${theme.palette.grey.g70}; + background-color: ${theme.palette.mobile.grey.g10}; + } + ` + : ''} +`; + +const TicketOptions = styled.div` + position: absolute; + white-space: nowrap; + border-radius: 6px; + background-color: ${({ theme }) => theme.palette.grey.w}; + border: 1px solid ${({ theme }) => theme.palette.grey.g20}; + box-shadow: 0px 8px 14px 0px rgba(172, 171, 171, 0.13); + right: 0; + margin-top: 4px; + padding: 16px 20px; +`; + +const TicketOptionTitle = styled.div` + ${({ theme }) => theme.typo.sh1}; + color: ${({ theme }) => theme.palette.grey.g90}; +`; + +const OptionList = styled.div` + margin: 16px 0; + display: flex; + flex-direction: column; +`; + +const OptionItem = styled.button` + cursor: pointer; + padding: 8px 0; + display: flex; + justify-content: flex-start; + align-items: center; + ${({ theme }) => theme.typo.b1}; + color: ${({ theme }) => theme.palette.grey.g90}; + + & > svg { + margin-right: 8px; + } +`; + +const ButtonWrap = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: nowrap; + + & > button { + flex: 1 0 auto; + + &:first-of-type { + margin-right: 36px; + } + } +`; + +export default { + Container, + TicketFilterButton, + TicketOptions, + TicketOptionTitle, + OptionList, + OptionItem, + ButtonWrap, +}; diff --git a/apps/admin/src/components/TicketNameFilter/index.tsx b/apps/admin/src/components/TicketNameFilter/index.tsx new file mode 100644 index 00000000..d0143a7e --- /dev/null +++ b/apps/admin/src/components/TicketNameFilter/index.tsx @@ -0,0 +1,90 @@ +import { FilterIcon, SquareCheckIcon } from '@boolti/icon'; +import Styled from './TicketNameFilter.styles'; +import { useState } from 'react'; +import { Button, TextButton } from '@boolti/ui'; + +interface Option { + label: string; + value: string; +} + +interface Props { + updateSelectValues: (selectedValues: string[]) => void; + selectedValues: string[]; + options: Option[]; +} + +const TicketFilterOptions = ({ + selectedValues, + options, + updateSelectValues, + close, +}: Props & { close: VoidFunction }) => { + const [tempSelectedValues, setTempSelectedValues] = useState( + selectedValues.length === 0 ? options.map((option) => option.value) : selectedValues, + ); + return ( + + 필터 + + {options.map((option) => ( + { + setTempSelectedValues((prev) => + prev.includes(option.value) + ? prev.filter((value) => value !== option.value) + : [...prev, option.value], + ); + }} + > + + {option.label} + + ))} + + + { + updateSelectValues([]); + close(); + }} + > + 전체 선택 + + + + + ); +}; + +const TicketNameFilter = (props: Props) => { + const [isOpen, setIsOpen] = useState(false); + + const toggle = () => setIsOpen((prev) => !prev); + + return ( + + + + 필터 + + {isOpen && setIsOpen(false)} />} + + ); +}; + +export default TicketNameFilter; diff --git a/apps/admin/src/pages/ShowEnterancePage/index.tsx b/apps/admin/src/pages/ShowEnterancePage/index.tsx index 8fe0f02d..4f4db269 100644 --- a/apps/admin/src/pages/ShowEnterancePage/index.tsx +++ b/apps/admin/src/pages/ShowEnterancePage/index.tsx @@ -14,12 +14,12 @@ import EnteranceTable from '~/components/EnteranceTable'; import EntranceConfirmDialogContent from '~/components/EntranceConfirmDialogContent'; import MobileCardList from '~/components/MobileCardList'; import Pagination from '~/components/Pagination'; -import TicketTypeSelect from '~/components/TicketTypeSelect'; import Styled from './ShowEnterancePage.styles'; import { useDeviceWidth } from '~/hooks/useDeviceWidth'; import { useTheme } from '@emotion/react'; import { BooltiGreyIcon } from '@boolti/icon/src/components/BooltiGreyIcon'; +import TicketNameFilter from '~/components/TicketNameFilter'; type TicketType = 'ALL' | 'USED' | 'UNUSED'; @@ -27,9 +27,6 @@ const ShowEnterancePage = () => { const params = useParams<{ showId: string }>(); const { open, close } = useDialog(); - const [selectedTicketType, setSelectedTicketType] = useState< - React.ComponentProps['value'] - >({ value: 'ALL', label: '티켓 전체' }); const [enteranceTicketType, setEnteranceTicetType] = useState('ALL'); const [searchText, setSearchText] = useState(''); const [debouncedSearchText, setDebouncedSearchText] = useState(''); @@ -42,11 +39,18 @@ const ShowEnterancePage = () => { const useTicketUsedFilter = enteranceTicketType === 'ALL' ? undefined : enteranceTicketType === 'USED'; - const { data: salesTicketList } = useAdminSalesTicketList(showId); + + const { data: salesTicketList = [] } = useAdminSalesTicketList(showId); + + const [selectedTicketId, setSelectedTicketId] = useState([]); + const options = salesTicketList.map((ticket) => ({ + value: ticket.id.toString(), + label: ticket.ticketName, + })); const { data: ticketList, isLoading: isTicketListLoading } = useAdminTicketList( showId, searchText, - [], + selectedTicketId, useTicketUsedFilter, ); @@ -56,15 +60,11 @@ const ShowEnterancePage = () => { const totalPages = ticketList?.totalPages ?? 0; const tickets = (ticketList?.content ?? []).filter( - ({ usedAt, salesTicketType }) => - useTicketUsedFilter === undefined || - (!!usedAt === useTicketUsedFilter && - (selectedTicketType.value === 'ALL' || - salesTicketType.ticketType === selectedTicketType.value)), + ({ usedAt }) => useTicketUsedFilter === undefined || !!usedAt === useTicketUsedFilter, ); - console.log(ticketList); + const onClickReset = () => { - setSelectedTicketType({ value: 'ALL', label: '티켓 전체' }); + setSelectedTicketId([]); setSearchText(''); }; @@ -77,7 +77,7 @@ const ShowEnterancePage = () => { useEffect(() => { setCurrentPage(0); - }, [selectedTicketType, useTicketUsedFilter, debouncedSearchText]); + }, [selectedTicketId, useTicketUsedFilter, debouncedSearchText]); if (!show || !entranceSummary || !enteranceInfo || !ticketList) return null; @@ -158,9 +158,10 @@ const ShowEnterancePage = () => { - setSelectedTicketType(value)} + { + return ( + + + + + + + + + ); +}; diff --git a/packages/icon/src/components/SquareCheckIcon.tsx b/packages/icon/src/components/SquareCheckIcon.tsx new file mode 100644 index 00000000..71977390 --- /dev/null +++ b/packages/icon/src/components/SquareCheckIcon.tsx @@ -0,0 +1,22 @@ +interface Props { + checked: boolean; +} + +export const SquareCheckIcon = ({ checked }: Props) => { + return checked ? ( + + + + + ) : ( + + + + + ); +}; diff --git a/packages/icon/src/components/index.ts b/packages/icon/src/components/index.ts index a0bf8db9..95b483ad 100644 --- a/packages/icon/src/components/index.ts +++ b/packages/icon/src/components/index.ts @@ -14,6 +14,7 @@ import { Calendar } from './Calendar'; import { CB } from './CB'; import { Check } from './Check'; import { ChevronDown } from './ChevronDown'; +import { Filter } from './Filter'; import { ChevronLeft } from './ChevronLeft'; import { ChevronRight } from './ChevronRight'; import { ChevronUp } from './ChevronUp'; @@ -34,6 +35,7 @@ import { Hana } from './Hana'; import { HSBC } from './HSBC'; import { IBK } from './IBK'; import { ICBC } from './ICBC'; +import { SquareCheckIcon } from './SquareCheckIcon'; import { Instagram } from './Instagram'; import { JPMorgan } from './JPMorgan'; import { K } from './K'; @@ -84,6 +86,7 @@ export { BooltiSmall as BooltiSmallLogo, Calendar as CalendarIcon, CB, + Filter as FilterIcon, Check as CheckIcon, ChevronDown as ChevronDownIcon, ChevronLeft as ChevronLeftIcon, @@ -100,6 +103,7 @@ export { Download as DownloadIcon, Edit as EditIcon, EPost, + SquareCheckIcon, FileUp as FileUpIcon, Github as GithubIcon, Hana, From 7a004aed269745358b85bcf6caa03d48533789f3 Mon Sep 17 00:00:00 2001 From: Minsu Kim Date: Sun, 17 Nov 2024 02:05:43 +0900 Subject: [PATCH 24/62] =?UTF-8?q?feat:=20=ED=95=84=ED=84=B0=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20=EC=A0=95=EC=B1=85=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/admin/src/components/TicketNameFilter/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/admin/src/components/TicketNameFilter/index.tsx b/apps/admin/src/components/TicketNameFilter/index.tsx index d0143a7e..9eeeac76 100644 --- a/apps/admin/src/components/TicketNameFilter/index.tsx +++ b/apps/admin/src/components/TicketNameFilter/index.tsx @@ -57,6 +57,7 @@ const TicketFilterOptions = ({ - - - { - await postCastTeams.mutateAsync( - { - showId, - name, - members: members - ?.filter(({ userCode, roleName }) => userCode && roleName) - .map(({ id, userCode, roleName }) => ({ - id: id < 0 ? undefined : id, - userCode, - roleName, - })) as ShowCastTeamCreateOrUpdateRequest['members'], - }, - { - onSuccess: () => { - queryClient.invalidateQueries(queryKeys.castTeams.list(showId)); - }, + + + + { + await postCastTeams.mutateAsync( + { + showId, + name, + members: members + ?.filter(({ userCode, roleName }) => userCode && roleName) + .map(({ id, userCode, roleName }) => ({ + id: id < 0 ? undefined : id, + userCode, + roleName, + })) as ShowCastTeamCreateOrUpdateRequest['members'], + }, + { + onSuccess: () => { + queryClient.invalidateQueries(queryKeys.castTeams.list(showId)); }, - ); - }} - /> - {castTeamListDraft?.map((info, index) => ( - { - if (info.id === undefined) return; - - await putCastTeams.mutateAsync( - { - name, - members: members - ?.filter(({ userCode, roleName }) => userCode && roleName) - .map(({ id, userCode, roleName }) => ({ - id: id < 0 ? undefined : id, - userCode, - roleName, - })) as ShowCastTeamCreateOrUpdateRequest['members'], - castTeamId: info.id, - }, - { + }, + ); + }} + /> + + info.id)} strategy={verticalListSortingStrategy}> + {castTeamListDraft?.map((info) => ( + { + if (info.id === undefined) return; + + await putCastTeams.mutateAsync( + { + name, + members: members + ?.filter(({ userCode, roleName }) => userCode && roleName) + .map(({ id, userCode, roleName }) => ({ + id: id < 0 ? undefined : id, + userCode, + roleName, + })) as ShowCastTeamCreateOrUpdateRequest['members'], + castTeamId: info.id, + }, + { + onSuccess: () => { + queryClient.invalidateQueries(queryKeys.castTeams.list(showId)); + }, + }, + ); + }} + onDelete={async () => { + if (info.id === undefined) return; + + await deleteCastTeams.mutateAsync(info.id, { onSuccess: () => { queryClient.invalidateQueries(queryKeys.castTeams.list(showId)); }, - }, - ); - }} - onDropHover={castTeamDropHoverHandler} - onDrop={castTeamDropHandler} - onDelete={async () => { - if (info.id === undefined) return; - - await deleteCastTeams.mutateAsync(info.id, { - onSuccess: () => { - queryClient.invalidateQueries(queryKeys.castTeams.list(showId)); - }, - }); + }); + }} + /> + ))} + + + + + + + + + { + setPreviewDrawerOpen(false); + }} + > + + + + + + + + + file.preview), + name: showInfoForm.watch('name') ? showInfoForm.watch('name') : '', + date: showInfoForm.watch('date') + ? format(showInfoForm.watch('date'), 'yyyy.MM.dd (E)') + : '', + startTime: showInfoForm.watch('startTime'), + runningTime: showInfoForm.watch('runningTime'), + salesStartTime: showSalesInfo + ? format(showSalesInfo.salesStartTime, 'yyyy.MM.dd (E)') + : '', + salesEndTime: showSalesInfo + ? format(showSalesInfo.salesEndTime, 'yyyy.MM.dd (E)') + : '', + placeName: showInfoForm.watch('placeName'), + placeStreetAddress: showInfoForm.watch('placeStreetAddress'), + placeDetailAddress: showInfoForm.watch('placeDetailAddress'), + notice: showInfoForm.watch('notice'), + hostName: showInfoForm.watch('hostName'), + hostPhoneNumber: showInfoForm.watch('hostPhoneNumber'), + }} + showCastTeams={castTeamList} + hasNoticePage + containerRef={showPreviewRef} + /> + + + + + + { + setPreviewDrawerOpen(false); }} - /> - ))} - - - - - - - { - setPreviewDrawerOpen(false); - }} - > - + 저장하기 + + + + + {previewDrawerOpen && ( + + - - - - - - - file.preview), - name: showInfoForm.watch('name') ? showInfoForm.watch('name') : '', - date: showInfoForm.watch('date') - ? format(showInfoForm.watch('date'), 'yyyy.MM.dd (E)') - : '', - startTime: showInfoForm.watch('startTime'), - runningTime: showInfoForm.watch('runningTime'), - salesStartTime: showSalesInfo - ? format(showSalesInfo.salesStartTime, 'yyyy.MM.dd (E)') - : '', - salesEndTime: showSalesInfo - ? format(showSalesInfo.salesEndTime, 'yyyy.MM.dd (E)') - : '', - placeName: showInfoForm.watch('placeName'), - placeStreetAddress: showInfoForm.watch('placeStreetAddress'), - placeDetailAddress: showInfoForm.watch('placeDetailAddress'), - notice: showInfoForm.watch('notice'), - hostName: showInfoForm.watch('hostName'), - hostPhoneNumber: showInfoForm.watch('hostPhoneNumber'), - }} - showCastTeams={castTeamList} - hasNoticePage - containerRef={showPreviewRef} - /> - - - + file.preview), + name: showInfoForm.watch('name') ? showInfoForm.watch('name') : '', + date: showInfoForm.watch('date') + ? format(showInfoForm.watch('date'), 'yyyy.MM.dd (E)') + : '', + startTime: showInfoForm.watch('startTime'), + runningTime: showInfoForm.watch('runningTime'), + salesStartTime: showSalesInfo + ? format(showSalesInfo.salesStartTime, 'yyyy.MM.dd (E)') + : '', + salesEndTime: showSalesInfo + ? format(showSalesInfo.salesEndTime, 'yyyy.MM.dd (E)') + : '', + placeName: showInfoForm.watch('placeName'), + placeStreetAddress: showInfoForm.watch('placeStreetAddress'), + placeDetailAddress: showInfoForm.watch('placeDetailAddress'), + notice: showInfoForm.watch('notice'), + hostName: showInfoForm.watch('hostName'), + hostPhoneNumber: showInfoForm.watch('hostPhoneNumber'), + }} + showCastTeams={castTeamList} + hasNoticePage + containerRef={showPreviewMobileRef} + /> { 저장하기 - - - {previewDrawerOpen && ( - - - - file.preview), - name: showInfoForm.watch('name') ? showInfoForm.watch('name') : '', - date: showInfoForm.watch('date') - ? format(showInfoForm.watch('date'), 'yyyy.MM.dd (E)') - : '', - startTime: showInfoForm.watch('startTime'), - runningTime: showInfoForm.watch('runningTime'), - salesStartTime: showSalesInfo - ? format(showSalesInfo.salesStartTime, 'yyyy.MM.dd (E)') - : '', - salesEndTime: showSalesInfo - ? format(showSalesInfo.salesEndTime, 'yyyy.MM.dd (E)') - : '', - placeName: showInfoForm.watch('placeName'), - placeStreetAddress: showInfoForm.watch('placeStreetAddress'), - placeDetailAddress: showInfoForm.watch('placeDetailAddress'), - notice: showInfoForm.watch('notice'), - hostName: showInfoForm.watch('hostName'), - hostPhoneNumber: showInfoForm.watch('hostPhoneNumber'), - }} - showCastTeams={castTeamList} - hasNoticePage - containerRef={showPreviewMobileRef} - /> - - - { - setPreviewDrawerOpen(false); - }} - > - 닫기 - - { - showInfoForm.handleSubmit(onSubmit)(); - }} - > - 저장하기 - - - - - )} - + + + )} )} diff --git a/packages/ui/package.json b/packages/ui/package.json index d056ecb5..07a9198e 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -19,10 +19,6 @@ "nanoid": "^5.0.4", "rdndmb-html5-to-touch": "^8.0.3", "react": "^18.2.0", - "react-dnd": "^16.0.1", - "react-dnd-html5-backend": "^16.0.1", - "react-dnd-multi-backend": "^8.0.3", - "react-dnd-touch-backend": "^16.0.1", "react-dom": "^18.2.0", "react-hot-toast": "^2.4.1", "swiper": "^11.0.7" diff --git a/packages/ui/src/components/BooltiUIProvider/index.tsx b/packages/ui/src/components/BooltiUIProvider/index.tsx index 68db223a..28972924 100644 --- a/packages/ui/src/components/BooltiUIProvider/index.tsx +++ b/packages/ui/src/components/BooltiUIProvider/index.tsx @@ -1,30 +1,25 @@ -import { DndProvider } from 'react-dnd-multi-backend' -import { HTML5toTouch } from 'rdndmb-html5-to-touch' - -import '../../index.css'; import AlertProvider from '../AlertProvider'; - import ConfirmProvider from '../ConfirmProvider'; import DialogProvider from '../DialogProvider'; import ThemeProvider from '../ThemeProvider'; +import '../../index.css'; + interface BooltiUIProviderProps { children: React.ReactNode; } const BooltiUIProvider = ({ children }: BooltiUIProviderProps) => { return ( - - - - - - {children} - - - - - + + + + + {children} + + + + ); }; diff --git a/yarn.lock b/yarn.lock index 744e3429..3ab0c8b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1732,10 +1732,6 @@ __metadata: nanoid: "npm:^5.0.4" rdndmb-html5-to-touch: "npm:^8.0.3" react: "npm:^18.2.0" - react-dnd: "npm:^16.0.1" - react-dnd-html5-backend: "npm:^16.0.1" - react-dnd-multi-backend: "npm:^8.0.3" - react-dnd-touch-backend: "npm:^16.0.1" react-dom: "npm:^18.2.0" react-hot-toast: "npm:^2.4.1" swiper: "npm:^11.0.7" @@ -1764,6 +1760,68 @@ __metadata: languageName: node linkType: hard +"@dnd-kit/accessibility@npm:^3.1.0": + version: 3.1.0 + resolution: "@dnd-kit/accessibility@npm:3.1.0" + dependencies: + tslib: "npm:^2.0.0" + peerDependencies: + react: ">=16.8.0" + checksum: 10c0/4f9d24e801d66d4fbb551ec389ed90424dd4c5bbdf527000a618e9abb9833cbd84d9a79e362f470ccbccfbd6d00217a9212c92f3cef66e01c951c7f79625b9d7 + languageName: node + linkType: hard + +"@dnd-kit/core@npm:^6.1.0": + version: 6.1.0 + resolution: "@dnd-kit/core@npm:6.1.0" + dependencies: + "@dnd-kit/accessibility": "npm:^3.1.0" + "@dnd-kit/utilities": "npm:^3.2.2" + tslib: "npm:^2.0.0" + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: 10c0/c793eb97cb59285ca8937ebcdfcd27cff09d750ae06722e36ca5ed07925e41abc36a38cff98f9f6056f7a07810878d76909826142a2968330e7e22060e6be584 + languageName: node + linkType: hard + +"@dnd-kit/modifiers@npm:^7.0.0": + version: 7.0.0 + resolution: "@dnd-kit/modifiers@npm:7.0.0" + dependencies: + "@dnd-kit/utilities": "npm:^3.2.2" + tslib: "npm:^2.0.0" + peerDependencies: + "@dnd-kit/core": ^6.1.0 + react: ">=16.8.0" + checksum: 10c0/542e1d2b6102a5c826118c36158aab23c5437d24008cab4848b0866d3d850b4410c4f465690767dd1f31fde33a1fa9d238675be70f174c179485ce376f0c8aa6 + languageName: node + linkType: hard + +"@dnd-kit/sortable@npm:^8.0.0": + version: 8.0.0 + resolution: "@dnd-kit/sortable@npm:8.0.0" + dependencies: + "@dnd-kit/utilities": "npm:^3.2.2" + tslib: "npm:^2.0.0" + peerDependencies: + "@dnd-kit/core": ^6.1.0 + react: ">=16.8.0" + checksum: 10c0/a6066c652b892c6a11320c7d8f5c18fdf723e721e8eea37f4ab657dee1ac5e7ca710ac32ce0712a57fe968bc07c13bcea5d5599d90dfdd95619e162befd4d2fb + languageName: node + linkType: hard + +"@dnd-kit/utilities@npm:^3.2.2": + version: 3.2.2 + resolution: "@dnd-kit/utilities@npm:3.2.2" + dependencies: + tslib: "npm:^2.0.0" + peerDependencies: + react: ">=16.8.0" + checksum: 10c0/9aa90526f3e3fd567b5acc1b625a63177b9e8d00e7e50b2bd0e08fa2bf4dba7e19529777e001fdb8f89a7ce69f30b190c8364d390212634e0afdfa8c395e85a0 + languageName: node + linkType: hard + "@emotion/babel-plugin@npm:^11.11.0": version: 11.11.0 resolution: "@emotion/babel-plugin@npm:11.11.0" @@ -3326,13 +3384,6 @@ __metadata: languageName: node linkType: hard -"@react-dnd/shallowequal@npm:^4.0.1": - version: 4.0.2 - resolution: "@react-dnd/shallowequal@npm:4.0.2" - checksum: 10c0/9a352fd176752e5d9c2797d598aca034b7829111ae0c992d80f40d5f068fcd6a039b1841c741dfa1ab67a36a00664310aec4f0ce216e4112f80875c9fe6fd8dc - languageName: node - linkType: hard - "@react-pdf/fns@npm:2.2.1": version: 2.2.1 resolution: "@react-pdf/fns@npm:2.2.1" @@ -5394,6 +5445,10 @@ __metadata: "@boolti/icon": "npm:*" "@boolti/typescript-config": "npm:*" "@boolti/ui": "npm:*" + "@dnd-kit/core": "npm:^6.1.0" + "@dnd-kit/modifiers": "npm:^7.0.0" + "@dnd-kit/sortable": "npm:^8.0.0" + "@dnd-kit/utilities": "npm:^3.2.2" "@emotion/babel-plugin": "npm:^11.11.0" "@emotion/react": "npm:^11.11.3" "@emotion/styled": "npm:^11.11.0" @@ -5413,8 +5468,6 @@ __metadata: qrcode.react: "npm:^3.1.0" react: "npm:^18.2.0" react-daum-postcode: "npm:^3.1.3" - react-dnd: "npm:^16.0.1" - react-dnd-html5-backend: "npm:^16.0.1" react-dom: "npm:^18.2.0" react-dropzone: "npm:^14.2.3" react-hook-form: "npm:^7.50.0" @@ -8558,7 +8611,7 @@ __metadata: languageName: node linkType: hard -"hoist-non-react-statics@npm:^3.3.1, hoist-non-react-statics@npm:^3.3.2": +"hoist-non-react-statics@npm:^3.3.1": version: 3.3.2 resolution: "hoist-non-react-statics@npm:3.3.2" dependencies: @@ -11711,31 +11764,6 @@ __metadata: languageName: node linkType: hard -"react-dnd-multi-backend@npm:^8.0.3": - version: 8.0.3 - resolution: "react-dnd-multi-backend@npm:8.0.3" - dependencies: - dnd-multi-backend: "npm:^8.0.3" - react-dnd-preview: "npm:^8.0.3" - peerDependencies: - dnd-core: ^16.0.1 - react: ^16.14.0 || ^17.0.2 || ^18.0.0 - react-dnd: ^16.0.1 - react-dom: ^16.14.0 || ^17.0.2 || ^18.0.0 - checksum: 10c0/9a6842cdf290bf4cd28105dc14cfe8aa6c09bd1f0da74fd459c2300aa26d0b141fcf5e2906dbe77b46eb6f555a93f7e1fdd64bac6930cd8d971992be79a6b39c - languageName: node - linkType: hard - -"react-dnd-preview@npm:^8.0.3": - version: 8.0.3 - resolution: "react-dnd-preview@npm:8.0.3" - peerDependencies: - react: ^16.14.0 || ^17.0.2 || ^18.0.0 - react-dnd: ^16.0.1 - checksum: 10c0/f010d04a386debe37c5375a0655686dc00b6e42f3b45aabb074ac52f9f1d92379fec552714991125789cbc7d370f94b0100b6c77d2d41037e87bff00234bc3ef - languageName: node - linkType: hard - "react-dnd-touch-backend@npm:^16.0.1": version: 16.0.1 resolution: "react-dnd-touch-backend@npm:16.0.1" @@ -11746,31 +11774,6 @@ __metadata: languageName: node linkType: hard -"react-dnd@npm:^16.0.1": - version: 16.0.1 - resolution: "react-dnd@npm:16.0.1" - dependencies: - "@react-dnd/invariant": "npm:^4.0.1" - "@react-dnd/shallowequal": "npm:^4.0.1" - dnd-core: "npm:^16.0.1" - fast-deep-equal: "npm:^3.1.3" - hoist-non-react-statics: "npm:^3.3.2" - peerDependencies: - "@types/hoist-non-react-statics": ">= 3.3.1" - "@types/node": ">= 12" - "@types/react": ">= 16" - react: ">= 16.14" - peerDependenciesMeta: - "@types/hoist-non-react-statics": - optional: true - "@types/node": - optional: true - "@types/react": - optional: true - checksum: 10c0/d069435750f0d6653cfa2b951cac8abb3583fb144ff134a20176608877d9c5964c63384ebbacaa0fdeef819b592a103de0d8e06f3b742311d64a029ffed0baa3 - languageName: node - linkType: hard - "react-docgen-typescript@npm:^2.2.2": version: 2.2.2 resolution: "react-docgen-typescript@npm:2.2.2" From 4b2c9b42768493b6d9493cee795af8e9f35c2486 Mon Sep 17 00:00:00 2001 From: Shim MunSeong Date: Sat, 23 Nov 2024 21:48:07 +0900 Subject: [PATCH 42/62] =?UTF-8?q?feat:=20=EC=B6=9C=EC=97=B0=EC=A7=84=20?= =?UTF-8?q?=ED=8C=80=EC=9B=90=20=EC=88=9C=EC=84=9C=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=8A=A4=ED=83=80=EC=9D=BC=EB=A7=81?= =?UTF-8?q?=EC=97=90=20blur=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DraggableShowCastInfoMemberRow.tsx | 4 +- .../ShowCastInfoFormDialogContent.styles.ts | 38 ++++++++++--------- .../ShowCastInfoFormDialogContent/index.tsx | 2 +- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/apps/admin/src/components/ShowCastInfoFormDialogContent/DraggableShowCastInfoMemberRow.tsx b/apps/admin/src/components/ShowCastInfoFormDialogContent/DraggableShowCastInfoMemberRow.tsx index fa309949..162f1188 100644 --- a/apps/admin/src/components/ShowCastInfoFormDialogContent/DraggableShowCastInfoMemberRow.tsx +++ b/apps/admin/src/components/ShowCastInfoFormDialogContent/DraggableShowCastInfoMemberRow.tsx @@ -19,9 +19,7 @@ const DraggableShowCastInfoMemberRow = ({ id, ...props }: DraggableShowCastInfoM const style = { transform: CSS.Translate.toString(transform), transition, - opacity: isDragging ? 0 : 1, - backdropFilter: isDragging ? 'blur(1.5px)' : undefined, - cursor: isDragging ? 'grabbing' : undefined, + opacity: isDragging ? 0 : 1 }; return ( diff --git a/apps/admin/src/components/ShowCastInfoFormDialogContent/ShowCastInfoFormDialogContent.styles.ts b/apps/admin/src/components/ShowCastInfoFormDialogContent/ShowCastInfoFormDialogContent.styles.ts index cb8b6322..77d44bef 100644 --- a/apps/admin/src/components/ShowCastInfoFormDialogContent/ShowCastInfoFormDialogContent.styles.ts +++ b/apps/admin/src/components/ShowCastInfoFormDialogContent/ShowCastInfoFormDialogContent.styles.ts @@ -106,6 +106,8 @@ const Row = styled.div` justify-content: center; align-items: flex-start; border-radius: 4px; + position: relative; + z-index: 99; `; const TrashCanButton = styled.button` @@ -186,27 +188,27 @@ const ErrorMessage = styled.span` color: ${({ theme }) => theme.palette.status.error}; `; -const DraggableShowCastInfoMemberRow = styled.div<{ isDragging: boolean }>` +const DraggableShowCastInfoMemberRow = styled.div` border-radius: 4px; cursor: grabbing; + backdrop-filter: blur(1.5px); + z-index: 100; + + & > div > div > div { + background: none; + } - ${({ isDragging }) => isDragging && ` - & > div > div > div { - background: none; - } - - &::after { - content: ''; - position: absolute; - top: -10px; - left: -10px; - width: calc(100% + 20px); - height: calc(100% + 20px); - background-color: rgba(231, 234, 242, 0.5); - border-radius: 4px; - z-index: -1; - } - `}; + &::after { + content: ''; + position: absolute; + top: -10px; + left: -10px; + width: calc(100% + 20px); + height: calc(100% + 20px); + background-color: rgba(231, 234, 242, 0.5); + border-radius: 4px; + z-index: -1; + } ` diff --git a/apps/admin/src/components/ShowCastInfoFormDialogContent/index.tsx b/apps/admin/src/components/ShowCastInfoFormDialogContent/index.tsx index 9604ae47..e4deb806 100644 --- a/apps/admin/src/components/ShowCastInfoFormDialogContent/index.tsx +++ b/apps/admin/src/components/ShowCastInfoFormDialogContent/index.tsx @@ -221,7 +221,7 @@ const ShowCastInfoFormDialogContent = ({ prevShowCastInfo, onDelete, onSave }: P {(draggingItemId && draggingField && draggingFieldIndex > -1) ? ( - + Date: Sat, 23 Nov 2024 22:04:19 +0900 Subject: [PATCH 43/62] =?UTF-8?q?feat:=20=EA=B3=B5=EC=97=B0=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=8B=9C=EC=97=90=20=EC=B6=9C=EC=97=B0=EC=A7=84=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EB=93=9C=EB=9E=98=EA=B7=B8=20=EC=95=A4=20?= =?UTF-8?q?=EB=93=9C=EB=A1=AD=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/admin/src/hooks/useCastTeamListOrder.ts | 22 ++++- apps/admin/src/pages/ShowAddPage/index.tsx | 99 ++++++++++---------- apps/admin/src/pages/ShowInfoPage/index.tsx | 23 +---- 3 files changed, 75 insertions(+), 69 deletions(-) diff --git a/apps/admin/src/hooks/useCastTeamListOrder.ts b/apps/admin/src/hooks/useCastTeamListOrder.ts index 704e5bb2..121c6cd4 100644 --- a/apps/admin/src/hooks/useCastTeamListOrder.ts +++ b/apps/admin/src/hooks/useCastTeamListOrder.ts @@ -1,7 +1,7 @@ import { useChangeCastTeamOrder } from "@boolti/api"; import { useCallback, useEffect, useRef, useState } from "react"; -import { DragOverEvent } from "@dnd-kit/core"; -import { arrayMove } from "@dnd-kit/sortable"; +import { DragOverEvent, KeyboardSensor, MouseSensor, TouchSensor, useSensor, useSensors } from "@dnd-kit/core"; +import { arrayMove, sortableKeyboardCoordinates } from '@dnd-kit/sortable'; import { TempShowCastInfoFormInput } from "~/components/ShowCastInfoFormDialogContent"; @@ -44,6 +44,23 @@ const useCastTeamListOrder = (params?: UseCastTeamListOrderParams) => { }); }, [changeCastTeamOrder, showId]); + const sensors = useSensors( + useSensor(MouseSensor, { + activationConstraint: { + distance: 10, + }, + }), + useSensor(TouchSensor, { + activationConstraint: { + delay: 0, + tolerance: 5, + }, + }), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + useEffect(() => { if (!castTeamList) return; @@ -62,6 +79,7 @@ const useCastTeamListOrder = (params?: UseCastTeamListOrderParams) => { return { castTeamListDraft, + sensors, setCastTeamListDraft, castTeamDragEndHandler, } diff --git a/apps/admin/src/pages/ShowAddPage/index.tsx b/apps/admin/src/pages/ShowAddPage/index.tsx index 7c9ed9a5..f8495a78 100644 --- a/apps/admin/src/pages/ShowAddPage/index.tsx +++ b/apps/admin/src/pages/ShowAddPage/index.tsx @@ -9,6 +9,9 @@ import { Button, useToast } from '@boolti/ui'; import { useState } from 'react'; import { SubmitHandler, useForm } from 'react-hook-form'; import { Navigate, useNavigate } from 'react-router-dom'; +import { DndContext, closestCenter } from '@dnd-kit/core'; +import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; +import { restrictToVerticalAxis } from '@dnd-kit/modifiers'; import ShowBasicInfoFormContent from '~/components/ShowInfoFormContent/ShowBasicInfoFormContent'; import ShowDetailInfoFormContent from '~/components/ShowInfoFormContent/ShowDetailInfoFormContent'; @@ -46,7 +49,7 @@ const ShowAddPage = ({ step }: ShowAddPageProps) => { const uploadShowImageMutation = useUploadShowImage(); const addShowMutation = useAddShow(); - const { castTeamListDraft, setCastTeamListDraft, castTeamDropHoverHandler, castTeamDropHandler } = useCastTeamListOrder(); + const { castTeamListDraft, sensors, setCastTeamListDraft, castTeamDragEndHandler } = useCastTeamListOrder(); const toast = useToast(); @@ -175,29 +178,30 @@ const ShowAddPage = ({ step }: ShowAddPageProps) => { return new Promise((reslve) => reslve()); }} /> - {castTeamListDraft.map((info, index) => ( - { - setCastTeamListDraft((prev) => - prev.map((prevCastInfo, currentIndex) => - index === currentIndex ? showCastInfoFormInput : prevCastInfo, - ) - ); - return new Promise((reslve) => reslve()); - }} - onDelete={() => { - setCastTeamListDraft((prev) => - prev.filter((_, currentIndex) => index !== currentIndex) - ); - return new Promise((reslve) => reslve()); - }} - onDrop={castTeamDropHandler} - onDropHover={castTeamDropHoverHandler} - /> - ))} + + info.id)} strategy={verticalListSortingStrategy}> + {castTeamListDraft.map((info) => ( + { + setCastTeamListDraft((prev) => + prev.map((item) => + item.id === info.id ? showCastInfoFormInput : item, + ) + ); + return new Promise((reslve) => reslve()); + }} + onDelete={() => { + setCastTeamListDraft((prev) => + prev.filter((item) => item.id !== info.id) + ); + return new Promise((reslve) => reslve()); + }} + /> + ))} + +