diff --git a/public/styles/casual.jpg b/public/styles/casual.jpg new file mode 100644 index 00000000..29573433 Binary files /dev/null and b/public/styles/casual.jpg differ diff --git a/public/styles/classic.jpg b/public/styles/classic.jpg new file mode 100644 index 00000000..d0418bc4 Binary files /dev/null and b/public/styles/classic.jpg differ diff --git a/public/styles/feminine.jpg b/public/styles/feminine.jpg new file mode 100644 index 00000000..3bb0df49 Binary files /dev/null and b/public/styles/feminine.jpg differ diff --git a/public/styles/formal.jpg b/public/styles/formal.jpg new file mode 100644 index 00000000..f0252c50 Binary files /dev/null and b/public/styles/formal.jpg differ diff --git a/public/styles/hip.jpg b/public/styles/hip.jpg new file mode 100644 index 00000000..a4a7a2d6 Binary files /dev/null and b/public/styles/hip.jpg differ diff --git a/public/styles/luxury.jpg b/public/styles/luxury.jpg new file mode 100644 index 00000000..b466eb6a Binary files /dev/null and b/public/styles/luxury.jpg differ diff --git a/public/styles/minimal.jpg b/public/styles/minimal.jpg new file mode 100644 index 00000000..1751fc61 Binary files /dev/null and b/public/styles/minimal.jpg differ diff --git a/public/styles/outdoor.jpg b/public/styles/outdoor.jpg new file mode 100644 index 00000000..e33c7872 Binary files /dev/null and b/public/styles/outdoor.jpg differ diff --git a/public/styles/street.jpg b/public/styles/street.jpg new file mode 100644 index 00000000..c48b29f0 Binary files /dev/null and b/public/styles/street.jpg differ diff --git a/src/App.tsx b/src/App.tsx index e2069bac..65eab80d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,7 @@ import LoginComplete from '@pages/Login/LoginComplete'; import SignUp from '@pages/SignUp'; import TermsAgreement from '@pages/SignUp/TermsAgreement'; +import PickMyStyle from '@pages/SignUp/PickMyStyle'; import Profile from '@pages/Profile'; import ProfileEdit from '@pages/Profile/ProfileEdit'; @@ -64,6 +65,7 @@ const publicRoutes = [ { path: '/signup', element: }, { path: '/signup/terms-agreement', element: }, + { path: '/signup/pick-my-style', element: }, ]; const App: React.FC = () => { diff --git a/src/apis/auth/dto.ts b/src/apis/auth/dto.ts index e0490291..1c7d361d 100644 --- a/src/apis/auth/dto.ts +++ b/src/apis/auth/dto.ts @@ -12,4 +12,5 @@ export interface getUserInfoByJwtData { profilePictureUrl: string; bio: string; birthDate: string; + userStyletags: string[]; } diff --git a/src/apis/user/dto.ts b/src/apis/user/dto.ts index 0cc1d5ff..3a6b8109 100644 --- a/src/apis/user/dto.ts +++ b/src/apis/user/dto.ts @@ -11,6 +11,7 @@ export interface UserInfoData { bio: string; birthDate: string; isFriend: boolean; + userStyletags: string[]; } // 사용자 정보 조회 응답 @@ -28,6 +29,7 @@ export interface PatchUserInfoRequest { nickname: string; profilePictureUrl: string; bio: string; + userStyletags: string[]; } // 회원 탈퇴 응답 diff --git a/src/pages/Profile/ProfileEdit/index.tsx b/src/pages/Profile/ProfileEdit/index.tsx index a3526002..b9eb24a3 100644 --- a/src/pages/Profile/ProfileEdit/index.tsx +++ b/src/pages/Profile/ProfileEdit/index.tsx @@ -49,6 +49,7 @@ const ProfileEdit: React.FC = () => { const [birthDate, setBirthDate] = useState(''); const [name, setName] = useState(''); const [email, setEmail] = useState(''); + const [userStyletags, setUserStyletags] = useState([]); const [isLoading, setIsLoading] = useState(true); const navigate = useNavigate(); const [modalContent, setModalContent] = useState(null); @@ -74,6 +75,7 @@ const ProfileEdit: React.FC = () => { setBirthDate(userInfo.birthDate || ''); setName(userInfo.name || ''); setEmail(userInfo.email || ''); + setUserStyletags(userInfo.userStyletags || []); } catch (error) { console.error('Error fetching user info:', error); } finally { @@ -128,6 +130,7 @@ const ProfileEdit: React.FC = () => { nickname: nickname || '닉네임 없음', profilePictureUrl: profilePictureUrl || '', bio: bio || '', + userStyletags: userStyletags || [], }; const response = await patchUserInfoApi(payload, currentUserId); diff --git a/src/pages/SignUp/PickMyStyle/index.tsx b/src/pages/SignUp/PickMyStyle/index.tsx new file mode 100644 index 00000000..ca9fc599 --- /dev/null +++ b/src/pages/SignUp/PickMyStyle/index.tsx @@ -0,0 +1,122 @@ +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { getUserInfoApi, patchUserInfoApi } from '@apis/user'; +import { PatchUserInfoRequest } from '@apis/user/dto'; +import { getCurrentUserId } from '@utils/getCurrentUserId'; +import { styleImages } from '@utils/styleImages'; + +import Back from '@assets/arrow/left.svg'; + +import BottomButton from '@components/BottomButton'; +import { OODDFrame } from '@components/Frame/Frame'; +import Modal from '@components/Modal'; +import TopBar from '@components/TopBar'; + +import { PickMyStyleLayout, StyledSubTitle, StyledTitle, CategoryList, PlaceholderImage } from './style'; + +const PickMyStyle: React.FC = () => { + const [nickname, setNickname] = useState(''); + const [clickedImages, setClickedImages] = useState<{ [key: number]: boolean }>({}); + + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalMessage, setModalMessage] = useState(''); + + const navigate = useNavigate(); + const currentUserId = getCurrentUserId(); + + // 유저 정보 가져오기 + useEffect(() => { + const getUserInfo = async () => { + try { + const userInfo = await getUserInfoApi(currentUserId); + setNickname(userInfo.data.nickname); + } catch (error) { + console.error('유저 정보 불러오기 실패:', error); + } + }; + getUserInfo(); + }, [currentUserId]); + + // 이미지 클릭 시 상태 변경 + const handleImageClick = (id: number) => { + setClickedImages((prev) => ({ + ...prev, + [id]: !prev[id], // 클릭할 때마다 토글 + })); + }; + + const handleSubmitBtnClick = async () => { + const selectedCategories = Object.keys(clickedImages) + .filter((id) => clickedImages[Number(id)]) // 클릭된 이미지만 필터링 + .map((id) => styleImages.find((img) => img.id === Number(id))?.category) // category 값 가져오기 + .filter((category): category is string => !!category); // undefined 제거 + + const requestData: Partial = { + userStyletags: selectedCategories, + }; + console.log(requestData); + + try { + const data = await patchUserInfoApi(requestData, currentUserId); + console.log(data); + navigate('/'); + } catch (error) { + console.error('API 요청 실패:', error); + setModalMessage('스타일 선택 중 오류가 발생했습니다.'); + console.log(requestData); + setIsModalOpen(true); + } + }; + + const handleModalClose = () => { + setIsModalOpen(false); + }; + + return ( + + { + window.history.back(); + }} + /> + + + {nickname}님의 취향을 알려주세요! + + + OODD가 당신의 취향을 분석하여 맞춤 스타일을 추천해 드릴게요. + + + {styleImages.map((image) => ( + handleImageClick(image.id)} + data-category={image.category} + > + {`${image.category} + + ))} + + + {isModalOpen && } + + + ); +}; + +export default PickMyStyle; diff --git a/src/pages/SignUp/PickMyStyle/style.tsx b/src/pages/SignUp/PickMyStyle/style.tsx new file mode 100644 index 00000000..defc0c93 --- /dev/null +++ b/src/pages/SignUp/PickMyStyle/style.tsx @@ -0,0 +1,76 @@ +import { styled } from 'styled-components'; + +import { StyledText } from '@components/Text/StyledText'; + +export const OODDFrame = styled.div` + width: 100%; + height: 100vh; // 화면 전체 높이 차지 + overflow: hidden; // 전체 화면 스크롤 방지 + display: flex; + flex-direction: column; +`; + +export const PickMyStyleLayout = styled.div` + display: flex; + flex-direction: column; + padding: 0 1.875rem; + flex: 1; // 남은 공간을 다 차지하도록 설정 + width: 100%; + height: 100%; + overflow: hidden; // 상위 요소의 스크롤 방지 +`; + +export const StyledTitle = styled(StyledText)` + margin: 0.625rem 0; +`; + +export const StyledSubTitle = styled(StyledText)` + margin-bottom: 1.25rem; +`; + +export const CategoryList = styled.div` + width: 100%; + max-width: 31.25rem; + margin: auto; + flex-grow: 1; + overflow-y: auto; + padding-bottom: 6.25rem; + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0.625rem; + + // 스크롤바 숨기기 + &::-webkit-scrollbar { + display: none; + } + scrollbar-width: none; +`; + +export const PlaceholderImage = styled.div<{ $isClicked: boolean }>` + width: 100%; + aspect-ratio: 1; + background-color: lightgray; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.875rem; + border-radius: 0.5rem; + transition: + transform 0.2s ease, + box-shadow 0.2s ease; + + transform: ${({ $isClicked }) => ($isClicked ? 'scale(0.9)' : 'scale(1)')}; + box-shadow: ${({ $isClicked }) => ($isClicked ? '0 0.125rem 0.25rem rgba(0, 0, 0, 0.2)' : 'none')}; + + &:hover { + transform: ${({ $isClicked }) => ($isClicked ? 'scale(0.9)' : 'scale(0.95)')}; + } + cursor: pointer; + + img { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 0.5rem; + } +`; diff --git a/src/pages/SignUp/TermsAgreement/index.tsx b/src/pages/SignUp/TermsAgreement/index.tsx index fee82a38..4d9cb54c 100644 --- a/src/pages/SignUp/TermsAgreement/index.tsx +++ b/src/pages/SignUp/TermsAgreement/index.tsx @@ -67,7 +67,7 @@ const TermsAgreement: React.FC = () => { try { const response = await postTermsAgreementApi(currentUserId); console.log(response); - navigate('/'); // 성공 시 홈으로 이동 + navigate('/signup/pick-my-style'); // 성공 시 취향 선택 UI로 이동 } catch (error) { console.error('약관 동의 API 호출 실패:', error); const errorMessage = handleError(error); @@ -102,7 +102,7 @@ const TermsAgreement: React.FC = () => { onChange={handleAllAgreementChange} id="all-agreement" /> -