From 158bd62f955fedddeebd84a960ab986e3a446328 Mon Sep 17 00:00:00 2001 From: JinHo Kim <81083461+jinhokim98@users.noreply.github.com> Date: Thu, 10 Oct 2024 16:43:07 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=B4=88=EB=8C=80=ED=95=98=EA=B8=B0?= =?UTF-8?q?=EB=A5=BC=20=EB=A7=81=ED=81=AC=20=EA=B3=B5=EC=9C=A0=EC=99=80=20?= =?UTF-8?q?=EC=B9=B4=EC=B9=B4=EC=98=A4=ED=86=A1=EC=9D=84=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(#719)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 공유버튼 모바일과 데탑 분리 * feat: 송금버튼 새로운 반영사항 적용 * feat: 계좌번호 없을 때 금액 복사 기능 추가 * test: prop 변경으로 인한 스토리북 변경 * feat: 계좌번호 없을 때 금액 복사되는 onCopy 추가 * feat: 다나까 -> 요 체로 변경 * feat: 송금기능 페이지 이동으로 인해 memberId 추가 * feat: 송금 페이지 라우트 * feat: 송금하기 버튼을 눌렀을 때 navigate state로 정보 전달 * feat: 송금 방법(복사, 카카오페이, 토스) 제공 * feat: 아래 => 토스열기 명시적으로 변경 * feat: Banner 컴포넌트 생성 * feat: 세션스토리지 util 추가 * feat: 계좌번호 입력 유도 기능 구현 * feat: Flex div prop도 받도록 설정 * feat: 돌아가는 Chevron 컴포넌트 생성 * feat: 바깥 클릭 시 실행되는 컴포넌트 생성 * feat: Select 컴포넌트 제작 * feat: 요구사항 변경으로 인한 송금 플로우 변경 * feat: Select 컴포넌트 제작 * design: 텍스트 색깔 변경 * style: export 정리 * refactor: ClickOutsideDetector 사용 * feat: dropdown base 미트볼, 버튼 두 개 지원 * style: dropdown list z-index 추가 * feat: 드랍다운 버튼을 클릭할 때 드랍다운 리스트가 닫히는 기능 구현 * feat: 모바일에서 링크 초대와 카카오톡 초대 분리 * feat: 공유 메시지 변경 --- .../Design/components/Dropdown/ButtonBase.tsx | 38 ++++++++++++ .../components/Dropdown/Dropdown.stories.tsx | 8 +++ .../components/Dropdown/Dropdown.style.ts | 59 +++++++++++++----- .../Design/components/Dropdown/Dropdown.tsx | 48 +++++++-------- .../components/Dropdown/Dropdown.type.ts | 5 ++ .../components/Dropdown/DropdownButton.tsx | 13 +++- .../components/Dropdown/MeatballBase.tsx | 39 ++++++++++++ .../Design/components/Dropdown/useDropdown.ts | 31 ++-------- client/src/components/Design/token/zIndex.ts | 5 +- .../DesktopShareEventButton.tsx | 33 +++++----- .../MobileShareEventButton.tsx | 28 ++++++--- client/src/hooks/useShareEvent.ts | 60 ++++--------------- .../src/pages/EventPage/EventPageLayout.tsx | 7 +-- 13 files changed, 225 insertions(+), 149 deletions(-) create mode 100644 client/src/components/Design/components/Dropdown/ButtonBase.tsx create mode 100644 client/src/components/Design/components/Dropdown/MeatballBase.tsx diff --git a/client/src/components/Design/components/Dropdown/ButtonBase.tsx b/client/src/components/Design/components/Dropdown/ButtonBase.tsx new file mode 100644 index 000000000..8c1c553d0 --- /dev/null +++ b/client/src/components/Design/components/Dropdown/ButtonBase.tsx @@ -0,0 +1,38 @@ +/** @jsxImportSource @emotion/react */ +import {useTheme} from '@components/Design/theme/HDesignProvider'; + +import Button from '../Button/Button'; +import Flex from '../Flex/Flex'; + +import {dropdownButtonBaseStyle} from './Dropdown.style'; +import {DropdownProps} from './Dropdown.type'; +import DropdownButton from './DropdownButton'; + +type ButtonBaseProps = DropdownProps & { + isOpen: boolean; + setIsOpen: React.Dispatch>; + dropdownRef: React.RefObject; +}; + +const ButtonBase = ({isOpen, setIsOpen, dropdownRef, baseButtonText, children}: ButtonBaseProps) => { + const {theme} = useTheme(); + + return ( + <> + + {isOpen && ( +
+ + {children.map((button, index) => ( + + ))} + +
+ )} + + ); +}; + +export default ButtonBase; diff --git a/client/src/components/Design/components/Dropdown/Dropdown.stories.tsx b/client/src/components/Design/components/Dropdown/Dropdown.stories.tsx index 2504ecae7..bf324f8f7 100644 --- a/client/src/components/Design/components/Dropdown/Dropdown.stories.tsx +++ b/client/src/components/Design/components/Dropdown/Dropdown.stories.tsx @@ -19,7 +19,15 @@ const meta = { ), ], + argTypes: { + base: { + description: '', + control: {type: 'select'}, + options: ['meatballs', 'button'], + }, + }, args: { + baseButtonText: '정산 초대하기', children: [ alert('전체 참여자 관리 클릭')} />, alert('계좌번호 입력하기 클릭')} />, diff --git a/client/src/components/Design/components/Dropdown/Dropdown.style.ts b/client/src/components/Design/components/Dropdown/Dropdown.style.ts index 0074a5be6..e26863620 100644 --- a/client/src/components/Design/components/Dropdown/Dropdown.style.ts +++ b/client/src/components/Design/components/Dropdown/Dropdown.style.ts @@ -4,21 +4,50 @@ import {Theme} from '@components/Design/theme/theme.type'; import {FlexProps} from '../Flex/Flex.type'; -export const dropdownStyle: FlexProps = { - flexDirection: 'column', - width: '12.5rem', - padding: '0.5rem', - paddingInline: '0.5rem', - gap: '0.25rem', - backgroundColor: 'white', - - cssProp: { - position: 'absolute', - top: '2rem', - right: '-1rem', - borderRadius: '0.75rem', - boxShadow: '2px 4px 16px 0 rgba(0, 0, 0, 0.08)', - }, +export const dropdownBaseStyle = css({ + position: 'relative', + + WebkitTapHighlightColor: 'transparent', +}); + +export const dropdownStyle = (theme: Theme): FlexProps => { + return { + flexDirection: 'column', + width: '12.5rem', + padding: '0.5rem', + paddingInline: '0.5rem', + gap: '0.25rem', + backgroundColor: 'white', + + cssProp: { + position: 'absolute', + top: '2rem', + right: '-1rem', + borderRadius: '0.75rem', + boxShadow: '2px 4px 16px 0 rgba(0, 0, 0, 0.08)', + zIndex: theme.zIndex.dropdownList, + }, + }; +}; + +export const dropdownButtonBaseStyle = (theme: Theme): FlexProps => { + return { + flexDirection: 'column', + width: '12.5rem', + padding: '0.5rem', + paddingInline: '0.5rem', + gap: '0.25rem', + backgroundColor: 'white', + + cssProp: { + position: 'absolute', + top: '2.5rem', + right: '-0.5rem', + borderRadius: '0.75rem', + boxShadow: '2px 4px 16px 0 rgba(0, 0, 0, 0.08)', + zIndex: theme.zIndex.dropdownList, + }, + }; }; export const dropdownButtonStyle = (theme: Theme) => diff --git a/client/src/components/Design/components/Dropdown/Dropdown.tsx b/client/src/components/Design/components/Dropdown/Dropdown.tsx index 63045dd56..ab5fe23e4 100644 --- a/client/src/components/Design/components/Dropdown/Dropdown.tsx +++ b/client/src/components/Design/components/Dropdown/Dropdown.tsx @@ -1,35 +1,33 @@ /** @jsxImportSource @emotion/react */ -import Icon from '../Icon/Icon'; -import IconButton from '../IconButton/IconButton'; -import Flex from '../Flex/Flex'; +import ClickOutsideDetector from '../ClickOutsideDetector'; import useDropdown from './useDropdown'; import {DropdownProps} from './Dropdown.type'; -import DropdownButton from './DropdownButton'; -import {dropdownStyle} from './Dropdown.style'; +import MeatballBase from './MeatballBase'; +import ButtonBase from './ButtonBase'; +import {dropdownBaseStyle} from './Dropdown.style'; -const Dropdown = ({children}: DropdownProps) => { - const {isOpen, openDropdown, meetBallsRef, dropdownRef} = useDropdown(); - const isDropdownOpen = isOpen && meetBallsRef.current; +const Dropdown = ({base = 'meatballs', baseButtonText, children}: DropdownProps) => { + const {isOpen, setIsOpen, baseRef, dropdownRef} = useDropdown(); + const isDropdownOpen = isOpen && !!baseRef.current; return ( - - - {isDropdownOpen && ( -
- - {children.map(button => ( - - ))} - -
- )} -
+ setIsOpen(false)}> +
+ {base === 'meatballs' && ( + + )} + {base === 'button' && ( + + )} +
+
); }; diff --git a/client/src/components/Design/components/Dropdown/Dropdown.type.ts b/client/src/components/Design/components/Dropdown/Dropdown.type.ts index 1b67bbc33..5d8c3855f 100644 --- a/client/src/components/Design/components/Dropdown/Dropdown.type.ts +++ b/client/src/components/Design/components/Dropdown/Dropdown.type.ts @@ -1,7 +1,12 @@ +export type DropdownBase = 'meatballs' | 'button'; + export type DropdownButtonProps = React.HTMLAttributes & { text: string; + setIsOpen?: React.Dispatch>; // 내부에서 사용하기 위한 props 외부에서 넣어주지 말 것 }; export type DropdownProps = { + base?: DropdownBase; + baseButtonText?: string; children: React.ReactElement[]; }; diff --git a/client/src/components/Design/components/Dropdown/DropdownButton.tsx b/client/src/components/Design/components/Dropdown/DropdownButton.tsx index 005a6042a..8e3981654 100644 --- a/client/src/components/Design/components/Dropdown/DropdownButton.tsx +++ b/client/src/components/Design/components/Dropdown/DropdownButton.tsx @@ -7,10 +7,19 @@ import Text from '../Text/Text'; import {dropdownButtonStyle} from './Dropdown.style'; import {DropdownButtonProps} from './Dropdown.type'; -const DropdownButton = ({text, ...buttonProps}: DropdownButtonProps) => { +const DropdownButton = ({text, onClick, setIsOpen, ...buttonProps}: DropdownButtonProps) => { const {theme} = useTheme(); + return ( - - + ); }; diff --git a/client/src/components/ShareEventButton/MobileShareEventButton.tsx b/client/src/components/ShareEventButton/MobileShareEventButton.tsx index 8526380e4..368dd611a 100644 --- a/client/src/components/ShareEventButton/MobileShareEventButton.tsx +++ b/client/src/components/ShareEventButton/MobileShareEventButton.tsx @@ -1,14 +1,28 @@ -import {Button} from '@components/Design'; +import toast from '@hooks/useToast/toast'; -type MobileShareEventButtonProps = React.HTMLAttributes & { - text: string; +import {Dropdown, DropdownButton} from '@components/Design'; + +type MobileShareEventButtonProps = { + copyShare: () => Promise; + kakaoShare: () => void; }; -const MobileShareEventButton = ({text, ...buttonProps}: MobileShareEventButtonProps) => { +const MobileShareEventButton = ({copyShare, kakaoShare}: MobileShareEventButtonProps) => { + const copyAndToast = async () => { + await copyShare(); + toast.confirm('링크가 복사되었어요 :) \n참여자들에게 링크를 공유해 주세요!', { + showingTime: 3000, + position: 'bottom', + }); + }; + return ( - +
+ + + + +
); }; diff --git a/client/src/hooks/useShareEvent.ts b/client/src/hooks/useShareEvent.ts index 7e2b6a39f..406357dc2 100644 --- a/client/src/hooks/useShareEvent.ts +++ b/client/src/hooks/useShareEvent.ts @@ -1,33 +1,26 @@ -import {useNavigate} from 'react-router-dom'; - -import {Event} from 'types/serviceType'; - -import {useAuthStore} from '@store/authStore'; - import getEventIdByUrl from '@utils/getEventIdByUrl'; import getEventPageUrlByEnvironment from '@utils/getEventPageUrlByEnvironment'; -import getDeletedLastPath from '@utils/getDeletedLastPath'; - -import toast from './useToast/toast'; type UserShareEventProps = { - event: Event; - isMobile: boolean; + eventName: string; }; -const useShareEvent = ({event, isMobile}: UserShareEventProps) => { - const {eventName, bankName, accountNumber} = event; +const useShareEvent = ({eventName}: UserShareEventProps) => { const eventId = getEventIdByUrl(); const url = getEventPageUrlByEnvironment(eventId, 'home'); - const navigate = useNavigate(); - const {isAdmin} = useAuthStore(); const shareInfo = { - title: `[행동대장]\n${eventName}에 대한 정산을 시작할게요:)`, + title: `행동대장이 ${eventName}에\n대한 정산을 요청했어요 :)`, text: '아래 링크에 접속해서 정산 내역을 확인해 주세요!', url, }; + const shareText = `${shareInfo.title}\n${shareInfo.text}\n${url}`; + + const copyShare = async () => { + await window.navigator.clipboard.writeText(shareText); + }; + const kakaoShare = () => { window.Kakao.Share.sendDefault({ objectType: 'feed', @@ -45,40 +38,9 @@ const useShareEvent = ({event, isMobile}: UserShareEventProps) => { }); }; - const shareText = `${shareInfo.title}\n${shareInfo.text}\n${url}`; - - const onShareButtonClick = () => { - const isReady = bankName !== '' && accountNumber !== ''; - - // induceBankInfoBeforeShare - if (!isReady && isAdmin) { - toast.error('잠깐! 정산을 초대하기 전에\n계좌를 등록해주세요', { - showingTime: 3000, - position: 'bottom', - }); - - const navigatePath = `${getDeletedLastPath(location.pathname)}/admin/edit`; - navigate(navigatePath); - return; - } - - if (!isReady && !isAdmin) { - toast.error('정산자가 계좌를 등록해야 초대 가능합니다.\n정산자에게 문의해주세요', { - showingTime: 3000, - position: 'bottom', - }); - return; - } - - // 모바일이 아닌 기기는 단순 텍스트 복사 - // 모바일 기기에서는 카카오톡 공유를 사용 - if (!isMobile) return; - kakaoShare(); - }; - return { - shareText, - onShareButtonClick: kakaoShare, + kakaoShare, + copyShare, }; }; diff --git a/client/src/pages/EventPage/EventPageLayout.tsx b/client/src/pages/EventPage/EventPageLayout.tsx index dfc7e42ec..59bac904f 100644 --- a/client/src/pages/EventPage/EventPageLayout.tsx +++ b/client/src/pages/EventPage/EventPageLayout.tsx @@ -24,8 +24,7 @@ const EventPageLayout = () => { }; const isMobile = isMobileDevice(); - - const {shareText, onShareButtonClick} = useShareEvent({event, isMobile}); + const {kakaoShare, copyShare} = useShareEvent({eventName: event.eventName}); return ( @@ -40,9 +39,9 @@ const EventPageLayout = () => { {isMobile ? ( - + ) : ( - + 정산 초대하기 )}