diff --git a/.pnp.cjs b/.pnp.cjs index e69b5376..7a4856b9 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -70,6 +70,7 @@ const RAW_RUNTIME_STATE = ["react", "npm:18.3.1"],\ ["react-dom", "virtual:c6b0d0664ee806a27edc15b7122fa341a11551c3706b5168c476b9c2c6566ea5acc0def30b35852c02335bd44650f766cc396b7821830ac82642f4873d7699e0#npm:18.3.1"],\ ["react-error-boundary", "virtual:c6b0d0664ee806a27edc15b7122fa341a11551c3706b5168c476b9c2c6566ea5acc0def30b35852c02335bd44650f766cc396b7821830ac82642f4873d7699e0#npm:4.0.13"],\ + ["react-helmet-async", "virtual:c6b0d0664ee806a27edc15b7122fa341a11551c3706b5168c476b9c2c6566ea5acc0def30b35852c02335bd44650f766cc396b7821830ac82642f4873d7699e0#npm:2.0.5"],\ ["react-router-dom", "virtual:c6b0d0664ee806a27edc15b7122fa341a11551c3706b5168c476b9c2c6566ea5acc0def30b35852c02335bd44650f766cc396b7821830ac82642f4873d7699e0#npm:6.24.1"],\ ["storybook", "npm:8.2.9"],\ ["typescript", "patch:typescript@npm%3A5.3.3#optional!builtin::version=5.3.3&hash=e012d7"],\ @@ -8363,6 +8364,7 @@ const RAW_RUNTIME_STATE = ["react", "npm:18.3.1"],\ ["react-dom", "virtual:c6b0d0664ee806a27edc15b7122fa341a11551c3706b5168c476b9c2c6566ea5acc0def30b35852c02335bd44650f766cc396b7821830ac82642f4873d7699e0#npm:18.3.1"],\ ["react-error-boundary", "virtual:c6b0d0664ee806a27edc15b7122fa341a11551c3706b5168c476b9c2c6566ea5acc0def30b35852c02335bd44650f766cc396b7821830ac82642f4873d7699e0#npm:4.0.13"],\ + ["react-helmet-async", "virtual:c6b0d0664ee806a27edc15b7122fa341a11551c3706b5168c476b9c2c6566ea5acc0def30b35852c02335bd44650f766cc396b7821830ac82642f4873d7699e0#npm:2.0.5"],\ ["react-router-dom", "virtual:c6b0d0664ee806a27edc15b7122fa341a11551c3706b5168c476b9c2c6566ea5acc0def30b35852c02335bd44650f766cc396b7821830ac82642f4873d7699e0#npm:6.24.1"],\ ["storybook", "npm:8.2.9"],\ ["typescript", "patch:typescript@npm%3A5.3.3#optional!builtin::version=5.3.3&hash=e012d7"],\ @@ -10555,6 +10557,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["invariant", [\ + ["npm:2.2.4", {\ + "packageLocation": "./.yarn/cache/invariant-npm-2.2.4-717fbdb119-5af133a917.zip/node_modules/invariant/",\ + "packageDependencies": [\ + ["invariant", "npm:2.2.4"],\ + ["loose-envify", "npm:1.4.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["ip-address", [\ ["npm:9.0.5", {\ "packageLocation": "./.yarn/cache/ip-address-npm-9.0.5-9fa024d42a-331cd07faf.zip/node_modules/ip-address/",\ @@ -13010,6 +13022,40 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["react-fast-compare", [\ + ["npm:3.2.2", {\ + "packageLocation": "./.yarn/cache/react-fast-compare-npm-3.2.2-45b585a872-0bbd2f3eb4.zip/node_modules/react-fast-compare/",\ + "packageDependencies": [\ + ["react-fast-compare", "npm:3.2.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["react-helmet-async", [\ + ["npm:2.0.5", {\ + "packageLocation": "./.yarn/cache/react-helmet-async-npm-2.0.5-f913a66ef6-f390ea8bf1.zip/node_modules/react-helmet-async/",\ + "packageDependencies": [\ + ["react-helmet-async", "npm:2.0.5"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:c6b0d0664ee806a27edc15b7122fa341a11551c3706b5168c476b9c2c6566ea5acc0def30b35852c02335bd44650f766cc396b7821830ac82642f4873d7699e0#npm:2.0.5", {\ + "packageLocation": "./.yarn/__virtual__/react-helmet-async-virtual-9d31364428/0/cache/react-helmet-async-npm-2.0.5-f913a66ef6-f390ea8bf1.zip/node_modules/react-helmet-async/",\ + "packageDependencies": [\ + ["react-helmet-async", "virtual:c6b0d0664ee806a27edc15b7122fa341a11551c3706b5168c476b9c2c6566ea5acc0def30b35852c02335bd44650f766cc396b7821830ac82642f4873d7699e0#npm:2.0.5"],\ + ["@types/react", "npm:18.3.3"],\ + ["invariant", "npm:2.2.4"],\ + ["react", "npm:18.3.1"],\ + ["react-fast-compare", "npm:3.2.2"],\ + ["shallowequal", "npm:1.1.0"]\ + ],\ + "packagePeers": [\ + "@types/react",\ + "react"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["react-is", [\ ["npm:16.13.1", {\ "packageLocation": "./.yarn/cache/react-is-npm-16.13.1-a9b9382b4f-33977da7a5.zip/node_modules/react-is/",\ @@ -13615,6 +13661,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["shallowequal", [\ + ["npm:1.1.0", {\ + "packageLocation": "./.yarn/cache/shallowequal-npm-1.1.0-6688d419cb-b926efb51c.zip/node_modules/shallowequal/",\ + "packageDependencies": [\ + ["shallowequal", "npm:1.1.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["shebang-command", [\ ["npm:2.0.0", {\ "packageLocation": "./.yarn/cache/shebang-command-npm-2.0.0-eb2b01921d-a41692e7d8.zip/node_modules/shebang-command/",\ diff --git a/.yarn/cache/invariant-npm-2.2.4-717fbdb119-5af133a917.zip b/.yarn/cache/invariant-npm-2.2.4-717fbdb119-5af133a917.zip new file mode 100644 index 00000000..dd6a6bf5 Binary files /dev/null and b/.yarn/cache/invariant-npm-2.2.4-717fbdb119-5af133a917.zip differ diff --git a/.yarn/cache/react-fast-compare-npm-3.2.2-45b585a872-0bbd2f3eb4.zip b/.yarn/cache/react-fast-compare-npm-3.2.2-45b585a872-0bbd2f3eb4.zip new file mode 100644 index 00000000..1d002a67 Binary files /dev/null and b/.yarn/cache/react-fast-compare-npm-3.2.2-45b585a872-0bbd2f3eb4.zip differ diff --git a/.yarn/cache/react-helmet-async-npm-2.0.5-f913a66ef6-f390ea8bf1.zip b/.yarn/cache/react-helmet-async-npm-2.0.5-f913a66ef6-f390ea8bf1.zip new file mode 100644 index 00000000..558dc72b Binary files /dev/null and b/.yarn/cache/react-helmet-async-npm-2.0.5-f913a66ef6-f390ea8bf1.zip differ diff --git a/.yarn/cache/shallowequal-npm-1.1.0-6688d419cb-b926efb51c.zip b/.yarn/cache/shallowequal-npm-1.1.0-6688d419cb-b926efb51c.zip new file mode 100644 index 00000000..b4686dcd Binary files /dev/null and b/.yarn/cache/shallowequal-npm-1.1.0-6688d419cb-b926efb51c.zip differ diff --git a/index.html b/index.html index d8971ad9..63bbfad4 100644 --- a/index.html +++ b/index.html @@ -5,6 +5,11 @@ 째깍 - 모두의 일정을 한눈에 +
diff --git a/package.json b/package.json index 9651d381..c3f1fcb4 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "react": "^18.3.1", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.13", + "react-helmet-async": "^2.0.5", "react-router-dom": "^6.24.1", "upgrade": "^1.1.0" }, diff --git a/public/images/kakao-share-thumbnail.png b/public/images/kakao-share-thumbnail.png new file mode 100644 index 00000000..39b03520 Binary files /dev/null and b/public/images/kakao-share-thumbnail.png differ diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 00000000..7bcd00b9 --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,5 @@ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +interface Window { + Kakao: any; +} diff --git a/src/hooks/useKakaoSdk.tsx b/src/hooks/useKakaoSdk.tsx new file mode 100644 index 00000000..f4ab67a7 --- /dev/null +++ b/src/hooks/useKakaoSdk.tsx @@ -0,0 +1,11 @@ +import { useEffect } from 'react'; + +import { ENV } from '@/lib/env'; + +export const useKakaoSdk = () => { + useEffect(() => { + if (!window.Kakao.isInitialized()) { + window.Kakao.init(ENV.KAKAO_JAVASCRIPT_KEY); + } + }, []); +}; diff --git a/src/lib/env.ts b/src/lib/env.ts index 2e7c1967..3e5c8af0 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -1,6 +1,5 @@ export const ENV = { IS_PRODUCTION: import.meta.env.PROD, API_BASE_URL: `${import.meta.env.VITE_API_BASE_URL}`, - KAKAO_REST_API_KEY: `${import.meta.env.VITE_KAKAO_REST_API_KEY}`, - KAKAO_LOGIN_REDIRECT_URI: `${import.meta.env.VITE_KAKAO_LOGIN_REDIRECT_URI}`, + KAKAO_JAVASCRIPT_KEY: `${import.meta.env.VITE_KAKAO_JAVASCRIPT_KEY}`, } as const; diff --git a/src/main.tsx b/src/main.tsx index 5df48d42..89367a7e 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,6 +2,7 @@ import React, { Suspense } from 'react'; import { QueryClientProvider } from '@tanstack/react-query'; import dayjs from 'dayjs'; import ReactDOM from 'react-dom/client'; +import { HelmetProvider } from 'react-helmet-async'; import { RouterProvider } from 'react-router-dom'; import 'dayjs/locale/ko'; @@ -13,12 +14,14 @@ import './index.css'; dayjs.locale('ko'); ReactDOM.createRoot(document.getElementById('root')!).render( - - - - {/* TODO Suspense fallback */} - - - - + + + + + {/* TODO Suspense fallback */} + + + + + ); diff --git a/src/pages/MeetingDetail.tsx b/src/pages/MeetingDetail.tsx index 228f52d9..745e2364 100644 --- a/src/pages/MeetingDetail.tsx +++ b/src/pages/MeetingDetail.tsx @@ -1,4 +1,5 @@ import { Suspense } from 'react'; +import { Helmet } from 'react-helmet-async'; import { useParams, useNavigate, Navigate } from 'react-router-dom'; import { FlexBox } from '@/components/common/FlexBox'; @@ -18,29 +19,41 @@ export const MeetingDetail = () => { } return ( - - {/* TODO Suspense fallback */} - - - - - - - - - - {!accessToken && ( - - navigate(`/${uuid}/edit`)} - style={{ cursor: 'pointer' }} - > - 일정 수정하기 - - - )} - + <> + + + + + + + + + {/* TODO Suspense fallback */} + + + + + + + + + + {!accessToken && ( + + navigate(`/${uuid}/edit`)} + style={{ cursor: 'pointer' }} + > + 일정 수정하기 + + + )} + + ); }; diff --git a/src/pages/NewMeetingShare.tsx b/src/pages/NewMeetingShare.tsx index 53ac3010..3e3eee81 100644 --- a/src/pages/NewMeetingShare.tsx +++ b/src/pages/NewMeetingShare.tsx @@ -7,9 +7,12 @@ import { FlexBox } from '@/components/common/FlexBox'; import { FormLayout } from '@/components/common/FormLayout'; import { Icon } from '@/components/common/Icon'; import { IconButton } from '@/components/common/IconButton'; -// import { ENV } from '@/lib/env'; // TODO 주석 해제 +import { useKakaoSdk } from '@/hooks/useKakaoSdk'; +import { ENV } from '@/lib/env'; +import { copyToClipboard } from '@/utils/copy'; export const NewMeetingShare = () => { + useKakaoSdk(); const navigate = useNavigate(); const { state } = useLocation(); const meetingUuid = state?.meetingUuid; @@ -18,10 +21,9 @@ export const NewMeetingShare = () => { return ; } - // TODO 주석 해제 (@typescript-eslint/no-unused-vars 경고때문에 임시 주석처리) - // const shareUrl = ENV.IS_PRODUCTION - // ? `https://jjakkak.com/${meetingUuid}` - // : `http://localhost:5173/${meetingUuid}`; + const shareUrl = ENV.IS_PRODUCTION + ? `https://jjakkak.com/${meetingUuid}` + : `http://localhost:5173/${meetingUuid}`; return ( <> @@ -36,17 +38,13 @@ export const NewMeetingShare = () => { variant="square" iconName="kakaotalk1" label="카카오톡" - onClick={() => { - /* TODO */ - }} + onClick={() => window.Kakao.Share.sendDefault(templateForShareMeeting(shareUrl))} /> { - /* TODO */ - }} + onClick={() => copyToClipboard(shareUrl)} /> @@ -72,3 +70,23 @@ const BlankSpace = styled.div` width: 100%; height: 52px; `; + +const templateForShareMeeting = (shareUrl: string) => ({ + objectType: 'feed', + content: { + title: '째깍 모임 링크가 도착했어요!', + description: '나의 일정을 입력하고 모두가 가능한 시간을 확인해요.', + imageUrl: 'https://ifh.cc/g/vK7L4P.jpg', + link: { + webUrl: shareUrl, + }, + }, + buttons: [ + { + title: '가능한 시간 입력하기', + link: { + webUrl: shareUrl, + }, + }, + ], +}); diff --git a/src/utils/copy.ts b/src/utils/copy.ts new file mode 100644 index 00000000..8fd32f65 --- /dev/null +++ b/src/utils/copy.ts @@ -0,0 +1,8 @@ +export const copyToClipboard = async (text: string) => { + try { + await navigator.clipboard.writeText(text); + alert('링크가 복사되었습니다'); + } catch (e) { + alert('잠시 후 다시 시도해 주세요'); + } +}; diff --git a/tsconfig.json b/tsconfig.json index 4e1af4ea..521d96b3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,6 +28,6 @@ "jsxImportSource": "@emotion/react" }, - "include": ["src", "svg.d.ts"], + "include": ["src", "svg.d.ts", "global.d.ts"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/yarn.lock b/yarn.lock index f3a12ed2..7b637bbb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5764,6 +5764,7 @@ __metadata: react: "npm:^18.3.1" react-dom: "npm:^18.2.0" react-error-boundary: "npm:^4.0.13" + react-helmet-async: "npm:^2.0.5" react-router-dom: "npm:^6.24.1" storybook: "npm:^8.2.9" typescript: "npm:5.3.3" @@ -7699,6 +7700,15 @@ __metadata: languageName: node linkType: hard +"invariant@npm:^2.2.4": + version: 2.2.4 + resolution: "invariant@npm:2.2.4" + dependencies: + loose-envify: "npm:^1.0.0" + checksum: 10c0/5af133a917c0bcf65e84e7f23e779e7abc1cd49cb7fdc62d00d1de74b0d8c1b5ee74ac7766099fb3be1b05b26dfc67bab76a17030d2fe7ea2eef867434362dfc + languageName: node + linkType: hard + "ip-address@npm:^9.0.5": version: 9.0.5 resolution: "ip-address@npm:9.0.5" @@ -8407,7 +8417,7 @@ __metadata: languageName: node linkType: hard -"loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": +"loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" dependencies: @@ -9768,6 +9778,26 @@ __metadata: languageName: node linkType: hard +"react-fast-compare@npm:^3.2.2": + version: 3.2.2 + resolution: "react-fast-compare@npm:3.2.2" + checksum: 10c0/0bbd2f3eb41ab2ff7380daaa55105db698d965c396df73e6874831dbafec8c4b5b08ba36ff09df01526caa3c61595247e3269558c284e37646241cba2b90a367 + languageName: node + linkType: hard + +"react-helmet-async@npm:^2.0.5": + version: 2.0.5 + resolution: "react-helmet-async@npm:2.0.5" + dependencies: + invariant: "npm:^2.2.4" + react-fast-compare: "npm:^3.2.2" + shallowequal: "npm:^1.1.0" + peerDependencies: + react: ^16.6.0 || ^17.0.0 || ^18.0.0 + checksum: 10c0/f390ea8bf13c2681850e5f8eb5b73d8613f407c245a5fd23e9db9b2cc14a3700dd1ce992d3966632886d1d613083294c2aeee009193f49dfa7d145d9f13ea2b0 + languageName: node + linkType: hard + "react-is@npm:18.1.0": version: 18.1.0 resolution: "react-is@npm:18.1.0" @@ -10383,6 +10413,13 @@ __metadata: languageName: node linkType: hard +"shallowequal@npm:^1.1.0": + version: 1.1.0 + resolution: "shallowequal@npm:1.1.0" + checksum: 10c0/b926efb51cd0f47aa9bc061add788a4a650550bbe50647962113a4579b60af2abe7b62f9b02314acc6f97151d4cf87033a2b15fc20852fae306d1a095215396c + languageName: node + linkType: hard + "shebang-command@npm:^2.0.0": version: 2.0.0 resolution: "shebang-command@npm:2.0.0"