(tooltipRef.current[index] = el!)} key={index}>
-
+
+
))}
diff --git a/src/pages/Home/OOTD/styles.tsx b/src/pages/Home/OOTD/styles.tsx
index 39ef863d..011a4aa8 100644
--- a/src/pages/Home/OOTD/styles.tsx
+++ b/src/pages/Home/OOTD/styles.tsx
@@ -5,51 +5,20 @@ export const OOTDContainer = styled.div`
flex-direction: column;
width: 100%;
align-items: flex-start;
- margin-bottom: 3.25rem;
+ margin-top: 2.5rem;
+ margin-bottom: 4.25rem;
`;
export const OOTDLoading = styled.div`
margin-top: 200px;
`;
-// Tag
-
-export const TagMent = styled.div`
- margin-top: 1.313rem;
- margin-left: 1.25rem;
- margin-bottom: 0.6875rem;
- width: auto;
- height: 1rem;
-`;
-
-export const TagContainer = styled.div`
- display: flex;
- flex-direction: column;
- width: 100%;
- gap: 0.5rem;
- margin-left: 1.25rem;
- padding-right: 1.25rem;
-`;
-
-export const TagRow = styled.div`
- display: flex;
- gap: 0.5rem;
- overflow-x: auto;
- white-space: nowrap;
- scrollbar-width: none; /* Firefox에서 스크롤바 숨기기 */
- -ms-overflow-style: none; /* Internet Explorer에서 스크롤바 숨기기 */
-
- &::-webkit-scrollbar {
- display: none; /* Chrome, Safari, Opera에서 스크롤바 숨기기 */
- }
-`;
-
// Feed
export const FeedContainer = styled.div`
display: flex;
flex-direction: column;
width: calc(100% - 2.5rem);
- margin: 1.625rem 1.25rem 1.625rem 1.25rem;
+ margin: 0 1.25rem 1.625rem 1.25rem;
box-sizing: border-box;
`;
diff --git a/src/pages/Home/ReportTextarea.tsx b/src/pages/Home/ReportTextarea.tsx
deleted file mode 100644
index 5912b9bc..00000000
--- a/src/pages/Home/ReportTextarea.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import React, { useState, useRef, useCallback, useEffect } from 'react';
-import { InputLayout } from '../Post/styles.tsx';
-import BottomButton from '../../components/BottomButton/index.tsx';
-
-interface ReportTextareaProps {
- onCloseReportSheet: () => void;
- onOpenModal: () => void;
-}
-
-const ReportTextarea: React.FC
= React.memo(({ onCloseReportSheet, onOpenModal }) => {
- const [inputValue, setInputValue] = useState('');
- const textareaRef = useRef(null);
-
- useEffect(() => {
- if (textareaRef.current) {
- textareaRef.current.focus(); // 마운트 또는 업데이트 시 textarea에 포커스 유지
- }
- }, []);
-
- const handleInputChange = useCallback((e: React.ChangeEvent) => {
- setInputValue(e.target.value);
- }, []);
-
- const handleSubmit = useCallback(() => {
- onCloseReportSheet();
- onOpenModal();
- }, [onCloseReportSheet, onOpenModal]);
-
- return (
-
-
-
-
- );
-});
-
-export default ReportTextarea;
diff --git a/src/pages/Home/Tooltip/TooltipBubble/index.tsx b/src/pages/Home/Tooltip/TooltipBubble/index.tsx
deleted file mode 100644
index 64c4aa84..00000000
--- a/src/pages/Home/Tooltip/TooltipBubble/index.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { StyledText } from '../../../../components/Text/StyledText';
-import theme from '../../../../styles/theme';
-import { TooltipLayout, TooltipContentBox, TooltipArrow } from './styles';
-
-const TooltipBubble: React.FC<{ content: string; arrow: string; top?: number }> = ({ content, arrow, top }) => {
- return (
-
-
-
- {content}
-
-
-
-
- );
-};
-
-export default TooltipBubble;
diff --git a/src/pages/Home/Tooltip/TooltipBubble/styles.tsx b/src/pages/Home/Tooltip/TooltipBubble/styles.tsx
deleted file mode 100644
index 53689cc5..00000000
--- a/src/pages/Home/Tooltip/TooltipBubble/styles.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import styled from 'styled-components';
-
-export const TooltipWrapper = styled.div`
- background-color: rgb(0, 0, 0, 0.3);
- position: fixed;
- display: inline-block;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- z-index: 100;
-`;
-
-export const TooltipLayout = styled.div<{ $top?: number }>`
- background-color: ${({ theme }) => theme.colors.white};
- position: fixed;
- display: flex;
- left: 50%;
- ${({ $top }) => ($top ? `top: ${$top}px` : 'bottom: 13.5rem')};
- transform: translate(-50%, -50%);
- border-radius: 0.625rem;
- width: 13rem;
- height: 4rem;
- z-index: 200;
-`;
-
-export const TooltipArrow = styled.div<{ $arrow: string }>`
- position: absolute;
- bottom: -11px;
- left: ${({ $arrow }) => `${$arrow}`};
- transform: translateX(-50%);
- border-width: 15px 8px 0 8px;
- border-style: solid;
- border-color: ${({ theme }) => theme.colors.white} transparent transparent transparent;
-`;
-
-export const TooltipContentBox = styled.div`
- display: flex;
- flex: 1;
- justify-content: center;
- align-items: center;
- text-align: center;
-`;
diff --git a/src/pages/Home/Tooltip/index.tsx b/src/pages/Home/Tooltip/index.tsx
deleted file mode 100644
index 89bd092e..00000000
--- a/src/pages/Home/Tooltip/index.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-import { useEffect, useState } from 'react';
-import TooltipBubble from './TooltipBubble';
-
-import Cookies from 'js-cookie';
-import { TooltipWrapper } from './styles';
-
-interface TooltipDto {
- cardRef: React.MutableRefObject;
- ootdTooltipRef: React.MutableRefObject;
- activeIndex: number;
-}
-
-const Tooltip: React.FC = ({ cardRef, ootdTooltipRef, activeIndex }) => {
- const [isOpenMatchingTooltip, setIsOpenMatchingTooltip] = useState(false);
- const [isOpenOotdTooltip, setIsOpenOotdTooltip] = useState(false);
- const [matchingTooltipIndex, setMatchingTooltipIndex] = useState(0);
- const [ootdTooltipIndex, setOotdTooltipIndex] = useState(0);
- const [matchingTooltipBottom, setMatchingTooltipBottom] = useState(0);
-
- const onClickMatchingTooltip = () => {
- if (matchingTooltipIndex < 1) {
- setMatchingTooltipIndex((prev) => prev + 1);
- } else {
- setIsOpenMatchingTooltip(false);
- Cookies.set('hasSeenMatchingTooltip', 'true');
- }
- };
-
- const onClickOotdTooltip = () => {
- if (ootdTooltipIndex < 2) {
- setOotdTooltipIndex((prev) => prev + 1);
- } else {
- setIsOpenOotdTooltip(false);
- Cookies.set('hasSeenOotdTooltip', 'true');
- }
- };
-
- useEffect(() => {
- const seenMatching = Cookies.get('hasSeenMatchingTooltip');
- const seenOotd = Cookies.get('hasSeenOotdTooltip');
-
- // 매칭 탭에서 툴팁이 표시된 적이 없으면
- if (!seenMatching && activeIndex === 0) {
- const element = cardRef.current;
- if (element) {
- setIsOpenMatchingTooltip(true);
-
- setTimeout(() => {
- // 선택된 요소의 위치 계산
- const rect = element.getBoundingClientRect();
- const scrollTop = document.documentElement.scrollTop;
- const viewportHeight = window.innerHeight;
-
- const desiredPosition = viewportHeight - 73;
- const scrollToPosition = rect.bottom + scrollTop - desiredPosition;
-
- window.scrollTo({
- top: scrollToPosition,
- behavior: 'smooth',
- });
- }, 300); // 스와이퍼가 완료된 후 스크롤
-
- // 스크롤 된 뷰포트를 기준으로 다시 위치 계산
- setTimeout(() => {
- const newRect = element.getBoundingClientRect();
- const tooltipBottom = newRect.bottom - 225;
-
- // 툴팁 위치 설정
- setMatchingTooltipBottom(tooltipBottom);
- }, 500); // 스크롤이 완료된 후 위치 계산
- }
- } else {
- setIsOpenMatchingTooltip(false);
- }
-
- // ootd 탭에서 툴팁이 표시된 적이 없으면
- if (!seenOotd && activeIndex === 1) {
- if (ootdTooltipRef) {
- setIsOpenOotdTooltip(true);
-
- const element = ootdTooltipRef.current[1];
-
- if (element) {
- setTimeout(() => {
- // 선택된 요소의 위치 계산
- const rect = element.getBoundingClientRect();
- const scrollTop = document.documentElement.scrollTop;
- const viewportHeight = window.innerHeight;
-
- const desiredPosition = viewportHeight - 73;
- const scrollToPosition = rect.bottom + scrollTop - desiredPosition;
-
- window.scrollTo({
- top: scrollToPosition,
- behavior: 'smooth',
- });
- }, 100); // 페이지가 다 렌더링 된 후 스크롤
- }
- setIsOpenOotdTooltip(true);
- }
- } else {
- setIsOpenOotdTooltip(false);
- }
- }, [activeIndex, ootdTooltipRef]);
-
- return (
- <>
- {isOpenMatchingTooltip && (
-
- {matchingTooltipIndex === 0 ? (
-
- ) : null}
- {matchingTooltipIndex === 1 ? (
-
- ) : null}
-
- )}
- {isOpenOotdTooltip && (
-
- {ootdTooltipIndex === 0 ? (
-
- ) : null}
- {ootdTooltipIndex === 1 ? (
-
- ) : null}
- {ootdTooltipIndex === 2 ? (
-
- ) : null}
-
- )}
- >
- );
-};
-
-export default Tooltip;
diff --git a/src/pages/Home/Tooltip/styles.tsx b/src/pages/Home/Tooltip/styles.tsx
deleted file mode 100644
index e6597236..00000000
--- a/src/pages/Home/Tooltip/styles.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import styled from 'styled-components';
-
-export const TooltipWrapper = styled.div`
- background-color: rgb(0, 0, 0, 0.3);
- position: fixed;
- display: inline-block;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- z-index: 100;
-`;
diff --git a/src/pages/Home/dto.ts b/src/pages/Home/dto.ts
index 6704e2e3..1151716d 100644
--- a/src/pages/Home/dto.ts
+++ b/src/pages/Home/dto.ts
@@ -4,3 +4,9 @@ export interface ApiDto {
message: string;
result: any[];
}
+
+export interface MatchingInfoDto {
+ requesterId: number;
+ targetId: number;
+ targetName: string;
+}
diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx
index f003d9ae..3756b895 100644
--- a/src/pages/Home/index.tsx
+++ b/src/pages/Home/index.tsx
@@ -1,232 +1,16 @@
-import React, { useEffect } from 'react';
-import { useNavigate } from 'react-router-dom';
import { OODDFrame } from '../../components/Frame/Frame';
-import HomeTabBar from './HomeTabBar';
-import HomeTopBar from './HomeTopBar';
import NavBar from '../../components/NavBar';
+import HomeTopBar from './HomeTopBar';
+import OOTD from './OOTD/index.tsx';
import { HomeContainer } from './styles';
-import request, { BaseResponse } from '../../apis/core';
-
-import Modal from '../../components/Modal/index.tsx';
-import HeartBottomSheet from './BottomSheets/HeartBottomSheet.tsx';
-import {
- IsOpenRequestFailModalAtom,
- IsOpenRequestSuccessModalAtom,
- PostRequestAtom,
-} from '../../recoil/HeartBottomSheetAtom.ts';
-import { useRecoilState, useRecoilValue } from 'recoil';
-import { ModalProps } from '../../components/Modal/dto.ts';
-import {
- IsOpenBlockConfirmationModalAtom,
- IsOpenBlockFailModalAtom,
- IsOpenBlockSuccessModalAtom,
- PostBlockAtom,
-} from '../../recoil/BlockBottomSheetAtom.ts';
-import BlockConfirmationModal from './BottomSheets/BlockBottomSheet.tsx';
-import MeatballBottomSheet from './BottomSheets/MeatballBottomSheet.tsx';
-import ReportBottomSheet from './BottomSheets/ReportBottomSheet.tsx';
-import {
- IsOpenMeatballBottomSheetAtom,
- IsOpenReportBottomSheetAtom,
- IsOpenReportFailModalAtom,
- IsOpenReportSuccessModalAtom,
- PostReportAtom,
-} from '../../recoil/MeatballBottomSheetAtom.ts';
-import PostCommentBottomSheet from './BottomSheets/PostCommentBottomSheet.tsx';
-import {
- IsOpenPostCommentBottomSheetAtom,
- IsOpenPostCommentFailModalAtom,
- IsOpenPostCommentSuccessModalAtom,
-} from '../../recoil/PostCommentBottomSheetAtom.ts';
-
-interface UserResponseType {
- id: number;
- name: string;
- email: string;
- nickname: string | null;
- phoneNumber: string | null;
- profilePictureUrl: string;
- bio: string | null;
- joinedAt: string;
-}
-
-interface UserResponse extends BaseResponse {}
// Home 페이지입니다.
const Home: React.FC = () => {
- const navigate = useNavigate();
-
- // 모달과 바텀시트 상태 및 로직
- const [isOpenRequestSuccessModal, setIsOpenRequestSuccessModal] = useRecoilState(IsOpenRequestSuccessModalAtom);
- const [isOpenRequestFailModal, setIsOpenRequestFailModal] = useRecoilState(IsOpenRequestFailModalAtom);
- const postRequest = useRecoilValue(PostRequestAtom);
- const [isOpenBlockSuccessModal, setIsOpenBlockSuccessModal] = useRecoilState(IsOpenBlockSuccessModalAtom);
- const [isOpenBlockFailModal, setIsOpenBlockFailModal] = useRecoilState(IsOpenBlockFailModalAtom);
- const isOpenBlockConfirmationModal = useRecoilValue(IsOpenBlockConfirmationModalAtom);
- const postBlock = useRecoilValue(PostBlockAtom);
- const [, setIsOpenMeatballBottomSheet] = useRecoilState(IsOpenMeatballBottomSheetAtom);
- const [, setIsOpenReportBottomSheet] = useRecoilState(IsOpenReportBottomSheetAtom);
- const [isOpenReportSuccessModal, setIsOpenReportSuccessModal] = useRecoilState(IsOpenReportSuccessModalAtom);
- const [isOpenReportFailModal, setIsOpenReportFailModal] = useRecoilState(IsOpenReportFailModalAtom);
- const postReport = useRecoilValue(PostReportAtom);
- const [, setIsOpenPostCommentBottomSheet] = useRecoilState(IsOpenPostCommentBottomSheetAtom);
- const [isOpenPostCommentSuccessModal, setIsOpenPostCommentSuccessModal] = useRecoilState(
- IsOpenPostCommentSuccessModalAtom,
- );
- const [isOpenPostCommentFailModal, setIsOpenPostCommentFailModal] = useRecoilState(IsOpenPostCommentFailModalAtom);
-
- // 로그인 여부에 따라 navigate
- useEffect(() => {
- const checkAuth = async () => {
- const userId = localStorage.getItem('id');
- const token = localStorage.getItem('jwt_token');
-
- if (!userId || !token) {
- navigate('/login');
- return;
- }
-
- try {
- const response = await request.get(`/users/${userId}`);
- if (!response || !response.result.id) {
- console.log(response);
- navigate('/login');
- }
- } catch (error) {
- console.error('Failed to authenticate user:', error);
- navigate('/login');
- }
- };
-
- checkAuth();
- }, [navigate]);
-
- // feed
- // x 버튼 클릭 시
- const blockSuccessModalProps: ModalProps = {
- onClose: () => {
- setIsOpenBlockSuccessModal(false);
- },
- content: `${postBlock?.friendName} 님을 차단했어요`,
- };
-
- const blockFailModalProps: ModalProps = {
- onClose: () => {
- setIsOpenBlockFailModal(false);
- },
- content: `차단에 실패했어요\n잠시 뒤 다시 시도해 보세요`,
- };
-
- // 하트 버튼 클릭 시
- const requestSuccessModalProps: ModalProps = {
- onClose: () => {
- setIsOpenRequestSuccessModal(false);
- },
- content: `${postRequest?.targetName} 님에게 대표 OOTD와\n한줄 메시지를 보냈어요!`,
- };
-
- const requestFailModalProps: ModalProps = {
- onClose: () => {
- setIsOpenRequestFailModal(false);
- },
- content: `요청에 실패했어요\n잠시 뒤 다시 시도해 보세요`,
- };
-
- // 코멘트 남기기 버튼
- const postCommentSuccessModalProps: ModalProps = {
- onClose: () => {
- setIsOpenPostCommentSuccessModal(false);
- },
- content: '코멘트가 전달되었어요',
- };
-
- const postCommentFailModalProps: ModalProps = {
- onClose: () => {
- setIsOpenPostCommentFailModal(false);
- },
- content: '일시적인 오류입니다다',
- };
-
- // 신고하기 메뉴
- const reportSuccessModalProps: ModalProps = {
- onClose: () => {
- setIsOpenReportSuccessModal(false);
- },
- content: `${postReport?.userName} 님의\nOOTD를 신고했어요`,
- };
-
- const reportFailModalProps: ModalProps = {
- onClose: () => {
- setIsOpenReportFailModal(false);
- },
- content: `신고에 실패했어요\n잠시 뒤 다시 시도해 보세요`,
- };
-
- // 코멘트 남기기 버튼 클릭 시
- // const commentProps: CommentProps = {
- // content: `${userName}님의 게시물에 대한 코멘트를 남겨주세요.\n코멘트는 ${userName}님에게만 전달됩니다.`,
- // sendComment: (message: string) => {
- // const postNewRequest = async () => {
- // if (postRequest) {
- // const response = await request.post('/user-relationships', {
- // requesterId: postRequest.requesterId,
- // targetId: postRequest.targetId,
- // message: message,
- // });
-
- // if (response.isSuccess) {
- // setIsOpenHeartBottomSheet(false);
- // setTimeout(() => {
- // setIsOpenRequestSuccessModal(true);
- // }, 100);
- // } else {
- // setIsOpenRequestFailModal(true);
- // }
- // } else {
- // alert('잘못된 요청입니다.');
- // }
- // };
-
- // postNewRequest();
- // },
- // };
-
- // const commentSheetProps: BottomSheetProps = {
- // isOpenBottomSheet: isCommentModalOpen,
- // isHandlerVisible: true,
- // Component: Comment,
- // componentProps: commentProps,
- // onCloseBottomSheet: () => {
- // setIsCommentModalOpen(false);
- // },
- // };
-
return (
- {isOpenBlockConfirmationModal && }
- {isOpenBlockSuccessModal && }
- {isOpenBlockFailModal && }
-
-
- {isOpenRequestSuccessModal && }
- {isOpenRequestFailModal && }
-
-
- {isOpenPostCommentSuccessModal && }
- {isOpenPostCommentFailModal && }
-
-
-
- {isOpenReportSuccessModal && }
- {isOpenReportFailModal && }
-
- setIsOpenMeatballBottomSheet(true)}
- onOpenReportSheet={() => setIsOpenReportBottomSheet(true)}
- onOpenCommentModal={() => setIsOpenPostCommentBottomSheet(true)}
- />
+
diff --git a/src/pages/Home/styles.tsx b/src/pages/Home/styles.tsx
index 88819d07..9f7bb5e2 100644
--- a/src/pages/Home/styles.tsx
+++ b/src/pages/Home/styles.tsx
@@ -10,74 +10,32 @@ export const HomeContainer = styled.div`
// HomeTopBar
-export const HomeTopBarContainer = styled.div`
+export const HomeTopBarContainer = styled.header`
width: 100%;
- max-width: 32rem;
- height: 2.75rem;
+ padding: 0.5rem 1.25rem;
display: flex;
justify-content: space-between;
background-color: white;
- z-index: 10;
+ z-index: 20;
align-items: center;
position: fixed;
+ ${({ theme }) => theme.visibleOnMobileTablet};
`;
export const HomeLogo = styled.img`
- width: 6.6875rem;
- height: 1.6875rem;
- margin-left: 1.25rem;
- cursor: pointer;
- overflow: hidden;
-
- img {
- width: 100%;
- height: 100%;
- }
+ padding: 0.0938rem 0;
`;
-// HomeTabBar
-
-export const TabLayout = styled.div`
+export const ButtonContainer = styled.div`
display: flex;
- flex-direction: column;
- width: 100%;
- height: auto;
-`;
-
-export const HomeTabBarLayout = styled.div`
- position: fixed;
- width: 100%;
- max-width: 32rem;
- background-color: white;
- z-index: 10;
- top: 2.75rem;
- height: 2.5rem;
- border-bottom: 0.063rem solid ${({ theme }) => theme.colors.gray2};
+ gap: 1rem;
`;
-export const HomeTabBarList = styled.ul`
- height: 2.5rem;
+export const Button = styled.button`
+ width: 1.125rem;
+ height: 1.125rem;
display: flex;
- gap: 1.25rem;
- justify-content: space-between;
- margin: 0 1.25rem;
-`;
-
-export const HomeTabBarWrapper = styled.li<{ $isSelected: boolean; $isPointer: boolean }>`
- margin-top: 1rem;
- border-bottom: 0.125rem solid ${({ $isSelected }) => ($isSelected ? 'black' : 'transparent')};
- text-align: center;
- flex-grow: 1;
- flex-basis: 0;
- cursor: ${({ $isPointer }) => ($isPointer ? 'pointer' : '')};
-`;
-
-export const Tabs = styled.div`
- margin-top: 5.25rem;
- z-index: 0;
- flex-grow: 1;
- height: 100%;
- .swiper-container {
- height: 100%;
- }
+ justify-content: center;
+ align-items: center;
+ border-radius: 0.03rem;
`;
diff --git a/src/pages/Login/components/Google/GoogleCallback.tsx b/src/pages/Login/components/Google/GoogleCallback.tsx
deleted file mode 100644
index 48962977..00000000
--- a/src/pages/Login/components/Google/GoogleCallback.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import React, { useEffect } from 'react';
-import { useNavigate } from 'react-router-dom';
-import axios from 'axios';
-import Loading from '../../../../components/Loading';
-
-const GoogleCallback: React.FC = () => {
- const navigate = useNavigate();
- useEffect(() => {
- const query = new URLSearchParams(window.location.search);
- const code = query.get('code');
- console.log(code);
-
- if (code) {
- // 인증 코드를 쿼리스트링으로 백엔드 서버에 전송
- axios
- .get(`https://api-dev.oodd.today/auth/login/google?code=${code}`)
- .then((response) => {
- const statusCode = response.status;
- console.log(JSON.stringify(response.data));
- const token = response.data.accessToken; // 응답 중, 성공 여부와 user 정보 추출
- localStorage.setItem('id', '22'); // 로그인 성공을 하면... isSuccess랑 토큰이 오니까... 내 정보 조회를 먼저 해서 id를 가져 와서 로컬에 저장하기~
- localStorage.removeItem('jwt_token');
- localStorage.setItem('jwt_token', token);
- console.log(token);
- console.log(localStorage.getItem('jwt_token'));
- if (statusCode === 200) {
- // Postman에서 api 호출해 보고 응답을 보고 적어야 함
- // user.id를 서버로 보내 해당 유저의 nickname 유무에 따른 리디렉션
- navigate('/');
- } else {
- console.error('로그인 실패:', response.data);
- alert('구글 계정의 정보를 불러오지 못했습니다.');
- navigate('/login');
- // 로그인 실패 시 처리 (예: 오류 페이지로 리디렉션)
- }
- })
- .catch((error) => {
- console.error('서버 요청 실패:', error);
- });
- } else {
- // 인증 코드가 없는 경우 처리
- console.error('인증 코드가 없습니다.');
- }
- }, [navigate]);
- return ;
-};
-
-export default GoogleCallback;
diff --git a/src/pages/Login/components/Google/index.tsx b/src/pages/Login/components/Google/index.tsx
deleted file mode 100644
index 6a272fbc..00000000
--- a/src/pages/Login/components/Google/index.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import React from 'react';
-import { StyledText } from '../../../../components/Text/StyledText';
-import theme from '../../../../styles/theme';
-import google from '../../../../assets/Login/google.png';
-import { SocialLogin, LogoImgWrapper, LogoImage, TextWrapper } from '../style';
-
-const Google: React.FC = () => {
- const clientId = import.meta.env.VITE_GOOGLE_CLIENT_ID;
- const redirectUri = encodeURIComponent(
- import.meta.env.VITE_DEV_DOMAIN
- ? import.meta.env.VITE_DEV_DOMAIN + '/auth/google/callback'
- : 'http://localhost:3000/auth/google/callback',
- );
-
- const handleLogin = () => {
- window.location.href = `https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}&scope=profile email`;
- };
-
- return (
-
-
-
-
-
-
- 구글로 시작하기
-
-
-
- );
-};
-
-export default Google;
diff --git a/src/pages/Login/components/Kakao/KakaoCallback.tsx b/src/pages/Login/components/Kakao/KakaoCallback.tsx
index ef51ff2b..8c3df685 100644
--- a/src/pages/Login/components/Kakao/KakaoCallback.tsx
+++ b/src/pages/Login/components/Kakao/KakaoCallback.tsx
@@ -1,68 +1,58 @@
-//카카오 인증 완료 후 인증 코드는 승인된 리디렉트 URL로
-
-import React, { useEffect } from 'react';
+import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
-import axios from 'axios';
-import { UserInfoDto } from '../../../ProfileViewer/ResponseDto/UserInfoDto';
-import request from '../../../../apis/core';
+
import Loading from '../../../../components/Loading';
+import Modal from '../../../../components/Modal';
+
+import { handleError } from '../../../../apis/util/handleError';
const KakaoCallback: React.FC = () => {
const navigate = useNavigate();
+ const apiBaseUrl = import.meta.env.VITE_NEW_API_URL;
+
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [modalMessage, setModalMessage] = useState('');
useEffect(() => {
- const code = new URL(window.location.href).searchParams.get('code'); // URL에서 인증 코드 추출
- console.log(code); // 인증 코드 출력
+ const handleKakaoLogin = async () => {
+ try {
+ // URL에서 인증 코드 추출
+ const code = new URL(window.location.href).searchParams.get('code');
+ console.log('인증 코드:', code);
+
+ if (!code) {
+ throw new Error('인증 코드가 없습니다.');
+ }
+
+ // 리다이렉트 URL 설정 및 서버 URL 생성
+ const redirectUrl = encodeURIComponent('http://localhost:3000/login/complete');
+ const serverUrl = `${apiBaseUrl}/auth/login/kakao?redirectUrl=${redirectUrl}`;
+
+ // 서버로 리다이렉션
+ window.location.href = serverUrl;
+ } catch (error) {
+ // 에러 처리
+ console.error('카카오 로그인 중 오류 발생:', error);
+ const errorMessage = handleError(error);
+ setModalMessage(`카카오 ${errorMessage}`);
+ setIsModalOpen(true);
+ }
+ };
- if (code) {
- // 인증 코드를 쿼리스트링으로 백엔드 서버에 전송
- axios
- .get(`https://api-dev.oodd.today/auth/login/kakao?code=${code}`)
- .then((response) => {
- const statusCode = response.status; // 200 OK
- console.log(JSON.stringify(response.data));
- if (statusCode === 200) {
- // 추후 Postman에서 api 호출해 보고 응답을 보고 적어야 함
- // userid를 서버로 보내 해당 유저의 nickname 유무에 따른 리디렉션
- const token = response.data.accessToken;
+ handleKakaoLogin();
+ }, [navigate, apiBaseUrl]);
- localStorage.setItem('id', response.data.id); // 응답으로 id가 오지 않기 때문에 여기서 설정해야 함 수정 필요?
- localStorage.removeItem('jwt_token');
- localStorage.setItem('jwt_token', token);
- const userid = localStorage.getItem('id');
+ const handleModalClose = () => {
+ setIsModalOpen(false);
+ navigate('/login'); // 모달 닫힌 후 로그인 페이지로 이동
+ };
- request
- .get(`/users/${userid}`)
- .then((response) => {
- console.log(response);
- if (response.result.nickname) {
- navigate('/');
- } else {
- navigate(`/signup`);
- }
- })
- .catch((error) => {
- // API 요청 실패 시 처리
- console.error('API 요청 실패:', error);
- alert('사용자 정보를 불러오지 못했습니다.');
- navigate('/login'); // 실패 시 로그인 페이지로 리디렉션
- });
- } else {
- console.error('로그인 실패:', response.data);
- alert('카카오 계정의 정보를 불러오지 못했습니다.');
- navigate('/login');
- // 로그인 실패 시 처리 (예: 오류 페이지로 리디렉션)
- }
- })
- .catch((error) => {
- console.error('서버 요청 실패:', error);
- });
- } else {
- // 인증 코드가 없는 경우 처리
- console.error('인증 코드가 없습니다.');
- }
- }, [navigate]);
- return ;
+ return (
+ <>
+
+ {isModalOpen && }
+ >
+ );
};
export default KakaoCallback;
diff --git a/src/pages/Login/components/Kakao/KakaoLoginDto.ts b/src/pages/Login/components/Kakao/KakaoLoginDto.ts
deleted file mode 100644
index 74f6a8d5..00000000
--- a/src/pages/Login/components/Kakao/KakaoLoginDto.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export interface KakaoLoginDto {
- status: number;
- data: {
- message: string;
- accessToken: string;
- };
-}
diff --git a/src/pages/Login/components/Kakao/index.tsx b/src/pages/Login/components/Kakao/index.tsx
index cf254d16..5a769018 100644
--- a/src/pages/Login/components/Kakao/index.tsx
+++ b/src/pages/Login/components/Kakao/index.tsx
@@ -1,11 +1,12 @@
import React from 'react';
-import { StyledText } from '../../../../components/Text/StyledText';
+
import theme from '../../../../styles/theme';
-import kakao from '../../../../assets/Login/kakao.png';
import { SocialLogin, LogoImgWrapper, LogoImage, TextWrapper } from '../style';
+import { StyledText } from '../../../../components/Text/StyledText';
+
+import kakao from '../../../../assets/default/snsIcon/kakao.svg';
const Kakao: React.FC = () => {
- // 환경 변수에서 값을 읽어옴
const REST_API_KEY = import.meta.env.VITE_KAKAO_REST_API_KEY;
const REDIRECT_URI = encodeURIComponent(
import.meta.env.VITE_DEV_DOMAIN
@@ -18,13 +19,13 @@ const Kakao: React.FC = () => {
};
return (
-
-
+
+
-
- 카카오로 시작하기
+
+ Kakao로 계속하기
diff --git a/src/pages/Login/components/LoginComplete.tsx b/src/pages/Login/components/LoginComplete.tsx
new file mode 100644
index 00000000..b7856395
--- /dev/null
+++ b/src/pages/Login/components/LoginComplete.tsx
@@ -0,0 +1,76 @@
+import React, { useEffect, useState } from 'react';
+import { useLocation, useNavigate } from 'react-router-dom';
+
+import Loading from '../../../components/Loading';
+import Modal from '../../../components/Modal';
+
+import { getUserInfoByJwtApi } from '../../../apis/auth';
+import { handleError } from '../../../apis/util/handleError';
+import { postTermsAgreementApi } from '../../../apis/user';
+
+const LoginComplete: React.FC = () => {
+ const location = useLocation();
+ const navigate = useNavigate();
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [modalMessage, setModalMessage] = useState('');
+
+ useEffect(() => {
+ // URLSearchParams를 사용해 쿼리 문자열에서 token 추출
+ const queryParams = new URLSearchParams(location.search);
+ const token = queryParams.get('token');
+
+ if (token) {
+ localStorage.setItem('new_jwt_token', token);
+ console.log('Extracted Token:', token);
+
+ // JWT로 사용자 정보 조회하는 함수
+ const getUserInfoByJwt = async () => {
+ try {
+ const response = await getUserInfoByJwtApi();
+ console.log(response);
+
+ const { nickname, name, userId } = response.data;
+ localStorage.setItem('my_id', `${userId}`);
+
+ if (nickname && name) {
+ if (nickname && name) {
+ const isAgreed = await checkTermsAgreement(userId);
+ navigate(isAgreed ? '/' : '/terms-agreement');
+ }
+ } else {
+ navigate('/signup');
+ }
+ } catch (error) {
+ console.error('사용자 정보 조회 실패:', error);
+ const errorMessage = handleError(error, 'user');
+ setModalMessage(errorMessage);
+ setIsModalOpen(true);
+ }
+ };
+ getUserInfoByJwt();
+ }
+ }, [location]);
+
+ const checkTermsAgreement = async (userId: string): Promise => {
+ try {
+ await postTermsAgreementApi(userId);
+ return true; // 동의 완료
+ } catch {
+ return false; // 동의 필요
+ }
+ };
+
+ const handleModalClose = () => {
+ setIsModalOpen(false);
+ navigate('/login'); // 모달 닫힌 후 로그인 페이지로 이동
+ };
+
+ return (
+ <>
+
+ {isModalOpen && }
+ >
+ );
+};
+
+export default LoginComplete;
diff --git a/src/pages/Login/components/Naver/NaverCallback.tsx b/src/pages/Login/components/Naver/NaverCallback.tsx
index 8f4fb520..d2156d5d 100644
--- a/src/pages/Login/components/Naver/NaverCallback.tsx
+++ b/src/pages/Login/components/Naver/NaverCallback.tsx
@@ -1,69 +1,54 @@
-import React, { useEffect } from 'react';
+import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
-import axios from 'axios';
-import request from '../../../../apis/core';
-import { UserInfoDto } from '../../../ProfileViewer/ResponseDto/UserInfoDto';
+
import Loading from '../../../../components/Loading';
+import Modal from '../../../../components/Modal';
+
+import { handleError } from '../../../../apis/util/handleError';
const NaverCallback: React.FC = () => {
const navigate = useNavigate();
+ const apiBaseUrl = import.meta.env.VITE_NEW_API_URL;
- useEffect(() => {
- const query = new URLSearchParams(window.location.search);
- const code = query.get('code');
- console.log(code);
-
- if (code) {
- // 인증 코드를 쿼리스트링으로 백엔드 서버에 전송
- axios
- .get(`https://api-dev.oodd.today/auth/login/naver?code=${code}&state=STATE_TOKEN`)
- .then((response) => {
- const statusCode = response.status; // 200 OK
- console.log(JSON.stringify(response.data));
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [modalMessage, setModalMessage] = useState('');
- if (statusCode === 200) {
- // 추후 Postman에서 api 호출해 보고 응답을 보고 적어야 함
- // userid를 서버로 보내 해당 유저의 nickname 유무에 따른 리디렉션
- const token = response.data.accessToken;
-
- localStorage.setItem('id', response.data.id);
- localStorage.removeItem('jwt_token');
- localStorage.setItem('jwt_token', token);
- const userid = localStorage.getItem('id');
-
- request
- .get(`/users/${userid}`)
- .then((response) => {
- console.log(response);
- if (response.result.nickname) {
- navigate('/');
- } else {
- navigate(`/signup`);
- }
- })
- .catch((error) => {
- // API 요청 실패 시 처리
- console.error('API 요청 실패:', error);
- alert('사용자 정보를 불러오지 못했습니다.');
- navigate('/login'); // 실패 시 로그인 페이지로 리디렉션
- });
- } else {
- console.error('로그인 실패:', response.data);
- alert('네이버 계정의 정보를 불러오지 못했습니다.');
- navigate('/login');
- // 로그인 실패 시 처리 (예: 오류 페이지로 리디렉션)
- }
- })
- .catch((error) => {
- console.error('서버 요청 실패:', error);
- });
- } else {
- // 인증 코드가 없는 경우 처리
- console.error('인증 코드가 없습니다.');
- }
- }, [navigate]);
-
- return ;
+ useEffect(() => {
+ const handleNaverLogin = async () => {
+ try {
+ // URL에서 인증 코드 추출
+ const code = new URL(window.location.href).searchParams.get('code');
+ console.log('인증 코드:', code);
+
+ if (!code) {
+ throw new Error('인증 코드가 없습니다.');
+ }
+
+ // 리다이렉트 URL 설정 및 서버 URL 생성 해 서버로 리다이렉션
+ const redirectUrl = encodeURIComponent('http://localhost:3000/login/complete');
+ const serverUrl = `${apiBaseUrl}/auth/login/naver?redirectUrl=${redirectUrl}`;
+ window.location.href = serverUrl;
+ } catch (error) {
+ console.error('네이버 로그인 중 오류 발생:', error);
+ const errorMessage = handleError(error);
+ setModalMessage(`네이버 ${errorMessage}`);
+ setIsModalOpen(true);
+ }
+ };
+
+ handleNaverLogin();
+ }, [navigate, apiBaseUrl]);
+
+ const handleModalClose = () => {
+ setIsModalOpen(false);
+ navigate('/login'); // 모달 닫힌 후 로그인 페이지로 이동
+ };
+ return (
+ <>
+
+ {isModalOpen && }
+ >
+ );
};
export default NaverCallback;
diff --git a/src/pages/Login/components/Naver/index.tsx b/src/pages/Login/components/Naver/index.tsx
index 2e3417fa..7928c545 100644
--- a/src/pages/Login/components/Naver/index.tsx
+++ b/src/pages/Login/components/Naver/index.tsx
@@ -1,8 +1,10 @@
import React from 'react';
+
import { StyledText } from '../../../../components/Text/StyledText';
import theme from '../../../../styles/theme';
import { SocialLogin, TextWrapper, LogoImgWrapper, LogoImage } from '../style';
-import naver from '../../../../assets/Login/naver.png';
+
+import naver from '../../../../assets/default/snsIcon/naver.svg';
const Naver: React.FC = () => {
const clientId = import.meta.env.VITE_NAVER_CLIENT_ID; // 네이버 개발자 센터에서 받은 클라이언트 ID
@@ -11,20 +13,19 @@ const Naver: React.FC = () => {
? import.meta.env.VITE_DEV_DOMAIN + '/auth/naver/callback'
: 'http://localhost:3000/auth/naver/callback',
);
- //const state = 'random_state_string'; // CSRF 공격 방지를 위한 랜덤 문자열
const handleLogin = () => {
window.location.href = `https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}&state=STATE_TOKEN`;
};
return (
-
-
+
+
-
- 네이버로 시작하기
+
+ 네이버로 계속하기
diff --git a/src/pages/Login/components/style.tsx b/src/pages/Login/components/style.tsx
index a21e2b3d..85611457 100644
--- a/src/pages/Login/components/style.tsx
+++ b/src/pages/Login/components/style.tsx
@@ -3,22 +3,22 @@ import styled from 'styled-components';
export const SocialLogin = styled.button<{ $bgColor: string; $border?: boolean }>`
display: flex;
align-items: center;
- width: 100%;
- max-width: 21.375rem; /* 342px / 16 */
- height: 3.5rem; /* 56px / 16 */
+ width: calc(100% - 3.5rem);
+ max-width: 40rem;
+ height: 3.5rem;
background-color: ${({ $bgColor }) => $bgColor};
- border-radius: 0.1875rem; /* 3px / 16 */
+ border-radius: 0.5rem;
border: ${({ $border }) => ($border ? '1px solid #000' : 'none')};
cursor: pointer;
- margin-bottom: 0.75rem;
+ margin-bottom: 0.5rem;
box-sizing: border-box;
`;
-export const LogoImgWrapper = styled.div<{ $logowidth: string; $logoheight: string }>`
+export const LogoImgWrapper = styled.figure`
display: flex;
align-items: center;
- width: ${({ $logowidth }) => $logowidth};
- height: ${({ $logoheight }) => $logoheight};
+ width: 2.25rem;
+ height: 2.25rem;
margin-left: 1rem;
`;
@@ -27,7 +27,7 @@ export const LogoImage = styled.img`
max-height: 100%;
`;
-export const TextWrapper = styled.div<{ $left?: string }>`
+export const TextWrapper = styled.section<{ $left?: string }>`
display: flex;
width: 12.5rem;
padding-left: ${({ $left }) => $left || '1.2rem'};
diff --git a/src/pages/Login/index.tsx b/src/pages/Login/index.tsx
index 1af714f0..d30c211c 100644
--- a/src/pages/Login/index.tsx
+++ b/src/pages/Login/index.tsx
@@ -1,36 +1,21 @@
import React from 'react';
-import { StyledText } from '../../components/Text/StyledText';
-import theme from '../../styles/theme';
+
import { OODDFrame } from '../../components/Frame/Frame';
-import { LogoWrapper, LoginContainer, WelcomeWrapper, Service, LogoImg } from './styles';
import Naver from './components/Naver';
import Kakao from './components/Kakao';
-import Google from './components/Google';
-import OODDlogo from '../../../src/assets/OODDlogo.svg';
+
+import { LoginContainer, StyledWelcomeWrapper } from './styles';
+import theme from '../../styles/theme';
const Login: React.FC = () => {
return (
-
-
-
-
-
- 반가워요!
-
-
- 계정을 선택해주세요
-
-
-
+
+ {'반가워요! \n계정을 선택해주세요.'}
+
-
-
-
- 서비스 약관 확인{' >'}
-
-
+
);
diff --git a/src/pages/Login/styles.tsx b/src/pages/Login/styles.tsx
index 0a6d76b5..544dc3ac 100644
--- a/src/pages/Login/styles.tsx
+++ b/src/pages/Login/styles.tsx
@@ -1,42 +1,21 @@
import styled from 'styled-components';
+import { StyledText } from '../../components/Text/StyledText';
-export const LoginContainer = styled.div`
+export const LoginContainer = styled.main`
display: flex;
flex-direction: column;
+ justify-content: center;
align-items: center;
width: 100%;
- max-width: 32rem; /* 최대 너비 512px */
- height: auto;
+ height: 100%;
margin: 0 auto; /* 중앙 정렬 */
- //box-shadow: 0 0 0.625rem rgba(0, 0, 0, 0.1); /* 경계 구분용*/
`;
-export const LogoWrapper = styled.div`
- display: flex;
- justify-content: center; /* 수평 중앙 정렬 */
- width: 100%;
- max-width: 7.25rem; /* 116px / 16 */
- margin-top: 10.8rem; /* 195px */
-`;
-
-export const LogoImg = styled.img`
- display: flex;
-`;
-
-export const WelcomeWrapper = styled.div`
+export const StyledWelcomeWrapper = styled(StyledText)`
display: flex;
flex-direction: column;
- max-width: 11.75rem;
width: 100%;
- height: 4rem; /* 64px / 16 */
+ height: 5rem;
text-align: center;
- margin: 1.5637rem 0 2.25rem 0; /* 36px / 16 */
-`;
-
-export const Service = styled.button`
- display: flex;
- border: none;
- width: fit-content; /* 버튼 너비가 내용에 맞도록 설정 */
- padding: 0 1rem; /* 16px / 16 */
- margin: 0.75rem 0 16rem 0;
+ margin-bottom: 2rem;
`;
diff --git a/src/pages/Mypage/ButtonSecondary/index.tsx b/src/pages/MyPage/ButtonSecondary/index.tsx
similarity index 93%
rename from src/pages/Mypage/ButtonSecondary/index.tsx
rename to src/pages/MyPage/ButtonSecondary/index.tsx
index 7abbf4e2..8682b4b3 100644
--- a/src/pages/Mypage/ButtonSecondary/index.tsx
+++ b/src/pages/MyPage/ButtonSecondary/index.tsx
@@ -13,7 +13,7 @@ const ButtonSecondary: React.FC = () => {
return (
diff --git a/src/pages/Mypage/ButtonSecondary/styles.tsx b/src/pages/MyPage/ButtonSecondary/styles.tsx
similarity index 60%
rename from src/pages/Mypage/ButtonSecondary/styles.tsx
rename to src/pages/MyPage/ButtonSecondary/styles.tsx
index 68334d5f..d4401860 100644
--- a/src/pages/Mypage/ButtonSecondary/styles.tsx
+++ b/src/pages/MyPage/ButtonSecondary/styles.tsx
@@ -1,14 +1,16 @@
import styled from 'styled-components';
export const Button = styled.button`
- width: calc(100% - 30px); /* 양옆에 30px씩 공간을 확보 */
+ width: 100%;
padding: 6px;
margin: 1.25rem auto;
-
- border: 1px solid #000;
- border-radius: 10px;
height: 3.1rem; /* 44px */
text-align: center;
+ color: #ff2389;
cursor: pointer;
box-sizing: border-box;
+ border: 1px solid;
+ border-radius: 10px;
+ border-color: #ff2389;
+ padding: 10px; /* 텍스트가 보더와 겹치지 않게 패딩 설정 */
`;
diff --git a/src/pages/Mypage/dto.tsx b/src/pages/MyPage/dto.tsx
similarity index 96%
rename from src/pages/Mypage/dto.tsx
rename to src/pages/MyPage/dto.tsx
index 23a1c3c8..30e058b1 100644
--- a/src/pages/Mypage/dto.tsx
+++ b/src/pages/MyPage/dto.tsx
@@ -5,7 +5,7 @@ export interface PostData {
comments: number;
}
-// src/pages/Mypage/dto.ts
+// src/pages/MyPage/dto.ts
export interface UserResponse {
id: number;
diff --git a/src/pages/MyPage/index.tsx b/src/pages/MyPage/index.tsx
new file mode 100644
index 00000000..b296fd79
--- /dev/null
+++ b/src/pages/MyPage/index.tsx
@@ -0,0 +1,166 @@
+import React, { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import {
+ ProfileContainer,
+ Header,
+ StatsContainer,
+ Stat,
+ StatNumber,
+ StatLabel,
+ PostsContainer,
+ AddButton,
+} from './styles';
+import { OODDFrame } from '../../components/Frame/Frame';
+import NavbarProfile from '../../components/NavbarProfile';
+import NavBar from '../../components/NavBar';
+import ButtonSecondary from './ButtonSecondary';
+import PostItem from '../../components/PostItem';
+import imageBasic from '../../assets/default/defaultProfile.svg';
+import Loading from '../../components/Loading';
+import BottomSheet from '../../components/BottomSheet';
+import { BottomSheetProps } from '../../components/BottomSheet/dto';
+import BottomSheetMenu from '../../components/BottomSheetMenu';
+import { BottomSheetMenuProps } from '../../components/BottomSheetMenu/dto';
+import button_plus from '../../assets/default/plus.svg';
+import insta from '../../assets/default/insta.svg';
+import photo from '../../assets/default/photo.svg';
+import UserProfile from '../../components/UserProfile';
+
+import { getUserPostListApi } from '../../apis/post';
+import { UserPostSummary } from '../../apis/post/dto';
+
+const MyPage: React.FC = () => {
+ const [isLoading, setIsLoading] = useState(true);
+ const [isBottomSheetOpen, setIsBottomSheetOpen] = useState(false);
+ const [posts, setPosts] = useState([]);
+ const [totalStats, setTotalStats] = useState<{
+ totalPostsCount: number;
+ totalPostCommentsCount: number;
+ totalPostLikesCount: number;
+ }>();
+ const navigate = useNavigate();
+
+ const bottomSheetMenuProps: BottomSheetMenuProps = {
+ items: [
+ {
+ text: '인스타 피드 가져오기',
+ action: () => {
+ setIsBottomSheetOpen(false);
+ navigate('/insta-connect');
+ },
+ icon: insta,
+ },
+ {
+ text: '사진 올리기',
+ action: () => {
+ setIsBottomSheetOpen(false);
+ navigate('/image-select');
+ },
+ icon: photo,
+ },
+ ],
+ marginBottom: '50px',
+ };
+
+ const bottomSheetProps: BottomSheetProps = {
+ isOpenBottomSheet: isBottomSheetOpen,
+ isHandlerVisible: true,
+ Component: BottomSheetMenu,
+ componentProps: bottomSheetMenuProps,
+ onCloseBottomSheet: () => {
+ setIsBottomSheetOpen(false);
+ },
+ };
+
+ const handleOpenSheet = () => {
+ setIsBottomSheetOpen(true);
+ };
+
+ //게시물 리스트 조회 api - 콘솔 삭제 예정!
+ const fetchPostList = async () => {
+ try {
+ const storedUserId = localStorage.getItem('my_id'); // my_id로 변경되었음
+ if (!storedUserId) {
+ console.error('User ID not found in localStorage');
+ return;
+ }
+
+ console.log('Fetching posts for user ID:', storedUserId); // 디버깅: User ID 확인
+
+ // API 호출
+ const response = await getUserPostListApi(1, 10, Number(storedUserId));
+ console.log('API Response:', response); // 디버깅: API 응답 확인
+
+ const { post, totalPostsCount, totalPostCommentsCount, totalPostLikesCount, meta } = response.data;
+
+ console.log('Post List:', post); // 디버깅: 게시물 리스트 확인
+ console.log('Pagination Meta:', meta); // 디버깅: 페이지네이션 정보 확인
+
+ // 상태 업데이트
+ setPosts(post);
+ setTotalStats({ totalPostsCount, totalPostCommentsCount: totalPostCommentsCount ?? 0, totalPostLikesCount });
+
+ if (totalPostsCount === 0) {
+ console.log('No posts available for the user.');
+ }
+ } catch (error) {
+ console.error('Error fetching post list:', error); // 디버깅: 에러 확인
+ } finally {
+ setIsLoading(false);
+ console.log('Loading completed.'); // 디버깅: 로딩 완료 확인
+ }
+ };
+ useEffect(() => {
+ fetchPostList();
+ }, []);
+
+ if (isLoading) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ OOTD
+ {totalStats?.totalPostsCount}
+
+
+ 코멘트
+ {totalStats?.totalPostCommentsCount}
+
+
+ 좋아요
+ {totalStats?.totalPostLikesCount}
+
+
+
+ {posts.length > 0 ? (
+ posts
+ .sort((a, b) => {
+ if (b.isRepresentative && !a.isRepresentative) return 1;
+ if (a.isRepresentative && !b.isRepresentative) return -1;
+ return 0;
+ })
+ .map((post) => )
+ ) : (
+ 게시물이 없습니다.
+ )}
+
+
+
+
+ );
+};
+
+export default MyPage;
diff --git a/src/pages/MyPage/styles.tsx b/src/pages/MyPage/styles.tsx
new file mode 100644
index 00000000..2c4a8eb6
--- /dev/null
+++ b/src/pages/MyPage/styles.tsx
@@ -0,0 +1,76 @@
+import styled from 'styled-components';
+
+export const ProfileContainer = styled.div`
+ width: 100%;
+ flex-grow: 1;
+ margin: 0 auto; /* 중앙 정렬 */
+ display: flex;
+ flex-direction: column;
+ align-self: center;
+ box-sizing: border-box; /* 패딩을 포함한 전체 크기를 설정 */
+ overflow-y: auto; /* 내용이 넘칠 경우 스크롤 */
+ padding-top: 0rem;
+`;
+
+export const Header = styled.div`
+ margin-top: 0;
+ display: flex;
+ align-items: center;
+ padding: 0rem;
+ margin-left: 20px;
+`;
+
+export const StatsContainer = styled.div`
+ display: flex;
+ justify-content: space-around;
+ padding: 0.625rem 0; /* 10px 0 */
+ border-top: 1px solid #eee;
+ border-bottom: 1px solid #eee;
+`;
+
+export const Stat = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+`;
+
+export const StatNumber = styled.div`
+ color: var(--Color-gray4, #8e8e8e);
+ text-align: center;
+
+ font-family: 'Pretendard';
+ font-size: 1rem; /* 16px */
+ font-style: normal;
+ font-weight: 400;
+ line-height: normal;
+`;
+
+export const StatLabel = styled.div`
+ color: var(--Color-gray4, #8e8e8e);
+ text-align: center;
+ font-family: 'Pretendard';
+ font-size: 0.75rem; /* 12px */
+ font-style: normal;
+ font-weight: 300;
+`;
+
+export const PostsContainer = styled.div`
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between; /* 두 개씩 나란히 배치 */
+ gap: 15px;
+ cursor: pointer;
+ margin-bottom: 100px;
+ padding: 20px;
+`;
+
+export const AddButton = styled.button`
+ position: absolute;
+ bottom: 6.75rem;
+ right: 1.25rem;
+ width: 5rem;
+ height: 5rem;
+ border: none;
+ border-radius: 50%;
+ z-index: 2;
+`;
diff --git a/src/pages/MyPost/assets/DeleteIcon.png b/src/pages/MyPost/assets/DeleteIcon.png
deleted file mode 100644
index 298d5d66..00000000
Binary files a/src/pages/MyPost/assets/DeleteIcon.png and /dev/null differ
diff --git a/src/pages/MyPost/assets/EditIcon.svg b/src/pages/MyPost/assets/EditIcon.svg
deleted file mode 100644
index 90ae81b5..00000000
--- a/src/pages/MyPost/assets/EditIcon.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-
diff --git a/src/pages/MyPost/assets/PinIcon.svg b/src/pages/MyPost/assets/PinIcon.svg
deleted file mode 100644
index b6939703..00000000
--- a/src/pages/MyPost/assets/PinIcon.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/src/pages/MyPost/assets/commentIcon.svg b/src/pages/MyPost/assets/commentIcon.svg
deleted file mode 100644
index 91cea06d..00000000
--- a/src/pages/MyPost/assets/commentIcon.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/src/pages/MyPost/assets/heartIcon.svg b/src/pages/MyPost/assets/heartIcon.svg
deleted file mode 100644
index cf0fc3a9..00000000
--- a/src/pages/MyPost/assets/heartIcon.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/src/pages/MyPost/assets/mockImage.png b/src/pages/MyPost/assets/mockImage.png
deleted file mode 100644
index 063d18c1..00000000
Binary files a/src/pages/MyPost/assets/mockImage.png and /dev/null differ
diff --git a/src/pages/MyPost/assets/nextIcon.svg b/src/pages/MyPost/assets/nextIcon.svg
deleted file mode 100644
index 05c1cff9..00000000
--- a/src/pages/MyPost/assets/nextIcon.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/src/pages/MyPost/dto.tsx b/src/pages/MyPost/dto.tsx
deleted file mode 100644
index dcaa4532..00000000
--- a/src/pages/MyPost/dto.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-// src/types.ts
-
-export interface BaseResponse {
- isSuccess: boolean;
- code: number;
- message: string;
- result?: T;
-}
-
-export interface PostDetailResponse {
- isSuccess: boolean;
- code: number;
- message: string;
- result: {
- postId: number;
- userId: number;
- likes: number | null;
- comments: { commentId: number; userId: number; text: string; timestamp: string }[] | null;
- photoUrls: string[];
- content: string;
- styletags: string[];
- clothingInfo: { imageUrl: string; brand: string; model: string; modelNumber: string; url: string }[] | null;
- };
-}
-
-export interface LikesResponse {
- isSuccess: boolean;
- code: number;
- message: string;
- result: {
- totalLikes: number;
- likes: Array<{
- id: number;
- userId: number;
- postId: number;
- status: string;
- createdAt: string;
- updatedAt: string;
- user: {
- id: number;
- nickname: string;
- profilePictureUrl: string;
- };
- }>;
- };
-}
-
-export interface CommentsResponse {
- isSuccess: boolean;
- code: number;
- message: string;
- result: {
- comments: Array<{
- id: number;
- postId: number;
- content: string;
- status: string;
- createdAt: string;
- updatedAt: string;
- deletedAt: string | null;
- user: {
- id: number;
- nickname: string;
- profilePictureUrl: string;
- };
- }>;
- };
-}
-export interface User {
- id: number;
- name: string;
- profilePictureUrl: string;
- nickname: string;
-}
-
-export interface UserResponse {
- id: number;
- name: string;
- email: string;
- nickname: string | null;
- phoneNumber: string | null;
- profilePictureUrl: string;
- bio: string | null;
- joinedAt: string;
- isSuccess: boolean;
- result: User;
- message: string;
-}
diff --git a/src/pages/MyPost/index.tsx b/src/pages/MyPost/index.tsx
index a90928c8..ef765396 100644
--- a/src/pages/MyPost/index.tsx
+++ b/src/pages/MyPost/index.tsx
@@ -1,390 +1,149 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
-import {
- PostDetailContainer,
- UserID,
- Pic_exam,
- UserInfoContainer,
- UserRow,
- Text,
- Menu,
- ImageWrapper,
- Image,
- IconRow,
- IconWrapper,
- ClothingInfoContainer,
- Tab,
- ContentContainer,
- UserItem,
- CircleIcon,
- ModalContainer,
- TabContainer,
- Arrow,
- Indicator,
-} from './styles';
-import TopBar from '../../components/TopBar';
-import { OODDFrame } from '../../components/Frame/Frame';
-import ConfirmationModal from '../../components/ConfirmationModal';
+import { useRecoilState } from 'recoil';
+import { isPostRepresentativeAtom } from '../../recoil/Post/PostAtom';
+
+import PostBase from '../../components/PostBase';
+import Modal from '../../components/Modal';
+import { ModalProps } from '../../components/Modal/dto';
import BottomSheet from '../../components/BottomSheet';
import { BottomSheetProps } from '../../components/BottomSheet/dto';
import BottomSheetMenu from '../../components/BottomSheetMenu';
import { BottomSheetMenuProps } from '../../components/BottomSheetMenu/dto';
-import ClothingInfoCard from '../Post/ClothingInfoCard';
-import imageBasic from '../../assets/imageBasic.svg';
-import back from '../../assets/back.svg';
-import nextIcon from '../../assets/Upload/next.svg';
-import DeleteIcon from './assets/DeleteIcon.png';
-import EditIcon from './assets/EditIcon.svg';
-import PinIcon from './assets/PinIcon.svg';
-import mockImage from './assets/mockImage.png';
-import heartIcon from './assets/heartIcon.svg';
-import commentIcon from './assets/commentIcon.svg';
+import Edit from '../../assets/default/edit.svg';
+import Pin from '../../assets/default/pin.svg';
+import Delete from '../../assets/default/delete.svg';
-import request from '../../apis/core';
-import { UserResponse } from './dto';
-import { BaseResponse, PostDetailResponse, LikesResponse, CommentsResponse } from './dto';
-import Loading from '../../components/Loading';
+import { modifyPostRepresentativeStatusApi, deletePostApi } from '../../apis/post';
const MyPost: React.FC = () => {
const { postId } = useParams<{ postId: string }>();
- const [postDetail, setPostDetail] = useState(null);
- const [currentImageIndex, setCurrentImageIndex] = useState(0);
- const [isBottomSheetOpen, setIsBottomSheetOpen] = useState(false);
- const [activeTab, setActiveTab] = useState<'likes' | 'comments' | 'menu'>('menu');
- const [likes, setLikes] = useState([]);
- const [comments, setComments] = useState([]);
- const [isConfirmationModalOpen, setIsConfirmationModalOpen] = useState(false);
+ const [isPostRepresentative, setIsPostRepresentative] = useRecoilState(isPostRepresentativeAtom);
+ const [postPinStatus, setPostPinStatus] = useState<'지정' | '해제'>('지정');
+ const [isMenuBottomSheetOpen, setIsMenuBottomSheetOpen] = useState(false);
+ const [isDeleteConfirmationModalOpen, setIsDeleteConfirmationModalOpen] = useState(false);
+ const [isApiResponseModalOpen, setIsApiResponseModalOpen] = useState(false);
+ const [pinPostResultlModalContent, setPinPostResultlModalContent] = useState('');
const navigate = useNavigate();
- const [user, setUser] = useState(null);
- const [isLoading, setIsLoading] = useState(true); // 로딩 상태 추가
-
- // 좋아요 리스트 불러오기
- const fetchLikes = async () => {
- try {
- const response = await request.get(`/posts/${postId}/like`);
- if (response.isSuccess) {
- setLikes(response.result.likes);
- } else {
- console.error('Failed to fetch likes:', response.message);
- }
- } catch (error) {
- console.error('Error fetching likes:', error);
- }
- };
-
- // 코멘트 리스트 불러오기
- const fetchComments = async () => {
- try {
- const response = await request.get(`/posts/${postId}/comments`);
- if (response.isSuccess) {
- setComments(response.result.comments);
- } else {
- console.error('Failed to fetch comments:', response.message);
- }
- } catch (error) {
- console.error('Error fetching comments:', error);
- }
- };
-
- // 유저 정보 가져오기
- const fetchUserData = async () => {
- try {
- const storedUserId = localStorage.getItem('id'); // Ensure correct key is used
- if (!storedUserId) {
- console.error('User is not logged in');
- return;
- }
-
- const response = await request.get>(`/users/${storedUserId}`);
- setUser(response.result as UserResponse);
- } catch (error) {
- console.error('Error fetching user data:', error);
- } finally {
- setIsLoading(false); // 로딩 완료 후 로딩 상태 false로 설정
- }
- };
useEffect(() => {
- fetchPostDetail();
- fetchUserData(); // Fetch user data
- }, [postId]);
+ if (isPostRepresentative) {
+ setPostPinStatus('해제');
+ } else {
+ setPostPinStatus('지정');
+ }
+ }, [isPostRepresentative]);
const bottomSheetMenuProps: BottomSheetMenuProps = {
items: [
{
- text: '대표 OOTD로 지정하기',
+ text: `대표 OOTD ${postPinStatus}하기`,
action: () => {
- setIsBottomSheetOpen(false);
- handlePinPost();
+ setIsMenuBottomSheetOpen(false);
+ modifyPostRepresentativeStatus();
},
- icon: PinIcon,
+ icon: Pin,
},
{
text: 'OODD 수정하기',
action: () => {
- setIsBottomSheetOpen(false);
- handleEditPost();
+ setIsMenuBottomSheetOpen(false);
+ handlePostEdit();
},
- icon: EditIcon,
+ icon: Edit,
},
{
text: 'OOTD 삭제하기',
action: () => {
- setIsBottomSheetOpen(false);
- handleDeletePost();
+ setIsMenuBottomSheetOpen(false);
+ setIsDeleteConfirmationModalOpen(true);
},
- icon: DeleteIcon,
+ icon: Delete,
},
],
marginBottom: '50px',
};
- const bottomSheetProps: BottomSheetProps = {
- isOpenBottomSheet: isBottomSheetOpen,
- isHandlerVisible: true,
- // TODO: 컴포넌트 분리에 따라 BottomSheetProps 제너릭 타입 추후 수정
- Component: () => {
- if (activeTab === 'menu') {
- return ;
- } else {
- return (
-
-
- setActiveTab('likes')}>
- 좋아요 {likes.length}
-
- setActiveTab('comments')}>
- 코멘트 {comments.length}
-
-
-
- {activeTab === 'likes' && (
- <>
- {likes.map((like) => (
-
-
-
-
- {like.user.nickname}
-
- ))}
- >
- )}
- {activeTab === 'comments' && (
- <>
- {comments.map((comment) => (
-
-
-
-
-
-
{comment.user.nickname}
-
{comment.content}
-
-
- ))}
- >
- )}
-
-
- );
- }
- },
- onCloseBottomSheet: () => {
- setIsBottomSheetOpen(false);
- },
- };
-
- const handleOpenSheet = async (tab: 'likes' | 'comments' | 'menu') => {
- setActiveTab(tab);
- if (tab === 'likes') await fetchLikes();
- if (tab === 'comments') await fetchComments();
- setIsBottomSheetOpen(true);
- };
-
- const fetchPostDetail = async () => {
- try {
- const response = await request.get(`/posts/${postId}`);
- if (response.isSuccess) {
- setPostDetail(response.result);
- } else {
- console.error('Unexpected response:', response.message);
- }
- } catch (error) {
- console.error('Error fetching post details:', error);
- } finally {
- setIsConfirmationModalOpen(false); // 확인 모달을 닫음
- }
- };
-
- const handleNextImage = () => {
- if (postDetail && currentImageIndex < postDetail.photoUrls.length - 1) {
- setCurrentImageIndex(currentImageIndex + 1);
- }
+ const menuBottomSheetProps: BottomSheetProps = {
+ isOpenBottomSheet: isMenuBottomSheetOpen,
+ Component: BottomSheetMenu,
+ componentProps: bottomSheetMenuProps,
+ onCloseBottomSheet: () => setIsMenuBottomSheetOpen(false),
};
- const handlePrevImage = () => {
- if (currentImageIndex > 0) {
- setCurrentImageIndex(currentImageIndex - 1);
- }
+ const handleMenuOpen = () => {
+ setIsMenuBottomSheetOpen(true);
};
- const handleEditPost = () => {
+ const handlePostEdit = () => {
navigate('/upload', { state: { mode: 'edit', postId: postId } });
};
- const handlePinPost = async () => {
- // localStorage에서 storedUserId를 가져옴
- const storedUserId = localStorage.getItem('id');
-
- if (!storedUserId) {
- console.error('User ID not found');
- return;
- }
-
+ const modifyPostRepresentativeStatus = async () => {
try {
- const response = await request.patch(`/posts/${postId}/isRepresentative/${storedUserId}`, {
- isRepresentative: true,
- });
+ const response = await modifyPostRepresentativeStatusApi(Number(postId));
if (response.isSuccess) {
- console.log('Post pinned successfully:', response.result);
- // PostDetail 재로드
- fetchPostDetail();
- navigate('/mypage');
+ setPinPostResultlModalContent(`대표 OOTD ${postPinStatus}에 성공했어요`);
+ setIsPostRepresentative((prev) => !prev);
} else {
- console.error('Failed to pin post:', response.message);
+ setPinPostResultlModalContent(`대표 OOTD ${postPinStatus}에 실패했어요\n잠시 뒤 다시 시도해 보세요`);
}
} catch (error) {
console.error('Error pinning post:', error);
} finally {
- setIsConfirmationModalOpen(false); // 확인 모달을 닫음
+ setIsApiResponseModalOpen(true);
}
};
- const handleDeletePost = () => {
- setIsConfirmationModalOpen(true);
- };
-
- const handleConfirmDelete = async () => {
+ const deletePost = async () => {
try {
- const response = await request.delete(`/posts/${postId}`);
- if (response.message === 'Post deleted successfully') {
- console.log(response.message);
- navigate('/mypage'); // 성공적으로 삭제 후 다른 페이지로 이동
+ const response = await deletePostApi(Number(postId));
+
+ if (response.isSuccess) {
+ setPinPostResultlModalContent('OOTD 삭제에 성공했어요');
+ // 1초 뒤에 mypage로 이동
+ setTimeout(() => {
+ navigate('/mypage');
+ }, 1000);
} else {
- console.error('Unexpected response:', response.message);
+ setPinPostResultlModalContent(`OOTD 삭제에 실패했어요\n잠시 뒤 다시 시도해 보세요`);
}
} catch (error) {
console.error('Error deleting post:', error);
} finally {
- setIsConfirmationModalOpen(false); // 확인 모달을 닫음
+ setIsApiResponseModalOpen(true);
+ setIsDeleteConfirmationModalOpen(false); // 확인 모달을 닫음
}
};
- const handleCancelDelete = () => {
- setIsConfirmationModalOpen(false);
+ const deleteConfirmationModalProps: ModalProps = {
+ isCloseButtonVisible: true,
+ onClose: () => setIsDeleteConfirmationModalOpen(false),
+ content: '해당 OOTD를 삭제하시겠습니까?',
+ button: {
+ content: '삭제하기',
+ onClick: deletePost,
+ },
};
- useEffect(() => {
- fetchPostDetail();
- }, [postId]);
- if (isLoading) {
- return ; // 로딩 중일 때 Loading 컴포넌트 표시
- }
- return (
-
-
- navigate(-1)} />
+ const apiResponseModalProps: ModalProps = {
+ onClose: () => setIsApiResponseModalOpen(false),
+ content: pinPostResultlModalContent,
+ };
-
-
-
-
-
- {user?.nickname || 'Unknown User'}
-
- {postDetail?.content || 'Loading...'}
-
-
+ return (
+ <>
+
-
- {postDetail?.photoUrls && postDetail.photoUrls.length > 1 && (
- <>
-
-
-
-
-
-
-
- {currentImageIndex + 1} / {postDetail.photoUrls.length}
-
- >
- )}
-
-
+
- {isBottomSheetOpen && }
- {isConfirmationModalOpen && (
-
- )}
-
- handleOpenSheet('likes')}>
-
- {postDetail?.likes || 0} {/* 좋아요 수 */}
-
- handleOpenSheet('comments')}>
-
- {postDetail?.comments?.length || 0} {/* 댓글 수 */}
-
-
-
- {postDetail?.clothingInfo?.map((clothingInfo, index: number) => (
-
- ))}
-
-
-
+ {isDeleteConfirmationModalOpen && }
+ {isApiResponseModalOpen && }
+ >
);
};
diff --git a/src/pages/MyPost/styles.tsx b/src/pages/MyPost/styles.tsx
index 8b141576..e130d565 100644
--- a/src/pages/MyPost/styles.tsx
+++ b/src/pages/MyPost/styles.tsx
@@ -1,187 +1,4 @@
import styled from 'styled-components';
-import theme from '../../styles/theme';
-
-export const PostDetailContainer = styled.div`
- max-width: 512px; /* 32rem */
- display: flex;
- flex-direction: column;
- position: relative;
-`;
-
-export const UserInfoContainer = styled.div`
- display: flex;
- flex-direction: column;
- align-items: flex-start; /* 왼쪽 정렬 */
- margin-left: 5px;
- object-fit: cover;
- padding: 10px 20px; /* 1.25rem */
-`;
-
-export const UserRow = styled.div`
- display: flex;
- align-items: center; /* Pic_exam과 UserID를 수평으로 정렬 */
-`;
-
-export const UserID = styled.div`
- /* Body1/Medium */
- font-family: 'Gmarket Sans';
- font-size: 16px; /* 1rem */
- color: #000;
- font-style: normal;
- font-weight: 400;
- line-height: normal;
- margin-left: 8px; /* 0.5rem */
-`;
-
-export const Pic_exam = styled.div`
- width: 36px; /* 2.25rem */
- height: 36px; /* 2.25rem */
- flex-shrink: 0;
- display: flex;
- padding: 0;
- margin-left: 0;
-
- img {
- width: 100%;
- height: 100%;
- object-fit: cover;
- border-radius: 50%;
- }
-`;
-
-export const Text = styled.div`
- color: var(--Color-black50, rgba(0, 0, 0, 0.5));
- font-family: 'Pretendard Variable';
- font-size: 12px; /* 0.75rem */
- font-style: normal;
- font-weight: 300;
- line-height: 1.2; /* 120% */
- margin-top: 4px; /* 0.25rem */
-`;
-
-export const Menu = styled.div`
- position: absolute;
- top: 70px; /* 4.375rem */
- right: 20px; /* 1.25rem */
- cursor: pointer;
-`;
-
-export const ImageWrapper = styled.div`
- position: relative;
- margin-top: 0px; /* 1.25rem */
- width: 100%;
- display: flex;
- justify-content: center;
-`;
-
-export const Image = styled.img`
- width: 100%;
- aspect-ratio: 3 / 4;
- height: auto;
- object-fit: cover;
-`;
-
-export const IconRow = styled.div`
- display: flex;
- align-items: center;
- padding: 10px 20px; /* 1.25rem */
-`;
-
-export const IconWrapper = styled.div`
- display: flex;
- align-items: center;
- cursor: pointer;
-
- img {
- width: 24px; /* 아이콘 크기 */
- height: 24px;
- margin-right: 5px;
- margin-top: 5px;
- }
-
- span {
- font-size: 14px;
- color: #000; /* 텍스트 색상 */
- margin-right: 14px;
- }
-`;
-
-export const ClothingInfoContainer = styled.div`
- display: flex;
- overflow-x: auto; /* 가로 스크롤 가능하도록 설정 */
- white-space: nowrap; /* 줄바꿈 없이 한 줄로 나열 */
- padding: 0.625rem 0;
- //margin-top: 16px; /* 상단과의 간격 */
- padding: 0px 20px; /* 1.25rem */
-
- &::-webkit-scrollbar {
- height: 0rem;
- }
-`;
-
-/*
-export const BrandBox = styled.div`
- display: inline-flex; // inline-flex를 사용하여 가로 배치 유지
- align-items: center;
- padding: 0.625rem;
- border: 0.0625rem solid #7b7b7b;
- margin-right: 0.625rem;
- width: 15.3243rem; // 지정된 너비
- height: 4.5rem; // 지정된 높이
- flex-shrink: 0; // 크기 고정
-
- img {
- width: 3.125rem;
- height: 3.125rem;
- margin-right: 0.625rem;
- }
-
- div {
- display: flex;
- flex-direction: column;
- justify-content: center;
- flex-grow: 1; /./ 텍스트 영역 확장
- }
-
- &:last-child {
- margin-right: 0;
- }
-
- .next-icon {
- width: 1.875rem; // 아이콘 크기
- height: 19px;
- margin-left: auto; // 자동으로 오른쪽 끝으로 배치
- }
-`;
-
-export const BrandLink = styled.div`
- display: flex;
- align-items: center;
- justify-content: space-between;
- width: 100%;
-`;
-*/
-
-export const TabContainer = styled.div`
- display: flex;
- justify-content: space-around;
- border-bottom: 1px solid ${theme.colors.gray3};
-`;
-
-export const TabButton = styled.button<{ active: boolean }>`
- flex: 1;
- padding: 10px;
- background-color: ${({ active }) => (active ? theme.colors.white : theme.colors.gray1)};
- color: ${({ active }) => (active ? theme.colors.black : theme.colors.gray4)};
- border: none;
- cursor: pointer;
- font-weight: ${({ active }) => (active ? 'bold' : 'normal')};
-`;
-
-export const TabContent = styled.div`
- padding: 20px;
- background-color: ${theme.colors.white};
-`;
export const ModalContainer = styled.div`
background: white;
@@ -191,64 +8,3 @@ export const ModalContainer = styled.div`
height: 377px;
flex-shrink: 0;
`;
-
-export const Tab = styled.div<{ active: boolean }>`
- flex: 1;
- text-align: center;
- padding: 16px 0;
- cursor: pointer;
- font-weight: ${(props) => (props.active ? 'bold' : 'normal')};
- border-bottom: ${(props) => (props.active ? '2px solid black' : 'none')};
- color: var(--Color-black, #000);
- text-align: center;
- font-family: 'Pretendard Variable';
- font-size: 16px;
- font-style: normal;
- font-weight: 500;
- line-height: 150%; /* 24px */
-`;
-
-export const ContentContainer = styled.div`
- padding: 16px;
-`;
-
-export const UserItem = styled.div`
- display: flex;
- align-items: center;
- padding: 8px 0;
- border-bottom: 1px solid #eee;
-`;
-
-export const CircleIcon = styled.div`
- width: 24px;
- height: 24px;
- border-radius: 50%;
- background-color: red; /* 동그라미 색상 */
- display: flex;
- align-items: center;
- justify-content: center;
- color: white;
- font-weight: bold;
- margin-right: 8px;
-`;
-
-export const Arrow = styled.div<{ direction: string; disabled: boolean }>`
- position: absolute;
- top: 50%;
- ${({ direction }) => (direction === 'left' ? 'left: 10px;' : 'right: 10px;')}
- transform: translateY(-50%);
- cursor: ${({ disabled }) => (disabled ? 'default' : 'pointer')};
- opacity: ${({ disabled }) => (disabled ? 0.5 : 1)};
-`;
-
-export const Indicator = styled.div`
- position: absolute;
- top: 10px;
- left: 50%;
- transform: translateX(-50%);
- background-color: rgba(0, 0, 0, 0.5);
- color: #fff;
- padding: 5px 10px;
- border-radius: 15px;
- font-size: 14px;
-`;
diff --git a/src/pages/Mypage/Post/Heart.svg b/src/pages/Mypage/Post/Heart.svg
deleted file mode 100644
index c9a9a0a7..00000000
--- a/src/pages/Mypage/Post/Heart.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/src/pages/Mypage/Post/comment.svg b/src/pages/Mypage/Post/comment.svg
deleted file mode 100644
index b321a5a9..00000000
--- a/src/pages/Mypage/Post/comment.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/src/pages/Mypage/Post/index.tsx b/src/pages/Mypage/Post/index.tsx
deleted file mode 100644
index 5257190d..00000000
--- a/src/pages/Mypage/Post/index.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import React from 'react';
-import { useNavigate } from 'react-router-dom';
-import { PostContainer, ImageWrapper, Image, IconContainer, Icon, PinIcon } from './styles';
-import Heart from './Heart.svg';
-import Comment from './comment.svg';
-import Pin from './pin.svg';
-
-interface PostProps {
- imgUrl: string;
- likes: number;
- comments: number;
- onClick: () => void;
- isFirst?: boolean; // 첫 번째 포스트인지 여부를 나타내는 새로운 prop
-}
-
-const Post: React.FC = ({ imgUrl, likes, comments, onClick, isFirst }) => {
- const navigate = useNavigate();
-
- const handleIconClick = (event: React.MouseEvent, type: 'likes' | 'comments') => {
- event.stopPropagation();
- navigate(`/post/1/${type}`);
- };
-
- return (
-
-
- {isFirst && }
-
-
- handleIconClick(event, 'likes')}>
-
- {likes}
-
- handleIconClick(event, 'comments')}>
-
- {comments}
-
-
-
-
- );
-};
-
-export default Post;
diff --git a/src/pages/Mypage/Post/pin.svg b/src/pages/Mypage/Post/pin.svg
deleted file mode 100644
index 61e93c64..00000000
--- a/src/pages/Mypage/Post/pin.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/src/pages/Mypage/Post/styles.tsx b/src/pages/Mypage/Post/styles.tsx
deleted file mode 100644
index 6e94ffd3..00000000
--- a/src/pages/Mypage/Post/styles.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import styled from 'styled-components';
-
-export const PostContainer = styled.div`
- width: 50%; /* 포스트 크기를 약간 키움 */
- max-width: 32rem; /* 512px */
- height: auto; /* 비율 유지를 위해 auto로 설정 */
- flex-shrink: 0; /* 포스트가 줄어들지 않도록 설정 */
- margin: 0; /* 간격을 없앰 */
- display: flex;
- flex-direction: column;
- align-items: center;
- position: relative;
-`;
-
-export const ImageWrapper = styled.div`
- width: 100%;
- padding-top: 150%; /* 3:2 비율을 유지 */
- position: relative;
-`;
-
-export const Image = styled.img`
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- object-fit: cover;
-`;
-
-export const IconContainer = styled.div`
- position: absolute;
- bottom: 0.625rem; /* 10px */
- right: 0.625rem; /* 10px */
- display: flex;
- align-items: center;
-`;
-
-export const Icon = styled.div`
- display: flex;
- align-items: center;
- margin-left: 1.25rem; /* 20px */
-
- svg {
- margin-right: 0.3125rem; /* 5px */
- }
-
- span {
- color: white;
- font-size: 0.875rem; /* 14px */
- }
-`;
-
-export const PinIcon = styled.img`
- position: absolute;
- top: 0.625rem; /* 10px */
- left: 0.625rem; /* 10px */
- width: 1.25rem; /* 20px */
- height: 1.25rem; /* 20px */
- z-index: 1; /* 이미지 위에 표시되도록 z-index 추가 */
-`;
diff --git a/src/pages/Mypage/assets/PinIcon.svg b/src/pages/Mypage/assets/PinIcon.svg
deleted file mode 100644
index b6939703..00000000
--- a/src/pages/Mypage/assets/PinIcon.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/src/pages/Mypage/index.tsx b/src/pages/Mypage/index.tsx
deleted file mode 100644
index a79f3116..00000000
--- a/src/pages/Mypage/index.tsx
+++ /dev/null
@@ -1,202 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { useNavigate } from 'react-router-dom';
-import {
- ProfileContainer,
- Header,
- AvatarWrapper,
- Avatar,
- UserInfo,
- Username,
- Bio,
- StatsContainer,
- Stat,
- StatNumber,
- StatLabel,
- PostsContainer,
- AddButton,
-} from './styles';
-import { OODDFrame } from '../../components/Frame/Frame';
-import NavbarProfile from '../../components/NavbarProfile';
-import NavBar from '../../components/NavBar';
-import ButtonSecondary from './ButtonSecondary';
-import Post from './Post';
-import request, { BaseResponse } from '../../apis/core';
-import { PostItem, PostsResponse, UserResponse } from './dto';
-import imageBasic from '../../assets/imageBasic.svg';
-import Loading from '../../components/Loading';
-import BottomSheet from '../../components/BottomSheet';
-import { BottomSheetProps } from '../../components/BottomSheet/dto';
-import BottomSheetMenu from '../../components/BottomSheetMenu';
-import { BottomSheetMenuProps } from '../../components/BottomSheetMenu/dto';
-import button_plus from '../../assets/Profile/button_plus.svg';
-import Insta from '../../assets/BottomSheetMenu/Insta.svg';
-import Picture from '../../assets/BottomSheetMenu/Picture.svg';
-
-const MyPage: React.FC = () => {
- const [user, setUser] = useState(null);
- const [posts, setPosts] = useState([]);
- const [totalPosts, setTotalPosts] = useState(0);
- const [totalLikes, setTotalLikes] = useState(0);
- const [totalComments, setTotalComments] = useState(0); // Comments count
- const [isLoading, setIsLoading] = useState(true); // 로딩 상태 추가
- const [isBottomSheetOpen, setIsBottomSheetOpen] = useState(false);
- const navigate = useNavigate();
-
- const bottomSheetMenuProps: BottomSheetMenuProps = {
- items: [
- {
- text: '인스타 피드 가져오기',
- action: () => {
- setIsBottomSheetOpen(false);
- handleInstagramSelect();
- },
- icon: Insta,
- },
- {
- text: '사진 올리기',
- action: () => {
- setIsBottomSheetOpen(false);
- handlePhotoUploadSelect();
- },
- icon: Picture,
- },
- ],
- marginBottom: '50px',
- };
-
- const bottomSheetProps: BottomSheetProps = {
- isOpenBottomSheet: isBottomSheetOpen,
- isHandlerVisible: true,
- Component: BottomSheetMenu,
- componentProps: bottomSheetMenuProps,
- onCloseBottomSheet: () => {
- setIsBottomSheetOpen(false);
- },
- };
-
- const handleOpenSheet = () => {
- setIsBottomSheetOpen(true);
- };
-
- const handleInstagramSelect = () => {
- navigate('/upload', { state: { mode: 'instagram' } });
- };
-
- const handlePhotoUploadSelect = () => {
- navigate('/upload', { state: { mode: 'image' } });
- };
-
- const handlePostClick = (postId: string) => {
- navigate(`/my-post/${postId}`);
- };
-
- // 사용자 정보 가져오기 함수
- const fetchUserData = async () => {
- try {
- const storedUserId = localStorage.getItem('id');
-
- if (!storedUserId) {
- console.error('User is not logged in');
- return;
- }
-
- const response = await request.get>(`/users/${storedUserId}`);
- setUser(response.result);
- } catch (error) {
- console.error('Error fetching user data:', error);
- }
- };
-
- // API에서 포스트 리스트를 가져오는 함수
- const handlePostList = async () => {
- try {
- const storedUserId = localStorage.getItem('id');
- if (!storedUserId) {
- console.error('User is not logged in');
- return;
- }
-
- const response = await request.get(`/posts?userId=${storedUserId}`);
- if (response.isSuccess) {
- const { totalPosts, totalLikes, posts } = response.result;
- setTotalPosts(totalPosts);
- setTotalLikes(totalLikes);
- setPosts(posts);
- const totalComments = posts.reduce((sum, post) => sum + (post.commentsCount || 0), 0);
- setTotalComments(totalComments);
- } else {
- console.error('Unexpected response:', response.message);
- }
- } catch (error) {
- console.error('Error fetching posts:', error);
- } finally {
- setIsLoading(false); // 로딩 완료 후 로딩 상태 false로 설정
- }
- };
-
- useEffect(() => {
- fetchUserData();
- handlePostList();
- }, []);
-
- if (isLoading) {
- return ; // 로딩 중일 때 Loading 컴포넌트 표시
- }
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
- {user?.nickname || '김아무개...'}
- {user?.bio || '소개글이 없습니다.'}
-
-
-
-
-
- OOTD
- {totalPosts}
-
-
- 코멘트
- {totalComments}
-
-
- 좋아요
- {totalLikes}
-
-
-
- {posts
- .sort((a, b) => {
- if (b.isRepresentative && !a.isRepresentative) return 1;
- if (a.isRepresentative && !b.isRepresentative) return -1;
- return 0;
- })
- .map((post) => (
- handlePostClick(post.postId.toString())}
- isFirst={post.isRepresentative}
- />
- ))}
-
-
-
-
- );
-};
-
-export default MyPage;
diff --git a/src/pages/Mypage/styles.tsx b/src/pages/Mypage/styles.tsx
deleted file mode 100644
index e1dd08a8..00000000
--- a/src/pages/Mypage/styles.tsx
+++ /dev/null
@@ -1,136 +0,0 @@
-import styled from 'styled-components';
-
-export const ProfileContainer = styled.div`
- width: 100%;
- max-width: 32rem;
- flex-grow: 1;
- margin: 0 auto; /* 중앙 정렬 */
- display: flex;
- flex-direction: column;
- align-self: center;
- box-sizing: border-box; /* 패딩을 포함한 전체 크기를 설정 */
- overflow-y: auto; /* 내용이 넘칠 경우 스크롤 */
- padding-top: 0rem;
-`;
-
-export const Header = styled.div`
- margin-top: 0;
- display: flex;
- align-items: center;
- padding: 0rem;
- margin-left: -0.9375rem;
-`;
-
-export const AvatarWrapper = styled.div`
- width: 4.5rem; /* 72px */
- height: 4.5rem; /* 72px */
- border-radius: 50%;
- overflow: hidden;
- margin-right: 1.25rem; /* 20px */
- margin-left: 30px;
-`;
-
-export const Avatar = styled.img`
- width: 100%;
- height: 100%;
- object-fit: cover;
-`;
-
-export const UserInfo = styled.div`
- display: flex;
- flex-direction: column;
- justify-content: center;
-`;
-
-export const Username = styled.div`
- color: var(--Color-black, #000);
-
- /* Body1/Medium */
- font-family: 'Gmarket Sans';
- font-size: 1rem; /* 16px */
- font-style: normal;
- font-weight: 400;
- line-height: normal;
-`;
-
-export const Bio = styled.div`
- color: var(--Color-gray4, #434343);
-
- /* Body4/Light */
- font-family: 'Pretendard Variable';
- font-size: 0.8125rem; /* 13px */
- font-style: normal;
- font-weight: 300;
- line-height: normal;
-
- margin-top: 10px;
-`;
-
-export const StatsContainer = styled.div`
- display: flex;
- justify-content: space-around;
- padding: 0.625rem 0; /* 10px 0 */
- border-top: 1px solid #eee;
- border-bottom: 1px solid #eee;
-`;
-
-export const Stat = styled.div`
- display: flex;
- flex-direction: column;
- align-items: center;
-`;
-
-export const StatNumber = styled.div`
- color: var(--Color-gray4, #434343);
- text-align: center;
-
- /* Body1/Medium */
- font-family: 'Gmarket Sans';
- font-size: 1rem; /* 16px */
- font-style: normal;
- font-weight: 400;
- line-height: normal;
-`;
-
-export const StatLabel = styled.div`
- color: var(--Color-gray4, #434343);
- text-align: center;
-
- /* Body6/Light */
- font-family: 'Pretendard Variable';
- font-size: 0.75rem; /* 12px */
- font-style: normal;
- font-weight: 300;
- line-height: normal;
-`;
-
-export const PostsContainer = styled.div`
- display: flex;
- flex-wrap: wrap;
- justify-content: space-between; /* 두 개씩 나란히 배치 */
- gap: 0; /* 간격을 없앰 */
- cursor: pointer;
-`;
-
-export const AddButton = styled.button`
- display: flex;
- align-items: center;
- justify-content: center;
- position: fixed; /* absolute에서 fixed로 변경 */
- bottom: 6.75rem;
- right: 1.25rem;
- width: 5rem;
- height: 5rem;
- border: none;
- border-radius: 50%;
- background-color: ${({ theme }) => theme.colors.white};
- color: ${({ theme }) => theme.colors.black};
- box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.1);
- font-size: 1rem;
- cursor: pointer;
- z-index: 2;
-
- &:hover {
- background-color: ${({ theme }) => theme.colors.gray3};
- }
-`;
diff --git a/src/pages/NotFound/index.tsx b/src/pages/NotFound/index.tsx
new file mode 100644
index 00000000..4c879a1f
--- /dev/null
+++ b/src/pages/NotFound/index.tsx
@@ -0,0 +1,44 @@
+import { useNavigate } from 'react-router-dom';
+import { OODDFrame } from '../../components/Frame/Frame';
+import { NotFoundContainer, TextContainer, ButtonContainer, StyledButton } from './styles';
+import { StyledText } from '../../components/Text/StyledText';
+import theme from '../../styles/theme';
+
+const NotFound = () => {
+ const navigate = useNavigate();
+
+ return (
+
+
+
+
+ 404 ERROR
+
+ 죄송합니다. 페이지를 찾을 수 없습니다.
+
+ 페이지의 주소가 잘못 입력되었거나,
+
+ 요청하신 페이지의 주소가 변경, 삭제되어 찾을 수 없습니다.
+
+
+
+
+
+ 메인으로
+
+ navigate(-1)}
+ className="prev"
+ $textTheme={{ style: 'body2-regular' }}
+ color={theme.colors.white}
+ >
+ 이전으로
+
+
+
+
+ );
+};
+
+export default NotFound;
diff --git a/src/pages/NotFound/styles.tsx b/src/pages/NotFound/styles.tsx
new file mode 100644
index 00000000..ec172fb8
--- /dev/null
+++ b/src/pages/NotFound/styles.tsx
@@ -0,0 +1,49 @@
+import styled from 'styled-components';
+import { StyledText } from '../../components/Text/StyledText';
+
+export const NotFoundContainer = styled.div`
+ display: flex;
+ height: 80%;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+`;
+
+export const TextContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+
+ div {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ }
+`;
+
+export const ButtonContainer = styled.div`
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ gap: 16px;
+ margin: 20px;
+`;
+
+export const StyledButton = styled(StyledText)`
+ display: inline-block;
+ text-align: center;
+ padding: 6px 16px;
+ border: 1px solid ${({ theme }) => theme.colors.pink3};
+ border-radius: 8px;
+ cursor: pointer;
+ text-decoration: none;
+
+ &.prev {
+ background-color: ${({ theme }) => theme.colors.pink3};
+ color: ${({ theme }) => theme.colors.white};
+ }
+`;
diff --git a/src/pages/Post/ClothingInfoCard.tsx b/src/pages/Post/ClothingInfoCard.tsx
deleted file mode 100644
index c15aefad..00000000
--- a/src/pages/Post/ClothingInfoCard.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import React from 'react';
-import { StyledText } from '../../components/Text/StyledText';
-import theme from '../../styles/theme';
-import {
- NextBtn,
- ClothingInfoCardContainer,
- ClothingInfoImg,
- ClothingInfoDetail,
- StyledTextClipped,
- ClothingInfoLeft,
-} from './styles';
-import nextBtn from './../../assets/Post/next.svg';
-import { ClothingInfoCardProps } from './dto';
-
-const ClothingInfoCard: React.FC = ({ imageUrl, brand, model, url }) => {
- const handleClick = () => {
- if (url) {
- window.location.href = url;
- }
- };
-
- return (
-
-
-
-
-
- {brand}
-
-
- {model}
-
-
-
-
-
-
-
- );
-};
-
-export default ClothingInfoCard;
diff --git a/src/pages/Post/PostTopBar.tsx b/src/pages/Post/PostTopBar.tsx
deleted file mode 100644
index 4d7eb0e6..00000000
--- a/src/pages/Post/PostTopBar.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import React from 'react';
-import { BackIcon, MidWrapper, PostTopBarContainer, RightSpace } from './styles';
-import backIcon from './../../assets/ProfileViewer/backIcon.svg';
-import { StyledText } from '../../components/Text/StyledText';
-import theme from '../../styles/theme';
-import { useNavigate } from 'react-router-dom';
-import { PostTopBarProps } from './dto';
-
-// Post 페이지의 상단 바입니다.
-const PostTopBar: React.FC = ({ userName }) => {
- const nav = useNavigate();
-
- return (
-
- nav(-1)} />
-
-
- {userName}
-
-
- OOTD
-
-
-
-
- );
-};
-
-export default PostTopBar;
diff --git a/src/pages/Post/dto.ts b/src/pages/Post/dto.ts
index 3b27eb24..e69de29b 100644
--- a/src/pages/Post/dto.ts
+++ b/src/pages/Post/dto.ts
@@ -1,61 +0,0 @@
-export interface PostTopBarProps {
- userName: string;
-}
-
-export interface PostResponse {
- isSuccess: boolean;
- code: number;
- message: string;
- result: PostData;
-}
-
-interface Comments {
- commentId: number | null;
- userId: number | null;
- text: string | null;
- timestamp: string | null;
-}
-
-export interface PostData {
- postId: number;
- userId: number;
- likes: number | null;
- comments: Comments[] | null;
- photoUrls: string[];
- content: string;
- styletags: string[];
- clothingInfo: ClothingInfo[] | null;
-}
-
-export interface ClothingInfo {
- brand: string;
- model: string;
- modelNumber: number;
- url: string;
- imageUrl: string;
-}
-
-export interface UserResponse {
- isSuccess: boolean;
- code: number;
- message: string;
- result: UserData;
-}
-
-export interface UserData {
- id: number;
- name: string;
- email: string;
- nickname: string | null;
- phoneNumber: string | null;
- profilePictureUrl: string;
- bio: string | null;
- joinedAt: string;
-}
-
-export interface ClothingInfoCardProps {
- imageUrl: string;
- brand: string;
- model: string;
- url: string;
-}
diff --git a/src/pages/Post/index.tsx b/src/pages/Post/index.tsx
index cb595c3e..d2c2cb17 100644
--- a/src/pages/Post/index.tsx
+++ b/src/pages/Post/index.tsx
@@ -1,302 +1,41 @@
-import React, { useEffect, useState } from 'react';
-import { OODDFrame } from '../../components/Frame/Frame';
-import { useLocation, useNavigate, useParams } from 'react-router-dom';
-import { Swiper, SwiperSlide } from 'swiper/react';
-import { Pagination, Navigation } from 'swiper/modules';
-import { BottomSheetMenuProps } from '../../components/BottomSheetMenu/dto.ts';
-import { StyledText } from '../../components/Text/StyledText';
-import {
- MoreBtn,
- PostImg,
- PostInfo,
- PostText,
- PostWrapper,
- ClothingInfos,
- UserInfo,
- UserName,
- UserProfile,
-} from './styles';
-import Loading from '../../components/Loading/index.tsx';
-import PostTopBar from './PostTopBar';
-import ClothingInfoCard from './ClothingInfoCard';
-import BottomSheet from '../../components/BottomSheet';
-import BottomSheetMenu from '../../components/BottomSheetMenu';
-import Modal from '../../components/Modal';
-import Comment from '../../components/Comment';
-import 'swiper/css';
-import 'swiper/css/pagination';
-import theme from '../../styles/theme';
-import profileImg from './../../assets/Post/profileImg.svg';
-import more from './../../assets/Post/more.svg';
-import declaration from '../../assets/Post/declaration.svg';
-import block from '../../assets/Post/block.svg';
-import ConfirmationModal from '../../components/ConfirmationModal/index.tsx';
-import { CommentProps } from '../../components/Comment/dto.ts';
-import { BottomSheetProps } from '../../components/BottomSheet/dto.ts';
-import ReportTextarea from '../Home/ReportTextarea.tsx';
-import { PostResponse, UserResponse, ClothingInfo } from './dto';
-import request from '../../apis/core';
+import React, { useState } from 'react';
-interface CommentResponse {
- isSuccess: boolean;
- message: string;
- result?: any; // 성공 시 반환되는 데이터가 있다면 여기에 정의할 수 있습니다.
-}
+import { useRecoilValue } from 'recoil';
+import { postIdAtom, userAtom } from '../../recoil/Post/PostAtom.ts';
-const Post: React.FC = () => {
- const { postId } = useParams<{ postId: string }>();
- const [postData, setPostData] = useState();
- const [user, setUser] = useState();
- const [userName, setUserName] = useState('');
- const [isOpenBottomSheet, setIsOpenBottomSheet] = useState(false);
- const [isOpenReportSheet, setIsOpenReportSheet] = useState(false);
- const [showInput, setShowInput] = useState(false);
- const [isModalOpen, setIsModalOpen] = useState(false);
- const [isConfirmationModalOpen, setIsConfirmationModalOpen] = useState(false);
- const [isBlockedModalOpen, setIsBlockedModalOpen] = useState(false);
- const [isCommentModalOpen, setIsCommentModalOpen] = useState(false);
- const nav = useNavigate();
- const location = useLocation();
-
- useEffect(() => {
- if (location.state && location.state.isCommentModalOpen) {
- setIsCommentModalOpen(true);
- }
- }, [location.state]);
-
- useEffect(() => {
- const fetchPostData = async () => {
- try {
- const response = await request.get(`/posts/${postId}`);
- if (response.isSuccess) {
- setPostData(response.result);
- fetchUser(response.result.userId);
- } else {
- console.error('Failed to fetch post data');
- }
- } catch (error) {
- console.error('Error fetching post data:', error);
- }
- };
-
- const fetchUser = async (userId: number) => {
- try {
- const response = await request.get(`/users/${userId}`);
- if (response.isSuccess) {
- setUser(response.result);
- setUserName(response.result.nickname || response.result.name);
- } else {
- console.error('Failed to fetch user data');
- }
- } catch (error) {
- console.error('Error fetching user data:', error);
- }
- };
-
- fetchPostData();
- }, [postId]);
-
- // 코멘트를 보내는 함수
- const sendComment = async (comment: string) => {
- try {
- setIsCommentModalOpen(false);
- const response = await request.post(`/posts/${postId}/comment`, {
- content: comment,
- });
-
- if (response.isSuccess) {
- console.log('Comment sent successfully');
- } else {
- console.error('Failed to send comment:', response.message);
- }
- } catch (error) {
- console.error('Error sending comment:', error);
- }
- };
-
- const bottomSheetMenuProps: BottomSheetMenuProps = {
- items: [
- {
- text: '신고하기',
- action: () => {
- setIsOpenBottomSheet(false);
- setIsOpenReportSheet(true);
- },
- icon: declaration,
- },
- {
- text: '차단하기',
- action: () => {
- setIsOpenBottomSheet(false);
- setIsConfirmationModalOpen(true);
- },
- icon: block,
- },
- ],
- marginBottom: '3.125rem',
- };
+import PostBase from '../../components/PostBase/index.tsx';
+import OptionsBottomSheet from '../../components/BottomSheet/OptionsBottomSheet/index.tsx';
+import { OptionsBottomSheetProps } from '../../components/BottomSheet/OptionsBottomSheet/dto.ts';
- const reportSheetMenuProps: BottomSheetMenuProps = {
- items: [
- {
- text: '스팸',
- action: () => {
- setIsOpenReportSheet(false);
- setIsModalOpen(true);
- },
- },
- {
- text: '부적절한 콘텐츠',
- action: () => {
- setIsOpenReportSheet(false);
- setIsModalOpen(true);
- },
- },
- {
- text: '선정적',
- action: () => {
- setIsOpenReportSheet(false);
- setIsModalOpen(true);
- },
- },
- {
- text: '직접 입력',
- action: () => {
- setShowInput((prev) => !prev);
- },
- },
- ],
- marginBottom: '3.125rem',
- };
-
- const bottomSheetProps: BottomSheetProps = {
- isOpenBottomSheet: isOpenBottomSheet,
- isHandlerVisible: true,
- Component: BottomSheetMenu,
- componentProps: bottomSheetMenuProps,
- onCloseBottomSheet: () => {
- setIsOpenBottomSheet(false);
- setShowInput(false);
- },
- };
-
- const commentProps: CommentProps = {
- content: `${userName}님의 게시물에 대한 코멘트를 남겨주세요.\n코멘트는 ${userName}님에게만 전달됩니다.`,
- sendComment: sendComment, // API 함수 전달
- };
+const Post: React.FC = () => {
+ const [isOptionsBottomSheetOpen, setIsOptionsBottomSheetOpen] = useState(false);
+ const postId = useRecoilValue(postIdAtom);
+ const user = useRecoilValue(userAtom);
- const commentSheetProps: BottomSheetProps = {
- isOpenBottomSheet: isCommentModalOpen,
- isHandlerVisible: true,
- Component: Comment,
- componentProps: commentProps,
- onCloseBottomSheet: () => {
- setIsCommentModalOpen(false);
- },
+ const handleMenuOpen = () => {
+ setIsOptionsBottomSheetOpen(true);
};
- const confirmationModalProps = {
- content: `${userName}님을 정말로 차단하시겠습니까?`,
- isCancelButtonVisible: true,
- confirm: {
- text: '차단하기',
- action: () => {
- setIsConfirmationModalOpen(false);
- setIsBlockedModalOpen(true); // 차단 완료 모달 열기
- },
+ // 게시글 옵션(더보기) 바텀시트
+ const optionsBottomSheetProps: OptionsBottomSheetProps = {
+ domain: 'post',
+ targetId: {
+ userId: user.userId || -1,
+ postId: postId || -1,
},
- onCloseModal: () => {
- setIsConfirmationModalOpen(false);
+ targetNickname: user.nickname || '알수없음',
+ isBottomSheetOpen: isOptionsBottomSheetOpen,
+ onClose: () => {
+ setIsOptionsBottomSheetOpen(false);
},
};
- if (!postData) {
- return ; // 로딩 중 표시
- }
-
return (
-
-
- (
-
-
- {showInput && (
- setIsOpenReportSheet(false)}
- onOpenModal={() => setIsModalOpen(true)}
- />
- )}
-
- )}
- onCloseBottomSheet={() => {
- setIsOpenReportSheet(false);
- setShowInput(false);
- }}
- />
-
- {isModalOpen && setIsModalOpen(false)} />}
- {isConfirmationModalOpen && }
- {isBlockedModalOpen && (
- setIsBlockedModalOpen(false)} />
- )}
+ <>
+
-
-
-
- nav(`/users/${postData.userId}`)}>
-
-
-
-
-
- {userName}
-
-
-
- setIsOpenBottomSheet(true)}>
-
-
-
-
-
- {postData.content}
-
-
-
-
- {postData.photoUrls.map((image: string, index: number) => (
-
-
-
- ))}
-
-
-
- {postData.clothingInfo?.map((clothingInfo: ClothingInfo, index: number) => (
-
- ))}
-
-
-
+
+ >
);
};
diff --git a/src/pages/Post/styles.tsx b/src/pages/Post/styles.tsx
index fe5a3d50..e7119c4a 100644
--- a/src/pages/Post/styles.tsx
+++ b/src/pages/Post/styles.tsx
@@ -1,168 +1,4 @@
import styled from 'styled-components';
-import { StyledText } from '../../components/Text/StyledText';
-
-// PostTopBar
-
-export const PostTopBarContainer = styled.div`
- width: 100%;
- max-width: 32rem;
- height: 2.75rem;
- display: flex;
- justify-content: space-between;
- background-color: #ffffff;
- z-index: 10;
- align-items: center;
- position: fixed;
-`;
-
-export const BackIcon = styled.img`
- width: 0.5625rem;
- height: 1.125rem;
- margin-left: 1.3125rem;
- cursor: pointer;
- overflow: hidden;
-
- img {
- width: 100%;
- height: 100%;
- }
-`;
-
-export const MidWrapper = styled.div`
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- gap: 0.125rem;
- margin-top: 0.25rem;
-`;
-
-export const RightSpace = styled.div`
- width: 0.5625rem;
- height: 1.125rem;
- margin-right: 1.3125rem;
-`;
-
-// Post
-
-export const PostWrapper = styled.div`
- width: 100%;
- height: auto;
-`;
-
-export const PostInfo = styled.div`
- display: flex;
- margin-top: 3.375rem;
- justify-content: space-between;
- align-items: center;
-`;
-
-export const UserInfo = styled.div`
- cursor: pointer;
- display: flex;
- align-items: center;
-`;
-
-export const UserProfile = styled.div`
- width: 2.25rem;
- height: 2.25rem;
- border-radius: 50%;
- overflow: hidden;
- margin: 0 0.75rem 0 1.25rem;
-
- img {
- width: 100%;
- height: 100%;
- object-fit: cover;
- }
-`;
-
-export const UserName = styled.div``;
-
-export const MoreBtn = styled.div`
- width: 1.5rem;
- height: 1.5rem;
- margin-right: 1.25rem;
- cursor: pointer;
-`;
-
-export const PostText = styled.span`
- margin: 0 1.25rem 0.5rem;
- margin-top: 0.75rem;
- margin-bottom: 0.4875rem;
- display: flex;
- align-items: center;
- word-wrap: break-word;
- word-break: break-all;
-`;
-
-export const PostImg = styled.div`
- width: 100%;
- position: relative;
-
- .postSwiper {
- position: relative;
- width: 100%;
- padding-bottom: 133.33%;
- }
-
- .postSwiper .swiper-pagination {
- position: absolute;
- top: 0.75rem;
- left: 50%;
- transform: translateX(-50%);
- z-index: 10;
- pointer-events: none;
- }
-
- .postSwiper .swiper-pagination-bullet {
- width: 0.375rem;
- height: 0.375rem;
- border: 0.0625rem solid ${({ theme }) => theme.colors.white};
- background: rgba(255, 255, 255, 0.5);
- opacity: 1;
- pointer-events: auto;
- }
-
- .postSwiper .swiper-pagination-bullet-active {
- width: 0.375rem;
- height: 0.375rem;
- background-color: ${({ theme }) => theme.colors.white};
- opacity: 1;
- }
-
- .postSwiper .swiper-button-prev:after,
- .postSwiper .swiper-button-next:after {
- text-shadow: 0rem 0rem 0.25rem rgba(0, 0, 0, 0.25);
- color: ${({ theme }) => theme.colors.white} !important;
- font-size: 1.5rem !important;
- }
-
- img {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- aspect-ratio: 3 / 4;
- height: auto;
- object-fit: cover;
- }
-`;
-
-export const ClothingInfos = styled.div`
- margin-top: 0.6687rem;
- margin-left: 1.25rem;
- margin-bottom: 4rem;
- display: flex;
- overflow-x: auto;
- white-space: nowrap;
- scrollbar-width: none; /* Firefox에서 스크롤바 숨기기 */
- -ms-overflow-style: none; /* Internet Explorer에서 스크롤바 숨기기 */
-
- &::-webkit-scrollbar {
- display: none; /* Chrome, Safari, Opera에서 스크롤바 숨기기 */
- }
-`;
export const InputLayout = styled.div`
display: flex;
@@ -190,83 +26,3 @@ export const InputLayout = styled.div`
resize: none;
}
`;
-
-// export const InputWrapper = styled.textarea`
-// display: block;
-// width: calc(100% - 3rem);
-// height: 5.75rem;
-// border-radius: 0.125rem;
-// border: 0.0625rem solid ${({ theme }) => theme.colors.gray3};
-// margin-bottom: 5.875rem;
-// z-index: 2;
-// margin-top: -3.75rem;
-// outline: none;
-// padding: 0.8125rem 0.9375rem;
-// font-family: 'Pretendard Variable';
-// font-size: 1rem;
-// font-style: normal;
-// font-weight: 300;
-// line-height: 150%;
-// color: ${({ theme }) => theme.colors.black};
-// resize: none;
-
-// &::placeholder {
-// color: ${({ theme }) => theme.colors.gray3};
-// }
-// `;
-
-// ClothingInfoCard
-
-export const ClothingInfoCardContainer = styled.div`
- position: relative;
- width: 15.3125rem;
- height: 4.5rem;
- border-radius: 0.1875rem;
- box-shadow: 0 0 0 0.0625rem ${({ theme }) => theme.colors.gray3} inset;
- display: flex;
- align-items: center;
- flex-shrink: 0;
- justify-content: space-between;
- margin-right: 0.75rem;
- cursor: pointer;
-`;
-
-export const ClothingInfoLeft = styled.div`
- display: flex;
- align-items: center;
-`;
-
-export const ClothingInfoImg = styled.img`
- width: 3.5rem;
- height: 3.5rem;
- margin: 0.5rem;
-`;
-
-export const ClothingInfoDetail = styled.div`
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- width: 8rem;
- display: flex;
- flex-direction: column;
-`;
-
-export const StyledTextClipped = styled(StyledText)`
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- width: 100%;
- display: inline-block; /* 텍스트 클리핑을 적용하기 위해 inline-block으로 설정 */
-`;
-
-export const NextBtn = styled.div`
- position: absolute;
- right: 0;
- width: 1.5rem;
- height: 1.5rem;
- margin-right: 0.375rem;
- img {
- width: 100%;
- height: 100%;
- }
-`;
diff --git a/src/pages/Upload/ImageReviewModal/ImageSwiper/index.tsx b/src/pages/PostImageSelect/ImageSwiper/index.tsx
similarity index 58%
rename from src/pages/Upload/ImageReviewModal/ImageSwiper/index.tsx
rename to src/pages/PostImageSelect/ImageSwiper/index.tsx
index 79ae4f64..0a2b09a2 100644
--- a/src/pages/Upload/ImageReviewModal/ImageSwiper/index.tsx
+++ b/src/pages/PostImageSelect/ImageSwiper/index.tsx
@@ -1,39 +1,22 @@
import React, { useRef, useState } from 'react';
import { Swiper, SwiperSlide } from 'swiper/react';
+import { Navigation, Pagination } from 'swiper/modules';
import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';
import { SwiperContainer, ImageWrapper, RemoveButton, StyledNavigation, AddButton, HiddenFileInput } from './styles';
-import remove from '../../../../assets/Upload/remove.svg';
-import plus from '../../../../assets/Upload/plus.svg';
-import { Navigation, Pagination } from 'swiper/modules';
+
+import Reject from '../../../assets/default/reject.svg';
+import Plus from '../../../assets/default/plus.svg';
+
import { ImageSwiperProps } from '../dto';
-const ImageSwiper: React.FC = ({ images, onRemove, onAddImages }) => {
+const ImageSwiper: React.FC = ({ images, onProcessFile, onRemoveImage }) => {
const fileInputRef = useRef(null);
const [currentSlide, setCurrentSlide] = useState(0);
- const handleFileUpload = (event: React.ChangeEvent) => {
- if (event.target.files) {
- const filesArray = Array.from(event.target.files);
- const newImages: string[] = [];
- filesArray.forEach((file) => {
- const reader = new FileReader();
- reader.onloadend = () => {
- if (reader.result) {
- newImages.push(reader.result.toString());
- if (newImages.length === filesArray.length) {
- onAddImages(newImages);
- }
- }
- };
- reader.readAsDataURL(file);
- });
- }
- };
-
- const handleAddImageClick = () => {
+ const handleSelectImage = () => {
if (fileInputRef.current) {
fileInputRef.current.click();
}
@@ -57,20 +40,30 @@ const ImageSwiper: React.FC = ({ images, onRemove, onAddImages
{images.map((image, index) => (
-
+
{images.length > 1 && (
- onRemove(image)}>
-
+ onRemoveImage(image.imageUrl)}>
+
)}
))}
-
-
+
+
-
+ {
+ if (event.target.files) {
+ onProcessFile(event.target.files);
+ }
+ }}
+ ref={fileInputRef}
+ multiple
+ accept="image/*,.heic"
+ />
diff --git a/src/pages/Upload/ImageReviewModal/ImageSwiper/styles.tsx b/src/pages/PostImageSelect/ImageSwiper/styles.tsx
similarity index 80%
rename from src/pages/Upload/ImageReviewModal/ImageSwiper/styles.tsx
rename to src/pages/PostImageSelect/ImageSwiper/styles.tsx
index 631eef9c..f7e511e5 100644
--- a/src/pages/Upload/ImageReviewModal/ImageSwiper/styles.tsx
+++ b/src/pages/PostImageSelect/ImageSwiper/styles.tsx
@@ -16,7 +16,7 @@ export const SwiperContainer = styled.div`
.review-swiper .swiper-slide {
width: 21.875rem;
max-width: calc(100% - 2.5rem);
- aspect-ratio: 3 / 4;
+ aspect-ratio: 4 / 5;
height: auto;
object-fit: cover;
transition: transform 0.3s;
@@ -48,8 +48,9 @@ export const ImageWrapper = styled.div`
height: 100%;
width: auto;
width: 100%;
- aspect-ratio: 3 / 4;
+ aspect-ratio: 4 / 5;
object-fit: cover;
+ border-radius: 8px;
}
`;
@@ -61,21 +62,24 @@ export const StyledNavigation = styled.button`
top: 50%;
transform: translateY(-50%);
color: white;
- width: 4.375rem;
- height: 4.375rem;
- padding: 1.25rem;
+ background-color: rgba(192, 192, 192, 0.5);
+ border-radius: 50%;
+ width: 40px;
+ height: 40px;
&::after {
- font-size: 1.25rem;
+ font-size: 1rem;
color: white;
}
&.swiper-button-prev {
- margin: 0 0 0 max(calc((100% - 21.875rem) / 2 - 1.25rem), 0rem);
+ margin: 0 0 0 max(calc((100% - 19.875rem) / 2 - 1.25rem), 0rem);
+ padding-right: 3px;
}
&.swiper-button-next {
- margin: 0 max(calc((100% - 21.875rem) / 2) - 1.25rem, 0rem) 0 0;
+ margin: 0 max(calc((100% - 19.875rem) / 2) - 1.25rem, 0rem) 0 0;
+ padding-left: 3px;
}
`;
@@ -94,16 +98,9 @@ export const AddButton = styled.button`
align-items: center;
justify-content: center;
background: none;
- color: #999;
- font-size: 1.875rem;
- width: 6.25rem;
- height: 6.25rem;
+ width: 80px;
+ height: 80px;
margin: auto;
-
- &:hover {
- border-color: #666;
- color: #666;
- }
`;
export const HiddenFileInput = styled.input`
diff --git a/src/pages/PostImageSelect/dto.ts b/src/pages/PostImageSelect/dto.ts
new file mode 100644
index 00000000..15dd65bf
--- /dev/null
+++ b/src/pages/PostImageSelect/dto.ts
@@ -0,0 +1,8 @@
+export interface ImageSelectModalProps {}
+import { PostImage } from '../../apis/post/dto';
+
+export interface ImageSwiperProps {
+ images: PostImage[];
+ onProcessFile: (files: FileList) => void;
+ onRemoveImage: (image: string) => void;
+}
diff --git a/src/pages/PostImageSelect/index.tsx b/src/pages/PostImageSelect/index.tsx
new file mode 100644
index 00000000..41495da4
--- /dev/null
+++ b/src/pages/PostImageSelect/index.tsx
@@ -0,0 +1,175 @@
+import React, { useState, useRef } from 'react';
+import { useNavigate, useLocation } from 'react-router-dom';
+
+import { useRecoilState } from 'recoil';
+import {
+ postImagesAtom,
+ postContentAtom,
+ postClothingInfosAtom,
+ postStyletagAtom,
+ postIsRepresentativeAtom,
+} from '../../recoil/PostUpload/PostUploadAtom';
+
+import { UploadContainer, ImageDragDropContainer, Content } from './styles';
+
+import { OODDFrame } from '../../components/Frame/Frame';
+import { StyledText } from '../../components/Text/StyledText';
+import TopBar from '../../components/TopBar';
+import BottomButton from '../../components/BottomButton';
+import ImageSwiper from './ImageSwiper';
+
+import X from '../../assets/default/x.svg';
+import Left from '../../assets/arrow/left.svg';
+import PhotoBig from '../../assets/default/photo-big.svg';
+
+import { ImageSelectModalProps } from './dto';
+import heic2any from 'heic2any';
+
+const PostImageSelect: React.FC = () => {
+ const [images, setImages] = useRecoilState(postImagesAtom);
+ const [, setContent] = useRecoilState(postContentAtom);
+ const [, setClothingInfos] = useRecoilState(postClothingInfosAtom);
+ const [, setStyletag] = useRecoilState(postStyletagAtom);
+ const [, setIsRepresentative] = useRecoilState(postIsRepresentativeAtom);
+ const [isActive, setActive] = useState(false);
+ const fileInputRef = useRef(null);
+ const location = useLocation();
+ const navigate = useNavigate();
+
+ const handleClose = () => {
+ navigate('/mypage');
+ };
+
+ const handlePrev = () => {
+ setImages([]);
+ setContent('');
+ setClothingInfos([]);
+ setStyletag([]);
+ setIsRepresentative(false);
+ };
+
+ const handleNext = () => {
+ const state = location.state as { mode?: string; postId?: number };
+ navigate('/upload', { state: { mode: state?.mode, postId: state?.postId } });
+ };
+
+ // 파일 선택기에서 사진 업로드
+ const handleSelectImage = () => {
+ if (fileInputRef.current) {
+ fileInputRef.current.click();
+ }
+ };
+
+ // 드래그 앤 드롭으로 사진 업로드
+ const handleDragEnter = () => setActive(true);
+ const handleDragLeave = () => setActive(false);
+ const handleDragOver = (event: React.DragEvent) => {
+ event.preventDefault(); // 파일을 드래그했을 때, 브라우저 기본 동작에 의해 새 창이 뜨는 것 방지
+ };
+
+ const handleDrop = (event: React.DragEvent) => {
+ event.preventDefault(); // 파일을 드롭했을 때, 브라우저 기본 동작에 의해 새 창이 뜨는 것 방지
+
+ setActive(false);
+
+ if (event.dataTransfer.files) {
+ handleProcessFile(event.dataTransfer.files);
+ }
+ };
+
+ const handleFileInputChange = (event: React.ChangeEvent) => {
+ event.preventDefault();
+ if (event.target.files) {
+ handleProcessFile(event.target.files);
+ // 파일 선택 후 input 값 초기화
+ if (fileInputRef.current) {
+ fileInputRef.current.value = ''; // input 값을 초기화하여 동일한 파일을 다시 추가할 수 있도록 함
+ }
+ }
+ };
+
+ const handleProcessFile = async (files: FileList) => {
+ const filesArray = Array.from(files);
+ for (const file of filesArray) {
+ try {
+ let fileBlob = file;
+
+ // HEIC 파일인 경우 변환
+ if (/\.(heic)$/i.test(fileBlob.name)) {
+ const convertedBlob = await heic2any({ blob: fileBlob, toType: 'image/jpeg' });
+
+ // Blob을 File로 변환
+ const newFile = new File([convertedBlob as Blob], fileBlob.name.replace(/\.heic$/i, '.jpeg'), {
+ type: 'image/jpeg',
+ lastModified: new Date().getTime(),
+ });
+ fileBlob = newFile; // 변환된 파일을 다시 fileBlob으로 할당
+ }
+
+ const reader = new FileReader();
+ reader.readAsDataURL(fileBlob);
+ reader.onload = () => {
+ if (reader.result) {
+ handleAddImage(reader.result.toString());
+ }
+ };
+ } catch (error) {
+ alert('이미지 처리 중 오류가 발생했습니다.');
+ console.error(error);
+ }
+ }
+ };
+
+ const handleAddImage = (newImage: string) => {
+ setImages((prevImages) => {
+ const maxOrderNum = prevImages.reduce((max, img) => (img.orderNum > max ? img.orderNum : max), -1);
+ return [...prevImages, { imageUrl: newImage, orderNum: maxOrderNum + 1 }];
+ });
+ };
+
+ const handleRemoveImage = (image: string) => {
+ // 이미지가 1개일 때는 삭제 할 수 없음
+ if (images.length > 1) {
+ const newImages = images.filter((img) => img.imageUrl !== image);
+ setImages(newImages.map((img, idx) => ({ ...img, orderNum: idx })));
+ }
+ };
+
+ return (
+
+
+
+
+ {images.length === 0 ? (
+
+
+ 사진을 여기에 끌어다 놓으세요
+
+
+
+
+ ) : (
+
+ )}
+
+
+
+
+
+ );
+};
+
+export default PostImageSelect;
diff --git a/src/pages/PostImageSelect/styles.tsx b/src/pages/PostImageSelect/styles.tsx
new file mode 100644
index 00000000..2fbe61c2
--- /dev/null
+++ b/src/pages/PostImageSelect/styles.tsx
@@ -0,0 +1,59 @@
+import styled from 'styled-components';
+
+export const UploadContainer = styled.div`
+ flex-grow: 1;
+ height: 100vh;
+ width: 100%;
+ position: relative;
+`;
+
+export const ImageDragDropContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ top: 2.75rem;
+ left: 0;
+
+ div {
+ margin-bottom: 4.5rem;
+ }
+
+ svg {
+ z-index: 2;
+ }
+
+ :nth-child(2) {
+ margin-bottom: 9rem;
+ }
+
+ &.active svg {
+ color: ${({ theme }) => theme.colors.black};
+ }
+
+ input {
+ display: none;
+ }
+`;
+
+export const Content = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ position: absolute;
+ top: 2.75rem;
+ left: 0;
+ width: 100%;
+ //max-width: 512px;
+ height: calc(100% - 10rem);
+ flex: 1;
+`;
+
+export const HiddenFileInput = styled.input`
+ display: none;
+`;
diff --git a/src/pages/PostInstaConnect/dto.ts b/src/pages/PostInstaConnect/dto.ts
new file mode 100644
index 00000000..2a10b46e
--- /dev/null
+++ b/src/pages/PostInstaConnect/dto.ts
@@ -0,0 +1,6 @@
+export interface InstaConnectModalProps {}
+
+export interface Post {
+ imgs: string[];
+ caption: string;
+}
diff --git a/src/pages/PostInstaConnect/index.tsx b/src/pages/PostInstaConnect/index.tsx
new file mode 100644
index 00000000..27c09fc4
--- /dev/null
+++ b/src/pages/PostInstaConnect/index.tsx
@@ -0,0 +1,93 @@
+import React, { useState } from 'react';
+
+import theme from '../../styles/theme';
+import { Content, StyledInput } from './styles';
+
+import { OODDFrame } from '../../components/Frame/Frame';
+import { StyledText } from '../../components/Text/StyledText';
+import TopBar from '../../components/TopBar';
+import BottomButton from '../../components/BottomButton';
+import Modal from '../../components/Modal';
+import { ModalProps } from '../../components/Modal/dto';
+
+import X from '../../assets/default/x.svg';
+
+import { InstaConnectModalProps } from './dto';
+
+const PostInstaConnect: React.FC = () => {
+ const [instagramID, setInstagramID] = useState('');
+ const [isConnectFailModalOpen, setIsConnectFailModalOpen] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleConnect = async () => {
+ try {
+ setIsLoading(true);
+ window.location.href = 'https://localhost:3001/auth'; //인스타그램 인증 처리
+ } catch (error) {
+ console.error('Failed to fetch Instagram media:', error);
+ setIsLoading(false);
+ setIsConnectFailModalOpen(true);
+ }
+ };
+
+ const connectFailModalProps: ModalProps = {
+ isCloseButtonVisible: false,
+ onClose: () => setIsConnectFailModalOpen(false),
+ content: `${instagramID} 계정 연동에 실패했어요`,
+ button: {
+ content: '다시 시도하기',
+ onClick: () => {
+ setIsConnectFailModalOpen(false);
+ handleConnect();
+ },
+ },
+ };
+
+ /*
+ useEffect(() => {
+ if (accessToken) {
+ fetchInstagramData(accessToken);
+ }
+ }, [accessToken]);
+ */
+
+ return (
+
+
+
+
+ {isLoading ? (
+
+ {instagramID} 계정에 연동하고 있어요
+
+ ) : (
+ <>
+ 인스타 계정 연동을 위해
+
+ 인스타그램 ID를 작성해주세요
+
+ setInstagramID(e.target.value)}
+ placeholder="인스타그램 ID"
+ />
+
+ {!instagramID ? '탭해서 ID를 작성하세요' : ' .'}
+
+ >
+ )}
+
+
+
+
+ {isConnectFailModalOpen && }
+
+ );
+};
+
+export default PostInstaConnect;
diff --git a/src/pages/Upload/InstaConnectModal/styles.tsx b/src/pages/PostInstaConnect/styles.tsx
similarity index 67%
rename from src/pages/Upload/InstaConnectModal/styles.tsx
rename to src/pages/PostInstaConnect/styles.tsx
index 2359b2d3..5d2cc0d4 100644
--- a/src/pages/Upload/InstaConnectModal/styles.tsx
+++ b/src/pages/PostInstaConnect/styles.tsx
@@ -1,5 +1,12 @@
import styled from 'styled-components';
+export const UploadContainer = styled.div`
+ flex-grow: 1;
+ height: 100vh;
+ width: 100%;
+ position: relative;
+`;
+
export const Content = styled.div`
display: flex;
flex-direction: column;
@@ -22,51 +29,40 @@ export const StyledInput = styled.input`
padding: 0;
margin: 6.25rem 0 0.9375rem 0;
border: none;
- font-family: 'Gmarket Sans';
- font-weight: 400;
- font-size: 2rem;
text-align: center;
+ ${({ theme }) => theme.fontStyles['title1-regular']}
+
&:focus {
outline: none;
}
::placeholder {
color: ${({ theme }) => theme.colors.gray3};
- font-family: 'Gmarket Sans';
- font-weight: 400;
- font-size: 2rem;
+ ${({ theme }) => theme.fontStyles['title1-regular']}
}
/* Firefox */
&:-moz-placeholder {
color: ${({ theme }) => theme.colors.gray3};
- font-family: 'Gmarket Sans';
- font-weight: 400;
- font-size: 2rem;
+ ${({ theme }) => theme.fontStyles['title1-regular']}
}
/* Internet Explorer 10-11 */
&:-ms-input-placeholder {
color: ${({ theme }) => theme.colors.gray3};
- font-family: 'Gmarket Sans';
- font-weight: 400;
- font-size: 2rem;
+ ${({ theme }) => theme.fontStyles['title1-regular']}
}
/* Edge */
&::-ms-input-placeholder {
color: ${({ theme }) => theme.colors.gray3};
- font-family: 'Gmarket Sans';
- font-weight: 400;
- font-size: 2rem;
+ ${({ theme }) => theme.fontStyles['title1-regular']}
}
/* Safari */
&::placeholder {
color: ${({ theme }) => theme.colors.gray3};
- font-family: 'Gmarket Sans';
- font-weight: 400;
- font-size: 2rem;
+ ${({ theme }) => theme.fontStyles['title1-regular']}
}
`;
diff --git a/src/pages/PostInstaFeedSelect/dto.ts b/src/pages/PostInstaFeedSelect/dto.ts
new file mode 100644
index 00000000..82dee836
--- /dev/null
+++ b/src/pages/PostInstaFeedSelect/dto.ts
@@ -0,0 +1,5 @@
+export interface InstaFeedSelectModalProps {}
+
+export interface Post {
+ imgs: string[];
+}
diff --git a/src/pages/PostInstaFeedSelect/index.tsx b/src/pages/PostInstaFeedSelect/index.tsx
new file mode 100644
index 00000000..fa422c52
--- /dev/null
+++ b/src/pages/PostInstaFeedSelect/index.tsx
@@ -0,0 +1,94 @@
+import React, { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import axios from 'axios';
+import { useRecoilState } from 'recoil';
+import { postImagesAtom } from '../../recoil/PostUpload/PostUploadAtom';
+
+import { Content, PostContainer, ImageWrapper } from './styles';
+
+import { OODDFrame } from '../../components/Frame/Frame';
+import TopBar from '../../components/TopBar';
+import Modal from '../../components/Modal';
+import { ModalProps } from '../../components/Modal/dto';
+
+import X from '../../assets/default/x.svg';
+
+import { InstaFeedSelectModalProps, Post } from './dto';
+
+const PostInstaFeedSelect: React.FC = () => {
+ const [isSuccessModalOpen, setIsSuccessModalOpen] = useState(true);
+ const [, setIsLoading] = useState(false);
+ const [isFailModalOpen, setIsFailModalOpen] = useState(false);
+ const [posts, setPosts] = useState([]); // Post 타입으로 지정
+ const [, setImages] = useRecoilState(postImagesAtom);
+ const navigate = useNavigate();
+
+ // 인스타그램 데이터 가져오는 함수
+ const fetchInstagramData = async (accessToken: string) => {
+ try {
+ setIsLoading(true);
+ const response = await axios.get('https://localhost:3001/instagram-import', {
+ params: { access_token: accessToken },
+ });
+ setPosts(response.data as Post[]); // Post 타입으로 받아옴
+ } catch (error) {
+ console.error('Failed to fetch Instagram media:', error);
+ setIsFailModalOpen(true); // 실패 모달 열기
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ // 연동 실패 모달 속성
+ const connectFailModalProps: ModalProps = {
+ isCloseButtonVisible: false,
+ content: `계정 연동에 실패했어요`,
+ onClose: () => setIsFailModalOpen(false),
+ button: {
+ content: '다시 시도하기',
+ onClick: () => {
+ setIsFailModalOpen(false);
+ fetchInstagramData('accessToken'); // 함수 호출 시 실행되도록 수정
+ },
+ },
+ };
+
+ // 연동 성공 모달 속성
+ const connectSuccessModalProps: ModalProps = {
+ content: `계정 연동에 성공했어요!\n가져올 OOTD를 선택해 보세요`,
+ onClose: () => {
+ setIsSuccessModalOpen(false);
+ },
+ };
+
+ // 이미지 선택 시 실행
+ const handlePostSelect = (post: Post) => {
+ const newImages = post.imgs.map((imageUrl, index) => ({ imageUrl, orderNum: index }));
+ setImages(newImages); // 선택한 이미지 Recoil 상태로 설정
+ navigate('/upload'); // 다음 페이지로 이동
+ };
+
+ // 페이지 종료 함수
+ const handleClose = () => {
+ navigate('/mypage'); // 마이페이지로 이동
+ };
+
+ return (
+
+ {isSuccessModalOpen && }
+ {isFailModalOpen && }
+ {' '}
+
+ {posts.map((post, index) => (
+ handlePostSelect(post)}>
+
+
{/* alt 추가 */}
+
+
+ ))}
+
+
+ );
+};
+
+export default PostInstaFeedSelect;
diff --git a/src/pages/Upload/InstaFeedSelectModal/styles.tsx b/src/pages/PostInstaFeedSelect/styles.tsx
similarity index 89%
rename from src/pages/Upload/InstaFeedSelectModal/styles.tsx
rename to src/pages/PostInstaFeedSelect/styles.tsx
index 25733a1f..295bd824 100644
--- a/src/pages/Upload/InstaFeedSelectModal/styles.tsx
+++ b/src/pages/PostInstaFeedSelect/styles.tsx
@@ -1,5 +1,12 @@
import styled from 'styled-components';
+export const UploadContainer = styled.div`
+ flex-grow: 1;
+ height: 100vh;
+ width: 100%;
+ position: relative;
+`;
+
export const Content = styled.div`
display: grid;
grid-template-columns: repeat(auto-fit, minmax(6.25rem, 1fr));
diff --git a/src/pages/Upload/PostUploadModal/ImageSwiper/index.tsx b/src/pages/PostUpload/ImageSwiper/index.tsx
similarity index 91%
rename from src/pages/Upload/PostUploadModal/ImageSwiper/index.tsx
rename to src/pages/PostUpload/ImageSwiper/index.tsx
index 3ed8fe2d..aaa0ef0a 100644
--- a/src/pages/Upload/PostUploadModal/ImageSwiper/index.tsx
+++ b/src/pages/PostUpload/ImageSwiper/index.tsx
@@ -1,11 +1,14 @@
import React, { useRef } from 'react';
import { Swiper, SwiperRef, SwiperSlide } from 'swiper/react';
+import { Navigation, Pagination } from 'swiper/modules';
import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';
+
import { SwiperContainer, ImageWrapper, StyledNavigation, StyledPagination } from './styles';
-import picture2 from '../../../../assets/Upload/picture2.svg';
-import { Navigation, Pagination } from 'swiper/modules';
+
+import PhotoWhite from '../../../assets/default/photo-white.svg';
+
import { ImageSwiperProps } from '../dto';
const ImageSwiper: React.FC = ({ images }) => {
@@ -29,7 +32,7 @@ const ImageSwiper: React.FC = ({ images }) => {
renderCustom: (_, current, total) => {
return `
`;
},
@@ -51,7 +54,7 @@ const ImageSwiper: React.FC = ({ images }) => {
{images.map((image, index) => (
-
+
))}
diff --git a/src/pages/Upload/PostUploadModal/ImageSwiper/styles.tsx b/src/pages/PostUpload/ImageSwiper/styles.tsx
similarity index 91%
rename from src/pages/Upload/PostUploadModal/ImageSwiper/styles.tsx
rename to src/pages/PostUpload/ImageSwiper/styles.tsx
index 279d174e..be380107 100644
--- a/src/pages/Upload/PostUploadModal/ImageSwiper/styles.tsx
+++ b/src/pages/PostUpload/ImageSwiper/styles.tsx
@@ -18,7 +18,7 @@ export const SwiperContainer = styled.div`
}
.upload-swiper .swiper-slide {
- width: 15.4375rem;
+ width: 16.45rem;
height: 20.5625rem;
object-fit: cover;
transition: transform 0.3s;
@@ -39,8 +39,9 @@ export const ImageWrapper = styled.div`
height: 100%;
img {
+ border-radius: 8px;
height: 100%;
- aspect-ratio: 3 / 4;
+ aspect-ratio: 4 / 5;
object-fit: cover;
}
`;
@@ -79,11 +80,11 @@ export const StyledPagination = styled.div`
display: flex;
justify-content: center;
align-items: center;
- width: 3.75rem;
- height: 1.5rem;
+ width: 65px;
+ height: 34px;
color: white;
- background: ${({ theme }) => theme.colors.black};
- border-radius: 0.75rem;
+ background: ${({ theme }) => theme.colors.gradient};
+ border-radius: 17px;
.swiper-pagination-custom {
display: flex;
diff --git a/src/pages/Upload/PostUploadModal/SearchBottomSheetContent/index.tsx b/src/pages/PostUpload/SearchBottomSheetContent/index.tsx
similarity index 93%
rename from src/pages/Upload/PostUploadModal/SearchBottomSheetContent/index.tsx
rename to src/pages/PostUpload/SearchBottomSheetContent/index.tsx
index 5715f81c..b3c1b0bc 100644
--- a/src/pages/Upload/PostUploadModal/SearchBottomSheetContent/index.tsx
+++ b/src/pages/PostUpload/SearchBottomSheetContent/index.tsx
@@ -1,8 +1,11 @@
import React, { useState, useEffect, useRef } from 'react';
import axios from 'axios';
+
import { Content, Input, SearchResultList, SearchResultItem, Loader } from './styles';
-import { StyledText } from '../../../../components/Text/StyledText';
-import theme from '../../../../styles/theme';
+import theme from '../../../styles/theme';
+
+import { StyledText } from '../../../components/Text/StyledText';
+
import { SearchBottomSheetProps } from '../dto';
const SearchBottomSheetContent: React.FC = ({ onClose, onSelectClothingInfo }) => {
@@ -115,9 +118,9 @@ const SearchBottomSheetContent: React.FC = ({ onClose, o
const handleAddClothingInfo = (item: any) => {
onSelectClothingInfo({
imageUrl: item.image,
- brand: item.brand,
- model: removeBrandFromTitle(item.title, item.brand), //검색 결과에서 태그 제거하고 텍스트만 표시
- modelNumber: 1,
+ brandName: item.brand,
+ modelName: removeBrandFromTitle(item.title, item.brand), //검색 결과에서 태그 제거하고 텍스트만 표시
+ modelNumber: '1',
url: item.link,
});
onClose();
@@ -151,7 +154,7 @@ const SearchBottomSheetContent: React.FC = ({ onClose, o
value={searchQuery}
onChange={(e) => handleInputChange(e.target.value)}
/>
-
+
취소
@@ -161,12 +164,12 @@ const SearchBottomSheetContent: React.FC