Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added public/styles/casual.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/styles/classic.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/styles/feminine.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/styles/formal.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/styles/hip.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/styles/luxury.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/styles/minimal.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/styles/outdoor.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/styles/street.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -64,6 +65,7 @@ const publicRoutes = [

{ path: '/signup', element: <SignUp /> },
{ path: '/signup/terms-agreement', element: <TermsAgreement /> },
{ path: '/signup/pick-my-style', element: <PickMyStyle /> },
];

const App: React.FC = () => {
Expand Down
1 change: 1 addition & 0 deletions src/apis/auth/dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ export interface getUserInfoByJwtData {
profilePictureUrl: string;
bio: string;
birthDate: string;
userStyletags: string[];
}
2 changes: 2 additions & 0 deletions src/apis/user/dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface UserInfoData {
bio: string;
birthDate: string;
isFriend: boolean;
userStyletags: string[];
}

// 사용자 정보 조회 응답
Expand All @@ -28,6 +29,7 @@ export interface PatchUserInfoRequest {
nickname: string;
profilePictureUrl: string;
bio: string;
userStyletags: string[];
}

// 회원 탈퇴 응답
Expand Down
3 changes: 3 additions & 0 deletions src/pages/Profile/ProfileEdit/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const ProfileEdit: React.FC = () => {
const [birthDate, setBirthDate] = useState<string>('');
const [name, setName] = useState<string>('');
const [email, setEmail] = useState<string>('');
const [userStyletags, setUserStyletags] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const navigate = useNavigate();
const [modalContent, setModalContent] = useState<string | null>(null);
Expand All @@ -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 {
Expand Down Expand Up @@ -128,6 +130,7 @@ const ProfileEdit: React.FC = () => {
nickname: nickname || '닉네임 없음',
profilePictureUrl: profilePictureUrl || '',
bio: bio || '',
userStyletags: userStyletags || [],
};

const response = await patchUserInfoApi(payload, currentUserId);
Expand Down
122 changes: 122 additions & 0 deletions src/pages/SignUp/PickMyStyle/index.tsx
Original file line number Diff line number Diff line change
@@ -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<PatchUserInfoRequest> = {
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);
}
};
Comment on lines +49 to +70
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

requestData에서 userStyletags 외에 다른 데이터가 포함될 여지가 없다면 Pick을 사용하는 편이 타입 안정성 측면에서 더 좋을 것 같기는 합니다!


const handleModalClose = () => {
setIsModalOpen(false);
};

return (
<OODDFrame>
<TopBar
LeftButtonSrc={Back}
onClickLeftButton={() => {
window.history.back();
}}
/>
<PickMyStyleLayout>
<StyledTitle
$textTheme={{
style: { mobile: 'heading1-bold', tablet: 'title2-bold', desktop: 'title2-bold' },
}}
>
{nickname}님의 취향을 알려주세요!
</StyledTitle>
<StyledSubTitle
$textTheme={{
style: { mobile: 'caption1-medium', tablet: 'body2-medium', desktop: 'body2-medium' },
}}
>
OODD가 당신의 취향을 분석하여 맞춤 스타일을 추천해 드릴게요.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

자신이 고른 옷 스타일과 유사한 이성을 추천해주는건가요?!
이것도 자신의 스타일 태그를 선택하는 페이지랑 자신이 원하는 이성의 스타일을 선택하는 페이지를 세분화해두면 나중에 홈에 띄울 때나 추천해줄 때 더 수월하고
유저 입장에서도 자기가 원하는 스타일을 직접 선택하고 추천받을 수 있어서 좋을 것 같다는 생각!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네넹 스트릿, 페미닌 등을 골랐으면 해당 스타일의 게시글을 가진 이성을 추천해주는 거라 이해했습니당
근데 자신의 스타일 태그를 회원가입 때 선택하는 건 좋은 의견인 것 같아요!!! 현재는 그냥 게시글의 태그를 바탕으로 남의 스타일을 분석해서 추천해 주는 건데 게시글의 스타일이 너무 중구난방일 수도 있으니 😅 @happbob

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋은 의견입니다. 다만 스타일 태그를 회원가입 때 선택할 때 그 스타일이 어떤 느낌인지 모르는 상황이 있을 수 있어 지금과 같은 기획을 한것이니 해당 리스트 카테고리를 보여주는 기준에 대한 정책을 개선시키는 방향이 좋을 듯합니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그럼 이건 다음 스프린트나 목요일 회의에서 정하나요?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이건 백로그로 등록 시켜두고 스프린트 회의때 얘기해보죠

</StyledSubTitle>
<CategoryList>
{styleImages.map((image) => (
<PlaceholderImage
key={image.id}
$isClicked={!!clickedImages[image.id]}
onClick={() => handleImageClick(image.id)}
data-category={image.category}
>
<img src={image.src} alt={`${image.category} 스타일`} />
</PlaceholderImage>
))}
</CategoryList>
<BottomButton
content="OODD 시작하기"
onClick={handleSubmitBtnClick}
disabled={!Object.values(clickedImages).some(Boolean)}
/>
{isModalOpen && <Modal content={modalMessage} onClose={handleModalClose} />}
</PickMyStyleLayout>
</OODDFrame>
);
};

export default PickMyStyle;
76 changes: 76 additions & 0 deletions src/pages/SignUp/PickMyStyle/style.tsx
Original file line number Diff line number Diff line change
@@ -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;
}
`;
8 changes: 4 additions & 4 deletions src/pages/SignUp/TermsAgreement/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -102,7 +102,7 @@ const TermsAgreement: React.FC = () => {
onChange={handleAllAgreementChange}
id="all-agreement"
/>
<label htmlFor="all-agreement">
<label htmlFor="all-agreement" style={{ cursor: 'pointer' }}>
<StyledText $textTheme={{ style: 'body1-medium' }}>약관 전체 동의</StyledText>
</label>
</CheckboxItem>
Expand All @@ -116,14 +116,14 @@ const TermsAgreement: React.FC = () => {
onChange={() => handleAgreementChange(key)}
id={key}
/>
<label htmlFor={key}>
<label htmlFor={key} style={{ cursor: 'pointer' }}>
<StyledText $textTheme={{ style: 'body2-regular' }}>{label}</StyledText>
</label>
</CheckboxItem>
))}
</CheckboxList>
<BottomButton
content="OODD 시작하기"
content="다음"
onClick={handleCompletedClick}
disabled={!agreements.terms || !agreements.privacy}
/>
Expand Down
22 changes: 22 additions & 0 deletions src/utils/styleImages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export const styleImages = [
{ category: 'luxury', id: 0, src: '/styles/classic.jpg' },
{ category: 'casual', id: 1, src: '/styles/casual.jpg' },
{ category: 'street', id: 2, src: '/styles/hip.jpg' },
{ category: 'feminine', id: 3, src: '/styles/feminine.jpg' },
{ category: 'hip', id: 4, src: '/styles/luxury.jpg' },
{ category: 'outdoor', id: 5, src: '/styles/street.jpg' },
{ category: 'casual', id: 6, src: '/styles/feminine.jpg' },
{ category: 'classic', id: 7, src: '/styles/hip.jpg' },
{ category: 'sporty', id: 8, src: '/styles/casual.jpg' },
{ category: 'formal', id: 9, src: '/styles/formal.jpg' },
{ category: 'feminine', id: 10, src: '/styles/luxury.jpg' },
{ category: 'street', id: 11, src: '/styles/street.jpg' },
{ category: 'minimal', id: 12, src: '/styles/casual.jpg' },
{ category: 'outdoor', id: 13, src: '/styles/classic.jpg' },
{ category: 'formal', id: 14, src: '/styles/formal.jpg' },
{ category: 'sporty', id: 15, src: '/styles/outdoor.jpg' },
{ category: 'hip', id: 16, src: '/styles/hip.jpg' },
{ category: 'minimal', id: 17, src: '/styles/minimal.jpg' },
{ category: 'classic', id: 18, src: '/styles/classic.jpg' },
{ category: 'luxury', id: 19, src: '/styles/luxury.jpg' },
];