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