diff --git a/.env.d.ts b/.env.d.ts index 59530b1..17a8293 100644 --- a/.env.d.ts +++ b/.env.d.ts @@ -1,5 +1,6 @@ declare namespace NodeJS { interface ProcessEnv { API_HOST: string + NEXT_PUBLIC_KAKAO_API_KEY: string } } diff --git a/package-lock.json b/package-lock.json index 7403e19..53a6608 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,9 +15,9 @@ "eslint-import-resolver-typescript": "^3.6.1", "next": "14.2.4", "next-auth": "^5.0.0-beta.20", - "path-to-regexp": "^7.1.0", "prettier-plugin-tailwindcss": "^0.6.5", "react": "^18", + "react-device-detect": "^2.2.3", "react-dom": "^18", "tailwind-merge": "^2.3.0", "tailwind-scrollbar-hide": "^1.1.7" @@ -17210,14 +17210,6 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true }, - "node_modules/path-to-regexp": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-7.1.0.tgz", - "integrity": "sha512-ZToe+MbUF4lBqk6dV8GKot4DKfzrxXsplOddH8zN3YK+qw9/McvP7+4ICjZvOne0jQhN4eJwHsX6tT0Ns19fvw==", - "engines": { - "node": ">=16" - } - }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -18087,6 +18079,18 @@ "react": "^16.3.0 || ^17.0.1 || ^18.0.0" } }, + "node_modules/react-device-detect": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-device-detect/-/react-device-detect-2.2.3.tgz", + "integrity": "sha512-buYY3qrCnQVlIFHrC5UcUoAj7iANs/+srdkwsnNjI7anr3Tt7UY6MqNxtMLlr0tMBied0O49UZVK8XKs3ZIiPw==", + "dependencies": { + "ua-parser-js": "^1.0.33" + }, + "peerDependencies": { + "react": ">= 0.14.0", + "react-dom": ">= 0.14.0" + } + }, "node_modules/react-docgen": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-7.0.3.tgz", @@ -20995,6 +20999,28 @@ "node": ">=14.17" } }, + "node_modules/ua-parser-js": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.38.tgz", + "integrity": "sha512-Aq5ppTOfvrCMgAPneW1HfWj66Xi7XL+/mIy996R1/CLS/rcyJQm6QZdsKrUeivDFQ+Oc9Wyuwor8Ze8peEoUoQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "engines": { + "node": "*" + } + }, "node_modules/ufo": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz", diff --git a/package.json b/package.json index 4e1011d..8fcb54e 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "next-auth": "^5.0.0-beta.20", "prettier-plugin-tailwindcss": "^0.6.5", "react": "^18", + "react-device-detect": "^2.2.3", "react-dom": "^18", "tailwind-merge": "^2.3.0", "tailwind-scrollbar-hide": "^1.1.7" diff --git a/public/icons/download.svg b/public/icons/download.svg new file mode 100644 index 0000000..9fc4a4a --- /dev/null +++ b/public/icons/download.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/public/icons/error.svg b/public/icons/error.svg new file mode 100644 index 0000000..0924e70 --- /dev/null +++ b/public/icons/error.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/icons/sns/sns-facebook.svg b/public/icons/sns/sns-facebook.svg new file mode 100644 index 0000000..e19134d --- /dev/null +++ b/public/icons/sns/sns-facebook.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/icons/sns/sns-ig-bg.png b/public/icons/sns/sns-ig-bg.png new file mode 100644 index 0000000..1cc2441 Binary files /dev/null and b/public/icons/sns/sns-ig-bg.png differ diff --git a/public/icons/sns/sns-ig.svg b/public/icons/sns/sns-ig.svg new file mode 100644 index 0000000..458e3b8 --- /dev/null +++ b/public/icons/sns/sns-ig.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/icons/sns/sns-kakao.svg b/public/icons/sns/sns-kakao.svg new file mode 100644 index 0000000..4270d6c --- /dev/null +++ b/public/icons/sns/sns-kakao.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/public/icons/sns/sns-x.svg b/public/icons/sns/sns-x.svg new file mode 100644 index 0000000..9a8d05c --- /dev/null +++ b/public/icons/sns/sns-x.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/app/(board)/board/[boardId]/_components/Share/Section.tsx b/src/app/(board)/board/[boardId]/_components/Share/Section.tsx new file mode 100644 index 0000000..d47c71f --- /dev/null +++ b/src/app/(board)/board/[boardId]/_components/Share/Section.tsx @@ -0,0 +1,49 @@ +import { ComponentProps, MouseEventHandler, ReactNode } from 'react' +import { twMerge } from 'tailwind-merge' + +interface SectionProps { + title: string + children: ReactNode +} + +const Section = ({ title, children }: SectionProps) => ( +
+

{title}

+
+ {children} +
+
+) + +const Item = ({ + icon, + desc = '', + bg = '', + onClick = () => {}, +}: { + icon: ReactNode + desc?: string + bg?: ComponentProps<'div'>['className'] + onClick?: MouseEventHandler +}) => { + return ( + + ) +} + +Section.Item = Item +export default Section diff --git a/src/app/(board)/board/[boardId]/_components/Share/index.tsx b/src/app/(board)/board/[boardId]/_components/Share/index.tsx new file mode 100644 index 0000000..c136565 --- /dev/null +++ b/src/app/(board)/board/[boardId]/_components/Share/index.tsx @@ -0,0 +1,95 @@ +'use client' + +import Modal from '@/components/Modal' +import CopyIcon from 'public/icons/copy.svg' +import Share from 'public/icons/ios_share.svg' +import TwoPolaroidsIcon from 'public/icons/linkShare.svg' +import { useEffect, useState } from 'react' +import DownloadIcon from 'public/icons/download.svg' +import KakaoIcon from 'public/icons/sns/sns-kakao.svg' +import IGIcon from 'public/icons/sns/sns-ig.svg' +import XIcon from 'public/icons/sns/sns-x.svg' +import FacebookIcon from 'public/icons/sns/sns-facebook.svg' +import Section from './Section' +import { useTutorial } from '../Tutorial/TutorialContext' +import useSnsShare from '../../_hooks/useSnsShare' + +const ShareBtn = () => { + const [showShareModal, setShowShareModal] = useState(false) + const [currentURL, setCurrentURL] = useState('') + const { shareToKakao, shareToInsta, shareToFacebook, shareToX } = + useSnsShare() + + useEffect(() => { + setCurrentURL(window.location.href) + }, []) + + const copyLink = () => { + return navigator.clipboard.writeText(currentURL) + } + + const { run, nextStep } = useTutorial() + + const handleClose = () => { + setShowShareModal(false) + if (run) { + nextStep() + } + } + + const handleShare = (shareFn: () => void) => { + shareFn() + setShowShareModal(false) + } + + return ( + <> + setShowShareModal(true)} className="w-6" /> + + + }> + + 보드를 친구에게 공유해보세요! +
+
+ } + bg="bg-gray-900" + desc="링크 복사" + onClick={() => handleShare(copyLink)} + /> + } + bg="bg-kakao" + desc="카카오톡" + onClick={() => handleShare(shareToKakao)} + /> + } + bg="bg-[url('/icons/sns/sns-ig-bg.png')]" + desc="인스타그램" + onClick={() => handleShare(shareToInsta)} + /> + } + bg="bg-[#000]" + desc="X" + onClick={() => handleShare(shareToX)} + /> + } + bg="bg-facebook" + desc="페이스북" + onClick={() => handleShare(shareToFacebook)} + /> +
+
+ } bg="bg-gray-200" /> +
+ + + + ) +} + +export default ShareBtn diff --git a/src/app/(board)/board/[boardId]/_components/ShareBtn.tsx b/src/app/(board)/board/[boardId]/_components/ShareBtn.tsx deleted file mode 100644 index 8c78234..0000000 --- a/src/app/(board)/board/[boardId]/_components/ShareBtn.tsx +++ /dev/null @@ -1,57 +0,0 @@ -'use client' - -import Modal from '@/components/Modal' -import CopyIcon from 'public/icons/copy.svg' -import Share from 'public/icons/ios_share.svg' -import TwoPolaroidsIcon from 'public/icons/twopolaroids.svg' -import { useEffect, useState } from 'react' -import { useTutorial } from './Tutorial/TutorialContext' - -const ShareBtn = () => { - const [showShareModal, setShowShareModal] = useState(false) - const [currentURL, setCurrentURL] = useState('') - - useEffect(() => { - setCurrentURL(window.location.href) - }, []) - - const copyLink = () => { - return navigator.clipboard.writeText(currentURL) - } - - const { run, nextStep } = useTutorial() - const handleClose = () => { - setShowShareModal(false) - if (run) { - nextStep() - } - } - - return ( - <> - setShowShareModal(true)} className="w-6" /> - - - }> - - 보드를 친구에게 공유해보세요! - -
{currentURL}
-
- - - - 복사하기 - - } - onConfirm={copyLink} - /> -
-
- - ) -} - -export default ShareBtn diff --git a/src/app/(board)/board/[boardId]/_hooks/useSnsShare.ts b/src/app/(board)/board/[boardId]/_hooks/useSnsShare.ts new file mode 100644 index 0000000..9cac3a9 --- /dev/null +++ b/src/app/(board)/board/[boardId]/_hooks/useSnsShare.ts @@ -0,0 +1,83 @@ +'use client' + +import { useEffect } from 'react' +import { isDevMode } from '@/lib/utils/env' +import { isIOS, isAndroid } from 'react-device-detect' + +const useSnsShare = () => { + useEffect(() => { + const script = document.createElement('script') + script.src = 'https://t1.kakaocdn.net/kakao_js_sdk/2.7.2/kakao.min.js' + script.async = true + script.integrity = + 'sha384-TiCUE00h649CAMonG018J2ujOgDKW/kVWlChEuu4jK2vxfAAD0eZxzCKakxg55G4' + script.crossOrigin = 'anonymous' + + document.body.appendChild(script) + + return () => { + document.body.removeChild(script) + } + }, []) + + const shareToKakao = () => { + const { Kakao, location } = window + if (Kakao === undefined) { + return + } + + if (!Kakao.isInitialized()) { + Kakao.init(process.env.NEXT_PUBLIC_KAKAO_API_KEY) + } + + Kakao.Share.sendDefault({ + objectType: 'feed', + content: { + title: 'POLABO | 함께 꾸미는 폴라로이드 보드, 폴라보', + description: '내 보드를 우리의 소중한 추억들로 꾸며줘!', + imageUrl: '/images/opengraph-image.png', + link: { + mobileWebUrl: isDevMode + ? 'https://dev.polabo.site' + : 'https://polabo.site', + webUrl: isDevMode ? 'https://dev.polabo.site' : 'https://polabo.site', + }, + }, + buttons: [ + { + title: '웹으로 보기', + link: { + mobileWebUrl: location.href, + webUrl: location.href, + }, + }, + ], + }) + } + + const shareToInsta = () => { + let url + if (isIOS) { + url = 'https://www.instagram.com/create/story' + } else if (isAndroid) { + url = + 'intent://instagram.com/#Intent;scheme=https;package=com.instagram.android;end' + } else { + url = 'https://www.instagram.com/' + } + + window.open(url) + } + + const shareToFacebook = () => { + window.open(`https://www.facebook.com/sharer.php?u=${window.location.href}`) + } + + const shareToX = () => { + window.open(`https://twitter.com/intent/tweet?url=${window.location.href}`) + } + + return { shareToKakao, shareToInsta, shareToFacebook, shareToX } +} + +export default useSnsShare diff --git a/src/app/(board)/board/[boardId]/page.tsx b/src/app/(board)/board/[boardId]/page.tsx index 4360153..ca96e97 100644 --- a/src/app/(board)/board/[boardId]/page.tsx +++ b/src/app/(board)/board/[boardId]/page.tsx @@ -9,7 +9,7 @@ import CreatePolaroid from './_components/CreatePolaroidModal' import { ModalProvider } from './_components/CreatePolaroidModal/ModalContext' import Empty from './_components/Empty' import OpenModalBtn from './_components/OpenModalBtn' -import ShareBtn from './_components/ShareBtn' +import ShareBtn from './_components/Share' import Tutorial from './_components/Tutorial' import { Step1Tooltip } from './_components/Tutorial/Tooltips' import { TutorialProvider } from './_components/Tutorial/TutorialContext' diff --git a/src/app/error.tsx b/src/app/error.tsx index 643b23b..4681698 100644 --- a/src/app/error.tsx +++ b/src/app/error.tsx @@ -1,9 +1,20 @@ 'use client' +import Button from '@/components/Button' +import { useRouter } from 'next/navigation' +import ErrorIcon from 'public/icons/error.svg' + const ErrorPage = () => { + const router = useRouter() return (
-

오류가 발생했어요.

+ +

오류가 발생했어요.

+

사용에 불편을 드려 죄송합니다.

+

+ (이 화면을 덜 보시도록 폴라보팀은 매일 노력하고있어요..!) +

+
) } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 76aaf4f..3628530 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -44,6 +44,13 @@ export const metadata: Metadata = { }, } +declare global { + interface Window { + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + Kakao: any + } +} + export default function RootLayout({ children, }: Readonly<{ diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx index dc2e084..9f8db63 100644 --- a/src/components/Modal/index.tsx +++ b/src/components/Modal/index.tsx @@ -106,10 +106,10 @@ const BottomModal = ({ const { isVisible } = useContext(ModalContext) return (
{icon && ( -
{icon}
+
{icon}
)}
{children} diff --git a/tailwind.config.ts b/tailwind.config.ts index f6d369e..235f831 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -29,6 +29,7 @@ const config: Config = { negative: '#ef4444', transparent: 'transparent', kakao: '#FEE500', + facebook: '#337FFF', }, fontSize: { xxs: '10px',