diff --git a/src/components/Goalpage/GoormScreen.tsx b/src/components/Goalpage/GoormScreen.tsx index 1f4bf3f7..d9a7dd12 100644 --- a/src/components/Goalpage/GoormScreen.tsx +++ b/src/components/Goalpage/GoormScreen.tsx @@ -16,29 +16,16 @@ interface Cloud { export default function GoormScreen() { const [stage, setStage] = useState(0); - const [myId, setMyId] = useState(''); - const [clouds, setClouds] = useState([]); + const [clouds, setClouds] = useState([]); useEffect(() => { const fetchMyData = async () => { try { - const response = await API.User.getMyInfo(); + const response = await API.User.currentStep(); if (response.status === 'OK' && response.data) { - // 사용자 ID 설정 - setMyId(response.data.id); - - // 'data' 배열이 존재하고 배열인지 확인 후 길이 설정 - if ( - response.data.data && - Array.isArray(response.data.data) - ) { - const dataCount = response.data.data.length; - setStage(dataCount); - } else { - // 'data' 배열이 없거나 배열이 아닌 경우 0으로 설정 - setStage(0); - } + setStage(response.data.currentStep); + console.log('현재 스테이지:', response.data.currentStep); } else { console.error( '응답 상태가 OK가 아니거나 데이터가 없습니다.' @@ -89,17 +76,18 @@ export default function GoormScreen() { }, [stage]); // stage가 변경될 때마다 실행 // 임시로 집어넣은 구름 단계 증가 함수 - const increaseStage = () => { - setStage((prev) => (prev < 4 ? prev + 1 : prev)); + const increaseStage = (stage:number) => { + setStage((prev) => (prev < stage ? prev + 1 : prev)); }; // 구름 증가시키는데 천천히 증가하도록 설정 useEffect(() => { - if (stage < 4) { - // 최대 스테이지가 4라면 + if (stage === 0) { + return; + } else if (stage < 4) { // 최대 스테이지가 4라면 const timer = setTimeout(() => { - increaseStage(); - }, 1000); + increaseStage(stage); + }, 1500); return () => clearTimeout(timer); } diff --git a/src/components/Goalpage/MemberList.tsx b/src/components/Goalpage/MemberList.tsx index b9f3e894..965bc148 100644 --- a/src/components/Goalpage/MemberList.tsx +++ b/src/components/Goalpage/MemberList.tsx @@ -1,6 +1,9 @@ import MemberCard from './MemberList/MemberCard'; import { API } from '../../lib/api/index.ts'; import { useState, useEffect } from 'react'; +import cn from '../../lib/cn'; +import UploadModal from './MemberList/UploadModal.tsx'; +import ShowImageModal from './MemberList/ShowImageModal.tsx'; interface Member { id: string; @@ -9,6 +12,8 @@ interface Member { } export default function MemberList() { + const [isModalVisible, setIsModalVisible] = useState(false); + const [isSeeMyImageModalVisible, setIsSeeMyImageModalVisible] = useState(false); const [members, setMembers] = useState([ { id: '1', username: '김', profileUrl: '' }, { id: '2', username: '박', profileUrl: '' }, @@ -16,6 +21,22 @@ export default function MemberList() { ]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [myId, setMyId] = useState(''); + const [myName, setMyName] = useState(''); + + useEffect(() => { + const fetchMyData = async () => { + try { + const response = await API.User.getMyInfo(); + setMyId(response.username); + setMyName(response.nickname); + console.log(myId, myName); + } catch (error) { + console.error('내 정보를 불러오는 중 오류가 발생했습니다:', error); + } + }; + fetchMyData(); + }, []); useEffect(() => { const fetchMemberList = async () => { @@ -37,6 +58,22 @@ export default function MemberList() { fetchMemberList(); }, []); + const openModal = () => { + setIsModalVisible(true); + }; + + const closeModal = () => { + setIsModalVisible(false); + }; + + const openSeeMyImageModal = () => { + setIsSeeMyImageModalVisible(true); + } + + const closeSeeMyImageModal = () => { + setIsSeeMyImageModalVisible(false); + } + return ( <>
@@ -57,6 +94,32 @@ export default function MemberList() { /> ))}
- + + {isModalVisible && ( + + )} + + {isSeeMyImageModalVisible && myId && myName && ( + + )} + ); } diff --git a/src/components/Goalpage/MemberList/ShowImageModal.tsx b/src/components/Goalpage/MemberList/ShowImageModal.tsx index 394e0c6b..dad1c426 100644 --- a/src/components/Goalpage/MemberList/ShowImageModal.tsx +++ b/src/components/Goalpage/MemberList/ShowImageModal.tsx @@ -23,7 +23,7 @@ export default function ShowImageModal({ }: ShowImageModalProps) { // 백엔드에서 가져온 이미지들을 저장할 상태 const [images, setImages] = useState([]); - + // 백엔드에서 이미지 데이터를 가져오는 함수 useEffect(() => { const fetchMemberImages = async () => { @@ -82,15 +82,15 @@ export default function ShowImageModal({
{displayImages.map((image, index) => ( -
- {image.alt} + {image.alt}
))}
diff --git a/src/components/Goalpage/MemberList/UploadModal.tsx b/src/components/Goalpage/MemberList/UploadModal.tsx new file mode 100644 index 00000000..2d5c8865 --- /dev/null +++ b/src/components/Goalpage/MemberList/UploadModal.tsx @@ -0,0 +1,130 @@ +import React, { useState } from 'react'; +import ImageUpload from './UploadModal/ImageUpload.tsx'; +import SelectRoutineList from './UploadModal/SelectRoutineList.tsx'; +import cn from '../../../lib/cn.ts'; +import { API } from '../../../lib/api/index.ts'; +import { LiaCloneSolid } from 'react-icons/lia'; + +interface MultiStepModalProps { + isVisible: boolean; + onClose: () => void; +} + +export default function MultiStepModal({ isVisible, onClose }: MultiStepModalProps) { + const [step, setStep] = useState(1); // 1: 이미지 업로드, 2: 루틴 선택 + const [index, setIndex] = useState(0); // 루틴 인덱스 + const [image, setImage] = useState(null); // 이미지 상태 + const [selectedRoutine, setSelectedRoutine] = useState(''); // 선택된 루틴 + + const handleNextStep = () => { + setStep(step + 1); + }; + + const handlePreviousStep = () => { + setStep(step - 1); + }; + + const handleCancel = () => { + onClose(); // 모달 닫기 + }; + + const handleUpload = async () => { + if (!image || !selectedRoutine) return; + + try { + // FormData 객체 생성 + const formData = new FormData(); + formData.append('routineIndex', index.toString()); // 인덱스 값을 문자열로 변환하여 추가 + formData.append('routineName', selectedRoutine); // 루틴 이름 추가 + formData.append('file', image); // 이미지 파일 추가 + + // API 호출 + const response = await API.User.uploadRoutine( + (index + 1).toString(), // 루틴 인덱스 + selectedRoutine, // 선택한 루틴 + image // 전송할 이미지 파일 + ); + + if (response.status === 'OK') { + setStep(3); // 성공 시 3단계로 이동 + } else { + console.error('업로드 실패:', response); + setStep(4); // 실패 시 4단계로 이동 (재시도 안내) + } + + } catch (error) { + console.error('업로드 중 오류 발생:', error); + setStep(4); // 실패 시 4단계로 이동 (재시도 안내) + } + }; + + return isVisible ? ( +
+
+ {step === 1 && ( + <> +

이미지 업로드

+ + + )} + {step === 2 && ( + <> +

루틴 선택

+ + + )} + {step === 3 && ( +
+

성공적으로 등록되었습니다!

+ +
+ )} + {step === 4 && ( +
+

업로드 실패. 재시도 하세요.

+ +
+ )} +
+
+ ) : null; +} \ No newline at end of file diff --git a/src/components/Goalpage/MemberList/UploadModal/ImageUpload.tsx b/src/components/Goalpage/MemberList/UploadModal/ImageUpload.tsx new file mode 100644 index 00000000..16700a80 --- /dev/null +++ b/src/components/Goalpage/MemberList/UploadModal/ImageUpload.tsx @@ -0,0 +1,77 @@ +// ImageUpload.tsx +import React, { ChangeEvent, useState, useEffect } from 'react'; + +interface ImageUploadProps { + image: File | null; // 이미지 상태 + setImage: (file: File | null) => void; // 이미지 상태를 업데이트하는 함수 + onNext: () => void; // 다음 단계로 이동 + onCancel: () => void; // 취소 시 모달 닫기 +} + +export default function ImageUpload({ image, setImage, onNext, onCancel }: ImageUploadProps) { + const [previewUrl, setPreviewUrl] = useState(null); + + useEffect(() => { + if (image) { + const reader = new FileReader(); + reader.onload = () => { + setPreviewUrl(reader.result as string); + }; + reader.readAsDataURL(image); + } else { + setPreviewUrl(null); + } + }, [image]); + + const handleImageChange = (e: ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + setImage(file); + } + }; + + const handleImageDelete = () => { + setImage(null); + }; + + return ( +
+ + {previewUrl ? ( +
+ preview + +
+ ) : ( +
+ )} + + +
+ ); +} diff --git a/src/components/Goalpage/MemberList/UploadModal/SelectRoutineList.tsx b/src/components/Goalpage/MemberList/UploadModal/SelectRoutineList.tsx new file mode 100644 index 00000000..9818dadf --- /dev/null +++ b/src/components/Goalpage/MemberList/UploadModal/SelectRoutineList.tsx @@ -0,0 +1,94 @@ +// SelectRoutineList.tsx +import React, { useEffect, useState } from 'react'; +import { API } from '../../../../lib/api/index.ts'; + +interface SelectRoutineListProps { + index: number; // 루틴 인덱스 + setIndex: (index: number) => void; // 루틴 인덱스를 업데이트하는 함수 + selectedRoutine: string; // 선택된 루틴 + setSelectedRoutine: (routine: string) => void; // 루틴 상태를 업데이트하는 함수 + onNext: () => void; // 다음 단계로 이동 + onPrevious: () => void; // 이전 단계로 이동 + onCancel: () => void; // 취소 시 모달 닫기 +} + +export default function SelectRoutineList({ + index, + setIndex, + selectedRoutine, + setSelectedRoutine, + onNext, + onPrevious, + onCancel, +}: SelectRoutineListProps) { + const [routines, setRoutines] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchRoutines = async () => { + setLoading(true); + try { + const response = await API.User.getUserRoutine(); + + if (response.status === 'OK' && response.data) { + const routineList: string[] = Object.values(response.data); + setRoutines(routineList); + } + } catch (e) { + setError('루틴 목록을 불러오는 중 오류가 발생했습니다.'); + } finally { + setLoading(false); + } + }; + + fetchRoutines(); + }, []); + + const handleSelect = (routine: string, routineIndex: number) => { + setSelectedRoutine(routine); + setIndex(routineIndex); + }; + + return ( +
+
+ {routines.map((routine, i) => ( + + ))} +
+
+ +
+
+ + +
+
+ ); +} diff --git a/src/components/GuideCard.tsx b/src/components/GuideCard.tsx index 1fa4efd4..866dfe80 100644 --- a/src/components/GuideCard.tsx +++ b/src/components/GuideCard.tsx @@ -34,7 +34,7 @@ const GuideCard: React.FC = ({ /> )}
-
+

+
- {' '} - {/* 로그인 상태에 따라 prop 변경 */} + {/* 로그인 상태에 따라 prop 변경 */}
); diff --git a/src/components/Mypage/Goals/MaintenanceGoals.tsx b/src/components/Mypage/Goals/MaintenanceGoals.tsx index de4b0788..223e1a60 100644 --- a/src/components/Mypage/Goals/MaintenanceGoals.tsx +++ b/src/components/Mypage/Goals/MaintenanceGoals.tsx @@ -5,6 +5,7 @@ import Calendar from './Calander'; import DDayCounter from './DDayCounter'; import Pending from '../../Pending/Loading.tsx'; // Pending 컴포넌트 추가 import dayjs from 'dayjs'; +import { useNavigate } from 'react-router-dom'; // useNavigate import 추가 const MaintenanceGoals: React.FC = () => { const [dDay, setDDay] = useState(null); @@ -16,6 +17,8 @@ const MaintenanceGoals: React.FC = () => { const [error, setError] = useState(null); const [currentDate, setCurrentDate] = useState(new Date()); + const navigate = useNavigate(); // useNavigate 훅 사용 + useEffect(() => { const fetchCalendar = async () => { try { @@ -53,12 +56,23 @@ const MaintenanceGoals: React.FC = () => { fetchCalendar(); }, [currentDate]); + useEffect(() => { + if (error) { + navigate('?sector=maintenance'); // 에러 발생 시 경로 변경 + } + }, [error, navigate]); + return ( -
+
{loading ? ( // 로딩 중일 때만 Pending 컴포넌트를 표시 ) : error ? ( -
{error}
// 에러 메시지 표시 +
+ Empty Data +
// 에러 메시지 대신 Empty Data 표시 ) : ( <> {/* 하위 컴포넌트에 필요한 데이터 전달 */} diff --git a/src/components/Mypage/Goals/Setting/TeamsRoutine.tsx b/src/components/Mypage/Goals/Setting/TeamsRoutine.tsx new file mode 100644 index 00000000..f103b206 --- /dev/null +++ b/src/components/Mypage/Goals/Setting/TeamsRoutine.tsx @@ -0,0 +1,55 @@ +import React from 'react'; + +interface TeamsRoutineProps { + routineData: { + routine1: string; + routine2: string; + routine3: string; + routine4: string; + }; +} + +const TeamsRoutine: React.FC = ({ routineData }) => { + return ( +
+
+
+

+ 기초 체력 기르기 +

+
+ +
+
+ +
+
+ {routineData.routine1 || '루틴이 없습니다'} +
+
+ {routineData.routine2 || '루틴이 없습니다'} +
+
+ {routineData.routine3 || '루틴이 없습니다'} +
+
+ {routineData.routine4 || '루틴이 없습니다'} +
+
+
+
+
+ +
+ +
+
+ ); +}; + +export default TeamsRoutine; diff --git a/src/components/Mypage/Goals/SettingGoals.tsx b/src/components/Mypage/Goals/SettingGoals.tsx index 6600d5d0..80f7b9ed 100644 --- a/src/components/Mypage/Goals/SettingGoals.tsx +++ b/src/components/Mypage/Goals/SettingGoals.tsx @@ -4,23 +4,32 @@ import { API } from '../../../lib/api'; // API 모듈 경로에 맞게 import import CreateTeam from './Setting/CreateTeam'; // CreateTeam 컴포넌트를 임포트합니다. import InputCodeField from './Setting/InputCodeField'; // InviteCodeField 컴포넌트를 임포트합니다. import NotGood from '../../../assets/NotGood.svg'; +import Pending from '../../Pending/Loading.tsx'; // Pending 컴포넌트 추가 +import TeamsRoutine from './Setting/TeamsRoutine'; // TeamsRoutine 컴포넌트 import const SettingGoals: React.FC = () => { const [showCreateTeam, setShowCreateTeam] = useState(false); const [showInviteCode, setShowInviteCode] = useState(false); + const [userRoutine, setUserRoutine] = useState(null); // 유저 루틴 상태 관리 + const [loading, setLoading] = useState(true); // 로딩 상태 추가 - // 화면 렌더될 때 기록 조회 + // 화면 렌더될 때 기록 조회 및 유저 루틴 조회 useEffect(() => { - const fetchHistories = async () => { + const fetchUserRoutine = async () => { try { - const response = await API.Team.getHistories(); - console.log('기록 조회 성공:', response); + const routineResponse = await API.Team.getUserRoutine(); + console.log('유저 루틴 조회 성공:', routineResponse); + if (routineResponse.data) { + setUserRoutine(routineResponse.data); // 루틴이 있으면 상태에 저장 + } } catch (error) { - console.error('기록 조회 실패:', error); + console.error('유저 루틴 조회 실패:', error); + } finally { + setLoading(false); // 데이터가 로드되면 로딩 상태 해제 } }; - fetchHistories(); + fetchUserRoutine(); // 유저 루틴을 조회하고 상태에 저장 }, []); // 빈 배열을 사용하여 컴포넌트가 처음 렌더링될 때만 실행 const handleCreateTeamClick = () => { @@ -31,6 +40,10 @@ const SettingGoals: React.FC = () => { setShowInviteCode(true); }; + if (loading) { + return ; // 로딩 중일 때 Pending 컴포넌트를 표시 + } + if (showCreateTeam) { return ; } @@ -39,6 +52,12 @@ const SettingGoals: React.FC = () => { return ; } + // 유저 루틴이 있으면 TeamsRoutine 컴포넌트를 렌더링 + if (userRoutine) { + return ; + } + + // 유저 루틴이 없으면 팀 생성 UI를 표시 return (
Not Good diff --git a/src/lib/api/team/index.ts b/src/lib/api/team/index.ts index 14835080..316f1ef3 100644 --- a/src/lib/api/team/index.ts +++ b/src/lib/api/team/index.ts @@ -69,4 +69,14 @@ export namespace __Team { tokenOn: true, // 인증이 필요한 경우 토큰 포함 }); } + + export async function getUserRoutine() { + const url = `${BASE_URL}/team/routine-list`; + + return fetchData({ + url, + method: 'GET', + tokenOn: true, + }); + } } diff --git a/src/lib/api/user/index.ts b/src/lib/api/user/index.ts index 196f613e..7a7bd2e9 100644 --- a/src/lib/api/user/index.ts +++ b/src/lib/api/user/index.ts @@ -84,6 +84,46 @@ export namespace __User { export async function getMyInfo() { const url = `${BASE_URL}/user/info`; + return fetchData({ + url, + method: 'GET', + tokenOn: true, + }); + } + export async function currentStep() { + const url = `${BASE_URL}/user/current-step`; + + return fetchData({ + url, + method: 'GET', + tokenOn: true, + }); + } + export async function uploadRoutine( + routineIndex: string, + routineName: string, + file: File + ) { + const url = `${BASE_URL}/routine/upload`; + + // FormData 객체 생성 + const formData = new FormData(); + formData.append('routineIndex', routineIndex.toString()); + formData.append('routineName', routineName); + formData.append('file', file); + + return fetchData({ + url, + method: 'POST', + body: formData, + tokenOn: true, + isFormData: true, + }); + } + + export async function getUserRoutine() { + const url = `${BASE_URL}/team/routine-list`; + return fetchData({ url, method: 'GET', diff --git a/src/pages/guide.tsx b/src/pages/guide.tsx index af668035..c1b162ef 100644 --- a/src/pages/guide.tsx +++ b/src/pages/guide.tsx @@ -70,10 +70,9 @@ export default function Guide() { )} {/* 부모 요소에 flex와 justify-center 추가 */} -
- {' '} - {/* 로그인 상태에 따라 prop 변경 */} -
+ +
+ {/* 로그인 상태에 따라 prop 변경 */}
); } diff --git a/src/pages/register.tsx b/src/pages/register.tsx index c02f67f8..1e4dda43 100644 --- a/src/pages/register.tsx +++ b/src/pages/register.tsx @@ -14,6 +14,7 @@ export default function Register() { const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [isPending, setIsPending] = useState(true); // 로딩 상태 관리 (처음엔 true) + const [passwordMatch, setPasswordMatch] = useState(true); // 비밀번호 일치 여부 상태 // 컴포넌트가 마운트될 때 0.2초 동안 Pending 표시 useEffect(() => { @@ -51,6 +52,11 @@ export default function Register() { }, 200); }; + // 비밀번호 확인 필드에서 입력할 때마다 일치 여부 확인 + useEffect(() => { + setPasswordMatch(password === confirmPassword); + }, [password, confirmPassword]); + return (
{isPending ? ( @@ -109,8 +115,15 @@ export default function Register() { /> + {!passwordMatch && ( + + 비밀번호가 일치하지 않습니다. + + )}
{/* 확인 및 취소 버튼 영역 */} @@ -133,6 +151,7 @@ export default function Register() { 'hover:bg-[#4A72D1] focus:outline-none focus:ring-2 focus:ring-[#5A82F1]' )} onClick={handleRegister} + disabled={!passwordMatch} // 비밀번호가 일치하지 않으면 버튼 비활성화 > 확인