From f54d7821c86680119e077b648a998259408df7bf Mon Sep 17 00:00:00 2001 From: taejun0 Date: Tue, 25 Feb 2025 17:52:26 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20api=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + api/proxy/index.js | 31 ++++ public/images/main/Calendar.svg | 4 + public/images/main/history.svg | 3 + public/images/main/historyactive.svg | 3 + public/images/main/house.svg | 3 + public/images/main/houseactive.svg | 5 + public/images/main/my.svg | 4 + public/images/main/myactive.svg | 4 + public/images/main/warning.svg | 3 + .../common/choicemodal/ChoiceModal.jsx | 18 ++- src/components/common/choicemodal/styled.js | 14 +- .../layout/footer/mainfooter/MainFooter.jsx | 25 +++- .../layout/footer/mainfooter/styled.js | 34 ++++- src/hooks/useSelect.js | 70 +++------ src/pages/mainpage/MainPage.jsx | 140 ++++++++++++++++-- src/pages/mainpage/styled.js | 86 ++++++++++- src/services/ExperienceService.js | 24 +++ 18 files changed, 407 insertions(+), 65 deletions(-) create mode 100644 api/proxy/index.js create mode 100644 public/images/main/Calendar.svg create mode 100644 public/images/main/history.svg create mode 100644 public/images/main/historyactive.svg create mode 100644 public/images/main/house.svg create mode 100644 public/images/main/houseactive.svg create mode 100644 public/images/main/my.svg create mode 100644 public/images/main/myactive.svg create mode 100644 public/images/main/warning.svg create mode 100644 src/services/ExperienceService.js diff --git a/.gitignore b/.gitignore index db75880..61178c6 100644 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,7 @@ Desktop.ini yarn.lock package-lock.json pnpm-lock.yaml +vercel.json # Parcel, Rollup, and Webpack files .cache/ diff --git a/api/proxy/index.js b/api/proxy/index.js new file mode 100644 index 0000000..651b70a --- /dev/null +++ b/api/proxy/index.js @@ -0,0 +1,31 @@ +export default async function handler(req) { + const backendURL = `${process.env.VITE_BASE_URL}${req.url.replace("/api/proxy", "/api")}`; + + try { + const response = await fetch(backendURL, { + method: req.method, + headers: { + ...Object.fromEntries(req.headers), // 모든 원본 헤더 전달 + host: new URL(process.env.VITE_BASE_URL).host, // 백엔드 호스트로 변경 + }, + body: ["GET", "HEAD"].includes(req.method) ? null : await req.text(), // GET, HEAD는 body 제거 + }); + + return new Response(response.body, { + status: response.status, + headers: { + ...Object.fromEntries(response.headers), // 응답 헤더 유지 + "Access-Control-Allow-Origin": "*", // CORS 문제 해결 + }, + }); + } catch (error) { + return new Response(JSON.stringify({ error: "Proxy request failed", details: error.message }), { + status: 500, + headers: { "Content-Type": "application/json" }, + }); + } +} + +export const config = { + runtime: "edge", +}; \ No newline at end of file diff --git a/public/images/main/Calendar.svg b/public/images/main/Calendar.svg new file mode 100644 index 0000000..d3d39a5 --- /dev/null +++ b/public/images/main/Calendar.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/main/history.svg b/public/images/main/history.svg new file mode 100644 index 0000000..760feb1 --- /dev/null +++ b/public/images/main/history.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/main/historyactive.svg b/public/images/main/historyactive.svg new file mode 100644 index 0000000..d8ea8fd --- /dev/null +++ b/public/images/main/historyactive.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/main/house.svg b/public/images/main/house.svg new file mode 100644 index 0000000..d8d5770 --- /dev/null +++ b/public/images/main/house.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/main/houseactive.svg b/public/images/main/houseactive.svg new file mode 100644 index 0000000..08c8a2a --- /dev/null +++ b/public/images/main/houseactive.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/main/my.svg b/public/images/main/my.svg new file mode 100644 index 0000000..972e9a4 --- /dev/null +++ b/public/images/main/my.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/main/myactive.svg b/public/images/main/myactive.svg new file mode 100644 index 0000000..d18b4db --- /dev/null +++ b/public/images/main/myactive.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/main/warning.svg b/public/images/main/warning.svg new file mode 100644 index 0000000..5fbbaba --- /dev/null +++ b/public/images/main/warning.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/common/choicemodal/ChoiceModal.jsx b/src/components/common/choicemodal/ChoiceModal.jsx index de590f8..b5553aa 100644 --- a/src/components/common/choicemodal/ChoiceModal.jsx +++ b/src/components/common/choicemodal/ChoiceModal.jsx @@ -11,12 +11,26 @@ export const ChoiceModal = ({ type, onClose, ContentTitle, ContentSemiTitle, Con > - {/* */} + {type===1 &&} {type===2 &&} + {type===3 &&} + {type===4 &&} {ContentTitle && {ContentTitle}} - {ContentSemiTitle && {ContentSemiTitle}} + {ContentSemiTitle && ( + + {Array.isArray(ContentSemiTitle) ? ( + ContentSemiTitle.map((line, index) => ( + {line} + )) + ) : ( + ContentSemiTitle.split("\n").map((line, index) => ( + {line} + )) + )} + + )} {ContentContent && ( <> {Array.isArray(ContentContent) ? ( diff --git a/src/components/common/choicemodal/styled.js b/src/components/common/choicemodal/styled.js index fd88e2a..91dd844 100644 --- a/src/components/common/choicemodal/styled.js +++ b/src/components/common/choicemodal/styled.js @@ -45,6 +45,8 @@ export const Icon24 = styled.img` export const thinkEmoji = styled.img` width: 61px; + + margin-bottom: ${({ $Type }) => ($Type===3) ? "1rem" : "0"}; `; export const Title = styled.div` @@ -54,10 +56,16 @@ export const Title = styled.div` margin-bottom: 0.5rem; `; +export const TextWrap = styled.div` + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 0.5rem; +`; + export const SemiTitle = styled.div` ${({ theme }) => theme.fonts.PretendardR} font-size: 16px; - margin-bottom: 0.5rem; `; export const Contents = styled.div` @@ -105,4 +113,8 @@ export const RightButton = styled.div` background-color: ${({ theme }) => theme.colors.black}; cursor: pointer; +`; + +export const DateWrap = styled.div` + display: flex; `; \ No newline at end of file diff --git a/src/components/layout/footer/mainfooter/MainFooter.jsx b/src/components/layout/footer/mainfooter/MainFooter.jsx index 7c97a1e..c45985d 100644 --- a/src/components/layout/footer/mainfooter/MainFooter.jsx +++ b/src/components/layout/footer/mainfooter/MainFooter.jsx @@ -1,9 +1,32 @@ import * as S from "./styled"; +import { useLocation } from "react-router-dom"; +import useCustomNavigate from "@hooks/useCustomNavigate"; + +import house from "/images/main/house.svg"; +import history from "/images/main/history.svg"; +import my from "/images/main/my.svg"; +import houseactive from "/images/main/houseactive.svg"; +import historyactive from "/images/main/historyactive.svg"; +import myactive from "/images/main/myactive.svg"; export const MainFooter = () => { + const location = useLocation(); + const { goToPage } = useCustomNavigate(); return ( - + + goToPage("/history")} $isActive={location.pathname === "/history"}> + + + + goToPage("/main")} $isActive={location.pathname === "/main"}> + + + + goToPage("/mypage")} $isActive={location.pathname === "/mypage"}> + + + ) } \ No newline at end of file diff --git a/src/components/layout/footer/mainfooter/styled.js b/src/components/layout/footer/mainfooter/styled.js index df82ce0..9ea9724 100644 --- a/src/components/layout/footer/mainfooter/styled.js +++ b/src/components/layout/footer/mainfooter/styled.js @@ -1,11 +1,39 @@ import styled from "styled-components"; export const Wrapper = styled.div` - position: absolute; + position: relative; + width: 100%; + height: 100px; + left: 0; bottom: 0; + display: flex; - width: 100%; - height: 83px; + justify-content: center; + align-items: center; + background: ${({ theme }) => theme.colors.white}; box-shadow: 0px -4px 4px 0px rgba(0, 0, 0, 0.05); +`; + +export const ImageWrap = styled.div` + display: flex; + width: 85%; + justify-content: space-between; +`; + +export const IconBox = styled.div` + display: flex; + width: 32%; + height: 50px; + + border-radius: 65px; + justify-content: center; + align-items: center; + + background-color: ${({ theme, $isActive }) => $isActive ? theme.colors.black : theme.colors.lightbluegray}; + cursor: pointer; +`; + +export const Icon36 = styled.img` + width: 36px; `; \ No newline at end of file diff --git a/src/hooks/useSelect.js b/src/hooks/useSelect.js index ce1f5ea..2a71aad 100644 --- a/src/hooks/useSelect.js +++ b/src/hooks/useSelect.js @@ -7,42 +7,6 @@ const getTodayDate = () => { return today.toISOString().split("T")[0]; }; -const mockCardData = [ - { - id: 1, - emoji: "🏔️", - title: "한라산 등반하기", - description: "한라산 정상까지 등반하며 자연을 느껴보세요.", - extra: [ - { id: 101, title: "등산 준비물", content: "나는 등산 짱 ㅋㅋ" }, - { id: 102, title: "소요 시간", content: "소요" } - ], - backgroundColor: "#F9FFD6" - }, - { - id: 2, - emoji: "📖", - title: "도서관에서 독서하기", - description: "조용한 도서관에서 책을 읽으며 집중하는 시간을 가져봐요.", - extra: [ - { id: 201, title: "추천 도서 목록", content: "도서" }, - { id: 202, title: "독서 시간", content: "시간" } - ], - backgroundColor: "#FFE7E7" - }, - { - id: 3, - emoji: "🍳", - title: "요리 도전하기", - description: "새로운 레시피로 요리에 도전해보세요.", - extra: [ - { id: 301, title: "필요한 재료", content: "필요한" }, - { id: 302, title: "조리법", content: "조리법" } - ], - backgroundColor: "#D5DDFF" - } -]; - export const useSelect = () => { const { goToPage } = useCustomNavigate(); @@ -61,10 +25,25 @@ export const useSelect = () => { const [endDate, setEndDate] = useState(getTodayDate()); useEffect(() => { - const shuffled = mockCardData.sort(() => 0.5 - Math.random()).slice(0, 3); - setCards(shuffled); - setSelectedCard(shuffled[1]); - setFlipped(new Array(shuffled.length).fill(false)); + const fetchCards = async () => { + try { + // 1. 랜덤 카드 3장 조회 + const randomResponse = await SelectService.getRandomCards(); + if (!randomResponse.success) throw new Error(randomResponse.message); + + // 2. 조회된 cardIds를 이용하여 개별 카드 정보 요청 + const cardDetailsPromises = randomResponse.cardIds.map(id => SelectService.getCardById(id).then((card) => ({ ...card, id }))); + const cardDetails = await Promise.all(cardDetailsPromises); + + setCards(cardDetails); + setSelectedCard(cardDetails[1]); // 기본 선택 카드 설정 + setFlipped(new Array(cardDetails.length).fill(false)); // 카드 뒷면 초기화 + } catch (error) { + console.error("🚨 카드 불러오기 실패:", error); + } + }; + + fetchCards(); }, []); const goToNextStep = () => setStep((prev) => prev + 1); @@ -121,14 +100,13 @@ export const useSelect = () => { } const requestData = { - emoji: selectedCard.emoji, - title: selectedCard.title, - description: selectedCard.description, + cardId: selectedCard.id, + cover: selectedCard.cover, startDate, - endDate, - extra: extraInputs, - backgroundColor: selectedBackgroundColor + endDate }; + + console.log("✅ 보낼 데이터:", requestData); try { await SelectService.submitExperience(requestData); diff --git a/src/pages/mainpage/MainPage.jsx b/src/pages/mainpage/MainPage.jsx index 92f0e88..4ca9d4a 100644 --- a/src/pages/mainpage/MainPage.jsx +++ b/src/pages/mainpage/MainPage.jsx @@ -3,28 +3,92 @@ import { useState, useEffect } from "react"; import Left from "/images/main/Left.svg"; import Right from "/images/main/Right.svg"; import Arrow from "/images/main/Arrow.svg"; +import warning from "/images/main/warning.svg"; +import Calendar from "/images/main/Calendar.svg"; import useCustomNavigate from "@hooks/useCustomNavigate"; +import { MainFooter } from "@components/layout/footer/mainfooter/MainFooter"; +import { instance } from "@services/instance"; import { UserService } from "@services/UserService"; +import { SelectService } from "@services/SelectService"; +import { ExperienceService } from "@services/ExperienceService"; +import { ChoiceModal } from "@components/common/choicemodal/ChoiceModal"; export const MainPage = () => { const { goToPage } = useCustomNavigate(); - const [userState, setUserState] = useState(null); // 받아온 state 저장 + const [userState, setUserState] = useState(null); + const [experience, setExperience] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isModalOpen2, setIsModalOpen2] = useState(false); + const [dDayText, setDDayText] = useState(""); useEffect(() => { const getUserState = async () => { try { const data = await UserService.fetchUserState(); console.log("✅ API 응답 데이터:", data.state); - setUserState(data.state); // `state` 값 저장 + setUserState(data.state); + + if (data.state === true) { + await fetchUserExperience(); + } } catch (error) { console.error("🚨 API 호출 실패"); } }; + const fetchUserExperience = async () => { + try { + const expData = await ExperienceService.getUserExperience(); + if (expData.success) { + setExperience(expData); + calculateDDay(expData.endDate); + } else { + console.warn("🚨 경험 조회 실패:", expData.message); + } + } catch (error) { + console.error("🚨 경험 데이터 불러오기 실패:", error); + } + }; + getUserState(); }, []); + const handleQuitExperience = async () => { + try { + console.log("📡 경험 종료 요청 중..."); + const response = await ExperienceService.quitExperience(); + console.log("✅ 경험 종료 성공:", response.data); + + // 경험 종료 후 두 번째 모달 열기 + setIsModalOpen(false); + setIsModalOpen2(true); + } catch (error) { + console.error("🚨 경험 종료 실패:", error.response?.data || error.message); + alert("경험 종료에 실패했습니다. 다시 시도해주세요."); + } + }; + + const calculateDDay = (endDate) => { + const today = new Date(); + const end = new Date(endDate); + const diff = Math.ceil((end - today) / (1000 * 60 * 60 * 24)); // 날짜 차이 계산 + + let dDayResult = ""; + if (diff === 0) { + dDayResult = "D-Day"; + } else if (diff > 0) { + dDayResult = `D-${diff}`; + } else { + dDayResult = "완료됨"; + } + + console.log(`📅 경험 종료일: ${endDate}, 오늘: ${today.toISOString().split("T")[0]}, 계산된 D-Day: ${dDayResult}`); + + setDDayText(dDayResult); + }; + + return ( {userState === null && ( @@ -65,28 +129,84 @@ export const MainPage = () => { )} - {userState === true && ( + {userState === true && experience && ( <> - + 현재 도전 중인, - 혼자 여행 떠나기 + {experience.title} - goToPage("/select")} + + setIsModalOpen(true)} > - 다른 경험 카드 열람하기 - - + + 현재 경험을 그만두고 싶어요 + + + + + + {experience.startDate} + ~ {experience.endDate} + + {dDayText} + + + goToPage("/home_check")} + > + 확인하기 + + + goToPage("/history")} + > + 기록하기 + + + + )} 경험의 가치를 알아봐요 + + + {isModalOpen && ( + setIsModalOpen(false)} + ContentTitle={""} + ContentSemiTitle={["CARDO의 방침은", "한 번 세운 계획은 끝까지 실천하는 거예요.", "..", "하지만 새로운 경험 카드를 작성하면", "다시 3장의 카드를 만날 수 있는 기회를 제공해요!"]} + LeftOnClick={() => setIsModalOpen(false)} + LeftContent="취소" + RightOnClick={handleQuitExperience} + RightContent="그만두기" + /> + )} + + {isModalOpen2 && ( + setIsModalOpen2(false)} + ContentTitle={"현재 진행 중인 경험을 그만두시겠어요?"} + ContentSemiTitle={"이 경험을 종료하면 다시 선택할 수 없어요."} + ContentContent={["'취소'를 원하시면 팝업창을 닫아주세요!"]} + LeftOnClick={() => setIsModalOpen2(false)} + LeftContent="메인으로" + RightOnClick={() => goToPage("/apply")} + RightContent="새 카드 등록하기" + /> + )} + ) } \ No newline at end of file diff --git a/src/pages/mainpage/styled.js b/src/pages/mainpage/styled.js index 4e0d7fe..1845a30 100644 --- a/src/pages/mainpage/styled.js +++ b/src/pages/mainpage/styled.js @@ -11,7 +11,7 @@ export const NoticeGroup = styled.div` display: flex; width: 100%; height: 120px; - background: ${({ theme }) => theme.colors.lightbluegray}; + background: ${({ theme, $Cover }) => $Cover || theme.colors.lightbluegray}; border-radius: 0 0 50px 50px; justify-content: space-evenly; `; @@ -37,12 +37,19 @@ export const NoticeContent2 = styled.div` font-size: 16px; `; +export const CardSubmitSet = styled.div` + display: flex; + width: 85%; + gap: 20px; +`; + export const CardSubmit = styled.div` display: flex; width: 85%; height: 60px; justify-content: center; align-items: center; + position: relative; border-radius: 25px; background: var(--linear, linear-gradient(90deg, #FCFFE1 0%, #FFE7E7 33%, #EFE5FF 66%, #D5DDFF 100%)); @@ -58,11 +65,86 @@ export const CardSubmitContent = styled.div` export const CardSubmitArrow = styled.img` width: 24px; position: absolute; - right: 30px; + right: 10px; `; export const SubTitle = styled.div` width: calc(90%); ${({ theme }) => theme.fonts.PretendardSB}; font-size: 16px; +`; + +export const Icon20 = styled.img` + width: 20px; +`; + +export const WarningText = styled.div` + ${({ theme }) => theme.fonts.PretendardR}; + font-size: 14px; + color: ${({ theme }) => theme.colors.graytext}; + cursor: pointer; +`; + +export const DateWrapper = styled.div` + display: flex; + width: 85%;; + position: relative; + height: 72px; + border-radius: 25px; + background-color: ${({ theme }) => theme.colors.lightbluegray}; +`; + +export const Icon33 = styled.img` + width: 33px; + position: absolute; + left: 5%; + top: 50%; + transform: translateY(-50%); +`; + +export const Dates = styled.div` + display: flex; + flex-direction: row; + position: absolute; + left: calc(7% + 33px); + top: 50%; + transform: translateY(-50%); + gap: 4px; +`; + +export const LeftDate = styled.div` + ${({ theme }) => theme.fonts.PretendardR}; + font-size: 12px; + font-weight: 300; + display: flex; + align-items: center; +`; + +export const RightDate = styled.div` + ${({ theme }) => theme.fonts.PretendardR}; + font-size: 14px; + font-weight: 500; + display: flex; + align-items: center; +`; + +export const DateBox = styled.div` + ${({ theme }) => theme.fonts.PretendardR}; + font-size: 20px; + font-weight: 500; + + width: 65px; + height: 36px; + + position: absolute; + right: calc(7%); + top: 50%; + transform: translateY(-50%); + + display: flex; + align-items: center; + justify-content: center; + border-radius: 12px; + background: ${({ theme }) => theme.colors.black}; + color: ${({ theme }) => theme.colors.white}; `; \ No newline at end of file diff --git a/src/services/ExperienceService.js b/src/services/ExperienceService.js new file mode 100644 index 0000000..12a3d0c --- /dev/null +++ b/src/services/ExperienceService.js @@ -0,0 +1,24 @@ +import { instance } from "./instance"; + +export const ExperienceService = { + // 📌 현재 진행 중인 경험 조회 API + getUserExperience: async () => { + try { + const response = await instance.get("/cards/experience"); + return response.data; + } catch (error) { + console.error("🚨 현재 진행 중인 경험 조회 실패:", error.response?.data || error.message); + throw error; + } + }, + + quitExperience: async () => { + try { + const response = await instance.put("/cards/experience/quit"); + return response.data; + } catch (error) { + console.error("🚨 경험 종료 실패:", error.response?.data || error.message); + throw error; + } + } +}; \ No newline at end of file