From ed1ea033cad5a7d74cd7b731cc3aa983e419c817 Mon Sep 17 00:00:00 2001 From: haesol822 Date: Tue, 19 Dec 2023 11:22:48 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=8F=AC=ED=86=A0=EB=B6=81=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=99=84=EC=A0=84=20=EA=B5=AC=ED=98=84,=20?= =?UTF-8?q?=ED=8F=AC=ED=86=A0=EC=85=80=EB=A0=89=ED=8A=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95,=20=EA=B8=B0=ED=83=80=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 6 + package.json | 1 + src/App.jsx | 4 + src/apis/emailCheck.js | 23 +- src/pages/Example/Example.jsx | 75 +++++ src/pages/Example/Example.module.scss | 23 ++ src/pages/Frame/ApplyFrame.jsx | 4 +- src/pages/Login/Login.jsx | 1 + src/pages/Photobook/PhotoSelect.jsx | 43 ++- src/pages/Photobook/PhotoSelect.module.scss | 15 +- src/pages/Photobook/Photobook.jsx | 236 +++++++++---- src/pages/Photobook/Photobook.module.scss | 68 ++-- src/pages/Photobook/PhotobookUuid.jsx | 313 ++++++++++++------ src/pages/Photobook/PhotobookUuid.module.scss | 265 --------------- 14 files changed, 608 insertions(+), 469 deletions(-) create mode 100644 src/pages/Example/Example.jsx create mode 100644 src/pages/Example/Example.module.scss delete mode 100644 src/pages/Photobook/PhotobookUuid.module.scss diff --git a/package-lock.json b/package-lock.json index a77473d..be2dce7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@reduxjs/toolkit": "^1.9.7", "axios": "^1.6.1", "crypto-js": "^4.2.0", + "file-saver": "^2.0.5", "html2canvas": "^1.4.1", "jotai": "^2.5.1", "lodash": "^4.17.21", @@ -11476,6 +11477,11 @@ "webpack": "^4.0.0 || ^5.0.0" } }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", diff --git a/package.json b/package.json index 10fd1b6..b3e7945 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@reduxjs/toolkit": "^1.9.7", "axios": "^1.6.1", "crypto-js": "^4.2.0", + "file-saver": "^2.0.5", "html2canvas": "^1.4.1", "jotai": "^2.5.1", "lodash": "^4.17.21", diff --git a/src/App.jsx b/src/App.jsx index 5f659c4..49d3df7 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -15,6 +15,7 @@ import PhotoSelect from "./pages/Photobook/PhotoSelect"; // import { useAtom } from "jotai"; // import axios from "axios"; // import { accessTokenAtom, setAccessToken } from "./store/jotaiAtoms"; +import Example from "./pages/Example/Example"; function App() { function setScreenSize() { @@ -43,6 +44,7 @@ function App() { } /> } /> } /> + } /> @@ -58,6 +60,8 @@ function App() { } /> } /> } /> + } /> + } /> diff --git a/src/apis/emailCheck.js b/src/apis/emailCheck.js index e021102..436758a 100644 --- a/src/apis/emailCheck.js +++ b/src/apis/emailCheck.js @@ -1,14 +1,21 @@ import axios from "axios"; -export const emailCheck = (formData, setFormData) => { - - axios - .post(`http://ec2-3-35-208-177.ap-northeast-2.compute.amazonaws.com:8080/v2/api-docs/user/${formData.email}`) - .then(() => { +export const emailCheck = async (formData, setFormData) => { + try { + // 서버에 이메일 중복 체크 요청 + const response = await axios.post(`http://ec2-3-35-208-177.ap-northeast-2.compute.amazonaws.com:8080/user/${formData.email}`); + + // 응답이 200이면 이메일 중복 없음 + if (response.status === 200) { alert("사용할 수 있는 이메일입니다."); - }) - .catch(() => { + } else { + // 다른 상태 코드가 반환된 경우 alert("사용할 수 없는 이메일입니다."); setFormData({ ...formData, email: "" }); - }); + } + } catch (error) { + // 오류가 발생한 경우 (401 등) + alert("오류가 발생했습니다. 다시 시도해주세요."); + console.error(error); + } }; diff --git a/src/pages/Example/Example.jsx b/src/pages/Example/Example.jsx new file mode 100644 index 0000000..a282406 --- /dev/null +++ b/src/pages/Example/Example.jsx @@ -0,0 +1,75 @@ +// import { useRef } from 'react'; +// import { useDrag } from 'react-use-gesture'; +// import memopic1 from "../../img/memopic1.png"; + +// import styles from './Example.module.scss'; + +// export default function App() { +// const domTarget = useRef(null); + +// const bind = useDrag(({ offset: [dx, dy] }) => { +// // 특정 범위 내에서만 드래그 허용 +// const newX = Math.min(Math.max(dx, -100), 100); +// const newY = Math.min(Math.max(dy, -100), 100); + +// domTarget.current.style.transform = `translate3d(${newX}px, ${newY}px, 0)`; +// }); + +// return ( +//
+// memopic +//
+// ); +// } + +import { useState, useRef } from 'react'; +import imageUrl from "../../img/memopic1.png"; + +const DraggableImage = () => { + const [imagePosition, setImagePosition] = useState({ x: 0, y: 0 }); + + const handleImageDrag = (clientX, clientY) => { + // Update the image position based on the drag + setImagePosition({ x: clientX, y: clientY }); + }; + const [isDragging, setIsDragging] = useState(false); + const imageRef = useRef(null); + + const handleDragStart = (e) => { + e.dataTransfer.setData('text/plain', ''); // Necessary for some browsers + setIsDragging(true); + }; + + const handleDragEnd = () => { + setIsDragging(false); + }; + + const handleDrag = (e) => { + if (isDragging) { + handleImageDrag(e.clientX, e.clientY); + } + }; + + return ( + draggable + ); +}; + +export default DraggableImage; \ No newline at end of file diff --git a/src/pages/Example/Example.module.scss b/src/pages/Example/Example.module.scss new file mode 100644 index 0000000..2cc2382 --- /dev/null +++ b/src/pages/Example/Example.module.scss @@ -0,0 +1,23 @@ +.card { + position: relative; + width: 200px; + height: 200px; + transition: box-shadow 0.5s, opacity 0.5s; + will-change: transform; + border: 10px solid white; + cursor: grab; + overflow: hidden; + touch-action: none; +} +img { + + transform: translate(-50%, -50%); +} + +.container { + height: 100vh; + width: 100vw; + display: flex; + align-items: center; + justify-content: center; +} diff --git a/src/pages/Frame/ApplyFrame.jsx b/src/pages/Frame/ApplyFrame.jsx index 8127a23..294be46 100644 --- a/src/pages/Frame/ApplyFrame.jsx +++ b/src/pages/Frame/ApplyFrame.jsx @@ -75,9 +75,7 @@ const ApplyFrame = () => { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", }, - data: { - url: frameUrl, - }, + data: frameUrl, }; // Axios를 사용하여 DELETE 요청 보내기 diff --git a/src/pages/Login/Login.jsx b/src/pages/Login/Login.jsx index f82b0c2..9583703 100644 --- a/src/pages/Login/Login.jsx +++ b/src/pages/Login/Login.jsx @@ -72,6 +72,7 @@ function LoginForm() { router("/frame"); } catch (error) { console.error(error); + alert("확인할 수 없는 회원정보입니다. 다시 입력해주세요."); } }; diff --git a/src/pages/Photobook/PhotoSelect.jsx b/src/pages/Photobook/PhotoSelect.jsx index d98ef65..12d92eb 100644 --- a/src/pages/Photobook/PhotoSelect.jsx +++ b/src/pages/Photobook/PhotoSelect.jsx @@ -8,6 +8,7 @@ export default function PhotoSelect() { const axios = useAxios(); const [getPhotos, setGetPhotos] = useState([]); const [selectedPhotos, setSelectedPhotos] = useState([]); + const [isPhotobookExist, setIsPhotobookExist] = useState(false); const router = useNavigate(); const handlePhotoClick = (index) => { @@ -36,8 +37,8 @@ export default function PhotoSelect() { axios .get("/photo") .then((response) => { - setGetPhotos(response.data); - // console.log(getPhotos); + setGetPhotos(response.data.photoList); + setIsPhotobookExist(response.data.present); }) .catch((error) => console.error(error)); }, [selectedPhotos]); @@ -47,20 +48,42 @@ export default function PhotoSelect() { 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); + + if (selectedPhotos.length === 0) { + // 선택된 사진이 없을 경우 알림 창 띄우기 + alert("사진을 선택해주세요."); + return; + } + else { + try { + const response = await axios.post("/photoBook", data); + console.log("API 응답:", response.data); + router("/photobook"); + } catch (error) { + console.error("API 오류:", error); + } } }; + const checkPhotobook = () => { + router("/photobook"); + } + return (
- + {isPhotobookExist ? ( + + ) : ( + + )}
diff --git a/src/pages/Photobook/PhotoSelect.module.scss b/src/pages/Photobook/PhotoSelect.module.scss index 0409611..1be3ee6 100644 --- a/src/pages/Photobook/PhotoSelect.module.scss +++ b/src/pages/Photobook/PhotoSelect.module.scss @@ -13,21 +13,22 @@ align-items: center; justify-content: center; background-color: white; - margin-top: 44px; - button { + margin-top: 20px; + margin-bottom: 20px; + .inactive, .active { height: 50px; width: 340px; border-radius: 10px; - border: 1px solid #009eff; + border: 2px solid #009eff; color: #009eff; text-align: center; font-size: 14px; font-weight: 500; background-color: white; - &:hover { - background-color: #009eff; - color: white; - } + } + .active { + background-color: #009eff; + color: white; } } diff --git a/src/pages/Photobook/Photobook.jsx b/src/pages/Photobook/Photobook.jsx index be085c8..3204697 100644 --- a/src/pages/Photobook/Photobook.jsx +++ b/src/pages/Photobook/Photobook.jsx @@ -2,13 +2,13 @@ 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 home from "../../img/home.png"; import emoji1 from "../../img/emoji1.png"; import emoji2 from "../../img/emoji2.png"; import emoji3 from "../../img/emoji3.png"; @@ -27,12 +27,10 @@ import memopic5 from "../../img/memopic5.png"; import useAxios from "../../apis/axiosWithToken"; export default function Photobook() { - { - /* 포토북 UI 시작 */ - } + {/* 포토북 UI 시작 */} const index = useRef(0); const [ref, { width }] = useMeasure(); - const [photos, setPhotos] = useState([]); + const [photos, setPhotos] = useState([]); const [activeDot, setActiveDot] = useState(0); const [props, api] = useSprings( photos.length, @@ -46,7 +44,7 @@ export default function Photobook() { const bind = useDrag( ({ active, movement: [mx], direction: [xDir], distance, cancel }) => { if (active && distance > width / 2) { - setActiveDot(null); + setActiveDot(null); index.current = clamp( index.current + (xDir > 0 ? -1 : 1), 0, @@ -61,18 +59,17 @@ export default function Photobook() { 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 activeDotIndex = clamp( + Math.round(index.current), + 0, + photos.length - 1 + ); + setActiveDot(activeDotIndex); + // console.log("스크롤 점 인덱스: ", activeDotIndex); + } ); // 동그라미를 클릭했을 때 해당 사진으로 이동하는 함수 const handleDotClick = (dotIndex) => { @@ -83,17 +80,20 @@ export default function Photobook() { const x = (i - index.current) * width; return { x, scale: 1, display: "block" }; }); - setActiveDot(null); // 클릭한 동그라미를 활성화 상태로 설정 + setActiveDot(null); // 클릭한 동그라미를 활성화 상태로 설정 }; - { - /* 포토북 UI 마침 */ - } + {/* 포토북 UI 마침 */} const axios = useAxios(); const [modal, setModal] = useState(0); const [selectedEmoji, setSelectedEmoji] = useState(null); + const [selectedPhotoIndex, setSelectedPhotoIndex] = useState(null); const [uuid, setUuid] = useState(null); const [isEmojiBoxVisible, setIsEmojiBoxVisible] = useState(false); + // const { uuid } = useParams(); + const [memos, setMemos] = useState([]); // 메모 리스트 Dto배열 저장 + const [modalMemoContent, setModalMemoContent] = useState(""); // 선택한 메모 content 저장 + const [modalMemoVisible, setModalMemoVisible] = useState(false); // 모달 창 오픈 시 메모 컨텐츠 보여줄지 말지 상태 저장 const emojiArray = [emoji1, emoji2, emoji3, emoji4, emoji5]; const emojiArray2 = [emoji01, emoji02, emoji03, emoji04, emoji05]; @@ -106,25 +106,24 @@ export default function Photobook() { console.log(response.data); setPhotos(response.data.photoList); setUuid(response.data.uuid); + setMemos(response.data.memoList); }) .catch((error) => console.error(error)); - }); + }, []); - const selectEmoji = (index) => { - setSelectedEmoji(index); - // // 이미 선택된 emoji가 있다면 선택 취소 - // if (selectedEmoji !== null && selectedEmoji === index) { - // // 이미 선택된 경우에는 아무 동작도 하지 않음 - // return; - // } else { - // // 선택된 emoji가 없거나 다른 emoji를 선택한 경우 크기 조절 - // setSelectedEmoji(index); - // } + const handleMemosClick = (memoIndex) => { + const modalContent = memos[memoIndex].content; + setModalMemoContent(modalContent); + setModal(1); + setModalMemoVisible(true); }; - // const toggleEmojiBox = () => { - // setIsEmojiBoxVisible(!isEmojiBoxVisible); - // }; + const selectEmoji = (i) => { + // 선택한 emoji 정보를 저장 + setSelectedEmoji(i); + // 이미지 선택 시 포토인덱스 저장 + setSelectedPhotoIndex(index.current); + }; // 이모지 박스 애니메이션 스타일 설정 const emojiBoxSpring = useSpring({ @@ -133,19 +132,71 @@ export default function Photobook() { config: { tension: 200, friction: 20, mass: 1, duration: 100 }, }); + // 메모픽 위치 상태 추가 + const [memopicPosition, setMemopicPosition] = useState({ + top: Math.floor(Math.random() * 365) - 10, + left: Math.floor(Math.random() * 250) - 10, + }); + + const handleMemopicClick = () => { + setModal(1); + setMemopicPosition({ + top: memopicPosition.top, + left: memopicPosition.left, + }); + setIsEmojiBoxVisible(false); + }; + + // 이모지 박스에서의 클릭 이벤트 핸들러 + const handleEmojiClick = (index) => { + // 선택한 emoji 정보를 저장 + selectEmoji(index); + + // 새로운 랜덤 위치를 생성하고 설정 + setMemopicPosition({ + top: Math.floor(Math.random() * 365) - 10, + left: Math.floor(Math.random() * 250) - 10, + }); + }; const shareLink = () => { // 현재 경로에 uuid를 붙여서 공유할 링크 생성 const shareableLink = `${window.location.href}/${uuid}`; + // 클립보드에 복사 - navigator.clipboard - .writeText(shareableLink) - .then(() => { - console.log("링크가 클립보드에 복사되었습니다."); - }) - .catch((err) => { - console.error("링크 복사 중 오류 발생:", err); - }); + const copyToClipboard = (text) => { + if (navigator.clipboard) { + navigator.clipboard.writeText(text).then( + () => { + alert("링크가 클립보드에 복사되었습니다."); + }, + (err) => { + console.error("링크 복사 중 오류 발생:", err); + alert("링크 복사 중 오류 발생"); + } + ); + } else { + // Navigator clipboard API가 지원되지 않는 경우 대체 방법 사용 + const textArea = document.createElement("textarea"); + textArea.value = text; + textArea.style.position = "fixed"; // 임시로 고정 위치에 텍스트 영역 생성 + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + try { + document.execCommand("copy"); + alert("링크가 클립보드에 복사되었습니다."); + } catch (err) { + console.error("링크 복사 중 오류 발생:", err); + alert("링크 복사 중 오류 발생"); + } finally { + document.body.removeChild(textArea); // 생성한 텍스트 영역 제거 + } + } + }; + + copyToClipboard(shareableLink); console.log("링크:", `${window.location.href}/${uuid}`); // // 현재 주소에 uuid 파라미터 추가 // const currentUrl = new URL(window.location.href); @@ -159,7 +210,36 @@ export default function Photobook() { // }); }; - const saveMemo = () => {}; + // 메모 저장 + const [memoText, setMemoText] = useState(""); // 메모 작성한 내용 저장 + + const saveMemo = async (uuid) => { + const apiURL = `http://ec2-3-35-208-177.ap-northeast-2.compute.amazonaws.com:8080/memo/${uuid}` + const saveMemoDto = { + "x": memopicPosition.left, + "y": memopicPosition.top, + "pageNum": selectedPhotoIndex, + "content": memoText, + "emojiNum": selectedEmoji + }; + console.log(saveMemoDto); + try { + // 서버에 메모 저장 요청 + const response = await axios.post(apiURL, saveMemoDto); + + // 응답이 200이면 성공 + if (response.status === 200) { + alert("메모가 성공적으로 저장되었습니다."); + } else { + // 다른 상태 코드가 반환된 경우 + alert("메모 저장에 실패했습니다."); + } + } catch (error) { + // 오류가 발생한 경우 (401 등) + alert("오류가 발생했습니다. 다시 시도해주세요."); + console.error(error); + } + }; return (
@@ -183,12 +263,38 @@ export default function Photobook() { className={styles.photo} style={{ scale, backgroundImage: `url(${photos[i]})` }} /> - {selectedEmoji !== null && ( + {memos.map((memo, memoIndex) => { + if (memo.pageNum === i) { + // Check if there is a memo for the current photo + return ( + handleMemosClick(memoIndex)} + /> + ); + } else { + return null; // No memo for the current photo + } + })} + {selectedEmoji !== null && selectedPhotoIndex === i && ( setModal(1)} + style={{ + scale, + position: "absolute", + top: `${memopicPosition.top}px`, // 클릭 시의 위치를 고정 + left: `${memopicPosition.left}px`, // 클릭 시의 위치를 고정 + }} + onClick={handleMemopicClick} /> )} @@ -229,15 +335,28 @@ export default function Photobook() {
{memoModal}
- - + {modalMemoVisible === true && ( +
+ {modalMemoContent} +
+ )} + {modalMemoVisible !== true && ( + <> + - + {modalMemoVisible === true && ( +
+ {modalMemoContent} +
+ )} + {modalMemoVisible !== true && ( + <> +