diff --git a/src/components/MyPage/organisms/StatusMenuList/index.tsx b/src/components/MyPage/organisms/StatusMenuList/index.tsx index c531820f..b72758f9 100644 --- a/src/components/MyPage/organisms/StatusMenuList/index.tsx +++ b/src/components/MyPage/organisms/StatusMenuList/index.tsx @@ -1,7 +1,7 @@ import { memo, useRef } from 'react'; import { DefaultData } from '#types/index'; -import Span from '@atoms/Span'; +import { NoService } from '@atoms/icon'; import MenuBtn from '@molecules/MenuBtn'; import ProductItemList from 'src/components/Shop/Organisms/ProductItemList'; import { useDragScroll, useSearch } from 'src/hooks'; @@ -46,7 +46,7 @@ function StatusMenuList(listProps: Props) { /> ) : (
- 서비스 준비중입니다. +
)} diff --git a/src/components/MyPage/organisms/UserProfile/style.module.scss b/src/components/MyPage/organisms/UserProfile/style.module.scss index 03ff7db0..3d6f6396 100644 --- a/src/components/MyPage/organisms/UserProfile/style.module.scss +++ b/src/components/MyPage/organisms/UserProfile/style.module.scss @@ -5,7 +5,6 @@ flex-direction: column; .profile { padding: 13.5px 0; - border: 0; } } .total-deal { diff --git a/src/components/Product/organisms/ProductFooter/index.tsx b/src/components/Product/organisms/ProductFooter/index.tsx index 47524353..b9cca94a 100644 --- a/src/components/Product/organisms/ProductFooter/index.tsx +++ b/src/components/Product/organisms/ProductFooter/index.tsx @@ -3,7 +3,7 @@ import { useState } from 'react'; import { ProductFooterInfo } from '#types/product'; import type { DefaultProps } from '#types/props'; import Button from '@atoms/Button'; -import { ClickHeart, SmallHeart, Time, Views } from '@atoms/icon'; +import { ClickHeart, ClipBoard, SmallHeart, Time, Views } from '@atoms/icon'; import IconText from '@atoms/IconText'; import Span from '@atoms/Span'; import DialogModal from '@templates/DialogModal'; @@ -12,6 +12,7 @@ import useTimeForToday from 'src/hooks/useTimeForToday'; import { toastSuccess } from 'src/utils/toaster'; import $ from './style.module.scss'; +import copyClipBoard from './utils'; type Props = { footer: ProductFooterInfo; @@ -24,6 +25,7 @@ export default function ProductFooter(footerProps: Props) { const [isOpen, setIsOpen] = useState(false); const openModal = () => setIsOpen(true); const closeModal = () => setIsOpen(false); + const handleClipBoard = copyClipBoard(); const handleClickLike = () => toastSuccess({ message: '좋아요 기능 준비중입니다.' }); @@ -88,9 +90,18 @@ export default function ProductFooter(footerProps: Props) { isOpen={isOpen} title="아래 정보를 통해 연락할 수 있습니다." content="현재 채팅 기능 준비중이에요. 서비스 준비 전까지 조금만 기다려주세요." - emphasisContent={contact} clickText="닫기" onClick={closeModal} + emphasisContent={contact} + emphasisIcon={ + + } /> diff --git a/src/components/Product/organisms/ProductFooter/style.module.scss b/src/components/Product/organisms/ProductFooter/style.module.scss index d75dca48..ef5b0520 100644 --- a/src/components/Product/organisms/ProductFooter/style.module.scss +++ b/src/components/Product/organisms/ProductFooter/style.module.scss @@ -53,3 +53,17 @@ } } } + +.clip-board { + box-sizing: border-box; + margin-left: 10px; + border: 1.5px solid $gray-280; + border-radius: $border-radius; + background-color: $white; + &:hover { + border-color: rgba($primary, 0.7); + svg { + stroke: rgba($primary, 0.7); + } + } +} diff --git a/src/components/Product/organisms/ProductFooter/utils.ts b/src/components/Product/organisms/ProductFooter/utils.ts new file mode 100644 index 00000000..4422e760 --- /dev/null +++ b/src/components/Product/organisms/ProductFooter/utils.ts @@ -0,0 +1,16 @@ +import { toastSuccess, toastError } from 'src/utils/toaster'; + +type onCopyFn = (text: string) => void; + +function copyClipBoard(): onCopyFn { + return async (text: string) => { + try { + await navigator.clipboard.writeText(text); + toastSuccess({ message: '클립보드에 복사되었습니다.' }); + } catch (error) { + toastError({ message: '클립보드 복사에 실패했습니다.' }); + } + }; +} + +export default copyClipBoard; diff --git a/src/components/Product/organisms/ProfileInfo/index.tsx b/src/components/Product/organisms/ProfileInfo/index.tsx new file mode 100644 index 00000000..3a8a49aa --- /dev/null +++ b/src/components/Product/organisms/ProfileInfo/index.tsx @@ -0,0 +1,33 @@ +import Button from '@atoms/Button'; +import { Share } from '@atoms/icon'; +import Profile from '@molecules/Profile'; + +import $ from './style.module.scss'; +import { getCurrentUrl, sharePage } from './utils'; + +type Props = { + title: string; + userId: number; + profileImage: string; + name: string; +}; + +function ProfileInfo(profileProps: Props) { + const { title, userId, profileImage, name } = profileProps; + const currentUrl = getCurrentUrl(); + const handleShare = sharePage({ + title: `re:Fashion | ${title} | 상품 상세 정보`, + url: currentUrl, + }); + + return ( +
+ + +
+ ); +} + +export default ProfileInfo; diff --git a/src/components/Product/organisms/ProfileInfo/style.module.scss b/src/components/Product/organisms/ProfileInfo/style.module.scss new file mode 100644 index 00000000..1ee89d94 --- /dev/null +++ b/src/components/Product/organisms/ProfileInfo/style.module.scss @@ -0,0 +1,19 @@ +.profile { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid $profile-border; +} + +.share { + margin: 0 23px; + border: 1.5px solid $gray-280; + border-radius: $border-radius; + &:hover { + border-color: rgba($primary, 0.7); + path { + fill: rgba($primary, 0.7); + } + } +} diff --git a/src/components/Product/organisms/ProfileInfo/utils.ts b/src/components/Product/organisms/ProfileInfo/utils.ts new file mode 100644 index 00000000..95c58273 --- /dev/null +++ b/src/components/Product/organisms/ProfileInfo/utils.ts @@ -0,0 +1,20 @@ +import copyClipBoard from '../ProductFooter/utils'; + +export function getCurrentUrl() { + if (typeof window !== 'undefined') + return `${window.location.protocol}//${window.location.host}${window.location.pathname}`; + return ''; +} + +export function sharePage(props: { title: string; url: string }): () => void { + return async () => { + try { + navigator.share({ + title: props.title, + url: props.url, + }); + } catch (err) { + copyClipBoard()(props.url); + } + }; +} diff --git a/src/components/Shop/Organisms/ProductItemList/NoProduct.view.tsx b/src/components/Shop/Organisms/ProductItemList/NoProduct.view.tsx index 8621330e..aa88ef86 100644 --- a/src/components/Shop/Organisms/ProductItemList/NoProduct.view.tsx +++ b/src/components/Shop/Organisms/ProductItemList/NoProduct.view.tsx @@ -1,4 +1,4 @@ -import Span from '@atoms/Span'; +import { NoProduct } from '@atoms/icon'; import $ from './style.module.scss'; @@ -13,7 +13,7 @@ function NoProductView({ isNoProducts, isLoading, isFetching, height }: Props) { return ( (!isLoading && !isFetching && isNoProducts && (
- 상품 결과가 없습니다. +
)) || null diff --git a/src/components/Shop/molecules/HeaderTool/index.tsx b/src/components/Shop/molecules/HeaderTool/index.tsx index dd2f5a17..7ec8391e 100644 --- a/src/components/Shop/molecules/HeaderTool/index.tsx +++ b/src/components/Shop/molecules/HeaderTool/index.tsx @@ -53,7 +53,6 @@ function HeaderTool(headerProps: Props) { {...{ onQueryChange: onClick }} isGender hasId - isSameCodeName options={data} selected={selectedMenu} name="gender" diff --git a/src/components/shared/atoms/icon/ClipBoard.tsx b/src/components/shared/atoms/icon/ClipBoard.tsx new file mode 100644 index 00000000..54028228 --- /dev/null +++ b/src/components/shared/atoms/icon/ClipBoard.tsx @@ -0,0 +1,24 @@ +import type { IconProps } from '#types/props'; + +function ClipBoard({ className, style, stroke }: IconProps) { + return ( + + + + + + ); +} +export { ClipBoard }; diff --git a/src/components/shared/atoms/icon/NoProduct.tsx b/src/components/shared/atoms/icon/NoProduct.tsx new file mode 100644 index 00000000..44a411df --- /dev/null +++ b/src/components/shared/atoms/icon/NoProduct.tsx @@ -0,0 +1,59 @@ +import type { IconProps } from '#types/props'; + +function NoProduct({ className, style }: IconProps) { + return ( + + + + + + + + + + + + + ); +} +export { NoProduct }; diff --git a/src/components/shared/atoms/icon/NoService.tsx b/src/components/shared/atoms/icon/NoService.tsx new file mode 100644 index 00000000..76e9be6a --- /dev/null +++ b/src/components/shared/atoms/icon/NoService.tsx @@ -0,0 +1,67 @@ +import type { IconProps } from '#types/props'; + +function NoService({ className, style }: IconProps) { + return ( + + + + + + + + + + + + + + + ); +} +export { NoService }; diff --git a/src/components/shared/atoms/icon/Share.tsx b/src/components/shared/atoms/icon/Share.tsx new file mode 100644 index 00000000..239a8fa2 --- /dev/null +++ b/src/components/shared/atoms/icon/Share.tsx @@ -0,0 +1,20 @@ +import type { IconProps } from '#types/props'; + +function Share({ className, style, fill }: IconProps) { + return ( + + + + ); +} +export { Share }; diff --git a/src/components/shared/atoms/icon/index.tsx b/src/components/shared/atoms/icon/index.tsx index 8050e20f..b10b5d5e 100644 --- a/src/components/shared/atoms/icon/index.tsx +++ b/src/components/shared/atoms/icon/index.tsx @@ -3,6 +3,7 @@ import { Chat } from './Chat'; import { Check } from './Check'; import { Circle } from './Circle'; import { ClickHeart } from './ClickHeart'; +import { ClipBoard } from './ClipBoard'; import { Close } from './Close'; import { Filter } from './Filter'; import { Google } from './Google'; @@ -11,6 +12,8 @@ import { ImgClose } from './ImgClose'; import { Kakao } from './Kakao'; import { More } from './More'; import { MyPage } from './MyPage'; +import { NoProduct } from './NoProduct'; +import { NoService } from './NoService'; import { OnePiece } from './OnePiece'; import { Pants } from './Pants'; import { Plus } from './Plus'; @@ -20,6 +23,7 @@ import { Search } from './Search'; import { SearchBlack } from './SearchBlack'; import { SelectArrow } from './SelectArrow'; import { Setting } from './Setting'; +import { Share } from './Share'; import { Shop } from './Shop'; import { Skirt } from './Skirt'; import { SmallHeart } from './SmallHeart'; @@ -62,4 +66,8 @@ export { Home, Circle, Star, + ClipBoard, + Share, + NoProduct, + NoService, }; diff --git a/src/components/shared/molecules/Profile/style.module.scss b/src/components/shared/molecules/Profile/style.module.scss index 143c5b10..9a3042ed 100644 --- a/src/components/shared/molecules/Profile/style.module.scss +++ b/src/components/shared/molecules/Profile/style.module.scss @@ -2,7 +2,6 @@ display: flex; align-items: center; padding: 12px 16px 12px 23px; - border-bottom: 1px solid $profile-border; &-box { display: flex; align-items: center; diff --git a/src/components/shared/templates/DialogModal/index.tsx b/src/components/shared/templates/DialogModal/index.tsx index 71d8e875..a7957935 100644 --- a/src/components/shared/templates/DialogModal/index.tsx +++ b/src/components/shared/templates/DialogModal/index.tsx @@ -1,11 +1,12 @@ -import { memo } from 'react'; +import React, { memo } from 'react'; import Button from '@atoms/Button'; import Span from '@atoms/Span'; -import { URL_REGEX } from '@constants/regExp'; +import { MAILTO, SMS } from '@constants/link'; +import { EMAIL_REGEX, PHONE_REGEX, URL_REGEX } from '@constants/regExp'; import { Modal } from '@templates/Modal'; import classnames from 'classnames'; -import { isSameRegExpCondition } from 'src/utils/regExp'; +import { isSameMultipleRegExp } from 'src/utils/regExp'; import $ from './style.module.scss'; @@ -17,6 +18,7 @@ type Props = { title?: string; content?: string | JSX.Element; emphasisContent?: string; + emphasisIcon?: JSX.Element; clickText?: string; cancelText?: string; onCancel?: () => void; @@ -25,9 +27,16 @@ type Props = { function DialogModal(dialogProps: Props) { const { id, label, isOpen, isVerticalBtn, onCancel, onClick } = dialogProps; - const { title, content, clickText, cancelText, emphasisContent } = - dialogProps; - const isUrl = isSameRegExpCondition(URL_REGEX, emphasisContent || ''); + const { title, content, clickText, cancelText } = dialogProps; + const { emphasisContent, emphasisIcon } = dialogProps; + const [isUrl, isPhone, isEmail] = isSameMultipleRegExp( + [URL_REGEX, PHONE_REGEX, EMAIL_REGEX], + emphasisContent || '', + ); + const isContactType = isUrl || isPhone || isEmail; + const smsStr = isPhone ? SMS : ''; + const mailStr = isEmail ? MAILTO : ''; + const strContent = (smsStr || mailStr) + emphasisContent; return ( @@ -38,19 +47,22 @@ function DialogModal(dialogProps: Props) { {title &&

{title}

} {content && {content}} - {emphasisContent && - (isUrl ? ( - - {emphasisContent} - - ) : ( -

{emphasisContent}

- ))} +
+ {emphasisContent && + (isContactType ? ( + + {emphasisContent} + + ) : ( +

{emphasisContent}

+ ))} + {emphasisContent && emphasisIcon && emphasisIcon} +
{ const isIncludeOtherStatus = includedStatusCodes?.some( (code) => code === status, ); - const renderCondition = !redirectUrl || isIncludeOtherStatus; if (redirectUrl && !isIncludeOtherStatus) { router.replace(redirectUrl); } @@ -94,7 +93,7 @@ class ErrorBoundary extends React.Component { return ; } - if (hasError && error !== null && renderCondition) { + if (hasError && error !== null) { return errorFallback({ error, reset: this.resetBoundary, diff --git a/src/components/shared/templates/Skeleton/Filter/style.module.scss b/src/components/shared/templates/Skeleton/Filter/style.module.scss index 678b3623..c27743e1 100644 --- a/src/components/shared/templates/Skeleton/Filter/style.module.scss +++ b/src/components/shared/templates/Skeleton/Filter/style.module.scss @@ -1,5 +1,6 @@ .skeleton-filter { display: flex; + margin-left: 20px; .filter { @include iconLoading(); } diff --git a/src/constants/link.ts b/src/constants/link.ts index e3bc9d4c..5ea71b8d 100644 --- a/src/constants/link.ts +++ b/src/constants/link.ts @@ -1 +1,4 @@ -export const KAKAO_TALK_ONE_TO_ONE_CHAT = 'http://pf.kakao.com/_jNwnxj/chat'; +const KAKAO_TALK_ONE_TO_ONE_CHAT = 'http://pf.kakao.com/_jNwnxj/chat'; +const MAILTO = 'mailto:'; +const SMS = 'sms:'; +export { KAKAO_TALK_ONE_TO_ONE_CHAT, MAILTO, SMS }; diff --git a/src/constants/regExp.ts b/src/constants/regExp.ts index c0cb3bd7..ecf2e92e 100644 --- a/src/constants/regExp.ts +++ b/src/constants/regExp.ts @@ -1,2 +1,6 @@ -export const URL_REGEX = - /^(https?:\/\/)?([da-z.-]+).([a-z.]{2,6})([/\w_.#-]*)*$/; +const URL_REGEX = /^(https?:\/\/)([da-z.-]+).([a-z.]{2,6})([/\w_.#-]*)*$/; +const PHONE_REGEX = /^\d{3,4}-?\d{3,4}-?\d{4}$/; +const EMAIL_REGEX = + /^[\da-zA-Z]([-_.]?[\da-zA-Z])*@[0-9a-zA-Z]([-_.]?[\da-zA-Z])*.[a-zA-Z]{2,3}$/i; + +export { URL_REGEX, PHONE_REGEX, EMAIL_REGEX }; diff --git a/src/hooks/api/login/index.ts b/src/hooks/api/login/index.ts index 11b7f92a..049f6d0c 100644 --- a/src/hooks/api/login/index.ts +++ b/src/hooks/api/login/index.ts @@ -2,6 +2,7 @@ import { useRouter } from 'next/router'; import { postAuthToken, authTest } from 'src/api/login'; import { setAccessToken } from 'src/utils/auth'; +import { getPrevPath } from 'src/utils/pathStorage'; import { toastError, toastSuccess } from 'src/utils/toaster'; import { useCoreMutation, useCoreQuery } from '../core'; @@ -14,7 +15,7 @@ export const usePostAuthToken = () => { toastSuccess({ message: '로그인되었습니다.' }); const { accessToken } = data.data; setAccessToken(accessToken); - router.replace('/'); + router.replace(getPrevPath()); }, onSettled: (_, error) => { if (error) { diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 888b05ad..0c2ea536 100755 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -16,6 +16,7 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import '../styles/globals.scss'; import { useMounted, useWindowResize } from 'src/hooks'; import * as gtag from 'src/lib/gtag'; +import { setPathValue } from 'src/utils/pathStorage'; export type NextPageWithLayout = NextPage & { getLayout?: (page: ReactElement) => ReactNode; @@ -54,6 +55,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { useEffect(() => { const handleRouteChange = (url: string) => { + setPathValue(url); gtag.pageview(url); }; router.events.on('routeChangeComplete', handleRouteChange); diff --git a/src/pages/mypage/setting/index.tsx b/src/pages/mypage/setting/index.tsx index e79a6f1b..c81c5b3d 100644 --- a/src/pages/mypage/setting/index.tsx +++ b/src/pages/mypage/setting/index.tsx @@ -42,13 +42,13 @@ function Setting() { }, { text: '앱 버전', - icon: v1.0.1, + icon: v1.0.6, }, { text: '로그아웃', onClick: () => { logoutUtil(); - router.push('/shop'); + router.push('/'); }, }, { diff --git a/src/pages/shop/[id]/index.tsx b/src/pages/shop/[id]/index.tsx index d22602a0..0f929263 100644 --- a/src/pages/shop/[id]/index.tsx +++ b/src/pages/shop/[id]/index.tsx @@ -5,7 +5,6 @@ import { useCallback } from 'react'; import HeadMeta from '@atoms/HeadMeta'; import { queryKey } from '@constants/react-query'; import { seoData } from '@constants/seo'; -import Profile from '@molecules/Profile'; import { dehydrate, QueryClient } from '@tanstack/react-query'; import Layout from '@templates/Layout'; import { withGetServerSideProps } from 'src/api/core/withGetServerSideProps'; @@ -16,6 +15,7 @@ import ProductFooter from 'src/components/Product/organisms/ProductFooter'; import ProductImgSlide from 'src/components/Product/organisms/ProductImgSlide'; import ProductNotice from 'src/components/Product/organisms/ProductNotice'; import ProductSize from 'src/components/Product/organisms/ProductSize'; +import ProfileInfo from 'src/components/Product/organisms/ProfileInfo'; import { useProdutDetail } from 'src/hooks/api/product'; import { useSearchStore } from 'src/store/useSearchStore'; @@ -66,7 +66,10 @@ function ShopDetail({ id }: { id: string }) { - + +
diff --git a/src/utils/pathStorage.ts b/src/utils/pathStorage.ts new file mode 100644 index 00000000..47c706fb --- /dev/null +++ b/src/utils/pathStorage.ts @@ -0,0 +1,15 @@ +export function getPrevPath() { + return localStorage.getItem('prevPath') || '/'; +} + +export function getCurrentPath() { + return localStorage.getItem('currentPath') || '/'; +} + +export function setPathValue(currentUrl: string) { + const prevPath = getCurrentPath() || '/'; + if (prevPath !== currentUrl) { + localStorage.setItem('prevPath', prevPath); + localStorage.setItem('currentPath', currentUrl); + } +} diff --git a/src/utils/regExp.ts b/src/utils/regExp.ts index eb807081..cb2cca42 100644 --- a/src/utils/regExp.ts +++ b/src/utils/regExp.ts @@ -1,2 +1,6 @@ export const isSameRegExpCondition = (condition: RegExp, str: string) => condition.test(str); + +export const isSameMultipleRegExp = (regExps: RegExp[], str: string) => { + return regExps.map((regExp) => regExp.test(str)); +};