diff --git a/README.md b/README.md index 85445ef3..9a331bbd 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,14 @@ - Storybook : https://ui.boolti.in ## 기술 스택 + - React - TypeScript - Turborepo - Vite ## 패키지 설명 + - `apps/admin`: 불티에서 공연을 생성하고 관리하는 사용자들을 위한 서비스입니다. - `apps/preview`: 공연 예매 페이지를 공유했을 때 랜딩될 페이지입니다. (WIP) - `apps/super-admin`: 불티 팀원이 사용할 슈퍼 어드민 페이지입니다. (WIP) diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx index 3341421e..81e03b8a 100644 --- a/apps/admin/src/App.tsx +++ b/apps/admin/src/App.tsx @@ -30,6 +30,8 @@ import ShowSettlementPage from './pages/ShowSettlementPage/ShowSettlementPage'; import ShowTicketPage from './pages/ShowTicketPage/ShowTicketPage'; import SignUpCompletePage from './pages/SignUpComplete/SignUpCompletePage'; import SitePolicyPage from './pages/SitePolicyPage/SitePolicyPage'; +import GiftRegisterPage from './pages/GiftRegisterPage'; +import GiftIntroPage from './pages/GiftIntroPage'; import { useAuthAtom } from './atoms/useAuthAtom'; setDefaultOptions({ locale: ko }); @@ -67,6 +69,14 @@ const publicRoutes = [ path: PATH.SITE_POLICY, element: , }, + { + path: PATH.GIFT_INTRO, + element: , + }, + { + path: PATH.GIFT_REGISTER, + element: , + }, { path: '*', element: , diff --git a/apps/admin/src/assets/images/gift-invitation.png b/apps/admin/src/assets/images/gift-invitation.png new file mode 100644 index 00000000..648d39b5 Binary files /dev/null and b/apps/admin/src/assets/images/gift-invitation.png differ diff --git a/apps/admin/src/assets/images/unknown-gift-invitation.png b/apps/admin/src/assets/images/unknown-gift-invitation.png new file mode 100644 index 00000000..40cb0ffe Binary files /dev/null and b/apps/admin/src/assets/images/unknown-gift-invitation.png differ diff --git a/apps/admin/src/constants/link.ts b/apps/admin/src/constants/link.ts index 3ed9c7c6..e73d34cd 100644 --- a/apps/admin/src/constants/link.ts +++ b/apps/admin/src/constants/link.ts @@ -1,7 +1,7 @@ export const LINK = { TERMS: 'https://boolti.notion.site/b4c5beac61c2480886da75a1f3afb982', PRIVACY_POLICY: 'https://boolti.notion.site/5f73661efdcd4507a1e5b6827aa0da70', - // TODO: 다이나믹 링크값 변경 - DYNAMIC_LINK: - 'https://boolti.page.link/?link=https://preview.boolti.in/show/&apn=com.nexters.boolti&ibi=com.nexters.boolti&isi=6476589322', + APP_QR: + 'https://boolti.page.link/?link=https://app.boolti.in/home/shows?apn=com.nexters.boolti&ibi=com.nexters.boolti&isi=6476589322', + BOOLTI_KAKAO_CHANNEL: 'http://pf.kakao.com/_pVxfxaG/chat', }; diff --git a/apps/admin/src/constants/routes.ts b/apps/admin/src/constants/routes.ts index d9672685..c8e1cfff 100644 --- a/apps/admin/src/constants/routes.ts +++ b/apps/admin/src/constants/routes.ts @@ -15,6 +15,8 @@ export const PATH = { SHOW_ENTRANCE: '/show/:showId/enterance', SHOW_SETTLEMENT: '/show/:showId/settlement', SITE_POLICY: '/site-policy/:policyId', + GIFT_INTRO: '/gift/:giftId', + GIFT_REGISTER: '/gift/:giftId/register', } as const; export const HREF = { diff --git a/apps/admin/src/pages/GiftIntroPage/GiftIntroPage.styles.ts b/apps/admin/src/pages/GiftIntroPage/GiftIntroPage.styles.ts new file mode 100644 index 00000000..3f9f499d --- /dev/null +++ b/apps/admin/src/pages/GiftIntroPage/GiftIntroPage.styles.ts @@ -0,0 +1,34 @@ +import styled from '@emotion/styled'; + +const Container = styled.div` + padding: 0 24px; + height: 100vh; + padding-top: 184px; + text-align: center; + background: linear-gradient(#121215, #434753); +`; + +const Button = styled.button` + cursor: pointer; +`; + +const LetterImg = styled.img``; + +const Description = styled.p` + ${({ theme }) => theme.typo.b4} + color: ${({ theme }) => theme.palette.grey.g20}; + white-space: pre-wrap; + text-align: center; + margin-bottom: 20px; + + & > strong { + ${({ theme }) => theme.typo.sh2}; + } +`; + +export default { + Container, + Button, + LetterImg, + Description, +}; diff --git a/apps/admin/src/pages/GiftIntroPage/index.tsx b/apps/admin/src/pages/GiftIntroPage/index.tsx new file mode 100644 index 00000000..4c0567a8 --- /dev/null +++ b/apps/admin/src/pages/GiftIntroPage/index.tsx @@ -0,0 +1,47 @@ +import { generatePath, useNavigate, useParams } from 'react-router-dom'; +import Styled from './GiftIntroPage.styles'; +import invitationImg from '~/assets/images/gift-invitation.png'; +import unknownGiftImg from '~/assets/images/unknown-gift-invitation.png'; +import { PATH } from '~/constants/routes'; +import { useGift } from '@boolti/api'; + +const GiftIntroPage = () => { + const { giftId = '' } = useParams<{ giftId: string }>(); + const navigate = useNavigate(); + + const { data, isLoading, error } = useGift(giftId); + return ( + + {!isLoading && ( + <> + + {error ? ( + '유효하지 않은 선물이에요.' + ) : ( + <> + {data?.recipientName}님이 선물을 보냈어요.{'\n'}터치해서 확인해 + 보세요! + + )} + + {error ? ( + + ) : ( + { + if (giftId) { + navigate(generatePath(PATH.GIFT_REGISTER, { giftId }), { replace: true }); + } + }} + > + + + )} + + )} + + ); +}; + +export default GiftIntroPage; diff --git a/apps/admin/src/pages/GiftRegisterPage/GiftRegisterPage.styles.ts b/apps/admin/src/pages/GiftRegisterPage/GiftRegisterPage.styles.ts new file mode 100644 index 00000000..eca350a5 --- /dev/null +++ b/apps/admin/src/pages/GiftRegisterPage/GiftRegisterPage.styles.ts @@ -0,0 +1,24 @@ +import styled from '@emotion/styled'; + +const Container = styled.div` + width: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background: linear-gradient(#121215, #434753); + margin: 0 auto; +`; + +const GiftWrapper = styled.section` + width: 100%; + max-width: ${({ theme }) => theme.breakpoint.mobile}; + background: linear-gradient(#121215, #434753); + position: relative; + padding: 0 20px; +`; + +export default { + Container, + GiftWrapper, +}; diff --git a/apps/admin/src/pages/GiftRegisterPage/components/GiftGuide/GiftGuide.styles.ts b/apps/admin/src/pages/GiftRegisterPage/components/GiftGuide/GiftGuide.styles.ts new file mode 100644 index 00000000..915a7f02 --- /dev/null +++ b/apps/admin/src/pages/GiftRegisterPage/components/GiftGuide/GiftGuide.styles.ts @@ -0,0 +1,64 @@ +import styled from '@emotion/styled'; + +const Container = styled.section` + width: 100%; + max-width: ${({ theme }) => theme.breakpoint.mobile}; + padding: 28px 20px; + background-color: ${({ theme }) => theme.palette.grey.w}; +`; + +const DescriptoinList = styled.ul` + list-style: none; + ${({ theme }) => theme.typo.c1}; + color: ${({ theme }) => theme.palette.grey.g60}; +`; + +const DescriptionListItem = styled.li` + ${({ theme }) => theme.typo.c1}; + color: ${({ theme }) => theme.palette.grey.g60}; + position: relative; + + &::before { + content: '・'; + padding-right: 4px; + color: ${({ theme }) => theme.palette.grey.g60}; + } +`; + +const RejectButton = styled.button` + text-decoration: underline; + ${({ theme }) => theme.typo.c1}; + color: ${({ theme }) => theme.palette.grey.g90}; + cursor: pointer; + margin-left: 15px; +`; + +const RegisterGuideContainer = styled.div` + padding: 12px; + background-color: ${({ theme }) => theme.palette.grey.g00}; + border-radius: 6px; + width: 100%; + margin-top: 28px; + margin-bottom: 20px; +`; + +const RegisterGuideTitle = styled.h3` + ${({ theme }) => theme.typo.sh0}; + color: ${({ theme }) => theme.palette.grey.g90}; + margin-bottom: 8px; +`; + +const RegisterGuideDescription = styled.p` + ${({ theme }) => theme.typo.c1}; + color: ${({ theme }) => theme.palette.grey.g60}; +`; + +export default { + Container, + DescriptoinList, + DescriptionListItem, + RegisterGuideContainer, + RejectButton, + RegisterGuideTitle, + RegisterGuideDescription, +}; diff --git a/apps/admin/src/pages/GiftRegisterPage/components/GiftGuide/index.tsx b/apps/admin/src/pages/GiftRegisterPage/components/GiftGuide/index.tsx new file mode 100644 index 00000000..baad0688 --- /dev/null +++ b/apps/admin/src/pages/GiftRegisterPage/components/GiftGuide/index.tsx @@ -0,0 +1,84 @@ +import { useConfirm } from '@boolti/ui'; +import Styled from './GiftGuide.styles'; +import { useGift, useRejectGift } from '@boolti/api'; +import { useParams } from 'react-router-dom'; +import { GiftStatus } from '@boolti/api/src/types/gift'; + +const GiftGuide = () => { + const confirm = useConfirm(); + const { giftId = '' } = useParams<{ giftId: string }>(); + const { data } = useGift(giftId); + + const { status } = data ?? {}; + + const isRegistered = status === GiftStatus.REGISTERED; + const isCancelled = status === GiftStatus.CANCELLED; + const isRegistrable = status === GiftStatus.REGISTRABLE; + const isRejected = status === GiftStatus.REJECTED; + + const rejectGiftMutation = useRejectGift(giftId); + + const descriptionList = [ + '선물 등록 후에는 거절 또는 결제 취소 및 환불이 불가합니다.', + '기한 내 미등록 시 선물 거절 처리되며 결제가 자동 취소됩니다.', + '선물 거절 시 보낸 분께 알림이 발송되며 결제가 자동 취소됩니다.', + ]; + return ( + + + {isRegistrable && ( + <> + {descriptionList.map((item, index) => ( + {item} + ))} + { + const result = await confirm( + '선물 거절 시 보낸 분께 알림이 발송되며 결제가 자동 취소됩니다.거절하시겠습니까?', + { + cancel: '취소하기', + confirm: '거절하기', + }, + ); + if (result) { + await rejectGiftMutation.mutateAsync(); + } + }} + > + 선물 거절하기 + + + )} + {isRegistered && ( + + 선물 등록 후에는 거절 또는 결제 취소 및 환불이 불가합니다. + + )} + {(isCancelled || isRejected) && ( + + 취소되어 등록할 수 없는 선물입니다. + + )} + + + {isRegistrable && ( + + 선물 등록 방법 + [불티앱이 있다면] + + 선물 등록하기 버튼 클릭 {'>'} 선물 등록 안내 확인 + + + [불티앱이 없다면] + + + 스토어에서 앱 다운로드 {'>'} 회원가입 및 로그인 {'>'} 해당 페이지 재접속 {'>'} 선물 + 등록하기 버튼 클릭{'>'} 선물 등록 안내 확인 + + + )} + + ); +}; + +export default GiftGuide; diff --git a/apps/admin/src/pages/GiftRegisterPage/components/GiftInformation/GiftInformation.styles.ts b/apps/admin/src/pages/GiftRegisterPage/components/GiftInformation/GiftInformation.styles.ts new file mode 100644 index 00000000..dbc4ee10 --- /dev/null +++ b/apps/admin/src/pages/GiftRegisterPage/components/GiftInformation/GiftInformation.styles.ts @@ -0,0 +1,149 @@ +import styled from '@emotion/styled'; + +const Container = styled.div` + max-width: 330px; + border-radius: 8px; + margin: 0 auto; + padding-top: 68px; +`; + +const Recipient = styled.p` + padding: 6px 12px; + border-radius: 100px; + ${({ theme }) => theme.typo.c1}; + background-color: ${({ theme }) => theme.palette.primary.o3}; + color: ${({ theme }) => theme.palette.grey.w}; + margin-bottom: 12px; +`; + +const Wrapper = styled.div` + background: linear-gradient(#ff5a14, #ffa883); + display: flex; + flex-direction: column; + align-items: center; + padding: 32px 20px; + border-radius: 8px 8px 0px 0px; + position: relative; + + &::before { + content: ''; + position: absolute; + width: 20px; + height: 20px; + border-radius: 50%; + bottom: -10px; + left: -10px; + background-color: #32353e; + } + + &::after { + content: ''; + position: absolute; + width: 20px; + height: 20px; + border-radius: 50%; + bottom: -10px; + right: -10px; + background-color: #32353e; + } +`; + +const InvitationDescription = styled.p` + ${({ theme }) => theme.typo.sh2} + color: ${({ theme }) => theme.palette.grey.w}; + text-align: center; +`; + +const InvitationImage = styled.img` + width: 100%; + max-width: 270; + margin-top: 28px; +`; + +const ShowContainer = styled.div` + background-color: ${({ theme }) => theme.palette.grey.w}; + border-radius: 0px 0px 8px 8px; + display: flex; + align-items: center; + padding: 24px 20px; +`; + +const PosterImage = styled.img` + width: 54px; + border-radius: 4px; + border: 1px solid ${({ theme }) => theme.palette.grey.g10}; + margin-right: 16px; +`; + +const ShowInformation = styled.div``; + +const ShowTitle = styled.h3` + word-break: keep-all + ${({ theme }) => theme.typo.point.p1} + color: ${({ theme }) => theme.palette.grey.g100}; +`; + +const ShowDetailLink = styled.button` + ${({ theme }) => theme.typo.b1} + color: ${({ theme }) => theme.palette.grey.g60}; + display: flex; + align-items: center; + cursor: pointer; +`; + +const Footer = styled.div` + width: calc(100% - 40px); + margin: 64px auto 28px; + text-align: center; +`; + +const RegisterDescription = styled.p` + margin-bottom: 16px; + ${({ theme }) => theme.typo.b1}; + color: ${({ theme }) => theme.palette.grey.g20}; +`; + +const ExpireDate = styled.span` + ${({ theme }) => theme.typo.sh0}; + color: ${({ theme }) => theme.palette.primary.o1}; +`; + +const Button = styled.button` + width: 100%; + height: 48px; + padding: 0 20px; + ${({ theme }) => theme.typo.sh0}; + background-color: ${({ theme }) => theme.palette.grey.g90}; + color: ${({ theme }) => theme.palette.grey.w}; + border-radius: 8px; + text-align: center; + cursor: pointer; + + &:disabled { + color: ${({ theme }) => theme.palette.grey.g40}; + background-color: ${({ theme }) => theme.palette.grey.g70}; + } +`; + +const CancelText = styled.span` + ${({ theme }) => theme.typo.sh0}; + color: ${({ theme }) => theme.palette.primary.o1}; +`; + +export default { + Container, + Recipient, + Wrapper, + InvitationDescription, + InvitationImage, + ShowContainer, + PosterImage, + ShowInformation, + ShowTitle, + ShowDetailLink, + Footer, + RegisterDescription, + ExpireDate, + Button, + CancelText, +}; diff --git a/apps/admin/src/pages/GiftRegisterPage/components/GiftInformation/index.tsx b/apps/admin/src/pages/GiftRegisterPage/components/GiftInformation/index.tsx new file mode 100644 index 00000000..da806b60 --- /dev/null +++ b/apps/admin/src/pages/GiftRegisterPage/components/GiftInformation/index.tsx @@ -0,0 +1,88 @@ +import { useConfirm } from '@boolti/ui'; +import Styled from './GiftInformation.styles'; +import { ChevronRightIcon } from '@boolti/icon'; +import { useParams } from 'react-router-dom'; +import { useGift } from '@boolti/api'; +import { format } from 'date-fns/format'; +import { GiftStatus } from '@boolti/api/src/types/gift'; + +const GiftInformation = () => { + const confirm = useConfirm(); + const { giftId = '' } = useParams<{ giftId: string }>(); + const { data } = useGift(giftId); + + const { recipientName, message, showId, status, giftImageUrl, showImageUrl, showDate, showName } = + data ?? {}; + + const isRegistered = status === GiftStatus.REGISTERED; + const isCancelled = status === GiftStatus.CANCELLED; + const isRejected = status === GiftStatus.REJECTED; + const isRegistrable = status === GiftStatus.REGISTRABLE; + const showDetailLink = `https://boolti.page.link/?link=https://preview.boolti.in/show/${showId}&apn=com.nexters.boolti&ibi=com.nexters.boolti&isi=6476589322`; + const giftRegisterLink = `https://boolti.page.link/?link=https://app.boolti.in/gift/${giftId}&apn=com.nexters.boolti&ibi=com.nexters.boolti&isi=6476589322`; + + return ( + <> + + + TO. {recipientName} + {message} + + + + + + {showName} + { + window.open(showDetailLink, '_blank'); + }} + > + 공연 자세히 보기 + + + + + + + {isRegistrable && ( + + + {format(showDate ?? new Date(), 'yyyy년 M월 d일')} + + 까지 선물을 등록해 주세요 + + )} + { + const result = await confirm( + '불티 앱에서만 이용이 가능합니다.스토어로 이동하시겠습니까?', + { + cancel: '취소하기', + confirm: '이동하기', + }, + ); + if (result) { + window.open(giftRegisterLink, '_blank'); + } + }} + > + {isRegistered && '등록한 선물'} + {(isCancelled || isRejected) && ( + <> + 취소된 선물 - + + {isCancelled && ' 보낸 분 취소'} + {isRejected && ' 받는 분 거절'} + + + )} + {isRegistrable && '선물 등록하기'} + + + + ); +}; + +export default GiftInformation; diff --git a/apps/admin/src/pages/GiftRegisterPage/components/GiftTerms/GiftTerms.styles.ts b/apps/admin/src/pages/GiftRegisterPage/components/GiftTerms/GiftTerms.styles.ts new file mode 100644 index 00000000..89bc18aa --- /dev/null +++ b/apps/admin/src/pages/GiftRegisterPage/components/GiftTerms/GiftTerms.styles.ts @@ -0,0 +1,50 @@ +import styled from '@emotion/styled'; +import { Link } from 'react-router-dom'; + +const Container = styled.div` + background-color: ${({ theme }) => theme.palette.grey.g10}; +`; + +const ExpansionPanel = styled.div``; + +const ExpansionPanelHeader = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 20px; +`; + +const ExpansionPanelHeaderTitle = styled.h2` + ${({ theme }) => theme.typo.b3}; + color: ${({ theme }) => theme.palette.grey.g70}; +`; + +const ExpansionPanelContents = styled.div` + padding: 0 20px 40px; +`; + +const List = styled.ul` + list-style-type: '・'; + margin: 0 8px; +`; + +const ListItem = styled.li` + ${({ theme }) => theme.typo.b1}; + color: ${({ theme }) => theme.palette.grey.g60}; + margin-left: 8px; +`; + +const BooltiChannelLink = styled(Link)` + text-decoration: underline; +`; + +export default { + Container, + ExpansionPanel, + ExpansionPanelHeader, + ExpansionPanelHeaderTitle, + ExpansionPanelContents, + List, + ListItem, + BooltiChannelLink, +}; diff --git a/apps/admin/src/pages/GiftRegisterPage/components/GiftTerms/index.tsx b/apps/admin/src/pages/GiftRegisterPage/components/GiftTerms/index.tsx new file mode 100644 index 00000000..aa49ba42 --- /dev/null +++ b/apps/admin/src/pages/GiftRegisterPage/components/GiftTerms/index.tsx @@ -0,0 +1,46 @@ +import { Footer } from '@boolti/ui'; +import Styled from './GiftTerms.styles'; +import { useState } from 'react'; +import { ChevronDown } from '@boolti/icon/src/components/ChevronDown'; +import { LINK } from '~/constants/link'; +import { ChevronUp } from '@boolti/icon/src/components/ChevronUp'; + +const GiftTerms = () => { + const [isOpen, setIsOpen] = useState(false); + const terms = [ + '서비스 내 발권 취소 및 환불은 주최자가 지정한 티켓 판매 기간 내에만 가능하며, 판매 기간 이후 환불은 주최자에게 직접 문의해 주시기 바랍니다.', + '초청 티켓의 경우 발권 취소가 불가합니다.', + '취소 요청 즉시 취소 완료 처리 및 환불이 진행됩니다.', + '환불은 기존 결제 수단으로 진행되며 계좌이체의 경우 결제하신 계좌로 환불이 진행됩니다.', + '결제 수단에 따라 환불 완료까지 약 1~5 영업일이 소요될 수 있습니다.', + ]; + return ( + + + setIsOpen((isOpen) => !isOpen)}> + 취소/환불 규정 + {isOpen ? : } + + {isOpen && ( + + + {terms.map((term, index) => ( + {term} + ))} + + 기타 사항은 카카오톡 채널{' '} + + @스튜디오불티 + + 로 문의해 주시기 바랍니다. + + + + )} + +