diff --git a/generateSitemap.mjs b/generateSitemap.mjs index 2f3855b9..e86521dd 100644 --- a/generateSitemap.mjs +++ b/generateSitemap.mjs @@ -16,7 +16,6 @@ const generateSitemap = async () => { { url: '/open-consult', changefreq: 'always', priority: 1.0 }, { url: '/open-consult/likes', changefreq: 'always', priority: 0.8 }, { url: '/open-consult/recents', changefreq: 'always', priority: 0.8 }, - { url: '/categorySearch', changefreq: 'daily', priority: 0.7 }, { url: '/service', changefreq: 'never', priority: 0.8 }, { url: '/service-unavailable', changefreq: 'monthly', priority: 0.5 }, ]; diff --git a/package-lock.json b/package-lock.json index d3d1153e..944130aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "react-cookie": "^7.0.1", "react-device-detect": "^2.2.3", "react-dom": "^18.2.0", + "react-helmet": "^6.1.0", "react-microsoft-clarity": "^1.2.0", "react-router-dom": "^6.21.1", "react-scripts": "5.0.1", @@ -44,6 +45,7 @@ }, "devDependencies": { "@types/react-dom": "^18.2.18", + "@types/react-helmet": "^6.1.11", "husky": "4" } }, @@ -4713,6 +4715,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-helmet": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-6.1.11.tgz", + "integrity": "sha512-0QcdGLddTERotCXo3VFlUSWO3ztraw8nZ6e3zJSgG7apwV5xt+pJUS8ewPBqT4NYB1optGLprNQzFleIY84u/g==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-transition-group": { "version": "4.4.10", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", @@ -15486,6 +15497,25 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" + }, + "node_modules/react-helmet": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz", + "integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==", + "dependencies": { + "object-assign": "^4.1.1", + "prop-types": "^15.7.2", + "react-fast-compare": "^3.1.1", + "react-side-effect": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.3.0" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -15614,6 +15644,14 @@ "node": ">=10" } }, + "node_modules/react-side-effect": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz", + "integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==", + "peerDependencies": { + "react": "^16.3.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-textarea-autosize": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.3.tgz", diff --git a/package.json b/package.json index fc04ea91..4d02293f 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "react-cookie": "^7.0.1", "react-device-detect": "^2.2.3", "react-dom": "^18.2.0", + "react-helmet": "^6.1.0", "react-microsoft-clarity": "^1.2.0", "react-router-dom": "^6.21.1", "react-scripts": "5.0.1", @@ -69,6 +70,7 @@ }, "devDependencies": { "@types/react-dom": "^18.2.18", + "@types/react-helmet": "^6.1.11", "husky": "4" } } diff --git a/public/android-icon-144x144.png b/public/android-icon-144x144.png deleted file mode 100644 index 7e9e9be0..00000000 Binary files a/public/android-icon-144x144.png and /dev/null differ diff --git a/public/android-icon-192x192.png b/public/android-icon-192x192.png deleted file mode 100644 index 12424a35..00000000 Binary files a/public/android-icon-192x192.png and /dev/null differ diff --git a/public/android-icon-36x36.png b/public/android-icon-36x36.png deleted file mode 100644 index 71f760a4..00000000 Binary files a/public/android-icon-36x36.png and /dev/null differ diff --git a/public/android-icon-48x48.png b/public/android-icon-48x48.png deleted file mode 100644 index c4b1ed95..00000000 Binary files a/public/android-icon-48x48.png and /dev/null differ diff --git a/public/android-icon-72x72.png b/public/android-icon-72x72.png deleted file mode 100644 index fc39c887..00000000 Binary files a/public/android-icon-72x72.png and /dev/null differ diff --git a/public/android-icon-96x96.png b/public/android-icon-96x96.png deleted file mode 100644 index fa3cb613..00000000 Binary files a/public/android-icon-96x96.png and /dev/null differ diff --git a/public/android/android-icon-144x144.png b/public/android/android-icon-144x144.png new file mode 100644 index 00000000..7e81ba4b Binary files /dev/null and b/public/android/android-icon-144x144.png differ diff --git a/public/android/android-icon-192x192.png b/public/android/android-icon-192x192.png new file mode 100644 index 00000000..164fb98a Binary files /dev/null and b/public/android/android-icon-192x192.png differ diff --git a/public/android/android-icon-36x36.png b/public/android/android-icon-36x36.png new file mode 100644 index 00000000..5b8790d2 Binary files /dev/null and b/public/android/android-icon-36x36.png differ diff --git a/public/android/android-icon-48x48.png b/public/android/android-icon-48x48.png new file mode 100644 index 00000000..f9b13f46 Binary files /dev/null and b/public/android/android-icon-48x48.png differ diff --git a/public/android/android-icon-512x512.png b/public/android/android-icon-512x512.png new file mode 100644 index 00000000..508b24e2 Binary files /dev/null and b/public/android/android-icon-512x512.png differ diff --git a/public/android/android-icon-72x72.png b/public/android/android-icon-72x72.png new file mode 100644 index 00000000..fd22bc08 Binary files /dev/null and b/public/android/android-icon-72x72.png differ diff --git a/public/android/android-icon-96x96.png b/public/android/android-icon-96x96.png new file mode 100644 index 00000000..da0addd9 Binary files /dev/null and b/public/android/android-icon-96x96.png differ diff --git a/public/apple-icon-114x114.png b/public/apple-icon-114x114.png deleted file mode 100644 index a95f2cfb..00000000 Binary files a/public/apple-icon-114x114.png and /dev/null differ diff --git a/public/apple-icon-120x120.png b/public/apple-icon-120x120.png deleted file mode 100644 index 93c6ceee..00000000 Binary files a/public/apple-icon-120x120.png and /dev/null differ diff --git a/public/apple-icon-144x144.png b/public/apple-icon-144x144.png deleted file mode 100644 index 7e9e9be0..00000000 Binary files a/public/apple-icon-144x144.png and /dev/null differ diff --git a/public/apple-icon-152x152.png b/public/apple-icon-152x152.png deleted file mode 100644 index e0df9c44..00000000 Binary files a/public/apple-icon-152x152.png and /dev/null differ diff --git a/public/apple-icon-180x180.png b/public/apple-icon-180x180.png deleted file mode 100644 index 70a142dc..00000000 Binary files a/public/apple-icon-180x180.png and /dev/null differ diff --git a/public/apple-icon-57x57.png b/public/apple-icon-57x57.png deleted file mode 100644 index 9855c166..00000000 Binary files a/public/apple-icon-57x57.png and /dev/null differ diff --git a/public/apple-icon-60x60.png b/public/apple-icon-60x60.png deleted file mode 100644 index a28ce16b..00000000 Binary files a/public/apple-icon-60x60.png and /dev/null differ diff --git a/public/apple-icon-72x72.png b/public/apple-icon-72x72.png deleted file mode 100644 index fc39c887..00000000 Binary files a/public/apple-icon-72x72.png and /dev/null differ diff --git a/public/apple-icon-76x76.png b/public/apple-icon-76x76.png deleted file mode 100644 index 492aa09a..00000000 Binary files a/public/apple-icon-76x76.png and /dev/null differ diff --git a/public/apple-icon-precomposed.png b/public/apple-icon-precomposed.png deleted file mode 100644 index 2debf969..00000000 Binary files a/public/apple-icon-precomposed.png and /dev/null differ diff --git a/public/apple-icon.png b/public/apple-icon.png deleted file mode 100644 index 2debf969..00000000 Binary files a/public/apple-icon.png and /dev/null differ diff --git a/public/index.html b/public/index.html index f466b1fd..2244527f 100644 --- a/public/index.html +++ b/public/index.html @@ -22,6 +22,56 @@ + + + + + + + + + + + + + + ; + + /** + * Returns a Promise that resolves to a DOMString containing either "accepted" or "dismissed". + */ + readonly userChoice: Promise<{ + outcome: 'accepted' | 'dismissed'; + platform: string; + }>; + + /** + * Allows a developer to show the install prompt at a time of their own choosing. + * This method returns a Promise. + */ + prompt(): Promise; +} diff --git a/src/components/Buyer/BuyerOpenConsultDetail/MainQuestionSection.tsx b/src/components/Buyer/BuyerOpenConsultDetail/MainQuestionSection.tsx index 2140cf88..be6cf971 100644 --- a/src/components/Buyer/BuyerOpenConsultDetail/MainQuestionSection.tsx +++ b/src/components/Buyer/BuyerOpenConsultDetail/MainQuestionSection.tsx @@ -18,6 +18,8 @@ import { deletePostLikes, deletePostScraps } from 'api/delete'; import { formattedMessage } from 'utils/formattedMessage'; import { Flex } from 'components/Common/Flex'; +import { Helmet } from 'react-helmet'; + // // // @@ -128,40 +130,46 @@ function MainQuestionSection() { // return ( - - -
- {card?.title} - {!card?.isPublic && ( - - - 비공개 - - )} -
- -
{formattedMessage(card?.content)}
- -
- {card?.updatedAt} - - {card?.consultCategory} -
- -
- - - {isLike ? : } - - {card?.totalLike} - - - {isSave ? : } - - {card?.totalScrap} - - -
+ <> + + {`${card?.title} | 셰어마인드 공개 상담`} + + + + +
+ {card?.title} + {!card?.isPublic && ( + + + 비공개 + + )} +
+ +
{formattedMessage(card?.content)}
+ +
+ {card?.updatedAt} + + {card?.consultCategory} +
+ +
+ + + {isLike ? : } + + {card?.totalLike} + + + {isSave ? : } + + {card?.totalScrap} + + +
+ ); } diff --git a/src/components/Common/Wrapper/PWAInstallWrapper.tsx b/src/components/Common/Wrapper/PWAInstallWrapper.tsx new file mode 100644 index 00000000..7b04dd12 --- /dev/null +++ b/src/components/Common/Wrapper/PWAInstallWrapper.tsx @@ -0,0 +1,168 @@ +import { Button } from 'components/Common/Button'; +import { Flex } from 'components/Common/Flex'; +import { useEffect, useState } from 'react'; + +import styled, { keyframes } from 'styled-components'; + +import { Black, Grey5, White } from 'styles/color'; +import { Body4 } from 'styles/font'; +import { getPWAInstallCase } from 'utils/device'; + +const slideDown = keyframes` + from { + transform: translateX(-50%) translateY(-100%); + } + to { + transform: translateX(-50%) translateY(0); + } +`; + +const slideUp = keyframes` + from { + transform: translateX(-50%) translateY(0); + } + to { + transform: translateX(-50%) translateY(-100%); + } +`; + +const ToastContainer = styled.div<{ isClosing: boolean }>` + position: fixed; + width: fit-content; + box-sizing: border-box; + left: 50%; + top: 1rem; + background-color: ${White}; + color: ${Black}; + padding: 1rem 1.5rem; + border-radius: 0.5rem; + box-shadow: 0 0 6px rgba(0, 0, 0, 0.1), 0 4px 8px rgba(0, 0, 0, 0.05); + z-index: 1000; + + display: flex; + flex-direction: column; + gap: 0.75rem; + + animation: ${({ isClosing }) => (isClosing ? slideUp : slideDown)} 0.3s ease + forwards; +`; + +const PWAInstallWrapper = ({ children }: { children: React.ReactNode }) => { + const [installPromptEvent, setInstallPromptEvent] = + useState(null); + + const installCase = getPWAInstallCase(); + + const getPWAInstallToastContent = () => { + switch (installCase) { + case 'webview': + return null; + case 'direct': + return '앱을 홈 화면에 추가하여 더 편리하게 사용하세요'; + // 모바일 safari만 처리 + case 'guide': + return '공유 버튼 > 홈 화면에 추가를 눌러 더 편리하게 사용하세요'; + case 'unsupported': + return null; + } + }; + + const content = getPWAInstallToastContent(); + + const [isToastVisible, setToastVisible] = useState(false); // Toast 표시 여부 + const [isClosing, setIsClosing] = useState(false); // Toast 닫히는 애니메이션 상태 + + // `beforeinstallprompt` 이벤트 감지 + useEffect(() => { + console.log( + "BeforeInstallPromptEvent' in window", + 'BeforeInstallPromptEvent' in window, + ); + + const handleBeforeInstallPrompt = (event: Event) => { + event.preventDefault(); // 기본 동작 방지 + + setInstallPromptEvent(event as BeforeInstallPromptEvent); // 이벤트 저장 + setToastVisible(true); // Toast 표시 + }; + + if (installCase === 'direct') { + window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt); + } else if (installCase === 'guide') { + setToastVisible(true); + } + + return () => { + if (installCase === 'direct') { + window.removeEventListener( + 'beforeinstallprompt', + handleBeforeInstallPrompt, + ); + } + }; + }, []); + + // 설치 실행 + const handleInstall = async () => { + console.log('installPromptEvent', installPromptEvent); + if (installPromptEvent) { + installPromptEvent.prompt(); // 설치 프롬프트 표시 + const choice = await installPromptEvent.userChoice; + console.log(`User choice: ${choice.outcome}`); // "accepted" 또는 "dismissed" + + setInstallPromptEvent(null); // 이벤트 초기화 + handleCloseToast(); // Toast 닫기 + } + }; + + // Toast 닫기 + const handleCloseToast = () => { + setIsClosing(true); // 닫히는 애니메이션 시작 + setTimeout(() => setToastVisible(false), 300); // 애니메이션 끝난 뒤 완전히 제거 + }; + + if (installCase === 'unsupported' || installCase === 'webview') { + return; + } + + return ( + <> + {children} + + {/* Toast UI */} + {isToastVisible && ( + + + {content} + + + + + {installCase === 'direct' ? ( +