diff --git a/package-lock.json b/package-lock.json index a5b921d..be68a2d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@emotion/styled": "^11.13.0", "hangul-js": "^0.2.6", "primereact": "^10.8.2", + "html2canvas": "^1.4.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.26.1", @@ -3534,6 +3535,14 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -4524,6 +4533,22 @@ "node": ">= 8" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/cssom": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", @@ -6177,6 +6202,18 @@ "dev": true, "license": "MIT" }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -10782,6 +10819,22 @@ "node": ">=8" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -11164,6 +11217,22 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", diff --git a/package.json b/package.json index 7a968d0..182dc85 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@emotion/styled": "^11.13.0", "hangul-js": "^0.2.6", "primereact": "^10.8.2", + "html2canvas": "^1.4.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.26.1", diff --git a/src/assets/back.svg b/src/assets/back.svg index fff4912..21ab279 100644 --- a/src/assets/back.svg +++ b/src/assets/back.svg @@ -1,5 +1,5 @@ - + diff --git a/src/assets/link.svg b/src/assets/link.svg new file mode 100644 index 0000000..c3dcdf8 --- /dev/null +++ b/src/assets/link.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/share/academic.svg b/src/assets/share/academic.svg new file mode 100644 index 0000000..bdd62cc --- /dev/null +++ b/src/assets/share/academic.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/share/art.svg b/src/assets/share/art.svg new file mode 100644 index 0000000..29a7989 --- /dev/null +++ b/src/assets/share/art.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/share/religion.svg b/src/assets/share/religion.svg new file mode 100644 index 0000000..3d0a2de --- /dev/null +++ b/src/assets/share/religion.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/share/social.svg b/src/assets/share/social.svg new file mode 100644 index 0000000..aa6064e --- /dev/null +++ b/src/assets/share/social.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/share/sports.svg b/src/assets/share/sports.svg new file mode 100644 index 0000000..4504df9 --- /dev/null +++ b/src/assets/share/sports.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/display/ResultCard.styled.ts b/src/components/display/ResultCard.styled.ts new file mode 100644 index 0000000..e2e94a9 --- /dev/null +++ b/src/components/display/ResultCard.styled.ts @@ -0,0 +1,73 @@ +import { keyframes } from "@emotion/react"; +import styled from "@emotion/styled"; + +interface ResultCardStyleProps extends React.ComponentProps<"div"> { + color: string; +} +export const ResultCardWrapper = styled.div` + position: relative; + margin-top: 30px; + + width: 100%; + max-width: 390px; + height: 230px; + border-radius: 20px; + + overflow: hidden; + + display: flex; + + background-color: ${(props) => props.color}; + box-shadow: 4px 4px 16px #0000001f; + + &:before { + position: absolute; + content: ""; + width: 100%; + height: 100%; + top: 0; + left: 0; + background: linear-gradient(135deg, #ffffff00 0%, #ffffff5a 100%); + } +`; + +const wiggleRotate = keyframes` + 0% { + transform: rotate(-8deg); + } + 50% { + transform: rotate(368deg); + } + 100% { + transform: rotate(-8deg); + } +`; + +const LeftContent = styled.div` + width: 38%; + background-color: #ffffff2f; + + display: flex; + justify-content: center; + align-items: center; + + & > img { + animation: ${wiggleRotate} 5s infinite ease-in-out; + } +`; + +const RightContent = styled.div` + padding: 30px 20px; + + height: 100%; + box-sizing: border-box; + + display: flex; + flex-direction: column; + justify-content: space-between; +`; + +export const RSC = { + LeftContent, + RightContent, +}; diff --git a/src/components/display/ResultCard.tsx b/src/components/display/ResultCard.tsx new file mode 100644 index 0000000..d819b47 --- /dev/null +++ b/src/components/display/ResultCard.tsx @@ -0,0 +1,43 @@ +import React from "react"; + +import { Text } from "../typography"; +import { ResultCardWrapper, RSC } from "./ResultCard.styled"; + +export interface ResultCardDataProps extends Object { + name: string; + dbti_type: string; + dbti_name: string; + cardOrder: number; + color: string; + emoji: string; +} + +export interface ResultCardProps extends React.ComponentProps<"div"> { + props: ResultCardDataProps; +} + +export const ResultCard: React.FC = ({ props }) => { + return ( + + + + + + + {props.name}의 동BTI 카드 + +
+ + {props.dbti_type} + + + {props.dbti_name} + +
+ + {props.cardOrder}번째로 발급된 카드 + +
+ + ); +}; diff --git a/src/components/display/ShareLink.styled.ts b/src/components/display/ShareLink.styled.ts new file mode 100644 index 0000000..0e185f3 --- /dev/null +++ b/src/components/display/ShareLink.styled.ts @@ -0,0 +1,10 @@ +import styled from "@emotion/styled"; + +export const ShareLinkWrapper = styled.div` + display: flex; + gap: 4px; + justify-content: center; + align-items: center; + + opacity: 0.5; +`; diff --git a/src/components/display/ShareLink.tsx b/src/components/display/ShareLink.tsx new file mode 100644 index 0000000..089378b --- /dev/null +++ b/src/components/display/ShareLink.tsx @@ -0,0 +1,40 @@ +import { Text } from "../typography"; +import { ShareLinkWrapper } from "./ShareLink.styled"; + +interface ShareLinkProps { + link: string; + color: string; +} + +interface LinkIconProps { + color: string; +} + +export const LinkIcon: React.FC = ({ color }) => { + return ( + + + + + + + + + + + ); +}; + +export const ShareLink: React.FC = ({ link, color }) => { + return ( + + + + {link} + + + ); +}; diff --git a/src/components/form/Button.style.ts b/src/components/form/Button.style.ts index 427d896..23bda5d 100644 --- a/src/components/form/Button.style.ts +++ b/src/components/form/Button.style.ts @@ -2,6 +2,8 @@ import { ButtonProps } from "./Button"; import styled from "@emotion/styled"; export const ButtonElement = styled.button` + font-family: NanumSquareNeo; + width: ${(props) => props.width}; height: ${(props) => props.height}; diff --git a/src/components/layout/TopBar.styled.tsx b/src/components/layout/TopBar.styled.tsx index f463ff3..1f3155b 100644 --- a/src/components/layout/TopBar.styled.tsx +++ b/src/components/layout/TopBar.styled.tsx @@ -1,16 +1,32 @@ import styled from "@emotion/styled"; export const TopBarWrapper = styled.div` + position: fixed; + top: 0; + left: 50%; + transform: translateX(-50%); + + width: 100%; + position: fixed; + top: 0; + left: 50%; + transform: translateX(-50%); + + width: 100%; height: 60px; + + // RootLayout의 max-width를 따라갑니다. + max-width: 800px; + + // RootLayout의 max-width를 따라갑니다. + max-width: 800px; `; export const TopBarContainer = styled.div` - position: fixed; - top: 0; - left: 0; - right: 0; + position: relative; + display: flex; - height: 56px; + height: 60px; justify-content: center; align-items: center; @@ -18,4 +34,9 @@ export const TopBarContainer = styled.div` border-bottom: 1px solid #e0e0e0; font-weight: 800; background-color: var(--color-background); + + & > img { + position: absolute; + left: 15px; + } `; diff --git a/src/components/layout/TopBar.tsx b/src/components/layout/TopBar.tsx index 60f6da4..efb2c97 100644 --- a/src/components/layout/TopBar.tsx +++ b/src/components/layout/TopBar.tsx @@ -1,13 +1,22 @@ +import { ReactElement } from "react"; + +import { Text } from "../typography"; import { TopBarContainer, TopBarWrapper } from "./TopBar.styled"; interface TopBarProps { title: string; + icon?: ReactElement; } -const TopBar: React.FC = ({ title }) => { +const TopBar: React.FC = ({ title, icon }) => { return ( - {title} + + {icon} + + {title} + + ); }; diff --git a/src/components/navigation/AppBar.style.ts b/src/components/navigation/AppBar.style.ts deleted file mode 100644 index 1aaa239..0000000 --- a/src/components/navigation/AppBar.style.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { AppBarProps } from "./AppBar"; -import styled from "@emotion/styled"; - -export const AppBarElement = styled.span` - position: relative; - - width: 100%; - height: 60px; - - display: flex; - justify-content: center; - align-items: center; - - box-sizing: border-box; - - border-bottom: 2px solid #0000001f; - - & > span { - font-weight: 800; - } - - & > img { - position: absolute; - left: 15px; - } -`; diff --git a/src/components/navigation/AppBar.tsx b/src/components/navigation/AppBar.tsx deleted file mode 100644 index 244d7a0..0000000 --- a/src/components/navigation/AppBar.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { forwardRef } from "react"; - -import { AppBarElement } from "./AppBar.style"; - -export interface AppBarProps extends React.ComponentProps<"div"> { - children: React.ReactNode; -} - -export const AppBar = forwardRef(({ children, ...props }, ref) => { - return ( - - {children} - - ); -}); diff --git a/src/pages/AnalyticsPage.tsx b/src/pages/AnalyticsPage.tsx index 94dfbc4..5242fb7 100644 --- a/src/pages/AnalyticsPage.tsx +++ b/src/pages/AnalyticsPage.tsx @@ -2,7 +2,7 @@ import React from "react"; import { useNavigate } from "react-router-dom"; import DropDown from "@/components/form/DropDown"; -import { AppBar } from "@/components/navigation/AppBar"; +import TopBar from "@/components/layout/TopBar"; import { Text } from "@/components/typography"; import useAxios from "@/hooks/useAxios"; @@ -75,7 +75,7 @@ export default function AnalyticsPage() { return ( <> - + 어떤 유형이 가장 많을까요? diff --git a/src/pages/ResultShare.styled.ts b/src/pages/ResultShare.styled.ts new file mode 100644 index 0000000..152becc --- /dev/null +++ b/src/pages/ResultShare.styled.ts @@ -0,0 +1,26 @@ +import styled from "@emotion/styled"; + +export const Content = styled.div` + // 100vh에서 상단 AppBar의 높이만큼 감소시킵니다. + // 상하 padidng 값을 추가합니다. (10+10) + // 100vh - 60px + 20px + height: calc(100vh - 40px); + box-sizing: border-box; + + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: left; +`; + +export const ResultCardDiv = styled.div` + margin: auto; + + width: 100%; + max-width: 390px; + padding-bottom: 30px; + + display: flex; + flex-direction: column; + gap: 20px; +`; diff --git a/src/pages/ResultShare.tsx b/src/pages/ResultShare.tsx index 5e2f189..df91503 100644 --- a/src/pages/ResultShare.tsx +++ b/src/pages/ResultShare.tsx @@ -1,45 +1,108 @@ +import React from "react"; import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; -import { AppBar } from "@/components/navigation/AppBar"; +import html2canvas from "html2canvas"; + +import { ResultCard, ResultCardDataProps } from "@/components/display/ResultCard"; +import { ShareLink } from "@/components/display/ShareLink"; +import { Button } from "@/components/form/Button"; +import TopBar from "@/components/layout/TopBar"; import { Text } from "@/components/typography/Text"; import BackIcon from "@/assets/back.svg"; import ShareIcon from "@/assets/shareLarge.svg"; +import { Content, ResultCardDiv } from "./ResultShare.styled"; +import { useUserInfo } from "@/store/store"; import styled from "@emotion/styled"; const Header = styled.div` - margin-top: 20px; - margin-left: 30px; + margin-top: 70px; + margin-left: 10px; & > img { + margin-left: 6px; margin-bottom: 20px; } + + & > span { + line-height: 30px; + } `; -export default function ResultShare() { - const [name, setName] = useState(""); +export default function ResultShare({ type = "체육형 스타일", desc = "신체 활동을 좋아하는 타입" }) { + const navigate = useNavigate(); + const link = "https://dongbti.com"; + + const { name, setName } = useUserInfo(); - useEffect(() => { - // 전역 상태에서 이름을 불러와서 setName으로 디스플레이 이름을 변경합니다. - setName("홍길동"); + const [cardOrder, setCardOrder] = useState(0); + const [color, setColor] = useState(""); + const [emoji, setEmoji] = useState(""); + + const cardRef = React.useRef(null); + + const cardDownload = () => { + if (cardRef.current) { + html2canvas(cardRef.current).then((canvas) => { + const link = document.createElement("a"); + link.href = canvas.toDataURL("image/png"); + // 혹시 모를 상황에 대비하여 모든 공백을 _로 수정합니다. + // js/ts는 stirng.replaceAll이 없기 때문에 모든 특정 문자를 바꾸기 위해 정규식으로 사용해야합니다. + link.download = `${name}-${desc.replace(/\s+/g, "_")}.png`; + link.click(); + }); + } + }; + + useEffect(function initDisplay() { + fetch("https://api.dongbti.com/stats/total") + .then((response) => { + return response.json(); + }) + .then((res) => { + // 전역 상태에서 정보를 불러와 디스플레이 요소를 변경합니다. + setEmoji("sports"); + setColor("#559de0"); + setName("장기원"); // 임시 사용 코드 + setCardOrder(res.total_count); // n번째 발급 표시 + }); }, []); + const props: ResultCardDataProps = { + name: name, + dbti_type: type, + dbti_name: desc, + cardOrder: cardOrder, + color: color, + emoji: emoji, + }; + return ( <> - {/* 나중에 통일된 Header||AppBar로 변경해도 됩니다. */} - - - 결과 공유 - -
- - - {name}님의 동BTI를 -
- 공유해보세요 -
-
+ navigate(-1)} />} /> + {/* 여기부터는 주요 콘텐츠 표시 영역입니다. */} + +
+ + + {name}님의 동BTI를 +
+ 공유해보세요 +
+
+ {/* ResultCardDiv 기준으로 이미지가 다운로드됩니다. */} + + + + + +
); }