diff --git a/.env b/.env new file mode 100644 index 0000000..f1a9b41 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +VITE_APP_SERVER_HOST=http://ec2-3-35-208-177.ap-northeast-2.compute.amazonaws.com:8080/ \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index db82695..a77473d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,13 @@ "name": "picpic", "version": "0.0.0", "dependencies": { + "@react-spring/web": "^9.7.3", "@reduxjs/toolkit": "^1.9.7", "axios": "^1.6.1", "crypto-js": "^4.2.0", "html2canvas": "^1.4.1", "jotai": "^2.5.1", + "lodash": "^4.17.21", "node-sass": "^9.0.0", "path": "^0.12.7", "react": "^18.2.0", @@ -23,6 +25,7 @@ "react-scripts": "^5.0.1", "react-spring": "^9.7.3", "react-use-gesture": "^9.1.3", + "react-use-measure": "^2.1.1", "redux": "^4.2.1", "redux-persist": "^6.0.0", "redux-persist-transform-encrypt": "^5.0.1", @@ -9648,8 +9651,7 @@ "node_modules/debounce": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", - "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", - "peer": true + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==" }, "node_modules/debug": { "version": "4.3.4", @@ -23530,7 +23532,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.1.tgz", "integrity": "sha512-nocZhN26cproIiIduswYpV5y5lQpSQS1y/4KuvUCjSKmw7ZWIS/+g3aFnX3WdBkyuGUtTLif3UTqnLLhbDoQig==", - "peer": true, "dependencies": { "debounce": "^1.2.1" }, @@ -26181,16 +26182,16 @@ } }, "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=14.17" + "node": ">=4.2.0" } }, "node_modules/ua-parser-js": { @@ -26475,8 +26476,8 @@ "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", "dependencies": { - "base64-arraybuffer": "^1.0.2", "@types/istanbul-lib-coverage": "^2.0.1", + "base64-arraybuffer": "^1.0.2", "convert-source-map": "^1.6.0", "source-map": "^0.7.3" }, @@ -26484,6 +26485,11 @@ "node": ">=10.12.0" } }, + "node_modules/utrie/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -26492,6 +26498,19 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-to-istanbul": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", + "integrity": "sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/v8-to-istanbul/node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", diff --git a/package.json b/package.json index 4a0654b..10fd1b6 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,13 @@ "deploy": "npm run build && aws s3 sync dist/ s3://picpic-bucket --delete" }, "dependencies": { + "@react-spring/web": "^9.7.3", "@reduxjs/toolkit": "^1.9.7", "axios": "^1.6.1", "crypto-js": "^4.2.0", "html2canvas": "^1.4.1", "jotai": "^2.5.1", + "lodash": "^4.17.21", "node-sass": "^9.0.0", "path": "^0.12.7", "react": "^18.2.0", @@ -26,6 +28,7 @@ "react-scripts": "^5.0.1", "react-spring": "^9.7.3", "react-use-gesture": "^9.1.3", + "react-use-measure": "^2.1.1", "redux": "^4.2.1", "redux-persist": "^6.0.0", "redux-persist-transform-encrypt": "^5.0.1", diff --git a/src/App.jsx b/src/App.jsx index 5967a58..8fb6e4a 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,12 +1,13 @@ import "./App.css"; import { useEffect } from "react"; -import { Routes, Route, BrowserRouter } from "react-router-dom"; +import { Routes, Route, BrowserRouter, useParams } from "react-router-dom"; import Login from "./pages/Login/Login"; import Join from "./pages/Join/Join"; import Main from "./pages/Main/Main"; import Frame from "./pages/Frame/Frame"; import MakeFrame from "./pages/Frame/MakeFrame"; import Photobook from "./pages/Photobook/Photobook"; +import PhotobookUuid from "./pages/Photobook/PhotobookUuid"; import { BrowserView, MobileView, @@ -41,8 +42,8 @@ function App() { } /> } /> } /> - } /> } /> + } /> diff --git a/src/apis/api.js b/src/apis/api.js deleted file mode 100644 index 58692be..0000000 --- a/src/apis/api.js +++ /dev/null @@ -1,10 +0,0 @@ -import axios from "axios"; -import { getCookie } from "../api/cookie"; -// axios.create는 나만의 엑시오스 인스턴스를 만드는 메서드 -const api = axios.create({ - //baseurl에는 반복되는 url을 넣어즘 - baseURL: "http://ec2-3-35-208-177.ap-northeast-2.compute.amazonaws.com:8080", - //추가로 넣어야하는 옵션들을 넣어주면 되는데, 헤더만 필요하여 헤더를 넣어줌 - headers: { authorization: `Bearer ${getCookie("is_login")}` }, -}); -export default api; \ No newline at end of file diff --git a/src/apis/axios.js b/src/apis/axios.js new file mode 100644 index 0000000..2f511e7 --- /dev/null +++ b/src/apis/axios.js @@ -0,0 +1,10 @@ +import axios from "axios"; + +const { VITE_APP_SERVER_PORT } = import.meta.env; + +const instance = axios.create({ + baseURL: VITE_APP_SERVER_PORT, + withCredentials: true +}); + +export default instance; diff --git a/src/apis/axiosWithToken.js b/src/apis/axiosWithToken.js new file mode 100644 index 0000000..144d3a8 --- /dev/null +++ b/src/apis/axiosWithToken.js @@ -0,0 +1,22 @@ +import axios from "axios"; +import { useAtom } from "jotai"; +import { accessTokenAtom } from "../store/jotaiAtoms"; + +const { VITE_APP_SERVER_PORT } = import.meta.env; + +const useAxios = () => { + const [accessToken, ] = useAtom(accessTokenAtom); + + const instance = axios.create({ + baseURL: VITE_APP_SERVER_PORT, + withCredentials: true, + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json" + } + }); + + return instance; +}; + +export default useAxios; diff --git a/src/apis/onSignUp.js b/src/apis/onSignUp.js index e1e6578..02bcff9 100644 --- a/src/apis/onSignUp.js +++ b/src/apis/onSignUp.js @@ -8,10 +8,7 @@ export const onSignUp = (userInfo, router) => { // password: sha256(userInfo.password).toString(), // }; axios - .post( - "http://ec2-3-35-208-177.ap-northeast-2.compute.amazonaws.com:8080/user/join", - userInfo - ) + .post("http://ec2-3-35-208-177.ap-northeast-2.compute.amazonaws.com:8080/user/join", userInfo) .then((res) => { console.log(res.data); router("/login"); diff --git a/src/assets/main-logo.png b/src/assets/main-logo.png index 666192f..850bd4b 100644 Binary files a/src/assets/main-logo.png and b/src/assets/main-logo.png differ diff --git a/src/component/AllFrameCpn/FrameList.jsx b/src/component/AllFrameCpn/FrameList.jsx index bb32edd..25319bc 100644 --- a/src/component/AllFrameCpn/FrameList.jsx +++ b/src/component/AllFrameCpn/FrameList.jsx @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; import styles from "./FrameList.module.css"; -import axios from "axios"; +import useAxios from "../../apis/axiosWithToken"; import { Link } from "react-router-dom"; // import { getAllFrames } from "../../apis/getFrame"; import { useAtom } from "jotai"; @@ -8,33 +8,23 @@ import { accessTokenAtom } from "../../store/jotaiAtoms"; const FrameList = () => { const [frames, setFrames] = useState([]); - const [accessToken, setAccessToken] = useAtom(accessTokenAtom); + const [accessToken, ] = useAtom(accessTokenAtom); + const axios = useAxios(); useEffect(() => { // 액세스 토큰이 있을 때만 API 요청을 보내도록 조건 처리 if (accessToken) { console.log("Framelist ACT:", accessToken); // accessToken jotai에서 잘 불러오는지 확인 // axios 요청 설정 - const config = { - headers: { - Authorization: `Bearer ${accessToken}`, // 헤더에 accessToken을 추가 - }, - }; + // const config = { + // headers: { + // Authorization: `Bearer ${accessToken}`, // 헤더에 accessToken을 추가 + // }, + // }; // API 요청 보내기 - axios - .get( - "http://ec2-3-35-208-177.ap-northeast-2.compute.amazonaws.com:8080/frame/list", - config - ) - .then((res) => { - console.log(res.data); - // 성공적으로 데이터를 받아온 경우 - setFrames(res.data); // 받아온 데이터로 frames 상태 업데이트 - }) - .catch((err) => { - // 오류 처리 - console.error("API 요청 중 오류 발생:", err); - }); + axios.get('/frame/list') // 인스턴스로 axios 요청 보내기 + .then((response) => setFrames(response.data)) + .catch((error) => console.error(error)); } }, [accessToken]); // useEffect가 실행되는 조건을 accessToken이 변경될 때로 설정 diff --git a/src/img/photo10.png b/src/img/photo10.png index 7b0f80f..6be81ca 100644 Binary files a/src/img/photo10.png and b/src/img/photo10.png differ diff --git a/src/img/photo11.png b/src/img/photo11.png index 6be81ca..785e7c3 100644 Binary files a/src/img/photo11.png and b/src/img/photo11.png differ diff --git a/src/img/photo12.png b/src/img/photo12.png new file mode 100644 index 0000000..e911ce6 Binary files /dev/null and b/src/img/photo12.png differ diff --git a/src/img/photo13.png b/src/img/photo13.png new file mode 100644 index 0000000..8b09eed Binary files /dev/null and b/src/img/photo13.png differ diff --git a/src/img/photo14.png b/src/img/photo14.png new file mode 100644 index 0000000..f39875b Binary files /dev/null and b/src/img/photo14.png differ diff --git a/src/img/photo15.png b/src/img/photo15.png new file mode 100644 index 0000000..b41046a Binary files /dev/null and b/src/img/photo15.png differ diff --git a/src/index.css b/src/index.css index f2e9315..b5e5210 100644 --- a/src/index.css +++ b/src/index.css @@ -4,15 +4,19 @@ } :root { + width: 100vw; + height: calc(var(--vh, 1vh) * 100); font-family: "SUIT Variable", -apple-system, BlinkMacSystemFont, Pretendard, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; font-weight: 400; color: #1a1e27; - height: 844px; margin: 0; overflow-x: hidden; overflow-y: hidden; + display: flex; + align-items: center; + justify-content: center; } button { diff --git a/src/pages/Login/Login.jsx b/src/pages/Login/Login.jsx index 62676c6..98c546f 100644 --- a/src/pages/Login/Login.jsx +++ b/src/pages/Login/Login.jsx @@ -10,7 +10,7 @@ import { useAtom } from "jotai"; import { accessTokenAtom } from "../../store/jotaiAtoms"; import axios from "axios"; -export default function Main() { +export default function Login() { return ( <> @@ -61,20 +61,15 @@ function LoginForm() { password, }; - axios - .post( - "http://ec2-3-35-208-177.ap-northeast-2.compute.amazonaws.com:8080/user/login", - data - ) - .then((res) => { - const accessToken = res.data; - setAct(accessToken); // 액세스 토큰을 Jotai 상태에 업데이트 - localStorage.setItem("accessToken", `${accessToken}`); - router('/framelist'); - }) - .catch((err) => { - console.log(err); - }); + try { + const response = await axios.post("http://ec2-3-35-208-177.ap-northeast-2.compute.amazonaws.com:8080/user/login", data); + const accessToken = response.data; + setAct(accessToken); + localStorage.setItem("accessToken", accessToken); + router('/frame'); + } catch (error) { + console.error(error); + } }; return ( diff --git a/src/pages/Login/Login.module.scss b/src/pages/Login/Login.module.scss index ec24ba9..928d660 100644 --- a/src/pages/Login/Login.module.scss +++ b/src/pages/Login/Login.module.scss @@ -327,7 +327,7 @@ body { } .LOGO { - margin: 0 auto; + margin: 0px auto 20px auto; align-items: center; } diff --git a/src/pages/Main/Main.jsx b/src/pages/Main/Main.jsx index 5acd3f1..022ab7d 100644 --- a/src/pages/Main/Main.jsx +++ b/src/pages/Main/Main.jsx @@ -5,7 +5,7 @@ import { useAtom } from "jotai"; import { accessTokenAtom } from "../../store/jotaiAtoms"; export default function Login() { - const [accessToken, setAccessToken] = useAtom(accessTokenAtom); + const [ , setAccessToken] = useAtom(accessTokenAtom); const router = useNavigate(); const logout = () => { @@ -19,28 +19,21 @@ export default function Login() { return (
- - - + + -
); } diff --git a/src/pages/Main/Main.module.scss b/src/pages/Main/Main.module.scss index ad6e17b..6bb3bf1 100644 --- a/src/pages/Main/Main.module.scss +++ b/src/pages/Main/Main.module.scss @@ -3,31 +3,28 @@ flex-direction: column; align-items: center; justify-items: center; - gap: 20px; } -.linkBtn { +.logoBtn { width: 100%; height: 100%; background-color: #ffffff; border: none; + margin-bottom: 100px; img { - width: 50%; + width: 56%; height: 50%; display: inline-flex; } } -button { +.logoutBtn, .loginBtn { width: 50vw; - height: 46px; - background-color: #009EFF; + height: 45px; + background-color: #009eff; border: none; border-radius: 50px; color: white; font-size: 14px; -} -.frameBtn { - background-color: #C4CAD4; - color: black; - font-weight: 500; + font-weight: 600; + margin-top: 10px; } \ No newline at end of file diff --git a/src/pages/Photobook/PhotoSelect.jsx b/src/pages/Photobook/PhotoSelect.jsx index c1911ea..dbbdfbe 100644 --- a/src/pages/Photobook/PhotoSelect.jsx +++ b/src/pages/Photobook/PhotoSelect.jsx @@ -1,62 +1,88 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import styles from "./Photoselect.module.scss"; -import photo1 from "../../img/photo1.png"; -import photo2 from "../../img/photo2.png"; -import photo3 from "../../img/photo3.png"; -import photo4 from "../../img/photo4.png"; -import photo5 from "../../img/photo5.png"; -import photo6 from "../../img/photo6.png"; -import photo7 from "../../img/photo7.png"; -import photo8 from "../../img/photo8.png"; -import photo9 from "../../img/photo9.png"; -import photo10 from "../../img/photo10.png"; -import photo11 from "../../img/photo11.png"; -import circleCheck from "../../img/circle-check-filled.png"; +// import circleCheck from "../../img/circle-check-filled.png"; +import useAxios from "../../apis/axiosWithToken"; +import { useNavigate } from "react-router-dom"; export default function PhotoSelect() { - const photoArray = [ - photo1, photo2, photo3, photo4, photo5, photo6, - photo7, photo8, photo9, photo10, photo11 - ]; - + const axios = useAxios(); + const [getPhotos, setGetPhotos] = useState([]); const [selectedPhotos, setSelectedPhotos] = useState([]); + const router = useNavigate(); const handlePhotoClick = (index) => { - // 이미 선택된 사진이면 제거, 아니면 추가 - if (selectedPhotos.includes(index)) { - setSelectedPhotos(selectedPhotos.filter((selectedIndex) => selectedIndex !== index)); - } else { - setSelectedPhotos([...selectedPhotos, index]); - } + setSelectedPhotos((prevSelectedPhotos) => { + // 이미 선택된 사진이면 제거, 아니면 추가 + if (prevSelectedPhotos.includes(index)) { + const newSelectedPhotos = prevSelectedPhotos.filter((selectedIndex) => selectedIndex !== index); + console.log("삭제 후:", newSelectedPhotos); + return newSelectedPhotos; + } else { + const newSelectedPhotos = [...prevSelectedPhotos, index]; + console.log("추가 후:", newSelectedPhotos); + return newSelectedPhotos; + } + }); + }; - console.log(selectedPhotos); + useEffect(() => { + console.log("세팅 후:", selectedPhotos); + console.log("api 보내기전: ", selectedPhotos.map(index => getPhotos[index])) + axios + .get('/photo') + .then((response) => { + setGetPhotos(response.data); + // console.log(getPhotos); + }) + .catch((error) => console.error(error)); + }, [selectedPhotos]); + + const handlePostData = async () => { + const data = { + "name" : "토리", + "addPhotoList" : selectedPhotos.map(index => getPhotos[index]) + }; + + try { + const response = await axios.post('/photoBook', data); + console.log("API 응답:", response.data); + router("/photobook"); + } catch (error) { + console.error("API 오류:", error); + } }; return (
-
- -
-
- -
+
{/* 이미지 배열을 map 함수를 사용하여 렌더링 */} - {photoArray.map((photo, index) => ( + {getPhotos.map((photo, index) => (
handlePhotoClick(index)} + className={styles.photoBox} > {`photo-${index}`} + {selectedPhotos.includes(index) && ( +
+
+ {selectedPhotos.indexOf(index) + 1} +
+
+ )}
))}
diff --git a/src/pages/Photobook/PhotoSelect.module.scss b/src/pages/Photobook/PhotoSelect.module.scss index 8d01431..4cc13f7 100644 --- a/src/pages/Photobook/PhotoSelect.module.scss +++ b/src/pages/Photobook/PhotoSelect.module.scss @@ -6,49 +6,25 @@ .header { width: 100vw; - height: 125px; + height: 12vh; display: flex; - flex-direction: column; + align-items: center; + justify-content: center; background-color: white; - .header1 { - height: 30px; - display: flex; - align-items: center; - justify-content: flex-end; - border-bottom: 1.5px solid rgba(0, 0, 0, 0.06); - button { - margin-right: 18px; - width: 45px; - height: 24px; - border: none; - border-radius: 19px; - background-color: #f7f8f8; - color: #009eff; - text-align: center; - font-size: 12px; - font-weight: 400; - cursor: pointer; - } - } - .header2 { - height: 85px; - display: flex; - align-items: center; - justify-content: center; - button { - height: 50px; - width: 340px; - border-radius: 10px; - border: 1px solid #009eff; - color: #009eff; - text-align: center; - font-size: 14px; - font-weight: 500; - background-color: white; - &:hover { - background-color: #009eff; - color: white; - } + margin-top: 44px; + button { + height: 50px; + width: 340px; + border-radius: 10px; + border: 1px solid #009eff; + color: #009eff; + text-align: center; + font-size: 14px; + font-weight: 500; + background-color: white; + &:hover { + background-color: #009eff; + color: white; } } } @@ -59,6 +35,7 @@ display: flex; justify-content: center; .boxWrapper { + position: relative; width: 340px; min-height: 673px; display: grid; @@ -67,25 +44,40 @@ grid-gap: 20px; /* 각 요소 사이의 간격 설정 */ } .photoBox { + position: relative; width: 160px; height: 160px; background-color: #f7f7f7; img { + position: absolute; + z-index: 1; height: 160px; + left: 26.22px; width: auto; } - .selectedPhoto { + .selectBox { position: absolute; z-index: 5; width: 160px; height: 160px; - background-color: rgba(0, 0, 0, 0.1); - } - &:hover { - border: 2px solid #009eff; // 마우스 호버 시 테두리 스타일 - } - &.selectedPhoto { - border: 2px solid #009eff; // 선택된 PhotoBox에 테두리 스타일 + background-color: rgba(255, 255, 255, 0.4); + border: 2.5px solid #009eff; + box-sizing: border-box; + display: flex; + flex-direction: row-reverse; + .selectNumber { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + color: white; + font-size: 14px; + font-weight: 600; + background-color: #009eff; + border-radius: 40px; + margin: 8px; + } } } } diff --git a/src/pages/Photobook/Photobook.jsx b/src/pages/Photobook/Photobook.jsx index aeba4dc..2b67d02 100644 --- a/src/pages/Photobook/Photobook.jsx +++ b/src/pages/Photobook/Photobook.jsx @@ -1,8 +1,11 @@ -import { useState } from "react"; +import { useEffect, useState, useRef } from "react"; +import { useSprings, useSpring, animated } from "@react-spring/web"; +import useMeasure from "react-use-measure"; +import { useDrag } from "react-use-gesture"; +import clamp from "lodash/clamp"; import styles from "./Photobook.module.scss"; -import memoIcon from "../../img/icon-memo.png"; +// import memoIcon from "../../img/icon-memo.png"; import shareIcon from "../../img/icon-share.png"; -import photo9 from "../../img/photo9.png"; import navBottom from "../../img/nav-bottom.png"; import memoModal from "../../img/memo-modal.png"; import emoji1 from "../../img/emoji1.png"; @@ -20,108 +23,242 @@ import memopic2 from "../../img/memopic2.png"; import memopic3 from "../../img/memopic3.png"; import memopic4 from "../../img/memopic4.png"; import memopic5 from "../../img/memopic5.png"; +import useAxios from "../../apis/axiosWithToken"; export default function Photobook() { - const [modal, setModal] = useState(0); + {/* 포토북 UI 시작 */} + const index = useRef(0); + const [ref, { width }] = useMeasure(); + const [photos, setPhotos] = useState([]); + const [activeDot, setActiveDot] = useState(0); + const [props, api] = useSprings( + photos.length, + (i) => ({ + x: i * width, + scale: width === 0 ? 0 : 1, + display: "block", + }), + [width] + ); + const bind = useDrag( + ({ active, movement: [mx], direction: [xDir], distance, cancel }) => { + if (active && distance > width / 2) { + setActiveDot(null); + index.current = clamp( + index.current + (xDir > 0 ? -1 : 1), + 0, + photos.length - 1 + ); + cancel(); + } + api.start((i) => { + if (i < index.current - 1 || i > index.current + 1) + return { display: "none" }; + const x = (i - index.current) * width + (active ? mx : 0); + const scale = active ? 1 - distance / width / 2 : 1; + return { x, scale, display: "block" }; + }); + + // 스크롤에 따른 활성화를 유지 + // setActiveDot(clamp(Math.round(index.current), 0, photos.length - 1)); + // 클릭한 동그라미가 있다면 스크롤로 인한 활성화를 무시하고 클릭한 동그라미를 유지 + const activeDotIndex = clamp( + Math.round(index.current), + 0, + photos.length - 1 + ); + setActiveDot(activeDotIndex); + // console.log("스크롤 점 인덱스: ", activeDotIndex); + } + ); + // 동그라미를 클릭했을 때 해당 사진으로 이동하는 함수 + const handleDotClick = (dotIndex) => { + index.current = dotIndex; + api.start((i) => { + if (i < index.current - 1 || i > index.current + 1) + return { display: "none" }; + const x = (i - index.current) * width; + return { x, scale: 1, display: "block" }; + }); + setActiveDot(null); // 클릭한 동그라미를 활성화 상태로 설정 + }; + {/* 포토북 UI 마침 */} + + const axios = useAxios(); + const [modal, setModal] = useState(0); const [selectedEmoji, setSelectedEmoji] = useState(null); + const [uuid, setUuid] = useState(null); + const [isEmojiBoxVisible, setIsEmojiBoxVisible] = useState(false); - const emojiArray = [emoji1, emoji2, emoji3, emoji4, emoji5]; - - const emojiArray2 = [emoji01, emoji02, emoji03, emoji04, emoji05]; + const emojiArray = [emoji1, emoji2, emoji3, emoji4, emoji5]; + const emojiArray2 = [emoji01, emoji02, emoji03, emoji04, emoji05]; + const memopicArray = [memopic1, memopic2, memopic3, memopic4, memopic5]; - const selectEmoji = (index) => { - // 이미 선택된 emoji가 있다면 선택 취소 - if (selectedEmoji === index) { - setSelectedEmoji(null); - } else { - // 선택된 emoji가 없거나 다른 emoji를 선택한 경우 크기 조절 - setSelectedEmoji(index); - } + useEffect(() => { + axios + .get("/photoBook") // 인스턴스로 axios 요청 보내기 + .then((response) => { + console.log(response.data); + setPhotos(response.data.photoList); + setUuid(response.data.uuid); + }) + .catch((error) => console.error(error)); + }); + + const selectEmoji = (index) => { + setSelectedEmoji(index); + // // 이미 선택된 emoji가 있다면 선택 취소 + // if (selectedEmoji !== null && selectedEmoji === index) { + // // 이미 선택된 경우에는 아무 동작도 하지 않음 + // return; + // } else { + // // 선택된 emoji가 없거나 다른 emoji를 선택한 경우 크기 조절 + // setSelectedEmoji(index); + // } }; - const saveMemo = () => { - - } + // const toggleEmojiBox = () => { + // setIsEmojiBoxVisible(!isEmojiBoxVisible); + // }; + + // 이모지 박스 애니메이션 스타일 설정 + const emojiBoxSpring = useSpring({ + opacity: isEmojiBoxVisible ? 1 : 0, + transform: `translateY(${isEmojiBoxVisible ? 0 : 100}%)`, + config: { tension: 200, friction: 20, mass: 1, duration: 100 }, + }); + + const shareLink = () => { + // 현재 경로에 uuid를 붙여서 공유할 링크 생성 + const shareableLink = `${window.location.href}/${uuid}`; + + // 클립보드에 복사 + navigator.clipboard + .writeText(shareableLink) + .then(() => { + console.log("링크가 클립보드에 복사되었습니다."); + }) + .catch((err) => { + console.error("링크 복사 중 오류 발생:", err); + }); + console.log("링크:", `${window.location.href}/${uuid}`); + // // 현재 주소에 uuid 파라미터 추가 + // const currentUrl = new URL(window.location.href); + // currentUrl.searchParams.set('uuid', 'YOUR_UUID'); // 여기에 받아온 uuid 변수를 넣어주세요 + // navigator.clipboard.writeText(currentUrl.href) + // .then(() => { + // alert('링크가 복사되었습니다.'); + // }) + // .catch((error) => { + // console.error('링크 복사 중 오류 발생:', error); + // }); + }; + + const saveMemo = () => {}; return ( -
-
-

'이름'님의 소중한 추억들입니다 -
스티커와 쪽지를 선물해 주세요~

-
-
- {photo9} -
-
-
- -

링크 공유하기

-
-
- -

쪽지 입력하기

-
-
-
- {navBottom} -
- {/* 모달 */} -
- {memoModal} -
-
- {emojiArray.map((emoji, index) => ( - +
+

+ '이름'님의 소중한 추억들입니다 +
+ 스티커와 쪽지를 선물해 주세요~ +

+
+
+
+ {props.map(({ x, display, scale }, i) => ( + + + {selectedEmoji !== null && ( + setModal(1)} + /> + )} + + ))} +
+ {/* 사진 갯수만큼 동그라미 렌더링 */} + {photos.map((_, dotIndex) => ( +
selectEmoji(index)} + onClick={() => handleDotClick(dotIndex)} /> - ))} -
- - -
-
-
setModal(0)} - > -
-
+ ))} +
+
+ {/* */} +
+
+
+ +

링크 공유하기

+
+ {/*
+ +

쪽지 입력하기

+
*/} +
+
+ {navBottom} +
+ {/* 모달 */} +
+ {memoModal} +
+ + +
+
+
setModal(0)} + >
+ {/* 이모지 박스 */} + {isEmojiBoxVisible && ( + + {emojiArray.map((emoji, index) => ( + {`emoji-${index}`} { + selectEmoji(index); + }} + /> + ))} + + )} +
); -} \ No newline at end of file +} diff --git a/src/pages/Photobook/Photobook.module.scss b/src/pages/Photobook/Photobook.module.scss index e6b443c..72b1cda 100644 --- a/src/pages/Photobook/Photobook.module.scss +++ b/src/pages/Photobook/Photobook.module.scss @@ -1,20 +1,93 @@ +*, +*:after, +*:before { + box-sizing: border-box; +} + .wrapper { width: 100vw; height: 100vh; - overflow-x: hidden; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.photosWrapper { + width: 100vw; + height: 56vh; + display: flex; + align-items: center; + justify-content: center; + position: relative; +} + +.photos { + position: absolute; + width: 290px; + height: 431.4px; + will-change: transform; + .photo { + position: absolute; + z-index: 2; + touch-action: none; + background-size: cover; + background-repeat: no-repeat; + background-position: center center; + width: 290px; + height: 430px; + margin-top: -12px; + will-change: transform; + box-shadow: + 0 62.5px 125px -25px rgba(50, 50, 73, 0.5), + 0 37.5px 75px -37.5px rgba(0, 0, 0, 0.6); + } + .memopic { + position: absolute; + z-index: 12; + width: 60px; + height: 60px; + } +} + +.dotsWrapper { + position: absolute; + bottom: 6px; + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: center; +} + +.dot { + width: 10px; + height: 10px; + background-color: #ccc; + border-radius: 50%; + margin: 0 5px; + cursor: pointer; + transition: background-color 0.3s ease; + + &:hover { + background-color: #555; + } +} + +.activeDot { + background-color: #555; } .section1 { width: 100vw; height: 12vh; display: flex; - flex-direction: column; + flex-direction: row; align-items: flex-start; justify-content: flex-start; background-color: white; border-bottom: 1.5px solid rgba(0, 0, 0, 0.06); p { - font-family: Pretendard; + font-family: Pretendard; margin-left: 18px; margin-top: 22px; font-size: 18px; @@ -25,24 +98,25 @@ } .section2 { + width: 100%; height: 58vh; + overflow-x: auto; background-color: #DCF2FF; - img { - height: 58vh; - width: auto; - } + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + position: relative; } .section3 { + position: relative; height: 21vh; display: flex; flex-direction: row; align-items: center; justify-content: center; background-color: white; - :nth-child(1) { - margin-right: 72px; - } .buttonBox { width: 72px; height: 89px; @@ -114,14 +188,6 @@ flex-direction: column; align-items: center; justify-content: space-between; - .emojiBox { - width: 66vw; - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - margin-top: -20px; - } textarea { width: 66%; height: 43%; @@ -179,3 +245,21 @@ .modalBG0 { display: none; } + +.emojiBox { + position: absolute; + z-index: 10; + bottom: 31vh; + width: 80vw; + height: 72px; + padding: 0 22px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + border-radius: 70px; + background-color: white; + opacity: 0; + transform: translateY(20px); + transition: opacity 0.5s ease, transform 0.5s ease; +} \ No newline at end of file diff --git a/src/pages/Photobook/PhotobookUuid.jsx b/src/pages/Photobook/PhotobookUuid.jsx new file mode 100644 index 0000000..537c58f --- /dev/null +++ b/src/pages/Photobook/PhotobookUuid.jsx @@ -0,0 +1,260 @@ +import { useEffect, useState, useRef } from "react"; +import { useSprings, useSpring, animated } from "@react-spring/web"; +import useMeasure from "react-use-measure"; +import { useDrag } from "react-use-gesture"; +import { useParams } from "react-router-dom"; +import clamp from "lodash/clamp"; +import styles from "./Photobook.module.scss"; +import memoIcon from "../../img/icon-memo.png"; +// import shareIcon from "../../img/icon-share.png"; +import navBottom from "../../img/nav-bottom.png"; +import memoModal from "../../img/memo-modal.png"; +import emoji1 from "../../img/emoji1.png"; +import emoji2 from "../../img/emoji2.png"; +import emoji3 from "../../img/emoji3.png"; +import emoji4 from "../../img/emoji4.png"; +import emoji5 from "../../img/emoji5.png"; +import emoji01 from "../../img/emoji-1.png"; +import emoji02 from "../../img/emoji-2.png"; +import emoji03 from "../../img/emoji-3.png"; +import emoji04 from "../../img/emoji-4.png"; +import emoji05 from "../../img/emoji-5.png"; +import memopic1 from "../../img/memopic1.png"; +import memopic2 from "../../img/memopic2.png"; +import memopic3 from "../../img/memopic3.png"; +import memopic4 from "../../img/memopic4.png"; +import memopic5 from "../../img/memopic5.png"; +import useAxios from "../../apis/axiosWithToken"; + +export default function Photobook() { + {/* 포토북 UI 시작 */} + const index = useRef(0); + const [ref, { width }] = useMeasure(); + const [photos, setPhotos] = useState([]); + const [activeDot, setActiveDot] = useState(0); + const [props, api] = useSprings( + photos.length, + (i) => ({ + x: i * width, + scale: width === 0 ? 0 : 1, + display: "block", + }), + [width] + ); + const bind = useDrag( + ({ active, movement: [mx], direction: [xDir], distance, cancel }) => { + if (active && distance > width / 2) { + setActiveDot(null); + index.current = clamp( + index.current + (xDir > 0 ? -1 : 1), + 0, + photos.length - 1 + ); + cancel(); + } + api.start((i) => { + if (i < index.current - 1 || i > index.current + 1) + return { display: "none" }; + const x = (i - index.current) * width + (active ? mx : 0); + const scale = active ? 1 - distance / width / 2 : 1; + return { x, scale, display: "block" }; + }); + + // 스크롤에 따른 활성화를 유지 + // setActiveDot(clamp(Math.round(index.current), 0, photos.length - 1)); + // 클릭한 동그라미가 있다면 스크롤로 인한 활성화를 무시하고 클릭한 동그라미를 유지 + const activeDotIndex = clamp( + Math.round(index.current), + 0, + photos.length - 1 + ); + setActiveDot(activeDotIndex); + // console.log("스크롤 점 인덱스: ", activeDotIndex); + } + ); + // 동그라미를 클릭했을 때 해당 사진으로 이동하는 함수 + const handleDotClick = (dotIndex) => { + index.current = dotIndex; + api.start((i) => { + if (i < index.current - 1 || i > index.current + 1) + return { display: "none" }; + const x = (i - index.current) * width; + return { x, scale: 1, display: "block" }; + }); + setActiveDot(null); // 클릭한 동그라미를 활성화 상태로 설정 + }; + {/* 포토북 UI 마침 */} + + const axios = useAxios(); + const [modal, setModal] = useState(0); + const [selectedEmoji, setSelectedEmoji] = useState(null); + const [uuidData, setUuidData] = useState(null); + const [isEmojiBoxVisible, setIsEmojiBoxVisible] = useState(false); + const { uuid } = useParams(); + + const emojiArray = [emoji1, emoji2, emoji3, emoji4, emoji5]; + const emojiArray2 = [emoji01, emoji02, emoji03, emoji04, emoji05]; + const memopicArray = [memopic1, memopic2, memopic3, memopic4, memopic5]; + + useEffect(() => { + axios + .get(`/photoBook/${uuid}`) // 인스턴스로 axios 요청 보내기 + .then((response) => { + console.log(response.data); + setPhotos(response.data.photoList); + setUuidData(response.data.uuid); + }) + .catch((error) => console.error(error)); + }, [uuid]); + + const selectEmoji = (index) => { + setSelectedEmoji(index); + // // 이미 선택된 emoji가 있다면 선택 취소 + // if (selectedEmoji !== null && selectedEmoji === index) { + // // 이미 선택된 경우에는 아무 동작도 하지 않음 + // return; + // } else { + // // 선택된 emoji가 없거나 다른 emoji를 선택한 경우 크기 조절 + // setSelectedEmoji(index); + // } + }; + + const toggleEmojiBox = () => { + setIsEmojiBoxVisible(!isEmojiBoxVisible); + }; + + // 이모지 박스 애니메이션 스타일 설정 + const emojiBoxSpring = useSpring({ + opacity: isEmojiBoxVisible ? 1 : 0, + transform: `translateY(${isEmojiBoxVisible ? 0 : 100}%)`, + config: { tension: 200, friction: 20, mass: 1, duration: 100 }, + }); + + // const shareLink = () => { + // // 현재 경로에 uuid를 붙여서 공유할 링크 생성 + // const shareableLink = `${window.location.href}/${uuidData}`; + + // // 클립보드에 복사 + // navigator.clipboard + // .writeText(shareableLink) + // .then(() => { + // console.log("링크가 클립보드에 복사되었습니다."); + // }) + // .catch((err) => { + // console.error("링크 복사 중 오류 발생:", err); + // }); + // console.log("링크:", `${window.location.href}/${uuidData}`); + // // // 현재 주소에 uuid 파라미터 추가 + // // const currentUrl = new URL(window.location.href); + // // currentUrl.searchParams.set('uuid', 'YOUR_UUID'); // 여기에 받아온 uuid 변수를 넣어주세요 + // // navigator.clipboard.writeText(currentUrl.href) + // // .then(() => { + // // alert('링크가 복사되었습니다.'); + // // }) + // // .catch((error) => { + // // console.error('링크 복사 중 오류 발생:', error); + // // }); + // }; + + const saveMemo = () => {}; + + return ( +
+
+

+ '이름'님의 소중한 추억들입니다 +
+ 스티커와 쪽지를 선물해 주세요~ +

+
+
+
+ {props.map(({ x, display, scale }, i) => ( + + + {selectedEmoji !== null && ( + setModal(1)} + /> + )} + + ))} +
+ {/* 사진 갯수만큼 동그라미 렌더링 */} + {photos.map((_, dotIndex) => ( +
handleDotClick(dotIndex)} + /> + ))} +
+
+ {/* */} +
+
+
+ +

쪽지 입력하기

+
+
+
+ {navBottom} +
+ {/* 모달 */} +
+ {memoModal} +
+ + +
+
+
setModal(0)} + >
+ {/* 이모지 박스 */} + {isEmojiBoxVisible && ( + + {emojiArray.map((emoji, index) => ( + {`emoji-${index}`} { + selectEmoji(index); + }} + /> + ))} + + )} +
+ ); +} diff --git a/src/pages/Photobook/PhotobookUuid.module.scss b/src/pages/Photobook/PhotobookUuid.module.scss new file mode 100644 index 0000000..8536dfb --- /dev/null +++ b/src/pages/Photobook/PhotobookUuid.module.scss @@ -0,0 +1,265 @@ +*, +*:after, +*:before { + box-sizing: border-box; +} + +.wrapper { + width: 100vw; + height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.photosWrapper { + width: 100vw; + height: 56vh; + display: flex; + align-items: center; + justify-content: center; + position: relative; +} + +.photos { + position: absolute; + width: 290px; + height: 431.4px; + will-change: transform; + .photo { + position: absolute; + z-index: 2; + touch-action: none; + background-size: cover; + background-repeat: no-repeat; + background-position: center center; + width: 290px; + height: 430px; + margin-top: -12px; + will-change: transform; + box-shadow: + 0 62.5px 125px -25px rgba(50, 50, 73, 0.5), + 0 37.5px 75px -37.5px rgba(0, 0, 0, 0.6); + } + .memopic { + position: absolute; + z-index: 12; + width: 60px; + height: 60px; + } +} + +.dotsWrapper { + position: absolute; + bottom: 6px; + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: center; +} + +.dot { + width: 10px; + height: 10px; + background-color: #ccc; + border-radius: 50%; + margin: 0 5px; + cursor: pointer; + transition: background-color 0.3s ease; + + &:hover { + background-color: #555; + } +} + +.activeDot { + background-color: #555; +} + +.section1 { + width: 100vw; + height: 12vh; + display: flex; + flex-direction: row; + align-items: flex-start; + justify-content: flex-start; + background-color: white; + border-bottom: 1.5px solid rgba(0, 0, 0, 0.06); + p { + font-family: Pretendard; + margin-left: 18px; + margin-top: 22px; + font-size: 18px; + font-weight: 600; + line-height: normal; + text-align: start; + } +} + +.section2 { + width: 100%; + height: 58vh; + overflow-x: auto; + background-color: #DCF2FF; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + position: relative; +} + +.section3 { + position: relative; + height: 21vh; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + background-color: white; + .buttonBox { + width: 72px; + height: 89px; + display: flex; + flex-direction: column; + button { + width: 72px; + height: 72px; + border-radius: 15px; + background: #F7F7F7; + display: flex; + align-items: center; + justify-content: center; + border: none; + img { + width: 72px; + height: 72px; + margin: 0; + } + } + p { + color: #009EFF; + text-align: center; + font-family: Pretendard; + font-size: 10px; + font-weight: 700; + line-height: normal; + } + } +} + +.navBottom { + width: 100vw; + img { + width: 100vw; + } +} + +.modal0 { + display: none; +} +.modal1 { + animation: fadeIn ease-in-out 1s; + position: absolute; + // box-shadow: 2px 4px 12px #cacaca; + z-index: 2; + width: 344px; + height: 234px; + top: 33vh; + left: 6vw; + background-color: url('../../img/memo-modal.png'); + background-size: cover; /* 이미지를 화면에 꽉 채우도록 조정 */ + background-position: center; /* 이미지를 가운데 정렬 */ + background-repeat: no-repeat; /* 이미지 반복 방지 */ + .memoModal { + width: 88vw; + height: auto; + position: absolute; + z-index: 3; + top: 0; + left: 0; + } + .modalBox { + width: 88vw; + height: 28vh; + position: absolute; + z-index: 4; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + textarea { + width: 66%; + height: 43%; + padding: 10px; + margin-top: 0px; + color: #000; + text-align: center; + font-family: "SUIT Variable", -apple-system, BlinkMacSystemFont, Pretendard, "Segoe UI", Roboto, Oxygen, + Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; + font-size: 16px; + font-weight: 400; + line-height: normal; + outline: none; + border: none; + border-radius: 14px; + white-space: pre-wrap; + background-color: #f8f8f8; + resize: none; + &:focus { + border: 1px solid #009EFF; + } + } + .saveBtn { + width: 45px; + height: 24px; + font-size: 18px; + margin-bottom: 24px; + display: flex; + align-items: center; + justify-content: center; + color: white; + background-color: #009EFF; + border: none; + border-radius: 19px; + cursor: pointer; + text-align: center; + font-family: Pretendard; + font-size: 12px; + font-weight: 400; + } + } +} + +.modalBG1 { + animation: fadeIn2 ease-in-out 1s; + z-index: 1; + width: 100vw; + height: 1100vh; + position: absolute; + background-color: rgba(0, 40, 64, 0.60); + // opacity: 40%; + top: 0px; + left: 0px; +} +.modalBG0 { + display: none; +} + +.emojiBox { + position: absolute; + z-index: 10; + bottom: 5px; + width: 80vw; + height: 72px; + padding: 0 22px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + border-radius: 70px; + background-color: white; + opacity: 0; + transform: translateY(20px); + transition: opacity 0.5s ease, transform 0.5s ease; +} \ No newline at end of file