diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index f5f66ab4a..170255f60 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -7,6 +7,7 @@ import { RouterContext } from 'next/dist/shared/lib/router-context'; import { QueryParamProvider } from 'use-query-params'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { RecoilRoot } from 'recoil'; +import { OverlayProvider } from '@toss/use-overlay'; import ResponsiveProvider from '../src/components/common/Responsive/ResponsiveProvider'; import StorybookToastProvider from '../src/components/common/Toast/providers/StorybookToastProvider'; @@ -17,7 +18,7 @@ import GlobalStyle from '../src/styles/GlobalStyle'; initialize(); const queryClient = new QueryClient({ - defaultOptions: { queries: { cacheTime: 300000, refetchOnWindowFocus: false, staleTime: 300000, retry: 1 } }, + defaultOptions: { queries: { gcTime: 300000, refetchOnWindowFocus: false, staleTime: 300000, retry: 1 } }, }); export const parameters = { @@ -52,7 +53,9 @@ export const decorators = [ - + + + diff --git a/package.json b/package.json index 474d40024..d95716767 100644 --- a/package.json +++ b/package.json @@ -41,8 +41,10 @@ "@radix-ui/react-tabs": "^1.0.2", "@radix-ui/react-tooltip": "^1.0.5", "@sopt-makers/colors": "^3.0.0", + "@sopt-makers/fonts": "^1.0.0", "@tanstack/react-query": "^5.4.3", "@toss/emotion-utils": "^1.1.10", + "@toss/use-overlay": "^1.3.6", "await-to-js": "^3.0.0", "axios": "^0.27.2", "cmdk": "^0.2.0", diff --git a/public/icons/icon_help.svg b/public/icons/icon_help.svg new file mode 100644 index 000000000..d7ffc79d0 --- /dev/null +++ b/public/icons/icon_help.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/icon_more.svg b/public/icons/icon_more.svg new file mode 100644 index 000000000..8018ac255 --- /dev/null +++ b/public/icons/icon_more.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/polygon.svg b/public/icons/polygon.svg new file mode 100644 index 000000000..8659e47c4 --- /dev/null +++ b/public/icons/polygon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/common/Modal/Alert.tsx b/src/components/common/Modal/Alert.tsx deleted file mode 100644 index 8fae48669..000000000 --- a/src/components/common/Modal/Alert.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import styled from '@emotion/styled'; -import { FC, PropsWithChildren, Suspense, useEffect, useState } from 'react'; -import { createRoot, Root } from 'react-dom/client'; - -import Button from '@/components/common/Button'; -import Modal, { ModalProps } from '@/components/common/Modal'; - -interface AlertModalProps extends Omit { - title?: string; - okText?: string; - afterClose?: () => void; - onClose?: () => void; -} -const AlertModal: FC> = ({ - title = '', - confirmIcon = true, - okText, - onClose, - afterClose, - children, - ...props -}) => { - const [isOpen, setIsOpen] = useState(false); - - const handleClose = () => { - setIsOpen(false); - onClose?.(); - afterClose?.(); - }; - - useEffect(() => { - if (typeof window !== undefined) { - setIsOpen(true); - } - }, []); - - return ( - - {children} - - - {okText ?? '완료'} - - - - ); -}; - -const StyledModalFooter = styled.footer` - display: flex; - align-items: center; - justify-content: center; - margin-top: 42px; -`; - -const StyledButton = styled(Button)` - border-radius: 12px; - padding: 14px 28px; -`; - -export default AlertModal; - -function createAlertElement(props: AlertModalProps) { - const divTarget = document.createElement('div'); - divTarget.id = 'makers-alert'; - document.body.appendChild(divTarget); - const root = createRoot(divTarget); - root.render( - - { - removeAlertElement(divTarget, root); - props.afterClose?.(); - }} - /> - , - ); -} - -function removeAlertElement(target: HTMLElement, root: Root) { - if (target && target.parentNode) { - root.unmount(); - target.parentNode.removeChild(target); - } -} - -export function Alert(props: AlertModalProps): Promise { - return new Promise((resolve) => { - createAlertElement({ - ...props, - afterClose: () => { - resolve(); - props.afterClose?.(); - }, - }); - }); -} diff --git a/src/components/common/Modal/Confirm.tsx b/src/components/common/Modal/Confirm.tsx deleted file mode 100644 index bc8df6d2f..000000000 --- a/src/components/common/Modal/Confirm.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import styled from '@emotion/styled'; -import { FC, PropsWithChildren, Suspense, useEffect, useState } from 'react'; -import { createRoot, Root } from 'react-dom/client'; - -import Button from '@/components/common/Button'; -import Modal, { ModalProps } from '@/components/common/Modal'; - -interface ConfirmModalProps extends Omit { - title?: string; - content?: string; - cancelText?: string; - okText?: string; - onConfirm?: () => void; - onCancel?: () => void; - onClose?: () => void; - confirmButtonVariable?: 'default' | 'primary' | 'danger'; -} -const ConfirmModal: FC> = ({ - title = '', - content, - okText, - cancelText, - onConfirm, - onCancel, - onClose, - children, - confirmButtonVariable = 'primary', - ...props -}) => { - const [isOpen, setIsOpen] = useState(false); - - const handleClose = () => { - onClose?.(); - setIsOpen(false); - }; - - const handleCancel = () => { - onCancel?.(); - handleClose(); - }; - - const handleConfirm = () => { - onConfirm?.(); - handleClose(); - }; - - useEffect(() => { - if (typeof window !== undefined) { - setIsOpen(true); - } - }, []); - - return ( - - {children} - - - {cancelText ?? '취소'} - - - {okText ?? '완료'} - - - - ); -}; - -const StyledModalFooter = styled.footer` - display: flex; - gap: 14px; - align-items: center; - justify-content: center; - margin-top: 42px; -`; - -const StyledButton = styled(Button)` - border-radius: 12px; - padding: 14px 44px; -`; - -export default ConfirmModal; - -function createConfirmElement(props: ConfirmModalProps) { - const divTarget = document.createElement('div'); - divTarget.id = 'makers-confirm'; - document.body.appendChild(divTarget); - const root = createRoot(divTarget); - root.render( - - { - removeConfirmElement(divTarget, root); - props.onClose?.(); - }} - /> - , - ); -} - -function removeConfirmElement(target: HTMLElement, root: Root) { - if (target && target.parentNode) { - root.unmount(); - target.parentNode.removeChild(target); - } -} - -export function Confirm(props: ConfirmModalProps): Promise { - return new Promise((resolve) => { - let resolved = false; - createConfirmElement({ - ...props, - onConfirm: () => { - resolved = true; - resolve(true); - props.onConfirm?.(); - }, - onCancel: () => { - resolved = true; - resolve(false); - props.onCancel?.(); - }, - onClose: () => { - if (!resolved) { - resolve(undefined); - } - props.onClose?.(); - }, - }); - }); -} diff --git a/src/components/common/Modal/index.stories.tsx b/src/components/common/Modal/index.stories.tsx index 45391d78d..0e95cb44a 100644 --- a/src/components/common/Modal/index.stories.tsx +++ b/src/components/common/Modal/index.stories.tsx @@ -1,8 +1,6 @@ import { Meta } from '@storybook/react'; import Button from '@/components/common/Button'; -import { Alert } from '@/components/common/Modal/Alert'; -import { Confirm } from '@/components/common/Modal/Confirm'; import useModalState from '@/components/common/Modal/useModalState'; import Modal from '.'; @@ -18,51 +16,19 @@ export const Default = { return ( <> - + + + 모달 제목! +
안녕하세요, 반가워요.
+ + 버튼1 + 닫기 + +
+
); }, name: '기본', }; - -export const ConfirmModal = { - render: () => { - const onConfirm = async () => { - const result = await Confirm({ - title: '컨펌 모달', - }); - if (result) { - alert('confirm!'); - } - }; - - return ( - <> - - - ); - }, - - name: '컨펌', -}; - -export const AlertModal = { - render: () => { - return ( - <> - - - ); - }, - - name: '알럿', -}; diff --git a/src/components/common/Modal/index.tsx b/src/components/common/Modal/index.tsx index 24ef10860..c65a872b5 100644 --- a/src/components/common/Modal/index.tsx +++ b/src/components/common/Modal/index.tsx @@ -1,96 +1,83 @@ import styled from '@emotion/styled'; +import * as Dialog from '@radix-ui/react-dialog'; import { colors } from '@sopt-makers/colors'; -import FocusTrap from 'focus-trap-react'; -import { FC, HTMLAttributes, PropsWithChildren, ReactNode, useEffect, useRef } from 'react'; -import { RemoveScroll } from 'react-remove-scroll'; +import { m } from 'framer-motion'; +import dynamic from 'next/dynamic'; +import { FC, HTMLAttributes, PropsWithChildren, ReactNode } from 'react'; -import Portal from '@/components/common/Portal'; -import useOnClickOutside from '@/hooks/useOnClickOutside'; -import IconModalCheck from '@/public/icons/icon-modal-check.svg'; +import { ModalButton, ModalContent, ModalDescription, ModalFooter, ModalTitle } from '@/components/common/Modal/parts'; import IconModalClose from '@/public/icons/icon-modal-close.svg'; import { MOBILE_MEDIA_QUERY } from '@/styles/mediaQuery'; -import { textStyles } from '@/styles/typography'; + +const DialogPortal = dynamic(() => import('@radix-ui/react-dialog').then((mod) => mod.Portal), { + ssr: false, +}); export interface ModalProps extends PropsWithChildren> { - confirmIcon?: boolean; - title?: string; - content?: ReactNode; + children?: ReactNode; isOpen?: boolean; - width?: number; - className?: string; onClose: () => void; + hideCloseButton?: boolean; } -const Modal: FC = (props) => { - const { confirmIcon, children, title = '', content, isOpen, onClose, width, ...restProps } = props; - const modalRef = useRef(null); - - useEffect(() => { - const keydownHandler = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - onClose(); - } - }; - window.addEventListener('keydown', keydownHandler); - return () => { - window.removeEventListener('keydown', keydownHandler); - }; - }, [onClose]); - - useOnClickOutside(modalRef, onClose); +const ModalComponent: FC = (props) => { + const { children, hideCloseButton, isOpen, onClose, ...restProps } = props; if (!isOpen) { return null; } return ( - - - - - - - - - - {confirmIcon && } - {title && {title}} - {content && {content}} + + + + + + {children} - - - - - - + {!hideCloseButton && ( + + + + )} + + + + + + ); }; +const Modal = Object.assign(ModalComponent, { + Title: ModalTitle, + Content: ModalContent, + Button: ModalButton, + Description: ModalDescription, + Footer: ModalFooter, +}); + export default Modal; -const StyledBackground = styled.div<{ visible?: boolean }>` +const StyledBackground = styled(Dialog.Overlay)` display: flex; position: fixed; - top: 0; - left: 0; + inset: 0; align-items: center; justify-content: center; z-index: 99999; background-color: rgb(0 0 0 / 30%); - width: 100%; - height: 100%; `; -const StyledModal = styled.div<{ width?: number }>` +const StyledModalContainer = styled(Dialog.Content)` position: relative; - z-index: 101; - border-radius: 22.94px; + border-radius: 22px; background: ${colors.gray800}; - width: ${({ width }) => width ?? 450}px; + max-width: calc(100vw - 60px); color: ${colors.gray10}; `; -const StyledCloseButton = styled.button` +const StyledCloseButton = styled(Dialog.Close)` display: flex; position: absolute; top: 22px; @@ -107,26 +94,3 @@ const StyledCloseButton = styled.button` `; const StyledIconClose = styled(IconModalClose)``; - -const StyledIconCheck = styled(IconModalCheck)` - margin-bottom: 18px; -`; - -const ModalContent = styled.div` - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 40px; -`; - -const StyledTitle = styled.h1` - ${textStyles.SUIT_24_B} -`; - -const StyledContent = styled.div` - margin-top: 18px; - color: ${colors.gray200}; - - ${textStyles.SUIT_18_M}; -`; diff --git a/src/components/common/Modal/parts/ModalButton.tsx b/src/components/common/Modal/parts/ModalButton.tsx new file mode 100644 index 000000000..103ad45e1 --- /dev/null +++ b/src/components/common/Modal/parts/ModalButton.tsx @@ -0,0 +1,31 @@ +import styled from '@emotion/styled'; +import * as Dialog from '@radix-ui/react-dialog'; +import { colors } from '@sopt-makers/colors'; +import { ComponentPropsWithoutRef, forwardRef } from 'react'; + +import { textStyles } from '@/styles/typography'; + +interface ModalCloseButtonProps extends ComponentPropsWithoutRef { + action?: 'normal' | 'close'; +} + +const ModalButton = forwardRef(({ action = 'normal', ...props }, ref) => { + const As = action === 'normal' ? 'button' : Dialog.Close; + + return ; +}); + +export default ModalButton; + +const StyledButton = styled.button` + display: flex; + align-items: center; + justify-content: center; + border-radius: 10px; + background-color: ${colors.gray700}; + padding: 12px 20px; + height: 48px; + color: ${colors.gray10}; + + ${textStyles.SUIT_16_SB}; +`; diff --git a/src/components/common/Modal/parts/index.tsx b/src/components/common/Modal/parts/index.tsx new file mode 100644 index 000000000..06f1aff9c --- /dev/null +++ b/src/components/common/Modal/parts/index.tsx @@ -0,0 +1,47 @@ +import { css } from '@emotion/react'; +import styled from '@emotion/styled'; + +import { textStyles } from '@/styles/typography'; + +export { default as ModalButton } from './ModalButton'; + +export const ModalContent = styled.div` + display: flex; + flex-direction: column; + padding: 20px; +`; + +export const ModalTitle = styled.h1` + margin-bottom: 12px; + line-height: 24px; + + ${textStyles.SUIT_18_B} +`; + +export const ModalDescription = styled.div` + ${textStyles.SUIT_14_R}; +`; + +export const ModalFooter = styled.div<{ align: 'left' | 'right' | 'stretch' }>` + display: grid; + grid-auto-flow: column; + column-gap: 8px; + margin-top: 24px; + + ${(props) => + props.align === 'stretch' && + css` + grid-auto-columns: minmax(10px, 1fr); + `} + ${(props) => + props.align === 'left' && + css` + grid-auto-columns: max-content; + `} + ${(props) => + props.align === 'right' && + css` + grid-auto-columns: max-content; + justify-content: end; + `} +`; diff --git a/src/components/common/Modal/useAlert.tsx b/src/components/common/Modal/useAlert.tsx new file mode 100644 index 000000000..8ea029ccc --- /dev/null +++ b/src/components/common/Modal/useAlert.tsx @@ -0,0 +1,45 @@ +import styled from '@emotion/styled'; +import { useOverlay } from '@toss/use-overlay'; +import { ReactNode, useCallback } from 'react'; + +import Modal from '@/components/common/Modal'; + +const useAlert = () => { + const { open, close } = useOverlay(); + + const alert = useCallback( + (options: { title: ReactNode; description: ReactNode }) => + new Promise((resolve) => { + open(({ isOpen, close }) => ( + { + resolve(false); + close(); + }} + > + + {options.title} + {options.description} + + 확인 + + + + )); + }), + [open], + ); + + return { alert, close }; +}; + +export default useAlert; + +const StyledModalContent = styled(Modal.Content)` + min-width: 320px; +`; + +const StyleModalDescription = styled.div` + width: 100%; +`; diff --git a/src/components/common/Modal/useConfirm.tsx b/src/components/common/Modal/useConfirm.tsx new file mode 100644 index 000000000..3631cee7e --- /dev/null +++ b/src/components/common/Modal/useConfirm.tsx @@ -0,0 +1,60 @@ +import styled from '@emotion/styled'; +import { colors } from '@sopt-makers/colors'; +import { useOverlay } from '@toss/use-overlay'; +import { ReactNode, useCallback } from 'react'; + +import Modal from '@/components/common/Modal'; + +const useConfirm = () => { + const { open, close } = useOverlay(); + + const confirm = useCallback( + (options: { + title: ReactNode; + description: ReactNode; + cancelButtonText: string; + okButtonText: string; + okButtonColor?: string; + }) => + new Promise((resolve) => { + open(({ isOpen, close }) => ( + { + resolve(false); + close(); + }} + > + + {options.title} + {options.description} + + {options.cancelButtonText} + resolve(true)}> + {options.okButtonText} + + + + + )); + }), + [open], + ); + + return { confirm, close }; +}; + +export default useConfirm; + +const StyledModalContent = styled(Modal.Content)` + min-width: 320px; +`; + +const StyledOkButton = styled(Modal.Button)<{ color?: string }>` + background-color: ${(props) => props.color ?? colors.white}; + color: ${colors.black}; +`; + +const StyleModalDescription = styled.div` + width: 100%; +`; diff --git a/src/components/feed/upload/UsingRules/UsingRulesButton/index.stories.tsx b/src/components/feed/upload/UsingRules/UsingRulesButton/index.stories.tsx new file mode 100644 index 000000000..05ad7f4b0 --- /dev/null +++ b/src/components/feed/upload/UsingRules/UsingRulesButton/index.stories.tsx @@ -0,0 +1,12 @@ +import { Meta } from '@storybook/react'; + +import UsingRulesButton from '@/components/feed/upload/UsingRules/UsingRulesButton'; + +export default { + component: UsingRulesButton, +} as Meta; + +export const Default = { + args: {}, + name: '기본', +}; diff --git a/src/components/feed/upload/UsingRules/UsingRulesButton/index.tsx b/src/components/feed/upload/UsingRules/UsingRulesButton/index.tsx new file mode 100644 index 000000000..9f5cc3b69 --- /dev/null +++ b/src/components/feed/upload/UsingRules/UsingRulesButton/index.tsx @@ -0,0 +1,49 @@ +import styled from '@emotion/styled'; +import { colors } from '@sopt-makers/colors'; + +import Responsive from '@/components/common/Responsive'; +import HelpIc from '@/public/icons/icon_help.svg'; +import ArrowIc from '@/public/icons/icon_more.svg'; +import { MOBILE_MEDIA_QUERY } from '@/styles/mediaQuery'; +import { textStyles } from '@/styles/typography'; + +export default function UsingRulesButton() { + return ( + <> + + + + +

커뮤니티 이용규칙

+
+
+ + +

커뮤니티 이용규칙

+ +
+
+
+ + ); +} + +const ShowMoreButton = styled.button` + display: flex; + align-items: center; + + ${textStyles.SUIT_13_R}; + + color: ${colors.gray300}; +`; + +const ButtonWrapper = styled.div` + display: flex; + gap: 4px; + align-items: center; + height: 22px; + + @media ${MOBILE_MEDIA_QUERY} { + height: 16px; + } +`; diff --git a/src/components/feed/upload/UsingRules/UsingRulesPreview/index.stories.tsx b/src/components/feed/upload/UsingRules/UsingRulesPreview/index.stories.tsx new file mode 100644 index 000000000..1376994d6 --- /dev/null +++ b/src/components/feed/upload/UsingRules/UsingRulesPreview/index.stories.tsx @@ -0,0 +1,12 @@ +import { Meta } from '@storybook/react'; + +import UsingRulesPreview from '@/components/feed/upload/UsingRules/UsingRulesPreview'; + +export default { + component: UsingRulesPreview, +} as Meta; + +export const Default = { + args: {}, + name: '기본', +}; diff --git a/src/components/feed/upload/UsingRules/UsingRulesPreview/index.tsx b/src/components/feed/upload/UsingRules/UsingRulesPreview/index.tsx new file mode 100644 index 000000000..0d5908168 --- /dev/null +++ b/src/components/feed/upload/UsingRules/UsingRulesPreview/index.tsx @@ -0,0 +1,81 @@ +import styled from '@emotion/styled'; +import { Content, Overlay, Portal, Root } from '@radix-ui/react-dialog'; +import { colors } from '@sopt-makers/colors'; +import { fonts } from '@sopt-makers/fonts'; +import { useEffect, useState } from 'react'; + +import Responsive from '@/components/common/Responsive'; +import { COMMUNITY_RULES_PREVIEW } from '@/components/feed/upload/UsingRules/constants'; +import BubbleTip from '@/public/icons/polygon.svg'; +import { textStyles } from '@/styles/typography'; + +export default function UsingRulesPreview() { + const [isOpen, setIsOpen] = useState(true); + + const handlePreviewModal = () => { + setIsOpen((isOpen) => !isOpen); + }; + + useEffect(() => { + setTimeout(() => { + setIsOpen(false); + }, 5000); + }, []); + + return ( + <> + + + + + + + {COMMUNITY_RULES_PREVIEW} + + + + + + {COMMUNITY_RULES_PREVIEW} + + + ); +} + +const PreviewBox = styled(Content)` + display: flex; + position: fixed; + right: 107px; + flex-direction: column; + width: 358px; + height: 166px; +`; + +const RulesDescription = styled.div` + position: absolute; + bottom: 0; + margin-top: -8px; + border-radius: 10px; + background-color: ${colors.gray800}; + padding: 16px; + width: 358px; + word-break: break-all; + color: ${colors.gray50}; + + ${fonts.BODY_13_L}; +`; + +const BubbleTipIcon = styled(BubbleTip)` + position: absolute; + top: 0; + right: 48px; +`; + +const RulesWrapper = styled.div` + margin-bottom: 8px; + width: 100%; + word-break: break-all; + color: ${colors.gray500}; + + ${textStyles.SUIT_12_R} +`; diff --git a/src/components/feed/upload/UsingRules/constants.ts b/src/components/feed/upload/UsingRules/constants.ts new file mode 100644 index 000000000..4923cc834 --- /dev/null +++ b/src/components/feed/upload/UsingRules/constants.ts @@ -0,0 +1,2 @@ +export const COMMUNITY_RULES_PREVIEW = + '플레이그라운드는 건전하고 기분좋은 소통을 지향해요. 서비스에 게시한 게시물로 파생되는 문제에 대해서 해당 게시물을 게시한 본인에게 책임을 물을 수 있습니다. 특히 타인을 비난하거나 저격하는 글을 작성하는 등 커뮤니티 이용규칙을 위반할 경우, 익명 작성자를 식별하는 등 엄격한 조치가 취해질 수 있습니다. 게시물 작성 전 커뮤니티 이용규칙 전문을 반드시 확인하시기 바랍니다.'; diff --git a/src/components/feed/upload/UsingRules/index.stories.tsx b/src/components/feed/upload/UsingRules/index.stories.tsx new file mode 100644 index 000000000..e45f9544f --- /dev/null +++ b/src/components/feed/upload/UsingRules/index.stories.tsx @@ -0,0 +1,12 @@ +import { Meta } from '@storybook/react'; + +import UsingRules from '@/components/feed/upload/UsingRules'; + +export default { + component: UsingRules, +} as Meta; + +export const Default = { + args: {}, + name: '기본', +}; diff --git a/src/components/feed/upload/UsingRules/index.tsx b/src/components/feed/upload/UsingRules/index.tsx new file mode 100644 index 000000000..9d7aeda98 --- /dev/null +++ b/src/components/feed/upload/UsingRules/index.tsx @@ -0,0 +1,11 @@ +import UsingRulesButton from '@/components/feed/upload/UsingRules/UsingRulesButton'; +import UsingRulesPreview from '@/components/feed/upload/UsingRules/UsingRulesPreview'; + +export default function UsingRules() { + return ( + <> + + + + ); +} diff --git a/src/components/members/detail/MessageSection/MessageModal.tsx b/src/components/members/detail/MessageSection/MessageModal.tsx index ca53e579b..a08e773ca 100644 --- a/src/components/members/detail/MessageSection/MessageModal.tsx +++ b/src/components/members/detail/MessageSection/MessageModal.tsx @@ -10,7 +10,7 @@ import { usePostMemberMessageMutation } from '@/api/endpoint_LEGACY/hooks'; import RHFControllerFormItem from '@/components/common/form/RHFControllerFormItem'; import Input from '@/components/common/Input'; import Loading from '@/components/common/Loading'; -import { Alert } from '@/components/common/Modal/Alert'; +import useAlert from '@/components/common/Modal/useAlert'; import Text from '@/components/common/Text'; import TextArea from '@/components/common/TextArea'; import Modal, { ModalProps } from '@/components/members/detail/MessageSection/Modal'; @@ -99,6 +99,7 @@ const MessageModal: FC = ({ }); const isValid = _isValid && Boolean(selectedCategory); const { mutateAsync, isPending } = usePostMemberMessageMutation(); + const { alert } = useAlert(); const onClickCategory = (category: MessageCategory) => { setSelectedCategory(category); @@ -116,9 +117,9 @@ const MessageModal: FC = ({ category: selectedCategory, receiverId, }); - await Alert({ + await alert({ title: '쪽지 보내기', - content: '성공적으로 전송되었어요!', + description: '성공적으로 전송되었어요!', }); onLog?.({ category: selectedCategory }); props.onClose(); diff --git a/src/components/projects/edit/ProjectEdit.tsx b/src/components/projects/edit/ProjectEdit.tsx index bb0810afc..0d91e0d3b 100644 --- a/src/components/projects/edit/ProjectEdit.tsx +++ b/src/components/projects/edit/ProjectEdit.tsx @@ -6,8 +6,8 @@ import { FC, useEffect } from 'react'; import { useGetMemberOfMe } from '@/api/endpoint/members/getMemberOfMe'; import { getProjectById, putProject } from '@/api/endpoint_LEGACY/projects'; import AuthRequired from '@/components/auth/AuthRequired'; -import { Alert } from '@/components/common/Modal/Alert'; -import { Confirm } from '@/components/common/Modal/Confirm'; +import useAlert from '@/components/common/Modal/useAlert'; +import useConfirm from '@/components/common/Modal/useConfirm'; import useToast from '@/components/common/Toast/useToast'; import useEventLogger from '@/components/eventLogger/hooks/useEventLogger'; import ProjectForm from '@/components/projects/upload/form/ProjectForm'; @@ -34,11 +34,15 @@ const ProjectEdit: FC = ({ projectId }) => { const { logSubmitEvent } = useEventLogger(); const router = useRouter(); const toast = useToast(); + const { confirm } = useConfirm(); + const { alert } = useAlert(); const handleSubmit = async (formData: ProjectFormType) => { - const notify = await Confirm({ + const notify = await confirm({ title: '알림', - content: '프로젝트를 수정하시겠습니까?', + description: '프로젝트를 수정하시겠습니까?', + okButtonText: '수정', + cancelButtonText: '취소', }); if (notify && myProfileData) { putProjectMutation( @@ -67,13 +71,14 @@ const ProjectEdit: FC = ({ projectId }) => { useEffect(() => { if (myProfileData && projectData && myProfileData?.id !== projectData?.writerId) { - Alert({ + alert({ title: '알림', - content: '해당 프로젝트를 작성한 유저만 접근 가능한 페이지 입니다.', - onClose: () => router.push(playgroundLink.projectList()), + description: '해당 프로젝트를 작성한 유저만 접근 가능한 페이지 입니다.', + }).then(() => { + router.push(playgroundLink.projectList()); }); } - }, [myProfileData, projectData, router]); + }, [myProfileData, projectData, router, alert]); if (!projectData) { return null; diff --git a/src/components/projects/main/ProjectDetail.tsx b/src/components/projects/main/ProjectDetail.tsx index de1d6fbc1..527d7b0bf 100644 --- a/src/components/projects/main/ProjectDetail.tsx +++ b/src/components/projects/main/ProjectDetail.tsx @@ -3,11 +3,11 @@ import { colors } from '@sopt-makers/colors'; import dayjs from 'dayjs'; import Link from 'next/link'; import { useRouter } from 'next/router'; -import { FC, useMemo, useState } from 'react'; +import { FC, useMemo } from 'react'; import { useGetMemberOfMe } from '@/api/endpoint/members/getMemberOfMe'; import { deleteProject } from '@/api/endpoint_LEGACY/projects'; -import ConfirmModal from '@/components/common/Modal/Confirm'; +import useConfirm from '@/components/common/Modal/useConfirm'; import MemberBlock from '@/components/members/common/MemberBlock'; import WithMemberMetadata from '@/components/members/common/WithMemberMetadata'; import { getLinkInfo } from '@/components/projects/constants'; @@ -49,7 +49,22 @@ const ProjectDetail: FC = ({ projectId }) => { const endAt = project?.endAt ? dayjs(project.endAt).format('YYYY-MM') : ''; const mainImage = project?.images[0]; const sortedMembers = useMemo(() => sortByRole([...(project?.members ?? [])]), [project]); - const [isDeleteConfirmModalOpened, setIsDeleteConfirmModalOpened] = useState(false); + + const { confirm } = useConfirm(); + + const askDelete = async () => { + const result = await confirm({ + title: '프로젝트 삭제', + description: '프로젝트를 정말 삭제하시겠어요?', + okButtonText: '삭제', + okButtonColor: colors.error, + cancelButtonText: '취소', + }); + + if (result) { + handleDeleteProject(); + } + }; const handleDeleteProject = async () => { if (project) { @@ -87,7 +102,7 @@ const ProjectDetail: FC = ({ projectId }) => { {project?.writerId === me?.id && (
project && router.push(playgroundLink.projectEdit(project.id))}>수정하기
-
setIsDeleteConfirmModalOpened(true)}> +
@@ -160,17 +175,6 @@ const ProjectDetail: FC = ({ projectId }) => { ))} - - {isDeleteConfirmModalOpened && ( - setIsDeleteConfirmModalOpened(false)} - onConfirm={handleDeleteProject} - cancelText='삭제' - confirmButtonVariable='danger' - /> - )} ); }; diff --git a/src/components/wordchain/WordchainChatting/Wordchain/index.tsx b/src/components/wordchain/WordchainChatting/Wordchain/index.tsx index 44b68a31d..e0b89283a 100644 --- a/src/components/wordchain/WordchainChatting/Wordchain/index.tsx +++ b/src/components/wordchain/WordchainChatting/Wordchain/index.tsx @@ -6,12 +6,11 @@ import TrophyIcon from 'public/icons/icon-trophy.svg'; import { useGetMemberOfMe } from '@/api/endpoint/members/getMemberOfMe'; import { useGetCurrentWinnerName } from '@/api/endpoint/wordchain/getWordchain'; import { useNewGameMutation } from '@/api/endpoint/wordchain/newGame'; -import { Confirm } from '@/components/common/Modal/Confirm'; +import useConfirm from '@/components/common/Modal/useConfirm'; import useEventLogger from '@/components/eventLogger/hooks/useEventLogger'; import StartWordChatMessage from '@/components/wordchain/WordchainChatting/StartWordChatMessage'; import { Word } from '@/components/wordchain/WordchainChatting/types'; import WordChatMessage from '@/components/wordchain/WordchainChatting/WordChatMessage'; -import { legacyColors } from '@/styles/colors'; import { MOBILE_MEDIA_QUERY } from '@/styles/mediaQuery'; import { textStyles } from '@/styles/typography'; @@ -48,21 +47,22 @@ export default function Wordchain({ initial, order, wordList, isProgress, winner const { data: currentWinnerName } = useGetCurrentWinnerName(); const { mutate } = useNewGameMutation(); const { data: me } = useGetMemberOfMe(); + const { confirm } = useConfirm(); const queryClient = useQueryClient(); const onClickGiveUp = async () => { - const confirm = await Confirm({ + const confirmResult = await confirm({ title: '정말 포기하시겠어요?', - content: `지금 포기하면 '${currentWinnerName ?? ''}'님이 우승자가 돼요.`, - cancelText: '돌아가기', - okText: '새로 시작하기', + description: `지금 포기하면 '${currentWinnerName ?? ''}'님이 우승자가 돼요.`, + cancelButtonText: '돌아가기', + okButtonText: '새로 시작하기', }); - if (confirm) { + if (confirmResult) { mutate(undefined, { onSuccess: () => { logSubmitEvent('wordchainNewGame'); queryClient.invalidateQueries({ - queryKey: ['getWordchainWinners'] + queryKey: ['getWordchainWinners'], }); }, }); diff --git a/src/components/wordchain/WordchainChatting/index.tsx b/src/components/wordchain/WordchainChatting/index.tsx index d91632659..9d93de3c0 100644 --- a/src/components/wordchain/WordchainChatting/index.tsx +++ b/src/components/wordchain/WordchainChatting/index.tsx @@ -53,14 +53,11 @@ export default function WordchainChatting({ className }: WordchainChattingProps) const { data: activeWordchain } = useGetActiveWordchain(); - const scrollToBottom = useCallback( - () => () => { - if (wordchainListRef.current) { - scrollTo(wordchainListRef.current.scrollHeight); - } - }, - [], - ); + const scrollToBottom = useCallback(() => { + if (wordchainListRef.current) { + scrollTo(wordchainListRef.current.scrollHeight); + } + }, []); useEffect(() => { setTimeout(() => { diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 898db09c7..3429d301b 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -2,6 +2,7 @@ import ProgressBar from '@badrap/bar-of-progress'; import { colors } from '@sopt-makers/colors'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { OverlayProvider } from '@toss/use-overlay'; import { LazyMotion } from 'framer-motion'; import type { AppProps } from 'next/app'; import dynamic from 'next/dynamic'; @@ -44,6 +45,26 @@ function MyApp({ Component, pageProps }: AppProps) { }; }, [router.events]); + useEffect(() => { + const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); + const $existingMeta = document.querySelector('meta[name="viewport"]'); + + /** 모바일 환경에서 input focus 시 화면이 확대되는 현상을 막아요 */ + if (isMobile) { + const $meta = $existingMeta ?? document.createElement('meta'); + + $meta.setAttribute('name', 'viewport'); + $meta.setAttribute( + 'content', + 'width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0', + ); + + if (!$existingMeta) { + document.head.appendChild($meta); + } + } + }, []); + return ( - - - + + + + + {DEBUG && } diff --git a/src/pages/projects/upload/index.tsx b/src/pages/projects/upload/index.tsx index 137387726..4ba06b109 100644 --- a/src/pages/projects/upload/index.tsx +++ b/src/pages/projects/upload/index.tsx @@ -4,7 +4,7 @@ import { useRouter } from 'next/router'; import { useGetMemberOfMe } from '@/api/endpoint/members/getMemberOfMe'; import AuthRequired from '@/components/auth/AuthRequired'; -import { Confirm } from '@/components/common/Modal/Confirm'; +import useConfirm from '@/components/common/Modal/useConfirm'; import useToast from '@/components/common/Toast/useToast'; import useEventLogger from '@/components/eventLogger/hooks/useEventLogger'; import ProjectForm from '@/components/projects/upload/form/ProjectForm'; @@ -23,11 +23,14 @@ const ProjectUploadPage = () => { const toast = useToast(); const queryClient = useQueryClient(); const { logSubmitEvent } = useEventLogger(); + const { confirm } = useConfirm(); const handleSubmit = async (formData: ProjectFormType) => { - const notify = await Confirm({ + const notify = await confirm({ title: '알림', - content: '프로젝트를 업로드 하시겠습니까?', + description: '프로젝트를 업로드 하시겠습니까?', + okButtonText: '업로드', + cancelButtonText: '취소', }); if (notify && myProfileData) { createProjectMutate(convertToProjectData(formData, myProfileData.id), { diff --git a/src/styles/global.ts b/src/styles/global.ts index 48d478bb8..7fd2c209e 100644 --- a/src/styles/global.ts +++ b/src/styles/global.ts @@ -1,5 +1,6 @@ import { css } from '@emotion/react'; import { colors } from '@sopt-makers/colors'; +import { fontBase } from '@sopt-makers/fonts'; import font from '@/styles/font'; import { MOBILE_MEDIA_QUERY } from '@/styles/mediaQuery'; @@ -9,6 +10,10 @@ export const global = css` ${reset}; ${font} + * { + ${fontBase} + } + :root { color-scheme: dark; } diff --git a/yarn.lock b/yarn.lock index 19cfcc812..219dd51fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5343,6 +5343,13 @@ __metadata: languageName: node linkType: hard +"@sopt-makers/fonts@npm:^1.0.0": + version: 1.0.0 + resolution: "@sopt-makers/fonts@npm:1.0.0" + checksum: 576e3689dfa95ef8b6bbebab646141286cbe93208236e83d61170351ccdd64dd327ef951685248e77a4a201b0971893a3cfd2eabcf426c39f0d8845df9077025 + languageName: node + linkType: hard + "@sopt-makers/playground-common@workspace:playground-common": version: 0.0.0-use.local resolution: "@sopt-makers/playground-common@workspace:playground-common" @@ -6791,6 +6798,17 @@ __metadata: languageName: node linkType: hard +"@toss/use-overlay@npm:^1.3.6": + version: 1.3.6 + resolution: "@toss/use-overlay@npm:1.3.6" + dependencies: + "@types/react": ^18.0.21 + peerDependencies: + react: ^16.8 || ^17 || ^18 + checksum: 90c7cf24a9523cfda5a37a4bc47aab1b4bab764fe3b0f21dcf0527f0f94ccc2b17f9a5c81da76671a5aee0f7bcd6346b9206dd81c33a5cbd3ea4e2027d53048f + languageName: node + linkType: hard + "@toss/utils@npm:^1.4.4": version: 1.4.4 resolution: "@toss/utils@npm:1.4.4" @@ -7337,6 +7355,17 @@ __metadata: languageName: node linkType: hard +"@types/react@npm:^18.0.21": + version: 18.2.34 + resolution: "@types/react@npm:18.2.34" + dependencies: + "@types/prop-types": "*" + "@types/scheduler": "*" + csstype: ^3.0.2 + checksum: 16446542228cba827143caf0ecb4718cbf02ae5befd4a6bc6d67ed144fe1c0cb4b06b20facf3d2b972d86c67a17cc82f5ec8a03fce42d50e12b2dcd0592fc66e + languageName: node + linkType: hard + "@types/scheduler@npm:*": version: 0.16.3 resolution: "@types/scheduler@npm:0.16.3" @@ -18228,6 +18257,7 @@ __metadata: "@radix-ui/react-tabs": ^1.0.2 "@radix-ui/react-tooltip": ^1.0.5 "@sopt-makers/colors": ^3.0.0 + "@sopt-makers/fonts": ^1.0.0 "@storybook/addon-actions": ^7.0.23 "@storybook/addon-docs": ^7.0.23 "@storybook/addon-essentials": ^7.0.23 @@ -18243,6 +18273,7 @@ __metadata: "@testing-library/jest-dom": ^6.1.3 "@testing-library/react": ^14.0.0 "@toss/emotion-utils": ^1.1.10 + "@toss/use-overlay": ^1.3.6 "@types/jest": ^29.4.0 "@types/jscodeshift": ^0 "@types/lodash-es": ^4.17.6