diff --git a/src/@types/api.ts b/src/@types/api.ts index be5abc6..551af80 100644 --- a/src/@types/api.ts +++ b/src/@types/api.ts @@ -7,7 +7,7 @@ export type CommonResponse = { }; export type CommonError = { - errorCode: string; + statusCode: string; message: string; - success: boolean; + content: string; }; diff --git a/src/@types/my.ts b/src/@types/my.ts new file mode 100644 index 0000000..98aa4df --- /dev/null +++ b/src/@types/my.ts @@ -0,0 +1,7 @@ +export type MyLikedBooth = { + photoBoothId: number; + name: string; + rating: number; + feature: string; + featureCount: number; +}; diff --git a/src/App.tsx b/src/App.tsx index d82f07c..8345273 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { BrowserRouter, Navigate, Outlet, Route, Routes, useNavigate } from "react-router-dom"; +import { BrowserRouter, Navigate, Outlet, Route, Routes } from "react-router-dom"; import Home from "./pages/Home"; import SplashScreen from "./pages/SplashScreen"; import Album from "./pages/Album"; @@ -6,8 +6,7 @@ import My from "./pages/My"; import PhotoUpload from "./pages/PhotoUpload"; import QRScan from "./pages/QRScan"; -import PhotoReview from "./pages/PhotoReview"; -import PhotoCheck from "./pages/PhotoCheck"; +import PhotoReview from "./pages/PhotoUpload/ReviewPhoto"; import LoginPage from "./pages/LoginPage"; import Token from "./pages/Token"; @@ -25,7 +24,11 @@ import MyReviewPage from "./pages/My/MyReviewPage"; import LikeBoothsPage from "./pages/My/LikeBoothsPage"; import VisitedBoothsPage from "./pages/My/VisitedBoothsPage"; import { useAuthStore } from "./store/useAuthStore"; -import Modal from "./components/Common/Modal"; +import UploadComplete from "./pages/PhotoUpload/UploadComplete"; +import CheckPhoto from "./pages/PhotoUpload/CheckPhoto"; +import WriteDetail from "./pages/PhotoUpload/WriteDetail"; +import Share from "./pages/Share"; +import AlertDialog from "./components/Alert/AlertDialog"; function App() { const { isLoggedIn } = useAuthStore(); @@ -45,45 +48,50 @@ function App() { }; return ( - - - } /> - } /> + + + + } /> + } /> - } /> - } /> + } /> + } /> - }> - } /> - } /> - } /> - - - }> - }> - } /> - } /> + }> + } /> + } /> + } /> - } /> - } /> - } /> + }> + }> + } /> + } /> + - }> - } /> - } /> - - } /> + } /> + } /> + } /> - } /> + }> + } /> + } /> + + } /> - } /> - } /> - } /> - } /> - - - + } /> + + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + + + ); } diff --git a/src/api/index.ts b/src/api/index.ts index c0334bd..0450d08 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -11,20 +11,27 @@ export const axiosInstance = axios.create({ withCredentials: true, }); -const errorCodes = ["SEC4001", "SEC4011", "SEC4012"]; +const errorCodes = ["SEC4001", "SEC4011", "SEC4012", "SEC4010"]; axiosInstance.interceptors.response.use( (response) => response, async (error: AxiosError) => { - if (error.config && error.response && error.response.data.errorCode in errorCodes) { + console.log(error.response); + if (error.config && error.response?.status && errorCodes.includes(error.response!.data.statusCode)) { + console.log("진입"); // 토큰 재발급 수행 - const accessToken = useAuthStore((state) => state.accessToken); + const { accessToken } = useAuthStore(); + console.log(accessToken); + + if (!accessToken) { + throw new Error("AccessToken is undefined or null"); + } try { const res = await reissueToken(accessToken!); + if (res) { useAuthStore.setState({ - isLoggedIn: true, accessToken: res.accessToken, }); @@ -32,6 +39,8 @@ axiosInstance.interceptors.response.use( error.config.headers.Authorization = `Bearer ${res.accessToken}`; //로직 재수행 return await axiosInstance(error.config); + } else { + throw new Error("AccessToken not received"); } } catch (error) { console.error("Error in reissuing token:", error); @@ -57,3 +66,11 @@ export const Post = async ( const response = await axiosInstance.post(url, body, config); return response; }; + +export const Delete = async ( + url: string, + config?: AxiosRequestConfig +): Promise>> => { + const response = await axiosInstance.delete(url, config); + return response; +}; diff --git a/src/api/my.ts b/src/api/my.ts index 1e0ce2a..2d80419 100644 --- a/src/api/my.ts +++ b/src/api/my.ts @@ -1,4 +1,5 @@ -import { Get } from "."; +import { Get, Post } from "."; +import { MyLikedBooth } from "../@types/my"; import { MyReview } from "../@types/review"; export const getMyReviews = async (accessToken: string) => { @@ -27,3 +28,41 @@ export const getVisitedBooths = async (accessToken: string) => { return res.data.payload; } catch (error) {} }; + +export const getLikedBooths = async (accessToken: string) => { + try { + const res = await Get("/api/v1/photobooth/like", { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + + return res.data.payload; + } catch (error) {} +}; + +export const postboothLike = async (accessToken: string, boothId: string) => { + try { + const res = await Post( + `/api/v1/photobooth/like/${boothId}`, + {}, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); + return res.data.payload; + } catch (error) {} +}; + +export const getPhotoLikes = async (accessToken: string) => { + try { + const res = await Get<{ albumId: number; photoUrl: string; like: boolean }[]>("/api/v1/album/favorite", { + headers: { + Authorization: `Bearer: ${accessToken}`, + }, + }); + return res.data.payload; + } catch (error) {} +}; diff --git a/src/api/photoupload.ts b/src/api/photoupload.ts index e3d2a23..b9a4e60 100644 --- a/src/api/photoupload.ts +++ b/src/api/photoupload.ts @@ -11,7 +11,16 @@ export const uploadPhoto = async ( filePath: string ) => { try { - const res = await Post( + const res = await Post<{ + photoboothId: number; + year: number; + month: number; + date: number; + hashtag: string[]; + albumId: string; + memo: string; + filePath: string; + }>( "/api/v1/album", { photoboothId: photoboothId, @@ -28,7 +37,7 @@ export const uploadPhoto = async ( }, } ); - return res.data.result; + return res.data.payload; } catch (error) { console.log(error); } diff --git a/src/api/share.ts b/src/api/share.ts new file mode 100644 index 0000000..a0ada16 --- /dev/null +++ b/src/api/share.ts @@ -0,0 +1,34 @@ +import { AxiosError } from "axios"; +import { Post } from "."; + +export const createLink = async (albumId: string, accessToken: string) => { + try { + const res = await Post( + `/api/v1/album/share?albumId=${albumId}`, + {}, + { + headers: { Authorization: `Bearer ${accessToken}` }, + } + ); + + return res.data.payload; + } catch (error) {} +}; + +export const saveShare = async (accessToken: string, shareId: string) => { + try { + const res = await Post( + `/api/v1/album/${shareId}/saveShare`, + {}, + { + headers: { Authorization: `Bearer ${accessToken}` }, + } + ); + + return res.data; + } catch (error) { + if (error instanceof AxiosError) { + return error.status; + } + } +}; diff --git a/src/api/user.ts b/src/api/user.ts index 86d14d5..ca591db 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -26,3 +26,14 @@ export const reissueToken = async (accessToken: string) => { return res.data.payload; } catch (error) {} }; + +export const getLogout = async (accessToken: string) => { + try { + const res = await Get("/api/v1/users/logout", { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + return res.data; + } catch (error) {} +}; diff --git a/src/assets/icons/like-filled-icon.tsx b/src/assets/icons/like-filled-icon.tsx index 128dc67..97918b8 100644 --- a/src/assets/icons/like-filled-icon.tsx +++ b/src/assets/icons/like-filled-icon.tsx @@ -10,10 +10,8 @@ function LikeFilledIcon({ width, height, color }: IconProps) { xmlns="http://www.w3.org/2000/svg" > ); diff --git a/src/assets/images/default-profile.svg b/src/assets/images/default-profile.svg new file mode 100644 index 0000000..e151843 --- /dev/null +++ b/src/assets/images/default-profile.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/components/Album/AlbumMap.tsx b/src/components/Album/AlbumMap.tsx new file mode 100644 index 0000000..f17af56 --- /dev/null +++ b/src/components/Album/AlbumMap.tsx @@ -0,0 +1,67 @@ +import MapContainer from "../Common/MapContainer"; +import { CustomOverlayMap } from "react-kakao-maps-sdk"; +import CustomMarker from "../../assets/icons/booth-marker"; +import { useEffect, useState } from "react"; +import ActiveCustomMarker from "../../assets/icons/active-booth-marker"; +import { getLogoUrl } from "../../hooks/getImageUrl"; +import { getCurrentLocation } from "../../hooks/getLocation"; +import { useQuery } from "@tanstack/react-query"; +import useBoothFilterStore from "../../store/useBoothFilterStore"; +import { getBoothLatLng } from "../../api/booth"; + +export default function AlbumMap() { + const { lat, lng, selectedBrands } = useBoothFilterStore(); + const [activeId, setActiveId] = useState(-1); + + //전체 포토부스 위치 정보 조회 api 호출 + const { isLoading, data } = useQuery({ + queryKey: ["getBoothLatLng", lat, lng, selectedBrands], + queryFn: () => getBoothLatLng(lat, lng, selectedBrands!), + }); + + //처음 페이지 접속 시 사용자의 현 위치를 받아와서 center 세팅 + useEffect(() => { + //현 위치 받아오기 + const fetchLocation = async () => { + const res = await getCurrentLocation(); + if (res) { + useBoothFilterStore.setState({ + lat: res.lat, + lng: res.lng, + }); + } + }; + + fetchLocation(); + }, []); + + const handleClick = (id: number) => { + if (activeId === id) { + setActiveId(-1); + } else { + setActiveId(id); + } + }; + + return ( + + {/* CustomOverlayMap으로 커스텀 마커를 직접 렌더링 */} + {!isLoading && + data && + data.map((item, index) => ( + +
handleClick(item.id)} className="flex flex-col items-center"> + {activeId === item.id ? ( + + ) : ( + + )} +
+
+ ))} + + {/* 클릭 시 해당 포토부스에 해당 되는 모달창 렌더링 */} + {/*{activeId >= 0 && }*/} +
+ ); +} diff --git a/src/components/Album/BoothFilterModal.tsx b/src/components/Album/BoothFilterModal.tsx index 12837c6..7a283b3 100644 --- a/src/components/Album/BoothFilterModal.tsx +++ b/src/components/Album/BoothFilterModal.tsx @@ -2,26 +2,9 @@ import React from "react"; import tw from "twin.macro"; import styled from "styled-components"; import CloseIcon from "../../assets/icons/close-icon"; - -type OptionProps = { - onClick: () => void; - isActive: boolean; - label: string; -}; - -const OptionComponent = ({ onClick, isActive, label }: OptionProps) => { - return ( - - ); -}; +import OptionButton from "../Common/OptionButton.tsx"; +import NextButton from "../Common/NextButton.tsx"; +import {BoothCategories} from "../../data/booth-categories.ts"; type BoothFilterProps = { photoBooth: string; @@ -30,10 +13,7 @@ type BoothFilterProps = { }; export default function BoothFilterModal({ photoBooth, setPhotoBooth, setIsBoothFilterModalOpen }: BoothFilterProps) { - const photoBoothOptions = [ - "하루필름", "포토매틱", "인생네컷", "포토시그니처", "셀픽스", - "인생사진", "포토이즘", "포토이즘 박스", "포토그레이", "비룸" - ]; + const photoBoothOptions = BoothCategories.map((item) => (item.label)); return ( @@ -50,14 +30,18 @@ export default function BoothFilterModal({ photoBooth, setPhotoBooth, setIsBooth {photoBoothOptions.map((booth) => ( - setPhotoBooth(booth)} isActive={photoBooth === booth} label={booth} + size={"small"} /> ))} +
+ setIsBoothFilterModalOpen(false)}/> +
); @@ -79,39 +63,6 @@ const Container = styled.div` const OptionContainer = styled.div` ${tw`grid grid-cols-2 gap-4`} - /* 한 줄에 두 개씩 배치 */ -`; - -const Option = styled.button` - ${tw`w-[150px] h-[90px] rounded-lg border mb-4 cursor-pointer transition-colors duration-200`} - padding: ${({ isActive }) => (isActive ? "23px 12px" : "8px 12px")}; - background-color: ${({ isActive }) => (isActive ? "#e1e0ff" : "transparent")}; - border-color: ${({ isActive }) => (isActive ? "#5453ee" : "#c7c9ce")}; - display: flex; - flex-direction: ${({ isActive }) => (isActive ? "column" : "row")}; - justify-content: center; - align-items: center; - gap: ${({ isActive }) => (isActive ? "10px" : "12px")}; -`; - -const CircleContainer = styled.div` - ${tw`w-[22px] h-[22px] relative`} -`; - -const CircleBorder = styled.div<{ isActive: boolean }>` - ${tw`absolute w-full h-full rounded-full border-2`} - border-color: ${({ isActive }) => (isActive ? "#5453ee" : "#c7c9ce")}; -`; - -const CircleInner = styled.div` - ${tw`absolute w-2.5 h-2.5 bg-[#5453ee] rounded-full`} - left: 6px; - top: 6px; -`; - -const Label = styled.div<{ isActive: boolean }>` - ${tw`text-base font-semibold font-['Pretendard']`} - color: ${({ isActive }) => (isActive ? "#5453ee" : "#c7c9ce")}; `; const CloseButton = styled.button` diff --git a/src/components/Album/DateModal.tsx b/src/components/Album/DateModal.tsx index 9ea1c3d..cdfba36 100644 --- a/src/components/Album/DateModal.tsx +++ b/src/components/Album/DateModal.tsx @@ -5,15 +5,19 @@ import { useState } from "react"; type ModalProps = { closeModal: () => void; - setDate: (tags: string) => void; + year:number; + month:number; + setYear: React.Dispatch>; + setMonth: React.Dispatch>; } -export default function DateModal({ closeModal, setDate }: ModalProps) { - const [year, setYear] = useState(""); - const [month, setMonth] = useState(""); +export default function DateModal({ closeModal, year, month, setYear, setMonth }: ModalProps) { + const [tempYear, setTempYear] = useState(year); + const [tempMonth, setTempMonth] = useState(month); const confirm = () => { - setDate(year + "년 " + month + "월"); + setYear(tempYear); + setMonth(tempMonth); closeModal(); }; @@ -27,9 +31,9 @@ export default function DateModal({ closeModal, setDate }: ModalProps) {
- setYear(e.target.value)} /> + setTempYear(Number(e.target.value))} />

- setMonth(e.target.value)} /> + setTempMonth(Number(e.target.value))} />

diff --git a/src/components/Album/HashtagSearchModal.tsx b/src/components/Album/HashtagSearchModal.tsx new file mode 100644 index 0000000..5628980 --- /dev/null +++ b/src/components/Album/HashtagSearchModal.tsx @@ -0,0 +1,159 @@ +import React, {useState} from "react"; +import tw from "twin.macro"; +import styled from "styled-components"; +import CloseIcon from "../../assets/icons/close-icon"; +import {Get} from "../../api"; +import Search from "../../assets/images/search.svg?react"; +import {useAuthStore} from "../../store/useAuthStore.ts"; +import ImageCard from "./ImageCard.tsx"; +import NoImage from "../../assets/images/no-images.svg?react"; + +type PhotoData = { + photoUrl: string; + hashtags: string[]; + year: number; + month: number; + date: number; + memo: string; + isLiked: boolean; +}; + +type BoothFilterProps = { + setIsModalOpen: React.Dispatch>; +}; + +export default function HashtagSearchModal({ setIsModalOpen }: BoothFilterProps) { + const [hashTag, setHashTag] = useState(""); + const [imageList, setImageList] = useState([]); + const { accessToken } = useAuthStore(); + + const handleSearchPhotos = async (hashTag: string, accessToken: string) => { + if (hashTag.length === 0) { + return; + } + + try { + const res = await Get(`/api/v1/album/hashtag/${hashTag}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + if (res.status === 200) { + setImageList(res.data.payload); + } + } catch (error) { + console.log(error); + } finally { + setHashTag(""); + } + }; + + return ( + + +
+
+ 해시태그별 검색 + setIsModalOpen(false)}> + + +
+
+
+ + handleSearchPhotos(hashTag, accessToken)}> + + + setHashTag(e.target.value)} + /> + + + {imageList.length === 0 ? ( +
+ +

해시태그로 검색!

+
+ ) : ( + <> +
+
{imageList.length}장의 추억
+
+ + + {imageList.map((image: PhotoData, index) => ( +
+
+ +
+
+ {image.hashtags + .filter((tag) => tag) // 빈 문자열을 제외 + .map((tag, index) => ( + + #{tag} + + ))} +
+ {image.year}년 {image.month}월 {image.date}일 +
+
+ {image.memo} +
+
+
+ ))} +
+
+ + )} +
+
+ ); +} +const Container = styled.div` + ${tw`bg-background flex flex-col items-center min-h-screen w-full max-w-[480px] m-auto`} + overflow-x: hidden; +`; + +const ImageContainer = styled.div` + ${tw`flex flex-col w-full h-[80vh] mt-4`} + overflow-y: auto; + position: relative; + z-index: 1; /* 다른 요소들보다 낮게 설정 */ +`; + +const ImageDiv = styled.div` + ${tw`grid gap-4 w-full pr-[16px] pl-[16px]`} + max-width: 480px; + grid-template-columns: repeat(2, 1fr); /* 두 개의 열 */ + grid-auto-rows: auto; + overflow-y: auto; +`; + +const Overlay = styled.div` + ${tw` + w-full h-full bg-[black] bg-opacity-40 + fixed top-[50%] left-[50%] transform translate-x-[-50%] translate-y-[-50%] + flex justify-center items-center + z-[30] + `} +`; + +const CloseButton = styled.button` + ${tw`absolute right-[10px]`} +`; + +const InputContainer = styled.div` + ${tw`w-10/12 p-2.5 bg-[#e9eaee] rounded-lg flex justify-end items-center `} + &:focus { + outline: none; + } +`; + +const SearchIcon = styled.button` + ${tw`w-6 p-px flex justify-center items-center`} +`; \ No newline at end of file diff --git a/src/components/Album/ImageCard.tsx b/src/components/Album/ImageCard.tsx index f13a81a..5e93a55 100644 --- a/src/components/Album/ImageCard.tsx +++ b/src/components/Album/ImageCard.tsx @@ -1,24 +1,54 @@ import tw from "twin.macro"; import styled from "styled-components"; -import DummyImg from "../../assets/images/dummy-photo.jpeg"; import LikeFilledIcon from "../../assets/icons/like-filled-icon"; import { useState } from "react"; import LikeNotFilledIcon from "../../assets/icons/like-not-filled-icon"; import CheckIcon from "../../assets/images/photo-checked.svg?react"; +import {Post} from "../../api"; +import {useAuthStore} from "../../store/useAuthStore.ts"; type ImageCardProps = { - isEditing: boolean; - isSelected: boolean; - onClick: () => void; + id : number; + isEditing?: boolean; + isSelected?: boolean; + onClick?: () => void; + photoUrl:string; + isLiked: boolean; }; -function ImageCard({ isEditing, isSelected, onClick }: ImageCardProps) { - const [like, setLike] = useState(false); +function ImageCard({ id, isEditing, isSelected, onClick, photoUrl, isLiked }: ImageCardProps) { + const [like, setLike] = useState(isLiked); + const { accessToken } = useAuthStore(); + + const likePhotos = async (albumId: number, accessToken: string) => { + try { + const res = await Post( + `/api/v1/album/like/${albumId}`, + null, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); + if (res.status === 200) { + console.log(res.data.payload); + } + } catch (error) { + console.log(error); + } + }; + + const handleLikeButtonClick = () => { + setLike(!like); + console.log(id); + likePhotos(id, accessToken); + } return ( - {/* 편집 모드에서 클릭 활성화 */} + {/* 편집 모드에서 클릭 활성화 */} {!isEditing && ( - setLike(!like)}> + {like ? : } )} diff --git a/src/components/Alert/Alert.tsx b/src/components/Alert/Alert.tsx new file mode 100644 index 0000000..01915a2 --- /dev/null +++ b/src/components/Alert/Alert.tsx @@ -0,0 +1,48 @@ +import { useEffect } from "react"; +import tw from "twin.macro"; +import styled from "styled-components"; +type Props = { + message: string; + onClose: () => void; +}; +function Alert({ message, onClose }: Props) { + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === "Escape") { + onClose(); + } + }; + + document.addEventListener("keydown", handleEscape); + + return () => document.removeEventListener("keydown", handleEscape); + }, [onClose]); + return ( + + + {message} + + + + ); +} + +export default Alert; +const Overlay = styled.div` + ${tw` + w-full h-full bg-[black] bg-opacity-20 + fixed top-[50%] left-[50%] transform translate-x-[-50%] translate-y-[-50%] + z-[30] flex-col items-center + `} +`; + +const Container = styled.div` + ${tw`w-[320px] h-[170px] flex flex-col bg-gray100 font-display p-[20px] rounded-[8px] mt-3 mx-auto justify-between`} + + .title { + ${tw`font-medium text-[18px] text-gray700 mt-1`} + } + button { + ${tw`w-full h-[50px] rounded-[8px] font-display font-semibold text-[16px] bg-main text-[#FFFFFF]`} + } +`; diff --git a/src/components/Alert/AlertDialog.tsx b/src/components/Alert/AlertDialog.tsx new file mode 100644 index 0000000..c48a4ab --- /dev/null +++ b/src/components/Alert/AlertDialog.tsx @@ -0,0 +1,15 @@ +import { useAlertStore } from "../../store/useAlertStore"; +import Alert from "./Alert"; + +const AlertDialog = ({ children }: { children: React.ReactNode }) => { + const { isOpen, message, closeAlert } = useAlertStore(); + + return ( + <> + {children} + {isOpen && } + + ); +}; + +export default AlertDialog; diff --git a/src/components/Booth/BoothInfo.tsx b/src/components/Booth/BoothInfoSection.tsx similarity index 61% rename from src/components/Booth/BoothInfo.tsx rename to src/components/Booth/BoothInfoSection.tsx index 37389fc..21c969e 100644 --- a/src/components/Booth/BoothInfo.tsx +++ b/src/components/Booth/BoothInfoSection.tsx @@ -4,9 +4,26 @@ import LikeIcon from "../../assets/icons/like-filled-icon"; import { getDistance } from "../../hooks/getLocation"; import useBoothFilterStore from "../../store/useBoothFilterStore"; import { SpecificBoothInfo } from "../../@types/booth"; +import { FaHeart } from "react-icons/fa"; +import { useState } from "react"; +import { postboothLike } from "../../api/my"; +import { useParams } from "react-router-dom"; +import { useAuthStore } from "../../store/useAuthStore"; +import LikeFilledIcon from "../../assets/icons/like-filled-icon"; +import LikeNotFilledIcon from "../../assets/icons/like-not-filled-icon"; function BoothInfoSection({ name, road, x, y }: SpecificBoothInfo) { const { lat, lng } = useBoothFilterStore(); + const { boothId } = useParams() as { boothId: string }; + const { accessToken } = useAuthStore(); + + const [like, setLike] = useState(false); + const handleLike = async () => { + const res = await postboothLike(accessToken!, boothId); + if (res === "success") { + setLike(!like); + } + }; return ( @@ -22,7 +39,9 @@ function BoothInfoSection({ name, road, x, y }: SpecificBoothInfo) { > 길안내 시작 - + ); @@ -31,7 +50,7 @@ function BoothInfoSection({ name, road, x, y }: SpecificBoothInfo) { export default BoothInfoSection; const Container = styled.div` - ${tw`w-full px-[16px] flex flex-row font-display justify-between items-start`} + ${tw`w-full px-[16px] flex flex-row font-display justify-between items-start mb-[30px]`} .main-text { ${tw`font-semibold text-[18px] text-gray700`} diff --git a/src/components/Common/Header.tsx b/src/components/Common/Header.tsx index 5e99723..8aea5e9 100644 --- a/src/components/Common/Header.tsx +++ b/src/components/Common/Header.tsx @@ -29,7 +29,7 @@ const Container = styled.header` } .booth-title { - ${tw`font-display font-semibold text-[22px] text-gray700`} + ${tw`font-display font-semibold text-[20px] text-gray700`} } .sub-text { ${tw`font-medium text-[12px] text-gray400`} diff --git a/src/components/Common/NextButton.tsx b/src/components/Common/NextButton.tsx index 29167f2..892121b 100644 --- a/src/components/Common/NextButton.tsx +++ b/src/components/Common/NextButton.tsx @@ -16,7 +16,7 @@ function NextButton({ text, onClick, disabled }: ButtonProps) { export default NextButton; const Container = styled.button` - ${tw`w-[280px] h-[60px] bg-main rounded-lg mt-12 flex justify-center items-center font-semibold text-[20px] text-[#FFFFFF] mx-auto`} + ${tw`w-[280px] h-[60px] bg-main rounded-lg flex justify-center items-center font-semibold text-[20px] text-[#FFFFFF] mx-auto `} &:disabled { ${tw`bg-gray400`} } diff --git a/src/components/Common/OptionButton.tsx b/src/components/Common/OptionButton.tsx new file mode 100644 index 0000000..e0e0dde --- /dev/null +++ b/src/components/Common/OptionButton.tsx @@ -0,0 +1,99 @@ +import styled from "styled-components"; +import tw from "twin.macro"; + +type OptionProps = { + onClick: () => void; + isActive: boolean; + label: string; + subLabel?: string; + size?: "small" | "medium" | "large"; // 사이즈 조정 props 추가 +}; + +export default function OptionButton({onClick, isActive, label, subLabel, size = "medium"}: OptionProps) { + return ( + + ); +}; + +const Option = styled.button` + ${({ size }) => { + switch (size) { + case "small": + return tw`w-[200px] h-[60px]`; + case "large": + return tw`w-[300px] h-[110px]`; + default: + return tw`w-[267px] h-[90px]`; // 기본 사이즈 + } +}} + ${tw`rounded-lg border mb-4 cursor-pointer transition-colors duration-200`} + padding: ${({ isActive }) => (isActive ? "23px 12px" : "8px 12px")}; + background-color: ${({ isActive }) => (isActive ? "#e1e0ff" : "transparent")}; + border-color: ${({ isActive }) => (isActive ? "#5453ee" : "#c7c9ce")}; + display: flex; + flex-direction: ${({ isActive }) => (isActive ? "column" : "row")}; + justify-content: center; + align-items: center; + gap: ${({ isActive }) => (isActive ? "10px" : "12px")}; +`; + +const CircleContainer = styled.div<{ size: string }>` + ${({ size }) => { + switch (size) { + case "small": + return tw`w-[16px] h-[16px]`; + case "large": + return tw`w-[30px] h-[30px]`; + default: + return tw`w-[22px] h-[22px]`; + } +}} + ${tw`relative`} +`; + +const CircleBorder = styled.div<{ isActive: boolean; size: string }>` + ${({ size }) => { + switch (size) { + case "small": + return tw`w-[16px] h-[16px] border-[1px]`; + case "large": + return tw`w-[30px] h-[30px] border-[2px]`; + default: + return tw`w-[22px] h-[22px] border-2`; + } +}} + ${tw`rounded-full absolute`} + border-color: ${({ isActive }) => (isActive ? "#5453ee" : "#c7c9ce")}; +`; + +const CircleInner = styled.div<{ size: string }>` + ${({ size }) => { + switch (size) { + case "small": + return tw`w-2 h-2 left-[4px] top-[4px]`; + case "large": + return tw`w-3 h-3 left-[8px] top-[8px]`; + default: + return tw`w-2.5 h-2.5 left-[6px] top-[6px]`; + } +}} + ${tw`bg-[#5453ee] rounded-full absolute`} +`; + +const Label = styled.div<{ isActive: boolean }>` + ${tw`text-base font-semibold font-['Pretendard']`} + color: ${({ isActive }) => (isActive ? "#5453ee" : "#c7c9ce")}; +`; + +const SubLabel = styled.div` + ${tw`text-[#5453ee] text-xs font-medium font-['Pretendard']`} +`; \ No newline at end of file diff --git a/src/components/My/LikeBoothCard.tsx b/src/components/My/LikeBoothCard.tsx index 71a3056..1824f39 100644 --- a/src/components/My/LikeBoothCard.tsx +++ b/src/components/My/LikeBoothCard.tsx @@ -1,29 +1,37 @@ import tw from "twin.macro"; import styled from "styled-components"; -import PlanBUrl from "../../assets/images/planb-logo.png?url"; import StarIcon from "../../assets/icons/star-icon"; import LikeFilledIcon from "../../assets/icons/like-filled-icon"; +import LikeNotFilledIcon from "../../assets/icons/like-not-filled-icon"; +import { searchLogoUrlByName } from "../../hooks/getImageUrl"; +import { useState } from "react"; type CardProps = { width?: string; height?: string; + photoBoothId: number; + name: string; + rating: number; + feature: string; + featureCount: number; }; -function LikeBoothCard({ width, height }: CardProps) { +function LikeBoothCard({ width, height, photoBoothId, name, rating, feature, featureCount }: CardProps) { + const [like, setLike] = useState(true); return ( - +
- 하루필름 건대입구역점 + {name}
- 4.5 + {rating}
- # 선명한 화질 - +3 + {feature} + {`+${featureCount - 1}`}
diff --git a/src/components/My/ProfileSection.tsx b/src/components/My/ProfileSection.tsx index a684bd0..4009477 100644 --- a/src/components/My/ProfileSection.tsx +++ b/src/components/My/ProfileSection.tsx @@ -4,6 +4,7 @@ import EditIcon from "../../assets/icons/edit-icon"; import { useQuery } from "@tanstack/react-query"; import { getUserInfo } from "../../api/user"; import { useAuthStore } from "../../store/useAuthStore"; +import DefaultImg from "../../assets/images/default-profile.svg?url"; function ProfileSection() { const { accessToken } = useAuthStore(); //사용자 프로필 정보 조회 api 호출 @@ -15,12 +16,12 @@ function ProfileSection() { if (!isLoading && data) { return ( - + {data.name} - + */} ); } else { diff --git a/src/components/My/VisitedBoothCard.tsx b/src/components/My/VisitedBoothCard.tsx index a76009d..509cc2c 100644 --- a/src/components/My/VisitedBoothCard.tsx +++ b/src/components/My/VisitedBoothCard.tsx @@ -1,8 +1,8 @@ import tw from "twin.macro"; import styled from "styled-components"; -import PlanBUrl from "../../assets/images/planb-logo.png?url"; import EditIcon from "../../assets/icons/edit-icon"; import { useNavigate } from "react-router-dom"; +import { searchLogoUrlByName } from "../../hooks/getImageUrl"; type CardProps = { width?: string; height?: string; @@ -15,13 +15,13 @@ function VisitedBoothCard({ width, height, photoboothId, name, month, date }: Ca const navigate = useNavigate(); return ( - +
{name} {`${month}월 ${date}일 이용`}
- diff --git a/src/components/PhotoCheck/HashModal.tsx b/src/components/PhotoCheck/HashModal.tsx index f6c652e..dfbdb46 100644 --- a/src/components/PhotoCheck/HashModal.tsx +++ b/src/components/PhotoCheck/HashModal.tsx @@ -7,10 +7,12 @@ type ModalProps = { hashTags: string[]; closeModal: () => void; setHashTags: React.Dispatch>; -} +}; function HashtagModal({ hashTags, closeModal, setHashTags }: ModalProps) { + const [keyboardHeight, setKeyboardHeight] = useState(0); const [countHash, setCountHash] = useState(0); + const [hash1, setHash1] = useState(hashTags[0] ?? ""); const [hash2, setHash2] = useState(hashTags[1] ?? ""); const [hash3, setHash3] = useState(hashTags[2] ?? ""); @@ -26,9 +28,40 @@ function HashtagModal({ hashTags, closeModal, setHashTags }: ModalProps) { closeModal(); }; + useEffect(() => { + // 키보드가 열릴 때 visualViewport 이벤트 리스너 추가 + const handleResize = () => { + const viewportHeight = window.innerHeight; + const visualViewport = window.visualViewport; + + if (visualViewport) { + const visualViewportHeight = visualViewport.height; + + // 키보드가 나타날 때 높이를 계산하여 모달 위치 조정 + if (visualViewportHeight < viewportHeight) { + setKeyboardHeight(viewportHeight - visualViewportHeight); + } else { + setKeyboardHeight(0); + } + } + }; + + // visualViewport가 존재할 경우에만 리스너 등록 + if (window.visualViewport) { + window.visualViewport.addEventListener("resize", handleResize); + } + + return () => { + // visualViewport가 존재할 경우에만 리스너 제거 + if (window.visualViewport) { + window.visualViewport.removeEventListener("resize", handleResize); + } + }; + }, []); + return ( - + 해시태그 추가 최대 3개까지 #를 추가해볼 수 있어요! @@ -38,31 +71,19 @@ function HashtagModal({ hashTags, closeModal, setHashTags }: ModalProps) {
0}># 0}> - setHash1(e.target.value)} - /> + setHash1(e.target.value)} />
0}># 0}> - setHash2(e.target.value)} - /> + setHash2(e.target.value)} />
0}># 0}> - setHash3(e.target.value)} - /> + setHash3(e.target.value)} /> ({countHash}/3)
@@ -76,50 +97,50 @@ function HashtagModal({ hashTags, closeModal, setHashTags }: ModalProps) { export default HashtagModal; const ModalOverlay = styled.div` - ${tw`fixed inset-0 flex justify-center items-center bg-opacity-50 z-50`} - background-color: rgba(23, 28, 36, 0.8); + ${tw`fixed inset-0 flex justify-center items-center bg-opacity-50 z-50`} + background-color: rgba(23, 28, 36, 0.8); `; const ModalContent = styled.div` - ${tw`w-[390px] h-auto relative bg-background rounded-tl-[26px] rounded-tr-[26px] p-8 flex flex-col items-center`} + ${tw`w-full h-auto relative bg-background rounded-tl-[26px] rounded-tr-[26px] p-8 flex flex-col items-center max-w-[480px]`} `; const Title = styled.h2` - ${tw`text-center text-[#171c24] text-[22px] font-semibold mb-2`} + ${tw`text-center text-[#171c24] text-[22px] font-semibold mb-2`} `; const SubText = styled.p` - ${tw`text-center text-[#676f7b] text-xs font-medium mb-2`} + ${tw`text-center text-[#676f7b] text-xs font-medium mb-2`} `; const CloseButton = styled.button` - ${tw`absolute top-4 right-4 w-[26px] h-[26px] bg-[#e9eaee] rounded-full`} + ${tw`absolute top-4 right-4 w-[26px] h-[26px] bg-[#e9eaee] rounded-full`} `; const HashtagContainer = styled.div` - ${tw`space-y-4 my-4 flex flex-col items-start`} + ${tw`space-y-4 my-4 flex flex-col items-start`} `; type HashtagItemProps = { hasText: boolean; -} +}; const HashtagItem = styled.div` - ${tw`w-[139px] h-[42px] flex items-center rounded-lg pl-4 text-gray400 text-lg font-medium`} - background-color: ${({ hasText }) => (hasText ? "#e1e0ff" : "#e9eaee")}; + ${tw`w-[139px] h-[42px] flex items-center rounded-lg pl-4 text-gray400 text-lg font-medium`} + background-color: ${({ hasText }) => (hasText ? "#e1e0ff" : "#e9eaee")}; `; const Hash = styled.div` - ${tw`mr-2 text-[22px] font-semibold`} - color: ${({ hasText }) => (hasText ? "#5453ee" : "#676f7b")}; + ${tw`mr-2 text-[22px] font-semibold`} + color: ${({ hasText }) => (hasText ? "#5453ee" : "#676f7b")}; `; const HashtagCount = styled.div` - ${tw`text-left text-[#b0b1b3] text-xs font-medium mt-2 ml-[10px]`} + ${tw`text-left text-[#b0b1b3] text-xs font-medium mt-2 ml-[10px]`} `; const ConfirmButton = styled.button` - ${tw`w-[225.81px] h-[50px] bg-[#5453ee] rounded-md text-[#FFFFFF] text-[22px] font-medium mt-3`} + ${tw`w-[225.81px] h-[50px] bg-[#5453ee] rounded-md text-[#FFFFFF] text-[22px] font-medium mt-3`} `; const StyledInput = styled.input` @@ -127,4 +148,4 @@ const StyledInput = styled.input` background-color: transparent; border: none; outline: none; -`; \ No newline at end of file +`; diff --git a/src/components/PhotoCheck/RecordModal.tsx b/src/components/PhotoCheck/RecordModal.tsx index 3392359..a0b6bb4 100644 --- a/src/components/PhotoCheck/RecordModal.tsx +++ b/src/components/PhotoCheck/RecordModal.tsx @@ -1,75 +1,104 @@ import styled from "styled-components"; import tw from "twin.macro"; import X from "../../assets/images/X.svg?react"; -import { useState } from "react"; +import { useEffect, useState } from "react"; type ModalProps = { closeModal: () => void; setRecords: (tags: string) => void; -} + records: string; +}; -function RecordModal({ closeModal, setRecords }: ModalProps) { - const [words, setWords] = useState(""); +function RecordModal({ closeModal, setRecords, records }: ModalProps) { + const [keyboardHeight, setKeyboardHeight] = useState(0); + const [words, setWords] = useState(records); const confirm = () => { setRecords(words); + closeModal(); - } - + }; + + useEffect(() => { + // 키보드가 열릴 때 visualViewport 이벤트 리스너 추가 + const handleResize = () => { + const viewportHeight = window.innerHeight; + const visualViewport = window.visualViewport; + + if (visualViewport) { + const visualViewportHeight = visualViewport.height; + + // 키보드가 나타날 때 높이를 계산하여 모달 위치 조정 + if (visualViewportHeight < viewportHeight) { + setKeyboardHeight(viewportHeight - visualViewportHeight); + } else { + setKeyboardHeight(0); + } + } + }; + + // visualViewport가 존재할 경우에만 리스너 등록 + if (window.visualViewport) { + window.visualViewport.addEventListener("resize", handleResize); + } + + return () => { + // visualViewport가 존재할 경우에만 리스너 제거 + if (window.visualViewport) { + window.visualViewport.removeEventListener("resize", handleResize); + } + }; + }, []); + return ( - + 기록 추가 자유롭게 사진에 대한 기록을 추가해보세요! -
- setWords(e.target.value)} - /> +
+ setWords(e.target.value)} />
- 확인 - - -) - ; + 확인 + + + ); } export default RecordModal; const ModalOverlay = styled.div` - ${tw`fixed inset-0 flex justify-center items-center bg-opacity-50 z-50`} - background-color: rgba(23, 28, 36, 0.8); + ${tw`fixed inset-0 flex justify-center items-center bg-opacity-50 z-50`} + background-color: rgba(23, 28, 36, 0.8); `; const ModalContent = styled.div` - ${tw`w-[390px] h-auto relative bg-background rounded-tl-[26px] rounded-tr-[26px] p-8 flex flex-col items-center`} + ${tw`w-full h-auto relative bg-background rounded-tl-[26px] rounded-tr-[26px] p-8 flex flex-col items-center max-w-[480px]`} `; const Title = styled.h2` - ${tw`text-center text-[#171c24] text-[22px] font-semibold mb-2`} + ${tw`text-center text-[#171c24] text-[22px] font-semibold mb-2`} `; const SubText = styled.p` - ${tw`text-center text-[#676f7b] text-xs font-medium mb-2`} + ${tw`text-center text-[#676f7b] text-xs font-medium mb-2`} `; const CloseButton = styled.button` - ${tw`absolute top-4 right-4 w-[26px] h-[26px] bg-[#e9eaee] rounded-full`} + ${tw`absolute top-4 right-4 w-[26px] h-[26px] bg-[#e9eaee] rounded-full`} `; const RecordContainer = styled.div` - ${tw`space-y-4 my-4 flex flex-col items-start`} + ${tw`space-y-4 my-4 flex flex-col items-start w-full`} `; const ConfirmButton = styled.button` - ${tw`w-[225.81px] h-[50px] bg-[#5453ee] rounded-md text-[#FFFFFF] text-[22px] font-medium mt-3`} + ${tw`w-full h-[50px] bg-[#5453ee] rounded-md text-[#FFFFFF] text-[22px] font-medium mt-3`} `; const StyledInput = styled.input` ${tw`w-full h-full rounded-lg text-gray400 bg-gray100 flex-grow`} -`; \ No newline at end of file +`; diff --git a/src/components/WriteReview/UploadImageSection.tsx b/src/components/WriteReview/UploadImageSection.tsx index 85ca4ac..20cbc0b 100644 --- a/src/components/WriteReview/UploadImageSection.tsx +++ b/src/components/WriteReview/UploadImageSection.tsx @@ -55,7 +55,7 @@ function UploadImageSection({ imageFiles, setImageFiles }: UploadImageProps) { export default UploadImageSection; const Container = styled.div` - ${tw`flex my-[25px] gap-[12px] overflow-x-auto`} + ${tw`flex my-[10px] gap-[12px] overflow-x-auto`} `; const UploadButtonWrapper = styled.div` diff --git a/src/hooks/getImageUrl.tsx b/src/hooks/getImageUrl.tsx index a8a18a1..7d79194 100644 --- a/src/hooks/getImageUrl.tsx +++ b/src/hooks/getImageUrl.tsx @@ -4,19 +4,24 @@ import MonoUrl from "../assets/images/mono-logo.png"; // BoothLogoUrl에서 type에 맞는 url 찾기 export const getLogoUrl = (type: string) => { const logo = BoothCategories.find((item) => item.id === type); - console.log(type); - console.log(logo); + + return logo ? logo!.imageUrl : MonoUrl; // 해당 type에 맞는 로고 URL 반환 +}; + +export const searchLogoUrlByName = (name: string) => { + const logo = BoothCategories.find((item) => name.includes(item.label)); + return logo ? logo!.imageUrl : MonoUrl; // 해당 type에 맞는 로고 URL 반환 }; export const getReviewBoothTagImgUrl = (name: string) => { const tag = BoothTagCategories.find((item) => item.label === name); - console.log(tag); + return tag!.imageUrl; // 해당 type에 맞는 로고 URL 반환 }; export const getReviewPhotoTagImgUrl = (name: string) => { const tag = PhotoTagCategories.find((item) => item.label === name); - console.log(tag); + return tag!.imageUrl; // 해당 type에 맞는 로고 URL 반환 }; diff --git a/src/pages/Album/index.tsx b/src/pages/Album/index.tsx index e9f62cb..980cbcf 100644 --- a/src/pages/Album/index.tsx +++ b/src/pages/Album/index.tsx @@ -10,118 +10,265 @@ import DateModal from "../../components/Album/DateModal.tsx"; import Footer from "../../components/Album/Footer.tsx"; import ConfirmModal from "../../components/Album/ConfirmModal.tsx"; import BoothFilterModal from "../../components/Album/BoothFilterModal.tsx"; +import { Delete, Get, Post } from "../../api"; +import { useAuthStore } from "../../store/useAuthStore.ts"; +import { useNavigate } from "react-router-dom"; +import HashtagSearchModal from "../../components/Album/HashtagSearchModal.tsx"; +import { getCurrentLocation } from "../../hooks/getLocation.tsx"; +import AlbumMap from "../../components/Album/AlbumMap.tsx"; +import Modal from "../../components/Common/Modal.tsx"; +import { searchPhotoBoothName } from "../../api/booth.ts"; +type Image = { + albumId: number; + photoUrl: string; + like: boolean; +}; -type Images = { - url: string; - title: string; -} +type ImageForLocation = { + photoUrl: string; + x: number; + y: number; +}; function Album() { - const [selectedCategory, setSelectedCategory] = useState('날짜별'); + const [searchCategory, setSearchCategory] = useState("날짜별"); const [isDateModalOpen, setIsDateModalOpen] = useState(false); - const [imageList, setImageList] = useState([]); + const [imageList, setImageList] = useState([]); + const [imageListForLocation, setImageListForLocation] = useState([]); const [isEditing, setIsEditing] = useState(false); const [selectedImages, setSelectedImages] = useState([]); // 선택된 이미지 상태 - const [footerStatus, setFooterStatus] = useState('initial'); + const [footerStatus, setFooterStatus] = useState("initial"); const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false); // 확인 모달 상태 - const [photoBooth,setPhotoBooth] = useState('하루필름'); + const [photoBooth, setPhotoBooth] = useState("인생네컷"); const [isBoothFilterModalOpen, setIsBoothFilterModalOpen] = useState(false); - const [date, setDate] = useState('2024년 9월'); - + const [isLoading, setIsLoading] = useState(true); + const [isHashtagSearchModalOpen, setIsHashtagSearchModalOpen] = useState(false); + const today = new Date(); + const [year, setYear] = useState(today.getFullYear()); + const [month, setMonth] = useState(today.getMonth() + 1); + const { accessToken } = useAuthStore(); + const navigate = useNavigate(); + + //리뷰 작성 할 부스 아이디 있는지 확인하는 상태변수 + const [boothInfoForReview, setBoothInfoForReview] = useState<{ id: string; name: string } | null>(null); + //방금 앨범 등록한 앨번 id가 있는지 확인하는 useEffect함수 -> albumId가 있다면 바로 리뷰를 작성하도록 유도 + useEffect(() => { + const checkForReview = async () => { + const boothId = localStorage.getItem("boothId"); + if (boothId && boothId != undefined) { + const boothName = await searchPhotoBoothName(boothId); + if (boothName) { + setBoothInfoForReview({ id: boothId, name: boothName }); + localStorage.removeItem("boothId"); + } + } + }; + checkForReview(); + }, [localStorage.getItem("boothId")]); + + const getPhotoByDate = async (year: number, month: number, accessToken: string) => { + try { + setIsLoading(true); + const res = await Get(`/api/v1/album/date/${year}/${month}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + if (res.status === 200) { + console.log(res.data.payload); + setImageList(res.data.payload); + } + } catch (error) { + console.log(error); + } finally { + setIsLoading(false); // 요청 완료 후 로딩 상태 비활성화 + } + }; + + const getPhotoByBooth = async (photoBooth: string, accessToken: string) => { + try { + setIsLoading(true); + const res = await Get(`/api/v1/album/photobooth/${photoBooth}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + if (res.status === 200) { + console.log(res.data.payload); + setImageList(res.data.payload); + } + } catch (error) { + console.log(error); + } finally { + setIsLoading(false); // 요청 완료 후 로딩 상태 비활성화 + } + }; + + const getPhotoByLocation = async (x: number, y: number, accessToken: string) => { + try { + setIsLoading(true); + const res = await Get(`/api/v1/album/location?${x}&${y}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + if (res.status === 200) { + console.log(res.data.payload); + setImageListForLocation(res.data.payload); + } + } catch (error) { + console.log(error); + } finally { + setIsLoading(false); // 요청 완료 후 로딩 상태 비활성화 + } + }; + useEffect(() => { - setImageList([ - { - url: "https://example.com/image1.jpg", - title: "Sample Image 1", - }, - { - url: "https://example.com/image2.jpg", - title: "Sample Image 2", - }, - { - url: "https://example.com/image3.jpg", - title: "Sample Image 3", - }, - { - url: "https://example.com/image4.jpg", - title: "Sample Image 4", - }, - { - url: "https://example.com/image5.jpg", - title: "Sample Image 5", - }, - { - url: "https://example.com/image6.jpg", - title: "Sample Image 6", - }, - { - url: "https://example.com/image7.jpg", - title: "Sample Image 7", - }, - { - url: "https://example.com/image8.jpg", - title: "Sample Image 8", - }, - ]); - }, []); - + if (searchCategory === "날짜별") { + getPhotoByDate(year, month, accessToken!); + } + if (searchCategory === "포토부스별") { + getPhotoByBooth(photoBooth, accessToken!); + } + }, [year, month, photoBooth, searchCategory]); + // "선택" 버튼 클릭 핸들러 const handleSelectClick = () => { setIsEditing(true); }; - + const handleCancelClick = () => { setSelectedImages([]); setIsEditing(false); }; - + const handleAddClick = () => { - - } - + navigate("/photo-upload"); + }; + + useEffect(() => { + if (searchCategory === "위치별") { + //현 위치 받아오기 + const fetchLocation = async () => { + const res = await getCurrentLocation(); + if (res) { + getPhotoByLocation(res.lat, res.lng, accessToken!); + } + }; + fetchLocation(); + } + }, [searchCategory]); + + const deletePhotos = async (albumId: number, accessToken: string) => { + try { + const res = await Delete(`/api/v1/album/${albumId}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + if (res.status === 200) { + console.log(res.data.payload); + } + } catch (error) { + console.log(error); + } + }; + + const likePhotos = async (albumId: number, accessToken: string) => { + try { + const res = await Post( + `/api/v1/album/like/${albumId}`, + null, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); + if (res.status === 200) { + console.log(res.data.payload); + } + } catch (error) { + console.log(error); + } + }; + // 이미지 카드 클릭 핸들러 - const handleImageClick = (index: number) => { + const handleImageClick = (albumId: number) => { setSelectedImages((prevSelected) => - prevSelected.includes(index) - ? prevSelected.filter((i) => i !== index) - : [...prevSelected, index] + prevSelected.includes(albumId) ? prevSelected.filter((i) => i !== albumId) : [...prevSelected, albumId] ); }; - + // 카테고리 버튼 클릭 핸들러 const handleCategoryClick = (category: string) => { - setSelectedCategory(category); + setSearchCategory(category); }; - + const handleCloseModal = () => { setIsDateModalOpen(false); - } - - const handleConfirm = () => { - setIsConfirmModalOpen(false); - } - + }; + + const handleConfirm = async () => { + try { + if (footerStatus === "liking") { + await Promise.all(selectedImages.map((albumId) => likePhotos(albumId, accessToken))); + } else if (footerStatus === "deleting") { + await Promise.all(selectedImages.map((albumId) => deletePhotos(albumId, accessToken))); + } + } catch (error) { + console.error("Error handling confirmation:", error); + } finally { + setSelectedImages([]); // 선택한 이미지 초기화 + setIsConfirmModalOpen(false); // 모달 닫기 + setFooterStatus("initial"); + setIsEditing(false); + if (searchCategory === "날짜별") { + getPhotoByDate(year, month, accessToken); + } + if (searchCategory === "포토부스별") { + getPhotoByBooth(photoBooth, accessToken); + } + } + }; + return ( + {boothInfoForReview != null && ( + navigate(`/write-review/${boothInfoForReview.id}/step/1`)} + onRightOptionClick={() => setBoothInfoForReview(null)} + /> + )} - -
-
-
- -
-
-
+ {searchCategory != "위치별" && ( + + setIsHashtagSearchModalOpen(true)}> + + + + 해시태그로 사진 검색! + + + )} - {selectedCategory === "날짜별" && setIsDateModalOpen(true)}>{date}} - {selectedCategory === "포토부스별" && - setIsBoothFilterModalOpen(true)}> - {photoBooth} - - } + {searchCategory === "날짜별" && ( + setIsDateModalOpen(true)}> + {year}년 {month}월 + + )} + {searchCategory === "포토부스별" && ( + setIsBoothFilterModalOpen(true)}> + {photoBooth} + + + )} - {selectedCategory === "날짜별" && ( + {searchCategory === "날짜별" && ( )} -
+
{isEditing ? ( 취소 ) : ( <> - 선택 - 추가 + {searchCategory != "위치별" && ( + <> + 선택 + 추가 + + )} )}
- {isDateModalOpen && } - - {imageList.length === 0 ? ( - <> - -

사진을 채워보세요

- - ) : ( - - {imageList.map((image, index) => ( - handleImageClick(index)} // 클릭 핸들러 전달 - /> - ))} - - )} -
- + {isDateModalOpen && ( + + )} + + {searchCategory === "위치별" ? ( + <> + ) : ( + <> + {isLoading ? ( + <> + ) : ( + <> + {imageList.length === 0 ? ( +
+ +

사진을 채워보세요

+
+ ) : ( + + + {imageList.map((image) => ( + handleImageClick(image.albumId)} // 클릭 핸들러 전달 + isLiked={image.like} + id={image.albumId} + /> + ))} + + + )} + + )} + + )} + {!isEditing && ( - handleCategoryClick("날짜별")}> + handleCategoryClick("날짜별")}> 날짜별 - handleCategoryClick("포토부스별")}> + handleCategoryClick("포토부스별")}> 포토부스별 - handleCategoryClick("위치별")}> + handleCategoryClick("위치별")}> 위치별 @@ -200,11 +369,11 @@ function Album() { )} {isConfirmModalOpen && ( setIsConfirmModalOpen(false)} - onLeftOptionClick={() => setIsConfirmModalOpen(false)} - onRightOptionClick={handleConfirm} + onLeftOptionClick={handleConfirm} + onRightOptionClick={() => setIsConfirmModalOpen(false)} /> )} {isBoothFilterModalOpen && ( @@ -214,6 +383,8 @@ function Album() { setIsBoothFilterModalOpen={setIsBoothFilterModalOpen} /> )} + {isHashtagSearchModalOpen && } + {searchCategory === "위치별" && } ); } @@ -226,19 +397,19 @@ const Layout = styled.div` `; const ImageContainer = styled.div` - ${tw`flex flex-col justify-center items-center w-full`} - height: 100vh; /* 높이를 조정하여 다른 UI 요소가 가리지 않도록 */ - overflow-y: auto; - position: relative; - z-index: 1; /* 다른 요소들보다 낮게 설정 */ + ${tw`flex flex-col w-full`} + height: 100vh; /* 높이를 조정하여 다른 UI 요소가 가리지 않도록 */ + overflow-y: auto; + position: relative; + z-index: 1; /* 다른 요소들보다 낮게 설정 */ `; const ImageDiv = styled.div` - ${tw`grid gap-4 w-full`} - grid-template-columns: repeat(2, 1fr); /* 두 개의 열 */ - grid-auto-rows: auto; - max-height: 100%; /* 부모 컨테이너 안에서 최대 높이 제한 */ - overflow-y: auto; + ${tw`grid gap-4 w-full`} + grid-template-columns: repeat(2, 1fr); /* 두 개의 열 */ + grid-auto-rows: auto; + max-height: 100%; /* 부모 컨테이너 안에서 최대 높이 제한 */ + overflow-y: auto; `; const Content = styled.div` @@ -247,61 +418,72 @@ const Content = styled.div` `; const HeaderSection = styled.div` - ${tw`flex flex-col items-center`} - position: relative; - z-index: 10; + ${tw`flex flex-col items-center`} + position: relative; + z-index: 10; `; -const Subtitle = styled.div` - ${tw`h-[33px] px-4 py-1.5 bg-[#5453ee] inline-flex items-center rounded-full shadow text-background text-base font-semibold`} - font-family: 'Pretendard', sans-serif; - flex-shrink: 0; +const ButtonGroup = styled.div` + ${tw`flex w-full gap-2 justify-between fixed`} // fixed로 위치 고정 + max-width: 480px; + top: 100px; // 원하는 위치로 조정 + z-index: 20; // ImageContainer보다 높은 z-index 설정 `; -const ButtonGroup = styled.div` - ${tw`flex gap-2 justify-between fixed`} // fixed로 위치 고정 - top: 100px; // 원하는 위치로 조정 - left: 50%; - transform: translateX(-70%); - z-index: 20; // ImageContainer보다 높은 z-index 설정 +const Subtitle = styled.div` + ${tw`h-[33px] px-4 py-1.5 bg-[#5453ee] inline-flex items-center rounded-full shadow text-background text-base font-semibold`} + font-family: 'Pretendard', sans-serif; + flex-shrink: 0; `; const PositionedDiv = styled.div` - ${tw`absolute mt-2`} - top: 100%; - left: 50%; - transform: translateX(-75%); - margin-top: 1px; + ${tw`absolute mt-2`} + top: 100%; + left: 50%; + transform: translateX(-90%); + margin-top: 1px; `; const ActionButton = styled.div` - ${tw`px-4 py-1.5 rounded-full text-[#4b515a] bg-[#c7c9ce] shadow text-center text-sm font-semibold cursor-pointer`} - font-family: 'Pretendard', sans-serif; - white-space: nowrap; + ${tw`px-4 py-1.5 rounded-full text-[#4b515a] bg-[#c7c9ce] shadow text-center text-sm font-semibold cursor-pointer`} + font-family: 'Pretendard', sans-serif; + white-space: nowrap; `; - const CategoryMenu = styled.div` - ${tw`flex bg-[#c7c9ce]/80 rounded-full shadow`} - width: 248.1px; - position: fixed; - bottom: 0; - left: 50%; - transform: translateX(-50%); - margin-bottom: 70px; - z-index: 10; + ${tw`flex bg-[#c7c9ce]/80 rounded-full shadow`} + width: 248.1px; + position: fixed; + bottom: 0; + left: 50%; + transform: translateX(-50%); + margin-bottom: 70px; + z-index: 10; `; const CategoryItem = styled.div<{ selected?: boolean }>` - ${tw`text-base font-semibold cursor-pointer flex items-center justify-center`} - height: 38px; - padding: 0 16px; - border-radius: 30px; - color: ${({ selected }) => (selected ? '#fff' : '#676f7b')}; - background-color: ${({ selected }) => (selected ? '#676f7b' : 'transparent')}; - font-family: 'Pretendard', sans-serif; - white-space: nowrap; - transition: background-color 0.3s ease, color 0.3s ease; + ${tw`text-base font-semibold cursor-pointer flex items-center justify-center`} + height: 38px; + padding: 0 16px; + border-radius: 30px; + color: ${({ selected }) => (selected ? "#fff" : "#676f7b")}; + background-color: ${({ selected }) => (selected ? "#676f7b" : "transparent")}; + font-family: "Pretendard", sans-serif; + white-space: nowrap; + transition: + background-color 0.3s ease, + color 0.3s ease; +`; + +const HashtagSearchButton = styled.button` + ${tw`w-2/3 p-2.5 bg-[#e9eaee] rounded-lg flex justify-end items-center mb-4`} + &:focus { + outline: none; + } +`; + +const SearchIcon = styled.div` + ${tw`w-6 p-px flex justify-center items-center`} `; export default Album; diff --git a/src/pages/Booth/index.tsx b/src/pages/Booth/index.tsx index b39ee95..796948c 100644 --- a/src/pages/Booth/index.tsx +++ b/src/pages/Booth/index.tsx @@ -4,9 +4,10 @@ import NavBar from "../../components/Common/NavBar"; import Header from "../../components/Common/Header"; import ImgSlider from "../../components/Booth/ImgSlider"; import { Outlet, useLocation, useNavigate, useParams } from "react-router-dom"; -import BoothInfoSection from "../../components/Booth/BoothInfo"; +import BoothInfoSection from "../../components/Booth/BoothInfoSection"; import { useQuery } from "@tanstack/react-query"; import { getBoothInfo } from "../../api/booth"; +import { useEffect, useRef, useState } from "react"; // 특정 부스 상세 정보 페이지 function BoothDetail() { @@ -18,6 +19,30 @@ function BoothDetail() { queryFn: () => getBoothInfo(boothId), }); + // MenuContainer 고정 여부를 결정할 상태 변수 + const [isSticky, setIsSticky] = useState(false); + const menuRef = useRef(null); + + // Intersection Observer 설정 + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + setIsSticky(!entry.isIntersecting); + }, + { threshold: 1 } + ); + + if (menuRef.current) { + observer.observe(menuRef.current); + } + + return () => { + if (menuRef.current) { + observer.unobserve(menuRef.current); + } + }; + }, []); + const navigate = useNavigate(); return ( @@ -33,7 +58,11 @@ function BoothDetail() { photoBoothBrand={""} /> )} - + +
+ {/* 고정 여부에 따라 위치가 변경되는 MenuContainer */} + + navigate("feed")}> 홈 @@ -61,10 +90,11 @@ const MainWrapper = styled.div` ${tw`overflow-auto flex flex-col w-full mt-[60px] items-center pb-[80px]`} `; -const MenuContainer = styled.div` - ${tw`flex flex-row w-full h-[32px] border-b-2 border-b-gray100 mt-[30px] justify-between px-[30px]`} +const MenuContainer = styled.div<{ isSticky: boolean }>` + ${tw`flex flex-row w-full h-[35px] border-b-2 border-b-gray100 justify-between px-[30px] bg-[white]`} + ${({ isSticky }) => isSticky && `position: fixed; top: 58px; z-index: 10;`} `; const MenuBtn = styled.button<{ $active: boolean }>` - ${tw`w-[67px] h-[31px] font-display font-semibold text-[14px]`} + ${tw`w-[67px] h-[35px] font-display font-semibold text-[16px]`} ${({ $active }) => ($active ? tw`text-gray700 border-b-4 border-b-main ` : tw`text-gray300`)} `; diff --git a/src/pages/My/FavoritesPage.tsx b/src/pages/My/FavoritesPage.tsx index 0b931ae..8ff3e8a 100644 --- a/src/pages/My/FavoritesPage.tsx +++ b/src/pages/My/FavoritesPage.tsx @@ -2,12 +2,30 @@ import React from "react"; import tw from "twin.macro"; import styled from "styled-components"; import ImageCard from "../../components/Album/ImageCard"; +import { useQuery } from "@tanstack/react-query"; +import { getPhotoLikes } from "../../api/my"; +import { useAuthStore } from "../../store/useAuthStore"; +import NoImage from "../../assets/images/no-images.svg?react"; function FavoritesPage() { + const { accessToken } = useAuthStore(); + //찜한 부스 조회 + const { data: likedPhotoData } = useQuery({ + queryKey: ["getLikedPhotos"], + queryFn: () => getPhotoLikes(accessToken!), + }); return ( - - - + {likedPhotoData && + (likedPhotoData.length > 0 ? ( + likedPhotoData.map((item, index) => ( + + )) + ) : ( +
+ +

즐겨찾기한 사진이 없어요

+
+ ))}
); } diff --git a/src/pages/My/LikeBoothsPage.tsx b/src/pages/My/LikeBoothsPage.tsx index c05776c..7b7c50d 100644 --- a/src/pages/My/LikeBoothsPage.tsx +++ b/src/pages/My/LikeBoothsPage.tsx @@ -3,13 +3,33 @@ import styled from "styled-components"; import Header from "../../components/Common/Header"; import { useNavigate } from "react-router-dom"; import LikeBoothCard from "../../components/My/LikeBoothCard"; +import { useQuery } from "@tanstack/react-query"; +import { getLikedBooths } from "../../api/my"; +import { useAuthStore } from "../../store/useAuthStore"; function LikeBoothsPage() { const navigate = useNavigate(); + + const { accessToken } = useAuthStore(); + //찜한 부스 조회 + const { data: likedBoothData } = useQuery({ + queryKey: ["getLikedBooths"], + queryFn: () => getLikedBooths(accessToken!), + }); return (
navigate(-1)} /> - + {likedBoothData && + likedBoothData.map((item, index) => ( + + ))} ); diff --git a/src/pages/My/RecordPage.tsx b/src/pages/My/RecordPage.tsx index 714064a..b7e0426 100644 --- a/src/pages/My/RecordPage.tsx +++ b/src/pages/My/RecordPage.tsx @@ -6,7 +6,7 @@ import { useNavigate } from "react-router-dom"; import LikeBoothCard from "../../components/My/LikeBoothCard"; import VisitedBoothCard from "../../components/My/VisitedBoothCard"; import { useQuery } from "@tanstack/react-query"; -import { getMyReviews, getVisitedBooths } from "../../api/my"; +import { getLikedBooths, getMyReviews, getVisitedBooths } from "../../api/my"; import { useAuthStore } from "../../store/useAuthStore"; function RecordPage() { const navigate = useNavigate(); @@ -19,19 +19,27 @@ function RecordPage() { }); //방문한 부스 조회 - const { isLoading, data: visitedBoothData } = useQuery({ + const { data: visitedBoothData } = useQuery({ queryKey: ["getVisitedBooths"], queryFn: () => getVisitedBooths(accessToken!), }); + //찜한 부스 조회 + const { data: likedBoothData } = useQuery({ + queryKey: ["getLikedBooths"], + queryFn: () => getLikedBooths(accessToken!), + }); + return (
{`${myReviewData && myReviewData.reviewCount ? myReviewData.reviewCount : "0"}개의 나의 리뷰`} - + {myReviewData && myReviewData?.reviewMypageDetailDtoList.length > 2 && ( + + )}
@@ -56,19 +64,37 @@ function RecordPage() {
찜해둔 부스 - + {likedBoothData && likedBoothData.length > 2 && ( + + )}
- - + {likedBoothData && likedBoothData.length > 0 ? ( + likedBoothData + .slice(0, 2) + .map((item, index) => ( + + )) + ) : ( + 방문한 부스가 없어요 + )}
방문한 부스 - {visitedBoothData && visitedBoothData.length > 0 && ( + {visitedBoothData && visitedBoothData.length > 2 && ( @@ -34,10 +78,11 @@ const Layout = styled.div` ${tw`flex flex-col [max-width: 480px] w-full h-[100vh] items-center m-auto bg-[#FFFFFF]`} `; -const MenuContainer = styled.div` - ${tw`flex flex-row w-full justify-center h-[32px] border-b-2 border-b-gray100 mt-[30px] px-[45px] gap-[100px]`} +const MenuContainer = styled.div<{ isSticky: boolean }>` + ${tw`flex flex-row w-full justify-center h-[35px] border-b-2 border-b-gray100 mt-[10px] px-[45px] gap-[100px]`} + ${({ isSticky }) => isSticky && `position: fixed; top: 0; z-index: 10; background-color: #FFFFFF;`} `; const MenuBtn = styled.button<{ $active: boolean }>` - ${tw`w-[105px] h-[31px] font-display font-semibold text-[14px]`} + ${tw`w-[105px] h-[35px] font-display font-semibold text-[16px]`} ${({ $active }) => ($active ? tw`text-gray700 border-b-4 border-b-main ` : tw`text-gray700`)} `; diff --git a/src/pages/PhotoCheck/index.tsx b/src/pages/PhotoCheck/index.tsx deleted file mode 100644 index 6d6af81..0000000 --- a/src/pages/PhotoCheck/index.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { useState, useEffect } from "react"; -import ShareComplete from '../../assets/images/share-complete.png'; -import PhotoCheck1 from "./step1.tsx"; -import PhotoCheck2 from "./step2.tsx"; -import PhotoCheck3 from "./step3.tsx"; -import { useLocation } from "react-router-dom"; -import {useNavigate} from "react-router-dom"; - -type InfoState = { - year: string; - month: string; - day: string; - boothLocation: string; - qrLink: string; - image : File; -}; - -function PhotoCheck() { - const location = useLocation(); - const { year, month, day, boothLocation, qrLink, image } = location.state as InfoState || {}; - const dateInfo = year + "년 " + month + "월 " + day + "일 " + boothLocation; - const [step, setStep] = useState(1); - const [hashTags, setHashTags] = useState([]); - const [records, setRecords] = useState("클릭하여 오늘 있었던 일들을 기록해보세요"); - const navigate = useNavigate(); - const imgSrc = qrLink ? qrLink : image; - - // Function to handle the next button click - const handleNextClick = () => { - setStep((prevStep) => prevStep + 1); // Increment the step state - }; - - const handleBackStep = () => { - setStep((prevStep) => prevStep - 1); - } - - useEffect(() => { - if (step === 4) { - const timeout = setTimeout(() => { - navigate('/home'); // 2초 후에 home으로 리디렉션 - }, 2000); - return () => clearTimeout(timeout); - } - }, [step]); - - return ( - <> - {step === 1 && ( - - )} - - {step === 2 && ( - - )} - - {step === 3 && ( - - )} - - {step === 4 && ( -
- share-complete - 공유가 완료됐어요 -
- )} - - ); -} - -export default PhotoCheck; diff --git a/src/pages/PhotoCheck/step1.tsx b/src/pages/PhotoCheck/step1.tsx deleted file mode 100644 index 522bf19..0000000 --- a/src/pages/PhotoCheck/step1.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useNavigate } from "react-router-dom"; -import styled from "styled-components"; -import tw from "twin.macro"; -import BackIcon from "../../assets/icons/back-icon.tsx"; - -type Step1Props = { - handleNextClick: () => void; - dateInfo: string; - imgSrc : string; -} - -function PhotoCheck1({ handleNextClick, dateInfo, imgSrc }: Step1Props) { - const navigate = useNavigate(); - - const handleBack = () => { - navigate("/photo-review"); - }; - - return ( - -
- 사진 확인 - {dateInfo} - -
- - QR 사진 - -
다음
-
-
- ); -} - -const Container = styled.div` - ${tw`bg-gray600 flex flex-col w-full min-h-screen items-center`} - overflow-x: hidden; -`; - -const Header = styled.header` - ${tw`relative w-full flex flex-col items-center justify-center mb-12 h-[80px] border-b-[1.5px] border-b-background`} -`; - -const Title = styled.div` - ${tw`text-[#FFFFFF] text-2xl font-semibold font-['Pretendard']`} -`; - -const DateText = styled.div` - ${tw`opacity-70 text-[#676f7b] text-xs font-medium font-['Pretendard'] mt-1`} -`; - -const ButtonContainer = styled.button` - ${tw`w-[280px] h-[62px] bg-[#5453ee] rounded-lg mt-8 mb-[72px] flex justify-center items-center`} -`; - -export default PhotoCheck1; diff --git a/src/pages/PhotoCheck/step2.tsx b/src/pages/PhotoCheck/step2.tsx deleted file mode 100644 index 8898fd5..0000000 --- a/src/pages/PhotoCheck/step2.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import styled from "styled-components"; -import tw from "twin.macro"; -import BackIcon from "../../assets/icons/back-icon.tsx"; -import React, { useEffect, useState } from "react"; -import HashTagModal from "../../components/PhotoCheck/HashModal.tsx"; -import RecordModal from "../../components/PhotoCheck/RecordModal.tsx"; - -type Step2Props = { - handleNextClick: () => void; - handleBackStep: () => void; - hashTags: string[]; - setHashTags: React.Dispatch>; - records: string; - setRecords: React.Dispatch>; - dateInfo: string; - imgSrc: string; -} - -function PhotoCheck2({ handleNextClick, handleBackStep, hashTags, setHashTags, records, setRecords, dateInfo, imgSrc }: Step2Props) { - const [isHashModalOpen, setIsHashModalOpen] = useState(false); - const [isRecordModalOpen, setIsRecordModalOpen] = useState(false); - const [countHash, setCountHash] = useState(0); - - const openHashModal = () => { - setIsHashModalOpen(true); - }; - - const closeHashModal = () => { - setIsHashModalOpen(false); - }; - - const openRecordModal = () => { - setIsRecordModalOpen(true); - }; - - const closeRecordModal = () => { - setIsRecordModalOpen(false); - }; - - useEffect(() => { - const count = hashTags.filter((tag) => tag.length > 0); - setCountHash(count.length); - }, [hashTags]); - - return ( - -
-
- 사진 기록 - -
- {dateInfo} -
-
- {countHash > 0 ? ( -
- {hashTags.map( - (tag, index) => - tag && ( - setIsHashModalOpen(true)}> -
-
- {tag} -
-
-
- ) - )} -
{countHash}/3
-
- ) : ( - <> -
- -
-
- - - - - 해시태그를 추가하면 사진을 쉽게 찾을 수 있어요 - - -
-
- {countHash}/3 -
- - )} -
- {isHashModalOpen && } - QR 사진 -
- -
- {isRecordModalOpen && } - -
다음
-
-
- ); -} - -const Container = styled.div` - ${tw`bg-gray600 flex flex-col w-full min-h-screen items-center`} - overflow-x: hidden; -`; - -const Header = styled.header` - ${tw`relative w-full flex flex-col items-center justify-center mb-12 h-[80px] border-b-[1.5px] border-b-background`} -`; - -const Title = styled.div` - ${tw`text-[#FFFFFF] text-2xl font-semibold font-['Pretendard']`} -`; - -const DateText = styled.div` - ${tw`opacity-70 text-[#676f7b] text-xs font-medium font-['Pretendard'] mt-1`} -`; - -const ButtonContainer = styled.button` - ${tw`w-[280px] h-[62px] bg-[#5453ee] rounded-lg mt-8 flex justify-center items-center`} -`; - -const TagItem = styled.div` - ${tw`mb-2`} -`; - -export default PhotoCheck2; diff --git a/src/pages/PhotoCheck/step3.tsx b/src/pages/PhotoCheck/step3.tsx deleted file mode 100644 index 40b6125..0000000 --- a/src/pages/PhotoCheck/step3.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { useNavigate } from "react-router-dom"; -import ShareLogo from "../../assets/images/share-logo.svg?react"; -import Checked from "../../assets/images/checked.svg?react"; -import styled from "styled-components"; -import tw from "twin.macro"; -import BackIcon from "../../assets/icons/back-icon.tsx"; -import { useState } from "react"; -import {useAuthStore} from "../../store/useAuthStore.ts"; -import {getPresignedUrl, uploadToS3} from "../../api/file.ts"; -import {uploadPhoto} from "../../api/photoupload.ts"; - -type Step3Props = { - handleNextClick: () => void; - handleBackStep: () => void; - dateInfo: string; - records:string; - hashtags:string[]; - year:string; - month:string; - day:string; - imgSrc : File; -} - -function PhotoCheck3({ handleNextClick, handleBackStep, year, month, day, hashtags, records, dateInfo, imgSrc }: Step3Props) { - const navigate = useNavigate(); - const [clicked, setClicked] = useState(false); - const [imageFiles, setImageFiles] = useState([imgSrc]); - const { accessToken } = useAuthStore(); - - const getUploadedFilePaths = async (imageFiles: File[], accessToken: string): Promise => { - const uploadPromises = imageFiles.map(async (image) => { - const presignedData = await getPresignedUrl("/images/album", image.name, accessToken); - if (presignedData) { - await uploadToS3(presignedData.url, image); - return presignedData.filePath; - } - return null; - }); - - const filePaths = (await Promise.all(uploadPromises)).filter(Boolean).join(""); - console.log(filePaths); - return filePaths; - }; - - const uploadImage = async ( - accessToken: string, - boothId: number, - year: string, - month: string, - day: string, - hashtags: string[], - records: string, - filePath: string - ) => { - const res = await uploadPhoto( - accessToken, - boothId, - year, - month, - day, - hashtags, - records, - filePath - ); - - if (res && res.code === 200) { - handleNextClick(); - } - }; - - // 메인 로직 - const handleUpload = async () => { - console.log(imgSrc) - // Step 2: 이미지 업로드 및 filePaths 저장 - const filePath = await getUploadedFilePaths(imageFiles, accessToken!); - console.log(filePath); - // Step 3: 리뷰등록 api 호출 - await uploadImage( - accessToken!, - 0, - year, - month, - day, - hashtags, - records, - filePath, - ); - }; - - return ( - -
-
- 사진 확인 - -
- {dateInfo} -
- -
- -
해시태그, 사진 기록까지 공유하기
-
- handleUpload()}> -
다음
-
- navigate("/home")}> -
다음에 할게요
-
-
- ); -} - -const Container = styled.div` - ${tw`bg-gray600 flex flex-col w-full min-h-screen items-center`} - overflow-x: hidden; -`; - -const Header = styled.header` - ${tw`relative w-full flex flex-col items-center justify-center mb-12 h-[80px] border-b-[1.5px] border-b-background`} -`; - -const Title = styled.div` - ${tw`text-[#FFFFFF] text-2xl font-semibold font-['Pretendard']`} -`; - -const DateText = styled.div` - ${tw`opacity-70 text-[#676f7b] text-xs font-medium font-['Pretendard'] mt-1`} -`; - -const ButtonContainer = styled.button` - ${tw`w-[280px] h-[62px] bg-[#5453ee] rounded-lg mt-8 flex justify-center items-center`} -`; - -const ButtonContainer2 = styled.button` - ${tw`w-[280px] h-[62px] bg-[#F9F9FB] rounded-lg mt-3 flex justify-center items-center`} -`; - -export default PhotoCheck3; diff --git a/src/pages/PhotoReview/index.tsx b/src/pages/PhotoReview/index.tsx deleted file mode 100644 index 9fea29e..0000000 --- a/src/pages/PhotoReview/index.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import styled from "styled-components"; -import tw from "twin.macro"; -import Search from "../../assets/images/search.svg?react"; -import BackIcon from "../../assets/icons/back-icon"; -import { useNavigate, useLocation } from "react-router-dom"; -import { useState } from "react"; - -function PhotoReview() { - // useState를 사용하여 날짜와 부스 위치 상태 관리 - const [year, setYear] = useState(""); - const [month, setMonth] = useState(""); - const [day, setDay] = useState(""); - const [boothLocation, setBoothLocation] = useState(""); - const navigate = useNavigate(); - const location = useLocation(); - - const handleNext = () => { - navigate("/photo-check", { - state: { - year: year, - month: month, - day: day, - boothLocation: boothLocation, - qrLink: location.state.qrLink, - image: location.state.image, - }, - }); - }; - - const handleBack = () => { - navigate("/qr-scan"); - }; - - return ( - -
-
- 사진 설명 - -
-
-
- Photo Check - - - - - 필수 - - -
-
-
- setYear(e.target.value)} - maxLength={4} - /> -
-
- 년 -
-
-
-
- setMonth(e.target.value)} - maxLength={2} - /> -
-
- 월 -
-
-
-
- setDay(e.target.value)} - maxLength={2} - /> -
-
- 일 -
-
-
- - - - 필수 - - - - - - - setBoothLocation(e.target.value)} - /> - -
- handleNext()}> -
다음
-
-
- ); -} - -const Container = styled.div` - ${tw`bg-background flex flex-col w-full min-h-screen items-center `} - overflow-x: hidden; - &::-webkit-scrollbar { - display: none; - } - -ms-overflow-style: none; -`; - -const ContentContainer = styled.div` - ${tw`flex flex-col items-start w-11/12 mt-10 gap-6`} -`; - -const LabelContainer = styled.div` - ${tw`flex items-center gap-2.5`} -`; - -const Label = styled.div` - ${tw`text-[#171d24] text-lg font-semibold font-['Pretendard']`} -`; - -const RequiredBadge = styled.div` - ${tw`px-2.5 py-1.5 bg-[#a1a6b5] rounded-3xl flex justify-center items-center`} -`; - -const RequiredText = styled.div` - ${tw`text-xs font-semibold font-['Pretendard']`} -`; - -const InputContainer = styled.div` - ${tw`w-11/12 p-2.5 bg-[#e9eaee] rounded-lg flex justify-end items-center`} -`; - -const SearchIcon = styled.div` - ${tw`w-6 p-px flex justify-center items-center`} -`; - -const ButtonContainer = styled.button` - ${tw`w-[280px] h-[62px] bg-[#5453ee] rounded-lg mt-20 flex justify-center items-center`} -`; - -export default PhotoReview; diff --git a/src/pages/PhotoUpload/CheckPhoto.tsx b/src/pages/PhotoUpload/CheckPhoto.tsx new file mode 100644 index 0000000..19307ff --- /dev/null +++ b/src/pages/PhotoUpload/CheckPhoto.tsx @@ -0,0 +1,49 @@ +import { useLocation, useNavigate } from "react-router-dom"; +import styled from "styled-components"; +import tw from "twin.macro"; +import BackIcon from "../../assets/icons/back-icon.tsx"; +import NextButton from "../../components/Common/NextButton.tsx"; + +function CheckPhoto() { + const navigate = useNavigate(); + + const location = useLocation(); + const { imageFile } = location.state; + + const handleNextClick = () => { + navigate("/photo-review", { state: { imageFile: imageFile } }); + }; + + return ( + +
+ 사진 확인 + +
+ + QR 사진 + +
+ ); +} + +const Container = styled.div` + ${tw`bg-gray600 flex flex-col w-full h-[100vh] items-center pb-[60px] justify-between`} + // overflow-x: hidden; + + .img-container { + ${tw`w-[70%]`} + } +`; + +const Header = styled.header` + ${tw`relative w-full flex flex-col items-center justify-center h-[80px] border-b-[1.5px] border-b-background`} +`; + +const Title = styled.div` + ${tw`text-[#FFFFFF] text-2xl font-semibold font-['Pretendard']`} +`; + +export default CheckPhoto; diff --git a/src/pages/PhotoUpload/ReviewPhoto.tsx b/src/pages/PhotoUpload/ReviewPhoto.tsx new file mode 100644 index 0000000..dfca9a2 --- /dev/null +++ b/src/pages/PhotoUpload/ReviewPhoto.tsx @@ -0,0 +1,189 @@ +import styled from "styled-components"; +import tw from "twin.macro"; +import Search from "../../assets/images/search.svg?react"; + +import { useNavigate, useLocation } from "react-router-dom"; +import { useState } from "react"; +import Header from "../../components/Common/Header"; +import NextButton from "../../components/Common/NextButton"; +import { searchBoothId } from "../../api/booth"; + +function ReviewPhoto() { + // useState를 사용하여 날짜와 부스 위치 상태 관리 + const today = new Date(); + const [year, setYear] = useState(String(today.getFullYear())); + const [month, setMonth] = useState(String(today.getMonth() + 1)); + const [day, setDay] = useState(String(today.getDate())); + const [boothLocation, setBoothLocation] = useState(""); + const [boothId, setBoothId] = useState(0); + const [searchData, setSearchData] = useState< + | { + id: number; + name: string; + }[] + | null + >(null); + const navigate = useNavigate(); + const location = useLocation(); + + const { imageFile } = location.state; + + console.log(imageFile); + + const handleNext = () => { + navigate("/write-detail", { + state: { + year: year, + month: month, + day: day, + qrLink: location.state.qrLink, + imageFile: imageFile, + boothId: boothId, + }, + }); + }; + + const handleSearch = async () => { + const res = await searchBoothId(boothLocation); + if (res) { + setSearchData(res); + console.log(searchData); + } + }; + const handleItemClick = (id: number, name: string) => { + setBoothId(id); + setBoothLocation(name); + setSearchData(null); + }; + return ( + +
navigate(-1)}>
+ + + + + + 필수 + + +
+ setYear(e.target.value)} + maxLength={4} + /> +
+ setMonth(e.target.value)} + maxLength={2} + /> + + + setDay(e.target.value)} + maxLength={2} + /> + +
+ + + + 필수 + + +
+ + + + + setBoothLocation(e.target.value)} + /> + + + {searchData && + (searchData.length > 0 ? ( + + {searchData.map((data, index) => ( +
+
  • handleItemClick(data.id, data.name)} + className="text-[14px] font-normal text-gray600 list-none cursor-default" + > + {data.name} +
  • + {index !== searchData.length - 1 &&
    } +
    + ))} +
    + ) : ( + +
  • + {"검색되는 포토부스 정보가 없어요."} +
  • +
    + ))} +
    +
    + + +
    + ); +} + +const Container = styled.div` + ${tw`bg-background flex flex-col w-full h-[100vh] items-center [max-width: 480px] justify-between pb-[60px] `} + overflow-x: hidden; + &::-webkit-scrollbar { + display: none; + } + -ms-overflow-style: none; +`; + +const ContentContainer = styled.div` + ${tw`flex flex-col items-start w-full px-[16px] mt-[20px] gap-4`} +`; + +const LabelContainer = styled.div` + ${tw`flex items-center gap-2.5 mt-[70px]`} +`; + +const Label = styled.div` + ${tw`text-[#171d24] text-lg font-semibold font-['Pretendard']`} +`; + +const RequiredBadge = styled.div` + ${tw`px-2.5 py-1.5 bg-[#a1a6b5] rounded-3xl flex justify-center items-center`} +`; + +const RequiredText = styled.div` + ${tw`text-xs font-semibold font-['Pretendard']`} +`; + +const InputContainer = styled.div` + ${tw`w-full p-2.5 bg-[#e9eaee] rounded-lg flex justify-end items-center `} + &:focus { + outline: none; + } +`; + +const SearchIcon = styled.div` + ${tw`w-6 p-px flex justify-center items-center`} +`; + +const ModalBox = styled.div` + ${tw`w-full rounded-[5px] bg-[#FFFFFF] border-[1px] border-gray100 font-display font-medium text-[14px] absolute top-[50px]`} +`; + +export default ReviewPhoto; diff --git a/src/pages/PhotoUpload/UploadComplete.tsx b/src/pages/PhotoUpload/UploadComplete.tsx new file mode 100644 index 0000000..043daf9 --- /dev/null +++ b/src/pages/PhotoUpload/UploadComplete.tsx @@ -0,0 +1,77 @@ +import { useLocation, useNavigate } from "react-router-dom"; +import ShareLogo from "../../assets/images/share-logo.svg?react"; +import styled from "styled-components"; +import tw from "twin.macro"; +import { createLink } from "../../api/share.ts"; +import { useAuthStore } from "../../store/useAuthStore.ts"; +import CloseIcon from "../../assets/icons/close-icon.tsx"; +import { useAlertStore } from "../../store/useAlertStore.ts"; + +function UploadComplete() { + const navigate = useNavigate(); + const location = useLocation(); + const { accessToken } = useAuthStore(); + + const { albumId } = location.state; + + const { openAlert } = useAlertStore(); + const createShareLink = async () => { + const res = await createLink(albumId, accessToken!); + if (res) { + await navigator.clipboard.writeText(res); + openAlert("공유링크가 클립보드에 복사되었어요!"); + } + }; + + const handleClose = () => { + navigate("/album"); + }; + + return ( + +
    + + + + 사진 공유 +
    +
    + + + 공유링크를 통해 같이 찍은 친구, 가족의 앨범에도 자동으로 등록할 수 있어요 + + +
    공유할래요
    +
    + navigate("/album")}> +
    다음에 할게요
    +
    +
    +
    + ); +} + +const Container = styled.div` + ${tw`bg-gray600 flex flex-col w-full min-h-screen items-center`} + overflow-x: hidden; +`; + +const Header = styled.header` + ${tw`relative w-full flex flex-col items-center justify-center py-[15px] border-b-[1.5px] border-b-gray100`} +`; + +const Title = styled.div` + ${tw`text-[#FFFFFF] text-2xl font-semibold font-['Pretendard']`} +`; + +const ButtonContainer = styled.button` + ${tw`w-[280px] h-[62px] bg-[#5453ee] rounded-lg flex justify-center items-center`} +`; + +const ButtonContainer2 = styled.button` + ${tw`w-[280px] h-[62px] bg-[#F9F9FB] rounded-lg flex justify-center items-center`} +`; +const CloseButton = styled.button` + ${tw` absolute left-[15px]`} +`; +export default UploadComplete; diff --git a/src/pages/PhotoUpload/WriteDetail.tsx b/src/pages/PhotoUpload/WriteDetail.tsx new file mode 100644 index 0000000..ae1e4d3 --- /dev/null +++ b/src/pages/PhotoUpload/WriteDetail.tsx @@ -0,0 +1,195 @@ +import styled from "styled-components"; +import tw from "twin.macro"; +import BackIcon from "../../assets/icons/back-icon.tsx"; +import { useEffect, useState } from "react"; +import HashTagModal from "../../components/PhotoCheck/HashModal.tsx"; +import RecordModal from "../../components/PhotoCheck/RecordModal.tsx"; +import { useAuthStore } from "../../store/useAuthStore.ts"; +import { getPresignedUrl, uploadToS3 } from "../../api/file.ts"; +import { uploadPhoto } from "../../api/photoupload.ts"; +import { useLocation, useNavigate } from "react-router-dom"; +import NextButton from "../../components/Common/NextButton.tsx"; + +function WriteDetail() { + const [isHashModalOpen, setIsHashModalOpen] = useState(false); + const [isRecordModalOpen, setIsRecordModalOpen] = useState(false); + const [countHash, setCountHash] = useState(0); + // const [imageFiles, setImageFiles] = useState([imgSrc]); + const { accessToken } = useAuthStore(); + const location = useLocation(); + + const [hashTags, setHashTags] = useState([]); + const [records, setRecords] = useState(""); + + const navigate = useNavigate(); + const { year, month, day, qrLink, imageFile, boothId } = location.state; + + console.log(location.state); + + const openHashModal = () => { + setIsHashModalOpen(true); + }; + + const closeHashModal = () => { + setIsHashModalOpen(false); + }; + + const openRecordModal = () => { + setIsRecordModalOpen(true); + }; + + const closeRecordModal = () => { + setIsRecordModalOpen(false); + }; + + useEffect(() => { + const count = hashTags.filter((tag) => tag.length > 0); + setCountHash(count.length); + }, [hashTags]); + + const getUploadedFilePaths = async () => { + const presignedData = await getPresignedUrl("/images/album", imageFile.name, accessToken!); + if (presignedData) { + //s3에 이미지 업로드 + await uploadToS3(presignedData.url, imageFile); + return presignedData.filePath; + } + }; + + const uploadImage = async ( + accessToken: string, + boothId: number, + year: string, + month: string, + day: string, + hashtags: string[], + records: string, + filePath: string + ) => { + const res = await uploadPhoto(accessToken, boothId, year, month, day, hashtags, records, filePath); + + if (res) { + //리뷰 작성을 위해 로컬 스토리지에 부스 아이디 저장 + localStorage.setItem("boothId", res.photoboothId.toString()); + navigate("/upload-complete", { + state: { + albumId: res.albumId, + }, + }); + } + }; + + // 메인 로직 + const handleUpload = async () => { + console.log(imageFile); + // Step 2: 이미지 업로드 및 filePaths 저장 + const filePath = await getUploadedFilePaths(); + console.log(filePath); + // Step 3: 리뷰등록 api 호출 + if (filePath) { + await uploadImage(accessToken!, boothId, year, month, day, hashTags, records, filePath); + } + }; + + return ( + + {/* 헤더 */} +
    +
    + 사진 기록 + +
    + {`${year}년 ${month}월 ${day}일`} +
    + + {/*
    */} +
    + {countHash > 0 ? ( + hashTags.map( + (tag, index) => + tag && ( + setIsHashModalOpen(true)}> +
    + {tag} +
    +
    + ) + ) + ) : ( + + )} +
    {countHash}/3
    + {countHash === 0 && ( +
    + + + + + 해시태그를 추가하면 사진을 쉽게 찾을 수 있어요 + + +
    + )} +
    + + {isHashModalOpen && } + QR 사진 + + + + {isRecordModalOpen && } + + + + ); +} + +const Container = styled.div` + ${tw`bg-gray600 flex flex-col w-full min-h-screen items-center [max-width: 480px]`} + overflow-x: hidden; +`; + +const Header = styled.header` + ${tw`w-full flex flex-col items-center justify-center h-[80px] border-b-[1.5px] border-b-background py-[13px] bg-gray600 mb-3`} +`; + +const MainWrapper = styled.div` + ${tw`relative w-full h-full flex flex-col px-[30px] items-center pb-[60px] gap-8 my-auto `} +`; +const Title = styled.div` + ${tw`text-[#FFFFFF] text-2xl font-semibold font-['Pretendard']`} +`; + +const DateText = styled.div` + ${tw`opacity-70 text-gray100 text-xs font-medium font-display mt-1`} +`; + +const TagItem = styled.div` + ${tw`px-5 py-1.5 bg-[#e1e0ff] rounded-3xl border border-[#676f7b] flex justify-center items-center overflow-hidden `} +`; + +export default WriteDetail; diff --git a/src/pages/PhotoUpload/index.tsx b/src/pages/PhotoUpload/index.tsx index aa8762d..40e8767 100644 --- a/src/pages/PhotoUpload/index.tsx +++ b/src/pages/PhotoUpload/index.tsx @@ -3,62 +3,42 @@ import tw from "twin.macro"; import styled from "styled-components"; import { useNavigate } from "react-router-dom"; import CloseIcon from "../../assets/icons/close-icon"; - -type OptionProps = { - onClick: () => void; - isActive: boolean; - label: string; - subLabel?: string; -}; - -const OptionComponent = ({ onClick, isActive, label, subLabel }: OptionProps) => { - return ( - - ); -}; +import NextButton from "../../components/Common/NextButton"; +import OptionButton from "../../components/Common/OptionButton.tsx"; function PhotoUpload() { const [activeOption, setActiveOption] = useState(null); const [selectedFile, setSelectedFile] = useState(null); const navigate = useNavigate(); - + const handleOptionClick = (option: string) => { setActiveOption(option); if (option === "Upload") { document.getElementById("file-input")?.click(); } }; - + const handleFileChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (file) { setSelectedFile(file); console.log("선택된 파일:", file); + navigate("/photo-check", { state: { imageFile: file } }); } }; - + const handleNext = () => { if (activeOption === "QR") { navigate("/qr-scan"); } else if (activeOption === "Upload" && selectedFile) { console.log("사진을 업로드합니다:", selectedFile); - navigate("/photo-review", { state: { image: URL.createObjectURL(selectedFile) } }); } }; - + const handleClose = () => { - navigate("/home"); + navigate(-1); }; - + return (
    @@ -70,88 +50,44 @@ function PhotoUpload() {

    - + - handleOptionClick("QR")} isActive={activeOption === "QR"} label="QR 인식" subLabel="QR 인식은 하루필름 매장만 가능해요" /> - handleOptionClick("Upload")} isActive={activeOption === "Upload"} label="내 사진첩 불러오기" /> - + {/* 파일 입력 요소 */} - - - -
    다음
    -
    + + + ); } const Container = styled.div` - ${tw`bg-background flex flex-col items-center min-h-screen w-full max-w-[400px] m-auto`} - overflow-x: hidden; + ${tw`bg-background flex flex-col items-center min-h-screen w-full [max-width: 480px] m-auto pb-[60px]`} + overflow-x: hidden; `; const OptionContainer = styled.div` - ${tw`flex flex-col items-center m-auto`} -`; - -const Option = styled.button` - ${tw`w-[267px] h-[90px] rounded-lg border mb-4 cursor-pointer transition-colors duration-200`} - padding: ${({ isActive }) => (isActive ? "23px 12px" : "8px 12px")}; - background-color: ${({ isActive }) => (isActive ? "#e1e0ff" : "transparent")}; - border-color: ${({ isActive }) => (isActive ? "#5453ee" : "#c7c9ce")}; - display: flex; - flex-direction: ${({ isActive }) => (isActive ? "column" : "row")}; - justify-content: center; - align-items: center; - gap: ${({ isActive }) => (isActive ? "10px" : "12px")}; -`; - -const CircleContainer = styled.div` - ${tw`w-[22px] h-[22px] relative`} -`; - -const CircleBorder = styled.div<{ isActive: boolean }>` - ${tw`absolute w-full h-full rounded-full border-2`} - border-color: ${({ isActive }) => (isActive ? "#5453ee" : "#c7c9ce")}; -`; - -const CircleInner = styled.div` - ${tw`absolute w-2.5 h-2.5 bg-[#5453ee] rounded-full`} - left: 6px; - top: 6px; -`; - -const Label = styled.div<{ isActive: boolean }>` - ${tw`text-base font-semibold font-['Pretendard']`} - color: ${({ isActive }) => (isActive ? "#5453ee" : "#c7c9ce")}; -`; - -const SubLabel = styled.div` - ${tw`text-[#5453ee] text-xs font-medium font-['Pretendard']`} + ${tw`flex flex-col items-center m-auto`} `; const ButtonContainer = styled.button` - ${tw`w-[280px] h-[62px] bg-[#5453ee] rounded-lg mt-12 mb-[88px] flex justify-center items-center`} + ${tw`w-[280px] h-[62px] bg-[#5453ee] rounded-lg mt-12 mb-[88px] flex justify-center items-center`} `; const CloseButton = styled.button` - ${tw`absolute right-[10px]`} + ${tw`absolute right-[10px]`} `; -export default PhotoUpload; \ No newline at end of file +export default PhotoUpload; diff --git a/src/pages/QRScan/index.tsx b/src/pages/QRScan/index.tsx index 5459fb6..ea27288 100644 --- a/src/pages/QRScan/index.tsx +++ b/src/pages/QRScan/index.tsx @@ -4,7 +4,7 @@ import tw from "twin.macro"; import styled from "styled-components"; import CloseIcon from "../../assets/icons/close-icon"; import ProgressIcon from "../../assets/icons/progress-icon"; -import { Scanner } from '@yudiel/react-qr-scanner'; +import { Scanner } from "@yudiel/react-qr-scanner"; type ScanResult = { boundingBox: object; @@ -18,7 +18,7 @@ function QRScan() { const navigate = useNavigate(); const [percentage, setPercentage] = useState(0); const [qrLink, setQrLink] = useState(undefined); - + useEffect(() => { if (isCaptured) { const interval = setInterval(() => { @@ -38,7 +38,7 @@ function QRScan() { if (percentage === 100) { // 100%가 화면에 렌더링된 후 페이지 이동 const timeout = setTimeout(() => { - navigate("/photo-review", {state: {qrLink}}); + navigate("/photo-check", { state: { qrLink } }); }, 500); // 약간의 지연 시간(0.5초)을 주어 100%가 표시된 후 페이지 이동 return () => clearTimeout(timeout); } diff --git a/src/pages/Share/index.tsx b/src/pages/Share/index.tsx new file mode 100644 index 0000000..6e2660f --- /dev/null +++ b/src/pages/Share/index.tsx @@ -0,0 +1,44 @@ +import { useEffect } from "react"; +import { useAuthStore } from "../../store/useAuthStore"; +import { useNavigate } from "react-router-dom"; +import { saveShare } from "../../api/share"; +import { useAlertStore } from "../../store/useAlertStore"; + +function Share() { + const { accessToken } = useAuthStore(); + const navigate = useNavigate(); + const { openAlert } = useAlertStore(); + useEffect(() => { + const fetchShare = async () => { + const query = new URLSearchParams(location.search); + const token = query.get("token"); + console.log(token); + if (token) { + if (accessToken) { + const res = await saveShare(accessToken, token); + if (res === 200) { + openAlert("사진 등록이 완료되었습니다."); + navigate("/album"); + } else if (res === 409) { + //작성자가 공유링크에 접속하였을 경우 + navigate("/album"); + } else if (res === 400) { + openAlert("유효하지 않은 공유링크입니다."); + navigate("/home"); + } else { + openAlert("잠시후 다시 시도해주세요."); + navigate("/album"); + } + } else { + localStorage.setItem("shareId", token!); + openAlert("로그인 해야 사진 저장이 가능해요!"); + navigate("/login"); + } + } + }; + fetchShare(); + }, [location.search]); + return
    ; +} + +export default Share; diff --git a/src/pages/Token/index.tsx b/src/pages/Token/index.tsx index 3ad4f3e..7304973 100644 --- a/src/pages/Token/index.tsx +++ b/src/pages/Token/index.tsx @@ -1,7 +1,8 @@ -import React, { useEffect } from "react"; +import { useEffect } from "react"; import { useNavigate } from "react-router-dom"; import { useAuthStore } from "../../store/useAuthStore"; import { axiosInstance } from "../../api"; +import { saveShare } from "../../api/share"; function Token() { const navigate = useNavigate(); @@ -13,7 +14,17 @@ function Token() { if (token) { useAuthStore.setState({ accessToken: token }); login(); + axiosInstance.defaults.headers.common["Authorization"] = `Bearer ${token}`; + + if (localStorage.getItem("shareId")) { + const res = await saveShare(token, localStorage.getItem("shareId")!); + if (res) { + alert("사진 등록이 완료되었습니다."); + localStorage.removeItem("shareId"); + navigate("/album"); + } + } navigate("/home"); } }; diff --git a/src/pages/WriteReview/step1.tsx b/src/pages/WriteReview/step1.tsx index d75d27f..f3ee5e2 100644 --- a/src/pages/WriteReview/step1.tsx +++ b/src/pages/WriteReview/step1.tsx @@ -1,6 +1,6 @@ import tw from "twin.macro"; import styled from "styled-components"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import Rating from "../../components/Common/Rating"; import InputTagSection from "../../components/WriteReview/InputTagSection"; import { BoothTagCategories, PhotoTagCategories } from "../../data/review-tag-categories"; @@ -10,6 +10,7 @@ import { useQuery } from "@tanstack/react-query"; import { searchPhotoBoothName } from "../../api/booth"; import { useAuthStore } from "../../store/useAuthStore"; import { searchBoothFeatures, searchPhotoFeatures } from "../../api/review"; +import { useAlertStore } from "../../store/useAlertStore"; function Step1() { const { boothId } = useParams() as { boothId: string }; @@ -17,6 +18,7 @@ function Step1() { const [rate, setRate] = useState(0); const [selectedBoothTags, setSelectedBoothTags] = useState([]); const [selectedPhotoTags, setSelectedPhotoTags] = useState([]); + const { openAlert } = useAlertStore(); //포토부스 이름 조회 api 호출 const { isLoading, data: boothName } = useQuery({ @@ -42,12 +44,18 @@ function Step1() { if (rate <= 5) return "완전만족해요"; }; + useEffect(() => { + const checkTags = async () => { + const totalSelectedTagsLength = selectedBoothTags.length + selectedPhotoTags.length; + if (totalSelectedTagsLength > 5) { + await openAlert("태그는 최대 5개만 고를 수 있어요!"); + return; + } + }; + checkTags(); + }, [selectedBoothTags, selectedPhotoTags]); + const handleNextStep = () => { - const totalSelectedTagsLength = selectedBoothTags.length + selectedPhotoTags.length; - if (totalSelectedTagsLength > 5) { - alert("태그는 최대 5개 선택 가능합니다."); - return; - } navigate(`/write-review/${boothId}/step/2`, { state: { rate: rate, diff --git a/src/pages/WriteReview/step2.tsx b/src/pages/WriteReview/step2.tsx index ab110aa..3d79524 100644 --- a/src/pages/WriteReview/step2.tsx +++ b/src/pages/WriteReview/step2.tsx @@ -78,20 +78,24 @@ function Step2() { return ( - - 사진을 등록해주세요 - 선택 - - - - 부스에 대한 설명을 작성해주세요 - 선택 - -
    -