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}
+ >
+
+
+ ))}
+
+
+ {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"
/>
-