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;