From b0c93b1b7ade16995cb01a92fe6e972f9d68b542 Mon Sep 17 00:00:00 2001 From: Minsu Kim Date: Wed, 31 Jul 2024 00:53:54 +0900 Subject: [PATCH] =?UTF-8?q?deploy:=206=EC=B0=A8=20MVP=20=EB=B0=B0=ED=8F=AC?= =?UTF-8?q?=20(#137)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 디자인 QA 누락건들 반영 (#126) * fix: 타임피커 스타일 수정 * fix: 공연 등록 페이지 디자인 qa 반영 * fix: 주최자 홈 -> 홈 텍스트 수정 * fix: 공연 등록 이미지 관련 수정사항 반영 * fix: 남은 디자인 QA 반영 * fix: Input 모바일 오른쪽 패딩 수정 (#127) * feat: 권한 설정 작업 (#125) * feat: Dialog Provider에서 Dialog 컴포넌트에 width prop 추가 * feat: 공연 정보 페이지 > 공연명 우측에 권한 설정 버튼 추가 * feat: 권한 설정 Dialog 추가 * feat: host 리스트, 본인 정보 쿼리 추가 * feat: host 권한 추가/수정/삭제 mutation 추가 * feat: host type 추가/수정/삭제 mutation invalidQueries 추가 * feat: ShowDetailLayout 이용처에서 showId prop 전달 * feat: 호스트 인풋폼 컴포넌트 추가 * feat: 호스트 리스트 컴포넌트 추가 * feat: query api path 수정 * feat: 권한 설정 Dialog 추가 * feat: 모바일 적용 * feat: 아이콘 추가 * fix: 추가한 아이콘 props 수정 * feat: 권한 없을 때 보여주는 화면 컴포넌트 추가 * feat: 권한 없을 때 보여주는 컴포넌트 모바일 대응 * feat: host list item 컴포넌트 추가 * feat: host 추가 mutation interface 변경 반영 * feat: 호스트 추가/수정/삭제 시 invalidateQueries 수정 * 불필요값 제거 * 불티 아이콘 props 카멜케이스로 수정 * fix: mutateAsync -> mutate로 변경 * fix: 디자인 qa 하면서 생긴 버그 수정 및 라우터 구조 수정 (#128) * fix: 라우트 구조 수정 * fix: 디자인 QA 하면서 생긴 버그 수정 * feat: 스크롤바 제거 및 바디 스크롤락 적용 * feat: 정산 관리 페이지에서 정산 계좌 입력 삭제 (#129) * feat: AccountInfo 삭제, userCode 반영 및 다이나믹 링크 반영 (#130) * feat: AccountInfo 컴포넌트 제거 * feat: 정산계좌 내려주는 훅 제거 및 사용자 코드 반영 * feat: 다이나믹 링크 .debug 붙이는 로직 추가 * feat: 태그를 운영에서만 심기로 * feat: 딥링크 분기 추가 반영 (#132) * Fix: 선물 디자인 QA (#131) * fix: 선물 수령 화면 > 일부 스타일 수정 * fix: 선물 수령 화면 배경색 변경 * fix: 선물 취소/환불 규정 영역 최대 넓이값 조정 * fix: 선물 수령 화면 > confirm message 줄바꿈 적용 * fix: desktop view에서 confirm message 좌측 정렬 * fix: confirm에서 alert 모달 대응 * fix: 선물 인트로 화면 > 선물 보낸이 이름으로 변경 * fix: 공연일 값 제거하고 선물 만료일 추가 * fix: confirmOptions의 type 필수 제외 * fix: Alert 컴포넌트 추가 * fix: Confirm에서 type으로 분기하던 것 제거 * fix: 등록하기 버튼 클릭 시 환경에 따라 confirm/alert 분기 처리 * Fix: 권한 설정 디자인 QA (#133) * fix: 권한 설정 Dialog > Host InputForm 스타일 수정 * fix: Dialog 닫기버튼 컬러 변경 * fix: 초대하기 버튼 disabled 시의 텍스트 컬러 변경 * fix: 호스트 멤버 권한 드롭다운 chevron down 아이콘 컬러 변경 * fix: 호스트 input form 해쉬태그 추가, 이미 초대한 회원 에러 처리 * fix: 방문자 관리 empty 화면 스타일 변경 * fix: 도우미일 때 권한 설정 버튼 미노출 * fix: 본인 권한 도우미로 바꿀 때 플로우 개선 * fix: 본인 권한 삭제 시 플로우 개선 * fix: 공연 리스트 아이템에 나의 호스트 타입 필드명 변경 * fix: 모바일에서 권한 설정 바텀시트 높이 수정 * fix: 권한 변경 시 confirm message 정렬 * fix: 호스트 권한 삭제 후 onSuccess 처리에서 본인 여부 분기 적용 * fix: 빼먹은 값 추가,, * Fix: 디자인 QA (#134) * fix: 호스트 input form hashtag 상시 노출 * fix: 모바일 공연 상세 > 공연명 말줄임 처리 * fix: Dialog close button icon 넓이, 높이 수정 * fix: 권한 설정 Dialog > chevron down icon 컬러 변경 * fix: 입장 관리 empty 화면 수정 * feat: 설정 팝업 관련 UI 구현 및 API 연동 (#135) * feat: 설정 팝업 UI 관련 작업 * feat: 계정 설정 팝업 API 연동 및 회원 탈퇴 API 연동 (카카오 로그인 시에만 회원 탈퇴 가능) * style: lint 실행 * Fix: 디자인 QA (#136) * fix: 호스트 input form hashtag 상시 노출 * fix: 모바일 공연 상세 > 공연명 말줄임 처리 * fix: Dialog close button icon 넓이, 높이 수정 * fix: 권한 설정 Dialog > chevron down icon 컬러 변경 * fix: 입장 관리 empty 화면 수정 * fix: 초대하기 버튼 disabled color 수정 * fix: 모바일에서 권한설정 버튼 텍스트 미노출 * fix: 권한 설정 버튼 최소 너비값 제거 --------- Co-authored-by: Park YoungHo <30437680+hexdrinker@users.noreply.github.com> Co-authored-by: Shim MunSeong --- apps/admin/index.html | 14 +- apps/admin/src/App.tsx | 24 +- .../AccountDeleteForm.styles.ts | 60 +++ .../components/AccountDeleteForm/index.tsx | 86 ++++ .../AccountInfo/AccountInfo.styles.ts | 72 --- .../src/components/AccountInfo/index.tsx | 68 --- .../AuthoritySettingDialogContent.styles.ts | 7 + .../HostInputForm/HostInputForm.styles.ts | 140 ++++++ .../components/HostInputForm/index.tsx | 106 +++++ .../components/HostList/HostList.styles.ts | 30 ++ .../components/HostList/index.tsx | 86 ++++ .../HostListItem/HostListItem.styles.ts | 116 +++++ .../components/HostListItem/index.tsx | 119 +++++ .../AuthoritySettingDialogContent/index.tsx | 22 + .../EntranceConfirmDialogContent.styles.ts | 10 +- .../src/components/Layout/Layout.styles.ts | 1 + apps/admin/src/components/Layout/index.tsx | 11 +- .../MobileCardList/MobileCardList.style.ts | 1 + .../ProfileDropdown/ProfileDropdown.styles.ts | 38 +- .../src/components/ProfileDropdown/index.tsx | 61 ++- .../SettingDialogContent.styles.ts | 169 +++++++ .../components/SettingDialogContent/index.tsx | 113 +++++ .../ShowDetailLayout.styles.ts | 45 +- .../src/components/ShowDetailLayout/index.tsx | 62 ++- .../ShowDetailUnauthorized.styles.ts | 57 +++ .../ShowDetailUnauthorized/index.tsx | 34 ++ .../ShowBasicInfoFormContent.tsx | 14 +- .../ShowDetailInfoFormContent.tsx | 2 +- .../ShowInfoFormContent.styles.ts | 70 +-- .../ShowInvitationTicketFormContent.tsx | 2 +- .../ShowSalesTicketFormContent.tsx | 4 +- .../ShowTicketInfoFormContent.tsx | 2 +- .../components/ShowList/ShowList.styles.ts | 5 +- apps/admin/src/components/ShowList/index.tsx | 2 + .../src/components/ShowListItem/index.tsx | 7 + .../UserProfile/UserProfile.styles.ts | 4 +- .../src/components/UserProfile/index.tsx | 8 +- apps/admin/src/constants/link.ts | 11 +- apps/admin/src/constants/phase.ts | 1 + apps/admin/src/pages/GiftIntroPage/index.tsx | 2 +- .../GiftRegisterPage.styles.ts | 2 +- .../components/GiftGuide/index.tsx | 2 +- .../GiftInformation/GiftInformation.styles.ts | 11 +- .../components/GiftInformation/index.tsx | 63 ++- .../components/GiftTerms/GiftTerms.styles.ts | 2 + .../src/pages/GiftRegisterPage/index.tsx | 2 +- .../src/pages/HomePage/HomePage.styles.ts | 54 ++- apps/admin/src/pages/HomePage/HomePage.tsx | 109 +++-- .../ShowAddCompletePage.tsx | 2 +- .../pages/ShowAddPage/ShowAddPage.styles.ts | 7 +- .../src/pages/ShowAddPage/ShowAddPage.tsx | 2 +- .../ShowEnterancePage.styles.ts | 40 +- .../src/pages/ShowEnterancePage/index.tsx | 20 +- .../src/pages/ShowInfoPage/ShowInfoPage.tsx | 277 +++++------ .../ShowReservationPage.styles.ts | 46 +- .../src/pages/ShowReservationPage/index.tsx | 21 +- .../ShowSettlementPage.styles.ts | 2 +- .../ShowSettlementPage/ShowSettlementPage.tsx | 430 ++++++++---------- .../pages/ShowTicketPage/ShowTicketPage.tsx | 245 +++++----- packages/api/src/mutations/index.ts | 8 + packages/api/src/mutations/useAddHost.ts | 24 + packages/api/src/mutations/useDeleteHost.ts | 22 + packages/api/src/mutations/useDeleteMe.ts | 16 + packages/api/src/mutations/useEditHost.ts | 28 ++ packages/api/src/queries/index.ts | 6 +- packages/api/src/queries/useHostList.ts | 7 + packages/api/src/queries/useMyHostInfo.ts | 7 + .../api/src/queries/useUserAccountInfo.ts | 7 - packages/api/src/queryKey.ts | 24 +- packages/api/src/types/error.ts | 5 + packages/api/src/types/gift.ts | 9 +- packages/api/src/types/host.ts | 27 ++ packages/api/src/types/show.ts | 3 + packages/api/src/types/users.ts | 10 +- .../icon/src/components/BooltiGreyIcon.tsx | 17 + .../icon/src/components/BooltiLightGrey.tsx | 22 + packages/icon/src/components/ChevronDown.tsx | 2 +- packages/icon/src/components/Logout.tsx | 16 + packages/icon/src/components/Setting.tsx | 27 ++ packages/icon/src/components/UserAdd.tsx | 34 ++ packages/icon/src/components/index.ts | 6 + .../ui/src/components/Alert/Alert.styles.ts | 87 ++++ packages/ui/src/components/Alert/index.tsx | 32 ++ .../ui/src/components/AlertProvider/index.tsx | 32 ++ .../src/components/BooltiUIProvider/index.tsx | 9 +- .../src/components/Confirm/Confirm.styles.ts | 4 + .../ui/src/components/Dialog/Dialog.styles.ts | 93 +++- packages/ui/src/components/Dialog/index.tsx | 22 +- .../src/components/DialogProvider/index.tsx | 3 + .../TimePicker/TimePicker.styles.ts | 21 +- .../ui/src/components/TimePicker/index.tsx | 105 ++--- packages/ui/src/contexts/alertContext.ts | 25 + packages/ui/src/contexts/dialogContext.ts | 2 + packages/ui/src/hooks/index.ts | 3 +- packages/ui/src/hooks/useAlert.ts | 18 + packages/ui/src/hooks/useDialog.ts | 6 + packages/ui/src/systems/palette.ts | 4 +- 97 files changed, 2860 insertions(+), 956 deletions(-) create mode 100644 apps/admin/src/components/AccountDeleteForm/AccountDeleteForm.styles.ts create mode 100644 apps/admin/src/components/AccountDeleteForm/index.tsx delete mode 100644 apps/admin/src/components/AccountInfo/AccountInfo.styles.ts delete mode 100644 apps/admin/src/components/AccountInfo/index.tsx create mode 100644 apps/admin/src/components/AuthoritySettingDialogContent/AuthoritySettingDialogContent.styles.ts create mode 100644 apps/admin/src/components/AuthoritySettingDialogContent/components/HostInputForm/HostInputForm.styles.ts create mode 100644 apps/admin/src/components/AuthoritySettingDialogContent/components/HostInputForm/index.tsx create mode 100644 apps/admin/src/components/AuthoritySettingDialogContent/components/HostList/HostList.styles.ts create mode 100644 apps/admin/src/components/AuthoritySettingDialogContent/components/HostList/index.tsx create mode 100644 apps/admin/src/components/AuthoritySettingDialogContent/components/HostListItem/HostListItem.styles.ts create mode 100644 apps/admin/src/components/AuthoritySettingDialogContent/components/HostListItem/index.tsx create mode 100644 apps/admin/src/components/AuthoritySettingDialogContent/index.tsx create mode 100644 apps/admin/src/components/SettingDialogContent/SettingDialogContent.styles.ts create mode 100644 apps/admin/src/components/SettingDialogContent/index.tsx create mode 100644 apps/admin/src/components/ShowDetailUnauthorized/ShowDetailUnauthorized.styles.ts create mode 100644 apps/admin/src/components/ShowDetailUnauthorized/index.tsx create mode 100644 apps/admin/src/constants/phase.ts create mode 100644 packages/api/src/mutations/useAddHost.ts create mode 100644 packages/api/src/mutations/useDeleteHost.ts create mode 100644 packages/api/src/mutations/useDeleteMe.ts create mode 100644 packages/api/src/mutations/useEditHost.ts create mode 100644 packages/api/src/queries/useHostList.ts create mode 100644 packages/api/src/queries/useMyHostInfo.ts delete mode 100644 packages/api/src/queries/useUserAccountInfo.ts create mode 100644 packages/api/src/types/error.ts create mode 100644 packages/api/src/types/host.ts create mode 100644 packages/icon/src/components/BooltiGreyIcon.tsx create mode 100644 packages/icon/src/components/BooltiLightGrey.tsx create mode 100644 packages/icon/src/components/Logout.tsx create mode 100644 packages/icon/src/components/Setting.tsx create mode 100644 packages/icon/src/components/UserAdd.tsx create mode 100644 packages/ui/src/components/Alert/Alert.styles.ts create mode 100644 packages/ui/src/components/Alert/index.tsx create mode 100644 packages/ui/src/components/AlertProvider/index.tsx create mode 100644 packages/ui/src/contexts/alertContext.ts create mode 100644 packages/ui/src/hooks/useAlert.ts diff --git a/apps/admin/index.html b/apps/admin/index.html index fff60305..fd324d3b 100644 --- a/apps/admin/index.html +++ b/apps/admin/index.html @@ -45,13 +45,15 @@ diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx index 81e03b8a..356f1868 100644 --- a/apps/admin/src/App.tsx +++ b/apps/admin/src/App.tsx @@ -9,6 +9,7 @@ import { createBrowserRouter, Navigate, Outlet, + RouteObject, RouterProvider, ScrollRestoration, } from 'react-router-dom'; @@ -128,16 +129,23 @@ const privateRoutes = [ }, ]; -const router = createBrowserRouter([...publicRoutes, ...privateRoutes]); +const routes: RouteObject[] = [ + { + element: ( + + + + + + ), + children: [...publicRoutes, ...privateRoutes], + }, +]; + +const router = createBrowserRouter(routes); const App = () => { - return ( - - - - - - ); + return ; }; export default App; diff --git a/apps/admin/src/components/AccountDeleteForm/AccountDeleteForm.styles.ts b/apps/admin/src/components/AccountDeleteForm/AccountDeleteForm.styles.ts new file mode 100644 index 00000000..68d8deb6 --- /dev/null +++ b/apps/admin/src/components/AccountDeleteForm/AccountDeleteForm.styles.ts @@ -0,0 +1,60 @@ +import { mq_lg } from '@boolti/ui'; +import styled from '@emotion/styled'; + +const AccountDeleteForm = styled.form` + padding: 24px 0; + + ${mq_lg} { + padding: 0; + } +`; + +const AccountDeleteFormDescription = styled.p` + ${({ theme }) => theme.typo.b3}; + color: ${({ theme }) => theme.palette.grey.g90}; + margin-bottom: 12px; + line-height: 24px; +`; + +const AccountDeleteFormTextArea = styled.textarea` + width: 100%; + padding: 12px; + border: 1px solid ${({ theme }) => theme.palette.grey.g20}; + border-radius: 4px; + background-color: ${({ theme }) => theme.palette.grey.w}; + color: ${({ theme }) => theme.palette.grey.g90}; + ${({ theme }) => theme.typo.b3}; + + &:placeholder-shown { + border: 1px solid ${({ theme }) => theme.palette.grey.g20}; + color: ${({ theme }) => theme.palette.grey.g30}; + } + + &:focus { + border: 1px solid ${({ theme }) => theme.palette.grey.g90}; + } + + &:disabled { + background: ${({ theme }) => theme.palette.grey.g10}; + border: 1px solid ${({ theme }) => theme.palette.grey.g20}; + color: ${({ theme }) => theme.palette.grey.g40}; + } + + &::placeholder { + color: ${({ theme }) => theme.palette.grey.g30}; + } +`; + +const AccountDeleteFormButtonWrapper = styled.div` + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 32px; +`; + +export default { + AccountDeleteForm, + AccountDeleteFormDescription, + AccountDeleteFormTextArea, + AccountDeleteFormButtonWrapper, +}; diff --git a/apps/admin/src/components/AccountDeleteForm/index.tsx b/apps/admin/src/components/AccountDeleteForm/index.tsx new file mode 100644 index 00000000..f5cb6470 --- /dev/null +++ b/apps/admin/src/components/AccountDeleteForm/index.tsx @@ -0,0 +1,86 @@ +import { Button } from '@boolti/ui'; +import Styled from './AccountDeleteForm.styles'; +import { useForm } from 'react-hook-form'; +import { useDeleteMe, useLogout } from '@boolti/api'; +import { useAuthAtom } from '~/atoms/useAuthAtom'; +import { useNavigate } from 'react-router-dom'; +import { PATH } from '~/constants/routes'; + +export interface AccountDeleteFormInputs { + reason: string; +} + +interface AccountDeleteFormProps { + oauthType?: 'KAKAO' | 'APPLE'; + onClose: () => void; +} + +const AccountDeleteForm = ({ oauthType, onClose }: AccountDeleteFormProps) => { + const navigate = useNavigate(); + + const deleteMeMutation = useDeleteMe(); + const { removeToken } = useAuthAtom(); + const logoutMutation = useLogout({ + onSuccess: () => { + removeToken(); + }, + }); + const { + register, + handleSubmit, + reset, + formState: { isValid }, + } = useForm(); + + const submitHandler = async (data: AccountDeleteFormInputs) => { + let appleIdAuthorizationCode: string | undefined = undefined; + + // TODO: 애플 로그인 시 탈퇴 로직 작성 + if (oauthType === 'APPLE') { + const appleAuthData = await window.AppleID?.auth.signIn(); + + appleIdAuthorizationCode = appleAuthData?.authorization.code; + } + + await deleteMeMutation.mutateAsync({ + reason: data.reason, + appleIdAuthorizationCode, + }); + await logoutMutation.mutateAsync(); + + onClose(); + navigate(PATH.INDEX); + }; + + return ( + + + 삭제 이유를 알려 주세요. 주신 의견 참고하여 더 나은 서비스를 제공하는 불티가 되겠습니다. + + + + + + + + ); +}; + +export default AccountDeleteForm; diff --git a/apps/admin/src/components/AccountInfo/AccountInfo.styles.ts b/apps/admin/src/components/AccountInfo/AccountInfo.styles.ts deleted file mode 100644 index 90096825..00000000 --- a/apps/admin/src/components/AccountInfo/AccountInfo.styles.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { mq_lg } from '@boolti/ui'; -import styled from '@emotion/styled'; - -const Container = styled.div` - display: flex; - flex-direction: column; - align-items: start; - justify-content: center; - border-radius: 8px; - background-color: ${({ theme }) => theme.palette.grey.g00}; - padding: 16px 20px; - margin-top: 20px; - - ${mq_lg} { - padding: 28px 32px; - margin-top: 40px; - } -`; - -const Title = styled.p<{ hasAccountInfo?: boolean }>` - ${({ theme, hasAccountInfo }) => (hasAccountInfo ? theme.typo.b1 : theme.typo.sh1)}; - color: ${({ theme, hasAccountInfo }) => - hasAccountInfo ? theme.palette.grey.g70 : theme.palette.grey.g90}; - margin-bottom: 2px; - - ${mq_lg} { - ${({ theme, hasAccountInfo }) => (hasAccountInfo ? theme.typo.b3 : theme.typo.h1)}; - } -`; - -const Description = styled.span` - ${({ theme }) => theme.typo.b1}; - color: ${({ theme }) => theme.palette.grey.g70}; - margin-bottom: 20px; - - ${mq_lg} { - ${({ theme }) => theme.typo.b3}; - margin-bottom: 24px; - } -`; - -const InfoContainer = styled.div` - display: flex; - justify-content: center; - align-items: center; - margin-bottom: 16px; - - ${mq_lg} { - margin-bottom: 24px; - } -`; - -const AccountText = styled.span` - ${({ theme }) => theme.typo.sh1}; - color: ${({ theme }) => theme.palette.grey.g90}; - margin-right: 8px; - &:last-of-type { - margin-right: 16px; - } - - ${mq_lg} { - ${({ theme }) => theme.typo.h1}; - } -`; - -export default { - Container, - Title, - Description, - AccountText, - InfoContainer, -}; diff --git a/apps/admin/src/components/AccountInfo/index.tsx b/apps/admin/src/components/AccountInfo/index.tsx deleted file mode 100644 index 27e9ae85..00000000 --- a/apps/admin/src/components/AccountInfo/index.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { queryKeys, usePutUserSettlementAccountInfo, useQueryClient } from '@boolti/api'; -import { Button, useDialog, useToast } from '@boolti/ui'; - -import SettlementDialogContent from '../SettlementDialogContent'; -import Styled from './AccountInfo.styles'; - -interface Props { - bankName?: string; - bankAccountNumber?: string; - bankCode?: string; - bankAccountHolder?: string; -} - -const AccountInfo = ({ bankName, bankAccountHolder, bankAccountNumber }: Props) => { - const { open, close } = useDialog(); - const toast = useToast(); - const queryClient = useQueryClient(); - - const putUserSettlementAccountInfoMutation = usePutUserSettlementAccountInfo(); - - return ( - - - 정산 계좌 정보 - - {bankName && bankAccountHolder && bankAccountNumber ? ( - - {bankName} - {bankAccountNumber} - {bankAccountHolder} - - ) : ( - 빠른 정산을 위해서는 정확한 계좌 정보가 필요해요. - )} - - - - ); -}; - -export default AccountInfo; diff --git a/apps/admin/src/components/AuthoritySettingDialogContent/AuthoritySettingDialogContent.styles.ts b/apps/admin/src/components/AuthoritySettingDialogContent/AuthoritySettingDialogContent.styles.ts new file mode 100644 index 00000000..c134d195 --- /dev/null +++ b/apps/admin/src/components/AuthoritySettingDialogContent/AuthoritySettingDialogContent.styles.ts @@ -0,0 +1,7 @@ +import styled from '@emotion/styled'; + +const Container = styled.div``; + +export default { + Container, +}; diff --git a/apps/admin/src/components/AuthoritySettingDialogContent/components/HostInputForm/HostInputForm.styles.ts b/apps/admin/src/components/AuthoritySettingDialogContent/components/HostInputForm/HostInputForm.styles.ts new file mode 100644 index 00000000..08c22296 --- /dev/null +++ b/apps/admin/src/components/AuthoritySettingDialogContent/components/HostInputForm/HostInputForm.styles.ts @@ -0,0 +1,140 @@ +import { Button, mq_lg } from '@boolti/ui'; +import styled from '@emotion/styled'; + +interface InputWrapperProps { + text: string; +} + +interface InputProps { + value: string; +} + +const Form = styled.form` + display: flex; + align-items: center; + margin-bottom: 28px; + margin-top: 20px; + + ${mq_lg} { + margin-top: 0; + } +`; + +const InputWrapper = styled.div` + ${({ theme }) => theme.typo.b3}; + border: 1px solid ${({ text, theme }) => (text ? theme.palette.grey.g90 : theme.palette.grey.g20)}; + border-radius: 4px; + background-color: ${({ theme }) => theme.palette.grey.w}; + padding: 12px; + margin-right: 8px; + flex: auto; + position: relative; + display: flex; + align-items: center; +`; + +const HashTag = styled.span` + color: ${({ theme }) => theme.palette.grey.g90}; + line-height: 24px; + padding-right: 4px; +`; + +const Input = styled.input` + width: ${({ value }) => (value ? 'calc(100% - 80px)' : '100%')}; + line-height: 24px; + + &::placeholder { + color: ${({ theme }) => theme.palette.grey.g30}; + } +`; + +const Dropdown = styled.div` + position: absolute; + right: 12px; + top: 12px; +`; + +const Chip = styled.div` + display: flex; + align-items: center; + justify-content: center; + padding: 0 0 0 6px; + ${({ theme }) => theme.typo.c1}; + border: none; + border-radius: 4px; + background-color: ${({ theme }) => theme.palette.grey.g10}; + color: ${({ theme }) => theme.palette.grey.g60}; + cursor: pointer; + width: 64px; + height: 24px; + margin-left: auto; + + svg { + color: ${({ theme }) => theme.palette.grey.g50}; + } +`; + +const DropdownList = styled.ul` + border-radius: 6px; + border: 1px solid ${({ theme }) => theme.palette.grey.g20}; + background-color: ${({ theme }) => theme.palette.grey.w}; + margin-top: 4px; + box-shadow: 0px 8px 14px 0px #acabab21; +`; + +const DropdownListItem = styled.li` + display: flex; + align-items: center; + justify-content: space-between; + width: 112px; + padding: 7px 12px; + ${({ theme }) => theme.typo.b1}; + color: ${({ theme }) => theme.palette.grey.g70}; + background-color: ${({ theme }) => theme.palette.grey.w}; + cursor: pointer; + + &:hover { + background-color: ${({ theme }) => theme.palette.grey.g10}; + } + + &:first-of-type { + border-top-left-radius: 6px; + border-top-right-radius: 6px; + } + + &:last-child { + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; + } + + svg { + color: ${({ theme }) => theme.palette.grey.g90}; + } +`; + +const InviteButton = styled(Button)` + width: 48px; + height: 48px; + padding: 14px; + + &:disabled { + color: ${({ theme }) => theme.palette.grey.g40}; + } + + ${mq_lg} { + width: auto; + padding: 13px 20px; + } +`; + +export default { + Form, + InputWrapper, + HashTag, + Input, + Dropdown, + DropdownList, + DropdownListItem, + Chip, + InviteButton, +}; diff --git a/apps/admin/src/components/AuthoritySettingDialogContent/components/HostInputForm/index.tsx b/apps/admin/src/components/AuthoritySettingDialogContent/components/HostInputForm/index.tsx new file mode 100644 index 00000000..77c7c66c --- /dev/null +++ b/apps/admin/src/components/AuthoritySettingDialogContent/components/HostInputForm/index.tsx @@ -0,0 +1,106 @@ +import { useState } from 'react'; +import { useDropdown, useToast } from '@boolti/ui'; +import { useAddHost } from '@boolti/api'; +import { CheckIcon, ChevronDownIcon } from '@boolti/icon'; +import { UserAdd } from '@boolti/icon/src/components/UserAdd'; +import { HostType, HostTypeInfo } from '@boolti/api/src/types/host'; +import { CustomError } from '@boolti/api/src/types/error'; +import Styled from './HostInputForm.styles'; +import { useDeviceWidth } from '~/hooks/useDeviceWidth'; +import { useTheme } from '@emotion/react'; +interface HostInputFormProps { + showId: number; +} + +const dropdownItems: HostTypeInfo[] = [ + { + type: HostType.MANAGER, + label: '관리자', + }, + { + type: HostType.SUPPORTER, + label: '도우미', + }, +]; + +const HostInputForm = ({ showId }: HostInputFormProps) => { + const [memberId, setMemberId] = useState(''); + + const [hostItem, setHostItem] = useState(dropdownItems[0]); + const { toggleDropdown, isOpen } = useDropdown(); + const toast = useToast(); + + const { mutateAsync, isLoading } = useAddHost(showId); + + const deviceWidth = useDeviceWidth(); + const theme = useTheme(); + const isMobile = deviceWidth < parseInt(theme.breakpoint.mobile, 10); + + const onSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + await mutateAsync({ + body: { + userCode: memberId, + type: hostItem.type, + }, + }); + toast.success('초대를 완료했습니다.'); + setMemberId(''); + } catch (err: unknown) { + const error = err as CustomError; + if (error.type === 'USER_ALREADY_IN_SHOW_GROUP') { + toast.error('이미 초대된 회원입니다.'); + } else { + toast.error( + '불티에 회원으로 등록된 식별 코드로만 초대가 가능합니다. 식별 코드를 확인 후 다시 시도해 주세요.', + ); + } + } + }; + + const onChange = (e: React.ChangeEvent) => { + setMemberId(e.target.value); + }; + + const onSelect = (type: HostType) => { + const selectedItem = dropdownItems.find((item) => item.type === type); + setHostItem(selectedItem as HostTypeInfo); + toggleDropdown(); + }; + + return ( + + + # + + {memberId && ( + + + {hostItem.label} + + {isOpen && ( + + {dropdownItems.map((item) => ( + onSelect(item.type)}> + {item.label} + {item.type === hostItem.type && } + + ))} + + )} + + )} + + + {isMobile ? : '초대하기'} + + + ); +}; + +export default HostInputForm; diff --git a/apps/admin/src/components/AuthoritySettingDialogContent/components/HostList/HostList.styles.ts b/apps/admin/src/components/AuthoritySettingDialogContent/components/HostList/HostList.styles.ts new file mode 100644 index 00000000..d4cb0742 --- /dev/null +++ b/apps/admin/src/components/AuthoritySettingDialogContent/components/HostList/HostList.styles.ts @@ -0,0 +1,30 @@ +import { mq_lg } from '@boolti/ui'; +import styled from '@emotion/styled'; + +const HostListWrapper = styled.div` + height: calc(100vh - 215px); + + ${mq_lg} { + height: auto; + } +`; + +const HostListTitle = styled.h3` + ${({ theme }) => theme.typo.b3}; + color: ${({ theme }) => theme.palette.grey.g90}; + margin-bottom: 20px; +`; + +const HostList = styled.ul` + &::-webkit-scrollbar { + display: none; + } + max-height: 242px; + overflow-y: scroll; +`; + +export default { + HostListWrapper, + HostListTitle, + HostList, +}; diff --git a/apps/admin/src/components/AuthoritySettingDialogContent/components/HostList/index.tsx b/apps/admin/src/components/AuthoritySettingDialogContent/components/HostList/index.tsx new file mode 100644 index 00000000..cce2447f --- /dev/null +++ b/apps/admin/src/components/AuthoritySettingDialogContent/components/HostList/index.tsx @@ -0,0 +1,86 @@ +import Styled from './HostList.styles'; +import { + HostListItem as IHostListItem, + HostListResponse, + HostType, +} from '@boolti/api/src/types/host'; +import HostListItem from '../HostListItem'; +import { useConfirm, useToast } from '@boolti/ui'; +import { useDeleteHost, useEditHost } from '@boolti/api'; +import { HREF, PATH } from '~/constants/routes'; +import { useNavigate } from 'react-router-dom'; +import { useBodyScrollLock } from '~/hooks/useBodyScrollLock'; + +interface HostListProps { + hosts: HostListResponse; + showId: number; + onCloseDialog: () => void; +} + +const HostList = ({ hosts, showId, onCloseDialog }: HostListProps) => { + const editHostMutation = useEditHost(showId); + const deleteHostMutation = useDeleteHost(showId); + const navigate = useNavigate(); + const confirm = useConfirm(); + const toast = useToast(); + + useBodyScrollLock(); + + const onDelete = async ({ hostName: name, self, hostId }: IHostListItem) => { + const hostName = self ? `${name} 님(나)` : `${name} 님`; + const result = await confirm(`${hostName}의 권한을 삭제하시겠어요?`, { + cancel: '취소하기', + confirm: '삭제하기', + }); + if (!result) return; + deleteHostMutation.mutate({ + hostId, + self, + }); + + toast.success('권한을 삭제했습니다.'); + if (self) { + onCloseDialog(); + navigate(PATH.HOME, { replace: true }); + } + }; + + const onEdit = async ({ hostName: name, self, hostId }: IHostListItem, type: HostType) => { + const hostName = self ? `${name} 님(나)` : `${name} 님`; + const confirmText = + type === HostType.MANAGER + ? `${hostName}의 권한을 관리자로 수정하시겠어요?${'\n'}관리자는 권한 편집이 가능하며, 정산 관리 페이지 이외의 모든 페이지 접근이 가능합니다.` + : `${hostName}의 권한을 도우미로 수정하시겠어요?${'\n'}도우미는 권한 편집이 불가하며, 방문자/입장 관리 페이지만 접근이 가능합니다.`; + const result = await confirm(confirmText, { + cancel: '취소하기', + confirm: '수정하기', + }); + if (!result) return; + editHostMutation.mutate({ + hostId, + body: { + type, + }, + }); + toast.success('권한을 수정했습니다.'); + + if (self && type === HostType.SUPPORTER) { + onCloseDialog(); + navigate(HREF.SHOW_RESERVATION(showId), { replace: true }); + } + }; + + return ( + + 팀원 + + {hosts && + hosts.map((host) => ( + + ))} + + + ); +}; + +export default HostList; diff --git a/apps/admin/src/components/AuthoritySettingDialogContent/components/HostListItem/HostListItem.styles.ts b/apps/admin/src/components/AuthoritySettingDialogContent/components/HostListItem/HostListItem.styles.ts new file mode 100644 index 00000000..c764021d --- /dev/null +++ b/apps/admin/src/components/AuthoritySettingDialogContent/components/HostListItem/HostListItem.styles.ts @@ -0,0 +1,116 @@ +import styled from '@emotion/styled'; + +interface DropdownListItemProps { + isDelete?: boolean; +} + +const HostListItem = styled.li` + display: flex; + align-items: center; + justify-content: space-between; + ${({ theme }) => theme.typo.b3}; + position: relative; + + & + & { + margin-top: 20px; + } +`; + +const HostInfoWrapper = styled.div` + display: flex; + align-items: center; +`; + +const HostImage = styled.img` + width: 36px; + height: 36px; + border: none; + border-radius: 50%; + object-fit: cover; +`; + +const HostName = styled.p` + margin-left: 6px; + margin-right: 4px; + color: ${({ theme }) => theme.palette.grey.g90}; +`; + +const HostSelfLabel = styled.span` + color: ${({ theme }) => theme.palette.grey.g50}; +`; + +const HostType = styled.span` + color: ${({ theme }) => theme.palette.grey.g60}; +`; + +const Dropdown = styled.div` + position: relative; +`; + +const NameButton = styled.button` + max-width: 68px; + display: flex; + align-items: center; + + svg { + width: 24px; + height: 24px; + color: ${({ theme }) => theme.palette.grey.g60}; + } +`; + +const Name = styled.span` + color: ${({ theme }) => theme.palette.grey.g60}; +`; + +const DropdownList = styled.ul` + position: fixed; + border-radius: 6px; + border: 1px solid ${({ theme }) => theme.palette.grey.g20}; + background-color: ${({ theme }) => theme.palette.grey.w}; + margin-top: 4px; + margin-left: -64px; + z-index: 1; +`; + +const DropdownListItem = styled.li` + display: flex; + align-items: center; + justify-content: space-between; + width: 112px; + padding: 7px 12px; + ${({ theme }) => theme.typo.b1}; + color: ${({ isDelete, theme }) => + isDelete ? theme.palette.status.error : theme.palette.grey.g70}; + background-color: ${({ theme }) => theme.palette.grey.w}; + cursor: pointer; + margin-top: ${({ isDelete }) => (isDelete ? '4px' : '0')}; + + &:hover { + background-color: ${({ theme }) => theme.palette.grey.g10}; + } + + &:first-of-type { + border-top-left-radius: 6px; + border-top-right-radius: 6px; + } + + &:last-child { + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; + } +`; + +export default { + HostListItem, + HostInfoWrapper, + HostImage, + HostName, + HostSelfLabel, + HostType, + Dropdown, + NameButton, + Name, + DropdownList, + DropdownListItem, +}; diff --git a/apps/admin/src/components/AuthoritySettingDialogContent/components/HostListItem/index.tsx b/apps/admin/src/components/AuthoritySettingDialogContent/components/HostListItem/index.tsx new file mode 100644 index 00000000..59ee42e8 --- /dev/null +++ b/apps/admin/src/components/AuthoritySettingDialogContent/components/HostListItem/index.tsx @@ -0,0 +1,119 @@ +import { useDropdown } from '@boolti/ui'; +import Styled from './HostListItem.styles'; +import { HostListItem as IHostListItem, HostType, HostTypeInfo } from '@boolti/api/src/types/host'; +import { CheckIcon, ChevronDownIcon } from '@boolti/icon'; +import { useAtom } from 'jotai'; +import { myHostInfoAtom } from '~/components/ShowDetailLayout'; + +interface HostListItemProps { + host: IHostListItem; + onDelete: (host: IHostListItem) => void; + onEdit: (host: IHostListItem, type: HostType) => void; +} + +const ProfileSVG = () => ( + + + + + + + + + + +); + +const dropdownItems: HostTypeInfo[] = [ + { + type: HostType.MANAGER, + label: '관리자', + }, + { + type: HostType.SUPPORTER, + label: '도우미', + }, +]; + +const HostListItem = ({ host, onEdit, onDelete }: HostListItemProps) => { + const { isOpen, toggleDropdown } = useDropdown(); + const [myHostInfo] = useAtom(myHostInfoAtom); + + const getHostTypeName = (type: HostType) => { + switch (type) { + case HostType.MAIN: + return '주최자'; + case HostType.MANAGER: + return '관리자'; + case HostType.SUPPORTER: + return '도우미'; + } + }; + + const handleEdit = async (host: IHostListItem, type: HostType) => { + if (host.type === type) { + return; + } + try { + onEdit(host, type); + toggleDropdown(); + } catch (error) { + console.log(error); + } + }; + + const handleDelete = (host: IHostListItem) => { + try { + onDelete(host); + toggleDropdown(); + } catch (error) { + console.log(error); + } + }; + + return ( + + + {host.imagePath ? ( + + ) : ( + + )} + {host.hostName} + {host.self && (나)} + + + { + if (host.type === HostType.MAIN || myHostInfo?.type === HostType.SUPPORTER) return; + toggleDropdown(); + }} + > + {getHostTypeName(host.type)} + {host.type !== HostType.MAIN && } + + {isOpen && ( + + {dropdownItems.map((item) => ( + handleEdit(host, item.type)} key={item.type}> + {item.label} + {host.type === item.type && } + + ))} + handleDelete(host)}> + 삭제하기 + + + )} + + + ); +}; + +export default HostListItem; diff --git a/apps/admin/src/components/AuthoritySettingDialogContent/index.tsx b/apps/admin/src/components/AuthoritySettingDialogContent/index.tsx new file mode 100644 index 00000000..50ea1cb2 --- /dev/null +++ b/apps/admin/src/components/AuthoritySettingDialogContent/index.tsx @@ -0,0 +1,22 @@ +import Styled from './AuthoritySettingDialogContent.styles'; +import { useHostList } from '@boolti/api'; +import HostInputForm from './components/HostInputForm'; +import HostList from './components/HostList'; + +interface AuthoritySettingDialogContentProps { + showId: number; + onClose: () => void; +} + +const AuthoritySettingDialogContent = ({ showId, onClose }: AuthoritySettingDialogContentProps) => { + const { data: hosts } = useHostList(showId); + + return ( + + + + + ); +}; + +export default AuthoritySettingDialogContent; diff --git a/apps/admin/src/components/EntranceConfirmDialogContent/EntranceConfirmDialogContent.styles.ts b/apps/admin/src/components/EntranceConfirmDialogContent/EntranceConfirmDialogContent.styles.ts index a8e89f7b..f2572c7c 100644 --- a/apps/admin/src/components/EntranceConfirmDialogContent/EntranceConfirmDialogContent.styles.ts +++ b/apps/admin/src/components/EntranceConfirmDialogContent/EntranceConfirmDialogContent.styles.ts @@ -2,7 +2,9 @@ import { mq_lg } from '@boolti/ui'; import styled from '@emotion/styled'; const Container = styled.div` + position: relative; height: 100vh; + width: 100%; padding: 40px 0 60px 0; overflow-y: auto; &::-webkit-scrollbar { @@ -11,12 +13,13 @@ const Container = styled.div` ${mq_lg} { padding: 0; + width: auto; height: 60vh; } `; const MobileHeader = styled.div` - position: absolute; + position: fixed; background: ${({ theme }) => theme.palette.grey.w}; top: 0; left: 0; @@ -55,7 +58,10 @@ const CloseButton = styled.button` const Title = styled.h2` ${({ theme }) => theme.typo.h1}; color: ${({ theme }) => theme.palette.grey.g90}; - margin-bottom: 2px; + margin: 52px 0 2px; + ${mq_lg} { + margin-top: 0; + } `; const Description = styled.h2` diff --git a/apps/admin/src/components/Layout/Layout.styles.ts b/apps/admin/src/components/Layout/Layout.styles.ts index f0b94af2..d08019b7 100644 --- a/apps/admin/src/components/Layout/Layout.styles.ts +++ b/apps/admin/src/components/Layout/Layout.styles.ts @@ -11,6 +11,7 @@ const HeaderContainer = styled.div` `; const Header = styled.header` + background-color: ${({ theme }) => theme.palette.grey.w}; max-width: ${({ theme }) => theme.breakpoint.desktop}; margin: 0 auto; diff --git a/apps/admin/src/components/Layout/index.tsx b/apps/admin/src/components/Layout/index.tsx index 6a470788..a865f03b 100644 --- a/apps/admin/src/components/Layout/index.tsx +++ b/apps/admin/src/components/Layout/index.tsx @@ -3,17 +3,26 @@ import Styled from './Layout.styles'; interface LayoutProps { children: React.ReactNode; header?: React.ReactNode; + headerMenu?: React.ReactNode; banner?: React.ReactNode; layoutStyle?: React.CSSProperties; headerContainerStyle?: React.CSSProperties; } -const Layout = ({ children, header, banner, layoutStyle, headerContainerStyle }: LayoutProps) => { +const Layout = ({ + children, + header, + headerMenu, + banner, + layoutStyle, + headerContainerStyle, +}: LayoutProps) => { return ( {header && ( {header} + {headerMenu} )} {banner && ( diff --git a/apps/admin/src/components/MobileCardList/MobileCardList.style.ts b/apps/admin/src/components/MobileCardList/MobileCardList.style.ts index 007cd60d..5c9f4aff 100644 --- a/apps/admin/src/components/MobileCardList/MobileCardList.style.ts +++ b/apps/admin/src/components/MobileCardList/MobileCardList.style.ts @@ -8,6 +8,7 @@ const Container = styled.div<{ isEmpty: boolean }>` align-items: center; white-space: pre-wrap; padding: 0 20px; + border-radius: 8px; margin: 12px 0 24px 0; min-height: ${({ isEmpty }) => (isEmpty ? '240px' : 'auto')}; text-align: center; diff --git a/apps/admin/src/components/ProfileDropdown/ProfileDropdown.styles.ts b/apps/admin/src/components/ProfileDropdown/ProfileDropdown.styles.ts index 637ad579..85edcad9 100644 --- a/apps/admin/src/components/ProfileDropdown/ProfileDropdown.styles.ts +++ b/apps/admin/src/components/ProfileDropdown/ProfileDropdown.styles.ts @@ -1,18 +1,24 @@ +import { mq_lg } from '@boolti/ui'; import styled from '@emotion/styled'; const DropdownContainer = styled.div` - width: 66px; height: 36px; display: flex; align-items: center; cursor: pointer; position: relative; + user-select: none; `; const UserProfileImageWrapper = styled.div` - width: 36px; - height: 36px; + width: 28px; + height: 28px; margin-right: 6px; + + ${mq_lg} { + width: 36px; + height: 36px; + } `; const UserProfileImage = styled.img` @@ -26,15 +32,30 @@ const DropdownMenuWrapper = styled.div` position: absolute; top: 42px; right: 0; - width: 96px; - height: 48px; + width: 112px; background-color: ${({ theme }) => theme.palette.grey.w}; box-shadow: 0px 8px 14px 0px #8b8b8b26; - border-radius: 4px; - border: 1px solid transparent; + border-radius: 6px; + border: 1px solid ${({ theme }) => theme.palette.grey.g20}; display: flex; - align-items: center; justify-content: center; + flex-direction: column; + z-index: 1; +`; + +const DropdownMenuItemButton = styled.button` + ${({ theme }) => theme.typo.b1}; + color: ${({ theme }) => theme.palette.grey.g70}; + height: 36px; + padding: 0 12px; + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + + &:hover { + background-color: ${({ theme }) => theme.palette.grey.g10}; + } `; export default { @@ -42,4 +63,5 @@ export default { UserProfileImageWrapper, UserProfileImage, DropdownMenuWrapper, + DropdownMenuItemButton, }; diff --git a/apps/admin/src/components/ProfileDropdown/index.tsx b/apps/admin/src/components/ProfileDropdown/index.tsx index 424a7f12..e2858359 100644 --- a/apps/admin/src/components/ProfileDropdown/index.tsx +++ b/apps/admin/src/components/ProfileDropdown/index.tsx @@ -1,16 +1,19 @@ import { useLogout } from '@boolti/api'; -import { ChevronDownIcon, ChevronUpIcon } from '@boolti/icon'; -import { TextButton } from '@boolti/ui'; -import { useDropdown } from '@boolti/ui/src/hooks'; +import { ChevronDownIcon, ChevronUpIcon, LogoutIcon, SettingIcon } from '@boolti/icon'; +import { useConfirm, useDialog, useDropdown } from '@boolti/ui/src/hooks'; import { useNavigate } from 'react-router-dom'; import { PATH } from '~/constants/routes'; import Styled from './ProfileDropdown.styles'; import { useAuthAtom } from '~/atoms/useAuthAtom'; +import SettingDialogContent from '../SettingDialogContent'; interface ProfileDropdownProps { image?: string; + open?: boolean; + disabledDropdown?: boolean; + onClick?: () => void; } // TODO: UserProfile svg 공통화 @@ -33,7 +36,7 @@ const ProfileSVG = () => ( ); -const ProfileDropdown = ({ image }: ProfileDropdownProps) => { +const ProfileDropdown = ({ image, open, disabledDropdown, onClick }: ProfileDropdownProps) => { const { isOpen, toggleDropdown } = useDropdown(); const { removeToken } = useAuthAtom(); const logoutMutation = useLogout({ @@ -42,26 +45,56 @@ const ProfileDropdown = ({ image }: ProfileDropdownProps) => { }, }); const navigate = useNavigate(); + const confirm = useConfirm(); + const settingDialog = useDialog(); + + const dropdownOpen = open ?? isOpen; return ( - toggleDropdown()}> + { + onClick?.(); + + if (disabledDropdown) return; + + toggleDropdown(); + }} + > {image ? : } - {isOpen ? : } - {isOpen && ( + {dropdownOpen ? : } + {dropdownOpen && !disabledDropdown && ( - { + settingDialog.open({ + title: '설정', + content: , + isAuto: true, + contentPadding: '0', + mobileType: 'fullPage', + }); + }} + > + 설정 + + { - await logoutMutation.mutateAsync(); - navigate(PATH.INDEX, { replace: true }); + const result = await confirm('로그아웃 할까요?', { + cancel: '취소하기', + confirm: '로그아웃', + }); + if (result) { + await logoutMutation.mutateAsync(); + navigate(PATH.INDEX, { replace: true }); + } }} > - 로그아웃 - + 로그아웃 + )} diff --git a/apps/admin/src/components/SettingDialogContent/SettingDialogContent.styles.ts b/apps/admin/src/components/SettingDialogContent/SettingDialogContent.styles.ts new file mode 100644 index 00000000..c0cd538b --- /dev/null +++ b/apps/admin/src/components/SettingDialogContent/SettingDialogContent.styles.ts @@ -0,0 +1,169 @@ +import { mq_lg } from '@boolti/ui'; +import { css } from '@emotion/react'; +import styled from '@emotion/styled'; + +interface SettingMenuItemButtonProps { + active?: boolean; +} + +const SettingDialogContent = styled.div` + height: calc(100vh - 48px); + display: flex; + + ${mq_lg} { + width: 600px; + height: 542px; + } +`; + +const SettingMenuWrapper = styled.aside` + width: 150px; + flex-shrink: 0; + display: none; + flex-direction: column; + justify-content: space-between; + border-right: 1px solid ${({ theme }) => theme.palette.grey.g30}; + padding: 24px 20px 28px; + + ${mq_lg} { + display: flex; + } +`; + +const SettingMenu = styled.div` + display: flex; + flex-direction: column; + gap: 8px; +`; + +const SettingMenuItemButton = styled.button` + ${({ theme, active }) => + active + ? css` + ${theme.typo.sh1}; + background-color: ${theme.palette.grey.g10}; + color: ${theme.palette.grey.g90}; + ` + : css` + ${theme.typo.b3}; + background-color: ${theme.palette.grey.w}; + color: ${theme.palette.grey.g70}; + `} + height: 40px; + padding: 0 12px; + border-radius: 4px; + cursor: pointer; + + &:hover { + ${({ theme }) => theme.typo.sh1}; + background-color: ${({ theme }) => theme.palette.grey.g10}; + color: ${({ theme }) => theme.palette.grey.g90}; + } +`; + +const SettingMenuBottomLogo = styled.div` + display: flex; + justify-content: center; +`; + +const SettingContent = styled.div` + flex: 1; + padding: 16px 0; + + ${mq_lg} { + padding: 24px 32px; + } +`; + +const SettingContentTitle = styled.h3` + display: none; + ${({ theme }) => theme.typo.h1}; + color: ${({ theme }) => theme.palette.grey.g90}; + margin-bottom: 24px; + + ${mq_lg} { + display: block; + } +`; + +const SettingContentFormControl = styled.div` + margin-bottom: 24px; + + div { + width: 100%; + } +`; + +const Label = styled.label` + ${({ theme }) => theme.typo.b3}; + color: ${({ theme }) => theme.palette.grey.g90}; + margin-bottom: 8px; + display: block; +`; + +const ConnectedServiceList = styled.div` + display: flex; + justify-content: flex-start; + align-items: center; + gap: 8px; + width: 100%; + flex-wrap: wrap; + + div { + width: auto; + } +`; + +const ConnectedServiceChip = styled.div` + display: inline-flex; + justify-content: center; + align-items: center; + gap: 12px; + border: 1px solid ${({ theme }) => theme.palette.grey.g20}; + border-radius: 100px; + padding-top: 6px; + padding-right: 12px; + padding-bottom: 6px; + padding-left: 6px; + color: ${({ theme }) => theme.palette.grey.g90}; + ${({ theme }) => theme.typo.b3}; +`; + +const Divider = styled.hr` + border-top: 1px solid ${({ theme }) => theme.palette.grey.g20}; + margin: 24px 0; +`; + +const SettingSubtitle = styled.h4` + ${({ theme }) => theme.typo.b3}; + color: ${({ theme }) => theme.palette.grey.g90}; + margin-bottom: 4px; +`; + +const SettingDescriptionList = styled.ul` + ${({ theme }) => theme.typo.b1}; + color: ${({ theme }) => theme.palette.grey.g60}; + list-style: disc; + padding-left: 16px; + margin-bottom: 12px; +`; + +const SettingDescriptionItem = styled.li``; + +export default { + SettingDialogContent, + SettingMenuWrapper, + SettingMenu, + SettingMenuItemButton, + SettingMenuBottomLogo, + SettingContent, + SettingContentTitle, + SettingContentFormControl, + Label, + ConnectedServiceList, + ConnectedServiceChip, + Divider, + SettingSubtitle, + SettingDescriptionList, + SettingDescriptionItem, +}; diff --git a/apps/admin/src/components/SettingDialogContent/index.tsx b/apps/admin/src/components/SettingDialogContent/index.tsx new file mode 100644 index 00000000..56d2c92a --- /dev/null +++ b/apps/admin/src/components/SettingDialogContent/index.tsx @@ -0,0 +1,113 @@ +import { BooltiLightGrey } from '@boolti/icon'; +import Styled from './SettingDialogContent.styles'; +import { Button, TextField, useDialog } from '@boolti/ui'; +import AccountDeleteForm from '../AccountDeleteForm'; +import { useUserSummary } from '@boolti/api'; + +const KakaoIcon = () => { + return ( + + + + + ); +}; + +const AppleIcon = () => { + return ( + + + + + ); +}; + +const SettingDialogContent = () => { + const accountDeleteDialog = useDialog(); + + const { data: userSummary } = useUserSummary(); + + return ( + + + + + 계정 + + + + + + + + 계정 + + 식별 코드 + { + event.preventDefault(); + }} + /> + + + 연결 서비스 + + {userSummary?.oauthType === 'KAKAO' && ( + + 카카오톡 + + )} + {userSummary?.oauthType === 'APPLE' && ( + + Apple + + )} + + + + 계정 삭제 + + + 주최한 공연 정보는 사라지지 않아요. + + + 예매한 티켓은 전부 사라지며 복구할 수 없어요. + + + 삭제일로 부터 30일 이내 재 로그인 시 삭제를 취소할 수 있어요. + + + + + + ); +}; + +export default SettingDialogContent; diff --git a/apps/admin/src/components/ShowDetailLayout/ShowDetailLayout.styles.ts b/apps/admin/src/components/ShowDetailLayout/ShowDetailLayout.styles.ts index a3ddcfe8..36678658 100644 --- a/apps/admin/src/components/ShowDetailLayout/ShowDetailLayout.styles.ts +++ b/apps/admin/src/components/ShowDetailLayout/ShowDetailLayout.styles.ts @@ -1,4 +1,4 @@ -import { mq_lg } from '@boolti/ui'; +import { Button, mq_lg } from '@boolti/ui'; import styled from '@emotion/styled'; interface TabItemProps { @@ -54,6 +54,12 @@ const HeaderContent = styled.div` left: 0; `; +const ShowNameWrapper = styled.div` + display: flex; + align-items: center; + justify-content: space-between; +`; + const ShowName = styled.h2` ${({ theme }) => theme.typo.h1}; margin: 12px 0 8px; @@ -61,6 +67,11 @@ const ShowName = styled.h2` transition: font-size 0.1s ease-in-out, margin 0.1s ease-in-out; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + overflow: hidden; + text-overflow: ellipsis; ${mq_lg} { padding: 0; @@ -75,6 +86,9 @@ const TabContainer = styled.div` overflow-x: auto; display: flex; flex-wrap: nowrap; + &::-webkit-scrollbar { + display: none; + } ${mq_lg} { padding: 0; @@ -98,7 +112,10 @@ const TabItem = styled.div` justify-content: center; align-items: center; height: 48px; - ${({ active }) => active && `font-weight: 600;`}; + ${({ active, theme }) => + active + ? `font-weight: 600; color: ${theme.palette.grey.g90};` + : `color: ${theme.palette.grey.g70};`}; cursor: pointer; &::after { @@ -124,6 +141,28 @@ const TabItem = styled.div` } `; +const AuthorSettingButton = styled(Button)` + ${({ theme }) => theme.typo.sh1}; + color: ${({ theme }) => theme.palette.grey.g90}; + background-color: ${({ theme }) => theme.palette.grey.w}; + border: none; + margin-right: 6px; + + path { + stroke: ${({ theme }) => theme.palette.grey.g90}; + } + + ${mq_lg} { + margin-right: 0; + background-color: ${({ theme }) => theme.palette.grey.g90}; + color: ${({ theme }) => theme.palette.grey.w}; + + path { + stroke: ${({ theme }) => theme.palette.grey.w}; + } + } +`; + export default { HeaderLeft, BackButton, @@ -131,8 +170,10 @@ export default { TopObserver, HeaderObserver, HeaderContent, + ShowNameWrapper, ShowName, TabContainer, Tab, TabItem, + AuthorSettingButton, }; diff --git a/apps/admin/src/components/ShowDetailLayout/index.tsx b/apps/admin/src/components/ShowDetailLayout/index.tsx index 9d554b0d..6abfd74f 100644 --- a/apps/admin/src/components/ShowDetailLayout/index.tsx +++ b/apps/admin/src/components/ShowDetailLayout/index.tsx @@ -1,6 +1,12 @@ -import { useLogout, useShowLastSettlementEvent, useShowSettlementInfo } from '@boolti/api'; +import { + useLogout, + useMyHostInfo, + useShowLastSettlementEvent, + useShowSettlementInfo, +} from '@boolti/api'; import { ArrowLeftIcon } from '@boolti/icon'; -import { TextButton } from '@boolti/ui'; +import { Setting } from '@boolti/icon/src/components/Setting.tsx'; +import { TextButton, useDialog } from '@boolti/ui'; import { useTheme } from '@emotion/react'; import { useInView } from 'react-intersection-observer'; import { useMatch, useNavigate, useParams } from 'react-router-dom'; @@ -12,6 +18,11 @@ import Header from '../Header/index.tsx'; import Layout from '../Layout/index.tsx'; import Styled from './ShowDetailLayout.styles.ts'; import { useAuthAtom } from '~/atoms/useAuthAtom.ts'; +import AuthoritySettingDialogContent from '../AuthoritySettingDialogContent'; +import { HostListItem, HostType } from '@boolti/api/src/types/host.ts'; +import { atom, useAtom } from 'jotai'; +import { useEffect } from 'react'; +import { useDeviceWidth } from '~/hooks/useDeviceWidth.ts'; const settlementTooltipText = { SEND: '내역서 확인 및 정산 요청을 진행해 주세요', @@ -26,6 +37,8 @@ interface ShowDetailLayoutProps { onClickMiddleware?: () => Promise; } +export const myHostInfoAtom = atom(null); + const ShowDetailLayout = ({ showName, children, onClickMiddleware }: ShowDetailLayoutProps) => { const { ref: topObserverRef, inView: topInView } = useInView({ threshold: 1, @@ -44,9 +57,14 @@ const ShowDetailLayout = ({ showName, children, onClickMiddleware }: ShowDetailL const matchReservationTab = useMatch(PATH.SHOW_RESERVATION); const matchEntryTab = useMatch(PATH.SHOW_ENTRANCE); const matchSettlementTab = useMatch(PATH.SHOW_SETTLEMENT); - - const { data: lastSettlementEvent } = useShowLastSettlementEvent(Number(params!.showId)); - const { data: settlementInfo } = useShowSettlementInfo(Number(params!.showId)); + const authoritySettingDialog = useDialog(); + const showId = Number(params!.showId); + const [, setMyHostInfo] = useAtom(myHostInfoAtom); + const deviceWidth = useDeviceWidth(); + const isMobile = deviceWidth < parseInt(theme.breakpoint.mobile, 10); + const { data: myHostInfoData } = useMyHostInfo(showId); + const { data: lastSettlementEvent } = useShowLastSettlementEvent(showId); + const { data: settlementInfo } = useShowSettlementInfo(showId); const logoutMutation = useLogout({ onSuccess: () => { removeToken(); @@ -87,6 +105,12 @@ const ShowDetailLayout = ({ showName, children, onClickMiddleware }: ShowDetailL return false; })(); + useEffect(() => { + if (myHostInfoData) { + setMyHostInfo({ ...myHostInfoData }); + } + }, [myHostInfoData, setMyHostInfo]); + return ( <> @@ -107,7 +131,7 @@ const ShowDetailLayout = ({ showName, children, onClickMiddleware }: ShowDetailL }} > - 주최자 홈 + } @@ -129,7 +153,31 @@ const ShowDetailLayout = ({ showName, children, onClickMiddleware }: ShowDetailL } /> - {showName} + + {showName} + {myHostInfoData?.type !== HostType.SUPPORTER && ( + { + authoritySettingDialog.open({ + title: '권한 설정', + width: '600px', + content: ( + + ), + }); + }} + > + + {!isMobile && 권한 설정} + + )} + theme.typo.b3}; + color: ${({ theme }) => theme.palette.grey.g60}; + text-align: center; + width: 200px; + white-space: pre-wrap; + + ${mq_lg} { + width: auto; + white-space: normal; + ${({ theme }) => theme.typo.b4}; + } +`; + +const DescriptionBox = styled.div` + padding: 20px 24px; + ${({ theme }) => theme.typo.b1}; + color: ${({ theme }) => theme.palette.grey.g60}; + background-color: ${({ theme }) => theme.palette.grey.g00}; + border: 1px solid ${({ theme }) => theme.palette.grey.g20}; + border-radius: 4px; + text-align: center; + margin: 32px 0 0 0; + width: calc(100% - 40px); + white-space: pre; + + strong { + ${({ theme }) => theme.typo.sh0}; + color: ${({ theme }) => theme.palette.grey.g90}; + } + + ${mq_lg} { + margin: 40px 0 0 0; + width: 600px; + } +`; + +export default { + Container, + Title, + DescriptionBox, +}; diff --git a/apps/admin/src/components/ShowDetailUnauthorized/index.tsx b/apps/admin/src/components/ShowDetailUnauthorized/index.tsx new file mode 100644 index 00000000..f015e607 --- /dev/null +++ b/apps/admin/src/components/ShowDetailUnauthorized/index.tsx @@ -0,0 +1,34 @@ +import { HostType } from '@boolti/api/src/types/host'; +import Styled from './ShowDetailUnauthorized.styles'; +import { BooltiGreyIcon } from '@boolti/icon/src/components/BooltiGreyIcon'; + +interface ShowDetailUnauthorizedProps { + pageName: string; + name: string; + type: HostType; +} + +const ShowDetailUnauthorized = ({ pageName, name, type }: ShowDetailUnauthorizedProps) => { + const isSupporter = type === HostType.SUPPORTER; + const hostTypeName = isSupporter ? '도우미' : '관리자'; + const descriptionText = isSupporter + ? '주최자, 관리자만 접근 가능합니다.' + : '주최자만 접근 가능합니다.'; + return ( + + + + {pageName} 페이지에 대한{'\n'}접근 권한이 없어요 + + + 현재 {name} 님의 권한은 {hostTypeName} 입니다. + {'\n'} + {pageName}는 {descriptionText} + {'\n'} + {isSupporter && '이 페이지를 보시려면 주최자에게 권한을 요청해 주세요.'} + + + ); +}; + +export default ShowDetailUnauthorized; diff --git a/apps/admin/src/components/ShowInfoFormContent/ShowBasicInfoFormContent.tsx b/apps/admin/src/components/ShowInfoFormContent/ShowBasicInfoFormContent.tsx index a6c809a8..bf129806 100644 --- a/apps/admin/src/components/ShowInfoFormContent/ShowBasicInfoFormContent.tsx +++ b/apps/admin/src/components/ShowInfoFormContent/ShowBasicInfoFormContent.tsx @@ -59,11 +59,12 @@ const ShowBasicInfoFormContent = ({ {MAX_IMAGE_COUNT}장 업로드 가능 / jpg, png 형식) - {imageFiles.map((file) => ( - + {imageFiles.map((file, index) => ( + + {!disabled && ( )} - + {index === 0 && 대표 사진} + ))} {imageFiles.length < MAX_IMAGE_COUNT && !disabled && ( diff --git a/apps/admin/src/components/ShowInfoFormContent/ShowDetailInfoFormContent.tsx b/apps/admin/src/components/ShowInfoFormContent/ShowDetailInfoFormContent.tsx index 2af07773..f6e3dd25 100644 --- a/apps/admin/src/components/ShowInfoFormContent/ShowDetailInfoFormContent.tsx +++ b/apps/admin/src/components/ShowInfoFormContent/ShowDetailInfoFormContent.tsx @@ -90,7 +90,7 @@ const ShowDetailInfoFormContent = ({ form, disabled }: ShowDetailInfoFormContent - 대표 연락처 + 대표자 연락처 ` max-width: 100%; - height: 100%; - border-radius: 4px; - border: 1px solid ${({ theme }) => theme.palette.grey.g20}; - background-size: contain; + height: ${({ isFirstImage }) => (isFirstImage ? 'calc(124px - 16px)' : '124px')}; + width: 100%; + background-size: cover; background-repeat: no-repeat; background-position: center; aspect-ratio: 182 / 256; - &:first-of-type::after { - content: '대표 사진'; - font-size: 8px; - font-weight: 600; - line-height: 8px; - background-color: ${({ theme }) => theme.palette.primary.o1}; - color: ${({ theme }) => theme.palette.grey.w}; - width: 100%; - height: 16px; - display: flex; - justify-content: center; - align-items: center; - position: absolute; - bottom: 0; - left: 0; + ${mq_lg} { + height: ${({ isFirstImage }) => (isFirstImage ? 'calc(256px - 32px)' : '100%')}; } +`; + +const PreviewImageWrap = styled.div<{ isFirstImage: boolean }>` + position: relative; + border-radius: 4px; + border: 1px solid ${({ theme }) => theme.palette.grey.g20}; +`; + +const FirstImageText = styled.span` + font-size: 8px; + font-weight: 600; + line-height: 8px; + background-color: ${({ theme }) => theme.palette.primary.o1}; + color: ${({ theme }) => theme.palette.grey.w}; + width: 100%; + height: 16px; + display: flex; + justify-content: center; + align-items: center; + margin-top: auto; ${mq_lg} { - &:first-of-type::after { - font-size: 14px; - line-height: 18px; - height: 32px; - } + font-size: 14px; + line-height: 18px; + height: 32px; } `; const PreviewImageDeleteButton = styled.button` position: absolute; - top: -10px; - right: -10px; + top: -6px; + right: -6px; background-color: ${({ theme }) => theme.palette.grey.g90}; opacity: 0.8; border: none; @@ -255,7 +257,7 @@ const TextAreaContainer = styled.div` const TextArea = styled.textarea` width: 100%; - margin-top: 8px; + margin-top: 16px; padding: 12px; border: 1px solid ${({ theme, hasError }) => @@ -279,6 +281,10 @@ const TextArea = styled.textarea` border: 1px solid ${({ theme }) => theme.palette.grey.g20}; color: ${({ theme }) => theme.palette.grey.g40}; } + + ${mq_lg} { + margin-top: 8px; + } `; const TextAreaErrorMessage = styled.p` @@ -507,7 +513,7 @@ const MobileTicketAction = styled.div` width: 24px; height: 24px; stroke: ${({ theme, disabled }) => - disabled ? theme.palette.grey.g70 : theme.palette.grey.g90}; + disabled ? theme.palette.grey.g40 : theme.palette.grey.g90}; } } } @@ -527,6 +533,8 @@ export default { ShowInfoFormButtonContainer, ShowInfoFormButton, PreviewImageContainer, + PreviewImageWrap, + FirstImageText, PreviewImage, PreviewImageDeleteButton, FileUploadArea, diff --git a/apps/admin/src/components/ShowInfoFormContent/ShowInvitationTicketFormContent.tsx b/apps/admin/src/components/ShowInfoFormContent/ShowInvitationTicketFormContent.tsx index c089efea..3f409067 100644 --- a/apps/admin/src/components/ShowInfoFormContent/ShowInvitationTicketFormContent.tsx +++ b/apps/admin/src/components/ShowInfoFormContent/ShowInvitationTicketFormContent.tsx @@ -105,7 +105,7 @@ const ShowInvitationTicketFormContent = ({ 재고 {ticket.quantity}/{ticket.totalForSale} - 1인당 1매 + 1인 1매 - - - - - - { - 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'), - }} - hasNoticePage - /> - - - - - - + + + + + + + + + + + { + 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'), + }} + hasNoticePage + /> + + + + + + { + setPreviewDrawerOpen(false); + }} + > + 닫기 + + { + showInfoForm.handleSubmit(onSubmit)(); + }} + > + 저장하기 + + + + + + + )} ); }; diff --git a/apps/admin/src/pages/ShowReservationPage/ShowReservationPage.styles.ts b/apps/admin/src/pages/ShowReservationPage/ShowReservationPage.styles.ts index 506edbd6..ac8a3a47 100644 --- a/apps/admin/src/pages/ShowReservationPage/ShowReservationPage.styles.ts +++ b/apps/admin/src/pages/ShowReservationPage/ShowReservationPage.styles.ts @@ -3,20 +3,36 @@ import styled from '@emotion/styled'; const Container = styled.div` padding: 0 20px; - margin: 40px 0 68px; + margin: 40px 0 32px; + + ${mq_lg} { + margin: 40px 0 68px; + } `; -const Empty = styled.div` - margin: 0 auto; - width: 1080px; - height: 770px; +const EmptyContainer = styled.div` display: flex; - justify-content: center; + flex-direction: column; align-items: center; - white-space: pre-wrap; + padding: 60px 0; + + ${mq_lg} { + padding: 100px 0; + } +`; + +const EmptyTitle = styled.p` + ${({ theme }) => theme.typo.b3}; + color: ${({ theme }) => theme.palette.grey.g60}; + margin-top: 16px; text-align: center; - ${({ theme }) => theme.typo.b4}; - color: ${({ theme }) => theme.palette.grey.g40}; + width: 200px; + white-space: pre-wrap; + + ${mq_lg} { + width: auto; + ${({ theme }) => theme.typo.b4}; + } `; const TicketSummaryContainer = styled.div` @@ -144,9 +160,9 @@ const InputContainer = styled.div` const Input = styled.input` display: flex; - width: 100%; + width: 180px; max-width: 262px; - padding: 8px 72px 8px 16px; + padding: 8px 32px 8px 16px; justify-content: space-between; align-items: center; border-radius: 100px; @@ -162,6 +178,11 @@ const Input = styled.input` &:placeholder-shown { color: ${({ theme }) => theme.palette.grey.g30}; } + + ${mq_lg} { + width: 100%; + padding: 8px 72px 8px 16px; + } `; const ButtonContainer = styled.div` @@ -210,5 +231,6 @@ export default { InputButton, ButtonContainer, FilterContainer, - Empty, + EmptyContainer, + EmptyTitle, }; diff --git a/apps/admin/src/pages/ShowReservationPage/index.tsx b/apps/admin/src/pages/ShowReservationPage/index.tsx index 3a48aba6..4d3456e6 100644 --- a/apps/admin/src/pages/ShowReservationPage/index.tsx +++ b/apps/admin/src/pages/ShowReservationPage/index.tsx @@ -15,6 +15,9 @@ import ShowDetailLayout from '~/components/ShowDetailLayout'; import TicketTypeSelect from '~/components/TicketTypeSelect'; import Styled from './ShowReservationPage.styles'; +import { useDeviceWidth } from '~/hooks/useDeviceWidth'; +import { useTheme } from '@emotion/react'; +import { BooltiGreyIcon } from '@boolti/icon/src/components/BooltiGreyIcon'; const emptyLabel: Record = { COMPLETE: '발권 왼료된 티켓이 없어요.', @@ -32,6 +35,11 @@ const ShowReservationPage = () => { const showId = Number(params!.showId); const [currentPage, setCurrentPage] = useState(0); + + const deviceWidth = useDeviceWidth(); + const theme = useTheme(); + const isMobile = deviceWidth < parseInt(theme.breakpoint.mobile, 10); + const { data: show } = useShowDetail(showId); const { data: reservationSummary } = useShowReservationSummary(showId); const { data: reservationData, isLoading: isReservationPagesLoading } = useShowReservations( @@ -78,10 +86,13 @@ const ShowReservationPage = () => { return ( {totalSoldCount === 0 ? ( - - 아직 판매한 티켓이 없어요.{'\n'} - 티켓을 판매하고 방문자 명단을 관리해 보세요. - + + + + 아직 판매한 티켓이 없어요.{'\n'} + 티켓을 판매하고 방문자 명단을 관리해 보세요. + + ) : ( @@ -136,7 +147,7 @@ const ShowReservationPage = () => { onChange={(event) => { setSearchText(event.target.value); }} - placeholder="방문자 이름, 연락처 검색" + placeholder={isMobile ? '이름, 연락처 검색' : '방문자 이름, 연락처 검색'} /> {searchText !== '' && ( diff --git a/apps/admin/src/pages/ShowSettlementPage/ShowSettlementPage.styles.ts b/apps/admin/src/pages/ShowSettlementPage/ShowSettlementPage.styles.ts index b97b5630..6e306148 100644 --- a/apps/admin/src/pages/ShowSettlementPage/ShowSettlementPage.styles.ts +++ b/apps/admin/src/pages/ShowSettlementPage/ShowSettlementPage.styles.ts @@ -116,7 +116,7 @@ const AccountAddButton = styled.div` const PageSectionDivider = styled.hr` border-top: 1px solid ${({ theme }) => theme.palette.grey.g20}; - margin: 88px 0 52px 0; + margin: 52px 0; `; const SettlementDoneDescription = styled.p` diff --git a/apps/admin/src/pages/ShowSettlementPage/ShowSettlementPage.tsx b/apps/admin/src/pages/ShowSettlementPage/ShowSettlementPage.tsx index 44f9a32b..00ca79f6 100644 --- a/apps/admin/src/pages/ShowSettlementPage/ShowSettlementPage.tsx +++ b/apps/admin/src/pages/ShowSettlementPage/ShowSettlementPage.tsx @@ -3,11 +3,8 @@ import 'react-pdf/dist/Page/TextLayer.css'; import { ShowSettlementEventResponse, - useAddBankAccount, - useBankAccountList, useDeleteBankAccountCopyPhoto, useDeleteIDCardPhotoFile, - usePutShowSettlementBankAccount, useReadSettlementBanner, useRequestSettlement, useSettlementBanners, @@ -18,18 +15,20 @@ import { useUploadBankAccountCopyPhoto, useUploadIDCardPhotoFile, } from '@boolti/api'; -import { DownloadIcon, PlusIcon } from '@boolti/icon'; -import { AgreeCheck, Button, Select, TextButton, useDialog, useToast } from '@boolti/ui'; +import { DownloadIcon } from '@boolti/icon'; +import { AgreeCheck, Button, TextButton, useToast } from '@boolti/ui'; import { format } from 'date-fns'; import { useEffect, useMemo, useState } from 'react'; import { Document, Page, pdfjs } from 'react-pdf'; import { useParams } from 'react-router-dom'; import FileInput from '~/components/FileInput/FileInput'; -import SettlementDialogContent from '~/components/SettlementDialogContent'; -import ShowDetailLayout from '~/components/ShowDetailLayout'; +import ShowDetailLayout, { myHostInfoAtom } from '~/components/ShowDetailLayout'; import Styled from './ShowSettlementPage.styles'; +import { useAtom } from 'jotai'; +import { HostType } from '@boolti/api/src/types/host'; +import ShowDetailUnauthorized from '~/components/ShowDetailUnauthorized'; pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`; @@ -39,35 +38,28 @@ const isSettlementStarted = (eventType?: ShowSettlementEventResponse['settlement const ShowSettlementPage = () => { const params = useParams<{ showId: string }>(); + const [myHostInfo] = useAtom(myHostInfoAtom); - const [account, setAccount] = useState(null); const [agreeChecked, setAgreeChecked] = useState(false); const [numPages, setNumPages] = useState(0); - const settlementDialog = useDialog(); const toast = useToast(); - const { data: show } = useShowDetail(Number(params!.showId)); - const { data: settlementInfo, refetch: refetchSettlementInfo } = useShowSettlementInfo( - Number(params!.showId), - ); - const { data: bankAccountList, refetch: refetchBankAccountList } = useBankAccountList(); + const showId = Number(params!.showId); + const { data: show } = useShowDetail(showId); + const { data: settlementInfo, refetch: refetchSettlementInfo } = useShowSettlementInfo(showId); const { data: lastSettlementEvent, refetch: refetchLastSettlementEvent } = - useShowLastSettlementEvent(Number(params!.showId)); - const { data: settlementStatementBlob } = useShowSettlementStatement(Number(params!.showId), { + useShowLastSettlementEvent(showId); + const { data: settlementStatementBlob } = useShowSettlementStatement(showId, { enabled: lastSettlementEvent?.settlementEventType != null, }); const { data: settlementBanners } = useSettlementBanners(); - const putShowSettlementBankAccountMutation = usePutShowSettlementBankAccount( - Number(params!.showId), - ); - const uploadIDCardPhotoFileMutation = useUploadIDCardPhotoFile(Number(params!.showId)); - const uploadBankAccountCopyPhotoMutation = useUploadBankAccountCopyPhoto(Number(params!.showId)); - const deleteIDCardPhotoFileMutation = useDeleteIDCardPhotoFile(Number(params!.showId)); - const deleteBankAccountCopyPhotoMutation = useDeleteBankAccountCopyPhoto(Number(params!.showId)); - const addBankAccountMutation = useAddBankAccount(); - const requestSettlementMutation = useRequestSettlement(Number(params!.showId)); + const uploadIDCardPhotoFileMutation = useUploadIDCardPhotoFile(showId); + const uploadBankAccountCopyPhotoMutation = useUploadBankAccountCopyPhoto(showId); + const deleteIDCardPhotoFileMutation = useDeleteIDCardPhotoFile(showId); + const deleteBankAccountCopyPhotoMutation = useDeleteBankAccountCopyPhoto(showId); + const requestSettlementMutation = useRequestSettlement(showId); const readSettlementBanner = useReadSettlementBanner(); const settlementStatementFile = useMemo(() => { @@ -78,18 +70,6 @@ const ShowSettlementPage = () => { return null; }, [settlementStatementBlob]); - const bankAccountOptions = - bankAccountList?.map((bankAccount) => ({ - value: `${bankAccount.bankAccountId}`, - label: `${bankAccount.bankName} ${bankAccount.bankAccountNumber} ${bankAccount.bankAccountHolder}`, - })) ?? []; - - useEffect(() => { - if (settlementInfo?.bankAccount?.bankAccountId) { - setAccount(`${settlementInfo.bankAccount.bankAccountId}`); - } - }, [settlementInfo?.bankAccount?.bankAccountId]); - useEffect(() => { const targetSettlementBanner = settlementBanners?.find( (banner) => banner.showId === Number(params.showId), @@ -107,223 +87,185 @@ const ShowSettlementPage = () => { return ( - - - 개인정보 처리방침을 확인 후 정산에 필요한 정보를 업로드해 주세요.{' '} - - 개인정보 처리방침 - -
- 업로드 시 불티의 개인정보 처리방침에 동의한 것으로 간주하며, 정보는 정산 및 세금계산서 - 발급에 사용됩니다. -
- {settlementInfo && ( - <> - - - 정산 정보 - - - - - - 신분증 또는 사업자등록증 사본 - - - (개인 - 신분증 / 사업자 - 사업자등록증) - - - { - if (event.target.files?.[0]) { - await uploadIDCardPhotoFileMutation.mutateAsync(event.target.files[0]); + {myHostInfo?.type !== HostType.MAIN ? ( + + ) : ( + + + 개인정보 처리방침을 확인 후 정산에 필요한 정보를 업로드해 주세요.{' '} + + 개인정보 처리방침 + +
+ 업로드 시 불티의 개인정보 처리방침에 동의한 것으로 간주하며, 정보는 정산 및 세금계산서 + 발급에 사용됩니다. +
+ {settlementInfo && ( + <> + + + 정산 정보 + + + + + + 신분증 또는 사업자등록증 사본 + + + (개인 - 신분증 / 사업자 - 사업자등록증) + + + { + if (event.target.files?.[0]) { + await uploadIDCardPhotoFileMutation.mutateAsync(event.target.files[0]); + await refetchSettlementInfo(); + } + }} + onClear={async () => { + await deleteIDCardPhotoFileMutation.mutateAsync(); await refetchSettlementInfo(); - } - }} - onClear={async () => { - await deleteIDCardPhotoFileMutation.mutateAsync(); - await refetchSettlementInfo(); - }} - /> - - - - 정산 계좌 - - -