diff --git a/apps/admin/src/components/EventPopupContent/EventPopupContent.styles.ts b/apps/admin/src/components/EventPopupContent/EventPopupContent.styles.ts new file mode 100644 index 00000000..2621dde6 --- /dev/null +++ b/apps/admin/src/components/EventPopupContent/EventPopupContent.styles.ts @@ -0,0 +1,54 @@ +import styled from '@emotion/styled'; +import { mq_lg } from '@boolti/ui'; + +interface PopupImageProps { + hasDetail: boolean; +} + +const HomePopupContent = styled.div` + margin: 0 -24px; + ${mq_lg} { + margin: 0; + width: 500px; + } +`; + +const PopupImage = styled.img` + border-top-left-radius: 8px; + border-top-right-radius: 8px; + cursor: ${({ hasDetail }) => (hasDetail ? 'pointer' : 'default')}; + + ${mq_lg} { + width: 500px; + height: 578px; + } +`; + +const PopupFooter = styled.div` + display: flex; + align-items: center; + justify-content: space-between; +`; + +const CheckLabel = styled.label` + display: flex; + align-items: center; + gap: 8px; + padding: 16px 20px; + color: ${({ theme }) => theme.palette.grey.g60}; + cursor: pointer; +`; + +const CloseButton = styled.button` + padding: 16px 20px; + color: ${({ theme }) => theme.palette.grey.g100}; + cursor: pointer; +`; + +export default { + HomePopupContent, + CheckLabel, + PopupImage, + PopupFooter, + CloseButton, +}; diff --git a/apps/admin/src/components/EventPopupContent/index.tsx b/apps/admin/src/components/EventPopupContent/index.tsx new file mode 100644 index 00000000..a581800a --- /dev/null +++ b/apps/admin/src/components/EventPopupContent/index.tsx @@ -0,0 +1,55 @@ +import { Checkbox } from '@boolti/ui'; +import Styled from './EventPopupContent.styles'; +import { useState } from 'react'; +import { useBodyScrollLock } from '~/hooks/useBodyScrollLock'; +import { useNavigate } from 'react-router-dom'; +import useCookie from '~/hooks/useCookie'; + +interface HomePopupContentProps { + id: number; + imagePath: string; + detailPath: string | null; + onClose: () => void; +} +const EventPopupContent = ({ id, imagePath, detailPath, onClose }: HomePopupContentProps) => { + const [checked, setChecked] = useState(false); + const navigate = useNavigate(); + useBodyScrollLock(); + const { setCookie } = useCookie(); + + const onChange = () => { + setChecked((checked) => !checked); + }; + + const closeDialog = () => { + if (checked) { + const midnight = new Date(); + midnight.setHours(24, 0, 0, 0); + setCookie('popup', `${id}`, { expires: midnight.toUTCString() }); + } + onClose(); + }; + + const onClickImage = () => { + if (!detailPath) { + return; + } + navigate(detailPath); + onClose(); + }; + + return ( + + + + + + 오늘 하루 그만보기 + + 닫기 + + + ); +}; + +export default EventPopupContent; diff --git a/apps/admin/src/components/NoticePopupContent/NoticePopupContent.styles.ts b/apps/admin/src/components/NoticePopupContent/NoticePopupContent.styles.ts new file mode 100644 index 00000000..ac2460bd --- /dev/null +++ b/apps/admin/src/components/NoticePopupContent/NoticePopupContent.styles.ts @@ -0,0 +1,73 @@ +import styled from '@emotion/styled'; +import { Button, mq_lg } from '@boolti/ui'; + +const Container = styled.div` + display: flex; + flex-direction: column; + align-items: center; + + ${mq_lg} { + width: 410px; + padding: 0 20px 20px 20px; + } +`; + +const Header = styled.div` + width: 100%; + display: flex; + justify-content: flex-end; + align-items: center; + padding: 12px 0; +`; + +const CloseButton = styled.button` + width: 24px; + height: 24px; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + color: ${({ theme }) => theme.palette.grey.g70}; + + svg { + width: 24px; + height: 24px; + } +`; + +const Title = styled.h1` + font-size: 18px; + color: ${({ theme }) => theme.palette.grey.g70}; + margin-bottom: 24px; +`; + +const Emphasized = styled.div` + width: 100%; + border-radius: 4px; + padding: 16px; + background-color: ${({ theme }) => theme.palette.grey.g00}; + color: ${({ theme }) => theme.palette.red.main}; + font-size: 15px; + text-align: center; + margin-bottom: 16px; +`; + +const Description = styled.p` + color: ${({ theme }) => theme.palette.grey.g60}; + font-size: 14px; + margin-bottom: 28px; +`; + +const ConfirmButton = styled(Button)` + width: 100%; +`; + +export default { + Container, + Header, + CloseButton, + Title, + Emphasized, + Description, + ConfirmButton, +}; diff --git a/apps/admin/src/components/NoticePopupContent/index.tsx b/apps/admin/src/components/NoticePopupContent/index.tsx new file mode 100644 index 00000000..5a3431d6 --- /dev/null +++ b/apps/admin/src/components/NoticePopupContent/index.tsx @@ -0,0 +1,50 @@ +import { CloseIcon } from '@boolti/icon'; +import Styled from './NoticePopupContent.styles'; +import { useMemo } from 'react'; + +interface NoticePopupContentProps { + title: string; + description: string; + onClose: () => void; +} + +const NoticePopupContent = ({ title, description, onClose }: NoticePopupContentProps) => { + const dividedDescription = useMemo(() => { + const regex = /`([^`]*)`|([^`]+)/g; + let match; + const result: { emphasized: string[]; normal: string[] } = { + emphasized: [], + normal: [], + }; + + while ((match = regex.exec(description)) !== null) { + if (match[1] !== undefined) { + result.emphasized.push(match[1]); + } else if (match[2] !== undefined) { + result.normal.push(match[2].trim()); + } + } + + return result; + }, [description]); + + return ( + + + + + + + {title} + {dividedDescription.emphasized.length ? ( + {dividedDescription.emphasized} + ) : null} + {dividedDescription.normal} + + 확인 + + + ); +}; + +export default NoticePopupContent; diff --git a/apps/admin/src/hooks/useCookie.ts b/apps/admin/src/hooks/useCookie.ts new file mode 100644 index 00000000..4c3e5193 --- /dev/null +++ b/apps/admin/src/hooks/useCookie.ts @@ -0,0 +1,54 @@ +interface CookieOptions { + path?: string; + 'max-age'?: number; + domain?: string; + secure?: boolean; + [key: string]: unknown; +} + +const uscCookie = () => { + const setCookie = (name: string, value: string, options: CookieOptions = {}) => { + options.path = options.path || '/'; + + let updatedCookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`; + + for (const [key, optionValue] of Object.entries(options)) { + updatedCookie += `; ${key}`; + if (optionValue !== true) { + updatedCookie += `=${optionValue}`; + } + } + + console.log(`updatedCookie: ${updatedCookie}`); + + document.cookie = updatedCookie; + }; + + const getCookie = (name: string) => { + const cookieString = document.cookie; + const cookies = cookieString.split('; '); + + for (const cookie of cookies) { + const [key, value] = cookie.split('='); + if (decodeURIComponent(key) === name) { + return decodeURIComponent(value); + } + } + + return null; + }; + + const deleteCookie = (name: string) => { + setCookie(name, '', { + 'max-age': -1, + }); + }; + + return { + setCookie, + getCookie, + deleteCookie, + }; +}; + +export default uscCookie; diff --git a/apps/admin/src/hooks/usePopupDialog.tsx b/apps/admin/src/hooks/usePopupDialog.tsx new file mode 100644 index 00000000..957c5d6f --- /dev/null +++ b/apps/admin/src/hooks/usePopupDialog.tsx @@ -0,0 +1,62 @@ +import { useEffect } from 'react'; +import useCookie from './useCookie'; +import { useDialog } from '@boolti/ui'; +import { Popup } from '@boolti/api'; +import NoticePopupContent from '~/components/NoticePopupContent'; +import EventPopupContent from '~/components/EventPopupContent'; + +const usePopupDialog = (popupData?: Popup) => { + const eventPopupDialog = useDialog(); + const noticePopupDialog = useDialog(); + const { getCookie } = useCookie(); + + useEffect(() => { + if (!popupData) { + return; + } + const today = new Date(); + const startDate = new Date(popupData.startDate); + const endDate = new Date(popupData.endDate); + if (!(startDate <= today && today <= endDate)) { + return; + } + const hasCookie = !!getCookie('popup'); + + switch (popupData.type) { + case 'EVENT': + if (hasCookie) { + return; + } + eventPopupDialog.open({ + content: ( + + ), + mobileType: 'centerPopup', + isAuto: true, + contentPadding: '0', + }); + return; + case 'NOTICE': + noticePopupDialog.open({ + content: ( + + ), + mobileType: 'centerPopup', + isAuto: true, + contentPadding: '0', + }); + return; + } + }, [popupData]); +}; + +export default usePopupDialog; diff --git a/apps/admin/src/pages/HomePage/index.tsx b/apps/admin/src/pages/HomePage/index.tsx index 7a33bda9..6c7e60d4 100644 --- a/apps/admin/src/pages/HomePage/index.tsx +++ b/apps/admin/src/pages/HomePage/index.tsx @@ -1,6 +1,7 @@ import { queryKeys, useLogout, + usePopup, useQueryClient, useSettlementBanners, useShowList, @@ -21,6 +22,7 @@ import { useAuthAtom } from '~/atoms/useAuthAtom'; import SettingDialogContent from '~/components/SettingDialogContent'; import { useNavigate } from 'react-router-dom'; import { useState } from 'react'; +import usePopupDialog from '~/hooks/usePopupDialog'; const bannerDescription = { REQUIRED: '공연의 정산 내역서가 도착했어요. 내역을 확인한 후 정산을 요청해 주세요.', @@ -42,6 +44,8 @@ const HomePage = () => { const { data: userProfileData, isLoading: isUserProfileLoading } = useUserProfile(); const { data: showList = [], isLoading: isShowListLoading } = useShowList(); const { data: settlementBanners } = useSettlementBanners(); + const { data: popupData } = usePopup(); + usePopupDialog(popupData); const { imgPath, nickname = '', userCode } = userProfileData ?? {}; diff --git a/packages/api/src/queries/index.ts b/packages/api/src/queries/index.ts index e819fbae..6c9b4a43 100644 --- a/packages/api/src/queries/index.ts +++ b/packages/api/src/queries/index.ts @@ -40,6 +40,7 @@ import useCastTeamList from './useCastTeamList'; import useAdminTicketList from './useAdminTicketList'; import useAdminSalesTicketList from './useAdminSalesTicketList'; import useAdminReservationSummaryV2 from './useAdminReservationSummaryV2'; +import usePopup from './usePopup'; import useSuperAdminShowSettlementStatement from './useSuperAdminShowSettlementStatement'; export { @@ -85,5 +86,6 @@ export { useSuperAdminSalesTicketList, useSuperAdminInvitationTicketList, useSuperAdminInvitationCodeList, + usePopup, useSuperAdminShowSettlementStatement, }; diff --git a/packages/api/src/queries/usePopup.ts b/packages/api/src/queries/usePopup.ts new file mode 100644 index 00000000..eb770ed9 --- /dev/null +++ b/packages/api/src/queries/usePopup.ts @@ -0,0 +1,7 @@ +import { useQuery } from '@tanstack/react-query'; + +import { queryKeys } from '../queryKey'; + +const usePopup = () => useQuery(queryKeys.popup.info); + +export default usePopup; diff --git a/packages/api/src/queryKey.ts b/packages/api/src/queryKey.ts index f3cd7897..719333be 100644 --- a/packages/api/src/queryKey.ts +++ b/packages/api/src/queryKey.ts @@ -23,6 +23,7 @@ import { ShowSummaryResponse, TicketStatus, TicketType, + Popup, } from './types'; import { AdminShowDetailResponse, @@ -445,6 +446,13 @@ export const castTeamQueryKeys = createQueryKeys('castTeams', { }), }); +export const popupQueryKeys = createQueryKeys('popup', { + info: { + queryKey: null, + queryFn: () => fetcher.get('web/papi/v1/popup'), + }, +}); + export const queryKeys = mergeQueryKeys( adminShowQueryKeys, adminEntranceQueryKeys, @@ -456,4 +464,5 @@ export const queryKeys = mergeQueryKeys( giftQueryKeys, hostQueryKeys, castTeamQueryKeys, + popupQueryKeys, ); diff --git a/packages/api/src/types/index.ts b/packages/api/src/types/index.ts index d4c560d2..c2af1956 100644 --- a/packages/api/src/types/index.ts +++ b/packages/api/src/types/index.ts @@ -3,3 +3,4 @@ export * from './entrance'; export * from './show'; export * from './users'; export * from './cast'; +export * from './popup'; diff --git a/packages/api/src/types/popup.ts b/packages/api/src/types/popup.ts new file mode 100644 index 00000000..a0d11683 --- /dev/null +++ b/packages/api/src/types/popup.ts @@ -0,0 +1,10 @@ +export interface Popup { + id: number; + type: 'EVENT' | 'NOTICE'; + eventUrl: string | null; + view: 'Home'; + noticeTitle: string | null; + description: string; + startDate: string; + endDate: string; +} diff --git a/packages/ui/src/components/Checkbox/index.tsx b/packages/ui/src/components/Checkbox/index.tsx index c63fb80f..bb48f815 100644 --- a/packages/ui/src/components/Checkbox/index.tsx +++ b/packages/ui/src/components/Checkbox/index.tsx @@ -5,8 +5,8 @@ import styled from '@emotion/styled'; const CheckIcon: React.FC = () => (