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);
+ setPreviewDrawerOpen(true);
}}
>
- 닫기
-
-
+
+
+ {
- showInfoForm.handleSubmit(onSubmit)();
+ deleteShowDialog.open({
+ title: '공연 삭제하기',
+ content: (
+ {
+ await deleteShowMutation.mutateAsync(show.id);
+
+ deleteShowDialog.close();
+ navigate(PATH.HOME);
+ toast.success('공연을 삭제했습니다.');
+ }}
+ />
+ ),
+ });
}}
>
- 저장하기
-
-
-
-
-
-
+ 공연 삭제하기
+
+
+
+ {
+ 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();
- }}
- />
-
-
-
- 정산 계좌
-
-
-
+
+
+ 통장 사본
+
+ {
+ if (event.target.files?.[0]) {
+ await uploadBankAccountCopyPhotoMutation.mutateAsync(
+ event.target.files[0],
+ );
+ await refetchSettlementInfo();
+ }
+ }}
+ onClear={async () => {
+ await deleteBankAccountCopyPhotoMutation.mutateAsync();
+ await refetchSettlementInfo();
}}
/>
-
-
-
-
- 통장 사본
-
- {
- if (event.target.files?.[0]) {
- uploadBankAccountCopyPhotoMutation.mutateAsync(event.target.files[0]);
- }
- }}
- onClear={async () => {
- await deleteBankAccountCopyPhotoMutation.mutateAsync();
- await refetchSettlementInfo();
- }}
- />
-
-
-
-
-
-
- 정산 내역서
- {settlementStatementFile && (
- }
- onClick={() => {
- if (!settlementStatementBlob) return;
+
+
+
+
+
+
+ 정산 내역서
+ {settlementStatementFile && (
+ }
+ onClick={() => {
+ if (!settlementStatementBlob) return;
- const downloadUrl = URL.createObjectURL(settlementStatementBlob);
+ const downloadUrl = URL.createObjectURL(settlementStatementBlob);
- const anchorElement = document.createElement('a');
- anchorElement.href = downloadUrl;
- anchorElement.download = `불티 정산 내역서 - ${show.name}.pdf`;
- anchorElement.click();
-
- URL.revokeObjectURL(downloadUrl);
- }}
- >
- 다운로드
-
- )}
-
- {lastSettlementEvent?.settlementEventType !== null &&
- settlementStatementFile !== null && (
- <>
-
- {
- setNumPages(data.numPages);
- }}
- >
- {Array.from(new Array(numPages), (_, index) => (
-
- ))}
-
-
- {lastSettlementEvent?.settlementEventType === 'SEND' && (
-
- {
- setAgreeChecked(event.target.checked);
- }}
- />
- {
- try {
- await requestSettlementMutation.mutateAsync();
- await refetchSettlementInfo();
- await refetchLastSettlementEvent();
+ const anchorElement = document.createElement('a');
+ anchorElement.href = downloadUrl;
+ anchorElement.download = `불티 정산 내역서 - ${show.name}.pdf`;
+ anchorElement.click();
- toast.success('정산을 요청했습니다');
- } catch (error) {
- toast.error('정산 요청에 실패했습니다. 잠시 후에 다시 시도해주세요.');
- }
+ URL.revokeObjectURL(downloadUrl);
+ }}
+ >
+ 다운로드
+
+ )}
+
+ {lastSettlementEvent?.settlementEventType !== null &&
+ settlementStatementFile !== null && (
+ <>
+
+ {
+ setNumPages(data.numPages);
}}
>
- 정산 요청하기
-
-
- )}
- {lastSettlementEvent?.settlementEventType === 'REQUEST' && (
-
-
- 정산 요청 완료
-
-
- )}
- >
- )}
- {lastSettlementEvent?.settlementEventType === 'DONE' &&
- lastSettlementEvent.triggeredAt && (
-
- {format(new Date(lastSettlementEvent.triggeredAt), 'yyyy년 MM월 dd일')}자로
- 정산이 완료된 공연입니다.
-
+ {Array.from(new Array(numPages), (_, index) => (
+
+ ))}
+
+
+ {lastSettlementEvent?.settlementEventType === 'SEND' && (
+
+ {
+ setAgreeChecked(event.target.checked);
+ }}
+ />
+ {
+ try {
+ await requestSettlementMutation.mutateAsync();
+ await refetchSettlementInfo();
+ await refetchLastSettlementEvent();
+
+ toast.success('정산을 요청했습니다');
+ } catch (error) {
+ toast.error(
+ '정산 요청에 실패했습니다. 잠시 후에 다시 시도해주세요.',
+ );
+ }
+ }}
+ >
+ 정산 요청하기
+
+
+ )}
+ {lastSettlementEvent?.settlementEventType === 'REQUEST' && (
+
+
+ 정산 요청 완료
+
+
+ )}
+ >
+ )}
+ {lastSettlementEvent?.settlementEventType === 'DONE' &&
+ lastSettlementEvent.triggeredAt && (
+
+ {format(new Date(lastSettlementEvent.triggeredAt), 'yyyy년 MM월 dd일')}자로
+ 정산이 완료된 공연입니다.
+
+ )}
+ {!lastSettlementEvent?.settlementEventType && (
+
+ 정산 내역서는 티켓 판매종료 후 생성돼요
+
)}
- {!lastSettlementEvent?.settlementEventType && (
-
- 정산 내역서는 티켓 판매종료 후 생성돼요
-
- )}
-
- >
- )}
-
+
+ >
+ )}
+
+ )}
);
};
diff --git a/apps/admin/src/pages/ShowTicketPage/ShowTicketPage.tsx b/apps/admin/src/pages/ShowTicketPage/ShowTicketPage.tsx
index dba135f9..10ace450 100644
--- a/apps/admin/src/pages/ShowTicketPage/ShowTicketPage.tsx
+++ b/apps/admin/src/pages/ShowTicketPage/ShowTicketPage.tsx
@@ -15,28 +15,29 @@ import { useEffect } from 'react';
import { SubmitHandler, useForm } from 'react-hook-form';
import { useParams } from 'react-router-dom';
-import ShowDetailLayout from '~/components/ShowDetailLayout';
+import ShowDetailLayout, { myHostInfoAtom } from '~/components/ShowDetailLayout';
import ShowInvitationTicketFormContent from '~/components/ShowInfoFormContent/ShowInvitationTicketFormContent';
import ShowSalesTicketFormContent from '~/components/ShowInfoFormContent/ShowSalesTicketFormContent';
import ShowTicketInfoFormContent from '~/components/ShowInfoFormContent/ShowTicketInfoFormContent';
import { ShowTicketFormInputs } from '~/components/ShowInfoFormContent/types';
import Styled from './ShowTicketPage.styles';
+import { useAtom } from 'jotai';
+import { HostType } from '@boolti/api/src/types/host';
+import ShowDetailUnauthorized from '~/components/ShowDetailUnauthorized';
const ShowTicketPage = () => {
const params = useParams<{ showId: string }>();
+ const [myHostInfo] = useAtom(myHostInfoAtom);
const showTicketForm = useForm();
- const { data: show } = useShowDetail(Number(params!.showId));
- const { data: showSalesInfo, refetch: refetchSalesTicketInfo } = useShowSalesInfo(
- Number(params!.showId),
- );
- const { data: salesTicketList, refetch: refetchSalesTicketList } = useSalesTicketList(
- Number(params!.showId),
- );
+ const showId = Number(params!.showId);
+ const { data: show } = useShowDetail(showId);
+ const { data: showSalesInfo, refetch: refetchSalesTicketInfo } = useShowSalesInfo(showId);
+ const { data: salesTicketList, refetch: refetchSalesTicketList } = useSalesTicketList(showId);
const { data: invitationTicketList, refetch: refetchInvitationTicketList } =
- useInvitationTicketList(Number(params!.showId));
+ useInvitationTicketList(showId);
const editSalesTicketInfoMutation = useEditSalesTicketInfo();
const createSalesTicketMutation = useCreateSalesTicket();
const createInvitationTicketMutation = useCreateInvitationTicket();
@@ -74,117 +75,125 @@ const ShowTicketPage = () => {
return (
-
-
+ {myHostInfo?.type === HostType.SUPPORTER ? (
+
+ ) : (
+
+
+
+
+
+
+
+ 저장하기
+
+
+
+
+
+ {salesTicketList && (
+ ({
+ id: ticket.id,
+ name: ticket.ticketName,
+ price: ticket.price,
+ quantity: ticket.quantity,
+ totalForSale: ticket.totalForSale,
+ }))}
+ disabled={show.isEnded}
+ onSubmitTicket={async (ticket) => {
+ await createSalesTicketMutation.mutateAsync({
+ showId: show.id,
+ ticketName: ticket.name,
+ price: Number(ticket.price),
+ totalForSale: Number(ticket.totalForSale),
+ });
+
+ await refetchSalesTicketList();
+ toast.success('일반 티켓을 생성했습니다.');
+ }}
+ onDeleteTicket={async (ticket) => {
+ if (ticket.id === undefined) return;
+
+ const result = await confirm(
+ '삭제한 티켓은 다시 생성할 수 없어요. 삭제하시겠어요?',
+ {
+ cancel: '취소하기',
+ confirm: '삭제하기',
+ },
+ );
+
+ if (!result) return;
+
+ await deleteSalesTicketMutation.mutateAsync(ticket.id);
+ await refetchSalesTicketList();
+ toast.success('티켓을 삭제했습니다.');
+ }}
+ />
+ )}
+
+
-
+ {invitationTicketList && (
+ ({
+ id: ticket.id,
+ name: ticket.ticketName,
+ quantity: ticket.quantity,
+ totalForSale: ticket.totalForSale,
+ }))}
+ description={
+ <>
+ 초청 티켓 이용을 원하시면 티켓을 생성해주세요.
+
* 사용 완료 처리된 코드는 재사용할 수 없습니다.
+ >
+ }
+ isShowEnded={show.isEnded}
+ onSubmitTicket={async (ticket) => {
+ await createInvitationTicketMutation.mutateAsync({
+ showId: show.id,
+ ticketName: ticket.name,
+ totalForSale: Number(ticket.totalForSale),
+ });
+ await refetchInvitationTicketList();
+ toast.success('초청 티켓을 생성했습니다.');
+ }}
+ onDeleteTicket={async (ticket) => {
+ if (ticket.id === undefined) return;
+
+ const result = await confirm(
+ '삭제한 티켓은 다시 생성할 수 없어요. 삭제하시겠어요?',
+ {
+ cancel: '취소하기',
+ confirm: '삭제하기',
+ },
+ );
+
+ if (!result) return;
+
+ await deleteInvitationTicketMutation.mutateAsync(ticket.id);
+ await refetchInvitationTicketList();
+ toast.success('티켓을 삭제했습니다.');
+ }}
+ />
+ )}
-
-
- 저장하기
-
-
-
-
-
- {salesTicketList && (
- ({
- id: ticket.id,
- name: ticket.ticketName,
- price: ticket.price,
- quantity: ticket.quantity,
- totalForSale: ticket.totalForSale,
- }))}
- disabled={show.isEnded}
- onSubmitTicket={async (ticket) => {
- await createSalesTicketMutation.mutateAsync({
- showId: show.id,
- ticketName: ticket.name,
- price: Number(ticket.price),
- totalForSale: Number(ticket.totalForSale),
- });
-
- await refetchSalesTicketList();
- toast.success('일반 티켓을 생성했습니다.');
- }}
- onDeleteTicket={async (ticket) => {
- if (ticket.id === undefined) return;
-
- const result = await confirm(
- '삭제한 티켓은 다시 생성할 수 없어요. 삭제하시겠어요?',
- {
- cancel: '취소하기',
- confirm: '삭제하기',
- },
- );
-
- if (!result) return;
-
- await deleteSalesTicketMutation.mutateAsync(ticket.id);
- await refetchSalesTicketList();
- toast.success('티켓을 삭제했습니다.');
- }}
- />
- )}
-
-
-
- {invitationTicketList && (
- ({
- id: ticket.id,
- name: ticket.ticketName,
- quantity: ticket.quantity,
- totalForSale: ticket.totalForSale,
- }))}
- description={
- <>
- 초청 티켓 이용을 원하시면 티켓을 생성해주세요.
-
* 사용 완료 처리된 코드는 재사용할 수 없습니다.
- >
- }
- isShowEnded={show.isEnded}
- onSubmitTicket={async (ticket) => {
- await createInvitationTicketMutation.mutateAsync({
- showId: show.id,
- ticketName: ticket.name,
- totalForSale: Number(ticket.totalForSale),
- });
- await refetchInvitationTicketList();
- toast.success('초청 티켓을 생성했습니다.');
- }}
- onDeleteTicket={async (ticket) => {
- if (ticket.id === undefined) return;
-
- const result = await confirm(
- '삭제한 티켓은 다시 생성할 수 없어요. 삭제하시겠어요?',
- {
- cancel: '취소하기',
- confirm: '삭제하기',
- },
- );
-
- if (!result) return;
-
- await deleteInvitationTicketMutation.mutateAsync(ticket.id);
- await refetchInvitationTicketList();
- toast.success('티켓을 삭제했습니다.');
- }}
- />
- )}
-
-
+
+ )}
);
};
diff --git a/packages/api/src/mutations/index.ts b/packages/api/src/mutations/index.ts
index 5689a585..30f7dba8 100644
--- a/packages/api/src/mutations/index.ts
+++ b/packages/api/src/mutations/index.ts
@@ -27,6 +27,10 @@ import useUploadBankAccountCopyPhoto from './useUploadBankAccountCopyPhoto';
import useUploadIDCardPhotoFile from './useUploadIDCardPhotoFile';
import useUploadShowImage, { ImageFile } from './useUploadShowImage';
import useRejectGift from './useRejectGift';
+import useAddHost from './useAddHost';
+import useEditHost from './useEditHost';
+import useDeleteHost from './useDeleteHost';
+import useDeleteMe from './useDeleteMe';
export {
useAddBankAccount,
@@ -58,6 +62,10 @@ export {
useUploadIDCardPhotoFile,
useUploadShowImage,
useRejectGift,
+ useAddHost,
+ useEditHost,
+ useDeleteHost,
+ useDeleteMe,
};
export type { ImageFile };
diff --git a/packages/api/src/mutations/useAddHost.ts b/packages/api/src/mutations/useAddHost.ts
new file mode 100644
index 00000000..66027bd4
--- /dev/null
+++ b/packages/api/src/mutations/useAddHost.ts
@@ -0,0 +1,24 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+
+import { fetcher } from '../fetcher';
+import { HostType } from '../types/host';
+import { queryKeys } from '../queryKey';
+
+interface PostHostRequest {
+ userCode: string;
+ type: HostType;
+}
+
+const postHost = (showId: number, body: PostHostRequest) =>
+ fetcher.post(`web/v1/shows/${showId}/hosts`, { json: body });
+
+const useAddHost = (showId: number) => {
+ const queryClient = useQueryClient();
+ return useMutation(({ body }: { body: PostHostRequest }) => postHost(showId, body), {
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: queryKeys.host.list(showId).queryKey });
+ },
+ });
+};
+
+export default useAddHost;
diff --git a/packages/api/src/mutations/useDeleteHost.ts b/packages/api/src/mutations/useDeleteHost.ts
new file mode 100644
index 00000000..61733af9
--- /dev/null
+++ b/packages/api/src/mutations/useDeleteHost.ts
@@ -0,0 +1,22 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+
+import { fetcher } from '../fetcher';
+import { queryKeys } from '../queryKey';
+
+const deleteHost = (showId: number, hostId: number) =>
+ fetcher.delete(`web/v1/shows/${showId}/hosts/${hostId}`);
+
+const useDeleteHost = (showId: number) => {
+ const queryClient = useQueryClient();
+ return useMutation(({ hostId }: { hostId: number }) => deleteHost(showId, hostId), {
+ onSuccess: (_mutationResponse, variables: { hostId: number; self: boolean }) => {
+ if (variables.self) {
+ queryClient.invalidateQueries({ queryKey: queryKeys.show.list.queryKey });
+ } else {
+ queryClient.invalidateQueries({ queryKey: queryKeys.host.list(showId).queryKey });
+ }
+ },
+ });
+};
+
+export default useDeleteHost;
diff --git a/packages/api/src/mutations/useDeleteMe.ts b/packages/api/src/mutations/useDeleteMe.ts
new file mode 100644
index 00000000..c38dc66e
--- /dev/null
+++ b/packages/api/src/mutations/useDeleteMe.ts
@@ -0,0 +1,16 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { fetcher } from '../fetcher';
+
+interface DeleteMeRequestBody {
+ reason: string;
+ appleIdAuthorizationCode?: string;
+}
+
+const deleteMe = (body: DeleteMeRequestBody) =>
+ fetcher.delete('web/v1/users/me', { json: body });
+
+const useDeleteMe = () =>
+ useMutation((body: DeleteMeRequestBody) => deleteMe(body));
+
+export default useDeleteMe;
diff --git a/packages/api/src/mutations/useEditHost.ts b/packages/api/src/mutations/useEditHost.ts
new file mode 100644
index 00000000..3011263f
--- /dev/null
+++ b/packages/api/src/mutations/useEditHost.ts
@@ -0,0 +1,28 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+
+import { fetcher } from '../fetcher';
+import { HostType } from '../types/host';
+import { queryKeys } from '../queryKey';
+
+interface PutHostRequest {
+ type: HostType;
+}
+
+const putHost = (showId: number, hostId: number, body: PutHostRequest) =>
+ fetcher.put(`web/v1/shows/${showId}/hosts/${hostId}/type`, {
+ json: body,
+ });
+
+const useEditHost = (showId: number) => {
+ const queryCleint = useQueryClient();
+ return useMutation(
+ ({ hostId, body }: { hostId: number; body: PutHostRequest }) => putHost(showId, hostId, body),
+ {
+ onSuccess: () => {
+ queryCleint.invalidateQueries({ queryKey: queryKeys.host.list(showId).queryKey });
+ },
+ },
+ );
+};
+
+export default useEditHost;
diff --git a/packages/api/src/queries/index.ts b/packages/api/src/queries/index.ts
index d2e6e96b..baff86fd 100644
--- a/packages/api/src/queries/index.ts
+++ b/packages/api/src/queries/index.ts
@@ -20,9 +20,10 @@ import useShowReservationSummary from './useShowReservationSummary';
import useShowSalesInfo from './useShowSalesInfo';
import useShowSettlementInfo from './useShowSettlementInfo';
import useShowSettlementStatement from './useShowSettlementStatement';
-import useUserAccountInfo from './useUserAccountInfo';
import useUserSummary from './useUserSummary';
import useGift from './useGift';
+import useHostList from './useHostList';
+import useMyHostInfo from './useMyHostInfo';
export {
useAdminSettlementEvent,
@@ -48,6 +49,7 @@ export {
useShowSalesInfo,
useShowSettlementInfo,
useShowSettlementStatement,
- useUserAccountInfo,
useUserSummary,
+ useHostList,
+ useMyHostInfo,
};
diff --git a/packages/api/src/queries/useHostList.ts b/packages/api/src/queries/useHostList.ts
new file mode 100644
index 00000000..d85f2114
--- /dev/null
+++ b/packages/api/src/queries/useHostList.ts
@@ -0,0 +1,7 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { queryKeys } from '../queryKey';
+
+const useHostList = (showId: number) => useQuery(queryKeys.host.list(showId));
+
+export default useHostList;
diff --git a/packages/api/src/queries/useMyHostInfo.ts b/packages/api/src/queries/useMyHostInfo.ts
new file mode 100644
index 00000000..e68d2660
--- /dev/null
+++ b/packages/api/src/queries/useMyHostInfo.ts
@@ -0,0 +1,7 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { queryKeys } from '../queryKey';
+
+const useMyHostInfo = (showId: number) => useQuery(queryKeys.host.me(showId));
+
+export default useMyHostInfo;
diff --git a/packages/api/src/queries/useUserAccountInfo.ts b/packages/api/src/queries/useUserAccountInfo.ts
deleted file mode 100644
index b0d7530c..00000000
--- a/packages/api/src/queries/useUserAccountInfo.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { useQuery } from '@tanstack/react-query';
-
-import { queryKeys } from '../queryKey';
-
-const useUserAccountInfo = () => useQuery(queryKeys.user.accountInfo);
-
-export default useUserAccountInfo;
diff --git a/packages/api/src/queryKey.ts b/packages/api/src/queryKey.ts
index 10699dfa..d1baacd3 100644
--- a/packages/api/src/queryKey.ts
+++ b/packages/api/src/queryKey.ts
@@ -29,12 +29,9 @@ import {
SuperAdminShowStatus,
TicketSalesInfoResponse,
} from './types/adminShow';
-import {
- BankAccountListResponse,
- SettlementAccountInfoResponse,
- UserProfileSummaryResponse,
-} from './types/users';
+import { BankAccountListResponse, UserProfileSummaryResponse } from './types/users';
import { GiftInfoResponse } from './types/gift';
+import { HostListItem, HostListResponse } from './types/host';
export const entranceQueryKeys = createQueryKeys('enterance', {
list: (
@@ -208,11 +205,6 @@ export const showQueryKeys = createQueryKeys('show', {
});
export const userQueryKeys = createQueryKeys('user', {
- accountInfo: {
- queryKey: null,
- queryFn: () =>
- fetcher.get(`web/v1/host/users/me/settlement-account-infos`),
- },
summary: {
queryKey: null,
queryFn: () => fetcher.get(`web/v1/host/users/me/summaries`),
@@ -230,10 +222,22 @@ export const giftQueryKeys = createQueryKeys('gift', {
}),
});
+export const hostQueryKeys = createQueryKeys('host', {
+ list: (showId: number) => ({
+ queryKey: [showId],
+ queryFn: () => fetcher.get(`web/v1/shows/${showId}/hosts`),
+ }),
+ me: (showId: number) => ({
+ queryKey: [showId],
+ queryFn: () => fetcher.get(`web/v1/shows/${showId}/hosts/me`),
+ }),
+});
+
export const queryKeys = mergeQueryKeys(
adminShowQueryKeys,
showQueryKeys,
userQueryKeys,
entranceQueryKeys,
giftQueryKeys,
+ hostQueryKeys,
);
diff --git a/packages/api/src/types/error.ts b/packages/api/src/types/error.ts
new file mode 100644
index 00000000..4c6489b6
--- /dev/null
+++ b/packages/api/src/types/error.ts
@@ -0,0 +1,5 @@
+export interface CustomError {
+ type: string;
+ errorTraceId: string;
+ detail: string;
+}
diff --git a/packages/api/src/types/gift.ts b/packages/api/src/types/gift.ts
index 6ad5e779..9e95a49f 100644
--- a/packages/api/src/types/gift.ts
+++ b/packages/api/src/types/gift.ts
@@ -6,6 +6,11 @@ export enum GiftStatus {
}
export interface GiftInfoResponse {
+ /**
+ * example: 임꺽정
+ * 선물 주는사람 이름
+ */
+ senderName: string;
/**
* example: 홍길동
* 선물 받는사람 이름
@@ -42,7 +47,7 @@ export interface GiftInfoResponse {
*/
showName: string;
/**
- * 공연 날짜
+ * 선물 만료일
*/
- showDate: string;
+ giftExpireDate: string;
}
diff --git a/packages/api/src/types/host.ts b/packages/api/src/types/host.ts
new file mode 100644
index 00000000..b7aacf2d
--- /dev/null
+++ b/packages/api/src/types/host.ts
@@ -0,0 +1,27 @@
+export const enum HostType {
+ MAIN = 'MAIN',
+ MANAGER = 'MANAGER',
+ SUPPORTER = 'SUPPORTER',
+}
+
+export interface HostTypeInfo {
+ type: HostType;
+ label: string;
+}
+
+export interface HostListItem {
+ /** 호스트 pk */
+ hostId: number;
+ /** 유저 id */
+ userId: number;
+ /** 호스트명 */
+ hostName: string;
+ /** 본인 여부 */
+ self: boolean;
+ /** 호스트 타입 */
+ type: HostType;
+ /** 호스트 썸네일 */
+ imagePath: string;
+}
+
+export type HostListResponse = HostListItem[];
diff --git a/packages/api/src/types/show.ts b/packages/api/src/types/show.ts
index 13c9d482..93791b0c 100644
--- a/packages/api/src/types/show.ts
+++ b/packages/api/src/types/show.ts
@@ -1,4 +1,5 @@
import { PageResponse, ReservationStatus, TicketStatus, TicketType } from './common';
+import { HostType } from './host';
export interface ShowImage {
sequence: number;
@@ -41,6 +42,8 @@ export type ShowSummaryResponse = Array<{
hostUserId: number;
/** 공연 대표 호스트 이름 */
hostName: string;
+ /** 나의 호스트 타입 */
+ myHostType: HostType;
/** 공연 판매 시작 날짜, 시간.ISO8601 */
salesStartTime: string;
/**공연 판매 종료 날짜, 시간.ISO8601 */
diff --git a/packages/api/src/types/users.ts b/packages/api/src/types/users.ts
index 3680cc67..69d385e5 100644
--- a/packages/api/src/types/users.ts
+++ b/packages/api/src/types/users.ts
@@ -9,12 +9,6 @@ export interface BankAccount {
bankAccountHolder: string;
}
-export interface SettlementAccountInfoResponse {
- /** 은행 계좌 정보가 등록되어 있는지 여부 */
- hasSettlementAccount: boolean;
- bankAccount?: BankAccount;
-}
-
export interface UserProfileSummaryResponse {
/** 사용자 pk */
id: number;
@@ -22,8 +16,12 @@ export interface UserProfileSummaryResponse {
nickname: string;
/** 사용자 이메일 */
email?: string;
+ /** 사용자 식별 코드 */
+ userCode: string;
/** 사용자 프로필 이미지 경로 */
imagePath?: string;
+ /** 사용자 연결 서비스 */
+ oauthType: 'KAKAO' | 'APPLE';
}
export type BankAccountListResponse = BankAccount[];
diff --git a/packages/icon/src/components/BooltiGreyIcon.tsx b/packages/icon/src/components/BooltiGreyIcon.tsx
new file mode 100644
index 00000000..96fef153
--- /dev/null
+++ b/packages/icon/src/components/BooltiGreyIcon.tsx
@@ -0,0 +1,17 @@
+export const BooltiGreyIcon = () => {
+ return (
+
+ );
+};
diff --git a/packages/icon/src/components/BooltiLightGrey.tsx b/packages/icon/src/components/BooltiLightGrey.tsx
new file mode 100644
index 00000000..189d42ea
--- /dev/null
+++ b/packages/icon/src/components/BooltiLightGrey.tsx
@@ -0,0 +1,22 @@
+export const BooltiLightGrey = () => (
+
+)
diff --git a/packages/icon/src/components/ChevronDown.tsx b/packages/icon/src/components/ChevronDown.tsx
index 0f9fb230..50bdfb9f 100644
--- a/packages/icon/src/components/ChevronDown.tsx
+++ b/packages/icon/src/components/ChevronDown.tsx
@@ -2,7 +2,7 @@ export const ChevronDown = () => (
>
diff --git a/packages/ui/src/contexts/alertContext.ts b/packages/ui/src/contexts/alertContext.ts
new file mode 100644
index 00000000..d10f5136
--- /dev/null
+++ b/packages/ui/src/contexts/alertContext.ts
@@ -0,0 +1,25 @@
+import { createContext } from 'react';
+
+export interface AlertOptions {
+ confirmButtonColorTheme?: 'primary' | 'neutral';
+}
+
+export interface AlertButtonText {
+ confirm?: string;
+}
+
+export interface IAlert {
+ message: React.ReactNode;
+ buttonText?: AlertButtonText;
+ options?: AlertOptions;
+ resolve: (value: boolean | PromiseLike) => void;
+}
+
+interface AlertContext {
+ currentAlert: IAlert | null;
+ setCurrentAlert: React.Dispatch>;
+}
+
+const alertContext = createContext(null);
+
+export default alertContext;
diff --git a/packages/ui/src/contexts/dialogContext.ts b/packages/ui/src/contexts/dialogContext.ts
index 10bd3201..769dc224 100644
--- a/packages/ui/src/contexts/dialogContext.ts
+++ b/packages/ui/src/contexts/dialogContext.ts
@@ -6,6 +6,8 @@ export interface IDialog {
isAuto?: boolean;
title?: string;
width?: string;
+ contentPadding?: string;
+ mobileType?: 'bottomSheet' | 'fullPage' | 'centerPopup';
onClose?: () => void;
}
diff --git a/packages/ui/src/hooks/index.ts b/packages/ui/src/hooks/index.ts
index 0a22f284..801fa1d5 100644
--- a/packages/ui/src/hooks/index.ts
+++ b/packages/ui/src/hooks/index.ts
@@ -2,5 +2,6 @@ import useConfirm from './useConfirm';
import useDialog from './useDialog';
import useDropdown from './useDropdown';
import useToast from './useToast';
+import useAlert from './useAlert';
-export { useConfirm, useDialog, useDropdown, useToast };
+export { useConfirm, useDialog, useDropdown, useToast, useAlert };
diff --git a/packages/ui/src/hooks/useAlert.ts b/packages/ui/src/hooks/useAlert.ts
new file mode 100644
index 00000000..1ba84b44
--- /dev/null
+++ b/packages/ui/src/hooks/useAlert.ts
@@ -0,0 +1,18 @@
+import { useCallback, useContext } from 'react';
+
+import alertContext, { AlertButtonText, AlertOptions } from '../contexts/alertContext';
+
+const useAlert = () => {
+ const context = useContext(alertContext);
+
+ return useCallback(
+ (message: React.ReactNode, buttonText?: AlertButtonText, options?: AlertOptions) => {
+ return new Promise((resolve) => {
+ context?.setCurrentAlert({ message, buttonText, options, resolve });
+ });
+ },
+ [context],
+ );
+};
+
+export default useAlert;
diff --git a/packages/ui/src/hooks/useDialog.ts b/packages/ui/src/hooks/useDialog.ts
index fe55a90c..c13e52d1 100644
--- a/packages/ui/src/hooks/useDialog.ts
+++ b/packages/ui/src/hooks/useDialog.ts
@@ -14,12 +14,16 @@ const useDialog = () => {
title,
isAuto,
width,
+ contentPadding,
+ mobileType,
onClose,
}: {
content: React.ReactNode;
title?: string;
isAuto?: boolean;
width?: string;
+ contentPadding?: string;
+ mobileType?: 'bottomSheet' | 'fullPage' | 'centerPopup';
onClose?: () => void;
}) => {
const newDialog: IDialog = {
@@ -28,6 +32,8 @@ const useDialog = () => {
title,
isAuto,
width,
+ contentPadding,
+ mobileType,
onClose,
};
diff --git a/packages/ui/src/systems/palette.ts b/packages/ui/src/systems/palette.ts
index 0352d3e8..18425bad 100644
--- a/packages/ui/src/systems/palette.ts
+++ b/packages/ui/src/systems/palette.ts
@@ -28,8 +28,8 @@ const palette = {
sub: '#FFCFBA',
},
grey: {
- main: '#87909B',
- sub: '#EAECEF',
+ main: '#888D9D',
+ sub: '#F3F5F9',
w: '#FFFFFF',
g00: '#F3F5F9',
g10: '#E7EAF2',