From 2fc4556a9f0fc451e33c071f5fb9fd2a75a43dea Mon Sep 17 00:00:00 2001 From: hyunju1211 Date: Tue, 4 Feb 2025 20:50:07 +0900 Subject: [PATCH] =?UTF-8?q?[FIX]=20Web=20Share=20API=20=EB=B2=84=EA=B7=B8?= =?UTF-8?q?=20=ED=94=BD=EC=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 127 +++++++++++++++++++++++++- package.json | 4 +- src/App.css | 62 +++++++------ src/App.js | 2 - src/components/TimePicker.js | 2 +- src/components/copyToClipBoard.ts | 58 ++++++++++++ src/components/share.ts | 51 +++++++++++ src/hooks/index.ts | 1 + src/hooks/useUserAgent.js | 20 ++++ src/index.js | 3 - src/pages/MonthView.js | 10 +- src/pages/ParentMonth.js | 8 +- src/pages/eventCalendar.js | 88 +++++++++++------- src/pages/invite.js | 12 +-- src/utils/parseUserAgent/constants.ts | 50 ++++++++++ src/utils/parseUserAgent/index.ts | 61 +++++++++++++ 16 files changed, 477 insertions(+), 82 deletions(-) create mode 100644 src/components/copyToClipBoard.ts create mode 100644 src/components/share.ts create mode 100644 src/hooks/index.ts create mode 100644 src/hooks/useUserAgent.js create mode 100644 src/utils/parseUserAgent/constants.ts create mode 100644 src/utils/parseUserAgent/index.ts diff --git a/package-lock.json b/package-lock.json index 7ab0f60..c2955c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "path-browserify": "^1.0.1", "react": "^18.3.1", "react-calendar": "^5.0.0", + "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.3.1", "react-router-dom": "^6.26.2", "react-scripts": "5.0.1", @@ -49,7 +50,8 @@ "autoprefixer": "^10.4.20", "firebase-tools": "^13.29.1", "postcss": "^8.5.1", - "tailwindcss": "^3.4.17" + "tailwindcss": "^3.4.17", + "ua-parser-js": "^2.0.0" } }, "node_modules/@adobe/css-tools": { @@ -9260,6 +9262,15 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "license": "MIT", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, "node_modules/core-js": { "version": "3.38.1", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.38.1.tgz", @@ -10311,6 +10322,27 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-europe-js": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/detect-europe-js/-/detect-europe-js-0.1.2.tgz", + "integrity": "sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "license": "MIT" + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -14977,6 +15009,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-standalone-pwa": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-standalone-pwa/-/is-standalone-pwa-0.1.1.tgz", + "integrity": "sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "license": "MIT" + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -22397,6 +22450,19 @@ } } }, + "node_modules/react-copy-to-clipboard": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz", + "integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==", + "license": "MIT", + "dependencies": { + "copy-to-clipboard": "^3.3.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": "^15.3.0 || 16 || 17 || 18" + } + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -25544,6 +25610,12 @@ "node": ">=8.0" } }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", + "license": "MIT" + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -25900,6 +25972,59 @@ "node": ">=4.2.0" } }, + "node_modules/ua-is-frozen": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ua-is-frozen/-/ua-is-frozen-0.1.2.tgz", + "integrity": "sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "license": "MIT" + }, + "node_modules/ua-parser-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-2.0.0.tgz", + "integrity": "sha512-SASgD4RlB7+SCMmlVNqrhPw0f/2pGawWBzJ2+LwGTD0GgNnrKGzPJDiraGHJDwW9Zm5DH2lTmUpqDpbZjJY4+Q==", + "dev": true, + "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" + } + ], + "license": "AGPL-3.0-or-later", + "dependencies": { + "detect-europe-js": "^0.1.2", + "is-standalone-pwa": "^0.1.1", + "ua-is-frozen": "^0.1.2" + }, + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", diff --git a/package.json b/package.json index a7e7934..eae39f4 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "path-browserify": "^1.0.1", "react": "^18.3.1", "react-calendar": "^5.0.0", + "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.3.1", "react-router-dom": "^6.26.2", "react-scripts": "5.0.1", @@ -67,6 +68,7 @@ "autoprefixer": "^10.4.20", "firebase-tools": "^13.29.1", "postcss": "^8.5.1", - "tailwindcss": "^3.4.17" + "tailwindcss": "^3.4.17", + "ua-parser-js": "^2.0.0" } } diff --git a/src/App.css b/src/App.css index f346f76..03cd3f8 100644 --- a/src/App.css +++ b/src/App.css @@ -63,35 +63,43 @@ You can apply the theme on any DOM node, not just the `body` margin: 0 auto; padding: 0; } - -/* Android 최소 사이즈 (360x640) */ -@media only screen and (min-device-width: 360px) and (max-device-width: 640px) and (-webkit-device-pixel-ratio: 3) { - .App { - width: 360px; - height: 640px; + /* 안드로이드 최소 사이즈 (360 x 640) + w 360px 이상, h 640px 이하인 기기에서 h 640px로 적용 */ + @media screen and (min-width: 360px) and (max-height: 640px) { + .App { + height: 640px; + width:auto; + /* background-color:#c30010; */ + } } -} + + + /* iOS 최소 사이즈 (375 x 667) + iOS 기기 중 h 667px 이하인 경우 적용 */ + @media screen and (min-width: 375px) and (max-height: 667px) { + .App { + height: 667px; + /* background-color:#c30010; */ -/* Android 기본 작업 사이즈 (360x800) */ -/* @media only screen and (min-device-width: 360px) and (max-device-width: 800px) and (-webkit-device-pixel-ratio: 3) { - .App { - width: 360px; - height: 800px; + } } -} */ - -/* iOS 최소 사이즈 (375x667) */ -@media only screen and (min-device-width: 375px) and (max-device-height: 667px) and (-webkit-device-pixel-ratio: 3) { - .App { - width: 375px; - height: 667px; + + /* iOS 기본 작업 사이즈 (375 x 812) + iOS 기기 중 h가 668px 이상인 경우 기본 작업 사이즈로 적용 */ + @media screen and (min-width: 375px) and (min-height: 668px) { + .App { + height: 812px; + /* background-color:#c30010; */ +/* 왜 여기서 pc가 적용되지..?? */ + } } -} + + /* PC 버전 또는 LandPage 전용 스타일: + 뷰포트 너비가 768px 이상인 경우, LandPage 디자인 요구에 따라 430px로 변경 */ + @media screen and (min-width: 768px) { + .App { + width: 430px; + /* background-color:#c30010; */ -/* iOS 기본 작업 사이즈 (375x812) */ -@media only screen and (min-device-width: 375px) and (max-device-height: 812px) and (-webkit-device-pixel-ratio: 3) { - .App { - width: 375px; - height: 812px; - } -} + } + } \ No newline at end of file diff --git a/src/App.js b/src/App.js index 9e60980..07c3273 100644 --- a/src/App.js +++ b/src/App.js @@ -8,8 +8,6 @@ import GetAppointmentRedirect from './pages/GetAppointmentRedirect'; import IndividualCalendar from './pages/individualCalendar'; import MyPage from './pages/MyPage'; import AccountManagement from './pages/AccountManagement'; - -// import LandingPage from "./components/LandingPage"; import LandingPage from "./components/LandingPage"; function App() { diff --git a/src/components/TimePicker.js b/src/components/TimePicker.js index 17bd9d7..6eb82a2 100644 --- a/src/components/TimePicker.js +++ b/src/components/TimePicker.js @@ -8,7 +8,7 @@ import { cn } from '../utils/cn'; const TimePicker = ({ startTime, endTime, setStartTime, setEndTime, onCreateCalendar,isFormReady, setIsFormReady }) => { - console.log('[TimePicker] isFormReady:', isFormReady); + // console.log('[TimePicker] isFormReady:', isFormReady); const startMeridiemDialRef = useRef(null); const startHourDialRef = useRef(null); diff --git a/src/components/copyToClipBoard.ts b/src/components/copyToClipBoard.ts new file mode 100644 index 0000000..3aba7a4 --- /dev/null +++ b/src/components/copyToClipBoard.ts @@ -0,0 +1,58 @@ +const getDummyTextarea = () => { + const textarea = document.createElement("textarea") as HTMLTextAreaElement; + textarea.style.top = "0"; + textarea.style.left = "0"; + textarea.style.display = "fixed"; + + return textarea; + }; + + export const isClipboardSupported = () => navigator?.clipboard != null; + export const isClipboardCommandSupported = () => + document.queryCommandSupported?.("copy") ?? false; + + /** + * 인자로 받은 텍스트를 클립보드에 복사합니다. + * @param text 복사할 텍스트 + * + * @example + * ```ts + * const result = await copyToClipboard('하이'); + * if (result) { + * console.log('클립보드에 복사 성공'); + * } else { + * console.log('클립보드에 복사 실패'); + * } + * ``` + */ + export const copyToClipboard = (text: string) => { + return new Promise(async (resolve) => { + const rootElement = document.body; + + // if (isClipboardSupported()) { + // await navigator.clipboard.writeText(text); + // resolve(true); + // return; + // } + + if (isClipboardCommandSupported()) { + const textarea = getDummyTextarea(); + textarea.value = text; + + rootElement.appendChild(textarea); + + textarea.focus(); + textarea.select(); + + document.execCommand("copy"); + rootElement.removeChild(textarea); + resolve(true); + return; + } + + resolve(false); + return; + }); + }; + + export default copyToClipboard; \ No newline at end of file diff --git a/src/components/share.ts b/src/components/share.ts new file mode 100644 index 0000000..be2aea5 --- /dev/null +++ b/src/components/share.ts @@ -0,0 +1,51 @@ +import copyToClipboard from "./copyToClipBoard.ts"; + +export const isShareSupported = () => navigator.share ?? false; + +/** + * 인자로 받은 data를 OS 기본옵션으로 공유합니다. + * 기본 공유옵션이 지원되지 않을 경우, url만을 클립보드에 링크를 복사하는 기능으로 대체됩니다. + * + * @param data 공유할 data 객체 + * @param data.url 공유될 또는 클립복드에 복사될 url + * @param data.text 공유시 해당 메신저에 추가적인 텍스트로 전달되는 문구 + * @param data.title 공유시 썸네일에 제공되는 타이틀 문구 + * @param data.files 공유할 file 리스트 + * + * @example + * ```ts + * const result = await share('data'); + * if (result === 'share') { + * console.log('공유 성공'); + * } else if (result === 'clipboard') { + * console.log('클립보드 복사 성공'); + * } else { + * console.log('공유 실패'); + * } + * ``` + */ + +export const share = (data: ShareData) => { + return new Promise<"shared" | "copiedToClipboard" | "failed">( + async (resolve) => { + if (isShareSupported()) { + await navigator.share(data); + resolve("shared"); + return "shared"; + } + + if (data.url) { + const result = await copyToClipboard(data.url); + + if (result) { + resolve("copiedToClipboard"); + return "copiedToClipboard"; + } + } + resolve("failed"); + return "failed"; + } + ); +}; + +export default share; \ No newline at end of file diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 0000000..978d9aa --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1 @@ +export { default as useUserAgent } from "./useUserAgent"; \ No newline at end of file diff --git a/src/hooks/useUserAgent.js b/src/hooks/useUserAgent.js new file mode 100644 index 0000000..c5158cb --- /dev/null +++ b/src/hooks/useUserAgent.js @@ -0,0 +1,20 @@ +import { useEffect, useState } from "react"; +import { parseUserAgent } from "../utils/parseUserAgent/index.ts"; + +function useUserAgent() { + const [result, setResult] = useState(null); + + useEffect(() => { + if (typeof window !== "undefined") { + const uaString = navigator.userAgent; + const parsedResult = parseUserAgent(uaString); + setResult(parsedResult); + } + }, []); + + return { + userAgent: result, + }; +} + +export default useUserAgent; diff --git a/src/index.js b/src/index.js index d563c0f..8d5cddf 100644 --- a/src/index.js +++ b/src/index.js @@ -11,7 +11,4 @@ root.render( ); -// If you want to start measuring performance in your app, pass a function -// to log results (for example: reportWebVitals(console.log)) -// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals(); diff --git a/src/pages/MonthView.js b/src/pages/MonthView.js index 47e2105..4bea159 100644 --- a/src/pages/MonthView.js +++ b/src/pages/MonthView.js @@ -65,9 +65,9 @@ const inputClasses = twMerge( const currentYear = new Date().getFullYear(); const yearRange = Array.from({ length: 3 }, (_, i) => currentYear + i); - useEffect(() => { - console.log('Form ready status:', isFormReady); // 상태 변경 확인용 로그 - }, [isFormReady]); + // useEffect(() => { + // console.log('Form ready status:', isFormReady); // 상태 변경 확인용 로그 + // }, [isFormReady]); //서버에 보낼 json 만들기 useEffect(() => { @@ -95,8 +95,8 @@ const inputClasses = twMerge( const endDateTime = moment .tz(`${latestDateString} ${endTime}`, 'YYYY-MM-DD HH:mm', 'Asia/Seoul') .format('YYYY-MM-DDTHH:mm:ss[Z]'); - console.log("startDateTime", startDateTime); - console.log("endDateTime", endDateTime); + // console.log("startDateTime", startDateTime); + // console.log("endDateTime", endDateTime); const data = { name: eventName, schedules: schedules, diff --git a/src/pages/ParentMonth.js b/src/pages/ParentMonth.js index e92f331..14253eb 100644 --- a/src/pages/ParentMonth.js +++ b/src/pages/ParentMonth.js @@ -5,10 +5,14 @@ import { useNavigate } from 'react-router-dom'; const ParentMonth = () => { // NODE_ENV에 기반하여 BASE_URL에 환경변수 할당 + // console.log("process.env.NODE_ENV: ", process.env.NODE_ENV); + // // development로 출력됨 const BASE_URL = process.env.NODE_ENV === "production" ? process.env.REACT_APP_WWWM_BE_ENDPOINT : process.env.REACT_APP_WWWM_BE_DEV_EP; - + + // 정상작동 console.log("BASE_URL: ", BASE_URL); + const [jsonData, setJsonData] = useState(null); const [startTime, setStartTime] = useState('09:00'); const [endTime, setEndTime] = useState('20:00'); @@ -41,7 +45,7 @@ const ParentMonth = () => { const calendarData = await calendarResponse.json(); console.log("나는 캘린더data: ",calendarData); const appointmentId = calendarData.object.id; - console.log("왜안되는걸까?",appointmentId); //정상작동 + // console.log("왜안되는걸까?",appointmentId); //정상작동 // invite 페이지로 이동하면서 appointmentId를 쿼리 파라미터로 전달 navigate(`/getAppointment?appointmentId=${appointmentId}`); diff --git a/src/pages/eventCalendar.js b/src/pages/eventCalendar.js index 227f501..6ab68bb 100644 --- a/src/pages/eventCalendar.js +++ b/src/pages/eventCalendar.js @@ -3,12 +3,15 @@ import { useSwipeable } from "react-swipeable"; import { useLocation, useNavigate } from 'react-router-dom'; import moment from 'moment'; import 'moment/locale/ko'; - +import copyToClipboard from '../components/copyToClipBoard.ts'; +import { share } from '../components/share.ts'; import { typographyVariants } from '../styles/typography.ts'; import { colorVariants, colors } from '../styles/color.ts'; import { cn } from '../utils/cn'; import { Button } from '../components/Button.tsx'; +// import { CopyToClipboard } from "react-copy-to-clipboard"; +import useUserAgent from "../hooks/useUserAgent"; const EventCalendar = () => { // const { responseData, appointmentId, userSchedule } = location.state; @@ -30,14 +33,18 @@ const EventCalendar = () => { const [selectedTimes, setSelectedTimes] = useState({}); const navigate = useNavigate(); const location = useLocation(); - const[TotalUsers, setTotalUsers] = useState(""); + const state = location.state || {}; const appointmentId = state.id; const userName= state.userName; - console.log("userName:: ",userName); + // console.log("userName:: ",userName); const[userList, setUserList]=useState(""); + const [result, setResult] = useState(''); + const { userAgent } = useUserAgent(); + + const androidWebView = useUserAgent?.isAndroidWebView; const [showToast, setShowToast] = useState(false); @@ -45,21 +52,33 @@ const EventCalendar = () => { title: "언제볼까?", text: "링크 공유로 초대하기: 공유만 하면 끝, 간편한 친구 초대", url: `https://when-will-we-meet.site/invite?appointmentId=${appointmentId}`, - }; - - const isShareSupported = () => navigator.share ?? false; - + }; + //해당 url로 접속시, 정상적으로 작동함. + // 즉, url 자체는 문제없는디 공유 라이브러리 자체 문제임. + + + // const handleShare = async () => { + // if (isShareSupported()) { + // try { + // await navigator.share(shareData); + // setResult("공유가 완료되었습니다. "); + // console.log("shareData 성공공: ", shareData); + // } catch (err) { + // setResult(`Error: ${err}`); + // console.log("shareData 실패: ", shareData); + + // } + // } + // }; const handleShare = async () => { - if (isShareSupported()) { - try { - await navigator.share(shareData); - setResult("공유가 완료되었습니다. "); - } catch (err) { - setResult(`Error: ${err}`); - } - } + const result = await share(shareData); + if (result === "copiedToClipboard") { + alert("링크를 클립보드에 복사했습니다."); + console.log("링크 공유 성공공"); + } else if (result === "failed") { + alert("공유하기가 지원되지 않는 환경입니다."); + } }; - // const copyToClipboard = async (url) => { // try { // await navigator.clipboard.writeText(url); @@ -88,7 +107,7 @@ const EventCalendar = () => { ); const responseData = await appointmentResponse.json(); - console.log("responseData 전체: ", responseData); + // console.log("responseData 전체: ", responseData); if (!responseData || !responseData.object) { console.error('응답 데이터가 올바르지 않습니다'); return; @@ -104,7 +123,7 @@ const EventCalendar = () => { return; } - console.log("schedules 보호구역: ", schedules); //정상작동 + // console.log("schedules 보호구역: ", schedules); //정상작동 // 날짜 및 시간 데이터 설정 const datesArray = schedules.map((schedule, index) => { @@ -150,8 +169,8 @@ const EventCalendar = () => { setDates(datesArray); setTimes(timesFormatted); - console.log("그냥 responseData.object: ", responseData.object); - console.log("responseData.object.schedules: ", responseData.object.schedules); + // console.log("그냥 responseData.object: ", responseData.object); + // console.log("responseData.object.schedules: ", responseData.object.schedules); // 공용 스케줄 페이지 - 화면에 색 입힐 유저 몇명인지 찾는 과정 const userSelections = responseData.object.schedules.reduce((acc, daySchedule) => { @@ -178,7 +197,7 @@ const EventCalendar = () => { return acc; }, {}); - console.log("userSelections입니다 ~!!: ", userSelections); + // console.log("userSelections입니다 ~!!: ", userSelections); // const TotalUsers = responseData.object.users.length; @@ -188,15 +207,14 @@ const EventCalendar = () => { setTotalUsers(responseData.object.users.length); setUserList(responseData.object.users); - const savedTimes = {}; datesArray.forEach((dateInfo, dateIndex) => { const datePath = dateInfo.date; - console.log("처리중인 날짜:", { - datePath, - existingSelections: userSelections[datePath], - }); + // console.log("처리중인 날짜:", { + // datePath, + // existingSelections: userSelections[datePath], + // }); if (userSelections[datePath]) { if (!savedTimes[dateIndex]) { @@ -209,7 +227,7 @@ const EventCalendar = () => { const mm = moment(time, 'HH:mm').format('mm'); const minutesArray = userSelections[datePath][hour]; - console.log("minutesArray: ", minutesArray); + // console.log("minutesArray: ", minutesArray); if (minutesArray) { savedTimes[dateIndex][timeIndex] = {}; minutesArray.forEach((minuteObj) => { @@ -226,7 +244,7 @@ const EventCalendar = () => { }); } }); - console.log("최종 savedTimes:", savedTimes); + // console.log("최종 savedTimes:", savedTimes); setSelectedTimes(savedTimes); } } catch (error) { @@ -236,11 +254,11 @@ const EventCalendar = () => { fetchData(); }, [appointmentId]); -useEffect(() => { - if (selectedTimes) { - console.log("selectedTimes가 업데이트됨:", selectedTimes); - } -}, [selectedTimes]); +// useEffect(() => { +// if (selectedTimes) { +// console.log("selectedTimes가 업데이트됨:", selectedTimes); +// } +// }, [selectedTimes]); @@ -408,7 +426,9 @@ useEffect(() => { // 정상동작 --> console.log('Comparison result:', user.name === userName.toString()); return ( -
{ `${BASE_URL}/appointment/getAppointment?appointmentId=${appointmentId}` ); - // 사용자 ID를 localStorage에 저장, + // 사용자 이전 로그인 여부를 flag로 localStorage에 저장, // appointmentId와 쌍으로 저장해 정확히 일치할때만 재로그인으로 간주 if (responseData.object.name) { const key = `loggedInFlag_${appointmentId}`; @@ -129,11 +129,11 @@ const Invite = () => { firstLogin: false }; - console.log("(((재로그인))))저쪽 invite.js 신사 분이 보내주신 재로그인시의 responseData 구조:", { - ...appointmentData, - userSchedule: userScheduleData.object, - firstLogin: false - }); + // console.log("(((재로그인))))저쪽 invite.js 신사 분이 보내주신 재로그인시의 responseData 구조:", { + // ...appointmentData, + // userSchedule: userScheduleData.object, + // firstLogin: false + // }); // console.log("invite.js가 보낸 userSchedule: ", userScheduleData.object); } else { //첫로그인 case diff --git a/src/utils/parseUserAgent/constants.ts b/src/utils/parseUserAgent/constants.ts new file mode 100644 index 0000000..cd0e2b1 --- /dev/null +++ b/src/utils/parseUserAgent/constants.ts @@ -0,0 +1,50 @@ +export interface UserAgent { + // The original user agent string. + readonly source: string; + readonly deviceType: string | null; + readonly deviceVendor: string | null; + readonly os: string | undefined; + readonly osVersion: number; + readonly browser: string | undefined; + readonly browserVersion: number; + readonly engine: string | undefined; + readonly engineVersion: number; + readonly isIphone: boolean; + readonly isIpad: boolean; + readonly isMobile: boolean; + readonly isTablet: boolean; + readonly isDesktop: boolean; + readonly isBot: boolean; + readonly isChrome: boolean; + readonly isFirefox: boolean; + readonly isSafari: boolean; + readonly isIE: boolean; + readonly isEdge: boolean; + readonly isOpera: boolean; + readonly isMac: boolean; + readonly isChromeOS: boolean; + readonly isWindows: boolean; + readonly isIos: boolean; + readonly isAndroid: boolean; + readonly isAndroidWebView: boolean; + readonly isIosWebView: boolean; + } + + export const BOT_UA = [ + "\\+https:\\/\\/developers.google.com\\/\\+\\/web\\/snippet\\/", + "googlebot", + "baiduspider", + "gurujibot", + "yandexbot", + "slurp", + "msnbot", + "bingbot", + "facebookexternalhit", + "linkedinbot", + "twitterbot", + "slackbot", + "telegrambot", + "applebot", + "pingdom", + "tumblr", + ]; \ No newline at end of file diff --git a/src/utils/parseUserAgent/index.ts b/src/utils/parseUserAgent/index.ts new file mode 100644 index 0000000..0c26a6d --- /dev/null +++ b/src/utils/parseUserAgent/index.ts @@ -0,0 +1,61 @@ +import { UAParser } from "ua-parser-js"; +import { UserAgent, BOT_UA } from "./constants.ts"; + +/** + * Get the information of an useragent string. + * + * @param phrase user agent strings. + * @returns parsed information. + */ +export function parseUserAgent(phrase: string): UserAgent { + const result: UAParser.IResult = new UAParser( + "Mozilla/5.0 (Linux; Android 12; SM-F711N Build/SP1A.210812.016; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/104.0.5112.69 Mobile Safari/537.36_Proof Android" + ).getResult(); + + const regex = new RegExp(`(${BOT_UA.join("|")})`, "ig"); + const isBot = phrase ? regex.test(phrase.toLowerCase()) : false; + const fromApp = phrase.includes("Proof"); + + const browser: string | undefined = result.browser.name; + const deviceType: string | null = result.device.type || null; + const os: string | undefined = result.os.name; + const engine: string | undefined = result.engine.name; + const isMobile: boolean = deviceType === "mobile"; + const isTablet: boolean = deviceType === "tablet"; + const isIos: boolean = os === "iOS"; + const isAndroidWebView: boolean = os === "Android" && fromApp; + const isIosWebView: boolean = os === "iOS" && fromApp; + + const ua: UserAgent = { + browser, + deviceType, + os, + engine, + isMobile, + isTablet, + isIos, + source: phrase, + deviceVendor: result.device.vendor || null, + osVersion: parseInt(`${result.os.version}`, 10), + browserVersion: parseFloat(`${result.browser.version}`), + engineVersion: parseFloat(`${result.engine.version}`), + isIphone: isMobile && isIos, + isIpad: isTablet && isIos, + isDesktop: !isMobile && !isTablet, + isChrome: browser === "Chrome", + isFirefox: browser === "Firefox", + isSafari: browser === "Safari", + isIE: browser === "IE", + isEdge: browser === "Edge", + isOpera: browser === "Opera", + isMac: os === "Mac OS", + isChromeOS: os === "Chromium OS", + isWindows: os === "Windows", + isAndroid: os === "Android", + isBot: isBot, + isAndroidWebView, + isIosWebView, + } as const; + + return ua; +} \ No newline at end of file