Skip to content

Commit

Permalink
Merge pull request #27 from KingBoRam/feat/myprofile
Browse files Browse the repository at this point in the history
✨ Feat: myprofile 수정 기능
  • Loading branch information
KingBoRam authored Sep 3, 2024
2 parents 13d60bc + a62b1de commit 64c177d
Show file tree
Hide file tree
Showing 23 changed files with 465 additions and 224 deletions.
3 changes: 0 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { MyProfile, MyProfileBoard, MyProfileEdit, MyProfileThunder } from './pa
import { Thunder, ThunderChat, ThunderId, ThunderPost } from './pages/thunder';
import Landing from './pages/Home/Landing.tsx';
import ProfileId from './pages/profile/ProfileId.tsx';
import Terms from './pages/terms/Terms.tsx';
import ImageOverview from './pages/Image/ImageOverview.tsx';

function App() {
Expand Down Expand Up @@ -62,8 +61,6 @@ function App() {
<Route path="thunderpost" element={<ThunderPost />} />
</Route>

<Route path="terms" element={<Terms />} />

<Route path="profile/:Id" element={<ProfileId />} />

<Route path="image" element={<ImageOverview />} />
Expand Down
6 changes: 4 additions & 2 deletions src/api/util/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,10 @@ export async function refreshAccessToken(option?: string): Promise<string | null
}

// 로그아웃 함수
function logout(): void {
export function logout(option?: string): void {
removeItem('access'); // 로컬 스토리지에서 액세스 토큰 삭제
deleteCookie('refresh'); // 쿠키에서 리프레시 토큰 삭제
window.location.href = '/home'; // 홈 페이지로 리디렉션
if (!option) {
window.location.href = '/'; // 홈 페이지로 리디렉션
}
}
8 changes: 4 additions & 4 deletions src/components/common/ModalCenter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import ReactDOM from 'react-dom';
interface ModalCenterProps {
isOpen: boolean;
onClose: () => void;
title1: string;
title2: string;
children: React.ReactNode;
title1?: string;
title2?: string;
children?: React.ReactNode;
}

// ModalCenter 를 위한 컴포넌트.
Expand All @@ -16,7 +16,7 @@ const ModalCenter: React.FC<ModalCenterProps> = ({ isOpen, title1, title2, child

return ReactDOM.createPortal(
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
<div className="relative w-[600px] rounded-lg bg-white p-8 shadow-lg">
<div className="relative mx-3 w-[600px] rounded-lg bg-white p-8 shadow-lg">
<div className="text-center text-2xl font-semibold">{title1}</div>
<div className="text-center text-2xl font-semibold">{title2}</div>
<div className="mt-4 text-center">{children}</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/landing/FirstSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Link } from 'react-router-dom';
interface FirstSectionProps {
isVisible: boolean;
setIsImageLoaded: (loaded: boolean) => void;
spicy: number | null;
spicy: number | null | undefined;
}

const FirstSection: React.FC<FirstSectionProps> = ({ isVisible, setIsImageLoaded, spicy }) => (
Expand Down
6 changes: 2 additions & 4 deletions src/components/landing/FoodSection.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { Link } from 'react-router-dom';
import { IoIosArrowForward } from 'react-icons/io';
import FoodsCarousel from './FoodsCarousel';
import { Dispatch, SetStateAction } from 'react';

interface FoodSectionProps {
spicy: number | null;
setConfirmFlavor: Dispatch<SetStateAction<boolean>>;
}

const FoodSection: React.FC<FoodSectionProps> = ({ spicy, setConfirmFlavor }) => {
const FoodSection: React.FC<FoodSectionProps> = ({ spicy }) => {
return (
<div className="relative h-full w-full">
<div className="flex items-center gap-5 p-4 pb-0">
Expand All @@ -17,7 +15,7 @@ const FoodSection: React.FC<FoodSectionProps> = ({ spicy, setConfirmFlavor }) =>
더보기
</Link>
</div>
<FoodsCarousel spicy={spicy} setConfirmFlavor={setConfirmFlavor} />
<FoodsCarousel spicy={spicy} />
{!spicy && (
<div className="absolute left-0 top-0 z-10 flex h-full w-full items-center justify-center bg-black bg-opacity-50">
<div className="flex h-[40%] w-[80%] flex-col items-center justify-center rounded-lg bg-[#f5f5f5] p-6 xs:h-[50%]">
Expand Down
6 changes: 2 additions & 4 deletions src/components/landing/FoodsCarousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'swiper/css';
import 'swiper/css/pagination';
import ImageWithPlaceholder from './ImageWithPlaceHolder';
import { Link } from 'react-router-dom';
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import { FoodsCarouselList } from '../../data/FoodsCarouselList';
import { Swiper, SwiperSlide } from 'swiper/react';
import { FoodsList } from '../../types/types';
Expand All @@ -11,10 +11,9 @@ import { authInstance } from '../../api/util/instance';

interface FoodsCarouselProps {
spicy: number | null;
setConfirmFlavor: Dispatch<SetStateAction<boolean>>;
}

const FoodsCarousel: React.FC<FoodsCarouselProps> = ({ spicy, setConfirmFlavor }) => {
const FoodsCarousel: React.FC<FoodsCarouselProps> = ({ spicy }) => {
const [userFoods, setUserFoods] = useState<FoodsList[]>(FoodsCarouselList);
const { setFoodsList } = useFoodsListStore();

Expand All @@ -38,7 +37,6 @@ const FoodsCarousel: React.FC<FoodsCarouselProps> = ({ spicy, setConfirmFlavor }
})
.catch((err) => {
console.error('Error fetching foods:', err);
setConfirmFlavor(false);
});
}
}, [spicy]);
Expand Down
17 changes: 17 additions & 0 deletions src/components/myprofile/FTILink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Link } from 'react-router-dom';

interface FTILinkProps {
ftiType: string | null;
}

export const FTILink: React.FC<FTILinkProps> = ({ ftiType }) => {
return ftiType ? (
<p className="text-[1rem] font-medium">{ftiType}</p>
) : (
<Link
to={'/FTI'}
className="mt-3 flex h-[52px] w-[33%] items-center justify-center rounded-lg bg-[#F5E3DB] text-[1rem] font-bold xs:h-[42px] xs:w-[40%] xs:text-[13px]">
FTI 설정하기
</Link>
);
};
10 changes: 10 additions & 0 deletions src/components/myprofile/LoadingSpinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { motion } from 'framer-motion';

export const LoadingSpinner = () => (
<motion.div
className="absolute inset-0 flex items-center justify-center"
animate={{ rotate: 360 }}
transition={{ repeat: Infinity, duration: 1, ease: 'linear' }}>
<div className="h-10 w-10 rounded-full border-4 border-primary border-t-transparent" />
</motion.div>
);
50 changes: 50 additions & 0 deletions src/components/myprofile/ProfileForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react';
import Button from '../../components/common/Button';
import { UseFormRegister } from 'react-hook-form';

interface ProfileFormProps {
register: UseFormRegister<any>;
duplicateNickname: string;
handleNicknameBlur: (e: React.FocusEvent<HTMLInputElement>) => void;
isLoading: boolean;
}

export const ProfileForm: React.FC<ProfileFormProps> = ({
register,
duplicateNickname,
handleNicknameBlur,
isLoading,
}) => {
return (
<div className="m-[1rem]">
<label htmlFor="nickname" className="mb-2 block font-medium xs:text-[14px]">
닉네임
</label>
<input
id="nickname"
type="text"
className={`${duplicateNickname ? 'border-primary' : ''} h-[56px] w-full rounded-lg border py-[15px] pl-[15px] focus:border-gray-98 xs:h-[45px]`}
{...register('nickname', { required: true })}
onBlur={handleNicknameBlur}
/>
{duplicateNickname && <p className="ml-1 text-[12px] text-red">{duplicateNickname}</p>}
<label htmlFor="introduce" className="mb-2 mt-5 block font-medium xs:mb-1 xs:mt-2 xs:text-[14px]">
내 소개
</label>
<input
id="introduce"
type="text"
className="h-[56px] w-full rounded-lg border py-[15px] pl-[15px] text-[#2D2C2C] focus:border-gray-98 xs:h-[45px]"
{...register('introduce')}
/>
<Button
type="submit"
className={`mt-10 h-[56px] font-bold xs:mt-8 xs:h-[45px] ${isLoading ? 'cursor-not-allowed opacity-50' : ''}`}
buttonSize="normal"
bgColor="filled"
disabled={duplicateNickname !== '' ? true : false}>
저장하기
</Button>
</div>
);
};
15 changes: 15 additions & 0 deletions src/components/myprofile/ProfileImageInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';

interface ProfileImageInputProps {
handleImageChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}

export const ProfileImageInput: React.FC<ProfileImageInputProps> = ({ handleImageChange }) => (
<>
<input id="profileImg" type="file" accept="image/*" className="hidden" onChange={handleImageChange} />
<label
htmlFor="profileImg"
className="absolute bottom-2 right-2 h-[20%] w-[20%] cursor-pointer bg-[url('/images/edit_pencil.svg')] bg-cover xs:h-[30px] xs:w-[30px]"
/>
</>
);
36 changes: 36 additions & 0 deletions src/components/myprofile/ProfileImageSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from 'react';
import { FTILink } from './FTILink';
import { LoadingSpinner } from './LoadingSpinner';
import { ProfileImageInput } from './ProfileImageInput';

interface ProfileImageSectionProps {
profileImg: string;
isLoading: boolean;
handleImageChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
ftiType: string | null;
}

export const ProfileImageSection: React.FC<ProfileImageSectionProps> = ({
profileImg,
isLoading,
handleImageChange,
ftiType,
}) => {
return (
<div className="flex h-[35%] flex-col items-center justify-evenly p-[1rem]">
<div className="relative mb-3 flex w-[30%] items-center justify-center pt-[30%] xs:w-[35%] xs:pt-[35%]">
{isLoading ? (
<LoadingSpinner />
) : (
<img
className="absolute inset-0 h-full w-full rounded-full object-cover"
src={profileImg}
alt="프로필 이미지"
/>
)}
<ProfileImageInput handleImageChange={handleImageChange} />
</div>
<FTILink ftiType={ftiType} />
</div>
);
};
2 changes: 1 addition & 1 deletion src/components/myprofile/UserLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const UserLink: React.FC<UserLinkProps> = ({ text, src, isUserLoggedIn }) => {
<Link
to={isUserLoggedIn ? src : '#'}
onClick={handleNavigate}
className="flex w-full cursor-pointer items-center justify-between p-[1rem] text-[20px] hover:rounded-lg hover:font-bold hover:text-primary xs:py-[0.7rem] xs:text-[14px]">
className="flex w-full cursor-pointer items-center justify-between p-[12px] text-[20px] hover:rounded-lg hover:font-bold hover:text-primary xs:py-[0.7rem] xs:text-[14px]">
{text}
<IoIosArrowForward className="text-[20px] xs:text-[16px]" />
</Link>
Expand Down
9 changes: 2 additions & 7 deletions src/pages/Home/Landing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ import { getItem } from '../../utils/storage';
const Landing: React.FC = () => {
const [isVisible, setIsVisible] = useState<boolean>(false);
const [isImageLoaded, setIsImageLoaded] = useState<boolean>(false);
const [confirmFlavor, setConfirmFlavor] = useState<boolean>(false);
const [spicy, setSpicy] = useState<number | null>(null);
const [spicy, setSpicy] = useState<number | undefined | null>(undefined);

useEffect(() => {
const checkLoginStatus = async () => {
Expand All @@ -20,15 +19,11 @@ const Landing: React.FC = () => {
token = await refreshAccessToken('noErrorCode');
}
if (token) {
setConfirmFlavor(true);
const res = await authInstance.get('/api/profile/');
setSpicy(res.data.spicy_preference);
} else {
setConfirmFlavor(false);
}
} catch (error) {
console.error('Failed to fetch profile or refresh token:', error);
setConfirmFlavor(false);
}
};
checkLoginStatus();
Expand All @@ -44,7 +39,7 @@ const Landing: React.FC = () => {
return (
<div className="mx-auto mb-[75px] flex max-w-full flex-col items-center xs:mb-[65px]">
<FirstSection isVisible={isVisible} setIsImageLoaded={setIsImageLoaded} spicy={spicy} />
{confirmFlavor && <FoodSection spicy={spicy} setConfirmFlavor={setConfirmFlavor} />}
{spicy !== undefined && <FoodSection spicy={spicy} />}
<Section
title="FTI 검사"
subtitle={
Expand Down
47 changes: 35 additions & 12 deletions src/pages/board/Board.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Link } from 'react-router-dom';
import { useNavigate } from 'react-router-dom'; // useNavigate 추가
import { useState, useEffect } from 'react';
import { authInstance } from '../../api/util/instance'; // authInstance 가져오기
import { baseInstance } from '../../api/util/instance'; // authInstance 가져오기
import { getCookie } from '../../utils/cookie'; // getCookie 가져오기
import BoardCard from '../../components/board/BoardCard'; // BoardCard 컴포넌트 추가
import Loading from '../../components/common/Loading'; // Loading 컴포넌트 추가
import ModalBottom from '../../components/common/ModalBottom';

const Board = () => {
const [selectedBoard, setSelectedBoard] = useState<string>('전체'); // 초기값을 '전체'로 설정
Expand All @@ -20,6 +22,8 @@ const Board = () => {
}[]
>([]); // 게시판 목록 상태 추가
const [isLoading, setIsLoading] = useState<boolean>(true); // 로딩 상태 추가
const [isLoginModalOpen, setIsLoginModalOpen] = useState<boolean>(false); // 로그인 모달 상태 추가
const navigate = useNavigate(); // useNavigate 훅 사용

useEffect(() => {
setSelectedBoard('전체'); // 컴포넌트가 처음 렌더링될 때 '전체'로 설정
Expand All @@ -28,7 +32,7 @@ const Board = () => {

const fetchBoardList = async () => {
try {
const response = await authInstance.get('/api/reviews/');
const response = await baseInstance.get('/api/reviews/');
setBoardList(response.data.reviews);
setIsLoading(false);
} catch (error) {
Expand All @@ -37,6 +41,15 @@ const Board = () => {
}
};

const checkLogin = () => {
const token = getCookie('refresh');
if (!token) {
setIsLoginModalOpen(true); // 로그인 모달 열기
} else {
navigate('/board/boardpost'); // 로그인 되어 있으면 게시물 작성 페이지로 이동
}
};

const filteredBoardList =
selectedBoard === '전체'
? boardList
Expand All @@ -50,11 +63,6 @@ const Board = () => {
return <Loading />;
}

// 로딩 상태일 때 Loading 컴포넌트 렌더링
if (isLoading) {
return <Loading />;
}

return (
<div className="relative mb-2 h-full w-full max-w-[600px] p-4 pt-0">
<div className="fixed top-[72px] z-20 w-full max-w-[600px] bg-white pr-8 xs:top-[52px]">
Expand Down Expand Up @@ -113,11 +121,26 @@ const Board = () => {
})}
</div>
)}
<Link
to={'/board/boardpost'}
className="fixed bottom-[120px] right-[calc(50%-260px)] z-10 xs:bottom-[100px] xs:right-[calc(5%)]">
<div
onClick={checkLogin} // 링크 대신 onClick으로 로그인 체크
className="fixed bottom-[120px] right-[calc(50%-260px)] z-10 cursor-pointer xs:bottom-[100px] xs:right-[calc(5%)]">
<div className='h-[63px] w-[63px] bg-[url("/images/plusCircle.svg")] bg-cover bg-center bg-no-repeat transition-transform duration-200 ease-in-out hover:scale-110 active:scale-90 xs:h-[53px] xs:w-[53px]' />
</Link>
</div>
{/* 로그인 모달 추가 */}
<ModalBottom isOpen={isLoginModalOpen} onClose={() => setIsLoginModalOpen(false)}>
<div className="p-4 text-center">
<p className="text-lg font-semibold">로그인이 필요한 서비스 입니다.</p>
<p className="mt-2 text-sm text-gray-600">게시글을 만들려면 먼저 로그인해주세요.</p>
<button
className="mt-4 w-full rounded-lg bg-primary px-10 py-2 font-bold text-white"
onClick={() => {
setIsLoginModalOpen(false);
navigate('/signIn');
}}>
로그인하기
</button>
</div>
</ModalBottom>
</div>
);
};
Expand Down
Loading

0 comments on commit 64c177d

Please sign in to comment.