Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

이미지 다운로드 모바일 기기 대응 #424

Merged
merged 5 commits into from
Jul 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions src/components/modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { type ComponentProps, type MouseEvent, type PropsWithChildren } from 'react';
import { css, type Theme } from '@emotion/react';
import { m } from 'framer-motion';

import Header from '~/components/header/Header';
import { defaultFadeInVariants } from '~/constants/motions';
import useScrollLock from '~/hooks/common/useScrollLock';

import AnimatePortal from '../portal/AnimatePortal';

interface Props {
/**
* 외부영역 클릭시 호출될 함수
*/
onClickOutside?: VoidFunction;
}

/**
*
* @param isShowing 열림/닫힘 상태
* @param mode AnimatePresence mode
* @param onClickOutside 외부영역 클릭시 호출될 함수
*/
const Modal = ({
isShowing,
mode,
onClickOutside,
children,
}: PropsWithChildren<Props> & ComponentProps<typeof AnimatePortal>) => {
useScrollLock({ lock: isShowing });

return (
<AnimatePortal isShowing={isShowing} mode={mode}>
<div css={dialogPositionCss}>
<ModalBlur onClickOutside={onClickOutside} />
<div css={containerCss}>{children}</div>
</div>
</AnimatePortal>
);
};

const dialogPositionCss = css`
position: absolute;
top: 0;
left: 0;

display: flex;
align-items: center;
justify-content: center;

width: 100%;
height: 100vh;
`;

const ModalBlur = ({ onClickOutside }: Pick<Props, 'onClickOutside'>) => {
const onClickOutsideDefault = (e: MouseEvent) => {
if (e.target !== e.currentTarget) return;
if (onClickOutside) onClickOutside();
};

return (
<m.div
onClick={onClickOutsideDefault}
css={blurCss}
variants={defaultFadeInVariants}
initial="initial"
animate="animate"
exit="exit"
/>
);
};

const blurCss = (theme: Theme) => css`
position: fixed;
z-index: ${theme.zIndex.backdrop};
top: 0;
left: 0;

width: 100%;
height: 100%;

background-color: #d8e3ff99;
backdrop-filter: blur(12.5px);
`;

const containerCss = (theme: Theme) => css`
position: fixed;
z-index: ${theme.zIndex.modal};

display: flex;
flex-direction: column;

max-width: ${theme.size.maxWidth};

border-radius: 16px;
`;

const ModalHeader = ({ ...props }: ComponentProps<typeof Header>) => {
return <Header {...props} />;
};

Modal.Header = ModalHeader;

export default Modal;
95 changes: 86 additions & 9 deletions src/pages/dna/LoadedDna.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { type FC } from 'react';
/* eslint-disable @next/next/no-img-element */
import { type FC, useState } from 'react';
import Image from 'next/image';
import { css, type Theme } from '@emotion/react';
import { useQueryClient } from '@tanstack/react-query';
import { m } from 'framer-motion';

import { type Softskills } from '~/components/graphic/softskills/type';
import Header from '~/components/header/Header';
import DownloadCircleIcon from '~/components/icons/DownloadCircleIcon';
import HomeIcon from '~/components/icons/HomeIcon';
import Modal from '~/components/modal/Modal';
import { type DNA } from '~/constants/dna';
import BookmarkSection from '~/features/dna/BookmarkSection';
import DnaBanner from '~/features/dna/DnaBanner';
Expand All @@ -18,7 +21,8 @@ import type useGetUserInfoBySurveyId from '~/hooks/api/user/useGetUserInfoBySurv
import { getUserInfoBySurveyIdQueryKey } from '~/hooks/api/user/useGetUserInfoBySurveyId';
import useInternalRouter from '~/hooks/router/useInternalRouter';
import { BODY_1, HEAD_2_BOLD } from '~/styles/typo';
import { imageDownloadPC } from '~/utils/image';
import { detectMobileDevice, getBrowser } from '~/utils/browser';
import { imageDownloadPC, imageShare } from '~/utils/image';
import { type Group } from '~/utils/resultLogic';

import { type DnaOwnerStatus } from './type';
Expand Down Expand Up @@ -65,13 +69,26 @@ const LoadedDna: FC<Props> = ({
onSuccess: () => queryClient.invalidateQueries(getUserInfoBySurveyIdQueryKey(surveyId)),
});

const onDownloadClick = () => {
const [isImageModalShowing, setIsImageModalShowing] = useState(false);

const onDownloadClick = async () => {
const imageObj = JSON.parse(downloadableImageBase64);
const imageBase64 = 'data:image/png;base64,' + imageObj.base64 ?? '';
// if (detectMobileDevice(window.navigator.userAgent)) {
// return;
// }
imageDownloadPC(imageBase64, 'dna');
const browser = getBrowser();

if (typeof navigator.share !== 'undefined') {
const isImageShared = await imageShare(imageBase64);

if (isImageShared) return;
}

if (!detectMobileDevice() || browser === 'Safari') {
imageDownloadPC(imageBase64, 'dna');

return;
}

setIsImageModalShowing(true);
};

return (
Expand All @@ -93,8 +110,7 @@ const LoadedDna: FC<Props> = ({
<source srcSet={IMAGE_BY_GROUP[group].webp} type="image/webp" />
<Image priority unoptimized css={dnaImageCss} src={IMAGE_BY_GROUP[group].png} alt="DNA 이미지" fill />
</picture>
{/* {dnaOwnerStatus === 'current_user' */}
{false && (
{dnaOwnerStatus === 'current_user' && (
<button type="button" css={downloadIconCss} onClick={onDownloadClick}>
<DownloadCircleIcon />
</button>
Expand Down Expand Up @@ -145,6 +161,12 @@ const LoadedDna: FC<Props> = ({
<BookmarkSection bookmarkedFeedbacks={bookmarkedFeedbacks} dnaOwnerStatus={dnaOwnerStatus} />
<DnaCta surveyId={surveyId} dnaOwnerStatus={dnaOwnerStatus} userInfo={userInfo} />
</main>

<DNAImageDownloadModal
downloadableImageBase64={downloadableImageBase64}
isShowing={isImageModalShowing}
onClose={() => setIsImageModalShowing(false)}
/>
</>
);
};
Expand Down Expand Up @@ -212,3 +234,58 @@ const downloadIconCss = css`
right: -2px;
bottom: -5px;
`;

const DNAImageDownloadModal = ({
downloadableImageBase64,
onClose,
isShowing,
}: {
downloadableImageBase64: string;
onClose: () => void;
isShowing: boolean;
}) => {
const imageObj = JSON.parse(downloadableImageBase64);
const imageBase64 = 'data:image/png;base64,' + imageObj.base64 ?? '';

return (
<Modal isShowing={isShowing}>
<Modal.Header onBackClick={onClose} overrideCss={imageDownloadModalHeaderCss} />
<m.div
css={imageDownloadModalCss}
variants={{
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
}}
initial="initial"
animate="animate"
exit="exit"
>
<h1>꾹 눌러서 이미지를 저장하세요</h1>
<m.img src={imageBase64} alt="dna" />
</m.div>
</Modal>
);
};

const imageDownloadModalHeaderCss = css`
background-color: transparent;
border-bottom: none;
`;

const imageDownloadModalCss = css`
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 16px;
align-items: center;

h1 {
${HEAD_2_BOLD};
}

img {
touch-action: none;
width: 80%;
}
`;
85 changes: 0 additions & 85 deletions src/pages/result/test.page.tsx

This file was deleted.

54 changes: 54 additions & 0 deletions src/utils/browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* @description 모바일 디바이스 환경인지 감지합니다.
* @example
* ```ts
* detectMobileDevice(window.navigator.userAgent)
* ```
* @returns boolean
*/

export const detectMobileDevice = (): boolean => {
if (!window) return false;

const agent = window.navigator.userAgent;
const mobileRegex = [/Android/i, /iPhone/i, /iPad/i, /iPod/i, /BlackBerry/i, /Windows Phone/i];

return mobileRegex.some((mobile) => agent.match(mobile));
};

const browsers = [
'Chrome',
'Opera',
'WebTV',
'Whale',
'Beonex',
'Chimera',
'NetPositive',
'Phoenix',
'Firefox',
'Safari',
'SkipStone',
'Netscape',
'Mozilla',
];

/**
* @description 브라우저 종류를 알아내는 함수입니다.
* @returns string - 브라우저 이름 or 'Other'
*/

export const getBrowser = () => {
if (!window) return null;

const userAgent = window.navigator.userAgent.toLowerCase();

if (userAgent.includes('edg')) {
return 'Edge';
}

if (userAgent.includes('trident') || userAgent.includes('msie')) {
return 'Internet Explorer';
}

return browsers.find((browser) => userAgent.includes(browser.toLowerCase())) || 'Other';
};
7 changes: 0 additions & 7 deletions src/utils/device.ts

This file was deleted.

Loading
Loading