From 7893b2a6ea3f512aeccb52b8439a98f1428b91c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B8=EC=95=84?= Date: Fri, 3 Apr 2026 00:06:04 +0900 Subject: [PATCH 01/84] =?UTF-8?q?feat:=20=EC=9D=BC=EA=B8=B0=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=9D=BC=EA=B8=B0?= =?UTF-8?q?=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=88=98=EC=A0=95=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/diaries.js | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/apis/diaries.js b/src/apis/diaries.js index ed9fa74..d4e36a9 100644 --- a/src/apis/diaries.js +++ b/src/apis/diaries.js @@ -1,6 +1,17 @@ -import instance from "./axios"; +import instance from "./instance"; -// 일기 조회 +// 일기 목록 조회 +export const getDiariesList = async (params) => { + try { + const res = await instance.get(`/api/v1/diaries`, { params }); + return res.data; + } catch (error) { + console.error("일기 목록 조회 실패:", error); + throw error; + } +}; + +// 일기 단건 조회 export const getDiaries = async (id) => { try { const res = await instance.get(`/api/v1/diaries/${id}`); @@ -26,15 +37,11 @@ export const updateDiaries = async (id, updateData) => { }; // 일기 이미지 수정 -export const updateDiariesImg = async (diary_id, imageFile) => { +export const updateDiariesImg = async (diaryId, imageList) => { try { - const formData = new FormData(); - formData.append("file", imageFile); - const res = await instance.patch( - `/api/v1/diaries/${diary_id}/images`, - formData, - { headers: { "Content-Type": "multipart/form-data" } }, + `/api/v1/diaries/${diaryId}/images`, + imageList, ); return res.data; } catch (error) { @@ -44,13 +51,12 @@ export const updateDiariesImg = async (diary_id, imageFile) => { }; // 일기 삭제 -export const deleteDiaries = async (diary_id) => { +export const deleteDiaries = async (diaryId) => { try { - const res = await instance.delete(`/api/v1/diaries/${diary_id}`); + const res = await instance.delete(`/api/v1/diaries/${diaryId}`); return res.data; } catch (error) { console.error("일기 삭제 실패:", error); throw error; } }; - From be66a23dd5f3c28b3c4c7957477c21f5907b83ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B8=EC=95=84?= Date: Sun, 5 Apr 2026 02:37:54 +0900 Subject: [PATCH 02/84] =?UTF-8?q?fix:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20div=20=EB=86=92=EC=9D=B4=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20css=20=EC=A3=BC=EC=84=9D=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Home.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index bd3e709..334092c 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -92,7 +92,7 @@ const Body = styled.section``; const Container = styled.section` display: flex; - margin: 45px 60px; // 상하좌우 여백 축소 + margin: 45px 60px; gap: 30px; `; @@ -189,7 +189,7 @@ const StyledCalendar = styled(Calendar)` } .react-calendar__tile { - height: 75px; // 90px -> 80px + height: 76px; display: flex; flex-direction: column; align-items: center; @@ -330,7 +330,7 @@ const Anxiety = styled.li` const ImgContainer = styled.div` display: flex; flex-direction: column; - height: 210px; // 250px -> 210px 축소 + height: 230px; // 250px -> 210px 축소 background-color: #f3f3f3; border-radius: 12px; padding: 14px; From ac7c0f0e2ecc2bb14419616e69cda76ad75975aa Mon Sep 17 00:00:00 2001 From: pbc1001 Date: Sun, 5 Apr 2026 21:14:00 +0900 Subject: [PATCH 03/84] =?UTF-8?q?fix:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=EC=8B=9C=20=EB=A9=94=EC=9D=B8=EC=9C=BC=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=ED=95=98=EB=8A=94=20=EA=B2=BD=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD,=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=EC=8B=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EC=9D=B8=EC=A6=9D=ED=9B=84=20?= =?UTF-8?q?=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EB=B3=80=EA=B2=BD=EC=9D=B4?= =?UTF-8?q?=EB=B6=88=EA=B0=80=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/LogIn.jsx | 2 +- src/pages/SignUp.jsx | 23 ++++++++++++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/pages/LogIn.jsx b/src/pages/LogIn.jsx index 4bd0cec..426fc33 100644 --- a/src/pages/LogIn.jsx +++ b/src/pages/LogIn.jsx @@ -56,7 +56,7 @@ const Login = () => { } alert("로그인 성공!"); - navigate("/"); + navigate("/home"); } } catch (error) { alert( diff --git a/src/pages/SignUp.jsx b/src/pages/SignUp.jsx index eec1933..9a8e263 100644 --- a/src/pages/SignUp.jsx +++ b/src/pages/SignUp.jsx @@ -47,6 +47,7 @@ const SignUp = () => { }; const handleSendCode = async () => { + setMessage(""); if (!email.trim()) { setMessage("이메일을 입력해주세요"); return; @@ -134,6 +135,7 @@ const SignUp = () => { 이메일 + {!verifyToken?( { setShowCode(false); setTimeleft(180); }} + >):( + { + setEmail(e.target.value); + }} > - - 코드 발송 - + )} + {!verifyToken ? ( + + 코드 발송 + + ) : ( + + 인증 완료 + + )} {message} {showCode && ( From a1fac4c74f953cf9aca5028f6f759555e5ab02d0 Mon Sep 17 00:00:00 2001 From: pbc1001 Date: Sun, 5 Apr 2026 21:31:57 +0900 Subject: [PATCH 04/84] =?UTF-8?q?fix:=20=EA=B8=B0=EC=A1=B4=20=EB=B9=84?= =?UTF-8?q?=EB=B0=80=EB=B2=88=ED=98=B8=20=ED=91=9C=EC=8B=9C=EA=B0=80=20?= =?UTF-8?q?=EB=91=90=EA=B0=9C=EC=9D=98=20input=EC=97=90=20=EB=AA=A8?= =?UTF-8?q?=EB=91=90=20=EC=A0=81=EC=9A=A9=EB=90=98=EC=97=88=EB=8D=98=20?= =?UTF-8?q?=EA=B2=83=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EB=B3=80=EA=B2=BD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/openPw.svg | 3 +++ src/pages/SignUp.jsx | 10 ++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 src/assets/openPw.svg diff --git a/src/assets/openPw.svg b/src/assets/openPw.svg new file mode 100644 index 0000000..a87de43 --- /dev/null +++ b/src/assets/openPw.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/pages/SignUp.jsx b/src/pages/SignUp.jsx index 9a8e263..7104fb0 100644 --- a/src/pages/SignUp.jsx +++ b/src/pages/SignUp.jsx @@ -6,6 +6,7 @@ import Left from "../assets/Left.svg"; import Right from "../assets/Right.svg"; import Arrow from "../assets/Arrow.svg"; import CheckPassword from "../assets/SeePass.svg"; +import ClosePw from "../assets/openPw.svg" import { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; import { sendAuthcode, authCode, signUp } from "../apis/auth.js"; @@ -19,6 +20,7 @@ const SignUp = () => { const [authcode, setAuthcode] = useState(""); const [verifyToken, setVerifyToken] = useState(""); const [showPw, setShowPw] = useState(false); + const [showCheckPw, setShowCheckPw] = useState(false); const [showCode, setShowCode] = useState(false); const [timeleft, setTimeleft] = useState(180); const [message, setMessage] = useState(""); @@ -195,7 +197,7 @@ const SignUp = () => { placeholder="비밀번호를 입력해주세요" > { setShowPw(!showPw); }} @@ -208,15 +210,15 @@ const SignUp = () => { 비밀번호 확인 setCheckpw(e.target.value)} placeholder="비밀번호를 다시 입력해주세요" > { - setShowPw(!showPw); + setShowCheckPw(!showCheckPw); }} alt="비밀번호 표시" /> From 4714d2574e5ba79c4b2ca8c8174e65de7c7b4b4d Mon Sep 17 00:00:00 2001 From: pbc1001 Date: Sun, 5 Apr 2026 21:43:00 +0900 Subject: [PATCH 05/84] =?UTF-8?q?fix:=20setCheckMessage=EA=B0=80=20?= =?UTF-8?q?=EB=82=A8=EC=95=84=EC=9E=88=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/SignUp.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/SignUp.jsx b/src/pages/SignUp.jsx index 7104fb0..2190595 100644 --- a/src/pages/SignUp.jsx +++ b/src/pages/SignUp.jsx @@ -49,12 +49,13 @@ const SignUp = () => { }; const handleSendCode = async () => { + setCheckMessage(""); setMessage(""); if (!email.trim()) { setMessage("이메일을 입력해주세요"); return; } - setCheckMessage(""); + const type = "REGISTER"; try { From 8a07c7683f45e7484889b4ba9034777b479f65bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B8=EC=95=84?= Date: Tue, 7 Apr 2026 00:24:57 +0900 Subject: [PATCH 06/84] =?UTF-8?q?feat:=20api=20=EC=B1=84=ED=8C=85=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/chat.js | 79 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 src/apis/chat.js diff --git a/src/apis/chat.js b/src/apis/chat.js new file mode 100644 index 0000000..b8895ee --- /dev/null +++ b/src/apis/chat.js @@ -0,0 +1,79 @@ +import instance from "./instance"; + +// 채팅 생성 +export const createChat = async (chatData) => { + try { + const res = await instance.post(`/api/v1/chats`, chatData); + return res.data; + } catch (error) { + // 상세 에러 로깅 + console.error("상태 코드:", error.response?.status); + console.error("에러 메시지:", error.response?.data); + console.error("요청 헤더:", error.config?.headers); + throw error; + } +}; + +// 채팅 음성 생성 +export const createChatVoice = async (chatId, voiceData) => { + try { + const res = await instance.post(`/api/v1/chats/${chatId}/voice`, voiceData); + return res.data; + } catch (error) { + console.error("음성 생성 실패:", error); + throw error; + } +}; + +// 채팅 메시지 전송 +export const createChatMessage = async (chatId, messageData) => { + try { + const res = await instance.post( + `/api/v1/chats/${chatId}/messages`, + messageData, + ); + return res.data; + } catch (error) { + console.error("메시지 전송 실패:", error); + throw error; + } +}; + +// 채팅 수정 +export const updateChat = async (chatId) => { + try { + const res = await instance.patch(`/api/v1/chats/${chatId}`); + return res.data; + } catch (error) { + console.error("채팅 수정 실패:", error); + throw error; + } +}; + +// 대화 중 사진 추가 +export const createChatImage = async (chatId, imageData) => { + try { + const res = await instance.post( + `/api/v1/chats/${chatId}/images`, + imageData, + ); + return res.data; + } catch (error) { + console.error("이미지 생성 실패:", error); + throw error; + } +}; + +// AI 이미지 생성 요청 +export const createChatImageGeneration = async (chatId, generationData) => { + try { + const res = await instance.post( + `/api/v1/chats/${chatId}/images/generations`, + generationData, + ); + return res.data; + } catch (error) { + console.error("이미지 생성 실패:", error); + throw error; + } +}; From 91bfcb6019218d1707a30541fd24dda0c120ac82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B8=EC=95=84?= Date: Tue, 7 Apr 2026 01:20:31 +0900 Subject: [PATCH 07/84] =?UTF-8?q?feat:=20=EB=8B=A4=EC=9D=B4=EC=96=B4?= =?UTF-8?q?=EB=A6=AC=20=EC=B6=94=EC=B2=9C=20=EC=A1=B0=ED=9A=8C=20=EB=B0=8F?= =?UTF-8?q?=20=EB=8B=A4=EC=9D=B4=EC=96=B4=EB=A6=AC=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/diaries.js | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/apis/diaries.js b/src/apis/diaries.js index d4e36a9..7b959e7 100644 --- a/src/apis/diaries.js +++ b/src/apis/diaries.js @@ -60,3 +60,39 @@ export const deleteDiaries = async (diaryId) => { throw error; } }; + +// 다이어리 추천 조회 +export const getDiaryRecommendation = async () => { + try { + const res = await instance.get(`/api/v1/diaries/recommendation`); + return res.data; + } catch (error) { + console.error("상태 코드:", error.response?.status); + console.error("에러 메시지:", error.response?.data); + console.error("요청 헤더:", error.config?.headers); + throw error; + } +}; + +// 다이어리 목록 조회 +export const getDiariesList = async (requestParams) => { + try { + const res = await instance.get(`/api/v1/diaries`, { + params: { + imageType: requestParams?.imageType, + hasPhoto: requestParams?.hasPhoto, + yearMonth: requestParams?.yearMonth, + limit: requestParams?.limit, + sort: requestParams?.sort, + tag: requestParams?.tag, + resolvedYearMonth: requestParams?.resolvedYearMonth, + }, + }); + return res.data; + } catch (error) { + console.error("상태 코드:", error.response?.status); + console.error("에러 메시지:", error.response?.data); + console.error("요청 헤더:", error.config?.headers); + throw error; + } +}; From 158fe929b4cf3b4b0aa4a8a1ae536105f2e6c62e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B8=EC=95=84?= Date: Tue, 7 Apr 2026 01:22:55 +0900 Subject: [PATCH 08/84] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C,=20=EB=8B=AC=EB=A0=A5?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20=EC=97=B0=EB=8F=99=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/user.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/apis/user.js diff --git a/src/apis/user.js b/src/apis/user.js new file mode 100644 index 0000000..def1ccc --- /dev/null +++ b/src/apis/user.js @@ -0,0 +1,29 @@ +import instance from "./instance"; + +// 사용자 정보 조회 +export const getUserSummary = async () => { + try { + const res = await instance.get(`/api/v1/users/me/summary`); + return res.data; + } catch (error) { + console.error("사용자 요약 조회 실패:", error); + throw error; + } +}; + +// 캘린더 목록 조회 +export const getCalendars = async (yearMonth) => { + try { + const res = await instance.get(`/api/v1/users/me/calendars`, { + params: { + yearMonth, + }, + }); + return res.data; + } catch (error) { + console.error("상태 코드:", error.response?.status); + console.error("에러 메시지:", error.response?.data); + console.error("요청 헤더:", error.config?.headers); + throw error; + } +}; From 8407257eddff1ca0925232eeb47fc0155e09bf8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B8=EC=95=84?= Date: Tue, 7 Apr 2026 10:48:25 +0900 Subject: [PATCH 09/84] =?UTF-8?q?fix:=20=EC=A4=91=EB=B3=B5=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/diaries.js | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/apis/diaries.js b/src/apis/diaries.js index 7b959e7..a325325 100644 --- a/src/apis/diaries.js +++ b/src/apis/diaries.js @@ -73,26 +73,3 @@ export const getDiaryRecommendation = async () => { throw error; } }; - -// 다이어리 목록 조회 -export const getDiariesList = async (requestParams) => { - try { - const res = await instance.get(`/api/v1/diaries`, { - params: { - imageType: requestParams?.imageType, - hasPhoto: requestParams?.hasPhoto, - yearMonth: requestParams?.yearMonth, - limit: requestParams?.limit, - sort: requestParams?.sort, - tag: requestParams?.tag, - resolvedYearMonth: requestParams?.resolvedYearMonth, - }, - }); - return res.data; - } catch (error) { - console.error("상태 코드:", error.response?.status); - console.error("에러 메시지:", error.response?.data); - console.error("요청 헤더:", error.config?.headers); - throw error; - } -}; From 6c579faa0ff1ef5db1fa8ed03310ecd33799bfee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B8=EC=95=84?= Date: Tue, 7 Apr 2026 10:56:11 +0900 Subject: [PATCH 10/84] =?UTF-8?q?fix:=20=EB=AF=BC=EA=B0=90=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EB=A1=9C=EA=B9=85=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/diaries.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/apis/diaries.js b/src/apis/diaries.js index a325325..c93b32d 100644 --- a/src/apis/diaries.js +++ b/src/apis/diaries.js @@ -69,7 +69,6 @@ export const getDiaryRecommendation = async () => { } catch (error) { console.error("상태 코드:", error.response?.status); console.error("에러 메시지:", error.response?.data); - console.error("요청 헤더:", error.config?.headers); throw error; } }; From 0679d83df147658158d9f857679643b80adec960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B8=EC=95=84?= Date: Sun, 5 Apr 2026 02:51:54 +0900 Subject: [PATCH 11/84] =?UTF-8?q?feat:=20=EC=9D=BC=EA=B8=B0=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/diaries.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/apis/diaries.js b/src/apis/diaries.js index c93b32d..77b4f48 100644 --- a/src/apis/diaries.js +++ b/src/apis/diaries.js @@ -22,6 +22,17 @@ export const getDiaries = async (id) => { } }; +// 일기 목록 조회 +export const getDiariesList = async (params) => { + try { + const res = await instance.get(`/api/v1/diaries`, { params }); + return res.data; + } catch (error) { + console.error("일기 목록 조회 실패:", error); + throw error; + } +}; + // 일기 내용 수정 export const updateDiaries = async (id, updateData) => { try { From 5075c0feb610d5d9ff862ae02781b10564b618ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B8=EC=95=84?= Date: Tue, 7 Apr 2026 11:48:15 +0900 Subject: [PATCH 12/84] =?UTF-8?q?fix:=20=EC=A4=91=EB=B3=B5=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/diaries.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/apis/diaries.js b/src/apis/diaries.js index 77b4f48..2e327f8 100644 --- a/src/apis/diaries.js +++ b/src/apis/diaries.js @@ -1,16 +1,5 @@ import instance from "./instance"; -// 일기 목록 조회 -export const getDiariesList = async (params) => { - try { - const res = await instance.get(`/api/v1/diaries`, { params }); - return res.data; - } catch (error) { - console.error("일기 목록 조회 실패:", error); - throw error; - } -}; - // 일기 단건 조회 export const getDiaries = async (id) => { try { From df70272f818ab9afe80c9bfe97e37522484e92fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B8=EC=95=84?= Date: Tue, 7 Apr 2026 11:58:40 +0900 Subject: [PATCH 13/84] =?UTF-8?q?fix:=20=EB=AF=BC=EA=B0=90=ED=95=9C=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EB=A9=94=ED=83=80=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=BD=98=EC=86=94=20=EB=85=B8=EC=B6=9C=EC=9D=84=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/user.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/apis/user.js b/src/apis/user.js index def1ccc..2a06c84 100644 --- a/src/apis/user.js +++ b/src/apis/user.js @@ -6,7 +6,9 @@ export const getUserSummary = async () => { const res = await instance.get(`/api/v1/users/me/summary`); return res.data; } catch (error) { - console.error("사용자 요약 조회 실패:", error); + const status = error.response?.status; + const message = error.response?.data?.message ?? error.message; + console.error("사용자 요약 조회 실패", { status, message }); throw error; } }; From 04c92d7c139e9feb5060c32b78208dc425a4f555 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B8=EC=95=84?= Date: Tue, 7 Apr 2026 12:02:22 +0900 Subject: [PATCH 14/84] =?UTF-8?q?fix:=20=EC=97=90=EB=9F=AC=20=EC=B6=9C?= =?UTF-8?q?=EB=A0=A5=20=EB=B0=A9=EB=B2=95=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/chat.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/apis/chat.js b/src/apis/chat.js index b8895ee..6eeed13 100644 --- a/src/apis/chat.js +++ b/src/apis/chat.js @@ -1,5 +1,11 @@ import instance from "./instance"; +const logApiError = (label, error) => { + const status = error?.response?.status; + const message = error?.response?.data?.message ?? error?.message; + console.error(label, { status, message }); +}; + // 채팅 생성 export const createChat = async (chatData) => { try { @@ -7,9 +13,7 @@ export const createChat = async (chatData) => { return res.data; } catch (error) { // 상세 에러 로깅 - console.error("상태 코드:", error.response?.status); - console.error("에러 메시지:", error.response?.data); - console.error("요청 헤더:", error.config?.headers); + logApiError("채팅 생성 실패:", error); throw error; } }; @@ -20,7 +24,7 @@ export const createChatVoice = async (chatId, voiceData) => { const res = await instance.post(`/api/v1/chats/${chatId}/voice`, voiceData); return res.data; } catch (error) { - console.error("음성 생성 실패:", error); + logApiError("음성 생성 실패:", error); throw error; } }; @@ -34,7 +38,7 @@ export const createChatMessage = async (chatId, messageData) => { ); return res.data; } catch (error) { - console.error("메시지 전송 실패:", error); + logApiError("메시지 전송 실패:", error); throw error; } }; @@ -45,7 +49,7 @@ export const updateChat = async (chatId) => { const res = await instance.patch(`/api/v1/chats/${chatId}`); return res.data; } catch (error) { - console.error("채팅 수정 실패:", error); + logApiError("채팅 수정 실패:", error); throw error; } }; @@ -59,7 +63,7 @@ export const createChatImage = async (chatId, imageData) => { ); return res.data; } catch (error) { - console.error("이미지 생성 실패:", error); + logApiError("이미지 생성 실패:", error); throw error; } }; @@ -73,7 +77,7 @@ export const createChatImageGeneration = async (chatId, generationData) => { ); return res.data; } catch (error) { - console.error("이미지 생성 실패:", error); + logApiError("이미지 생성 실패:", error); throw error; } }; From b59ce324b22482c257ac09fa05e56483b8454ed9 Mon Sep 17 00:00:00 2001 From: pbc1001 Date: Thu, 9 Apr 2026 00:39:25 +0900 Subject: [PATCH 15/84] =?UTF-8?q?fix:=20=ED=97=A4=EB=8D=94=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=EC=8B=9C=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/auth.js | 1 - src/components/Header.jsx | 39 +++++++++++++++++++++++++-------------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/apis/auth.js b/src/apis/auth.js index 4a6d29e..fb0dc50 100644 --- a/src/apis/auth.js +++ b/src/apis/auth.js @@ -20,7 +20,6 @@ export const login = async (userData) => { return response.data; }; -// apis/auth.js export const logout = async (refreshToken) => { const response = await instance.post('/api/v1/auth/logout', { diff --git a/src/components/Header.jsx b/src/components/Header.jsx index 78780a7..7e11742 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -12,25 +12,36 @@ const Header = () => { useEffect(() => { const token = localStorage.getItem("accessToken"); - setIsLogin(!!token); - }, [location.pathname]); - + setIsLogin(!!token); + }, [location.pathname]); + // 로그아웃 등 storage 변경 감지 + useEffect(() => { + const handleStorage = () => { + const token = localStorage.getItem("accessToken"); + setIsLogin(!!token); + }; + window.addEventListener("storage", handleStorage); + return () => window.removeEventListener("storage", handleStorage); + }, []); return ( navigate("/")}> HEAR_FOR_YOU - - - navigate("/home")} isActive={location.pathname === "/home"}>홈 - navigate("/ai/chats")} isActive={location.pathname === "/ai/chats"}>AI일기 - navigate("/photobook")} isActive={location.pathname === "/photobook"}>사진첩 - navigate("/statics")} isActive={location.pathname === "/statics"}>통계 - - - + + {/* 로그인 시에만 표시 */} + {isLogin && ( + + navigate("/home")} isActive={location.pathname === "/home"}>홈 + navigate("/ai/chats")} isActive={location.pathname === "/ai/chats"}>AI일기 + navigate("/photobook")} isActive={location.pathname === "/photobook"}>사진첩 + navigate("/statics")} isActive={location.pathname === "/statics"}>통계 + + )} + + {isLogin ? ( navigate("/mypage")}> @@ -103,10 +114,8 @@ const MenuText = styled.p` `; const HeaderRight = styled.div` - /* 고정 너비보다는 내부 콘텐츠에 맞게 늘어나도록 처리하는 게 깔끔합니다 */ min-width: 81px; height: 35px; - background-color: #ffe39a; border-radius: 50px; display: flex; align-items: center; @@ -114,6 +123,8 @@ const HeaderRight = styled.div` padding: 8px 12px; flex-shrink: 0; cursor: pointer; + + background-color: ${(props) => (props.isLogin ? "transparent" : "#ffe39a")}; `; const Login_complete = styled.div` display: flex; From eb54317dfde1056881ed2b8d8add15a358c29ae4 Mon Sep 17 00:00:00 2001 From: pbc1001 Date: Mon, 6 Apr 2026 09:33:03 +0900 Subject: [PATCH 16/84] =?UTF-8?q?fix:=20useState=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EB=8F=99=EA=B8=B0=20=EB=AC=B8=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/SignUp.jsx | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/pages/SignUp.jsx b/src/pages/SignUp.jsx index 2190595..e779fa1 100644 --- a/src/pages/SignUp.jsx +++ b/src/pages/SignUp.jsx @@ -28,19 +28,25 @@ const SignUp = () => { const [pwMessage, setPwMessage] = useState(""); const [checkPwMessage, setCheckPwMessage] = useState(""); - useEffect(() => { - let timer; - if (showCode && timeleft > 0) { - timer = setInterval(() => { - setTimeleft((prev) => prev - 1); - }, 1000); - } else if (timeleft === 0) { - alert("인증 시간이 만료되었습니다. 다시 시도해주세요."); - setShowCode(false); - setTimeleft(180); - } - return () => clearInterval(timer); - }, [showCode, timeleft]); +useEffect(() => { + let timer; + + if (showCode && timeleft > 0) { + timer = setInterval(() => { + setTimeleft((prev) => { + if (prev <= 1) { + clearInterval(timer); + alert("인증 시간이 만료되었습니다. 다시 시도해주세요."); + setShowCode(false); + return 180; + } + return prev - 1; + }); + }, 1000); + } + + return () => clearInterval(timer); +}, [showCode]); const formatTime = (seconds) => { const minutes = Math.floor(seconds / 60); From f8e39680e70a4076ab9248c133711d57f863180c Mon Sep 17 00:00:00 2001 From: pbc1001 Date: Tue, 7 Apr 2026 19:47:37 +0900 Subject: [PATCH 17/84] =?UTF-8?q?feat:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20api=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 회원 정보 불러오기 - 회원 탈퇴(미완) - 비밀번호 변경(미완) - 프로필 변경(미완) --- src/apis/instance.js | 11 +- src/apis/mypages.api.js | 36 +++ src/pages/MyPage.jsx | 526 +++++++++++++++++++++++++++------------- 3 files changed, 394 insertions(+), 179 deletions(-) create mode 100644 src/apis/mypages.api.js diff --git a/src/apis/instance.js b/src/apis/instance.js index 798c0de..a6ce6f2 100644 --- a/src/apis/instance.js +++ b/src/apis/instance.js @@ -22,13 +22,16 @@ const processQueue = (error, token = null) => { instance.interceptors.request.use( (config) => { - const accessToken = localStorage.getItem("accessToken"); - if (accessToken) { - config.headers.Authorization = `Bearer ${accessToken}`; + const token = localStorage.getItem("accessToken"); + + if (token) { + config.headers.Authorization = `Bearer ${token}`; } return config; }, - (error) => Promise.reject(error) + (error) => { + return Promise.reject(error); + } ); instance.interceptors.response.use( diff --git a/src/apis/mypages.api.js b/src/apis/mypages.api.js new file mode 100644 index 0000000..662d680 --- /dev/null +++ b/src/apis/mypages.api.js @@ -0,0 +1,36 @@ +import instance from "./instance"; + +export const getMyInfo = async () => { + const response = await instance.get("/api/v1/users/me"); + return response.data; +}; + +export const withdrawAccount = async () => { + const response = await instance.delete("/api/v1/auth/me"); + return response.data; +}; + +export const updateProfile = async (formData) => { + const response = await instance.patch('/api/v1/users/me/profile', formData, { + headers: { + "Content-Type": "multipart/form-data", + }, + }); + return response.data; +}; + +export const changePassword = async (passwordData) => { + const response = await instance.patch('/api/v1/users/me/password', passwordData); + console.log(passwordData) + return response.data; +}; + +export const deleteUser = async () => { + const refreshToken = localStorage.getItem("refreshToken"); + const response = await instance.delete('/api/v1/users/me', { + data: { + refreshToken: refreshToken + } + }); + return response.data; +}; \ No newline at end of file diff --git a/src/pages/MyPage.jsx b/src/pages/MyPage.jsx index f5c4018..bf85d36 100644 --- a/src/pages/MyPage.jsx +++ b/src/pages/MyPage.jsx @@ -1,5 +1,3 @@ -// 마이페이지 - import styled from "@emotion/styled"; import Header from "../components/Header.jsx"; import Happy from "../assets/Happy.svg"; @@ -8,31 +6,173 @@ import Profile from "../assets/Profile.svg"; import ReverseArrow from "../assets/Rev-Arrow.svg"; import Sad from "../assets/Sadness.svg"; import Arrow from "../assets/Arrow.svg"; -import Random from "../assets/random.svg" -import Upload_btnimg from "../assets/Upload_btn.svg" -import { useState } from "react"; +import Random from "../assets/random.svg"; +import Upload_btnimg from "../assets/Upload_btn.svg"; +import { useState, useEffect, useRef } from "react"; import { useNavigate } from "react-router-dom"; +import { getMyInfo, updateProfile, changePassword, deleteUser } from "../apis/mypages.api.js"; const Mypage = () => { - - const user_info = { - "status": "success", - "data": { - "userId": 102, - "email": "seungri@example.com", - "nickname": "쿠수리", - "profileImageUrl": "https://s3.../profiles/user_102.png", - "createdAt": "2025-06-09T10:00:00Z", - "updatedAt": "2026-03-04T23:55:00Z" - } -} + const [userInfo, setUserInfo] = useState(null); + const [isLoading, setIsLoading] = useState(true); const [isChangeModal, setIsChangeModal] = useState(false); const [isDeleteModal, setIsDeleteModal] = useState(false); const [isPwModal, setIsPwModal] = useState(false); - const navigate = useNavigate() - + const [newNickname, setNewNickname] = useState(""); + const [previewImage, setPreviewImage] = useState(null); + const [selectedFile, setSelectedFile] = useState(null); + + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + + const fileInputRef = useRef(null); + const navigate = useNavigate(); + + useEffect(() => { + const fetchUserData = async () => { + try { + const res = await getMyInfo(); + setUserInfo(res.data); + } catch (error) { + console.error("유저 정보 로드 실패:", error); + } finally { + setIsLoading(false); + } + }; + fetchUserData(); + }, []); + console.log(userInfo) + + const [password, setPassword] = useState(""); + + const handleOpenEditModal = () => { + if (!userInfo) return; + setNewNickname(userInfo.nickname); + setPreviewImage(userInfo.profileImageUrl || Profile); + setSelectedFile(null); + setIsChangeModal(true); + }; + + const handleOpenDeleteModal = () => { + if (!userInfo) return; + setIsDeleteModal(true); + }; + + const handleOpenPwModal = () => { + if (!userInfo) return; + setConfirmPassword(""); + setPassword("") + setNewPassword("") + setIsPwModal(true); + }; + + const handleFileChange = (e) => { + const file = e.target.files[0]; + if (file) { + setSelectedFile(file); + const reader = new FileReader(); + reader.onloadend = () => { + setPreviewImage(reader.result); + }; + reader.readAsDataURL(file); + } + }; + + const handleSaveProfile = async () => { + if (!newNickname.trim()) return alert("닉네임을 입력해주세요."); + + try { + const formData = new FormData(); + + formData.append("nickname", newNickname); + + if (selectedFile) { + formData.append("profileImage", selectedFile); + } + + const res = await updateProfile(formData); + + if (res.status === "success") { + alert("프로필이 성공적으로 변경되었습니다."); + + setUserInfo((prev) => ({ + ...prev, + nickname: newNickname, + profileImageUrl: previewImage, + })); + + setIsChangeModal(false); + } + } catch (error) { + console.error("수정 실패:", error); + alert("수정 중 오류가 발생했습니다. 서버 설정을 확인해주세요."); + } + }; + + const handleChangePassword = async () => { + if (!newPassword || newPassword !== confirmPassword) { + return alert("비밀번호가 일치하지 않거나 입력되지 않았습니다."); + } + + try { + const res = await changePassword({ + oldPassword: password, + newPassword: newPassword, + confirmPassword: confirmPassword + + }); + if (res.status === "success") { + alert("비밀번호가 변경되었습니다."); + isPwModal(false) + } + } catch (error) { + alert( + "비밀번호 변경 실패: " + (error.response?.data?.message || "오류 발생"), + ); + } + }; + +const handleDeleteAccount = async () => { + try { + const res = await deleteUser(); + if (res.status === "success" || res.code === 200) { + alert("탈퇴가 완료되었습니다."); + localStorage.clear(); + navigate("/"); + window.location.reload(); + } + } catch (error) { + console.error("탈퇴 에러 상세:", error.response?.data); + alert(error.response?.data?.message || "탈퇴 처리 중 오류가 발생했습니다."); + } +}; + + const handleLogout = () => { + if (window.confirm("로그아웃 하시겠습니까?")) { + localStorage.removeItem("accessToken"); + localStorage.removeItem("refreshToken"); + navigate("/"); + window.location.reload(); + } + }; + + if (isLoading) + return ( + +
+
로딩 중...
+ + ); + if (!userInfo) + return ( + +
+
정보를 불러올 수 없습니다.
+ + ); + return (
@@ -40,22 +180,27 @@ const Mypage = () => { - {setIsChangeModal(true)}}> + - + Profile - - - - {user_info.data.nickname} - {setIsChangeModal(true)}}> - + {userInfo.nickname} + + Edit - {user_info.data.email} + {userInfo.email} @@ -67,14 +212,16 @@ const Mypage = () => { 비밀번호 변경 - {setIsPwModal(true)}}> - + + Change - {navigate("/")}}>로그아웃 - {setIsDeleteModal(true)}}>회원 탈퇴 + 로그아웃 + + 회원 탈퇴 + @@ -93,89 +240,125 @@ const Mypage = () => { - {/*계정 탈퇴 모달*/} -{isDeleteModal && - {setIsDeleteModal(false)}}> - {e.stopPropagation()}}> - - - - - - 정말 탈퇴하시겠어요? - - 지금 탈퇴하시면 작성하신 일기 추억들을 다시 볼 수 없게됩니다.{" "} - 그래도 탈퇴를 진행하시겠어요? - - - - - - - {navigate("/")}}>회원탈퇴 - {setIsDeleteModal(false)}}>취소 - - - -} + {/* 계정 탈퇴 모달 */} + {isDeleteModal && ( + setIsDeleteModal(false)}> + e.stopPropagation()}> + + + Sad + + + 정말 탈퇴하시겠어요? + + 지금 탈퇴하시면 작성하신 일기 추억들을 다시 볼 수 없게됩니다.{" "} + 그래도 탈퇴를 진행하시겠어요? + + + + + + 회원탈퇴 + + setIsDeleteModal(false)}> + 취소 + + + + + )} {/* 비밀번호 변경 모달 */} - {isPwModal&& - {setIsPwModal(false)}}> - {e.stopPropagation()}}> - {setIsPwModal(false)}}> - - - - + {isPwModal && ( + setIsPwModal(false)}> + e.stopPropagation()}> + setIsPwModal(false)}> + Back + + + - 비밀번호 - - - - 비밀번호 확인 - - - - {setIsPwModal(false)}}>저장 - - - -} - - {/*프로필 변경 모달*/} - {isChangeModal && - {setIsChangeModal(false)}}> - {e.stopPropagation()}}> - {setIsChangeModal(false)}}> - - - - - - - - - - - - - - 닉네임 변경 - - - - - - - 파일 업로드 - - {setIsChangeModal(false)}}>저장 - - - - -} + 기존 비밀번호 + setPassword(e.target.value)} + /> + + + 비밀번호 + setNewPassword(e.target.value)} + /> + + + 비밀번호 확인 + setConfirmPassword(e.target.value)} + /> + + + + 저장 + + + + + )} + + {/* 프로필 변경 모달 */} + {isChangeModal && ( + setIsChangeModal(false)}> + e.stopPropagation()}> + setIsChangeModal(false)}> + Close + + + + + Preview + + Random + + + + 닉네임 변경 + setNewNickname(e.target.value)} + placeholder="변경할 닉네임을 입력해주세요" + /> + + + + + fileInputRef.current.click()}> + + 파일 업로드 + + 저장 + + + + + )} ); }; @@ -230,25 +413,22 @@ const Profile_img = styled.div` height: 60px; border: 2px solid #e0e0e0; border-radius: 50px; - background-color: #cfcfcf; display: flex; align-items: center; justify-content: center; position: relative; + overflow: hidden; cursor: pointer; `; const Img_box = styled.div` - width: 40px; - height: 40px; + width: 60px; + height: 60px; + background-color: #cfcfcf; display: flex; align-items: center; justify-content: center; `; -const Img_edit = styled.div` - position: absolute; - bottom: -4px; - right: 2px; -`; + const Nickname_box = styled.div` display: flex; flex-direction: column; @@ -427,7 +607,7 @@ const Text_box = styled.div` display: flex; flex-direction: column; align-items: center; -` +`; const Check_title = styled.div` font-size: 20px; font-weight: 600; @@ -494,7 +674,7 @@ const Password_back = styled.div` const Password_modal = styled.div` position: relative; width: 480px; - height: 352px; + height: 432px; display: flex; flex-direction: column; align-items: center; @@ -512,7 +692,7 @@ const Out_modal = styled.div` cursor: pointer; `; const Change_box = styled.div` - display: flex ; + display: flex; flex-direction: column; justify-content: space-between; width: 100%; @@ -525,7 +705,7 @@ const Input_box = styled.div` display: flex; flex-direction: column; gap: 16px; -` +`; const Pass_box = styled.div` width: 100%; height: 100%; @@ -539,16 +719,16 @@ const Change_pass = styled.p` color: #575141; `; const Pass_input = styled.input` -width: 100%; -height: 100%; -font-weight: 500; -font-size: 16px; -border: 1px solid #CFD3DC; -padding: 9.5px 16px; -border-radius: 12px; -::placeholder { - color: #CFD3DC; -} + width: 100%; + height: 100%; + font-weight: 500; + font-size: 16px; + border: 1px solid #cfd3dc; + padding: 9.5px 16px; + border-radius: 12px; + ::placeholder { + color: #cfd3dc; + } `; const Checking_box = styled.div` width: 100%; @@ -556,7 +736,6 @@ const Checking_box = styled.div` display: flex; flex-direction: column; gap: 4px; - `; const Check_pass = styled.div` font-size: 16px; @@ -564,16 +743,16 @@ const Check_pass = styled.div` color: #575141; `; const Check_input = styled.input` -width: 100%; -height: 100%; -font-weight: 500; -font-size: 16px; -border: 1px solid #CFD3DC; -padding: 9.5px 16px; -border-radius: 12px; -::placeholder { - color: #CFD3DC; -} + width: 100%; + height: 100%; + font-weight: 500; + font-size: 16px; + border: 1px solid #cfd3dc; + padding: 9.5px 16px; + border-radius: 12px; + ::placeholder { + color: #cfd3dc; + } `; const Save_button = styled.div` width: 100%; @@ -583,7 +762,7 @@ const Save_button = styled.div` justify-content: center; font-size: 16px; font-weight: 600; - background-color: #FCD671; + background-color: #fcd671; color: #575141; border-radius: 12px; cursor: pointer; @@ -600,7 +779,7 @@ const Profile_modalback = styled.div` display: flex; align-items: center; justify-content: center; -` +`; const Profile_modal = styled.div` width: 480px; @@ -611,7 +790,7 @@ const Profile_modal = styled.div` display: flex; flex-direction: column; border-radius: 12px; -` +`; const Pf_modalmain = styled.div` width: 100%; height: 100%; @@ -619,79 +798,75 @@ const Pf_modalmain = styled.div` flex-direction: column; justify-content: space-between; gap: 32px; -` +`; const Profile_change = styled.div` gap: 24px; display: flex; flex-direction: column; align-items: center; -` +`; const Change_imgbox = styled.div` width: 60px; height: 60px; - background-color: #CFCFCF; - border: 2px solid #E0E0E0; + background-color: #cfcfcf; + border: 2px solid #e0e0e0; border-radius: 50px; display: flex; align-items: center; justify-content: center; - position: relative; box-sizing: border-box; -` -const Change_img = styled.div` -width: 40px; -height: 40px; -` + overflow: hidden; +`; + const Profile_rand = styled.div` position: absolute; - right: -7px; - bottom: -7px; + right: 210px; + top: 105px; width: 22px; height: 22px; display: flex; background-color: #fff; - border: 1px solid #F3F3F3; + border: 1px solid #f3f3f3; border-radius: 12px; box-sizing: border-box; align-items: center; justify-content: center; -` +`; const Change_nickbox = styled.div` width: 100%; display: flex; flex-direction: column; gap: 4px; -` +`; const Nick_text = styled.p` font-size: 16px; font-weight: 500; color: #575141; -` +`; const Nick_input = styled.input` width: 100%; height: auto; padding: 9.5px 16px; font-size: 16px; font-weight: 500; - border: 1px solid #CFD3DC; + border: 1px solid #cfd3dc; border-radius: 12px; - ::placeholder{ - color: #CFD3DC; + ::placeholder { + color: #cfd3dc; } - :focus{ + :focus { outline: none; } -` +`; const Profile_btnbox = styled.div` width: 100%; height: 100%; display: flex; gap: 12px; - -` +`; const Img_upload = styled.button` width: 100%; - border: 2px solid #FCD671; + border: 2px solid #fcd671; display: flex; gap: 10px; align-items: center; @@ -702,7 +877,8 @@ const Img_upload = styled.button` font-size: 16px; border-radius: 12px; box-sizing: border-box; -` + cursor: pointer; +`; const Save_btn = styled.button` width: 100%; background-color: #fcd671; @@ -716,6 +892,6 @@ const Save_btn = styled.button` font-size: 16px; border-radius: 12px; cursor: pointer; -` +`; -export default Mypage; \ No newline at end of file +export default Mypage; From b2cca776c5d8a8265c403286e4d3f33719b1366b Mon Sep 17 00:00:00 2001 From: pbc1001 Date: Thu, 9 Apr 2026 20:28:44 +0900 Subject: [PATCH 18/84] =?UTF-8?q?fix:=20=ED=86=A0=ED=81=B0=20=EB=A7=8C?= =?UTF-8?q?=EB=A3=8C=EC=8B=9C=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20?= =?UTF-8?q?=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 17 +++++++++++++++++ src/apis/instance.js | 2 +- src/pages/LogIn.jsx | 1 + 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/App.jsx b/src/App.jsx index 4c45bcf..16c82ed 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -9,8 +9,25 @@ import Mypage from "./pages/MyPage.jsx"; import Statics from "./pages/Statics.jsx"; import Home from "./pages/Home.jsx"; import EditDiary from "./pages/EditDiary.jsx"; +import { useEffect } from "react"; +import { useNavigate } from "react-router-dom"; function App() { + + const navigate = useNavigate(); + useEffect(() => { + const interval = setInterval(() => { + const expiry = localStorage.getItem("tokenExpiry"); + if (expiry && Date.now() > expiry) { + alert("세션이 만료되었습니다. 다시 로그인해주세요."); + localStorage.clear(); + navigate("/login"); + } + }, 60000); + + return () => clearInterval(interval); +}, []); + return ( } /> diff --git a/src/apis/instance.js b/src/apis/instance.js index 798c0de..9e01023 100644 --- a/src/apis/instance.js +++ b/src/apis/instance.js @@ -17,7 +17,7 @@ const processQueue = (error, token = null) => { prom.resolve(token); } }); - failedQueue = []; + failedQueue = []; }; instance.interceptors.request.use( diff --git a/src/pages/LogIn.jsx b/src/pages/LogIn.jsx index 426fc33..f48e5ed 100644 --- a/src/pages/LogIn.jsx +++ b/src/pages/LogIn.jsx @@ -53,6 +53,7 @@ const Login = () => { localStorage.setItem("accessToken", accessToken); if (refreshToken) { localStorage.setItem("refreshToken", refreshToken); + localStorage.setItem("tokenExpiry", Date.now() + 60 * 60 * 1000); } alert("로그인 성공!"); From 18023a006979138827a6ee7089f3d467201c605b Mon Sep 17 00:00:00 2001 From: pbc1001 Date: Fri, 10 Apr 2026 00:06:10 +0900 Subject: [PATCH 19/84] =?UTF-8?q?fix:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20api=20=EC=97=B0=EB=8F=99=20(=EC=99=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 회원정보 불러오기 - 회원 탈퇴 - 비번 변경 - 프로필 변경 --- src/apis/mypages.api.js | 10 +++++- src/pages/MyPage.jsx | 78 ++++++++++++++++++++++------------------- 2 files changed, 51 insertions(+), 37 deletions(-) diff --git a/src/apis/mypages.api.js b/src/apis/mypages.api.js index 662d680..29fdef3 100644 --- a/src/apis/mypages.api.js +++ b/src/apis/mypages.api.js @@ -13,7 +13,7 @@ export const withdrawAccount = async () => { export const updateProfile = async (formData) => { const response = await instance.patch('/api/v1/users/me/profile', formData, { headers: { - "Content-Type": "multipart/form-data", + "Content-Type": undefined, }, }); return response.data; @@ -33,4 +33,12 @@ export const deleteUser = async () => { } }); return response.data; +}; + +export const logout = async () => { + const refreshToken = localStorage.getItem("refreshToken"); + const response = await instance.post('/api/v1/auth/logout', { + refreshToken: refreshToken + }); + return response.data; }; \ No newline at end of file diff --git a/src/pages/MyPage.jsx b/src/pages/MyPage.jsx index bf85d36..99a0da4 100644 --- a/src/pages/MyPage.jsx +++ b/src/pages/MyPage.jsx @@ -10,7 +10,7 @@ import Random from "../assets/random.svg"; import Upload_btnimg from "../assets/Upload_btn.svg"; import { useState, useEffect, useRef } from "react"; import { useNavigate } from "react-router-dom"; -import { getMyInfo, updateProfile, changePassword, deleteUser } from "../apis/mypages.api.js"; +import { getMyInfo, updateProfile, changePassword, deleteUser, logout } from "../apis/mypages.api.js"; const Mypage = () => { const [userInfo, setUserInfo] = useState(null); @@ -43,7 +43,6 @@ const Mypage = () => { }; fetchUserData(); }, []); - console.log(userInfo) const [password, setPassword] = useState(""); @@ -80,36 +79,36 @@ const Mypage = () => { } }; - const handleSaveProfile = async () => { - if (!newNickname.trim()) return alert("닉네임을 입력해주세요."); +const handleSaveProfile = async () => { + if (!newNickname.trim()) return alert("닉네임을 입력해주세요."); - try { - const formData = new FormData(); - - formData.append("nickname", newNickname); - - if (selectedFile) { - formData.append("profileImage", selectedFile); - } - - const res = await updateProfile(formData); - - if (res.status === "success") { - alert("프로필이 성공적으로 변경되었습니다."); - - setUserInfo((prev) => ({ - ...prev, - nickname: newNickname, - profileImageUrl: previewImage, - })); + try { + const formData = new FormData(); - setIsChangeModal(false); - } - } catch (error) { - console.error("수정 실패:", error); - alert("수정 중 오류가 발생했습니다. 서버 설정을 확인해주세요."); + const jsonBlob = new Blob( + [JSON.stringify({ nickname: newNickname })], + { type: "application/json" } + ); + + formData.append("data", jsonBlob); + formData.append("image", selectedFile ? selectedFile : null); + + const res = await updateProfile(formData); + + if (res.status === 200) { + alert("프로필이 성공적으로 변경되었습니다."); + setUserInfo((prev) => ({ + ...prev, + nickname: newNickname, + profileImageUrl: previewImage, + })); + setIsChangeModal(false); } - }; + } catch (error) { + console.error("수정 실패:", error); + alert("수정 중 오류가 발생했습니다."); + } +}; const handleChangePassword = async () => { if (!newPassword || newPassword !== confirmPassword) { @@ -123,9 +122,9 @@ const Mypage = () => { confirmPassword: confirmPassword }); - if (res.status === "success") { + if (res.status === 200) { alert("비밀번호가 변경되었습니다."); - isPwModal(false) + setIsPwModal(false); } } catch (error) { alert( @@ -137,7 +136,7 @@ const Mypage = () => { const handleDeleteAccount = async () => { try { const res = await deleteUser(); - if (res.status === "success" || res.code === 200) { + if (res.status === 200) { alert("탈퇴가 완료되었습니다."); localStorage.clear(); navigate("/"); @@ -149,14 +148,21 @@ const handleDeleteAccount = async () => { } }; - const handleLogout = () => { - if (window.confirm("로그아웃 하시겠습니까?")) { + +const handleLogout = async () => { + if (window.confirm("로그아웃 하시겠습니까?")) { + try { + await logout(); localStorage.removeItem("accessToken"); localStorage.removeItem("refreshToken"); navigate("/"); window.location.reload(); - } - }; + } catch (error) { + console.error("로그아웃 실패:", error); + alert("로그아웃 실패") + } + } +}; if (isLoading) return ( From 01992ab512f1a0613f95f47835a9c9c61bc7638c Mon Sep 17 00:00:00 2001 From: pbc1001 Date: Fri, 10 Apr 2026 00:15:56 +0900 Subject: [PATCH 20/84] =?UTF-8?q?fix:=20=EB=A7=88=EC=9A=B4=ED=8A=B8=20?= =?UTF-8?q?=EC=A7=81=ED=9B=84=201=ED=9A=8C=20=EA=B2=80=EC=82=AC=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 16c82ed..62ec751 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -16,17 +16,19 @@ function App() { const navigate = useNavigate(); useEffect(() => { - const interval = setInterval(() => { - const expiry = localStorage.getItem("tokenExpiry"); - if (expiry && Date.now() > expiry) { - alert("세션이 만료되었습니다. 다시 로그인해주세요."); - localStorage.clear(); - navigate("/login"); - } - }, 60000); - - return () => clearInterval(interval); -}, []); + const checkTokenExpiry = () => { + const expiry = localStorage.getItem("tokenExpiry"); + const expiryMs = Number(expiry); + if (Number.isFinite(expiryMs) && Date.now() > expiryMs) { + alert("세션이 만료되었습니다. 다시 로그인해주세요."); + localStorage.clear(); + navigate("/login"); + } + }; + checkTokenExpiry(); + const interval = setInterval(checkTokenExpiry, 60000); + return () => clearInterval(interval); + }, []); return ( From f725f9374e07f59ac72ca329360fa38236d714c8 Mon Sep 17 00:00:00 2001 From: pbc1001 Date: Fri, 10 Apr 2026 00:18:18 +0900 Subject: [PATCH 21/84] =?UTF-8?q?fix:=20=EC=93=B8=EB=AA=A8=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EC=BD=98=EC=86=94=EB=A1=9C=EA=B7=B8=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/mypages.api.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/apis/mypages.api.js b/src/apis/mypages.api.js index 29fdef3..b7d3a32 100644 --- a/src/apis/mypages.api.js +++ b/src/apis/mypages.api.js @@ -21,7 +21,6 @@ export const updateProfile = async (formData) => { export const changePassword = async (passwordData) => { const response = await instance.patch('/api/v1/users/me/password', passwordData); - console.log(passwordData) return response.data; }; From a4c5d8d57a0b507095d1961af91c2d4257671968 Mon Sep 17 00:00:00 2001 From: pbc1001 Date: Fri, 10 Apr 2026 00:22:28 +0900 Subject: [PATCH 22/84] =?UTF-8?q?fix:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EA=B2=80=EC=A6=9D=20=EC=9A=94=EC=B2=AD=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/MyPage.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/MyPage.jsx b/src/pages/MyPage.jsx index 99a0da4..06ebcb2 100644 --- a/src/pages/MyPage.jsx +++ b/src/pages/MyPage.jsx @@ -111,7 +111,7 @@ const handleSaveProfile = async () => { }; const handleChangePassword = async () => { - if (!newPassword || newPassword !== confirmPassword) { + if (!newPassword || newPassword !== confirmPassword || !password) { return alert("비밀번호가 일치하지 않거나 입력되지 않았습니다."); } From 725e877bf4012f221eb72375c133f6a2cc185d9b Mon Sep 17 00:00:00 2001 From: pbc1001 Date: Fri, 10 Apr 2026 00:30:26 +0900 Subject: [PATCH 23/84] =?UTF-8?q?feat:=20=EC=82=AC=EC=A7=84=EC=B2=A9=20api?= =?UTF-8?q?=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 데이터 가져오기 - 화면에 표시하기(미완) --- src/apis/photo.api.js | 15 ++++ src/pages/PhotoBook.jsx | 165 ++++++++++++++++++++++++---------------- 2 files changed, 115 insertions(+), 65 deletions(-) create mode 100644 src/apis/photo.api.js diff --git a/src/apis/photo.api.js b/src/apis/photo.api.js new file mode 100644 index 0000000..d03e7e9 --- /dev/null +++ b/src/apis/photo.api.js @@ -0,0 +1,15 @@ +import instance from "./instance"; + +export const getMyGallery = async (yearMonth, tag = "") => { + const response = await instance.get('/api/v1/diaries?imageType=MANUAL&hasPhoto=true', { + params: { + imageType: 'MANUAL', + hasPhoto: true, + yearMonth: yearMonth, + limit: 32, + tag: tag || null, + sort: 'createdAt,desc' + } + }); + return response.data; +}; \ No newline at end of file diff --git a/src/pages/PhotoBook.jsx b/src/pages/PhotoBook.jsx index 45a3bbc..793addf 100644 --- a/src/pages/PhotoBook.jsx +++ b/src/pages/PhotoBook.jsx @@ -1,31 +1,64 @@ -// 사진첩 - import styled from "@emotion/styled"; import Header from "../components/Header.jsx"; -import Test_img from "../assets/Test.svg"; import Arrow from "../assets/Arrow.svg"; import Reverse_Arrow from "../assets/Rev-Arrow.svg"; import Search_tag from "../assets/search_tag.svg"; -import { useState } from "react"; +import { useState, useEffect } from "react"; +import { getMyGallery } from "../apis/photo.api.js"; const Photo_Book = () => { - const [dateNum, setDateNum] = useState(1); + const [dateNum, setDateNum] = useState(4); const [yearNum, setYearNum] = useState(2026); + const [galleryData, setGalleryData] = useState([]); + const [searchTerm, setSearchTerm] = useState(""); + const [loading, setLoading] = useState(false); + + const formatYearMonth = (y, m) => { + return `${y}-${String(m).padStart(2, "0")}`; + }; + + const fetchGallery = async () => { + setLoading(true); + try { + const currentYM = formatYearMonth(yearNum, dateNum); + const res = await getMyGallery(currentYM, searchTerm); + + if (res.data && res.data.length > 0) { + setGalleryData(res.data[0].diaries); + } else { + setGalleryData([]); + } + } catch (error) { + console.error("갤러리 로드 실패:", error); + setGalleryData([]); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchGallery(); + }, [dateNum, yearNum]); + + const handleSearch = (e) => { + if (e.key === "Enter") fetchGallery(); + }; const date_minus = () => { if (dateNum > 1) { setDateNum(dateNum - 1); - } - if (dateNum <= 1) { - (setDateNum(12), setYearNum(yearNum - 1)); + } else { + setDateNum(12); + setYearNum(yearNum - 1); } }; + const date_plus = () => { if (dateNum < 12) { setDateNum(dateNum + 1); - } - if (dateNum >= 12) { - (setDateNum(1), setYearNum(yearNum + 1)); + } else { + setDateNum(1); + setYearNum(yearNum + 1); } }; @@ -34,74 +67,56 @@ const Photo_Book = () => {
- + prev {yearNum}년 {dateNum} - + next + - - + setSearchTerm(e.target.value)} + onKeyDown={handleSearch} + /> + search - - - 12일 - - - - - 12일 - - - - - 12일 - - - - - 12일 - - - - - 12일 - - - - - 12일 - - - - - 12일 - - - - - 12일 - - - - - 12일 - - - - - 12일 - - + {loading ? ( +

사진을 불러오는 중...

+ ) : galleryData.length > 0 ? ( + galleryData.map((item) => ( + window.location.href=`/diary/${item.id}`}> + diary + {item.createdAt.split('-')[2]}일 + + + )) + ) : ( + + + 사진이 없어요... + + + 일기에 추억을 남기러 갈까요? + + + )}
+ + {/* 페이징 처리: 명세서상 한 달 단위로 32개를 가져오므로, + 만약 32개가 꽉 찼다면 '더보기' 버튼을 만들거나 다음 페이지 API를 호출해야 합니다. + 현재는 달력 이동 방식으로 충분히 커버 가능합니다. */}
); }; + + const Body = styled.div` width: 100vw; height: 100vh; @@ -216,4 +231,24 @@ const Photo_date = styled.div` z-index: 2; `; + +const NoData = styled.div` + width: 100%; + height: 500px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +`; +const Nophoto = styled.p` + font-weight: 500; + font-size: 14px; + color: #828282; +` +const Letsgo = styled.p` + font-weight: 600; + font-size: 20px; + color: #4f4f4f; +` + export default Photo_Book; From bd131fb338b425392885d925c4572148da002304 Mon Sep 17 00:00:00 2001 From: pbc1001 Date: Fri, 10 Apr 2026 00:42:32 +0900 Subject: [PATCH 24/84] Revert "Merge branch 'develop' into feat/refresh-api" This reverts commit f3a30e7c3bbba616ab302b7dbf4c70bac03d314f, reversing changes made to 01992ab512f1a0613f95f47835a9c9c61bc7638c. --- src/apis/instance.js | 11 +- src/apis/mypages.api.js | 43 ---- src/pages/MyPage.jsx | 532 +++++++++++++--------------------------- src/pages/SignUp.jsx | 32 +-- 4 files changed, 192 insertions(+), 426 deletions(-) delete mode 100644 src/apis/mypages.api.js diff --git a/src/apis/instance.js b/src/apis/instance.js index c7d1c45..9e01023 100644 --- a/src/apis/instance.js +++ b/src/apis/instance.js @@ -22,16 +22,13 @@ const processQueue = (error, token = null) => { instance.interceptors.request.use( (config) => { - const token = localStorage.getItem("accessToken"); - - if (token) { - config.headers.Authorization = `Bearer ${token}`; + const accessToken = localStorage.getItem("accessToken"); + if (accessToken) { + config.headers.Authorization = `Bearer ${accessToken}`; } return config; }, - (error) => { - return Promise.reject(error); - } + (error) => Promise.reject(error) ); instance.interceptors.response.use( diff --git a/src/apis/mypages.api.js b/src/apis/mypages.api.js deleted file mode 100644 index b7d3a32..0000000 --- a/src/apis/mypages.api.js +++ /dev/null @@ -1,43 +0,0 @@ -import instance from "./instance"; - -export const getMyInfo = async () => { - const response = await instance.get("/api/v1/users/me"); - return response.data; -}; - -export const withdrawAccount = async () => { - const response = await instance.delete("/api/v1/auth/me"); - return response.data; -}; - -export const updateProfile = async (formData) => { - const response = await instance.patch('/api/v1/users/me/profile', formData, { - headers: { - "Content-Type": undefined, - }, - }); - return response.data; -}; - -export const changePassword = async (passwordData) => { - const response = await instance.patch('/api/v1/users/me/password', passwordData); - return response.data; -}; - -export const deleteUser = async () => { - const refreshToken = localStorage.getItem("refreshToken"); - const response = await instance.delete('/api/v1/users/me', { - data: { - refreshToken: refreshToken - } - }); - return response.data; -}; - -export const logout = async () => { - const refreshToken = localStorage.getItem("refreshToken"); - const response = await instance.post('/api/v1/auth/logout', { - refreshToken: refreshToken - }); - return response.data; -}; \ No newline at end of file diff --git a/src/pages/MyPage.jsx b/src/pages/MyPage.jsx index 06ebcb2..f5c4018 100644 --- a/src/pages/MyPage.jsx +++ b/src/pages/MyPage.jsx @@ -1,3 +1,5 @@ +// 마이페이지 + import styled from "@emotion/styled"; import Header from "../components/Header.jsx"; import Happy from "../assets/Happy.svg"; @@ -6,179 +8,31 @@ import Profile from "../assets/Profile.svg"; import ReverseArrow from "../assets/Rev-Arrow.svg"; import Sad from "../assets/Sadness.svg"; import Arrow from "../assets/Arrow.svg"; -import Random from "../assets/random.svg"; -import Upload_btnimg from "../assets/Upload_btn.svg"; -import { useState, useEffect, useRef } from "react"; +import Random from "../assets/random.svg" +import Upload_btnimg from "../assets/Upload_btn.svg" +import { useState } from "react"; import { useNavigate } from "react-router-dom"; -import { getMyInfo, updateProfile, changePassword, deleteUser, logout } from "../apis/mypages.api.js"; const Mypage = () => { - const [userInfo, setUserInfo] = useState(null); - const [isLoading, setIsLoading] = useState(true); + + const user_info = { + "status": "success", + "data": { + "userId": 102, + "email": "seungri@example.com", + "nickname": "쿠수리", + "profileImageUrl": "https://s3.../profiles/user_102.png", + "createdAt": "2025-06-09T10:00:00Z", + "updatedAt": "2026-03-04T23:55:00Z" + } +} const [isChangeModal, setIsChangeModal] = useState(false); const [isDeleteModal, setIsDeleteModal] = useState(false); const [isPwModal, setIsPwModal] = useState(false); - const [newNickname, setNewNickname] = useState(""); - const [previewImage, setPreviewImage] = useState(null); - const [selectedFile, setSelectedFile] = useState(null); - - const [newPassword, setNewPassword] = useState(""); - const [confirmPassword, setConfirmPassword] = useState(""); - - const fileInputRef = useRef(null); - const navigate = useNavigate(); - - useEffect(() => { - const fetchUserData = async () => { - try { - const res = await getMyInfo(); - setUserInfo(res.data); - } catch (error) { - console.error("유저 정보 로드 실패:", error); - } finally { - setIsLoading(false); - } - }; - fetchUserData(); - }, []); - - const [password, setPassword] = useState(""); - - const handleOpenEditModal = () => { - if (!userInfo) return; - setNewNickname(userInfo.nickname); - setPreviewImage(userInfo.profileImageUrl || Profile); - setSelectedFile(null); - setIsChangeModal(true); - }; - - const handleOpenDeleteModal = () => { - if (!userInfo) return; - setIsDeleteModal(true); - }; - - const handleOpenPwModal = () => { - if (!userInfo) return; - setConfirmPassword(""); - setPassword("") - setNewPassword("") - setIsPwModal(true); - }; - - const handleFileChange = (e) => { - const file = e.target.files[0]; - if (file) { - setSelectedFile(file); - const reader = new FileReader(); - reader.onloadend = () => { - setPreviewImage(reader.result); - }; - reader.readAsDataURL(file); - } - }; - -const handleSaveProfile = async () => { - if (!newNickname.trim()) return alert("닉네임을 입력해주세요."); - - try { - const formData = new FormData(); - - const jsonBlob = new Blob( - [JSON.stringify({ nickname: newNickname })], - { type: "application/json" } - ); - - formData.append("data", jsonBlob); - formData.append("image", selectedFile ? selectedFile : null); - - const res = await updateProfile(formData); - - if (res.status === 200) { - alert("프로필이 성공적으로 변경되었습니다."); - setUserInfo((prev) => ({ - ...prev, - nickname: newNickname, - profileImageUrl: previewImage, - })); - setIsChangeModal(false); - } - } catch (error) { - console.error("수정 실패:", error); - alert("수정 중 오류가 발생했습니다."); - } -}; - - const handleChangePassword = async () => { - if (!newPassword || newPassword !== confirmPassword || !password) { - return alert("비밀번호가 일치하지 않거나 입력되지 않았습니다."); - } - - try { - const res = await changePassword({ - oldPassword: password, - newPassword: newPassword, - confirmPassword: confirmPassword - - }); - if (res.status === 200) { - alert("비밀번호가 변경되었습니다."); - setIsPwModal(false); - } - } catch (error) { - alert( - "비밀번호 변경 실패: " + (error.response?.data?.message || "오류 발생"), - ); - } - }; - -const handleDeleteAccount = async () => { - try { - const res = await deleteUser(); - if (res.status === 200) { - alert("탈퇴가 완료되었습니다."); - localStorage.clear(); - navigate("/"); - window.location.reload(); - } - } catch (error) { - console.error("탈퇴 에러 상세:", error.response?.data); - alert(error.response?.data?.message || "탈퇴 처리 중 오류가 발생했습니다."); - } -}; - - -const handleLogout = async () => { - if (window.confirm("로그아웃 하시겠습니까?")) { - try { - await logout(); - localStorage.removeItem("accessToken"); - localStorage.removeItem("refreshToken"); - navigate("/"); - window.location.reload(); - } catch (error) { - console.error("로그아웃 실패:", error); - alert("로그아웃 실패") - } - } -}; - - if (isLoading) - return ( - -
-
로딩 중...
- - ); - if (!userInfo) - return ( - -
-
정보를 불러올 수 없습니다.
- - ); - + const navigate = useNavigate() + return (
@@ -186,27 +40,22 @@ const handleLogout = async () => { - + {setIsChangeModal(true)}}> - Profile + + + + - {userInfo.nickname} - - Edit + {user_info.data.nickname} + {setIsChangeModal(true)}}> + - {userInfo.email} + {user_info.data.email} @@ -218,16 +67,14 @@ const handleLogout = async () => { 비밀번호 변경 - - Change + {setIsPwModal(true)}}> + - 로그아웃 - - 회원 탈퇴 - + {navigate("/")}}>로그아웃 + {setIsDeleteModal(true)}}>회원 탈퇴 @@ -246,125 +93,89 @@ const handleLogout = async () => { - {/* 계정 탈퇴 모달 */} - {isDeleteModal && ( - setIsDeleteModal(false)}> - e.stopPropagation()}> - - - Sad - - - 정말 탈퇴하시겠어요? - - 지금 탈퇴하시면 작성하신 일기 추억들을 다시 볼 수 없게됩니다.{" "} - 그래도 탈퇴를 진행하시겠어요? - - - - - - 회원탈퇴 - - setIsDeleteModal(false)}> - 취소 - - - - - )} + {/*계정 탈퇴 모달*/} +{isDeleteModal && + {setIsDeleteModal(false)}}> + {e.stopPropagation()}}> + + + + + + 정말 탈퇴하시겠어요? + + 지금 탈퇴하시면 작성하신 일기 추억들을 다시 볼 수 없게됩니다.{" "} + 그래도 탈퇴를 진행하시겠어요? + + + + + + + {navigate("/")}}>회원탈퇴 + {setIsDeleteModal(false)}}>취소 + + + +} {/* 비밀번호 변경 모달 */} - {isPwModal && ( - setIsPwModal(false)}> - e.stopPropagation()}> - setIsPwModal(false)}> - Back - - - + {isPwModal&& + {setIsPwModal(false)}}> + {e.stopPropagation()}}> + {setIsPwModal(false)}}> + + + + - 기존 비밀번호 - setPassword(e.target.value)} - /> - - - 비밀번호 - setNewPassword(e.target.value)} - /> - - - 비밀번호 확인 - setConfirmPassword(e.target.value)} - /> - - - - 저장 - - - - - )} - - {/* 프로필 변경 모달 */} - {isChangeModal && ( - setIsChangeModal(false)}> - e.stopPropagation()}> - setIsChangeModal(false)}> - Close - - - - - Preview - - Random - - - - 닉네임 변경 - setNewNickname(e.target.value)} - placeholder="변경할 닉네임을 입력해주세요" - /> - - - - - fileInputRef.current.click()}> - - 파일 업로드 - - 저장 - - - - - )} + 비밀번호 + + + + 비밀번호 확인 + + + + {setIsPwModal(false)}}>저장 + + + +} + + {/*프로필 변경 모달*/} + {isChangeModal && + {setIsChangeModal(false)}}> + {e.stopPropagation()}}> + {setIsChangeModal(false)}}> + + + + + + + + + + + + + + 닉네임 변경 + + + + + + + 파일 업로드 + + {setIsChangeModal(false)}}>저장 + + + + +} ); }; @@ -419,22 +230,25 @@ const Profile_img = styled.div` height: 60px; border: 2px solid #e0e0e0; border-radius: 50px; + background-color: #cfcfcf; display: flex; align-items: center; justify-content: center; position: relative; - overflow: hidden; cursor: pointer; `; const Img_box = styled.div` - width: 60px; - height: 60px; - background-color: #cfcfcf; + width: 40px; + height: 40px; display: flex; align-items: center; justify-content: center; `; - +const Img_edit = styled.div` + position: absolute; + bottom: -4px; + right: 2px; +`; const Nickname_box = styled.div` display: flex; flex-direction: column; @@ -613,7 +427,7 @@ const Text_box = styled.div` display: flex; flex-direction: column; align-items: center; -`; +` const Check_title = styled.div` font-size: 20px; font-weight: 600; @@ -680,7 +494,7 @@ const Password_back = styled.div` const Password_modal = styled.div` position: relative; width: 480px; - height: 432px; + height: 352px; display: flex; flex-direction: column; align-items: center; @@ -698,7 +512,7 @@ const Out_modal = styled.div` cursor: pointer; `; const Change_box = styled.div` - display: flex; + display: flex ; flex-direction: column; justify-content: space-between; width: 100%; @@ -711,7 +525,7 @@ const Input_box = styled.div` display: flex; flex-direction: column; gap: 16px; -`; +` const Pass_box = styled.div` width: 100%; height: 100%; @@ -725,16 +539,16 @@ const Change_pass = styled.p` color: #575141; `; const Pass_input = styled.input` - width: 100%; - height: 100%; - font-weight: 500; - font-size: 16px; - border: 1px solid #cfd3dc; - padding: 9.5px 16px; - border-radius: 12px; - ::placeholder { - color: #cfd3dc; - } +width: 100%; +height: 100%; +font-weight: 500; +font-size: 16px; +border: 1px solid #CFD3DC; +padding: 9.5px 16px; +border-radius: 12px; +::placeholder { + color: #CFD3DC; +} `; const Checking_box = styled.div` width: 100%; @@ -742,6 +556,7 @@ const Checking_box = styled.div` display: flex; flex-direction: column; gap: 4px; + `; const Check_pass = styled.div` font-size: 16px; @@ -749,16 +564,16 @@ const Check_pass = styled.div` color: #575141; `; const Check_input = styled.input` - width: 100%; - height: 100%; - font-weight: 500; - font-size: 16px; - border: 1px solid #cfd3dc; - padding: 9.5px 16px; - border-radius: 12px; - ::placeholder { - color: #cfd3dc; - } +width: 100%; +height: 100%; +font-weight: 500; +font-size: 16px; +border: 1px solid #CFD3DC; +padding: 9.5px 16px; +border-radius: 12px; +::placeholder { + color: #CFD3DC; +} `; const Save_button = styled.div` width: 100%; @@ -768,7 +583,7 @@ const Save_button = styled.div` justify-content: center; font-size: 16px; font-weight: 600; - background-color: #fcd671; + background-color: #FCD671; color: #575141; border-radius: 12px; cursor: pointer; @@ -785,7 +600,7 @@ const Profile_modalback = styled.div` display: flex; align-items: center; justify-content: center; -`; +` const Profile_modal = styled.div` width: 480px; @@ -796,7 +611,7 @@ const Profile_modal = styled.div` display: flex; flex-direction: column; border-radius: 12px; -`; +` const Pf_modalmain = styled.div` width: 100%; height: 100%; @@ -804,75 +619,79 @@ const Pf_modalmain = styled.div` flex-direction: column; justify-content: space-between; gap: 32px; -`; +` const Profile_change = styled.div` gap: 24px; display: flex; flex-direction: column; align-items: center; -`; +` const Change_imgbox = styled.div` width: 60px; height: 60px; - background-color: #cfcfcf; - border: 2px solid #e0e0e0; + background-color: #CFCFCF; + border: 2px solid #E0E0E0; border-radius: 50px; display: flex; align-items: center; justify-content: center; + position: relative; box-sizing: border-box; - overflow: hidden; -`; - +` +const Change_img = styled.div` +width: 40px; +height: 40px; +` const Profile_rand = styled.div` position: absolute; - right: 210px; - top: 105px; + right: -7px; + bottom: -7px; width: 22px; height: 22px; display: flex; background-color: #fff; - border: 1px solid #f3f3f3; + border: 1px solid #F3F3F3; border-radius: 12px; box-sizing: border-box; align-items: center; justify-content: center; -`; +` const Change_nickbox = styled.div` width: 100%; display: flex; flex-direction: column; gap: 4px; -`; +` const Nick_text = styled.p` font-size: 16px; font-weight: 500; color: #575141; -`; +` const Nick_input = styled.input` width: 100%; height: auto; padding: 9.5px 16px; font-size: 16px; font-weight: 500; - border: 1px solid #cfd3dc; + border: 1px solid #CFD3DC; border-radius: 12px; - ::placeholder { - color: #cfd3dc; + ::placeholder{ + color: #CFD3DC; } - :focus { + :focus{ outline: none; } -`; +` const Profile_btnbox = styled.div` width: 100%; height: 100%; display: flex; gap: 12px; -`; + +` const Img_upload = styled.button` width: 100%; - border: 2px solid #fcd671; + border: 2px solid #FCD671; display: flex; gap: 10px; align-items: center; @@ -883,8 +702,7 @@ const Img_upload = styled.button` font-size: 16px; border-radius: 12px; box-sizing: border-box; - cursor: pointer; -`; +` const Save_btn = styled.button` width: 100%; background-color: #fcd671; @@ -898,6 +716,6 @@ const Save_btn = styled.button` font-size: 16px; border-radius: 12px; cursor: pointer; -`; +` -export default Mypage; +export default Mypage; \ No newline at end of file diff --git a/src/pages/SignUp.jsx b/src/pages/SignUp.jsx index e779fa1..2190595 100644 --- a/src/pages/SignUp.jsx +++ b/src/pages/SignUp.jsx @@ -28,25 +28,19 @@ const SignUp = () => { const [pwMessage, setPwMessage] = useState(""); const [checkPwMessage, setCheckPwMessage] = useState(""); -useEffect(() => { - let timer; - - if (showCode && timeleft > 0) { - timer = setInterval(() => { - setTimeleft((prev) => { - if (prev <= 1) { - clearInterval(timer); - alert("인증 시간이 만료되었습니다. 다시 시도해주세요."); - setShowCode(false); - return 180; - } - return prev - 1; - }); - }, 1000); - } - - return () => clearInterval(timer); -}, [showCode]); + useEffect(() => { + let timer; + if (showCode && timeleft > 0) { + timer = setInterval(() => { + setTimeleft((prev) => prev - 1); + }, 1000); + } else if (timeleft === 0) { + alert("인증 시간이 만료되었습니다. 다시 시도해주세요."); + setShowCode(false); + setTimeleft(180); + } + return () => clearInterval(timer); + }, [showCode, timeleft]); const formatTime = (seconds) => { const minutes = Math.floor(seconds / 60); From 616a7cd25b774169a91fb7fb5b5a3d3f79d3f073 Mon Sep 17 00:00:00 2001 From: pbc1001 Date: Fri, 10 Apr 2026 00:43:01 +0900 Subject: [PATCH 25/84] Reapply "Merge branch 'develop' into feat/refresh-api" This reverts commit bd131fb338b425392885d925c4572148da002304. --- src/apis/instance.js | 11 +- src/apis/mypages.api.js | 43 ++++ src/pages/MyPage.jsx | 532 +++++++++++++++++++++++++++------------- src/pages/SignUp.jsx | 32 ++- 4 files changed, 426 insertions(+), 192 deletions(-) create mode 100644 src/apis/mypages.api.js diff --git a/src/apis/instance.js b/src/apis/instance.js index 9e01023..c7d1c45 100644 --- a/src/apis/instance.js +++ b/src/apis/instance.js @@ -22,13 +22,16 @@ const processQueue = (error, token = null) => { instance.interceptors.request.use( (config) => { - const accessToken = localStorage.getItem("accessToken"); - if (accessToken) { - config.headers.Authorization = `Bearer ${accessToken}`; + const token = localStorage.getItem("accessToken"); + + if (token) { + config.headers.Authorization = `Bearer ${token}`; } return config; }, - (error) => Promise.reject(error) + (error) => { + return Promise.reject(error); + } ); instance.interceptors.response.use( diff --git a/src/apis/mypages.api.js b/src/apis/mypages.api.js new file mode 100644 index 0000000..b7d3a32 --- /dev/null +++ b/src/apis/mypages.api.js @@ -0,0 +1,43 @@ +import instance from "./instance"; + +export const getMyInfo = async () => { + const response = await instance.get("/api/v1/users/me"); + return response.data; +}; + +export const withdrawAccount = async () => { + const response = await instance.delete("/api/v1/auth/me"); + return response.data; +}; + +export const updateProfile = async (formData) => { + const response = await instance.patch('/api/v1/users/me/profile', formData, { + headers: { + "Content-Type": undefined, + }, + }); + return response.data; +}; + +export const changePassword = async (passwordData) => { + const response = await instance.patch('/api/v1/users/me/password', passwordData); + return response.data; +}; + +export const deleteUser = async () => { + const refreshToken = localStorage.getItem("refreshToken"); + const response = await instance.delete('/api/v1/users/me', { + data: { + refreshToken: refreshToken + } + }); + return response.data; +}; + +export const logout = async () => { + const refreshToken = localStorage.getItem("refreshToken"); + const response = await instance.post('/api/v1/auth/logout', { + refreshToken: refreshToken + }); + return response.data; +}; \ No newline at end of file diff --git a/src/pages/MyPage.jsx b/src/pages/MyPage.jsx index f5c4018..06ebcb2 100644 --- a/src/pages/MyPage.jsx +++ b/src/pages/MyPage.jsx @@ -1,5 +1,3 @@ -// 마이페이지 - import styled from "@emotion/styled"; import Header from "../components/Header.jsx"; import Happy from "../assets/Happy.svg"; @@ -8,31 +6,179 @@ import Profile from "../assets/Profile.svg"; import ReverseArrow from "../assets/Rev-Arrow.svg"; import Sad from "../assets/Sadness.svg"; import Arrow from "../assets/Arrow.svg"; -import Random from "../assets/random.svg" -import Upload_btnimg from "../assets/Upload_btn.svg" -import { useState } from "react"; +import Random from "../assets/random.svg"; +import Upload_btnimg from "../assets/Upload_btn.svg"; +import { useState, useEffect, useRef } from "react"; import { useNavigate } from "react-router-dom"; +import { getMyInfo, updateProfile, changePassword, deleteUser, logout } from "../apis/mypages.api.js"; const Mypage = () => { - - const user_info = { - "status": "success", - "data": { - "userId": 102, - "email": "seungri@example.com", - "nickname": "쿠수리", - "profileImageUrl": "https://s3.../profiles/user_102.png", - "createdAt": "2025-06-09T10:00:00Z", - "updatedAt": "2026-03-04T23:55:00Z" - } -} + const [userInfo, setUserInfo] = useState(null); + const [isLoading, setIsLoading] = useState(true); const [isChangeModal, setIsChangeModal] = useState(false); const [isDeleteModal, setIsDeleteModal] = useState(false); const [isPwModal, setIsPwModal] = useState(false); - const navigate = useNavigate() - + const [newNickname, setNewNickname] = useState(""); + const [previewImage, setPreviewImage] = useState(null); + const [selectedFile, setSelectedFile] = useState(null); + + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + + const fileInputRef = useRef(null); + const navigate = useNavigate(); + + useEffect(() => { + const fetchUserData = async () => { + try { + const res = await getMyInfo(); + setUserInfo(res.data); + } catch (error) { + console.error("유저 정보 로드 실패:", error); + } finally { + setIsLoading(false); + } + }; + fetchUserData(); + }, []); + + const [password, setPassword] = useState(""); + + const handleOpenEditModal = () => { + if (!userInfo) return; + setNewNickname(userInfo.nickname); + setPreviewImage(userInfo.profileImageUrl || Profile); + setSelectedFile(null); + setIsChangeModal(true); + }; + + const handleOpenDeleteModal = () => { + if (!userInfo) return; + setIsDeleteModal(true); + }; + + const handleOpenPwModal = () => { + if (!userInfo) return; + setConfirmPassword(""); + setPassword("") + setNewPassword("") + setIsPwModal(true); + }; + + const handleFileChange = (e) => { + const file = e.target.files[0]; + if (file) { + setSelectedFile(file); + const reader = new FileReader(); + reader.onloadend = () => { + setPreviewImage(reader.result); + }; + reader.readAsDataURL(file); + } + }; + +const handleSaveProfile = async () => { + if (!newNickname.trim()) return alert("닉네임을 입력해주세요."); + + try { + const formData = new FormData(); + + const jsonBlob = new Blob( + [JSON.stringify({ nickname: newNickname })], + { type: "application/json" } + ); + + formData.append("data", jsonBlob); + formData.append("image", selectedFile ? selectedFile : null); + + const res = await updateProfile(formData); + + if (res.status === 200) { + alert("프로필이 성공적으로 변경되었습니다."); + setUserInfo((prev) => ({ + ...prev, + nickname: newNickname, + profileImageUrl: previewImage, + })); + setIsChangeModal(false); + } + } catch (error) { + console.error("수정 실패:", error); + alert("수정 중 오류가 발생했습니다."); + } +}; + + const handleChangePassword = async () => { + if (!newPassword || newPassword !== confirmPassword || !password) { + return alert("비밀번호가 일치하지 않거나 입력되지 않았습니다."); + } + + try { + const res = await changePassword({ + oldPassword: password, + newPassword: newPassword, + confirmPassword: confirmPassword + + }); + if (res.status === 200) { + alert("비밀번호가 변경되었습니다."); + setIsPwModal(false); + } + } catch (error) { + alert( + "비밀번호 변경 실패: " + (error.response?.data?.message || "오류 발생"), + ); + } + }; + +const handleDeleteAccount = async () => { + try { + const res = await deleteUser(); + if (res.status === 200) { + alert("탈퇴가 완료되었습니다."); + localStorage.clear(); + navigate("/"); + window.location.reload(); + } + } catch (error) { + console.error("탈퇴 에러 상세:", error.response?.data); + alert(error.response?.data?.message || "탈퇴 처리 중 오류가 발생했습니다."); + } +}; + + +const handleLogout = async () => { + if (window.confirm("로그아웃 하시겠습니까?")) { + try { + await logout(); + localStorage.removeItem("accessToken"); + localStorage.removeItem("refreshToken"); + navigate("/"); + window.location.reload(); + } catch (error) { + console.error("로그아웃 실패:", error); + alert("로그아웃 실패") + } + } +}; + + if (isLoading) + return ( + +
+
로딩 중...
+ + ); + if (!userInfo) + return ( + +
+
정보를 불러올 수 없습니다.
+ + ); + return (
@@ -40,22 +186,27 @@ const Mypage = () => { - {setIsChangeModal(true)}}> + - + Profile - - - - {user_info.data.nickname} - {setIsChangeModal(true)}}> - + {userInfo.nickname} + + Edit - {user_info.data.email} + {userInfo.email} @@ -67,14 +218,16 @@ const Mypage = () => { 비밀번호 변경 - {setIsPwModal(true)}}> - + + Change - {navigate("/")}}>로그아웃 - {setIsDeleteModal(true)}}>회원 탈퇴 + 로그아웃 + + 회원 탈퇴 + @@ -93,89 +246,125 @@ const Mypage = () => { - {/*계정 탈퇴 모달*/} -{isDeleteModal && - {setIsDeleteModal(false)}}> - {e.stopPropagation()}}> - - - - - - 정말 탈퇴하시겠어요? - - 지금 탈퇴하시면 작성하신 일기 추억들을 다시 볼 수 없게됩니다.{" "} - 그래도 탈퇴를 진행하시겠어요? - - - - - - - {navigate("/")}}>회원탈퇴 - {setIsDeleteModal(false)}}>취소 - - - -} + {/* 계정 탈퇴 모달 */} + {isDeleteModal && ( + setIsDeleteModal(false)}> + e.stopPropagation()}> + + + Sad + + + 정말 탈퇴하시겠어요? + + 지금 탈퇴하시면 작성하신 일기 추억들을 다시 볼 수 없게됩니다.{" "} + 그래도 탈퇴를 진행하시겠어요? + + + + + + 회원탈퇴 + + setIsDeleteModal(false)}> + 취소 + + + + + )} {/* 비밀번호 변경 모달 */} - {isPwModal&& - {setIsPwModal(false)}}> - {e.stopPropagation()}}> - {setIsPwModal(false)}}> - - - - + {isPwModal && ( + setIsPwModal(false)}> + e.stopPropagation()}> + setIsPwModal(false)}> + Back + + + - 비밀번호 - - - - 비밀번호 확인 - - - - {setIsPwModal(false)}}>저장 - - - -} - - {/*프로필 변경 모달*/} - {isChangeModal && - {setIsChangeModal(false)}}> - {e.stopPropagation()}}> - {setIsChangeModal(false)}}> - - - - - - - - - - - - - - 닉네임 변경 - - - - - - - 파일 업로드 - - {setIsChangeModal(false)}}>저장 - - - - -} + 기존 비밀번호 + setPassword(e.target.value)} + /> + + + 비밀번호 + setNewPassword(e.target.value)} + /> + + + 비밀번호 확인 + setConfirmPassword(e.target.value)} + /> + + + + 저장 + + + + + )} + + {/* 프로필 변경 모달 */} + {isChangeModal && ( + setIsChangeModal(false)}> + e.stopPropagation()}> + setIsChangeModal(false)}> + Close + + + + + Preview + + Random + + + + 닉네임 변경 + setNewNickname(e.target.value)} + placeholder="변경할 닉네임을 입력해주세요" + /> + + + + + fileInputRef.current.click()}> + + 파일 업로드 + + 저장 + + + + + )} ); }; @@ -230,25 +419,22 @@ const Profile_img = styled.div` height: 60px; border: 2px solid #e0e0e0; border-radius: 50px; - background-color: #cfcfcf; display: flex; align-items: center; justify-content: center; position: relative; + overflow: hidden; cursor: pointer; `; const Img_box = styled.div` - width: 40px; - height: 40px; + width: 60px; + height: 60px; + background-color: #cfcfcf; display: flex; align-items: center; justify-content: center; `; -const Img_edit = styled.div` - position: absolute; - bottom: -4px; - right: 2px; -`; + const Nickname_box = styled.div` display: flex; flex-direction: column; @@ -427,7 +613,7 @@ const Text_box = styled.div` display: flex; flex-direction: column; align-items: center; -` +`; const Check_title = styled.div` font-size: 20px; font-weight: 600; @@ -494,7 +680,7 @@ const Password_back = styled.div` const Password_modal = styled.div` position: relative; width: 480px; - height: 352px; + height: 432px; display: flex; flex-direction: column; align-items: center; @@ -512,7 +698,7 @@ const Out_modal = styled.div` cursor: pointer; `; const Change_box = styled.div` - display: flex ; + display: flex; flex-direction: column; justify-content: space-between; width: 100%; @@ -525,7 +711,7 @@ const Input_box = styled.div` display: flex; flex-direction: column; gap: 16px; -` +`; const Pass_box = styled.div` width: 100%; height: 100%; @@ -539,16 +725,16 @@ const Change_pass = styled.p` color: #575141; `; const Pass_input = styled.input` -width: 100%; -height: 100%; -font-weight: 500; -font-size: 16px; -border: 1px solid #CFD3DC; -padding: 9.5px 16px; -border-radius: 12px; -::placeholder { - color: #CFD3DC; -} + width: 100%; + height: 100%; + font-weight: 500; + font-size: 16px; + border: 1px solid #cfd3dc; + padding: 9.5px 16px; + border-radius: 12px; + ::placeholder { + color: #cfd3dc; + } `; const Checking_box = styled.div` width: 100%; @@ -556,7 +742,6 @@ const Checking_box = styled.div` display: flex; flex-direction: column; gap: 4px; - `; const Check_pass = styled.div` font-size: 16px; @@ -564,16 +749,16 @@ const Check_pass = styled.div` color: #575141; `; const Check_input = styled.input` -width: 100%; -height: 100%; -font-weight: 500; -font-size: 16px; -border: 1px solid #CFD3DC; -padding: 9.5px 16px; -border-radius: 12px; -::placeholder { - color: #CFD3DC; -} + width: 100%; + height: 100%; + font-weight: 500; + font-size: 16px; + border: 1px solid #cfd3dc; + padding: 9.5px 16px; + border-radius: 12px; + ::placeholder { + color: #cfd3dc; + } `; const Save_button = styled.div` width: 100%; @@ -583,7 +768,7 @@ const Save_button = styled.div` justify-content: center; font-size: 16px; font-weight: 600; - background-color: #FCD671; + background-color: #fcd671; color: #575141; border-radius: 12px; cursor: pointer; @@ -600,7 +785,7 @@ const Profile_modalback = styled.div` display: flex; align-items: center; justify-content: center; -` +`; const Profile_modal = styled.div` width: 480px; @@ -611,7 +796,7 @@ const Profile_modal = styled.div` display: flex; flex-direction: column; border-radius: 12px; -` +`; const Pf_modalmain = styled.div` width: 100%; height: 100%; @@ -619,79 +804,75 @@ const Pf_modalmain = styled.div` flex-direction: column; justify-content: space-between; gap: 32px; -` +`; const Profile_change = styled.div` gap: 24px; display: flex; flex-direction: column; align-items: center; -` +`; const Change_imgbox = styled.div` width: 60px; height: 60px; - background-color: #CFCFCF; - border: 2px solid #E0E0E0; + background-color: #cfcfcf; + border: 2px solid #e0e0e0; border-radius: 50px; display: flex; align-items: center; justify-content: center; - position: relative; box-sizing: border-box; -` -const Change_img = styled.div` -width: 40px; -height: 40px; -` + overflow: hidden; +`; + const Profile_rand = styled.div` position: absolute; - right: -7px; - bottom: -7px; + right: 210px; + top: 105px; width: 22px; height: 22px; display: flex; background-color: #fff; - border: 1px solid #F3F3F3; + border: 1px solid #f3f3f3; border-radius: 12px; box-sizing: border-box; align-items: center; justify-content: center; -` +`; const Change_nickbox = styled.div` width: 100%; display: flex; flex-direction: column; gap: 4px; -` +`; const Nick_text = styled.p` font-size: 16px; font-weight: 500; color: #575141; -` +`; const Nick_input = styled.input` width: 100%; height: auto; padding: 9.5px 16px; font-size: 16px; font-weight: 500; - border: 1px solid #CFD3DC; + border: 1px solid #cfd3dc; border-radius: 12px; - ::placeholder{ - color: #CFD3DC; + ::placeholder { + color: #cfd3dc; } - :focus{ + :focus { outline: none; } -` +`; const Profile_btnbox = styled.div` width: 100%; height: 100%; display: flex; gap: 12px; - -` +`; const Img_upload = styled.button` width: 100%; - border: 2px solid #FCD671; + border: 2px solid #fcd671; display: flex; gap: 10px; align-items: center; @@ -702,7 +883,8 @@ const Img_upload = styled.button` font-size: 16px; border-radius: 12px; box-sizing: border-box; -` + cursor: pointer; +`; const Save_btn = styled.button` width: 100%; background-color: #fcd671; @@ -716,6 +898,6 @@ const Save_btn = styled.button` font-size: 16px; border-radius: 12px; cursor: pointer; -` +`; -export default Mypage; \ No newline at end of file +export default Mypage; diff --git a/src/pages/SignUp.jsx b/src/pages/SignUp.jsx index 2190595..e779fa1 100644 --- a/src/pages/SignUp.jsx +++ b/src/pages/SignUp.jsx @@ -28,19 +28,25 @@ const SignUp = () => { const [pwMessage, setPwMessage] = useState(""); const [checkPwMessage, setCheckPwMessage] = useState(""); - useEffect(() => { - let timer; - if (showCode && timeleft > 0) { - timer = setInterval(() => { - setTimeleft((prev) => prev - 1); - }, 1000); - } else if (timeleft === 0) { - alert("인증 시간이 만료되었습니다. 다시 시도해주세요."); - setShowCode(false); - setTimeleft(180); - } - return () => clearInterval(timer); - }, [showCode, timeleft]); +useEffect(() => { + let timer; + + if (showCode && timeleft > 0) { + timer = setInterval(() => { + setTimeleft((prev) => { + if (prev <= 1) { + clearInterval(timer); + alert("인증 시간이 만료되었습니다. 다시 시도해주세요."); + setShowCode(false); + return 180; + } + return prev - 1; + }); + }, 1000); + } + + return () => clearInterval(timer); +}, [showCode]); const formatTime = (seconds) => { const minutes = Math.floor(seconds / 60); From 01b83b6a0f6286ce938ba00fef740dc842da0813 Mon Sep 17 00:00:00 2001 From: pbc1001 Date: Fri, 10 Apr 2026 19:27:45 +0900 Subject: [PATCH 26/84] =?UTF-8?q?fix:=20=ED=86=B5=EA=B3=84=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20api=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/statics.js | 7 +++ src/pages/Statics.jsx | 123 +++++++++++++++++++++++++++++++----------- 2 files changed, 98 insertions(+), 32 deletions(-) create mode 100644 src/apis/statics.js diff --git a/src/apis/statics.js b/src/apis/statics.js new file mode 100644 index 0000000..1ac80bd --- /dev/null +++ b/src/apis/statics.js @@ -0,0 +1,7 @@ +import instance from "./instance"; + +export const getStatistics = async (year, month) => { + const yearMonth = `${year}-${String(month).padStart(2, "0")}`; + const response = await instance.get(`/api/v1/users/me/statistics?yearMonth=${yearMonth}`); + return response.data; +}; \ No newline at end of file diff --git a/src/pages/Statics.jsx b/src/pages/Statics.jsx index ebfba29..952a57d 100644 --- a/src/pages/Statics.jsx +++ b/src/pages/Statics.jsx @@ -1,15 +1,53 @@ import styled from "@emotion/styled"; +import { Global, css } from "@emotion/react"; import Header from "../components/Header.jsx"; import ReverseArrow from "../assets/Rev-Arrow.svg"; import Arrow from "../assets/Arrow.svg"; import Happy from "../assets/Star.svg"; -import { useState } from "react"; +import { useState, useEffect } from "react"; +import { getStatistics } from "../apis/statics.js"; + +const globalStyles = css` + @property --rate { + syntax: ''; + inherits: false; + initial-value: 0%; + } +`; + const Statics = () => { const colors = ["#FEA2A9", "#FCD671", "#5DC19B", "#89D9FF", "#CBA3FF"]; + const emotionLabels = ["화남", "슬픔", "행복", "불안", "평범"]; + const emotionKeys = ["ANGRY", "SAD", "HAPPY", "ANXIETY", "NEUTRAL"]; const [dateNum, setDateNum] = useState(3); const [yearNum, setYearNum] = useState(2026); + const [statistics, setStatistics] = useState(null); + + + + + const maxEmotion = statistics + ? Math.max( + ...emotionKeys.map( + (key) => statistics.emotionDistribution.values[key] ?? 0, + ), + ) + : 1; + + useEffect(() => { + const fetchStatistics = async () => { + try { + const res = await getStatistics(yearNum, dateNum); + console.log("통계 데이터:", res.data); + setStatistics(res.data); + } catch (error) { + console.error("통계 로드 실패:", error); + } + }; + fetchStatistics(); + }, [yearNum, dateNum]); const date_minus = () => { if (dateNum > 1) { @@ -30,6 +68,7 @@ const Statics = () => { return ( +
@@ -46,10 +85,12 @@ const Statics = () => { - + 일기 작성률 - 60% + + {statistics ? `${statistics.writingRate}%` : "0%"} + @@ -57,39 +98,48 @@ const Statics = () => { - 총 19일 + 총{" "} + + {statistics ? `${statistics.diaryCount}일` : "0일"} + - 사진 8회 + + 사진 {statistics ? `${statistics.photoCount}회` : "0회"} + - - 화남 - 평범 - 행복 - 슬픔 - 불안 + {emotionLabels.map((label, index) => ( + {label} + ))} - {colors.map((color, index) => ( - - 12일 - - - ))} + {emotionKeys.map((key, index) => { + const value = statistics + ? (statistics.emotionDistribution.values[key] ?? 0) + : 0; + const height = + maxEmotion > 0 ? (value / maxEmotion) * 100 : 0; + return ( + + + {statistics ? `${Math.round(value)}일` : "0일"} + + + + ); + })} - 최히원 님, 이번 달은 전반적으로 '행복'한 날이 많았네요! 특히 - 주말에 사진 기록이 활발했습니다. 승리 님, 이번 달은 전반적으로 - '행복'한 날이 많았네요! 특히 주말에 사진 기록이 활발했습니다. 승리 - 님, 이번 달은 전반적으로 '행복'한 날이 많았네요! 특히 주말에 사진 - 기록이 활발했습니다. + {statistics + ? statistics.aiReportContent + : "데이터를 불러오는 중..."} @@ -102,7 +152,6 @@ const Statics = () => { ); }; - const Body = styled.div` width: 100%; height: 100%; @@ -181,8 +230,13 @@ const Circle_graph = styled.div` display: flex; align-items: center; justify-content: center; - background: conic-gradient(#f3f3f3 0% 40%, #fcd671 40% 100%); + --rate: ${(props) => props.rate}%; + background: conic-gradient( + #fcd671 0% var(--rate), + #f3f3f3 var(--rate) 100% + ); border: 2px solid #e0e0e0; + transition: --rate 0.8s ease; `; const Circle_status = styled.div` @@ -310,13 +364,25 @@ const Bar_graphbox = styled.div` padding: 0 50px; display: flex; justify-content: space-around; + align-items: flex-end; overflow-y: hidden; `; const Bar_box = styled.div` width: 60px; height: 100%; - margin-top: 200px; // 기본값 235px + display: flex; + flex-direction: column; + justify-content: flex-end; + gap: 6px; +`; + +const Emotion_bar = styled.div` + width: 100%; + height: ${(props) => props.height}%; + border-radius: 12px 12px 0 0; + background-color: ${(props) => props.bgColor}; + transition: height 0.8s ease; `; const Bar_date = styled.p` @@ -328,13 +394,6 @@ const Bar_date = styled.p` margin-bottom: 6px; `; -const Emotion_bar = styled.div` - width: 100%; - height: 100%; - border-radius: 12px; - background-color: ${(props) => props.bgColor}; -`; - const Static_Message = styled.div` width: 100%; height: 122px; From ce298ab90cbbe5701cb875745f3e308fcdd941a6 Mon Sep 17 00:00:00 2001 From: pbc1001 Date: Fri, 10 Apr 2026 19:36:33 +0900 Subject: [PATCH 27/84] =?UTF-8?q?fix:=20emotionDistribution=EC=9D=B4=20nul?= =?UTF-8?q?l=EC=9D=BC=20=EA=B2=BD=EC=9A=B0=EB=A5=BC=20=EC=9C=84=ED=95=9C?= =?UTF-8?q?=20=EC=98=B5=EC=85=94=EB=84=90=20=EC=B2=B4=EC=9D=B4=EB=8B=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Statics.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Statics.jsx b/src/pages/Statics.jsx index 952a57d..f46e3b4 100644 --- a/src/pages/Statics.jsx +++ b/src/pages/Statics.jsx @@ -119,7 +119,7 @@ const Statics = () => { {emotionKeys.map((key, index) => { const value = statistics - ? (statistics.emotionDistribution.values[key] ?? 0) + ? (statistics?.emotionDistribution.values[key] ?? 0) : 0; const height = maxEmotion > 0 ? (value / maxEmotion) * 100 : 0; From a06e6ca04016bea782eadc4a8c2a25b34b23a29d Mon Sep 17 00:00:00 2001 From: pbc1001 Date: Fri, 10 Apr 2026 19:40:40 +0900 Subject: [PATCH 28/84] =?UTF-8?q?fix:=20=EB=B9=A0=EB=A5=B8=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=EC=98=A4=EB=A5=98?= =?UTF-8?q?=EB=B0=9C=EC=83=9D=EC=9D=84=20=EB=A7=89=EA=B8=B0=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20Abortcontroller=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Statics.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/Statics.jsx b/src/pages/Statics.jsx index f46e3b4..a200155 100644 --- a/src/pages/Statics.jsx +++ b/src/pages/Statics.jsx @@ -37,16 +37,19 @@ const Statics = () => { : 1; useEffect(() => { + const controller = new AbortController(); const fetchStatistics = async () => { try { - const res = await getStatistics(yearNum, dateNum); + const res = await getStatistics(yearNum, dateNum, controller.signal); console.log("통계 데이터:", res.data); setStatistics(res.data); } catch (error) { + if(error.name === 'AbortError') return; console.error("통계 로드 실패:", error); } }; fetchStatistics(); + return () => controller.abort(); }, [yearNum, dateNum]); const date_minus = () => { From 33e38963452e0de3b8576fca9b7dd069262825df Mon Sep 17 00:00:00 2001 From: pbc1001 Date: Fri, 10 Apr 2026 19:44:10 +0900 Subject: [PATCH 29/84] =?UTF-8?q?Terminal=20fix:=20emotionDistribution?= =?UTF-8?q?=EC=9D=B4=20null=EC=9D=BC=20=EA=B2=BD=EC=9A=B0=EB=A5=BC=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=EC=98=B5=EC=85=94=EB=84=90=20=EC=B2=B4?= =?UTF-8?q?=EC=9D=B4=EB=8B=9D(2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Statics.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Statics.jsx b/src/pages/Statics.jsx index a200155..915923e 100644 --- a/src/pages/Statics.jsx +++ b/src/pages/Statics.jsx @@ -31,7 +31,7 @@ const Statics = () => { const maxEmotion = statistics ? Math.max( ...emotionKeys.map( - (key) => statistics.emotionDistribution.values[key] ?? 0, + (key) => statistics?.emotionDistribution.values[key] ?? 0, ), ) : 1; From bdde1260a334affe8cc51e85ba452c2209461fa8 Mon Sep 17 00:00:00 2001 From: pbc1001 Date: Fri, 10 Apr 2026 00:30:26 +0900 Subject: [PATCH 30/84] =?UTF-8?q?feat:=20=EC=82=AC=EC=A7=84=EC=B2=A9=20api?= =?UTF-8?q?=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 데이터 가져오기 - 화면에 표시하기(미완) --- src/apis/photo.api.js | 15 ++++ src/pages/PhotoBook.jsx | 165 ++++++++++++++++++++++++---------------- 2 files changed, 115 insertions(+), 65 deletions(-) create mode 100644 src/apis/photo.api.js diff --git a/src/apis/photo.api.js b/src/apis/photo.api.js new file mode 100644 index 0000000..d03e7e9 --- /dev/null +++ b/src/apis/photo.api.js @@ -0,0 +1,15 @@ +import instance from "./instance"; + +export const getMyGallery = async (yearMonth, tag = "") => { + const response = await instance.get('/api/v1/diaries?imageType=MANUAL&hasPhoto=true', { + params: { + imageType: 'MANUAL', + hasPhoto: true, + yearMonth: yearMonth, + limit: 32, + tag: tag || null, + sort: 'createdAt,desc' + } + }); + return response.data; +}; \ No newline at end of file diff --git a/src/pages/PhotoBook.jsx b/src/pages/PhotoBook.jsx index 45a3bbc..793addf 100644 --- a/src/pages/PhotoBook.jsx +++ b/src/pages/PhotoBook.jsx @@ -1,31 +1,64 @@ -// 사진첩 - import styled from "@emotion/styled"; import Header from "../components/Header.jsx"; -import Test_img from "../assets/Test.svg"; import Arrow from "../assets/Arrow.svg"; import Reverse_Arrow from "../assets/Rev-Arrow.svg"; import Search_tag from "../assets/search_tag.svg"; -import { useState } from "react"; +import { useState, useEffect } from "react"; +import { getMyGallery } from "../apis/photo.api.js"; const Photo_Book = () => { - const [dateNum, setDateNum] = useState(1); + const [dateNum, setDateNum] = useState(4); const [yearNum, setYearNum] = useState(2026); + const [galleryData, setGalleryData] = useState([]); + const [searchTerm, setSearchTerm] = useState(""); + const [loading, setLoading] = useState(false); + + const formatYearMonth = (y, m) => { + return `${y}-${String(m).padStart(2, "0")}`; + }; + + const fetchGallery = async () => { + setLoading(true); + try { + const currentYM = formatYearMonth(yearNum, dateNum); + const res = await getMyGallery(currentYM, searchTerm); + + if (res.data && res.data.length > 0) { + setGalleryData(res.data[0].diaries); + } else { + setGalleryData([]); + } + } catch (error) { + console.error("갤러리 로드 실패:", error); + setGalleryData([]); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchGallery(); + }, [dateNum, yearNum]); + + const handleSearch = (e) => { + if (e.key === "Enter") fetchGallery(); + }; const date_minus = () => { if (dateNum > 1) { setDateNum(dateNum - 1); - } - if (dateNum <= 1) { - (setDateNum(12), setYearNum(yearNum - 1)); + } else { + setDateNum(12); + setYearNum(yearNum - 1); } }; + const date_plus = () => { if (dateNum < 12) { setDateNum(dateNum + 1); - } - if (dateNum >= 12) { - (setDateNum(1), setYearNum(yearNum + 1)); + } else { + setDateNum(1); + setYearNum(yearNum + 1); } }; @@ -34,74 +67,56 @@ const Photo_Book = () => {
- + prev {yearNum}년 {dateNum} - + next + - - + setSearchTerm(e.target.value)} + onKeyDown={handleSearch} + /> + search - - - 12일 - - - - - 12일 - - - - - 12일 - - - - - 12일 - - - - - 12일 - - - - - 12일 - - - - - 12일 - - - - - 12일 - - - - - 12일 - - - - - 12일 - - + {loading ? ( +

사진을 불러오는 중...

+ ) : galleryData.length > 0 ? ( + galleryData.map((item) => ( + window.location.href=`/diary/${item.id}`}> + diary + {item.createdAt.split('-')[2]}일 + + + )) + ) : ( + + + 사진이 없어요... + + + 일기에 추억을 남기러 갈까요? + + + )}
+ + {/* 페이징 처리: 명세서상 한 달 단위로 32개를 가져오므로, + 만약 32개가 꽉 찼다면 '더보기' 버튼을 만들거나 다음 페이지 API를 호출해야 합니다. + 현재는 달력 이동 방식으로 충분히 커버 가능합니다. */}
); }; + + const Body = styled.div` width: 100vw; height: 100vh; @@ -216,4 +231,24 @@ const Photo_date = styled.div` z-index: 2; `; + +const NoData = styled.div` + width: 100%; + height: 500px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +`; +const Nophoto = styled.p` + font-weight: 500; + font-size: 14px; + color: #828282; +` +const Letsgo = styled.p` + font-weight: 600; + font-size: 20px; + color: #4f4f4f; +` + export default Photo_Book; From fe1a42214d6bf54f98430480159681437cb46ac5 Mon Sep 17 00:00:00 2001 From: pbc1001 Date: Fri, 10 Apr 2026 00:42:32 +0900 Subject: [PATCH 31/84] Revert "Merge branch 'develop' into feat/refresh-api" This reverts commit f3a30e7c3bbba616ab302b7dbf4c70bac03d314f, reversing changes made to 01992ab512f1a0613f95f47835a9c9c61bc7638c. --- src/apis/instance.js | 11 +- src/apis/mypages.api.js | 43 ---- src/pages/MyPage.jsx | 532 +++++++++++++--------------------------- src/pages/SignUp.jsx | 32 +-- 4 files changed, 192 insertions(+), 426 deletions(-) delete mode 100644 src/apis/mypages.api.js diff --git a/src/apis/instance.js b/src/apis/instance.js index c7d1c45..9e01023 100644 --- a/src/apis/instance.js +++ b/src/apis/instance.js @@ -22,16 +22,13 @@ const processQueue = (error, token = null) => { instance.interceptors.request.use( (config) => { - const token = localStorage.getItem("accessToken"); - - if (token) { - config.headers.Authorization = `Bearer ${token}`; + const accessToken = localStorage.getItem("accessToken"); + if (accessToken) { + config.headers.Authorization = `Bearer ${accessToken}`; } return config; }, - (error) => { - return Promise.reject(error); - } + (error) => Promise.reject(error) ); instance.interceptors.response.use( diff --git a/src/apis/mypages.api.js b/src/apis/mypages.api.js deleted file mode 100644 index b7d3a32..0000000 --- a/src/apis/mypages.api.js +++ /dev/null @@ -1,43 +0,0 @@ -import instance from "./instance"; - -export const getMyInfo = async () => { - const response = await instance.get("/api/v1/users/me"); - return response.data; -}; - -export const withdrawAccount = async () => { - const response = await instance.delete("/api/v1/auth/me"); - return response.data; -}; - -export const updateProfile = async (formData) => { - const response = await instance.patch('/api/v1/users/me/profile', formData, { - headers: { - "Content-Type": undefined, - }, - }); - return response.data; -}; - -export const changePassword = async (passwordData) => { - const response = await instance.patch('/api/v1/users/me/password', passwordData); - return response.data; -}; - -export const deleteUser = async () => { - const refreshToken = localStorage.getItem("refreshToken"); - const response = await instance.delete('/api/v1/users/me', { - data: { - refreshToken: refreshToken - } - }); - return response.data; -}; - -export const logout = async () => { - const refreshToken = localStorage.getItem("refreshToken"); - const response = await instance.post('/api/v1/auth/logout', { - refreshToken: refreshToken - }); - return response.data; -}; \ No newline at end of file diff --git a/src/pages/MyPage.jsx b/src/pages/MyPage.jsx index 06ebcb2..f5c4018 100644 --- a/src/pages/MyPage.jsx +++ b/src/pages/MyPage.jsx @@ -1,3 +1,5 @@ +// 마이페이지 + import styled from "@emotion/styled"; import Header from "../components/Header.jsx"; import Happy from "../assets/Happy.svg"; @@ -6,179 +8,31 @@ import Profile from "../assets/Profile.svg"; import ReverseArrow from "../assets/Rev-Arrow.svg"; import Sad from "../assets/Sadness.svg"; import Arrow from "../assets/Arrow.svg"; -import Random from "../assets/random.svg"; -import Upload_btnimg from "../assets/Upload_btn.svg"; -import { useState, useEffect, useRef } from "react"; +import Random from "../assets/random.svg" +import Upload_btnimg from "../assets/Upload_btn.svg" +import { useState } from "react"; import { useNavigate } from "react-router-dom"; -import { getMyInfo, updateProfile, changePassword, deleteUser, logout } from "../apis/mypages.api.js"; const Mypage = () => { - const [userInfo, setUserInfo] = useState(null); - const [isLoading, setIsLoading] = useState(true); + + const user_info = { + "status": "success", + "data": { + "userId": 102, + "email": "seungri@example.com", + "nickname": "쿠수리", + "profileImageUrl": "https://s3.../profiles/user_102.png", + "createdAt": "2025-06-09T10:00:00Z", + "updatedAt": "2026-03-04T23:55:00Z" + } +} const [isChangeModal, setIsChangeModal] = useState(false); const [isDeleteModal, setIsDeleteModal] = useState(false); const [isPwModal, setIsPwModal] = useState(false); - const [newNickname, setNewNickname] = useState(""); - const [previewImage, setPreviewImage] = useState(null); - const [selectedFile, setSelectedFile] = useState(null); - - const [newPassword, setNewPassword] = useState(""); - const [confirmPassword, setConfirmPassword] = useState(""); - - const fileInputRef = useRef(null); - const navigate = useNavigate(); - - useEffect(() => { - const fetchUserData = async () => { - try { - const res = await getMyInfo(); - setUserInfo(res.data); - } catch (error) { - console.error("유저 정보 로드 실패:", error); - } finally { - setIsLoading(false); - } - }; - fetchUserData(); - }, []); - - const [password, setPassword] = useState(""); - - const handleOpenEditModal = () => { - if (!userInfo) return; - setNewNickname(userInfo.nickname); - setPreviewImage(userInfo.profileImageUrl || Profile); - setSelectedFile(null); - setIsChangeModal(true); - }; - - const handleOpenDeleteModal = () => { - if (!userInfo) return; - setIsDeleteModal(true); - }; - - const handleOpenPwModal = () => { - if (!userInfo) return; - setConfirmPassword(""); - setPassword("") - setNewPassword("") - setIsPwModal(true); - }; - - const handleFileChange = (e) => { - const file = e.target.files[0]; - if (file) { - setSelectedFile(file); - const reader = new FileReader(); - reader.onloadend = () => { - setPreviewImage(reader.result); - }; - reader.readAsDataURL(file); - } - }; - -const handleSaveProfile = async () => { - if (!newNickname.trim()) return alert("닉네임을 입력해주세요."); - - try { - const formData = new FormData(); - - const jsonBlob = new Blob( - [JSON.stringify({ nickname: newNickname })], - { type: "application/json" } - ); - - formData.append("data", jsonBlob); - formData.append("image", selectedFile ? selectedFile : null); - - const res = await updateProfile(formData); - - if (res.status === 200) { - alert("프로필이 성공적으로 변경되었습니다."); - setUserInfo((prev) => ({ - ...prev, - nickname: newNickname, - profileImageUrl: previewImage, - })); - setIsChangeModal(false); - } - } catch (error) { - console.error("수정 실패:", error); - alert("수정 중 오류가 발생했습니다."); - } -}; - - const handleChangePassword = async () => { - if (!newPassword || newPassword !== confirmPassword || !password) { - return alert("비밀번호가 일치하지 않거나 입력되지 않았습니다."); - } - - try { - const res = await changePassword({ - oldPassword: password, - newPassword: newPassword, - confirmPassword: confirmPassword - - }); - if (res.status === 200) { - alert("비밀번호가 변경되었습니다."); - setIsPwModal(false); - } - } catch (error) { - alert( - "비밀번호 변경 실패: " + (error.response?.data?.message || "오류 발생"), - ); - } - }; - -const handleDeleteAccount = async () => { - try { - const res = await deleteUser(); - if (res.status === 200) { - alert("탈퇴가 완료되었습니다."); - localStorage.clear(); - navigate("/"); - window.location.reload(); - } - } catch (error) { - console.error("탈퇴 에러 상세:", error.response?.data); - alert(error.response?.data?.message || "탈퇴 처리 중 오류가 발생했습니다."); - } -}; - - -const handleLogout = async () => { - if (window.confirm("로그아웃 하시겠습니까?")) { - try { - await logout(); - localStorage.removeItem("accessToken"); - localStorage.removeItem("refreshToken"); - navigate("/"); - window.location.reload(); - } catch (error) { - console.error("로그아웃 실패:", error); - alert("로그아웃 실패") - } - } -}; - - if (isLoading) - return ( - -
-
로딩 중...
- - ); - if (!userInfo) - return ( - -
-
정보를 불러올 수 없습니다.
- - ); - + const navigate = useNavigate() + return (
@@ -186,27 +40,22 @@ const handleLogout = async () => { - + {setIsChangeModal(true)}}> - Profile + + + + - {userInfo.nickname} - - Edit + {user_info.data.nickname} + {setIsChangeModal(true)}}> + - {userInfo.email} + {user_info.data.email} @@ -218,16 +67,14 @@ const handleLogout = async () => { 비밀번호 변경 - - Change + {setIsPwModal(true)}}> + - 로그아웃 - - 회원 탈퇴 - + {navigate("/")}}>로그아웃 + {setIsDeleteModal(true)}}>회원 탈퇴 @@ -246,125 +93,89 @@ const handleLogout = async () => { - {/* 계정 탈퇴 모달 */} - {isDeleteModal && ( - setIsDeleteModal(false)}> - e.stopPropagation()}> - - - Sad - - - 정말 탈퇴하시겠어요? - - 지금 탈퇴하시면 작성하신 일기 추억들을 다시 볼 수 없게됩니다.{" "} - 그래도 탈퇴를 진행하시겠어요? - - - - - - 회원탈퇴 - - setIsDeleteModal(false)}> - 취소 - - - - - )} + {/*계정 탈퇴 모달*/} +{isDeleteModal && + {setIsDeleteModal(false)}}> + {e.stopPropagation()}}> + + + + + + 정말 탈퇴하시겠어요? + + 지금 탈퇴하시면 작성하신 일기 추억들을 다시 볼 수 없게됩니다.{" "} + 그래도 탈퇴를 진행하시겠어요? + + + + + + + {navigate("/")}}>회원탈퇴 + {setIsDeleteModal(false)}}>취소 + + + +} {/* 비밀번호 변경 모달 */} - {isPwModal && ( - setIsPwModal(false)}> - e.stopPropagation()}> - setIsPwModal(false)}> - Back - - - + {isPwModal&& + {setIsPwModal(false)}}> + {e.stopPropagation()}}> + {setIsPwModal(false)}}> + + + + - 기존 비밀번호 - setPassword(e.target.value)} - /> - - - 비밀번호 - setNewPassword(e.target.value)} - /> - - - 비밀번호 확인 - setConfirmPassword(e.target.value)} - /> - - - - 저장 - - - - - )} - - {/* 프로필 변경 모달 */} - {isChangeModal && ( - setIsChangeModal(false)}> - e.stopPropagation()}> - setIsChangeModal(false)}> - Close - - - - - Preview - - Random - - - - 닉네임 변경 - setNewNickname(e.target.value)} - placeholder="변경할 닉네임을 입력해주세요" - /> - - - - - fileInputRef.current.click()}> - - 파일 업로드 - - 저장 - - - - - )} + 비밀번호 + + + + 비밀번호 확인 + + + + {setIsPwModal(false)}}>저장 + + + +} + + {/*프로필 변경 모달*/} + {isChangeModal && + {setIsChangeModal(false)}}> + {e.stopPropagation()}}> + {setIsChangeModal(false)}}> + + + + + + + + + + + + + + 닉네임 변경 + + + + + + + 파일 업로드 + + {setIsChangeModal(false)}}>저장 + + + + +} ); }; @@ -419,22 +230,25 @@ const Profile_img = styled.div` height: 60px; border: 2px solid #e0e0e0; border-radius: 50px; + background-color: #cfcfcf; display: flex; align-items: center; justify-content: center; position: relative; - overflow: hidden; cursor: pointer; `; const Img_box = styled.div` - width: 60px; - height: 60px; - background-color: #cfcfcf; + width: 40px; + height: 40px; display: flex; align-items: center; justify-content: center; `; - +const Img_edit = styled.div` + position: absolute; + bottom: -4px; + right: 2px; +`; const Nickname_box = styled.div` display: flex; flex-direction: column; @@ -613,7 +427,7 @@ const Text_box = styled.div` display: flex; flex-direction: column; align-items: center; -`; +` const Check_title = styled.div` font-size: 20px; font-weight: 600; @@ -680,7 +494,7 @@ const Password_back = styled.div` const Password_modal = styled.div` position: relative; width: 480px; - height: 432px; + height: 352px; display: flex; flex-direction: column; align-items: center; @@ -698,7 +512,7 @@ const Out_modal = styled.div` cursor: pointer; `; const Change_box = styled.div` - display: flex; + display: flex ; flex-direction: column; justify-content: space-between; width: 100%; @@ -711,7 +525,7 @@ const Input_box = styled.div` display: flex; flex-direction: column; gap: 16px; -`; +` const Pass_box = styled.div` width: 100%; height: 100%; @@ -725,16 +539,16 @@ const Change_pass = styled.p` color: #575141; `; const Pass_input = styled.input` - width: 100%; - height: 100%; - font-weight: 500; - font-size: 16px; - border: 1px solid #cfd3dc; - padding: 9.5px 16px; - border-radius: 12px; - ::placeholder { - color: #cfd3dc; - } +width: 100%; +height: 100%; +font-weight: 500; +font-size: 16px; +border: 1px solid #CFD3DC; +padding: 9.5px 16px; +border-radius: 12px; +::placeholder { + color: #CFD3DC; +} `; const Checking_box = styled.div` width: 100%; @@ -742,6 +556,7 @@ const Checking_box = styled.div` display: flex; flex-direction: column; gap: 4px; + `; const Check_pass = styled.div` font-size: 16px; @@ -749,16 +564,16 @@ const Check_pass = styled.div` color: #575141; `; const Check_input = styled.input` - width: 100%; - height: 100%; - font-weight: 500; - font-size: 16px; - border: 1px solid #cfd3dc; - padding: 9.5px 16px; - border-radius: 12px; - ::placeholder { - color: #cfd3dc; - } +width: 100%; +height: 100%; +font-weight: 500; +font-size: 16px; +border: 1px solid #CFD3DC; +padding: 9.5px 16px; +border-radius: 12px; +::placeholder { + color: #CFD3DC; +} `; const Save_button = styled.div` width: 100%; @@ -768,7 +583,7 @@ const Save_button = styled.div` justify-content: center; font-size: 16px; font-weight: 600; - background-color: #fcd671; + background-color: #FCD671; color: #575141; border-radius: 12px; cursor: pointer; @@ -785,7 +600,7 @@ const Profile_modalback = styled.div` display: flex; align-items: center; justify-content: center; -`; +` const Profile_modal = styled.div` width: 480px; @@ -796,7 +611,7 @@ const Profile_modal = styled.div` display: flex; flex-direction: column; border-radius: 12px; -`; +` const Pf_modalmain = styled.div` width: 100%; height: 100%; @@ -804,75 +619,79 @@ const Pf_modalmain = styled.div` flex-direction: column; justify-content: space-between; gap: 32px; -`; +` const Profile_change = styled.div` gap: 24px; display: flex; flex-direction: column; align-items: center; -`; +` const Change_imgbox = styled.div` width: 60px; height: 60px; - background-color: #cfcfcf; - border: 2px solid #e0e0e0; + background-color: #CFCFCF; + border: 2px solid #E0E0E0; border-radius: 50px; display: flex; align-items: center; justify-content: center; + position: relative; box-sizing: border-box; - overflow: hidden; -`; - +` +const Change_img = styled.div` +width: 40px; +height: 40px; +` const Profile_rand = styled.div` position: absolute; - right: 210px; - top: 105px; + right: -7px; + bottom: -7px; width: 22px; height: 22px; display: flex; background-color: #fff; - border: 1px solid #f3f3f3; + border: 1px solid #F3F3F3; border-radius: 12px; box-sizing: border-box; align-items: center; justify-content: center; -`; +` const Change_nickbox = styled.div` width: 100%; display: flex; flex-direction: column; gap: 4px; -`; +` const Nick_text = styled.p` font-size: 16px; font-weight: 500; color: #575141; -`; +` const Nick_input = styled.input` width: 100%; height: auto; padding: 9.5px 16px; font-size: 16px; font-weight: 500; - border: 1px solid #cfd3dc; + border: 1px solid #CFD3DC; border-radius: 12px; - ::placeholder { - color: #cfd3dc; + ::placeholder{ + color: #CFD3DC; } - :focus { + :focus{ outline: none; } -`; +` const Profile_btnbox = styled.div` width: 100%; height: 100%; display: flex; gap: 12px; -`; + +` const Img_upload = styled.button` width: 100%; - border: 2px solid #fcd671; + border: 2px solid #FCD671; display: flex; gap: 10px; align-items: center; @@ -883,8 +702,7 @@ const Img_upload = styled.button` font-size: 16px; border-radius: 12px; box-sizing: border-box; - cursor: pointer; -`; +` const Save_btn = styled.button` width: 100%; background-color: #fcd671; @@ -898,6 +716,6 @@ const Save_btn = styled.button` font-size: 16px; border-radius: 12px; cursor: pointer; -`; +` -export default Mypage; +export default Mypage; \ No newline at end of file diff --git a/src/pages/SignUp.jsx b/src/pages/SignUp.jsx index e779fa1..2190595 100644 --- a/src/pages/SignUp.jsx +++ b/src/pages/SignUp.jsx @@ -28,25 +28,19 @@ const SignUp = () => { const [pwMessage, setPwMessage] = useState(""); const [checkPwMessage, setCheckPwMessage] = useState(""); -useEffect(() => { - let timer; - - if (showCode && timeleft > 0) { - timer = setInterval(() => { - setTimeleft((prev) => { - if (prev <= 1) { - clearInterval(timer); - alert("인증 시간이 만료되었습니다. 다시 시도해주세요."); - setShowCode(false); - return 180; - } - return prev - 1; - }); - }, 1000); - } - - return () => clearInterval(timer); -}, [showCode]); + useEffect(() => { + let timer; + if (showCode && timeleft > 0) { + timer = setInterval(() => { + setTimeleft((prev) => prev - 1); + }, 1000); + } else if (timeleft === 0) { + alert("인증 시간이 만료되었습니다. 다시 시도해주세요."); + setShowCode(false); + setTimeleft(180); + } + return () => clearInterval(timer); + }, [showCode, timeleft]); const formatTime = (seconds) => { const minutes = Math.floor(seconds / 60); From 7931f51195193a59ba8645a35310e9281377074e Mon Sep 17 00:00:00 2001 From: pbc1001 Date: Fri, 10 Apr 2026 00:43:01 +0900 Subject: [PATCH 32/84] Reapply "Merge branch 'develop' into feat/refresh-api" This reverts commit bd131fb338b425392885d925c4572148da002304. --- src/apis/instance.js | 11 +- src/apis/mypages.api.js | 43 ++++ src/pages/MyPage.jsx | 532 +++++++++++++++++++++++++++------------- src/pages/SignUp.jsx | 32 ++- 4 files changed, 426 insertions(+), 192 deletions(-) create mode 100644 src/apis/mypages.api.js diff --git a/src/apis/instance.js b/src/apis/instance.js index 9e01023..c7d1c45 100644 --- a/src/apis/instance.js +++ b/src/apis/instance.js @@ -22,13 +22,16 @@ const processQueue = (error, token = null) => { instance.interceptors.request.use( (config) => { - const accessToken = localStorage.getItem("accessToken"); - if (accessToken) { - config.headers.Authorization = `Bearer ${accessToken}`; + const token = localStorage.getItem("accessToken"); + + if (token) { + config.headers.Authorization = `Bearer ${token}`; } return config; }, - (error) => Promise.reject(error) + (error) => { + return Promise.reject(error); + } ); instance.interceptors.response.use( diff --git a/src/apis/mypages.api.js b/src/apis/mypages.api.js new file mode 100644 index 0000000..b7d3a32 --- /dev/null +++ b/src/apis/mypages.api.js @@ -0,0 +1,43 @@ +import instance from "./instance"; + +export const getMyInfo = async () => { + const response = await instance.get("/api/v1/users/me"); + return response.data; +}; + +export const withdrawAccount = async () => { + const response = await instance.delete("/api/v1/auth/me"); + return response.data; +}; + +export const updateProfile = async (formData) => { + const response = await instance.patch('/api/v1/users/me/profile', formData, { + headers: { + "Content-Type": undefined, + }, + }); + return response.data; +}; + +export const changePassword = async (passwordData) => { + const response = await instance.patch('/api/v1/users/me/password', passwordData); + return response.data; +}; + +export const deleteUser = async () => { + const refreshToken = localStorage.getItem("refreshToken"); + const response = await instance.delete('/api/v1/users/me', { + data: { + refreshToken: refreshToken + } + }); + return response.data; +}; + +export const logout = async () => { + const refreshToken = localStorage.getItem("refreshToken"); + const response = await instance.post('/api/v1/auth/logout', { + refreshToken: refreshToken + }); + return response.data; +}; \ No newline at end of file diff --git a/src/pages/MyPage.jsx b/src/pages/MyPage.jsx index f5c4018..06ebcb2 100644 --- a/src/pages/MyPage.jsx +++ b/src/pages/MyPage.jsx @@ -1,5 +1,3 @@ -// 마이페이지 - import styled from "@emotion/styled"; import Header from "../components/Header.jsx"; import Happy from "../assets/Happy.svg"; @@ -8,31 +6,179 @@ import Profile from "../assets/Profile.svg"; import ReverseArrow from "../assets/Rev-Arrow.svg"; import Sad from "../assets/Sadness.svg"; import Arrow from "../assets/Arrow.svg"; -import Random from "../assets/random.svg" -import Upload_btnimg from "../assets/Upload_btn.svg" -import { useState } from "react"; +import Random from "../assets/random.svg"; +import Upload_btnimg from "../assets/Upload_btn.svg"; +import { useState, useEffect, useRef } from "react"; import { useNavigate } from "react-router-dom"; +import { getMyInfo, updateProfile, changePassword, deleteUser, logout } from "../apis/mypages.api.js"; const Mypage = () => { - - const user_info = { - "status": "success", - "data": { - "userId": 102, - "email": "seungri@example.com", - "nickname": "쿠수리", - "profileImageUrl": "https://s3.../profiles/user_102.png", - "createdAt": "2025-06-09T10:00:00Z", - "updatedAt": "2026-03-04T23:55:00Z" - } -} + const [userInfo, setUserInfo] = useState(null); + const [isLoading, setIsLoading] = useState(true); const [isChangeModal, setIsChangeModal] = useState(false); const [isDeleteModal, setIsDeleteModal] = useState(false); const [isPwModal, setIsPwModal] = useState(false); - const navigate = useNavigate() - + const [newNickname, setNewNickname] = useState(""); + const [previewImage, setPreviewImage] = useState(null); + const [selectedFile, setSelectedFile] = useState(null); + + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + + const fileInputRef = useRef(null); + const navigate = useNavigate(); + + useEffect(() => { + const fetchUserData = async () => { + try { + const res = await getMyInfo(); + setUserInfo(res.data); + } catch (error) { + console.error("유저 정보 로드 실패:", error); + } finally { + setIsLoading(false); + } + }; + fetchUserData(); + }, []); + + const [password, setPassword] = useState(""); + + const handleOpenEditModal = () => { + if (!userInfo) return; + setNewNickname(userInfo.nickname); + setPreviewImage(userInfo.profileImageUrl || Profile); + setSelectedFile(null); + setIsChangeModal(true); + }; + + const handleOpenDeleteModal = () => { + if (!userInfo) return; + setIsDeleteModal(true); + }; + + const handleOpenPwModal = () => { + if (!userInfo) return; + setConfirmPassword(""); + setPassword("") + setNewPassword("") + setIsPwModal(true); + }; + + const handleFileChange = (e) => { + const file = e.target.files[0]; + if (file) { + setSelectedFile(file); + const reader = new FileReader(); + reader.onloadend = () => { + setPreviewImage(reader.result); + }; + reader.readAsDataURL(file); + } + }; + +const handleSaveProfile = async () => { + if (!newNickname.trim()) return alert("닉네임을 입력해주세요."); + + try { + const formData = new FormData(); + + const jsonBlob = new Blob( + [JSON.stringify({ nickname: newNickname })], + { type: "application/json" } + ); + + formData.append("data", jsonBlob); + formData.append("image", selectedFile ? selectedFile : null); + + const res = await updateProfile(formData); + + if (res.status === 200) { + alert("프로필이 성공적으로 변경되었습니다."); + setUserInfo((prev) => ({ + ...prev, + nickname: newNickname, + profileImageUrl: previewImage, + })); + setIsChangeModal(false); + } + } catch (error) { + console.error("수정 실패:", error); + alert("수정 중 오류가 발생했습니다."); + } +}; + + const handleChangePassword = async () => { + if (!newPassword || newPassword !== confirmPassword || !password) { + return alert("비밀번호가 일치하지 않거나 입력되지 않았습니다."); + } + + try { + const res = await changePassword({ + oldPassword: password, + newPassword: newPassword, + confirmPassword: confirmPassword + + }); + if (res.status === 200) { + alert("비밀번호가 변경되었습니다."); + setIsPwModal(false); + } + } catch (error) { + alert( + "비밀번호 변경 실패: " + (error.response?.data?.message || "오류 발생"), + ); + } + }; + +const handleDeleteAccount = async () => { + try { + const res = await deleteUser(); + if (res.status === 200) { + alert("탈퇴가 완료되었습니다."); + localStorage.clear(); + navigate("/"); + window.location.reload(); + } + } catch (error) { + console.error("탈퇴 에러 상세:", error.response?.data); + alert(error.response?.data?.message || "탈퇴 처리 중 오류가 발생했습니다."); + } +}; + + +const handleLogout = async () => { + if (window.confirm("로그아웃 하시겠습니까?")) { + try { + await logout(); + localStorage.removeItem("accessToken"); + localStorage.removeItem("refreshToken"); + navigate("/"); + window.location.reload(); + } catch (error) { + console.error("로그아웃 실패:", error); + alert("로그아웃 실패") + } + } +}; + + if (isLoading) + return ( + +
+
로딩 중...
+ + ); + if (!userInfo) + return ( + +
+
정보를 불러올 수 없습니다.
+ + ); + return (
@@ -40,22 +186,27 @@ const Mypage = () => { - {setIsChangeModal(true)}}> + - + Profile - - - - {user_info.data.nickname} - {setIsChangeModal(true)}}> - + {userInfo.nickname} + + Edit - {user_info.data.email} + {userInfo.email} @@ -67,14 +218,16 @@ const Mypage = () => { 비밀번호 변경 - {setIsPwModal(true)}}> - + + Change - {navigate("/")}}>로그아웃 - {setIsDeleteModal(true)}}>회원 탈퇴 + 로그아웃 + + 회원 탈퇴 + @@ -93,89 +246,125 @@ const Mypage = () => { - {/*계정 탈퇴 모달*/} -{isDeleteModal && - {setIsDeleteModal(false)}}> - {e.stopPropagation()}}> - - - - - - 정말 탈퇴하시겠어요? - - 지금 탈퇴하시면 작성하신 일기 추억들을 다시 볼 수 없게됩니다.{" "} - 그래도 탈퇴를 진행하시겠어요? - - - - - - - {navigate("/")}}>회원탈퇴 - {setIsDeleteModal(false)}}>취소 - - - -} + {/* 계정 탈퇴 모달 */} + {isDeleteModal && ( + setIsDeleteModal(false)}> + e.stopPropagation()}> + + + Sad + + + 정말 탈퇴하시겠어요? + + 지금 탈퇴하시면 작성하신 일기 추억들을 다시 볼 수 없게됩니다.{" "} + 그래도 탈퇴를 진행하시겠어요? + + + + + + 회원탈퇴 + + setIsDeleteModal(false)}> + 취소 + + + + + )} {/* 비밀번호 변경 모달 */} - {isPwModal&& - {setIsPwModal(false)}}> - {e.stopPropagation()}}> - {setIsPwModal(false)}}> - - - - + {isPwModal && ( + setIsPwModal(false)}> + e.stopPropagation()}> + setIsPwModal(false)}> + Back + + + - 비밀번호 - - - - 비밀번호 확인 - - - - {setIsPwModal(false)}}>저장 - - - -} - - {/*프로필 변경 모달*/} - {isChangeModal && - {setIsChangeModal(false)}}> - {e.stopPropagation()}}> - {setIsChangeModal(false)}}> - - - - - - - - - - - - - - 닉네임 변경 - - - - - - - 파일 업로드 - - {setIsChangeModal(false)}}>저장 - - - - -} + 기존 비밀번호 + setPassword(e.target.value)} + /> + + + 비밀번호 + setNewPassword(e.target.value)} + /> + + + 비밀번호 확인 + setConfirmPassword(e.target.value)} + /> + + + + 저장 + + + + + )} + + {/* 프로필 변경 모달 */} + {isChangeModal && ( + setIsChangeModal(false)}> + e.stopPropagation()}> + setIsChangeModal(false)}> + Close + + + + + Preview + + Random + + + + 닉네임 변경 + setNewNickname(e.target.value)} + placeholder="변경할 닉네임을 입력해주세요" + /> + + + + + fileInputRef.current.click()}> + + 파일 업로드 + + 저장 + + + + + )} ); }; @@ -230,25 +419,22 @@ const Profile_img = styled.div` height: 60px; border: 2px solid #e0e0e0; border-radius: 50px; - background-color: #cfcfcf; display: flex; align-items: center; justify-content: center; position: relative; + overflow: hidden; cursor: pointer; `; const Img_box = styled.div` - width: 40px; - height: 40px; + width: 60px; + height: 60px; + background-color: #cfcfcf; display: flex; align-items: center; justify-content: center; `; -const Img_edit = styled.div` - position: absolute; - bottom: -4px; - right: 2px; -`; + const Nickname_box = styled.div` display: flex; flex-direction: column; @@ -427,7 +613,7 @@ const Text_box = styled.div` display: flex; flex-direction: column; align-items: center; -` +`; const Check_title = styled.div` font-size: 20px; font-weight: 600; @@ -494,7 +680,7 @@ const Password_back = styled.div` const Password_modal = styled.div` position: relative; width: 480px; - height: 352px; + height: 432px; display: flex; flex-direction: column; align-items: center; @@ -512,7 +698,7 @@ const Out_modal = styled.div` cursor: pointer; `; const Change_box = styled.div` - display: flex ; + display: flex; flex-direction: column; justify-content: space-between; width: 100%; @@ -525,7 +711,7 @@ const Input_box = styled.div` display: flex; flex-direction: column; gap: 16px; -` +`; const Pass_box = styled.div` width: 100%; height: 100%; @@ -539,16 +725,16 @@ const Change_pass = styled.p` color: #575141; `; const Pass_input = styled.input` -width: 100%; -height: 100%; -font-weight: 500; -font-size: 16px; -border: 1px solid #CFD3DC; -padding: 9.5px 16px; -border-radius: 12px; -::placeholder { - color: #CFD3DC; -} + width: 100%; + height: 100%; + font-weight: 500; + font-size: 16px; + border: 1px solid #cfd3dc; + padding: 9.5px 16px; + border-radius: 12px; + ::placeholder { + color: #cfd3dc; + } `; const Checking_box = styled.div` width: 100%; @@ -556,7 +742,6 @@ const Checking_box = styled.div` display: flex; flex-direction: column; gap: 4px; - `; const Check_pass = styled.div` font-size: 16px; @@ -564,16 +749,16 @@ const Check_pass = styled.div` color: #575141; `; const Check_input = styled.input` -width: 100%; -height: 100%; -font-weight: 500; -font-size: 16px; -border: 1px solid #CFD3DC; -padding: 9.5px 16px; -border-radius: 12px; -::placeholder { - color: #CFD3DC; -} + width: 100%; + height: 100%; + font-weight: 500; + font-size: 16px; + border: 1px solid #cfd3dc; + padding: 9.5px 16px; + border-radius: 12px; + ::placeholder { + color: #cfd3dc; + } `; const Save_button = styled.div` width: 100%; @@ -583,7 +768,7 @@ const Save_button = styled.div` justify-content: center; font-size: 16px; font-weight: 600; - background-color: #FCD671; + background-color: #fcd671; color: #575141; border-radius: 12px; cursor: pointer; @@ -600,7 +785,7 @@ const Profile_modalback = styled.div` display: flex; align-items: center; justify-content: center; -` +`; const Profile_modal = styled.div` width: 480px; @@ -611,7 +796,7 @@ const Profile_modal = styled.div` display: flex; flex-direction: column; border-radius: 12px; -` +`; const Pf_modalmain = styled.div` width: 100%; height: 100%; @@ -619,79 +804,75 @@ const Pf_modalmain = styled.div` flex-direction: column; justify-content: space-between; gap: 32px; -` +`; const Profile_change = styled.div` gap: 24px; display: flex; flex-direction: column; align-items: center; -` +`; const Change_imgbox = styled.div` width: 60px; height: 60px; - background-color: #CFCFCF; - border: 2px solid #E0E0E0; + background-color: #cfcfcf; + border: 2px solid #e0e0e0; border-radius: 50px; display: flex; align-items: center; justify-content: center; - position: relative; box-sizing: border-box; -` -const Change_img = styled.div` -width: 40px; -height: 40px; -` + overflow: hidden; +`; + const Profile_rand = styled.div` position: absolute; - right: -7px; - bottom: -7px; + right: 210px; + top: 105px; width: 22px; height: 22px; display: flex; background-color: #fff; - border: 1px solid #F3F3F3; + border: 1px solid #f3f3f3; border-radius: 12px; box-sizing: border-box; align-items: center; justify-content: center; -` +`; const Change_nickbox = styled.div` width: 100%; display: flex; flex-direction: column; gap: 4px; -` +`; const Nick_text = styled.p` font-size: 16px; font-weight: 500; color: #575141; -` +`; const Nick_input = styled.input` width: 100%; height: auto; padding: 9.5px 16px; font-size: 16px; font-weight: 500; - border: 1px solid #CFD3DC; + border: 1px solid #cfd3dc; border-radius: 12px; - ::placeholder{ - color: #CFD3DC; + ::placeholder { + color: #cfd3dc; } - :focus{ + :focus { outline: none; } -` +`; const Profile_btnbox = styled.div` width: 100%; height: 100%; display: flex; gap: 12px; - -` +`; const Img_upload = styled.button` width: 100%; - border: 2px solid #FCD671; + border: 2px solid #fcd671; display: flex; gap: 10px; align-items: center; @@ -702,7 +883,8 @@ const Img_upload = styled.button` font-size: 16px; border-radius: 12px; box-sizing: border-box; -` + cursor: pointer; +`; const Save_btn = styled.button` width: 100%; background-color: #fcd671; @@ -716,6 +898,6 @@ const Save_btn = styled.button` font-size: 16px; border-radius: 12px; cursor: pointer; -` +`; -export default Mypage; \ No newline at end of file +export default Mypage; diff --git a/src/pages/SignUp.jsx b/src/pages/SignUp.jsx index 2190595..e779fa1 100644 --- a/src/pages/SignUp.jsx +++ b/src/pages/SignUp.jsx @@ -28,19 +28,25 @@ const SignUp = () => { const [pwMessage, setPwMessage] = useState(""); const [checkPwMessage, setCheckPwMessage] = useState(""); - useEffect(() => { - let timer; - if (showCode && timeleft > 0) { - timer = setInterval(() => { - setTimeleft((prev) => prev - 1); - }, 1000); - } else if (timeleft === 0) { - alert("인증 시간이 만료되었습니다. 다시 시도해주세요."); - setShowCode(false); - setTimeleft(180); - } - return () => clearInterval(timer); - }, [showCode, timeleft]); +useEffect(() => { + let timer; + + if (showCode && timeleft > 0) { + timer = setInterval(() => { + setTimeleft((prev) => { + if (prev <= 1) { + clearInterval(timer); + alert("인증 시간이 만료되었습니다. 다시 시도해주세요."); + setShowCode(false); + return 180; + } + return prev - 1; + }); + }, 1000); + } + + return () => clearInterval(timer); +}, [showCode]); const formatTime = (seconds) => { const minutes = Math.floor(seconds / 60); From 8d4da669d594c9882e031bf427c2e12ab1da97e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B8=EC=95=84?= Date: Sat, 11 Apr 2026 13:36:55 +0900 Subject: [PATCH 33/84] =?UTF-8?q?refator:=20=ED=86=A0=ED=81=B0=20=EB=A7=8C?= =?UTF-8?q?=EB=A3=8C=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20=EB=B0=8F?= =?UTF-8?q?=20header=EC=97=90=20Authorization=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/instance.js | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/apis/instance.js b/src/apis/instance.js index c7d1c45..8d54cba 100644 --- a/src/apis/instance.js +++ b/src/apis/instance.js @@ -2,7 +2,7 @@ import axios from "axios"; const instance = axios.create({ baseURL: import.meta.env.VITE_BASE_URL, - timeout: 5000, + timeout: 30000, headers: { "Content-Type": "application/json" }, }); @@ -17,21 +17,18 @@ const processQueue = (error, token = null) => { prom.resolve(token); } }); - failedQueue = []; + failedQueue = []; }; instance.interceptors.request.use( (config) => { const token = localStorage.getItem("accessToken"); - if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, - (error) => { - return Promise.reject(error); - } + (error) => Promise.reject(error), ); instance.interceptors.response.use( @@ -56,43 +53,45 @@ instance.interceptors.response.use( try { const refreshToken = localStorage.getItem("refreshToken"); + const accessToken = localStorage.getItem("accessToken"); + const res = await axios.post( `${import.meta.env.VITE_BASE_URL}/api/v1/auth/reissue`, - { refreshToken: refreshToken } + { refreshToken }, + { headers: { Authorization: `Bearer ${accessToken}` } }, ); if (res.data.status === "success") { - const { accessToken, refreshToken: newRefreshToken } = res.data.data; + const { accessToken: newAccessToken, refreshToken: newRefreshToken } = + res.data.data; - localStorage.setItem("accessToken", accessToken); + localStorage.setItem("accessToken", newAccessToken); localStorage.setItem("refreshToken", newRefreshToken); - originalRequest.headers.Authorization = `Bearer ${accessToken}`; - processQueue(null, accessToken); - + originalRequest.headers.Authorization = `Bearer ${newAccessToken}`; + processQueue(null, newAccessToken); + return instance(originalRequest); } else { - const failError = new Error("토큰 리프레시가 실패했어요"); - processQueue(failError, null); - return Promise.reject(failError); + throw new Error("Token refresh failed"); } } catch (refreshError) { processQueue(refreshError, null); - + const status = refreshError.response?.status; if (status === 401 || status === 403) { - console.warn("세션이 만료되었습니다. 다시 로그인해주세요."); + console.warn("세션이 만료되어 로그아웃됩니다."); localStorage.clear(); window.location.href = "/login"; } - return Promise.reject(refreshError); } finally { isRefreshing = false; } } + return Promise.reject(error); - } + }, ); -export default instance; \ No newline at end of file +export default instance; From 18c68cf941d681e5c4dbe51b1f39212f834680b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B8=EC=95=84?= Date: Sat, 11 Apr 2026 22:03:02 +0900 Subject: [PATCH 34/84] =?UTF-8?q?fix:=20=EA=B2=BD=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 62ec751..93f953f 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -28,7 +28,7 @@ function App() { checkTokenExpiry(); const interval = setInterval(checkTokenExpiry, 60000); return () => clearInterval(interval); - }, []); + }, [navigate]); return ( @@ -36,12 +36,12 @@ function App() { } /> } /> } /> - } /> + } /> } /> } /> } /> } /> - } /> + } /> ); } From 2fba8f72c1c446cb36b5208f7d35800f8a8d84b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B8=EC=95=84?= Date: Sat, 11 Apr 2026 22:23:16 +0900 Subject: [PATCH 35/84] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EB=B9=97=20=EC=BD=94=EB=93=9C=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/instance.js | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/apis/instance.js b/src/apis/instance.js index 8d54cba..f30b9e1 100644 --- a/src/apis/instance.js +++ b/src/apis/instance.js @@ -54,19 +54,35 @@ instance.interceptors.response.use( try { const refreshToken = localStorage.getItem("refreshToken"); const accessToken = localStorage.getItem("accessToken"); + if (!refreshToken) { + throw Object.assign(new Error("Missing refresh token"), { + response: { status: 401 }, + }); + } + + const refreshHeaders = accessToken + ? { Authorization: `Bearer ${accessToken}` } + : {}; const res = await axios.post( `${import.meta.env.VITE_BASE_URL}/api/v1/auth/reissue`, { refreshToken }, - { headers: { Authorization: `Bearer ${accessToken}` } }, + { headers: refreshHeaders }, ); if (res.data.status === "success") { + const tokenPayload = res.data.data ?? res.data; const { accessToken: newAccessToken, refreshToken: newRefreshToken } = - res.data.data; + tokenPayload ?? {}; + if (!newAccessToken) { + throw new Error("Token refresh response missing access token"); + } localStorage.setItem("accessToken", newAccessToken); - localStorage.setItem("refreshToken", newRefreshToken); + if (newRefreshToken) { + localStorage.setItem("refreshToken", newRefreshToken); + } + localStorage.setItem("tokenExpiry", Date.now() + 60 * 60 * 1000); originalRequest.headers.Authorization = `Bearer ${newAccessToken}`; processQueue(null, newAccessToken); From a8f2e0f778aa426cdd6c6ac76a8d2555166ab936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B8=EC=95=84?= Date: Sat, 11 Apr 2026 22:29:09 +0900 Subject: [PATCH 36/84] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EB=B9=97=20=EC=BD=94=EB=93=9C=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/instance.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/apis/instance.js b/src/apis/instance.js index f30b9e1..b3c7442 100644 --- a/src/apis/instance.js +++ b/src/apis/instance.js @@ -67,7 +67,10 @@ instance.interceptors.response.use( const res = await axios.post( `${import.meta.env.VITE_BASE_URL}/api/v1/auth/reissue`, { refreshToken }, - { headers: refreshHeaders }, + { + headers: refreshHeaders, + timeout: instance.defaults.timeout, + }, ); if (res.data.status === "success") { From 441d534c0b61677525e7361fbb4114f3a4298b2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B8=EC=95=84?= Date: Sat, 11 Apr 2026 22:33:24 +0900 Subject: [PATCH 37/84] =?UTF-8?q?feat:=20emotion=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/Angry.svg | 9 +++++++++ src/assets/Special.svg | 9 +++++++++ 2 files changed, 18 insertions(+) create mode 100644 src/assets/Angry.svg create mode 100644 src/assets/Special.svg diff --git a/src/assets/Angry.svg b/src/assets/Angry.svg new file mode 100644 index 0000000..fe21c99 --- /dev/null +++ b/src/assets/Angry.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/Special.svg b/src/assets/Special.svg new file mode 100644 index 0000000..5dbcb4e --- /dev/null +++ b/src/assets/Special.svg @@ -0,0 +1,9 @@ + + + + + + + + + From 4d2c9ee87b24bc0871046630f8f758526603c560 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B8=EC=95=84?= Date: Sat, 11 Apr 2026 22:35:49 +0900 Subject: [PATCH 38/84] =?UTF-8?q?feat:=20=EC=A0=84=EC=B2=B4=EC=A0=81?= =?UTF-8?q?=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Home.jsx | 306 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 254 insertions(+), 52 deletions(-) diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index 334092c..fec6910 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -1,13 +1,116 @@ import styled from "@emotion/styled"; +import React, { useState, useEffect } from "react"; import Calendar from "react-calendar"; import "react-calendar/dist/Calendar.css"; import Header from "../components/Header"; import ArrowRight from "../assets/ArrowRight.svg"; -import NoDiary from "../assets/NoImg.svg"; + +import Sadness from "../assets/Sadness.svg"; +import Special from "../assets/Special.svg"; +import Happy from "../assets/Happy.svg"; +import Star from "../assets/Star.svg"; +import DefaultIcon from "../assets/NoImg.svg"; import { useNavigate } from "react-router-dom"; +import { getDiariesList, getDiaryRecommendation } from "../apis/diaries"; + +const emotionMap = { + HAPPY: { text: "행복한", color: "#5dc19b", icon: Happy }, + SAD: { text: "슬픈", color: "#89D9FF", icon: Sadness }, + ANGRY: { text: "화나는", color: "#FEA2A9", icon: Special }, + ANXIETY: { text: "불안한", color: "#CBA3FF", icon: Special }, + NEUTRAL: { text: "평범한", color: "#FCD671", icon: Star }, +}; + const Home = () => { const navigate = useNavigate(); + + const [diaries, setDiaries] = useState([]); + const [totalDays, setTotalDays] = useState(0); + const [recentPhotos, setRecentPhotos] = useState([]); + const [recommendation, setRecommendation] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + const diaryRes = await getDiariesList(); + if (diaryRes && diaryRes.data) { + const diaryData = diaryRes.data; + setDiaries(diaryData); + setTotalDays(diaryData.length); + + const photos = diaryData + .filter((d) => d.thumbnailUrl && d.thumbnailUrl !== "string") + .map((d) => d.thumbnailUrl); + setRecentPhotos(photos); + } + + const recRes = await getDiaryRecommendation(); + if (recRes && recRes.data) { + setRecommendation(recRes.data); + } + } catch (error) { + console.error("데이터 로딩 실패:", error); + } + }; + + fetchData(); + }, []); + + const formatDate = (date) => { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; + }; + + const tileContent = ({ date, view }) => { + if (view === "month") { + const dateString = formatDate(date); + const hasDiary = diaries.find((diary) => diary.date === dateString); + + if (hasDiary) { + return ( + + + + ); + } + } + return null; + }; + + const getRecommendationDisplay = () => { + if (!recommendation) { + return { + dateText: "", + emotionText: "", + emotionColor: emotionMap.color, + icon: emotionMap.icon, + }; + } + + const dateParts = recommendation.targetDate.split("-"); + const formattedDate = `${dateParts[0]}년 ${parseInt(dateParts[1], 10)}월 ${parseInt(dateParts[2], 10)}일`; + + const emotionInfo = emotionMap[recommendation.emotion] || emotionMap; + + return { + dateText: `${formattedDate},`, + emotionText: emotionInfo.text, + emotionColor: emotionInfo.color, + icon: emotionInfo.icon, + }; + }; + + const recDisplay = getRecommendationDisplay(); + + const handleRecommendationClick = () => { + if (recommendation && recommendation.diaryId) { + navigate(`/diary/${recommendation.diaryId}`); + } + }; + return (
@@ -17,12 +120,25 @@ const Home = () => { locale="ko-KR" calendarType="gregory" formatDay={(locale, date) => date.getDate()} - /* formatShortWeekday 수정: API 의도에 맞게 '일요일' -> '일'로 축약 */ formatShortWeekday={(locale, date) => ["일", "월", "화", "수", "목", "금", "토"][date.getDay()] } + tileContent={tileContent} + onClickDay={(value) => { + const dateString = formatDate(value); + const selectedDiary = diaries.find( + (diary) => diary.createdAt === dateString, + ); + + if (selectedDiary) { + navigate(`/diary/${selectedDiary.id}`); + } else { + alert("해당 날짜에 작성된 일기가 없습니다."); + } + }} /> + @@ -34,14 +150,14 @@ const Home = () => { - 총 0일 + 총 {totalDays}일
    화남 평범 - 행복 + 행복 슬픔 불안
@@ -56,26 +172,64 @@ const Home = () => { - 사진이 없어요... - 일기에 추억을 남기러 가볼까요? + {recentPhotos.length > 0 ? ( + + {recentPhotos.slice(0, 3).map((url, index) => ( + + + + ))} + + ) : ( + + 사진이 없어요... + 일기에 추억을 남기러 가볼까요? + + )} - - - - - hear 에 오신 것을 환영해요! - - 아래 버튼을 눌러 일기를 작성해볼까요? - - - - - - - 일기 없음 아이콘 - - + + {recommendation && recommendation.diaryId ? ( + + + + + + {recDisplay.dateText} +
+ 나에게는 무슨{" "} + + {recDisplay.emotionText} + {" "} + 일이 있었을까요? +
+
+ +
+
+ + 일기 추천 감정 아이콘 + +
+ ) : ( + + + + + + Hear에 오신 것을 환영해요! +
+ 아래 버튼을 눌러 일기를 작성해볼까요? +
+
+ +
+
+ + 일기 추천 감정 아이콘 + +
+ )}
navigate("/ai/chats")}> @@ -116,16 +270,14 @@ const DateTitleBox = styled.div` flex-direction: row; align-items: center; gap: 12px; - hr { flex: 1; border: none; border-top: 1px solid #ccc; margin: 0; } - span { - font-size: 26px; // 32px -> 26px 크기 축소 + font-size: 26px; font-weight: 600; color: #575141; } @@ -145,13 +297,12 @@ const StyledCalendar = styled(Calendar)` border: none !important; width: 100% !important; font-family: "Pretendard", sans-serif; - .react-calendar__navigation { justify-content: center; margin-bottom: 15px; button { background: none; - font-size: 20px; // 24px -> 20px + font-size: 20px; font-weight: 700; color: #575141; &:disabled { @@ -167,7 +318,6 @@ const StyledCalendar = styled(Calendar)` display: none; } } - .react-calendar__month-view__weekdays { text-align: center; font-weight: 600; @@ -187,7 +337,6 @@ const StyledCalendar = styled(Calendar)` color: #4d8aff; } } - .react-calendar__tile { height: 76px; display: flex; @@ -199,14 +348,12 @@ const StyledCalendar = styled(Calendar)` font-weight: 500; color: #444; background: none !important; - &:nth-of-type(7n + 1) { color: #ff6b6b; } &:nth-of-type(7n) { color: #4d8aff; } - &:enabled:hover, &:enabled:focus, &.react-calendar__tile--active { @@ -214,7 +361,6 @@ const StyledCalendar = styled(Calendar)` border-radius: 12px; } } - .react-calendar__tile--now { background: none !important; abbr { @@ -228,10 +374,6 @@ const StyledCalendar = styled(Calendar)` color: white; } } - - .react-calendar__month-view__days__day--neighboringMonth { - color: #d4d4d0 !important; - } `; const StatSection = styled.div` @@ -286,7 +428,6 @@ const Angry = styled.li` color: #fea2a9; } `; - const Normal = styled.li` display: flex; align-items: center; @@ -296,8 +437,7 @@ const Normal = styled.li` color: #fcd671; } `; - -const Happy = styled.li` +const Happyspan = styled.li` display: flex; align-items: center; gap: 4px; @@ -306,7 +446,6 @@ const Happy = styled.li` color: #5dc19b; } `; - const Sad = styled.li` display: flex; align-items: center; @@ -316,7 +455,6 @@ const Sad = styled.li` color: #89d9ff; } `; - const Anxiety = styled.li` display: flex; align-items: center; @@ -330,11 +468,11 @@ const Anxiety = styled.li` const ImgContainer = styled.div` display: flex; flex-direction: column; - height: 230px; // 250px -> 210px 축소 + height: 210px; background-color: #f3f3f3; border-radius: 12px; - padding: 14px; - gap: 10px; + padding: 16px; + gap: 12px; `; const TextContainer = styled.div` @@ -351,23 +489,55 @@ const ImgBox = styled.div` width: 100%; background-color: white; flex: 1; - border-radius: 8px; + border-radius: 10px; display: flex; - flex-direction: column; justify-content: center; align-items: center; + overflow: hidden; +`; + +const RecentPhotosList = styled.div` + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: 15px; + width: 100%; + height: 100%; + padding: 10px; +`; + +const RecentImageWrapper = styled.div` + width: 120px; + height: 120px; + border-radius: 12px; + overflow: hidden; + background-color: #f9f9f9; + border: 1px solid #eee; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); +`; + +const RecentImage = styled.img` + width: 100%; + height: 100%; + object-fit: cover; +`; + +const NoImgContent = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; `; const NoImg = styled.span` color: #828282; font-size: 13px; `; - const StartText = styled.span` font-size: 17px; font-weight: 500; `; - const ImgTotal = styled.a` color: #828282; font-size: 12px; @@ -380,6 +550,13 @@ const NoDiaryContainer = styled.div` align-items: center; width: 100%; gap: 15px; + margin-top: 10px; + cursor: pointer; + transition: transform 0.2s ease; + + &:hover { + transform: translateY(-2px); + } `; const BubbleContainerWrapper = styled.div` @@ -387,31 +564,43 @@ const BubbleContainerWrapper = styled.div` flex: 1; align-items: center; `; - const BubbleContainer = styled.div` position: relative; display: flex; flex-direction: column; - align-items: center; + align-items: flex-start; /* 텍스트 정렬 */ width: 100%; background: white; border: 1.5px solid #d4d4d0; border-radius: 18px; padding: 15px 20px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + transition: box-shadow 0.2s ease; `; const BubbleContent = styled.div` display: flex; flex-direction: column; - align-items: center; + align-self: center; gap: 4px; `; const MessageText = styled.span` - font-size: 12px; + font-size: 13px; + text-align: center; color: #6b6560; + line-height: 1.4; word-break: keep-all; + + span { + font-weight: bold; + display: inline-block; + } +`; + +const EmotionText = styled.span` + color: ${(props) => props.color || "inherit"}; + font-weight: bold; `; const BubbleTail = styled.div` @@ -470,4 +659,17 @@ const AiChatButton = styled.button` } `; +const DiaryMark = styled.div` + display: flex; + justify-content: center; + align-items: center; + margin-top: 4px; +`; +const Dot = styled.div` + width: 8px; + height: 8px; + background-color: ${(props) => props.color || "#fcd671"}; + border-radius: 50%; +`; + export default Home; From 5854ac062653707b7b4d098679bc82fdf3bfe44c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B8=EC=95=84?= Date: Sat, 11 Apr 2026 22:35:49 +0900 Subject: [PATCH 39/84] =?UTF-8?q?feat:=20=EC=A0=84=EC=B2=B4=EC=A0=81?= =?UTF-8?q?=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Home.jsx | 306 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 254 insertions(+), 52 deletions(-) diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index 334092c..fec6910 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -1,13 +1,116 @@ import styled from "@emotion/styled"; +import React, { useState, useEffect } from "react"; import Calendar from "react-calendar"; import "react-calendar/dist/Calendar.css"; import Header from "../components/Header"; import ArrowRight from "../assets/ArrowRight.svg"; -import NoDiary from "../assets/NoImg.svg"; + +import Sadness from "../assets/Sadness.svg"; +import Special from "../assets/Special.svg"; +import Happy from "../assets/Happy.svg"; +import Star from "../assets/Star.svg"; +import DefaultIcon from "../assets/NoImg.svg"; import { useNavigate } from "react-router-dom"; +import { getDiariesList, getDiaryRecommendation } from "../apis/diaries"; + +const emotionMap = { + HAPPY: { text: "행복한", color: "#5dc19b", icon: Happy }, + SAD: { text: "슬픈", color: "#89D9FF", icon: Sadness }, + ANGRY: { text: "화나는", color: "#FEA2A9", icon: Special }, + ANXIETY: { text: "불안한", color: "#CBA3FF", icon: Special }, + NEUTRAL: { text: "평범한", color: "#FCD671", icon: Star }, +}; + const Home = () => { const navigate = useNavigate(); + + const [diaries, setDiaries] = useState([]); + const [totalDays, setTotalDays] = useState(0); + const [recentPhotos, setRecentPhotos] = useState([]); + const [recommendation, setRecommendation] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + const diaryRes = await getDiariesList(); + if (diaryRes && diaryRes.data) { + const diaryData = diaryRes.data; + setDiaries(diaryData); + setTotalDays(diaryData.length); + + const photos = diaryData + .filter((d) => d.thumbnailUrl && d.thumbnailUrl !== "string") + .map((d) => d.thumbnailUrl); + setRecentPhotos(photos); + } + + const recRes = await getDiaryRecommendation(); + if (recRes && recRes.data) { + setRecommendation(recRes.data); + } + } catch (error) { + console.error("데이터 로딩 실패:", error); + } + }; + + fetchData(); + }, []); + + const formatDate = (date) => { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; + }; + + const tileContent = ({ date, view }) => { + if (view === "month") { + const dateString = formatDate(date); + const hasDiary = diaries.find((diary) => diary.date === dateString); + + if (hasDiary) { + return ( + + + + ); + } + } + return null; + }; + + const getRecommendationDisplay = () => { + if (!recommendation) { + return { + dateText: "", + emotionText: "", + emotionColor: emotionMap.color, + icon: emotionMap.icon, + }; + } + + const dateParts = recommendation.targetDate.split("-"); + const formattedDate = `${dateParts[0]}년 ${parseInt(dateParts[1], 10)}월 ${parseInt(dateParts[2], 10)}일`; + + const emotionInfo = emotionMap[recommendation.emotion] || emotionMap; + + return { + dateText: `${formattedDate},`, + emotionText: emotionInfo.text, + emotionColor: emotionInfo.color, + icon: emotionInfo.icon, + }; + }; + + const recDisplay = getRecommendationDisplay(); + + const handleRecommendationClick = () => { + if (recommendation && recommendation.diaryId) { + navigate(`/diary/${recommendation.diaryId}`); + } + }; + return (
@@ -17,12 +120,25 @@ const Home = () => { locale="ko-KR" calendarType="gregory" formatDay={(locale, date) => date.getDate()} - /* formatShortWeekday 수정: API 의도에 맞게 '일요일' -> '일'로 축약 */ formatShortWeekday={(locale, date) => ["일", "월", "화", "수", "목", "금", "토"][date.getDay()] } + tileContent={tileContent} + onClickDay={(value) => { + const dateString = formatDate(value); + const selectedDiary = diaries.find( + (diary) => diary.createdAt === dateString, + ); + + if (selectedDiary) { + navigate(`/diary/${selectedDiary.id}`); + } else { + alert("해당 날짜에 작성된 일기가 없습니다."); + } + }} /> + @@ -34,14 +150,14 @@ const Home = () => { - 총 0일 + 총 {totalDays}일
    화남 평범 - 행복 + 행복 슬픔 불안
@@ -56,26 +172,64 @@ const Home = () => { - 사진이 없어요... - 일기에 추억을 남기러 가볼까요? + {recentPhotos.length > 0 ? ( + + {recentPhotos.slice(0, 3).map((url, index) => ( + + + + ))} + + ) : ( + + 사진이 없어요... + 일기에 추억을 남기러 가볼까요? + + )} - - - - - hear 에 오신 것을 환영해요! - - 아래 버튼을 눌러 일기를 작성해볼까요? - - - - - - - 일기 없음 아이콘 - - + + {recommendation && recommendation.diaryId ? ( + + + + + + {recDisplay.dateText} +
+ 나에게는 무슨{" "} + + {recDisplay.emotionText} + {" "} + 일이 있었을까요? +
+
+ +
+
+ + 일기 추천 감정 아이콘 + +
+ ) : ( + + + + + + Hear에 오신 것을 환영해요! +
+ 아래 버튼을 눌러 일기를 작성해볼까요? +
+
+ +
+
+ + 일기 추천 감정 아이콘 + +
+ )}
navigate("/ai/chats")}> @@ -116,16 +270,14 @@ const DateTitleBox = styled.div` flex-direction: row; align-items: center; gap: 12px; - hr { flex: 1; border: none; border-top: 1px solid #ccc; margin: 0; } - span { - font-size: 26px; // 32px -> 26px 크기 축소 + font-size: 26px; font-weight: 600; color: #575141; } @@ -145,13 +297,12 @@ const StyledCalendar = styled(Calendar)` border: none !important; width: 100% !important; font-family: "Pretendard", sans-serif; - .react-calendar__navigation { justify-content: center; margin-bottom: 15px; button { background: none; - font-size: 20px; // 24px -> 20px + font-size: 20px; font-weight: 700; color: #575141; &:disabled { @@ -167,7 +318,6 @@ const StyledCalendar = styled(Calendar)` display: none; } } - .react-calendar__month-view__weekdays { text-align: center; font-weight: 600; @@ -187,7 +337,6 @@ const StyledCalendar = styled(Calendar)` color: #4d8aff; } } - .react-calendar__tile { height: 76px; display: flex; @@ -199,14 +348,12 @@ const StyledCalendar = styled(Calendar)` font-weight: 500; color: #444; background: none !important; - &:nth-of-type(7n + 1) { color: #ff6b6b; } &:nth-of-type(7n) { color: #4d8aff; } - &:enabled:hover, &:enabled:focus, &.react-calendar__tile--active { @@ -214,7 +361,6 @@ const StyledCalendar = styled(Calendar)` border-radius: 12px; } } - .react-calendar__tile--now { background: none !important; abbr { @@ -228,10 +374,6 @@ const StyledCalendar = styled(Calendar)` color: white; } } - - .react-calendar__month-view__days__day--neighboringMonth { - color: #d4d4d0 !important; - } `; const StatSection = styled.div` @@ -286,7 +428,6 @@ const Angry = styled.li` color: #fea2a9; } `; - const Normal = styled.li` display: flex; align-items: center; @@ -296,8 +437,7 @@ const Normal = styled.li` color: #fcd671; } `; - -const Happy = styled.li` +const Happyspan = styled.li` display: flex; align-items: center; gap: 4px; @@ -306,7 +446,6 @@ const Happy = styled.li` color: #5dc19b; } `; - const Sad = styled.li` display: flex; align-items: center; @@ -316,7 +455,6 @@ const Sad = styled.li` color: #89d9ff; } `; - const Anxiety = styled.li` display: flex; align-items: center; @@ -330,11 +468,11 @@ const Anxiety = styled.li` const ImgContainer = styled.div` display: flex; flex-direction: column; - height: 230px; // 250px -> 210px 축소 + height: 210px; background-color: #f3f3f3; border-radius: 12px; - padding: 14px; - gap: 10px; + padding: 16px; + gap: 12px; `; const TextContainer = styled.div` @@ -351,23 +489,55 @@ const ImgBox = styled.div` width: 100%; background-color: white; flex: 1; - border-radius: 8px; + border-radius: 10px; display: flex; - flex-direction: column; justify-content: center; align-items: center; + overflow: hidden; +`; + +const RecentPhotosList = styled.div` + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: 15px; + width: 100%; + height: 100%; + padding: 10px; +`; + +const RecentImageWrapper = styled.div` + width: 120px; + height: 120px; + border-radius: 12px; + overflow: hidden; + background-color: #f9f9f9; + border: 1px solid #eee; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); +`; + +const RecentImage = styled.img` + width: 100%; + height: 100%; + object-fit: cover; +`; + +const NoImgContent = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; `; const NoImg = styled.span` color: #828282; font-size: 13px; `; - const StartText = styled.span` font-size: 17px; font-weight: 500; `; - const ImgTotal = styled.a` color: #828282; font-size: 12px; @@ -380,6 +550,13 @@ const NoDiaryContainer = styled.div` align-items: center; width: 100%; gap: 15px; + margin-top: 10px; + cursor: pointer; + transition: transform 0.2s ease; + + &:hover { + transform: translateY(-2px); + } `; const BubbleContainerWrapper = styled.div` @@ -387,31 +564,43 @@ const BubbleContainerWrapper = styled.div` flex: 1; align-items: center; `; - const BubbleContainer = styled.div` position: relative; display: flex; flex-direction: column; - align-items: center; + align-items: flex-start; /* 텍스트 정렬 */ width: 100%; background: white; border: 1.5px solid #d4d4d0; border-radius: 18px; padding: 15px 20px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + transition: box-shadow 0.2s ease; `; const BubbleContent = styled.div` display: flex; flex-direction: column; - align-items: center; + align-self: center; gap: 4px; `; const MessageText = styled.span` - font-size: 12px; + font-size: 13px; + text-align: center; color: #6b6560; + line-height: 1.4; word-break: keep-all; + + span { + font-weight: bold; + display: inline-block; + } +`; + +const EmotionText = styled.span` + color: ${(props) => props.color || "inherit"}; + font-weight: bold; `; const BubbleTail = styled.div` @@ -470,4 +659,17 @@ const AiChatButton = styled.button` } `; +const DiaryMark = styled.div` + display: flex; + justify-content: center; + align-items: center; + margin-top: 4px; +`; +const Dot = styled.div` + width: 8px; + height: 8px; + background-color: ${(props) => props.color || "#fcd671"}; + border-radius: 50%; +`; + export default Home; From 30f8bf3631db2d0db7de343f1a147e14e7dffc31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B8=EC=95=84?= Date: Sat, 11 Apr 2026 22:55:06 +0900 Subject: [PATCH 40/84] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EB=B9=97=20=EC=BD=94=EB=93=9C=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Home.jsx | 57 +++++++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index fec6910..289a1d3 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -34,21 +34,20 @@ const Home = () => { const fetchData = async () => { try { const diaryRes = await getDiariesList(); - if (diaryRes && diaryRes.data) { - const diaryData = diaryRes.data; - setDiaries(diaryData); - setTotalDays(diaryData.length); + const diaryData = Array.isArray(diaryRes) + ? diaryRes + : (diaryRes?.data ?? []); + setDiaries(diaryData); + setTotalDays(diaryData.length); - const photos = diaryData - .filter((d) => d.thumbnailUrl && d.thumbnailUrl !== "string") - .map((d) => d.thumbnailUrl); - setRecentPhotos(photos); - } + const photos = diaryData + .filter((d) => d.thumbnailUrl && d.thumbnailUrl !== "string") + .map((d) => d.thumbnailUrl); + setRecentPhotos(photos); const recRes = await getDiaryRecommendation(); - if (recRes && recRes.data) { - setRecommendation(recRes.data); - } + const recData = recRes?.data ?? recRes; + if (recData) setRecommendation(recData); } catch (error) { console.error("데이터 로딩 실패:", error); } @@ -81,19 +80,28 @@ const Home = () => { }; const getRecommendationDisplay = () => { + const defaultEmotion = emotionMap.NEUTRAL; if (!recommendation) { return { dateText: "", emotionText: "", - emotionColor: emotionMap.color, - icon: emotionMap.icon, + emotionColor: defaultEmotion.color, + icon: defaultEmotion.icon, }; } + if (!recommendation.targetDate) { + return { + dateText: "", + emotionText: "", + emotionColor: defaultEmotion.color, + icon: defaultEmotion.icon, + }; + } const dateParts = recommendation.targetDate.split("-"); const formattedDate = `${dateParts[0]}년 ${parseInt(dateParts[1], 10)}월 ${parseInt(dateParts[2], 10)}일`; - const emotionInfo = emotionMap[recommendation.emotion] || emotionMap; + const emotionInfo = emotionMap[recommendation.emotion] ?? defaultEmotion; return { dateText: `${formattedDate},`, @@ -126,9 +134,10 @@ const Home = () => { tileContent={tileContent} onClickDay={(value) => { const dateString = formatDate(value); - const selectedDiary = diaries.find( - (diary) => diary.createdAt === dateString, - ); + const selectedDiary = diaries.find((diary) => { + const diaryDate = diary.date ?? diary.createdAt?.slice(0, 10); + return diaryDate === dateString; + }); if (selectedDiary) { navigate(`/diary/${selectedDiary.id}`); @@ -190,7 +199,7 @@ const Home = () => { {recommendation && recommendation.diaryId ? ( - + @@ -212,7 +221,7 @@ const Home = () => { ) : ( - + @@ -551,11 +560,13 @@ const NoDiaryContainer = styled.div` width: 100%; gap: 15px; margin-top: 10px; - cursor: pointer; - transition: transform 0.2s ease; + cursor: ${({ $clickable }) => ($clickable ? "pointer" : "default")}; + transition: ${({ $clickable }) => + $clickable ? "transform 0.2s ease" : "none"}; &:hover { - transform: translateY(-2px); + transform: ${({ $clickable }) => + $clickable ? "translateY(-2px)" : "none"}; } `; From 477af6b493a90a42b0208c9182f5f999112b57c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B8=EC=95=84?= Date: Sun, 12 Apr 2026 09:27:33 +0900 Subject: [PATCH 41/84] =?UTF-8?q?feat:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=ED=81=B4=EB=A6=AD=20=EC=8B=9C=20=ED=95=B4=EB=8B=B9=20=EC=9D=BC?= =?UTF-8?q?=EA=B8=B0=EB=A1=9C=20=EC=9D=B4=EB=8F=99=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Home.jsx | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index 289a1d3..b3d8e4c 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -30,6 +30,10 @@ const Home = () => { const [recentPhotos, setRecentPhotos] = useState([]); const [recommendation, setRecommendation] = useState(null); + const handlePhotoClick = (diaryId) => { + navigate(`/diary/${diaryId}`); + }; + useEffect(() => { const fetchData = async () => { try { @@ -42,7 +46,10 @@ const Home = () => { const photos = diaryData .filter((d) => d.thumbnailUrl && d.thumbnailUrl !== "string") - .map((d) => d.thumbnailUrl); + .map((d) => ({ + url: d.thumbnailUrl, + diaryId: d.id, + })); setRecentPhotos(photos); const recRes = await getDiaryRecommendation(); @@ -183,9 +190,15 @@ const Home = () => { {recentPhotos.length > 0 ? ( - {recentPhotos.slice(0, 3).map((url, index) => ( - - + {recentPhotos.slice(0, 3).map((photo, index) => ( + handlePhotoClick(photo.diaryId)} + > + ))} @@ -524,6 +537,15 @@ const RecentImageWrapper = styled.div` background-color: #f9f9f9; border: 1px solid #eee; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); + cursor: pointer; + transition: + transform 0.2s ease, + box-shadow 0.2s ease; + + &:hover { + transform: scale(1.05); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + } `; const RecentImage = styled.img` From 47dedfe0e7d4fe8776de3efa735804d77a46871d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B8=EC=95=84?= Date: Sun, 12 Apr 2026 09:30:31 +0900 Subject: [PATCH 42/84] =?UTF-8?q?fix:=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/diaries.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apis/diaries.js b/src/apis/diaries.js index 2e327f8..497f0ed 100644 --- a/src/apis/diaries.js +++ b/src/apis/diaries.js @@ -4,7 +4,7 @@ import instance from "./instance"; export const getDiaries = async (id) => { try { const res = await instance.get(`/api/v1/diaries/${id}`); - return res.data; + return res.data.data; } catch (error) { console.error("일기 조회 실패:", error); throw error; From 993adbf4a16c0ce31d2d6f67a9721caf74623888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B8=EC=95=84?= Date: Sun, 12 Apr 2026 09:27:33 +0900 Subject: [PATCH 43/84] =?UTF-8?q?feat:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=ED=81=B4=EB=A6=AD=20=EC=8B=9C=20=ED=95=B4=EB=8B=B9=20=EC=9D=BC?= =?UTF-8?q?=EA=B8=B0=EB=A1=9C=20=EC=9D=B4=EB=8F=99=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Home.jsx | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index 289a1d3..b3d8e4c 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -30,6 +30,10 @@ const Home = () => { const [recentPhotos, setRecentPhotos] = useState([]); const [recommendation, setRecommendation] = useState(null); + const handlePhotoClick = (diaryId) => { + navigate(`/diary/${diaryId}`); + }; + useEffect(() => { const fetchData = async () => { try { @@ -42,7 +46,10 @@ const Home = () => { const photos = diaryData .filter((d) => d.thumbnailUrl && d.thumbnailUrl !== "string") - .map((d) => d.thumbnailUrl); + .map((d) => ({ + url: d.thumbnailUrl, + diaryId: d.id, + })); setRecentPhotos(photos); const recRes = await getDiaryRecommendation(); @@ -183,9 +190,15 @@ const Home = () => { {recentPhotos.length > 0 ? ( - {recentPhotos.slice(0, 3).map((url, index) => ( - - + {recentPhotos.slice(0, 3).map((photo, index) => ( + handlePhotoClick(photo.diaryId)} + > + ))} @@ -524,6 +537,15 @@ const RecentImageWrapper = styled.div` background-color: #f9f9f9; border: 1px solid #eee; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); + cursor: pointer; + transition: + transform 0.2s ease, + box-shadow 0.2s ease; + + &:hover { + transform: scale(1.05); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + } `; const RecentImage = styled.img` From ca36d373b62936b75b0f28dffa5dd70f7e40aaca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B8=EC=95=84?= Date: Sun, 12 Apr 2026 09:38:20 +0900 Subject: [PATCH 44/84] =?UTF-8?q?feat:=20=EC=9D=BC=EA=B8=B0=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/EditDiary.jsx | 160 ++++++++++++++++++++++++++-------------- 1 file changed, 106 insertions(+), 54 deletions(-) diff --git a/src/pages/EditDiary.jsx b/src/pages/EditDiary.jsx index fdb7ad6..0f8d1a0 100644 --- a/src/pages/EditDiary.jsx +++ b/src/pages/EditDiary.jsx @@ -1,5 +1,6 @@ import styled from "@emotion/styled"; -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; +import { useNavigate, useParams } from "react-router-dom"; import Header from "../components/Header"; import Happy from "../assets/Happy.svg"; import ArrowRight from "../assets/ArrowRight.svg"; @@ -8,13 +9,44 @@ import NoAngry from "../assets/NoAngry.svg"; import NoStar from "../assets/NoStar.svg"; import NoSad from "../assets/NoSad.svg"; import NoAnxiety from "../assets/NoAnxiety.svg"; -import { useNavigate } from "react-router-dom"; + +import { getDiaries, updateDiaries } from "../apis/diaries"; const EditDiary = () => { const navigate = useNavigate(); - // 슬라이드 이미지 배열 (필요시 추가) - const images = [Test, Test, Test]; // 같은 이미지 3개 (실제로는 다른 이미지 사용) + const { id } = useParams(); + + const [content, setContent] = useState(""); + const [date, setDate] = useState(""); + const [hashTags, setHashTags] = useState([]); + const [images, setImages] = useState([Test]); const [currentIndex, setCurrentIndex] = useState(0); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const fetchDiaryData = async () => { + if (!id) return; + try { + setIsLoading(true); + const data = await getDiaries(id); + + setDate(data.createdAt || ""); + setContent(data.content || ""); + setHashTags(data.tags || []); + + if (data.images && data.images.length > 0) { + setImages(data.images); + } + } catch (error) { + console.error("데이터를 불러오는데 실패했습니다.", error); + alert("일기 데이터를 가져올 수 없습니다."); + } finally { + setIsLoading(false); + } + }; + + fetchDiaryData(); + }, [id]); const goToPrevious = () => { setCurrentIndex((prevIndex) => @@ -27,6 +59,22 @@ const EditDiary = () => { prevIndex === images.length - 1 ? 0 : prevIndex + 1, ); }; + + const handleUpdate = async () => { + try { + await updateDiaries(id, { content }); + alert("일기가 성공적으로 수정되었습니다."); + navigate(`/diary/${id}`); + } catch (error) { + alert("일기 수정에 실패했습니다.", error); + } + }; + + if (isLoading) + return ( + 로딩 중... + ); + return (
@@ -41,49 +89,53 @@ const EditDiary = () => { - navigate("/diary")}> + navigate(`/diary/${id}`)}> 취소 - navigate("/diary")}> - 완료 - + 완료 -

2026년 7월 20일

-

- 오늘은 정말 행복한 하루였어요! 아침에 일어나서 햇살이 너무 - 좋아서 기분이 좋았어요. 친구들과 함께 공원에서 피크닉을 했는데, - 맛있는 음식과 좋은 대화로 즐거운 시간을 보냈어요. 저녁에는 - 가족과 함께 맛있는 저녁을 먹으면서 웃음이 끊이지 않았어요. 오늘 - 하루가 너무 소중하고 행복했어요! -

+

+ {date.includes("-") + ? `${date.split("-")[0]}년 ${parseInt(date.split("-")[1])}월 ${parseInt(date.split("-")[2])}일` + : date} +

+