- {matching.requesterPost.styleTags.map((tag, index) => (
+ {requester.representativePost.styleTags.map((tag, index) => (
-
+
{tag}
- {index < matching.requesterPost.styleTags.length - 1 && (
-
+ {index < requester.representativePost.styleTags.length - 1 && (
+
,
)}
@@ -112,8 +117,8 @@ const Card: React.FC = ({ removeRejectedMatching, matching }) => {
))}
-
nav(`/users/${requester.requesterId}`)}>
-
+ nav(`/profile/${requester.id}`)}>
+
OOTD 더 보기
@@ -130,7 +135,7 @@ const Card: React.FC = ({ removeRejectedMatching, matching }) => {
modules={[Pagination]}
className="childSwiper"
>
- {matching.requesterPost.postImages.map((postImage) => (
+ {requester.representativePost.postImages.map((postImage) => (
@@ -147,6 +152,7 @@ const Card: React.FC = ({ removeRejectedMatching, matching }) => {
+ {isStatusModalOpen && }
);
};
diff --git a/src/pages/Chats/Request/Cards/Card/styles.tsx b/src/pages/Chats/Matching/Cards/Card/styles.tsx
similarity index 92%
rename from src/pages/Chats/Request/Cards/Card/styles.tsx
rename to src/pages/Chats/Matching/Cards/Card/styles.tsx
index e97c4316..3d345859 100644
--- a/src/pages/Chats/Request/Cards/Card/styles.tsx
+++ b/src/pages/Chats/Matching/Cards/Card/styles.tsx
@@ -1,8 +1,9 @@
-import styled from 'styled-components';
-import ArrowIcon from '../../../../../assets/arrow/min-right.svg';
+import { styled } from 'styled-components';
+
+import ArrowIcon from '@assets/arrow/min-right.svg';
export const CardLayout = styled.div`
- background-color: #ececec;
+ background-color: ${({ theme }) => theme.colors.background.divider};
border-radius: 0.5rem;
position: relative;
height: 100%;
@@ -79,7 +80,6 @@ export const OOTDImgBox = styled.div`
.childSwiper {
z-index: 10;
- // align-items: stretch;
width: 100%;
height: 100%;
}
@@ -109,7 +109,7 @@ export const OOTDImgBox = styled.div`
.childSwiper .swiper-pagination-bullet-active {
width: 0.375rem;
height: 0.375rem;
- background-color: ${({ theme }) => theme.colors.white};
+ background-color: ${({ theme }) => theme.colors.background.primary};
opacity: 1;
}
diff --git a/src/pages/Chats/Request/Cards/dto.ts b/src/pages/Chats/Matching/Cards/dto.ts
similarity index 100%
rename from src/pages/Chats/Request/Cards/dto.ts
rename to src/pages/Chats/Matching/Cards/dto.ts
diff --git a/src/pages/Chats/Request/Cards/index.tsx b/src/pages/Chats/Matching/Cards/index.tsx
similarity index 81%
rename from src/pages/Chats/Request/Cards/index.tsx
rename to src/pages/Chats/Matching/Cards/index.tsx
index df837c52..a5648bec 100644
--- a/src/pages/Chats/Request/Cards/index.tsx
+++ b/src/pages/Chats/Matching/Cards/index.tsx
@@ -1,28 +1,23 @@
-import React, { useEffect, useRef, useState } from 'react';
-import { Swiper, SwiperRef, SwiperSlide } from 'swiper/react';
+import { useEffect, useRef, useState } from 'react';
+
import { Pagination } from 'swiper/modules';
+import { Swiper, SwiperRef, SwiperSlide } from 'swiper/react';
+
import 'swiper/css';
import 'swiper/css/pagination';
-import { CardsContainer } from './styles';
-import Card from './Card';
-import { getMatchingListApi } from '../../../../apis/matching/index.ts';
-import { MatchingDto } from '../../../../apis/matching/dto.ts';
-import { CardsProps } from './dto.ts';
+import { getMatchingListApi } from '@apis/matching';
-const Cards: React.FC = ({ decreaseMatchingCount }) => {
- const swiperRef = useRef(null);
- const [matchings, setMatchings] = useState([]);
+import type { MatchingDto } from '@apis/matching/dto';
- useEffect(() => {
- getMatchingList();
- }, []);
+import type { CardsProps } from './dto';
- // 매칭 리스트 조회
- const getMatchingList = async () => {
- const response = await getMatchingListApi();
+import Card from './Card/index';
- setMatchings(response.data.matching);
- };
+import { CardsContainer } from './styles';
+
+const Cards: React.FC = ({ decreaseMatchingCount }) => {
+ const [matchings, setMatchings] = useState([]);
+ const swiperRef = useRef(null);
// 매칭 요청 거절 시 거절한 요청을 제거하는 함수
const removeRejectedMatching = (index: number) => {
@@ -36,6 +31,17 @@ const Cards: React.FC = ({ decreaseMatchingCount }) => {
}
};
+ // 매칭 리스트 조회 api
+ const getMatchingList = async () => {
+ const response = await getMatchingListApi();
+
+ setMatchings(response.data.matching);
+ };
+
+ useEffect(() => {
+ getMatchingList();
+ }, []);
+
return (
= ({ decreaseMatchingCount }) => {
className="parentSwiper"
>
{matchings.map((matching, index) => (
-
+
removeRejectedMatching(index)}
diff --git a/src/pages/Chats/Request/Cards/styles.tsx b/src/pages/Chats/Matching/Cards/styles.tsx
similarity index 91%
rename from src/pages/Chats/Request/Cards/styles.tsx
rename to src/pages/Chats/Matching/Cards/styles.tsx
index ff912438..7b4fe6c7 100644
--- a/src/pages/Chats/Request/Cards/styles.tsx
+++ b/src/pages/Chats/Matching/Cards/styles.tsx
@@ -1,4 +1,4 @@
-import styled from 'styled-components';
+import { styled } from 'styled-components';
export const CardsContainer = styled.div`
display: flex;
diff --git a/src/pages/Chats/Request/dto.ts b/src/pages/Chats/Matching/dto.ts
similarity index 65%
rename from src/pages/Chats/Request/dto.ts
rename to src/pages/Chats/Matching/dto.ts
index 16a852d5..77f292c7 100644
--- a/src/pages/Chats/Request/dto.ts
+++ b/src/pages/Chats/Matching/dto.ts
@@ -1,4 +1,4 @@
-export interface RequestProps {
+export interface MatchingProps {
matchingCount: number;
decreaseMatchingCount: () => void;
}
diff --git a/src/pages/Chats/Matching/index.tsx b/src/pages/Chats/Matching/index.tsx
new file mode 100644
index 00000000..b3822165
--- /dev/null
+++ b/src/pages/Chats/Matching/index.tsx
@@ -0,0 +1,27 @@
+import { memo } from 'react';
+
+import theme from '@styles/theme';
+
+import { StyledText } from '@components/Text/StyledText';
+
+import type { MatchingProps } from './dto';
+
+import Cards from './Cards/index';
+
+import { ReqeustInfo } from './styles';
+
+const Matching: React.FC = ({ matchingCount, decreaseMatchingCount }) => {
+ return (
+ <>
+
+ Message
+
+ {matchingCount}
+
+
+
+ >
+ );
+};
+
+export default memo(Matching);
diff --git a/src/pages/Chats/Request/styles.tsx b/src/pages/Chats/Matching/styles.tsx
similarity index 55%
rename from src/pages/Chats/Request/styles.tsx
rename to src/pages/Chats/Matching/styles.tsx
index 5282c6d3..57375bbf 100644
--- a/src/pages/Chats/Request/styles.tsx
+++ b/src/pages/Chats/Matching/styles.tsx
@@ -1,5 +1,6 @@
-import styled from 'styled-components';
-import { StyledText } from '../../../components/Text/StyledText';
+import { styled } from 'styled-components';
+
+import { StyledText } from '@components/Text/StyledText';
export const ReqeustInfo = styled(StyledText)`
display: flex;
diff --git a/src/pages/Chats/RecentChat/index.tsx b/src/pages/Chats/RecentChat/index.tsx
index e29da52f..b0de9f2b 100644
--- a/src/pages/Chats/RecentChat/index.tsx
+++ b/src/pages/Chats/RecentChat/index.tsx
@@ -1,11 +1,20 @@
-import { ChatRoomList, NoChatRoomWrapper, RecentChatInfo } from './styles';
-import React, { useEffect, useState } from 'react';
+import { useEffect, useState } from 'react';
+
import SwiperCore from 'swiper';
-import Loading from '../../../components/Loading';
-import ChatRoomItem from '../ChatRoomItem';
-import { StyledText } from '../../../components/Text/StyledText';
-import { useSocket } from '../../../context/SocketProvider';
-import { ChatRoomData } from '../../../apis/chatting/dto';
+
+import theme from '@styles/theme';
+
+import { useSocket } from '@context/SocketProvider';
+import { getCurrentUserId } from '@utils/getCurrentUserId';
+
+import Loading from '@components/Loading';
+import { StyledText } from '@components/Text/StyledText';
+
+import type { ChatRoomData } from '@apis/chatting/dto';
+
+import ChatRoomItem from '../ChatRoomItem/index';
+
+import { ChatRoomList, NoChatRoomWrapper, RecentChatInfo } from './styles';
interface RecentChatProps {
matchingCount: number;
@@ -14,10 +23,9 @@ interface RecentChatProps {
const RecentChat: React.FC = () => {
const [chatRoomList, setChatRoomList] = useState([]);
- const storageValue = localStorage.getItem('my_id');
- const userId = Number(storageValue);
const [isLoading, setIsLoading] = useState(true);
const socket = useSocket();
+ const currentUserId = getCurrentUserId();
useEffect(() => {
// 채팅방 리스트 조회
@@ -27,7 +35,7 @@ const RecentChat: React.FC = () => {
};
if (socket) {
- socket.emit('getChatRooms', { userId: userId });
+ socket.emit('getChatRooms', { userId: currentUserId });
socket.on('chatRoomList', getChatRooms);
}
@@ -46,18 +54,18 @@ const RecentChat: React.FC = () => {
) : chatRoomList.length !== 0 ? (
<>
-
+
최근 채팅방
- {chatRoomList.map((room) => (
-
+ {chatRoomList.map((chatRoom) => (
+
))}
>
) : (
-
+
개설된 채팅방이 없어요.
diff --git a/src/pages/Chats/RecentChat/styles.tsx b/src/pages/Chats/RecentChat/styles.tsx
index dbb17905..7de1cfe6 100644
--- a/src/pages/Chats/RecentChat/styles.tsx
+++ b/src/pages/Chats/RecentChat/styles.tsx
@@ -1,5 +1,6 @@
-import styled from 'styled-components';
-import { StyledText } from '../../../components/Text/StyledText';
+import { styled } from 'styled-components';
+
+import { StyledText } from '@components/Text/StyledText';
export const RecentChatInfo = styled(StyledText)`
padding: 0.5rem 1.25rem;
diff --git a/src/pages/Chats/Request/index.tsx b/src/pages/Chats/Request/index.tsx
deleted file mode 100644
index a7e7c3ca..00000000
--- a/src/pages/Chats/Request/index.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import React from 'react';
-import Cards from './Cards';
-import theme from '../../../styles/theme';
-import { ReqeustInfo } from './styles';
-import { StyledText } from '../../../components/Text/StyledText';
-import { RequestProps } from './dto';
-
-const Request: React.FC = ({ matchingCount, decreaseMatchingCount }) => {
- return (
- <>
-
- Message
-
- {matchingCount}
-
-
-
- >
- );
-};
-
-export default React.memo(Request);
diff --git a/src/pages/Chats/TabBar/index.tsx b/src/pages/Chats/TabBar/index.tsx
index d04b615f..a48c9607 100644
--- a/src/pages/Chats/TabBar/index.tsx
+++ b/src/pages/Chats/TabBar/index.tsx
@@ -1,40 +1,38 @@
-import React, { useCallback, useEffect, useRef, useState } from 'react';
-import { StyledText } from '../../../components/Text/StyledText';
-import { TabBarLayout, TabBarContainer, TabBarWrapper, TabBarList, Tabs } from './styles';
-import Request from '../Request';
-import RecentChat from '../RecentChat';
+import { memo, useCallback, useEffect, useRef, useState } from 'react';
-import { Swiper, SwiperSlide } from 'swiper/react';
import SwiperCore from 'swiper';
+import { Swiper, SwiperSlide } from 'swiper/react';
+
+import theme from '@styles/theme';
+
+import { getMatchingListApi } from '@apis/matching';
+
+import { StyledText } from '@components/Text/StyledText';
+
+import Matching from '../Matching/index';
+import RecentChat from '../RecentChat/index';
+
+import { TabBarLayout, TabBarContainer, TabBarWrapper, TabBarList, Tabs } from './styles';
+
import 'swiper/css';
-import { getMatchingListApi } from '../../../apis/matching';
const TabBar: React.FC = () => {
const [matchingCount, setMatchingCount] = useState(0);
const [hasMatchingRequest, setHasMatchingRequest] = useState(false);
- const [activeIndex, setActiveIndex] = useState(1);
+ const [activeIndex, setActiveIndex] = useState(1);
const swiperRef = useRef(null);
const tabs = [`요청 ${activeIndex === 1 ? matchingCount : ''}`, '최근 채팅'];
- useEffect(() => {
- // 첫 탭을 최근 채팅으로 설정
- if (swiperRef.current) {
- swiperRef.current.slideTo(1, 0);
- }
-
- getMatchingList();
- }, []);
-
- // 매칭 리스트 조회
- const getMatchingList = async () => {
- const response = await getMatchingListApi();
-
- if (response.isSuccess) {
- setMatchingCount(response.data.matchingsCount);
- setHasMatchingRequest(response.data.isMatching);
+ // request 컴포넌트에서 매칭 거절 시 matchingCount 감소
+ const decreaseMatchingCount = useCallback(() => {
+ if (matchingCount !== 1) {
+ setMatchingCount((prev) => Math.max(0, prev - 1));
+ } else {
+ setHasMatchingRequest(false);
+ swiperRef.current?.slideNext();
}
- };
+ }, [matchingCount]);
// 매칭 요청이 있는 경우에만 '요청' 탭을 활성화
const handleTabClick = useCallback(
@@ -66,15 +64,24 @@ const TabBar: React.FC = () => {
[hasMatchingRequest],
);
- // request 컴포넌트에서 매칭 거절 시 matchingCount 감소
- const decreaseMatchingCount = useCallback(() => {
- if (matchingCount !== 1) {
- setMatchingCount((prev) => Math.max(0, prev - 1));
- } else {
- setHasMatchingRequest(false);
- swiperRef.current?.slideNext();
+ // 매칭 리스트 조회 api
+ const getMatchingList = async () => {
+ const response = await getMatchingListApi();
+
+ if (response.isSuccess) {
+ setMatchingCount(response.data.matchingsCount);
+ setHasMatchingRequest(response.data.hasMatching);
}
- }, [matchingCount]);
+ };
+
+ useEffect(() => {
+ // 첫 탭을 최근 채팅으로 설정
+ if (swiperRef.current) {
+ swiperRef.current.slideTo(1, 0);
+ }
+
+ getMatchingList();
+ }, []);
return (
@@ -91,7 +98,11 @@ const TabBar: React.FC = () => {
$textTheme={{
style: activeIndex === index && (index !== 0 || hasMatchingRequest) ? 'body2-bold' : 'body2-medium',
}}
- color={activeIndex === index && (index !== 0 || hasMatchingRequest) ? '#FF2389' : '#888888'}
+ color={
+ activeIndex === index && (index !== 0 || hasMatchingRequest)
+ ? theme.colors.brand.primary
+ : theme.colors.text.caption
+ }
>
{tab}
@@ -111,16 +122,15 @@ const TabBar: React.FC = () => {
autoHeight={true} // 각 슬라이드 높이를 자동으로 조정
>
-
+
- {/* */}
);
};
-export default React.memo(TabBar);
+export default memo(TabBar);
diff --git a/src/pages/Chats/TabBar/styles.tsx b/src/pages/Chats/TabBar/styles.tsx
index 4db08dec..05b2cae6 100644
--- a/src/pages/Chats/TabBar/styles.tsx
+++ b/src/pages/Chats/TabBar/styles.tsx
@@ -1,4 +1,4 @@
-import styled from 'styled-components';
+import { styled } from 'styled-components';
export const TabBarLayout = styled.div`
display: flex;
@@ -22,8 +22,8 @@ export const TabBarList = styled.ul`
`;
export const TabBarWrapper = styled.li<{ $isSelected: boolean; $isPointer: boolean }>`
- border-bottom: 0.13rem solid #e9e9e9;
- border-image: ${({ $isSelected, theme }) => ($isSelected ? `${theme.colors.gradient} 0 0 1 0` : 'transparent')};
+ border-bottom: 0.13rem solid ${({ theme }) => theme.colors.border.divider};
+ border-image: ${({ $isSelected, theme }) => ($isSelected ? `${theme.colors.brand.gradient} 0 0 1 0` : 'transparent')};
text-align: center;
flex-grow: 1;
padding: 0.62rem;
diff --git a/src/pages/Chats/dto.ts b/src/pages/Chats/dto.ts
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/pages/Chats/index.tsx b/src/pages/Chats/index.tsx
index ec5203de..bf4ff29f 100644
--- a/src/pages/Chats/index.tsx
+++ b/src/pages/Chats/index.tsx
@@ -1,13 +1,16 @@
-import { OODDFrame } from '../../components/Frame/Frame';
+import theme from '@styles/theme';
+
+import { OODDFrame } from '@components/Frame/Frame';
+import NavBar from '@components/NavBar';
+
+import TabBar from './TabBar/index';
+
import { Header } from './styles';
-import theme from '../../styles/theme';
-import NavBar from '../../components/NavBar';
-import TabBar from './TabBar';
const Chats: React.FC = () => {
return (
-
+
diff --git a/src/pages/Chats/styles.tsx b/src/pages/Chats/styles.tsx
index 48b9cb71..82c76c78 100644
--- a/src/pages/Chats/styles.tsx
+++ b/src/pages/Chats/styles.tsx
@@ -1,5 +1,6 @@
-import styled from 'styled-components';
-import { StyledText } from '../../components/Text/StyledText';
+import { styled } from 'styled-components';
+
+import { StyledText } from '@components/Text/StyledText';
export const Header = styled(StyledText)`
width: 100%;
diff --git a/src/pages/Home/HomeTopBar.tsx b/src/pages/Home/HomeTopBar/index.tsx
similarity index 72%
rename from src/pages/Home/HomeTopBar.tsx
rename to src/pages/Home/HomeTopBar/index.tsx
index a0e70a29..dc046e27 100644
--- a/src/pages/Home/HomeTopBar.tsx
+++ b/src/pages/Home/HomeTopBar/index.tsx
@@ -1,7 +1,8 @@
-import React from 'react';
+import logo from '@assets/default/oodd.svg';
+
+import Alarm from '@components/Icons/Alarm';
+
import { Button, ButtonContainer, HomeLogo, HomeTopBarContainer } from './styles';
-import logo from './../../assets/default/oodd.svg';
-import alarm from '../../assets/default/alarm.svg';
// Home 페이지의 상단 바입니다. 로고와 알림이 있습니다.
// TODO: 버튼 클릭 이벤트 처리 필요
@@ -11,7 +12,7 @@ const HomeTopBar: React.FC = () => {
diff --git a/src/pages/Home/HomeTopBar/styles.tsx b/src/pages/Home/HomeTopBar/styles.tsx
new file mode 100644
index 00000000..1b219068
--- /dev/null
+++ b/src/pages/Home/HomeTopBar/styles.tsx
@@ -0,0 +1,31 @@
+import { styled } from 'styled-components';
+
+export const HomeTopBarContainer = styled.header`
+ width: 100%;
+ padding: 0.5rem 1.25rem;
+ display: flex;
+ justify-content: space-between;
+ background-color: ${({ theme }) => theme.colors.background.primary};
+ z-index: 20;
+ align-items: center;
+ position: fixed;
+ ${({ theme }) => theme.visibleOnMobileTablet};
+`;
+
+export const HomeLogo = styled.img`
+ padding: 0.0938rem 0;
+`;
+
+export const ButtonContainer = styled.div`
+ display: flex;
+ gap: 1rem;
+`;
+
+export const Button = styled.button`
+ width: 1.125rem;
+ height: 1.125rem;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border-radius: 0.03rem;
+`;
diff --git a/src/pages/Home/OOTD/Feed/dto.ts b/src/pages/Home/OOTD/Feed/dto.ts
new file mode 100644
index 00000000..b89b9f6a
--- /dev/null
+++ b/src/pages/Home/OOTD/Feed/dto.ts
@@ -0,0 +1,5 @@
+import { PostSummary } from '@apis/post/dto';
+
+export interface FeedProps {
+ feed: PostSummary;
+}
diff --git a/src/pages/Home/OOTD/Feed/index.tsx b/src/pages/Home/OOTD/Feed/index.tsx
index db2fab77..66dc117f 100644
--- a/src/pages/Home/OOTD/Feed/index.tsx
+++ b/src/pages/Home/OOTD/Feed/index.tsx
@@ -1,10 +1,40 @@
-import React, { useState } from 'react';
-import { Swiper, SwiperSlide } from 'swiper/react';
+import { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+
+import dayjs from 'dayjs';
import { Pagination } from 'swiper/modules';
+import { Swiper, SwiperSlide } from 'swiper/react';
import 'swiper/css';
import 'swiper/css/pagination';
-import { StyledText } from '../../../../components/Text/StyledText';
-import theme from '../../../../styles/theme';
+
+import theme from '@styles/theme';
+
+import { createMatchingApi } from '@apis/matching';
+import { togglePostLikeStatusApi } from '@apis/post-like';
+import { postUserBlockApi } from '@apis/user-block';
+import { handleError } from '@apis/util/handleError';
+import { getCurrentUserId } from '@utils/getCurrentUserId';
+
+import defaultProfile from '@assets/default/defaultProfile.svg';
+import more from '@assets/default/more.svg';
+import xBtn from '@assets/default/reject.svg';
+
+import Heart from '@components/Icons/Heart';
+import Message from '@components/Icons/Message';
+
+import CommentBottomSheet from '@components/BottomSheet/CommentBottomSheet';
+import OptionsBottomSheet from '@components/BottomSheet/OptionsBottomSheet';
+import Modal from '@components/Modal';
+import { StyledText } from '@components/Text/StyledText';
+
+import type { CreateMatchingRequest } from '@apis/matching/dto';
+import type { PostUserBlockRequest } from '@apis/user-block/dto';
+import type { CommentBottomSheetProps } from '@components/BottomSheet/CommentBottomSheet/dto';
+import { OptionsBottomSheetProps } from '@components/BottomSheet/OptionsBottomSheet/dto';
+import type { ModalProps } from '@components/Modal/dto';
+
+import type { FeedProps } from './dto';
+
import {
MatchingBtn,
FeedImgBox,
@@ -19,43 +49,57 @@ import {
ReactionWrapper,
FeedImgBackground,
} from './styles';
-import more from '../../../../assets/default/more.svg';
-import xBtn from '../../../../assets/default/reject.svg';
-import likeBtn from '../../../../assets/default/heart.svg';
-import likeFillBtn from '../../../../assets/default/heart-fill.svg';
-import commentBtn from '../../../../assets/default/message-white.svg';
-import { useNavigate } from 'react-router-dom';
-import { PostSummary } from '../../../../apis/post/dto';
-import defaultProfile from '../../../../assets/default/defaultProfile.svg';
-import dayjs from 'dayjs';
-import { OptionsBottomSheetProps } from '../../../../components/BottomSheet/OptionsBottomSheet/dto';
-import OptionsBottomSheet from '../../../../components/BottomSheet/OptionsBottomSheet';
-import CommentBottomSheet from '../../../../components/CommentBottomSheet';
-import Modal from '../../../../components/Modal';
-import { CreateMatchingRequest } from '../../../../apis/matching/dto';
-import { createMatchingApi } from '../../../../apis/matching';
-import { handleError } from '../../../../apis/util/handleError';
-import { CommentBottomSheetProps } from '../../../../components/CommentBottomSheet/dto';
-import { ModalProps } from '../../../../components/Modal/dto';
-
-import { togglePostLikeStatusApi } from '../../../../apis/post-like';
-import { postUserBlockApi } from '../../../../apis/user-block';
-import { PostUserBlockRequest } from '../../../../apis/user-block/dto';
-
-interface FeedProps {
- feed: PostSummary;
-}
const Feed: React.FC = ({ feed }) => {
- const nav = useNavigate();
const [isLikeClicked, setIsLikeClicked] = useState(feed.isPostLike);
- const timeAgo = dayjs(feed.createdAt).locale('ko').fromNow();
const [isBlockModalOpen, setIsBlockModalOpen] = useState(false);
- const [isOptionsBottomSheetOpen, setIsOptionsBottomSheetOpen] = useState(false);
const [isMatchingCommentBottomSheetOpen, setIsMatchingCommentBottomSheetOpen] = useState(false);
+ const [isOptionsBottomSheetOpen, setIsOptionsBottomSheetOpen] = useState(false);
const [isStatusModalOpen, setIsStatusModalOpen] = useState(false);
const [modalContent, setModalContent] = useState('');
- const userId = localStorage.getItem('my_id');
+
+ const nav = useNavigate();
+ const currentUserId = getCurrentUserId();
+ const timeAgo = dayjs(feed.createdAt).locale('ko').fromNow();
+
+ const handleMoreButtonClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ setIsOptionsBottomSheetOpen(true);
+ };
+
+ const handleRejectButtonClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ setIsBlockModalOpen(true);
+ };
+
+ const handleLikeButtonClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ togglePostLikeStatus();
+ };
+
+ const handleMatchingButtonClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ setIsMatchingCommentBottomSheetOpen(true);
+ };
+
+ const handleUserClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ sessionStorage.setItem('scrollPosition', String(window.scrollY));
+ nav(`/profile/${feed.user.id}`);
+ };
+
+ const handleFeedClick = (e: React.MouseEvent) => {
+ const target = e.target as HTMLElement;
+
+ // 페이지네이션 bullet 클릭 시 이벤트 차단
+ if (target.classList.contains('swiper-pagination-bullet')) {
+ e.stopPropagation();
+ } else {
+ // 그 외에 게시글 상세 조회 페이지로 이동
+ sessionStorage.setItem('scrollPosition', String(window.scrollY));
+ nav(`/post/${feed.id}`);
+ }
+ };
// 게시글 좋아요 & 좋아요 취소 api
const togglePostLikeStatus = async () => {
@@ -76,8 +120,8 @@ const Feed: React.FC = ({ feed }) => {
const postUserBlock = async () => {
try {
const data: PostUserBlockRequest = {
- fromUserId: Number(userId) || -1,
- toUserId: feed.user.id,
+ requesterId: currentUserId || -1,
+ targetId: feed.user.id,
action: 'block',
};
const response = await postUserBlockApi(data);
@@ -98,7 +142,7 @@ const Feed: React.FC = ({ feed }) => {
const createMatching = async (comment: string) => {
try {
const matchingRequest: CreateMatchingRequest = {
- requesterId: Number(userId) || -1,
+ requesterId: currentUserId || -1,
targetId: feed.user.id || -1,
message: comment,
};
@@ -116,45 +160,6 @@ const Feed: React.FC = ({ feed }) => {
}
};
- const handleUserClick = (e: React.MouseEvent) => {
- e.stopPropagation();
- sessionStorage.setItem('scrollPosition', String(window.scrollY));
- nav(`/users/${feed.user.id}`);
- };
-
- const handleMoreButtonClick = (e: React.MouseEvent) => {
- e.stopPropagation();
- setIsOptionsBottomSheetOpen(true);
- };
-
- const handleRejectButtonClick = (e: React.MouseEvent) => {
- e.stopPropagation();
- setIsBlockModalOpen(true);
- };
-
- const handleLikeButtonClick = (e: React.MouseEvent) => {
- e.stopPropagation();
- togglePostLikeStatus();
- };
-
- const handleMatchingButtonClick = (e: React.MouseEvent) => {
- e.stopPropagation();
- setIsMatchingCommentBottomSheetOpen(true);
- };
-
- const handleClickFeed = (e: React.MouseEvent) => {
- const target = e.target as HTMLElement;
-
- // 페이지네이션 bullet 클릭 시 이벤트 차단
- if (target.classList.contains('swiper-pagination-bullet')) {
- e.stopPropagation();
- } else {
- // 그 외에 게시글 상세 조회 페이지로 이동
- sessionStorage.setItem('scrollPosition', String(window.scrollY));
- nav(`/post/${feed.id}`);
- }
- };
-
// 게시글 옵션(더보기) 바텀시트
const optionsBottomSheetProps: OptionsBottomSheetProps = {
domain: 'post',
@@ -203,27 +208,22 @@ const Feed: React.FC = ({ feed }) => {
};
return (
-
-
- {isBlockModalOpen && }
-
- {isStatusModalOpen && }
-
+
-
+
{feed.user.nickname}
-
+
{timeAgo}
-
+
{feed.content}
@@ -236,32 +236,33 @@ const Feed: React.FC = ({ feed }) => {
className="ootdSwiper"
>
{feed.postImages.map((postImage) => (
-
-
-
-
-
-
-
+
+
+
+
+
))}
- {isLikeClicked ? (
-
- ) : (
-
- )}
+
+
+
-
-
+
+
매칭 요청
+
+ {isBlockModalOpen && }
+
+
+ {isStatusModalOpen && }
);
};
diff --git a/src/pages/Home/OOTD/Feed/styles.tsx b/src/pages/Home/OOTD/Feed/styles.tsx
index 78c6304b..92d10c01 100644
--- a/src/pages/Home/OOTD/Feed/styles.tsx
+++ b/src/pages/Home/OOTD/Feed/styles.tsx
@@ -1,8 +1,9 @@
-import styled from 'styled-components';
-import { StyledText } from '../../../../components/Text/StyledText';
+import { styled } from 'styled-components';
+
+import { StyledText } from '@components/Text/StyledText';
export const FeedWrapper = styled.article`
- background-color: rgba(255, 255, 255, 0.5);
+ background-color: ${({ theme }) => theme.colors.background.primary};
width: 100%;
margin-bottom: 1rem;
height: auto;
@@ -35,7 +36,7 @@ export const FeedProfileImgWrapper = styled.img`
overflow: hidden;
object-fit: cover;
- background: #ffdeed;
+ background: ${({ theme }) => theme.colors.brand.primaryLighter};
box-shadow:
0px 1px 2px 0px rgba(0, 0, 0, 0.12),
@@ -110,7 +111,7 @@ export const FeedImgBox = styled.div`
width: 0.375rem;
height: 0.375rem;
border: 0.0625rem solid ${({ theme }) => theme.colors.white};
- background: rgba(255, 255, 255, 0.5);
+ background: ${({ theme }) => theme.colors.background.primary}50;
opacity: 1;
pointer-events: auto; /* 페이지네이션 클릭 가능 */
}
@@ -118,7 +119,7 @@ export const FeedImgBox = styled.div`
.ootdSwiper .swiper-pagination-bullet-active {
width: 0.375rem;
height: 0.375rem;
- background-color: ${({ theme }) => theme.colors.white};
+ background-color: ${({ theme }) => theme.colors.background.primary};
opacity: 1;
}
@@ -162,16 +163,14 @@ export const Reaction = styled.div`
`;
export const MatchingBtn = styled.button`
- background: ${({ theme }) => theme.colors.gradient};
+ background: ${({ theme }) => theme.colors.brand.gradient};
border-radius: 3.19rem;
- backdrop-filter: blur(0.3125rem);
display: flex;
justify-content: center;
align-items: center;
padding: 0.85rem 1.25rem;
gap: 0.58rem;
width: 11.5rem;
- color: white;
`;
export const MoreBtn = styled.button`
diff --git a/src/pages/Home/OOTD/dto.ts b/src/pages/Home/OOTD/dto.ts
deleted file mode 100644
index b91d8f5b..00000000
--- a/src/pages/Home/OOTD/dto.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-export interface TagProps {
- tagImgUrl: string;
- tagName: string;
-}
-
-export interface FeedProps {
- profilePictureUrl: string;
- userName: string;
- text: string;
- feedImgUrls: string[];
- userId: number;
- postId: number;
-}
-
-export interface OOTDAPIResponse {
- isSuccess: boolean;
- code: number;
- message: string;
- result: {
- posts: Post[];
- };
-}
-
-export interface Post {
- postId: number;
- userId: number;
- likes: number;
- photoUrls: string[];
- content: string;
- styletags: string[];
-}
-
-export interface UserResponse {
- isSuccess: boolean;
- code: number;
- message: string;
- result: {
- id: number;
- name: string;
- nickname: string;
- profilePictureUrl: string;
- };
-}
diff --git a/src/pages/Home/OOTD/index.tsx b/src/pages/Home/OOTD/index.tsx
index 55e0f034..46fc288a 100644
--- a/src/pages/Home/OOTD/index.tsx
+++ b/src/pages/Home/OOTD/index.tsx
@@ -1,31 +1,51 @@
-import React, { useState, useEffect, useRef, useLayoutEffect } from 'react';
+import { useState, useEffect, useRef } from 'react';
+
+import debounce from 'lodash/debounce';
+
+import { getPostListApi } from '@apis/post';
+import { handleError } from '@apis/util/handleError';
+
+import Modal from '@components/Modal';
+
+import type { PostSummary } from '@apis/post/dto';
+import type { ModalProps } from '@components/Modal/dto';
+
+import Feed from './Feed/index';
+
import { OOTDContainer, FeedContainer } from './styles';
-import Feed from './Feed';
-import { getPostListApi } from '../../../apis/post';
-import { PostSummary } from '../../../apis/post/dto';
-import { handleError } from '../../../apis/util/handleError';
-import { ModalProps } from '../../../components/Modal/dto';
-import Modal from '../../../components/Modal';
const OOTD: React.FC = () => {
const [feeds, setFeeds] = useState([]);
- const [reachedEnd, setReachedEnd] = useState(false);
+
const [modalContent, setModalContent] = useState('알 수 없는 오류입니다.\n관리자에게 문의해 주세요.');
const [isStatusModalOpen, setIsStatusModalOpen] = useState(false);
- const savedScrollPosition = sessionStorage.getItem('scrollPosition');
+
+ const isFetchingRef = useRef(false);
+ const isReachedEndRef = useRef(false);
const feedPageRef = useRef(1);
+
+ // IntersectionObserver 인스턴스를 참조하는 변수
+ const observerRef = useRef(null);
+ // observer 콜백 함수를 트리거하는 요소를 참조하는 변수
+ const loadMoreRef = useRef(null);
+
+ // 세션 스토리지에서 이전 스크롤 위치를 가져와 초기화
+ const savedScrollPosition = sessionStorage.getItem('scrollPosition');
const scrollPositionRef = useRef(Number(savedScrollPosition) || 0);
- const isFetchingRef = useRef(false);
+ // 전체 게시글(피드) 조회 API
const getPostList = async () => {
- if (reachedEnd) return;
+ // 모든 데이터를 불러왔거나 요청 중이라면 함수 실행 중단
+ if (isReachedEndRef.current || isFetchingRef.current) return;
+
+ isFetchingRef.current = true;
try {
const response = await getPostListApi(feedPageRef.current, 20);
if (response.isSuccess) {
if (response.data.post.length === 0) {
- setReachedEnd(true);
+ isReachedEndRef.current = true;
} else {
setFeeds((prevFeeds) => [...prevFeeds, ...response.data.post]);
feedPageRef.current += 1;
@@ -35,34 +55,59 @@ const OOTD: React.FC = () => {
const errorMessage = handleError(error);
setModalContent(errorMessage);
setIsStatusModalOpen(true);
+ } finally {
+ isFetchingRef.current = false;
+ console.log(feeds);
}
};
- // 스크롤 이벤트 핸들러 추가
- const handleScroll = () => {
- // 모든 데이터를 불러왔거나 아직 렌더링이 다 안 된 경우 반환
- if (reachedEnd || isFetchingRef.current) return;
+ useEffect(() => {
+ // 데이터의 끝에 다다르면 옵저버 해제 (더이상 피드가 없으면)
+ if (isReachedEndRef.current && observerRef.current && loadMoreRef.current) {
+ observerRef.current.unobserve(loadMoreRef.current);
- if (window.innerHeight + document.documentElement.scrollTop >= document.body.scrollHeight - window.innerHeight) {
- isFetchingRef.current = true;
- scrollPositionRef.current = window.scrollY;
- getPostList();
+ return;
}
- };
- useEffect(() => {
- getPostList();
- window.addEventListener('scroll', handleScroll);
+ // Intersection Observer 생성
+ observerRef.current = new IntersectionObserver(
+ debounce((entries) => {
+ const target = entries[0];
+ console.log('Intersection Observer:', target.isIntersecting);
+ if (target.isIntersecting && !isFetchingRef.current && !isReachedEndRef.current) {
+ getPostList();
+ }
+ }, 300),
+ {
+ root: null,
+ rootMargin: '100px',
+ threshold: 0,
+ },
+ );
+ // 옵저버를 마지막 요소에 연결
+ if (loadMoreRef.current) {
+ observerRef.current.observe(loadMoreRef.current);
+ }
return () => {
- window.removeEventListener('scroll', handleScroll);
+ // 컴포넌트 언마운트 시 옵저버 해제
+ if (observerRef.current && loadMoreRef.current) {
+ observerRef.current.unobserve(loadMoreRef.current);
+ }
};
}, []);
- useLayoutEffect(() => {
+ useEffect(() => {
+ getPostList();
+
+ // 세션에 저장된 이전 스크롤 위치 복원
window.scrollTo(0, scrollPositionRef.current);
- isFetchingRef.current = false;
- }, [feeds]); // feeds가 변경될 때 실행
+
+ return () => {
+ // 컴포넌트 언마운트 시 현재 스크롤 위치를 세션 스토리지에 저장
+ sessionStorage.setItem('scrollPosition', String(window.scrollY));
+ };
+ }, []);
const statusModalProps: ModalProps = {
content: modalContent,
@@ -73,14 +118,16 @@ const OOTD: React.FC = () => {
return (
- {isStatusModalOpen && }
{feeds.map((feed) => (
))}
+ {/* Intersection Observer가 감지할 마지막 요소 */}
+
+ {isStatusModalOpen && }
);
};
diff --git a/src/pages/Home/OOTD/styles.tsx b/src/pages/Home/OOTD/styles.tsx
index 011a4aa8..61840601 100644
--- a/src/pages/Home/OOTD/styles.tsx
+++ b/src/pages/Home/OOTD/styles.tsx
@@ -1,4 +1,4 @@
-import styled from 'styled-components';
+import { styled } from 'styled-components';
export const OOTDContainer = styled.div`
display: flex;
@@ -9,12 +9,6 @@ export const OOTDContainer = styled.div`
margin-bottom: 4.25rem;
`;
-export const OOTDLoading = styled.div`
- margin-top: 200px;
-`;
-
-// Feed
-
export const FeedContainer = styled.div`
display: flex;
flex-direction: column;
diff --git a/src/pages/Home/dto.ts b/src/pages/Home/dto.ts
deleted file mode 100644
index 1151716d..00000000
--- a/src/pages/Home/dto.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-export interface ApiDto {
- isSuccess: boolean;
- code: number;
- 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 3756b895..b735dafa 100644
--- a/src/pages/Home/index.tsx
+++ b/src/pages/Home/index.tsx
@@ -1,10 +1,11 @@
-import { OODDFrame } from '../../components/Frame/Frame';
-import NavBar from '../../components/NavBar';
-import HomeTopBar from './HomeTopBar';
-import OOTD from './OOTD/index.tsx';
+import { OODDFrame } from '@components/Frame/Frame';
+import NavBar from '@components/NavBar';
+
+import HomeTopBar from './HomeTopBar/index';
+import OOTD from './OOTD/index';
+
import { HomeContainer } from './styles';
-// Home 페이지입니다.
const Home: React.FC = () => {
return (
diff --git a/src/pages/Home/styles.tsx b/src/pages/Home/styles.tsx
index 9f7bb5e2..fe2af26a 100644
--- a/src/pages/Home/styles.tsx
+++ b/src/pages/Home/styles.tsx
@@ -1,41 +1,7 @@
-import styled from 'styled-components';
-
-// HomeContainer
+import { styled } from 'styled-components';
export const HomeContainer = styled.div`
flex-grow: 1;
height: auto;
width: 100%;
`;
-
-// HomeTopBar
-
-export const HomeTopBarContainer = styled.header`
- width: 100%;
- padding: 0.5rem 1.25rem;
- display: flex;
- justify-content: space-between;
- background-color: white;
- z-index: 20;
- align-items: center;
- position: fixed;
- ${({ theme }) => theme.visibleOnMobileTablet};
-`;
-
-export const HomeLogo = styled.img`
- padding: 0.0938rem 0;
-`;
-
-export const ButtonContainer = styled.div`
- display: flex;
- gap: 1rem;
-`;
-
-export const Button = styled.button`
- width: 1.125rem;
- height: 1.125rem;
- display: flex;
- justify-content: center;
- align-items: center;
- border-radius: 0.03rem;
-`;
diff --git a/src/pages/Login/components/LoginComplete.tsx b/src/pages/Login/LoginComplete/index.tsx
similarity index 66%
rename from src/pages/Login/components/LoginComplete.tsx
rename to src/pages/Login/LoginComplete/index.tsx
index bcf83689..4eb06f79 100644
--- a/src/pages/Login/components/LoginComplete.tsx
+++ b/src/pages/Login/LoginComplete/index.tsx
@@ -1,18 +1,32 @@
-import React, { useEffect, useState } from 'react';
+import { 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 { postTermsAgreementApi } from '@apis/user';
+import { handleError } from '@apis/util/handleError';
-import { getUserInfoByJwtApi } from '../../../apis/auth';
-import { handleError } from '../../../apis/util/handleError';
-import { postTermsAgreementApi } from '../../../apis/user';
+import Loading from '@components/Loading';
+import Modal from '@components/Modal';
const LoginComplete: React.FC = () => {
- const location = useLocation();
- const navigate = useNavigate();
const [isModalOpen, setIsModalOpen] = useState(false);
const [modalMessage, setModalMessage] = useState('');
+ const location = useLocation();
+ const navigate = useNavigate();
+
+ const handleModalClose = () => {
+ setIsModalOpen(false);
+ navigate('/login'); // 모달 닫힌 후 로그인 페이지로 이동
+ };
+
+ const hasAgreedToTerms = async (userId: number): Promise => {
+ try {
+ await postTermsAgreementApi(userId);
+ return true; // 이용 약관 동의 완료된 사용자
+ } catch {
+ return false; // 이용 약관 동의 필요한 사용자
+ }
+ };
useEffect(() => {
// URLSearchParams를 사용해 쿼리 문자열에서 token 추출
@@ -29,13 +43,13 @@ const LoginComplete: React.FC = () => {
const response = await getUserInfoByJwtApi();
console.log(response);
- const { nickname, name, userId } = response.data;
- localStorage.setItem('my_id', `${userId}`);
+ const { nickname, name, id } = response.data;
+ localStorage.setItem('current_user_id', `${id}`);
if (nickname && name) {
if (nickname && name) {
- const isAgreed = await checkTermsAgreement(userId);
- navigate(isAgreed ? '/' : '/terms-agreement');
+ const isAgreed = await hasAgreedToTerms(id);
+ navigate(isAgreed ? '/' : '/signup/terms-agreement'); // 이용 약관이 필요하면 (false) 해당 페이지로 이동
}
} else {
navigate('/signup');
@@ -51,20 +65,6 @@ const LoginComplete: React.FC = () => {
}
}, [location]);
- const checkTermsAgreement = async (userId: number): Promise => {
- try {
- await postTermsAgreementApi(userId);
- return true; // 동의 완료
- } catch {
- return false; // 동의 필요
- }
- };
-
- const handleModalClose = () => {
- setIsModalOpen(false);
- navigate('/login'); // 모달 닫힌 후 로그인 페이지로 이동
- };
-
return (
<>
diff --git a/src/pages/Login/SocialLoginButton/dto.ts b/src/pages/Login/SocialLoginButton/dto.ts
new file mode 100644
index 00000000..66b3ca4e
--- /dev/null
+++ b/src/pages/Login/SocialLoginButton/dto.ts
@@ -0,0 +1,7 @@
+export interface SocialLoginProps {
+ bgColor: string;
+ logoSrc: string;
+ altText: string;
+ buttonText: string;
+ provider: 'naver' | 'kakao';
+}
diff --git a/src/pages/Login/SocialLoginButton/index.tsx b/src/pages/Login/SocialLoginButton/index.tsx
new file mode 100644
index 00000000..ae5c0481
--- /dev/null
+++ b/src/pages/Login/SocialLoginButton/index.tsx
@@ -0,0 +1,36 @@
+import theme from '@styles/theme';
+
+import type { SocialLoginProps } from './dto';
+
+import { SocialLoginContainer, LogoImgWrapper, LogoImage, StyledTextWrapper } from './style';
+
+const SERVER_URI = import.meta.env.VITE_NEW_API_URL;
+
+const SocialLoginButton: React.FC = ({ bgColor, logoSrc, altText, buttonText, provider }) => {
+ const handleSocialLoginClick = () => {
+ // 리다이렉트 URL 설정
+ const redirectUrl = encodeURIComponent(`${import.meta.env.VITE_DOMAIN || window.location.origin}/login/complete`);
+
+ // 서버 URL 생성
+ const serverUrl = `${SERVER_URI}/auth/login/${provider}?redirectUrl=${redirectUrl}`;
+
+ // 서버로 리다이렉션
+ window.open(serverUrl, '_self');
+ };
+
+ return (
+
+
+
+
+
+ {buttonText}
+
+
+ );
+};
+
+export default SocialLoginButton;
diff --git a/src/pages/Login/components/style.tsx b/src/pages/Login/SocialLoginButton/style.tsx
similarity index 64%
rename from src/pages/Login/components/style.tsx
rename to src/pages/Login/SocialLoginButton/style.tsx
index 85611457..3120dc95 100644
--- a/src/pages/Login/components/style.tsx
+++ b/src/pages/Login/SocialLoginButton/style.tsx
@@ -1,6 +1,8 @@
-import styled from 'styled-components';
+import { styled } from 'styled-components';
-export const SocialLogin = styled.button<{ $bgColor: string; $border?: boolean }>`
+import { StyledText } from '@components/Text/StyledText';
+
+export const SocialLoginContainer = styled.button<{ $bgColor: string }>`
display: flex;
align-items: center;
width: calc(100% - 3.5rem);
@@ -8,7 +10,6 @@ export const SocialLogin = styled.button<{ $bgColor: string; $border?: boolean }
height: 3.5rem;
background-color: ${({ $bgColor }) => $bgColor};
border-radius: 0.5rem;
- border: ${({ $border }) => ($border ? '1px solid #000' : 'none')};
cursor: pointer;
margin-bottom: 0.5rem;
box-sizing: border-box;
@@ -27,10 +28,10 @@ export const LogoImage = styled.img`
max-height: 100%;
`;
-export const TextWrapper = styled.section<{ $left?: string }>`
+export const StyledTextWrapper = styled(StyledText)`
display: flex;
width: 12.5rem;
- padding-left: ${({ $left }) => $left || '1.2rem'};
+ padding-left: 1.2rem;
align-items: center;
margin: 0 auto;
`;
diff --git a/src/pages/Login/components/Kakao/index.tsx b/src/pages/Login/components/Kakao/index.tsx
deleted file mode 100644
index d1d1f515..00000000
--- a/src/pages/Login/components/Kakao/index.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import React from 'react';
-
-import theme from '../../../../styles/theme';
-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 SERVER_URI = import.meta.env.VITE_NEW_API_URL;
-
- const handleLogin = () => {
- // 리다이렉트 URL 설정
- const redirectUrl = encodeURIComponent(`${import.meta.env.VITE_DOMAIN || window.location.origin}/login/complete`);
-
- // 서버 URL 생성
- const serverUrl = `${SERVER_URI}/auth/login/kakao?redirectUrl=${redirectUrl}`;
-
- // 서버로 리다이렉션
- window.open(serverUrl, '_self');
- };
-
- return (
-
-
-
-
-
-
- Kakao로 계속하기
-
-
-
- );
-};
-export default Kakao;
diff --git a/src/pages/Login/components/Naver/index.tsx b/src/pages/Login/components/Naver/index.tsx
deleted file mode 100644
index 963e278f..00000000
--- a/src/pages/Login/components/Naver/index.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-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/default/snsIcon/naver.svg';
-
-const Naver: React.FC = () => {
- const SERVER_URI = import.meta.env.VITE_NEW_API_URL;
-
- const handleLogin = () => {
- // 리다이렉트 URL 설정
- const redirectUrl = encodeURIComponent(`${import.meta.env.VITE_DOMAIN || window.location.origin}/login/complete`);
-
- // 서버 URL 생성
- const serverUrl = `${SERVER_URI}/auth/login/naver?redirectUrl=${redirectUrl}`;
-
- // 서버로 리다이렉션
- window.open(serverUrl, '_self');
- };
-
- return (
-
-
-
-
-
-
- 네이버로 계속하기
-
-
-
- );
-};
-
-export default Naver;
diff --git a/src/pages/Login/index.tsx b/src/pages/Login/index.tsx
index d30c211c..dac1405a 100644
--- a/src/pages/Login/index.tsx
+++ b/src/pages/Login/index.tsx
@@ -1,21 +1,35 @@
-import React from 'react';
+import theme from '@styles/theme';
-import { OODDFrame } from '../../components/Frame/Frame';
-import Naver from './components/Naver';
-import Kakao from './components/Kakao';
+import kakaoLogo from '@assets/default/snsIcon/kakao.svg';
+import naverLogo from '@assets/default/snsIcon/naver.svg';
+
+import { OODDFrame } from '@components/Frame/Frame';
+
+import SocialLoginButton from './SocialLoginButton/index';
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 544dc3ac..f9a18246 100644
--- a/src/pages/Login/styles.tsx
+++ b/src/pages/Login/styles.tsx
@@ -1,5 +1,6 @@
-import styled from 'styled-components';
-import { StyledText } from '../../components/Text/StyledText';
+import { styled } from 'styled-components';
+
+import { StyledText } from '@components/Text/StyledText';
export const LoginContainer = styled.main`
display: flex;
diff --git a/src/pages/MyPage/index.tsx b/src/pages/MyPage/index.tsx
deleted file mode 100644
index f9f80579..00000000
--- a/src/pages/MyPage/index.tsx
+++ /dev/null
@@ -1,175 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { useNavigate } from 'react-router-dom';
-import {
- ProfileContainer,
- Header,
- StatsContainer,
- Stat,
- StatNumber,
- StatLabel,
- PostsContainer,
- AddButton,
- NoPostWrapper,
-} 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 { StyledText } from '../../components/Text/StyledText';
-
-import { getUserPostListApi } from '../../apis/post';
-import { UserPostSummary } from '../../apis/post/dto';
-import { getUserInfoApi } from '../../apis/user';
-import { UserInfoData } from '../../apis/user/dto';
-
-const MyPage: React.FC = () => {
- const [isLoading, setIsLoading] = useState(true);
- const [isBottomSheetOpen, setIsBottomSheetOpen] = useState(false);
- const [posts, setPosts] = useState([]);
- const [totalPosts, setTotalPosts] = useState(0);
- const [userInfo, setUserInfo] = useState(null);
- 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 fetchUserInfo = async () => {
- try {
- const storedUserId = Number(localStorage.getItem('my_id'));
- if (!storedUserId) {
- console.error('User ID not found in localStorage');
- return;
- }
-
- const response = await getUserInfoApi(Number(storedUserId));
- setUserInfo(response.data);
- } catch (error) {
- console.error('Error fetching user info:', error);
- }
- };
-
- // 게시물 리스트 조회 API
- const fetchPostList = async () => {
- try {
- const storedUserId = Number(localStorage.getItem('my_id'));
- if (!storedUserId) {
- console.error('User ID not found in localStorage');
- return;
- }
-
- const response = await getUserPostListApi(1, 10, Number(storedUserId));
- const { post, totalPostsCount } = response.data;
-
- setPosts(post); // 게시물 목록 상태 업데이트 (UserPostSummary 사용!)
- setTotalPosts(totalPostsCount); // 총 게시물 수 상태 업데이트
- } catch (error) {
- console.error('Error fetching user post list:', error);
- } finally {
- setIsLoading(false); // 로딩 상태 종료
- }
- };
-
- useEffect(() => {
- fetchPostList();
- fetchUserInfo();
- }, []);
-
- if (isLoading) {
- return ;
- }
-
- return (
-
-
-
-
-
-
-
-
-
-
-
- OOTD
- {totalPosts || 0}
-
-
- 코멘트
- {posts.reduce((sum, post) => sum + (post.postCommentsCount || 0), 0)}
-
-
- 좋아요
- {posts.reduce((sum, post) => sum + (post.postLikesCount || 0), 0)}
-
-
-
- {posts.length > 0 ? (
- posts
- .sort((a, b) => Number(b.isRepresentative) - Number(a.isRepresentative))
- .map((post) => )
- ) : (
-
-
- 게시물이 없어요.
-
-
- )}
-
-
-
-
- );
-};
-
-export default MyPage;
diff --git a/src/pages/MyPost/index.tsx b/src/pages/MyPost/index.tsx
deleted file mode 100644
index ef765396..00000000
--- a/src/pages/MyPost/index.tsx
+++ /dev/null
@@ -1,150 +0,0 @@
-import React, { useState, useEffect } from 'react';
-
-import { useParams, useNavigate } from 'react-router-dom';
-
-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 Edit from '../../assets/default/edit.svg';
-import Pin from '../../assets/default/pin.svg';
-import Delete from '../../assets/default/delete.svg';
-
-import { modifyPostRepresentativeStatusApi, deletePostApi } from '../../apis/post';
-
-const MyPost: React.FC = () => {
- const { postId } = useParams<{ postId: string }>();
- 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();
-
- useEffect(() => {
- if (isPostRepresentative) {
- setPostPinStatus('해제');
- } else {
- setPostPinStatus('지정');
- }
- }, [isPostRepresentative]);
-
- const bottomSheetMenuProps: BottomSheetMenuProps = {
- items: [
- {
- text: `대표 OOTD ${postPinStatus}하기`,
- action: () => {
- setIsMenuBottomSheetOpen(false);
- modifyPostRepresentativeStatus();
- },
- icon: Pin,
- },
- {
- text: 'OODD 수정하기',
- action: () => {
- setIsMenuBottomSheetOpen(false);
- handlePostEdit();
- },
- icon: Edit,
- },
- {
- text: 'OOTD 삭제하기',
- action: () => {
- setIsMenuBottomSheetOpen(false);
- setIsDeleteConfirmationModalOpen(true);
- },
- icon: Delete,
- },
- ],
- marginBottom: '50px',
- };
-
- const menuBottomSheetProps: BottomSheetProps = {
- isOpenBottomSheet: isMenuBottomSheetOpen,
- Component: BottomSheetMenu,
- componentProps: bottomSheetMenuProps,
- onCloseBottomSheet: () => setIsMenuBottomSheetOpen(false),
- };
-
- const handleMenuOpen = () => {
- setIsMenuBottomSheetOpen(true);
- };
-
- const handlePostEdit = () => {
- navigate('/upload', { state: { mode: 'edit', postId: postId } });
- };
-
- const modifyPostRepresentativeStatus = async () => {
- try {
- const response = await modifyPostRepresentativeStatusApi(Number(postId));
-
- if (response.isSuccess) {
- setPinPostResultlModalContent(`대표 OOTD ${postPinStatus}에 성공했어요`);
- setIsPostRepresentative((prev) => !prev);
- } else {
- setPinPostResultlModalContent(`대표 OOTD ${postPinStatus}에 실패했어요\n잠시 뒤 다시 시도해 보세요`);
- }
- } catch (error) {
- console.error('Error pinning post:', error);
- } finally {
- setIsApiResponseModalOpen(true);
- }
- };
-
- const deletePost = async () => {
- try {
- const response = await deletePostApi(Number(postId));
-
- if (response.isSuccess) {
- setPinPostResultlModalContent('OOTD 삭제에 성공했어요');
- // 1초 뒤에 mypage로 이동
- setTimeout(() => {
- navigate('/mypage');
- }, 1000);
- } else {
- setPinPostResultlModalContent(`OOTD 삭제에 실패했어요\n잠시 뒤 다시 시도해 보세요`);
- }
- } catch (error) {
- console.error('Error deleting post:', error);
- } finally {
- setIsApiResponseModalOpen(true);
- setIsDeleteConfirmationModalOpen(false); // 확인 모달을 닫음
- }
- };
-
- const deleteConfirmationModalProps: ModalProps = {
- isCloseButtonVisible: true,
- onClose: () => setIsDeleteConfirmationModalOpen(false),
- content: '해당 OOTD를 삭제하시겠습니까?',
- button: {
- content: '삭제하기',
- onClick: deletePost,
- },
- };
-
- const apiResponseModalProps: ModalProps = {
- onClose: () => setIsApiResponseModalOpen(false),
- content: pinPostResultlModalContent,
- };
-
- return (
- <>
-
-
-
-
- {isDeleteConfirmationModalOpen && }
- {isApiResponseModalOpen && }
- >
- );
-};
-
-export default MyPost;
diff --git a/src/pages/MyPost/styles.tsx b/src/pages/MyPost/styles.tsx
deleted file mode 100644
index e130d565..00000000
--- a/src/pages/MyPost/styles.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import styled from 'styled-components';
-
-export const ModalContainer = styled.div`
- background: white;
- border-radius: 10px 10px 0 0;
- max-width: 512px;
- position: flex;
- height: 377px;
- flex-shrink: 0;
-`;
diff --git a/src/pages/NotFound/index.tsx b/src/pages/NotFound/index.tsx
index 4c879a1f..7afc7f13 100644
--- a/src/pages/NotFound/index.tsx
+++ b/src/pages/NotFound/index.tsx
@@ -1,8 +1,11 @@
import { useNavigate } from 'react-router-dom';
-import { OODDFrame } from '../../components/Frame/Frame';
+
+import theme from '@styles/theme';
+
+import { OODDFrame } from '@components/Frame/Frame';
+import { StyledText } from '@components/Text/StyledText';
+
import { NotFoundContainer, TextContainer, ButtonContainer, StyledButton } from './styles';
-import { StyledText } from '../../components/Text/StyledText';
-import theme from '../../styles/theme';
const NotFound = () => {
const navigate = useNavigate();
diff --git a/src/pages/NotFound/styles.tsx b/src/pages/NotFound/styles.tsx
index ec172fb8..e6624b5a 100644
--- a/src/pages/NotFound/styles.tsx
+++ b/src/pages/NotFound/styles.tsx
@@ -1,5 +1,6 @@
-import styled from 'styled-components';
-import { StyledText } from '../../components/Text/StyledText';
+import { styled } from 'styled-components';
+
+import { StyledText } from '@components/Text/StyledText';
export const NotFoundContainer = styled.div`
display: flex;
diff --git a/src/components/PostBase/ImageSwiper/index.tsx b/src/pages/Post/PostBase/ImageSwiper/index.tsx
similarity index 97%
rename from src/components/PostBase/ImageSwiper/index.tsx
rename to src/pages/Post/PostBase/ImageSwiper/index.tsx
index e965161d..d813bb13 100644
--- a/src/components/PostBase/ImageSwiper/index.tsx
+++ b/src/pages/Post/PostBase/ImageSwiper/index.tsx
@@ -1,15 +1,15 @@
-import React, { useRef } from 'react';
+import { useRef } from 'react';
-import { Swiper, SwiperRef, SwiperSlide } from 'swiper/react';
import { Navigation, Pagination } from 'swiper/modules';
+import { Swiper, SwiperRef, SwiperSlide } from 'swiper/react';
import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';
-import { SwiperContainer, ImageWrapper, StyledNavigation } from './styles';
-
import { ImageSwiperProps } from '../dto';
+import { SwiperContainer, ImageWrapper, StyledNavigation } from './styles';
+
const ImageSwiper: React.FC = ({ images }) => {
const swiperRef = useRef(null);
diff --git a/src/components/PostBase/ImageSwiper/styles.tsx b/src/pages/Post/PostBase/ImageSwiper/styles.tsx
similarity index 92%
rename from src/components/PostBase/ImageSwiper/styles.tsx
rename to src/pages/Post/PostBase/ImageSwiper/styles.tsx
index a558a831..914383f0 100644
--- a/src/components/PostBase/ImageSwiper/styles.tsx
+++ b/src/pages/Post/PostBase/ImageSwiper/styles.tsx
@@ -1,4 +1,4 @@
-import styled from 'styled-components';
+import { styled } from 'styled-components';
export const SwiperContainer = styled.div`
display: flex;
diff --git a/src/components/PostBase/LikeCommentBottomSheetContent/CommentItem/dto.ts b/src/pages/Post/PostBase/LikeCommentBottomSheetContent/CommentItem/dto.ts
similarity index 75%
rename from src/components/PostBase/LikeCommentBottomSheetContent/CommentItem/dto.ts
rename to src/pages/Post/PostBase/LikeCommentBottomSheetContent/CommentItem/dto.ts
index 2e5da179..5a1eba0b 100644
--- a/src/components/PostBase/LikeCommentBottomSheetContent/CommentItem/dto.ts
+++ b/src/pages/Post/PostBase/LikeCommentBottomSheetContent/CommentItem/dto.ts
@@ -1,4 +1,4 @@
-import { Comment } from '../../../../apis/post-comment/dto';
+import { Comment } from '@apis/post-comment/dto';
export interface CommentItemProps {
comment: Comment;
diff --git a/src/components/PostBase/LikeCommentBottomSheetContent/CommentItem/index.tsx b/src/pages/Post/PostBase/LikeCommentBottomSheetContent/CommentItem/index.tsx
similarity index 83%
rename from src/components/PostBase/LikeCommentBottomSheetContent/CommentItem/index.tsx
rename to src/pages/Post/PostBase/LikeCommentBottomSheetContent/CommentItem/index.tsx
index ad42e400..5deb501a 100644
--- a/src/components/PostBase/LikeCommentBottomSheetContent/CommentItem/index.tsx
+++ b/src/pages/Post/PostBase/LikeCommentBottomSheetContent/CommentItem/index.tsx
@@ -1,9 +1,16 @@
-import React, { useEffect, useState } from 'react';
+import { useEffect, useState } from 'react';
import dayjs from 'dayjs';
import 'dayjs/locale/ko';
-import theme from '../../../../styles/theme';
+import theme from '@styles/theme';
+
+import More from '@assets/default/more.svg';
+
+import { StyledText } from '@components/Text/StyledText';
+
+import type { CommentItemProps } from './dto';
+
import {
StyledBigUserProfile,
CommentItem as StyledCommentItem,
@@ -12,12 +19,6 @@ import {
MenuBtn,
} from './styles';
-import { StyledText } from '../../../Text/StyledText';
-
-import { CommentItemProps } from './dto';
-
-import More from '../../../../assets/default/more.svg';
-
const CommentItem: React.FC = ({ comment, handleUserClick, handleMenuOpen }) => {
const [timeAgo, setTimeAgo] = useState();
diff --git a/src/components/PostBase/LikeCommentBottomSheetContent/CommentItem/styles.tsx b/src/pages/Post/PostBase/LikeCommentBottomSheetContent/CommentItem/styles.tsx
similarity index 94%
rename from src/components/PostBase/LikeCommentBottomSheetContent/CommentItem/styles.tsx
rename to src/pages/Post/PostBase/LikeCommentBottomSheetContent/CommentItem/styles.tsx
index 46347022..dbd3a127 100644
--- a/src/components/PostBase/LikeCommentBottomSheetContent/CommentItem/styles.tsx
+++ b/src/pages/Post/PostBase/LikeCommentBottomSheetContent/CommentItem/styles.tsx
@@ -1,4 +1,5 @@
-import styled from 'styled-components';
+import { styled } from 'styled-components';
+
import { BigUserProfile } from '../styles';
export const CommentItem = styled.div`
diff --git a/src/components/PostBase/LikeCommentBottomSheetContent/MenuButtonList/dto.ts b/src/pages/Post/PostBase/LikeCommentBottomSheetContent/MenuButtonList/dto.ts
similarity index 100%
rename from src/components/PostBase/LikeCommentBottomSheetContent/MenuButtonList/dto.ts
rename to src/pages/Post/PostBase/LikeCommentBottomSheetContent/MenuButtonList/dto.ts
diff --git a/src/components/PostBase/LikeCommentBottomSheetContent/MenuButtonList/index.tsx b/src/pages/Post/PostBase/LikeCommentBottomSheetContent/MenuButtonList/index.tsx
similarity index 82%
rename from src/components/PostBase/LikeCommentBottomSheetContent/MenuButtonList/index.tsx
rename to src/pages/Post/PostBase/LikeCommentBottomSheetContent/MenuButtonList/index.tsx
index 7fa7b870..709411cb 100644
--- a/src/components/PostBase/LikeCommentBottomSheetContent/MenuButtonList/index.tsx
+++ b/src/pages/Post/PostBase/LikeCommentBottomSheetContent/MenuButtonList/index.tsx
@@ -1,8 +1,10 @@
-import React from 'react';
-import ReactDOM from 'react-dom';
-import { MenuButtonListProps } from './dto';
+import { createPortal } from 'react-dom';
+
+import { StyledText } from '@components/Text/StyledText';
+
+import type { MenuButtonListProps } from './dto';
+
import { MenuListWrapper, MenuListContainer, MenuButtonItem } from './styles';
-import { StyledText } from '../../../Text/StyledText';
const MenuButtonList: React.FC = ({ items, onClose, position }) => {
const handleWrapperClick = () => {
@@ -13,7 +15,7 @@ const MenuButtonList: React.FC = ({ items, onClose, positio
event.stopPropagation(); // Container 클릭 시 이벤트 중단
};
- return ReactDOM.createPortal(
+ return createPortal(
{items.map((item, index) => (
diff --git a/src/components/PostBase/LikeCommentBottomSheetContent/MenuButtonList/styles.tsx b/src/pages/Post/PostBase/LikeCommentBottomSheetContent/MenuButtonList/styles.tsx
similarity index 90%
rename from src/components/PostBase/LikeCommentBottomSheetContent/MenuButtonList/styles.tsx
rename to src/pages/Post/PostBase/LikeCommentBottomSheetContent/MenuButtonList/styles.tsx
index 460d2321..579a1de7 100644
--- a/src/components/PostBase/LikeCommentBottomSheetContent/MenuButtonList/styles.tsx
+++ b/src/pages/Post/PostBase/LikeCommentBottomSheetContent/MenuButtonList/styles.tsx
@@ -1,4 +1,4 @@
-import styled from 'styled-components';
+import { styled } from 'styled-components';
export const MenuListWrapper = styled.div`
position: fixed;
@@ -19,7 +19,7 @@ export const MenuListContainer = styled.div<{ $position: { top: number; left: nu
display: flex;
flex-direction: column;
border-radius: 10px;
- background-color: ${({ theme }) => theme.colors.gray1};
+ background-color: ${({ theme }) => theme.colors.gray[200]};
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
`;
diff --git a/src/components/PostBase/LikeCommentBottomSheetContent/index.tsx b/src/pages/Post/PostBase/LikeCommentBottomSheetContent/index.tsx
similarity index 89%
rename from src/components/PostBase/LikeCommentBottomSheetContent/index.tsx
rename to src/pages/Post/PostBase/LikeCommentBottomSheetContent/index.tsx
index 38c04ec4..00e3f978 100644
--- a/src/components/PostBase/LikeCommentBottomSheetContent/index.tsx
+++ b/src/pages/Post/PostBase/LikeCommentBottomSheetContent/index.tsx
@@ -1,49 +1,49 @@
-import React, { useEffect, useState, useRef, useCallback } from 'react';
+import { useEffect, useState, useRef, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useRecoilState } from 'recoil';
+
+import theme from '@styles/theme';
+
+import { createCommentApi, deleteCommentApi, getCommentListApi } from '@apis/post-comment';
+import { getPostLikeListApi } from '@apis/post-like';
+import { postUserBlockApi } from '@apis/user-block';
+import { PostUserBlockRequest } from '@apis/user-block/dto';
+import { handleError } from '@apis/util/handleError';
import {
IsCommentDeleteConfirmationModalOpenAtom,
IsCommentReportModalOpenAtom,
selectedCommentAtom,
-} from '../../../recoil/Post/PostCommentAtom';
+} from '@recoil/Post/PostCommentAtom';
+import { getCurrentUserId } from '@utils/getCurrentUserId';
-import { TabContainer, Tab, ContentContainer, Content, BigUserProfile, LikeItem, InputLayout } from './styles';
+import Block from '@assets/default/block.svg';
+import Delete from '@assets/default/delete.svg';
+import Report from '@assets/default/report.svg';
+import X from '@assets/default/x.svg';
+
+import Loading from '@components/Loading';
+import Modal from '@components/Modal';
+import { StyledText } from '@components/Text/StyledText';
+
+import type { Comment, GetCommentListResponse } from '@apis/post-comment/dto';
+import type { GetPostLikeListResponse } from '@apis/post-like/dto';
+import type { ModalProps } from '@components/Modal/dto';
-import { StyledText } from '../../Text/StyledText';
-import theme from '../../../styles/theme';
-import Loading from '../../Loading';
-import Modal from '../../Modal';
-import CommentItem from './CommentItem';
-import MenuButtonList from './MenuButtonList';
-
-import { LikeCommentBottomSheetProps } from '../dto';
-import { ModalProps } from '../../Modal/dto';
-import { GetPostLikeListResponse } from '../../../apis/post-like/dto';
-import { Comment, GetCommentListResponse } from '../../../apis/post-comment/dto';
-
-import Delete from '../../../assets/default/delete.svg';
-import Block from '../../../assets/default/block.svg';
-import Report from '../../../assets/default/report.svg';
-import X from '../../../assets/default/x.svg';
-
-import { getPostLikeListApi } from '../../../apis/post-like';
-import { postUserBlockApi } from '../../../apis/user-block';
-import { PostUserBlockRequest } from '../../../apis/user-block/dto';
-import { createCommentApi, deleteCommentApi, getCommentListApi } from '../../../apis/post-comment';
-import { handleError } from '../../../apis/util/handleError';
+import type { LikeCommentBottomSheetProps } from '../dto';
+
+import CommentItem from './CommentItem/index';
+import MenuButtonList from './MenuButtonList/index';
+
+import { TabContainer, Tab, ContentContainer, Content, BigUserProfile, LikeItem, InputLayout } from './styles';
const LikeCommentBottomSheetContent: React.FC = ({ tab, likeCount, commentCount }) => {
const [activeTab, setActiveTab] = useState<'likes' | 'comments'>(tab);
- const { postId } = useParams<{ postId: string }>();
const [likes, setLikes] = useState([]);
const [postLikeCount, setPostLikeCount] = useState(likeCount);
const [comments, setComments] = useState([]);
const [postCommentCount, setPostCommentCount] = useState(commentCount);
- const [isBlockConfirmationModalOpen, setIsBlockConfirmationModalOpen] = useState(false);
- const [isStatusModalOpen, setIsStatusModalOpen] = useState(false);
- const [modalContent, setModalContent] = useState('알 수 없는 오류입니다.\n관리자에게 문의해 주세요.');
const [isLoading, setIsLoading] = useState(false);
const [page, setPage] = useState(1);
@@ -59,50 +59,43 @@ const LikeCommentBottomSheetContent: React.FC = ({
const [isMenuVisible, setIsMenuVisible] = useState(false);
const [menuPosition, setMenuPosition] = useState<{ top: number; left: number }>({ top: 0, left: 0 });
+ const [isBlockConfirmationModalOpen, setIsBlockConfirmationModalOpen] = useState(false);
+ const [isStatusModalOpen, setIsStatusModalOpen] = useState(false);
const [isCommentDeleteConfirmationModalOpen, setIsCommentDeleteConfirmationModalOpen] = useRecoilState(
IsCommentDeleteConfirmationModalOpenAtom,
);
const [, setIsCommentReportModalOpen] = useRecoilState(IsCommentReportModalOpenAtom);
+ const [modalContent, setModalContent] = useState('알 수 없는 오류입니다.\n관리자에게 문의해 주세요.');
+ const { postId } = useParams<{ postId: string }>();
const nav = useNavigate();
- useEffect(() => {
- setPage(1);
- setReachedEnd(false);
- setLikes([]);
- setComments([]);
-
- if (activeTab === 'likes') {
- getPostLikeList(1);
- } else if (activeTab === 'comments') {
- getPostCommentList();
- }
- }, [activeTab]);
-
- // IntersectionObserver를 활용하여 무한 스크롤 감지
- useEffect(() => {
- if (observerRef.current) observerRef.current.disconnect();
-
- const handleIntersection = (entries: IntersectionObserverEntry[]) => {
- const [entry] = entries;
- if (entry.isIntersecting && !isLoading) {
- console.log('호출');
- getPostLikeList(page);
- }
- };
+ // 댓글 메뉴 클릭한 경우
+ const handleMenuOpen = (comment: Comment, event: React.MouseEvent) => {
+ setSelectedComment(comment);
+ const rect = event.currentTarget.getBoundingClientRect();
+ setMenuPosition({ top: rect.bottom + window.scrollY - 90, left: rect.left + window.scrollX - 100 });
+ setIsMenuVisible(true);
+ };
- observerRef.current = new IntersectionObserver(handleIntersection, {
- root: null,
- rootMargin: '0px',
- threshold: 1.0,
- });
+ // 유저 클릭한 경우
+ const handleUserClick = (userId: number) => {
+ // 로컬 스토리지에서 사용자 ID 가져오기
+ const myUserId = getCurrentUserId(); // 로컬 스토리지에 저장된 사용자 ID를 가져옴
- if (loadMoreRef.current) observerRef.current.observe(loadMoreRef.current);
+ if (String(myUserId) === String(userId)) {
+ // 나인 경우
+ nav(`/profile/${userId}`);
+ } else {
+ // 다른 유저인 경우
+ nav(`/users/${userId}`);
+ }
+ };
- return () => {
- if (observerRef.current) observerRef.current.disconnect();
- };
- }, [page, reachedEnd, loadMoreRef.current, activeTab]);
+ // 댓글 작성 Input
+ const handleInputChange = useCallback((e: React.ChangeEvent) => {
+ setInputValue(e.target.value);
+ }, []);
// 좋아요 리스트 불러오기 api
const getPostLikeList = async (currentPage: number) => {
@@ -155,11 +148,6 @@ const LikeCommentBottomSheetContent: React.FC = ({
}
};
- // 댓글 작성 Input
- const handleInputChange = useCallback((e: React.ChangeEvent) => {
- setInputValue(e.target.value);
- }, []);
-
// 댓글 작성 api
const createComment = async () => {
if (isSubmitting) return; // 중복 요청 방지
@@ -202,7 +190,7 @@ const LikeCommentBottomSheetContent: React.FC = ({
// 유저 차단 api
const postUserBlock = async () => {
- const storedUserId = localStorage.getItem('my_id');
+ const storedUserId = getCurrentUserId();
// 사용자 ID 또는 선택된 댓글이 없으면 함수 종료
if (!storedUserId || !selectedComment) {
@@ -213,8 +201,8 @@ const LikeCommentBottomSheetContent: React.FC = ({
try {
const blockRequest: PostUserBlockRequest = {
- fromUserId: Number(storedUserId),
- toUserId: selectedComment.user.id,
+ requesterId: Number(storedUserId),
+ targetId: selectedComment.user.id,
action: 'block',
};
@@ -232,6 +220,44 @@ const LikeCommentBottomSheetContent: React.FC = ({
}
};
+ useEffect(() => {
+ setPage(1);
+ setReachedEnd(false);
+ setLikes([]);
+ setComments([]);
+
+ if (activeTab === 'likes') {
+ getPostLikeList(1);
+ } else if (activeTab === 'comments') {
+ getPostCommentList();
+ }
+ }, [activeTab]);
+
+ // IntersectionObserver를 활용하여 무한 스크롤 감지
+ useEffect(() => {
+ if (observerRef.current) observerRef.current.disconnect();
+
+ const handleIntersection = (entries: IntersectionObserverEntry[]) => {
+ const [entry] = entries;
+ if (entry.isIntersecting && !isLoading) {
+ console.log('호출');
+ getPostLikeList(page);
+ }
+ };
+
+ observerRef.current = new IntersectionObserver(handleIntersection, {
+ root: null,
+ rootMargin: '0px',
+ threshold: 1.0,
+ });
+
+ if (loadMoreRef.current) observerRef.current.observe(loadMoreRef.current);
+
+ return () => {
+ if (observerRef.current) observerRef.current.disconnect();
+ };
+ }, [page, reachedEnd, loadMoreRef.current, activeTab]);
+
// 본인 댓글 메뉴 항목
const MyCommentMenuItems = [
{
@@ -306,28 +332,6 @@ const LikeCommentBottomSheetContent: React.FC = ({
},
};
- // 댓글 메뉴 클릭한 경우
- const handleMenuOpen = (comment: Comment, event: React.MouseEvent) => {
- setSelectedComment(comment);
- const rect = event.currentTarget.getBoundingClientRect();
- setMenuPosition({ top: rect.bottom + window.scrollY - 90, left: rect.left + window.scrollX - 100 });
- setIsMenuVisible(true);
- };
-
- // 유저 클릭한 경우
- const handleUserClick = (userId: number) => {
- // 로컬 스토리지에서 사용자 ID 가져오기
- const myUserId = localStorage.getItem('my_id'); // 로컬 스토리지에 저장된 사용자 ID를 가져옴
-
- if (String(myUserId) === String(userId)) {
- // 나인 경우
- nav('/mypage');
- } else {
- // 다른 유저인 경우
- nav(`/users/${userId}`);
- }
- };
-
return (
<>
diff --git a/src/components/PostBase/LikeCommentBottomSheetContent/styles.tsx b/src/pages/Post/PostBase/LikeCommentBottomSheetContent/styles.tsx
similarity index 86%
rename from src/components/PostBase/LikeCommentBottomSheetContent/styles.tsx
rename to src/pages/Post/PostBase/LikeCommentBottomSheetContent/styles.tsx
index 49de8dbf..4a481925 100644
--- a/src/components/PostBase/LikeCommentBottomSheetContent/styles.tsx
+++ b/src/pages/Post/PostBase/LikeCommentBottomSheetContent/styles.tsx
@@ -1,6 +1,9 @@
-import styled from 'styled-components';
-import { StyledText } from '../../Text/StyledText';
-import theme from '../../../styles/theme';
+import { styled } from 'styled-components';
+
+import theme from '@styles/theme';
+
+import { StyledText } from '@components/Text/StyledText';
+
import { UserProfile } from '../styles';
export const TabContainer = styled.div`
@@ -22,7 +25,7 @@ export const Tab = styled.div<{ $active: boolean }>`
left: 0;
width: 100%;
height: 2px; /* 하단 경계선 두께 */
- background: ${(props) => (props.$active ? theme.colors.gradient : 'none')};
+ background: ${(props) => (props.$active ? theme.colors.brand.gradient : 'none')};
}
`;
@@ -77,7 +80,7 @@ export const InputLayout = styled.div`
align-items: center;
gap: 10px;
background-color: white;
- border-top: 1px solid ${({ theme }) => theme.colors.gray1};
+ border-top: 1px solid ${({ theme }) => theme.colors.border.devider};
textarea {
flex: 1;
@@ -107,7 +110,7 @@ export const InputLayout = styled.div`
}
button {
- background: ${({ theme }) => theme.colors.gradient};
+ background: ${({ theme }) => theme.colors.brand.gradient};
width: 50px;
height: 50px;
border-radius: 8px;
diff --git a/src/components/PostBase/dto.ts b/src/pages/Post/PostBase/dto.ts
similarity index 100%
rename from src/components/PostBase/dto.ts
rename to src/pages/Post/PostBase/dto.ts
diff --git a/src/components/PostBase/index.tsx b/src/pages/Post/PostBase/index.tsx
similarity index 83%
rename from src/components/PostBase/index.tsx
rename to src/pages/Post/PostBase/index.tsx
index 73183f85..074145ed 100644
--- a/src/components/PostBase/index.tsx
+++ b/src/pages/Post/PostBase/index.tsx
@@ -1,22 +1,37 @@
-import React, { useEffect, useState, useRef } from 'react';
+import { useEffect, useState, useRef } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
-import { useRecoilState } from 'recoil';
import dayjs from 'dayjs';
+import { useRecoilState } from 'recoil';
import 'dayjs/locale/ko';
-import theme from '../../styles/theme';
+import theme from '@styles/theme';
+
+import { getPostDetailApi } from '@apis/post';
+import { togglePostLikeStatusApi } from '@apis/post-like';
+import { postIdAtom, userAtom, isPostRepresentativeAtom } from '@recoil/Post/PostAtom';
+import { getCurrentUserId } from '@utils/getCurrentUserId';
+
+import Left from '@assets/arrow/left.svg';
+import LikeFill from '@assets/default/like-fill.svg';
+import Like from '@assets/default/like.svg';
+import Message from '@assets/default/message.svg';
+import More from '@assets/default/more.svg';
-import { postIdAtom, userAtom, isPostRepresentativeAtom } from '../../recoil/Post/PostAtom';
+import BottomSheet from '@components/BottomSheet';
+import ClothingInfoItem from '@components/ClothingInfoItem';
+import { OODDFrame } from '@components/Frame/Frame';
+import NavBar from '@components/NavBar';
+import { StyledText } from '@components/Text/StyledText';
+import TopBar from '@components/TopBar';
-import { OODDFrame } from '../Frame/Frame';
-import { StyledText } from '../Text/StyledText';
-import TopBar from '../TopBar';
-import NavBar from '../NavBar';
-import BottomSheet from '../BottomSheet';
-import ClothingInfoItem from '../ClothingInfoItem';
-import ImageSwiper from './ImageSwiper';
-import LikeCommentBottomSheetContent from './LikeCommentBottomSheetContent';
+import type { GetPostDetailResponse } from '@apis/post/dto';
+import type { BottomSheetProps } from '@components/BottomSheet/dto';
+
+import type { PostBaseProps } from './dto';
+
+import ImageSwiper from './ImageSwiper/index';
+import LikeCommentBottomSheetContent from './LikeCommentBottomSheetContent/index';
import {
PostLayout,
@@ -36,21 +51,7 @@ import {
ClothingInfoList,
} from './styles';
-import Left from '../../assets/arrow/left.svg';
-import Like from '../../assets/default/like.svg';
-import LikeFill from '../../assets/default/like-fill.svg';
-import Message from '../../assets/default/message.svg';
-import More from '../../assets/default/more.svg';
-
-import { BottomSheetProps } from '../BottomSheet/dto';
-import { PostBaseProps } from './dto';
-import { GetPostDetailResponse } from '../../apis/post/dto';
-
-import { getPostDetailApi } from '../../apis/post';
-import { togglePostLikeStatusApi } from '../../apis/post-like';
-
const PostBase: React.FC = ({ onClickMenu }) => {
- const { postId } = useParams<{ postId: string }>();
const [, setPostId] = useRecoilState(postIdAtom);
const [post, setPost] = useState();
const [user, setUser] = useRecoilState(userAtom);
@@ -61,8 +62,56 @@ const PostBase: React.FC = ({ onClickMenu }) => {
const [isLikeCommentBottomSheetOpen, setIsLikeCommentBottomSheetOpen] = useState(false);
const [activeTab, setActiveTab] = useState<'likes' | 'comments'>('likes'); // activeTab state
+ const { postId } = useParams<{ postId: string }>();
+ const userId = getCurrentUserId();
+
const nav = useNavigate();
+ const handleLikeCommentOpen = (tab: 'likes' | 'comments') => {
+ setActiveTab(tab); // 클릭한 버튼에 따라 activeTab 설정
+ setIsLikeCommentBottomSheetOpen(true);
+ };
+
+ const handleUserClick = () => {
+ if (post?.isPostWriter) {
+ // 내 게시물인 경우
+ nav(`/profile/${userId}`);
+ } else {
+ // 다른 유저의 게시물인 경우
+ nav(`/users/${post?.user.id}`);
+ }
+ };
+
+ const contentRef = useRef(null);
+
+ const toggleTextDisplay = () => {
+ setShowFullText((prev) => !prev);
+ };
+
+ // 게시글 좋아요 누르기/취소하기 api
+ const togglePostLikeStatus = async () => {
+ if (!post || !postId) return;
+
+ const prevPost = { ...post }; // 현재 상태 저장
+ setPost({
+ ...post,
+ isPostLike: !post.isPostLike,
+ postLikesCount: post.isPostLike ? post.postLikesCount - 1 : post.postLikesCount + 1,
+ }); //사용자가 좋아요를 누르면 먼저 클라이언트에서 post 상태를 변경(낙관적 업데이트)
+
+ try {
+ const response = await togglePostLikeStatusApi(Number(postId));
+ setPost({
+ ...post,
+ isPostLike: response.data.isPostLike,
+ postLikesCount: response.data.postLikesCount,
+ }); // 서버로 요청 후 성공하면 그대로 유지
+ } catch (error) {
+ console.error('Error toggling like status:', error);
+ setPost(prevPost); // 실패하면 원래 상태로 롤백
+ }
+ };
+
useEffect(() => {
setPostId(Number(postId));
@@ -83,8 +132,6 @@ const PostBase: React.FC = ({ onClickMenu }) => {
getPost();
}, [postId]);
- const contentRef = useRef(null);
-
useEffect(() => {
if (contentRef.current) {
// 실제 높이와 줄 제한 높이 비교
@@ -93,25 +140,6 @@ const PostBase: React.FC = ({ onClickMenu }) => {
}
}, [post?.content]);
- const toggleTextDisplay = () => {
- setShowFullText((prev) => !prev);
- };
-
- const handleUserClick = () => {
- if (post?.isPostWriter) {
- // 내 게시물인 경우
- nav('/mypage');
- } else {
- // 다른 유저의 게시물인 경우
- nav(`/users/${post?.user.id}`);
- }
- };
-
- const handleLikeCommentOpen = (tab: 'likes' | 'comments') => {
- setActiveTab(tab); // 클릭한 버튼에 따라 activeTab 설정
- setIsLikeCommentBottomSheetOpen(true);
- };
-
const likeCommentbottomSheetProps: BottomSheetProps = {
isOpenBottomSheet: isLikeCommentBottomSheetOpen,
isHandlerVisible: true,
@@ -126,30 +154,6 @@ const PostBase: React.FC = ({ onClickMenu }) => {
},
};
- // 게시글 좋아요 누르기/취소하기
- const togglePostLikeStatus = async () => {
- if (!post || !postId) return;
-
- const prevPost = { ...post }; // 현재 상태 저장
- setPost({
- ...post,
- isPostLike: !post.isPostLike,
- postLikesCount: post.isPostLike ? post.postLikesCount - 1 : post.postLikesCount + 1,
- }); //사용자가 좋아요를 누르면 먼저 클라이언트에서 post 상태를 변경(낙관적 업데이트)
-
- try {
- const response = await togglePostLikeStatusApi(Number(postId));
- setPost({
- ...post,
- isPostLike: response.data.isPostLike,
- postLikesCount: response.data.postLikesCount,
- }); // 서버로 요청 후 성공하면 그대로 유지
- } catch (error) {
- console.error('Error toggling like status:', error);
- setPost(prevPost); // 실패하면 원래 상태로 롤백
- }
- };
-
return (
diff --git a/src/components/PostBase/styles.tsx b/src/pages/Post/PostBase/styles.tsx
similarity index 97%
rename from src/components/PostBase/styles.tsx
rename to src/pages/Post/PostBase/styles.tsx
index bf1d6dbe..856e812d 100644
--- a/src/components/PostBase/styles.tsx
+++ b/src/pages/Post/PostBase/styles.tsx
@@ -1,5 +1,6 @@
-import styled, { keyframes } from 'styled-components';
-import { StyledText } from '../Text/StyledText';
+import { styled, keyframes } from 'styled-components';
+
+import { StyledText } from '@components/Text/StyledText';
// 그라데이션 애니메이션 정의
const shimmer = keyframes`
diff --git a/src/pages/PostImageSelect/ImageSwiper/index.tsx b/src/pages/Post/PostImageSelect/ImageSwiper/index.tsx
similarity index 90%
rename from src/pages/PostImageSelect/ImageSwiper/index.tsx
rename to src/pages/Post/PostImageSelect/ImageSwiper/index.tsx
index c70cce07..27025f65 100644
--- a/src/pages/PostImageSelect/ImageSwiper/index.tsx
+++ b/src/pages/Post/PostImageSelect/ImageSwiper/index.tsx
@@ -1,16 +1,17 @@
-import React, { useRef, useState } from 'react';
-import { Swiper, SwiperSlide } from 'swiper/react';
+import { useRef, useState } from 'react';
+
import { Navigation, Pagination } from 'swiper/modules';
+import { Swiper, SwiperSlide } from 'swiper/react';
import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';
-import { SwiperContainer, ImageWrapper, RemoveButton, StyledNavigation, AddButton, HiddenFileInput } from './styles';
+import Plus from '@assets/default/plus.svg';
+import Reject from '@assets/default/reject.svg';
-import Reject from '../../../assets/default/reject.svg';
-import Plus from '../../../assets/default/plus.svg';
+import type { ImageSwiperProps } from '../dto';
-import { ImageSwiperProps } from '../dto';
+import { SwiperContainer, ImageWrapper, RemoveButton, StyledNavigation, AddButton, HiddenFileInput } from './styles';
const ImageSwiper: React.FC = ({ images, onProcessFile, onRemoveImage }) => {
const fileInputRef = useRef(null);
diff --git a/src/pages/PostImageSelect/ImageSwiper/styles.tsx b/src/pages/Post/PostImageSelect/ImageSwiper/styles.tsx
similarity index 97%
rename from src/pages/PostImageSelect/ImageSwiper/styles.tsx
rename to src/pages/Post/PostImageSelect/ImageSwiper/styles.tsx
index f7e511e5..444ba68c 100644
--- a/src/pages/PostImageSelect/ImageSwiper/styles.tsx
+++ b/src/pages/Post/PostImageSelect/ImageSwiper/styles.tsx
@@ -1,4 +1,4 @@
-import styled from 'styled-components';
+import { styled } from 'styled-components';
export const SwiperContainer = styled.div`
display: flex;
diff --git a/src/pages/PostImageSelect/dto.ts b/src/pages/Post/PostImageSelect/dto.ts
similarity index 61%
rename from src/pages/PostImageSelect/dto.ts
rename to src/pages/Post/PostImageSelect/dto.ts
index 15dd65bf..c9272190 100644
--- a/src/pages/PostImageSelect/dto.ts
+++ b/src/pages/Post/PostImageSelect/dto.ts
@@ -1,5 +1,4 @@
-export interface ImageSelectModalProps {}
-import { PostImage } from '../../apis/post/dto';
+import { PostImage } from '@apis/post/dto';
export interface ImageSwiperProps {
images: PostImage[];
diff --git a/src/pages/PostImageSelect/index.tsx b/src/pages/Post/PostImageSelect/index.tsx
similarity index 86%
rename from src/pages/PostImageSelect/index.tsx
rename to src/pages/Post/PostImageSelect/index.tsx
index 9a5c8cea..6554d2e5 100644
--- a/src/pages/PostImageSelect/index.tsx
+++ b/src/pages/Post/PostImageSelect/index.tsx
@@ -1,31 +1,31 @@
-import React, { useState, useRef } from 'react';
+import { useState, useRef } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
+import heic2any from 'heic2any';
import { useRecoilState } from 'recoil';
+
import {
postImagesAtom,
postContentAtom,
postClothingInfosAtom,
postStyletagAtom,
postIsRepresentativeAtom,
-} from '../../recoil/PostUpload/PostUploadAtom';
-
-import { UploadContainer, ImageDragDropContainer, Content } from './styles';
+} from '@recoil/PostUpload/PostUploadAtom';
+import { getCurrentUserId } from '@utils/getCurrentUserId';
-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 Left from '@assets/arrow/left.svg';
+import PhotoBig from '@assets/default/photo-big.svg';
+import X from '@assets/default/x.svg';
-import X from '../../assets/default/x.svg';
-import Left from '../../assets/arrow/left.svg';
-import PhotoBig from '../../assets/default/photo-big.svg';
+import BottomButton from '@components/BottomButton';
+import { OODDFrame } from '@components/Frame/Frame';
+import { StyledText } from '@components/Text/StyledText';
+import TopBar from '@components/TopBar';
-import { ImageSelectModalProps } from './dto';
-import heic2any from 'heic2any';
+import ImageSwiper from './ImageSwiper';
+import { UploadContainer, ImageDragDropContainer, Content } from './styles';
-const PostImageSelect: React.FC = () => {
+const PostImageSelect: React.FC = () => {
const [images, setImages] = useRecoilState(postImagesAtom);
const [, setContent] = useRecoilState(postContentAtom);
const [, setClothingInfos] = useRecoilState(postClothingInfosAtom);
@@ -35,9 +35,10 @@ const PostImageSelect: React.FC = () => {
const fileInputRef = useRef(null);
const location = useLocation();
const navigate = useNavigate();
+ const userId = getCurrentUserId();
const handleClose = () => {
- navigate('/mypage');
+ navigate(`/profile/${userId}`);
};
const handlePrev = () => {
@@ -50,7 +51,7 @@ const PostImageSelect: React.FC = () => {
const handleNext = () => {
const state = location.state as { mode?: string; postId?: number };
- navigate('/upload', { state: { mode: state?.mode, postId: state?.postId } });
+ navigate('/post/upload/content', { state: { mode: state?.mode, postId: state?.postId } });
};
// 파일 선택기에서 사진 업로드
@@ -141,7 +142,7 @@ const PostImageSelect: React.FC = () => {
{images.length === 0 ? (
diff --git a/src/pages/PostImageSelect/styles.tsx b/src/pages/Post/PostImageSelect/styles.tsx
similarity index 95%
rename from src/pages/PostImageSelect/styles.tsx
rename to src/pages/Post/PostImageSelect/styles.tsx
index 2fbe61c2..98cab6b6 100644
--- a/src/pages/PostImageSelect/styles.tsx
+++ b/src/pages/Post/PostImageSelect/styles.tsx
@@ -1,4 +1,4 @@
-import styled from 'styled-components';
+import { styled } from 'styled-components';
export const UploadContainer = styled.div`
flex-grow: 1;
diff --git a/src/pages/PostInstaConnect/dto.ts b/src/pages/Post/PostInstaConnect/dto.ts
similarity index 58%
rename from src/pages/PostInstaConnect/dto.ts
rename to src/pages/Post/PostInstaConnect/dto.ts
index 2a10b46e..1db54e31 100644
--- a/src/pages/PostInstaConnect/dto.ts
+++ b/src/pages/Post/PostInstaConnect/dto.ts
@@ -1,5 +1,3 @@
-export interface InstaConnectModalProps {}
-
export interface Post {
imgs: string[];
caption: string;
diff --git a/src/pages/PostInstaConnect/index.tsx b/src/pages/Post/PostInstaConnect/index.tsx
similarity index 79%
rename from src/pages/PostInstaConnect/index.tsx
rename to src/pages/Post/PostInstaConnect/index.tsx
index 27c09fc4..f7eb9450 100644
--- a/src/pages/PostInstaConnect/index.tsx
+++ b/src/pages/Post/PostInstaConnect/index.tsx
@@ -1,20 +1,20 @@
-import React, { useState } from 'react';
+import { useState } from 'react';
-import theme from '../../styles/theme';
-import { Content, StyledInput } from './styles';
+import theme from '@styles/theme';
+
+import X from '@assets/default/x.svg';
-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 BottomButton from '@components/BottomButton';
+import { OODDFrame } from '@components/Frame/Frame';
+import Modal from '@components/Modal';
+import { StyledText } from '@components/Text/StyledText';
+import TopBar from '@components/TopBar';
-import X from '../../assets/default/x.svg';
+import { ModalProps } from '@components/Modal/dto';
-import { InstaConnectModalProps } from './dto';
+import { Content, StyledInput } from './styles';
-const PostInstaConnect: React.FC = () => {
+const PostInstaConnect: React.FC = () => {
const [instagramID, setInstagramID] = useState('');
const [isConnectFailModalOpen, setIsConnectFailModalOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
diff --git a/src/pages/PostInstaConnect/styles.tsx b/src/pages/Post/PostInstaConnect/styles.tsx
similarity index 96%
rename from src/pages/PostInstaConnect/styles.tsx
rename to src/pages/Post/PostInstaConnect/styles.tsx
index 5d2cc0d4..f4c9c7dc 100644
--- a/src/pages/PostInstaConnect/styles.tsx
+++ b/src/pages/Post/PostInstaConnect/styles.tsx
@@ -1,4 +1,4 @@
-import styled from 'styled-components';
+import { styled } from 'styled-components';
export const UploadContainer = styled.div`
flex-grow: 1;
diff --git a/src/pages/Post/PostInstaFeedSelect/dto.ts b/src/pages/Post/PostInstaFeedSelect/dto.ts
new file mode 100644
index 00000000..aeec4b58
--- /dev/null
+++ b/src/pages/Post/PostInstaFeedSelect/dto.ts
@@ -0,0 +1,3 @@
+export interface Post {
+ imgs: string[];
+}
diff --git a/src/pages/PostInstaFeedSelect/index.tsx b/src/pages/Post/PostInstaFeedSelect/index.tsx
similarity index 77%
rename from src/pages/PostInstaFeedSelect/index.tsx
rename to src/pages/Post/PostInstaFeedSelect/index.tsx
index 8df6a31b..96d3d976 100644
--- a/src/pages/PostInstaFeedSelect/index.tsx
+++ b/src/pages/Post/PostInstaFeedSelect/index.tsx
@@ -1,27 +1,32 @@
-import React, { useState } from 'react';
+import { 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 { postImagesAtom } from '@recoil/PostUpload/PostUploadAtom';
+import { getCurrentUserId } from '@utils/getCurrentUserId';
+
+import X from '@assets/default/x.svg';
-import { OODDFrame } from '../../components/Frame/Frame';
-import TopBar from '../../components/TopBar';
-import Modal from '../../components/Modal';
-import { ModalProps } from '../../components/Modal/dto';
+import { OODDFrame } from '@components/Frame/Frame';
+import Modal from '@components/Modal';
+import TopBar from '@components/TopBar';
-import X from '../../assets/default/x.svg';
+import type { ModalProps } from '@components/Modal/dto';
-import { InstaFeedSelectModalProps, Post } from './dto';
+import type { Post } from './dto';
+
+import { Content, PostContainer, ImageWrapper } from './styles';
-const PostInstaFeedSelect: React.FC = () => {
+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 userId = getCurrentUserId();
// 인스타그램 데이터 가져오는 함수
const fetchInstagramData = async (accessToken: string) => {
@@ -65,19 +70,19 @@ const PostInstaFeedSelect: React.FC = () => {
const handlePostSelect = (post: Post) => {
const newImages = post.imgs.map((url, index) => ({ url, orderNum: index }));
setImages(newImages); // 선택한 이미지 Recoil 상태로 설정
- navigate('/upload'); // 다음 페이지로 이동
+ navigate('/post/upload/content'); // 다음 페이지로 이동
};
// 페이지 종료 함수
const handleClose = () => {
- navigate('/mypage'); // 마이페이지로 이동
+ navigate(`/profile/${userId}`);
};
return (
{isSuccessModalOpen && }
{isFailModalOpen && }
- {' '}
+ {' '}
{posts.map((post, index) => (
handlePostSelect(post)}>
diff --git a/src/pages/PostInstaFeedSelect/styles.tsx b/src/pages/Post/PostInstaFeedSelect/styles.tsx
similarity index 96%
rename from src/pages/PostInstaFeedSelect/styles.tsx
rename to src/pages/Post/PostInstaFeedSelect/styles.tsx
index 295bd824..226ca78b 100644
--- a/src/pages/PostInstaFeedSelect/styles.tsx
+++ b/src/pages/Post/PostInstaFeedSelect/styles.tsx
@@ -1,4 +1,4 @@
-import styled from 'styled-components';
+import { styled } from 'styled-components';
export const UploadContainer = styled.div`
flex-grow: 1;
diff --git a/src/pages/PostUpload/ImageSwiper/index.tsx b/src/pages/Post/PostUpload/ImageSwiper/index.tsx
similarity index 93%
rename from src/pages/PostUpload/ImageSwiper/index.tsx
rename to src/pages/Post/PostUpload/ImageSwiper/index.tsx
index 81fe5322..50677c5d 100644
--- a/src/pages/PostUpload/ImageSwiper/index.tsx
+++ b/src/pages/Post/PostUpload/ImageSwiper/index.tsx
@@ -1,15 +1,16 @@
-import React, { useRef } from 'react';
-import { Swiper, SwiperRef, SwiperSlide } from 'swiper/react';
+import { useRef } from 'react';
+
import { Navigation, Pagination } from 'swiper/modules';
+import { Swiper, SwiperRef, SwiperSlide } from 'swiper/react';
import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';
-import { SwiperContainer, ImageWrapper, StyledNavigation, StyledPagination } from './styles';
+import PhotoWhite from '@assets/default/photo-white.svg';
-import PhotoWhite from '../../../assets/default/photo-white.svg';
+import type { ImageSwiperProps } from '../dto';
-import { ImageSwiperProps } from '../dto';
+import { SwiperContainer, ImageWrapper, StyledNavigation, StyledPagination } from './styles';
const ImageSwiper: React.FC = ({ images }) => {
const swiperRef = useRef(null);
diff --git a/src/pages/PostUpload/ImageSwiper/styles.tsx b/src/pages/Post/PostUpload/ImageSwiper/styles.tsx
similarity index 97%
rename from src/pages/PostUpload/ImageSwiper/styles.tsx
rename to src/pages/Post/PostUpload/ImageSwiper/styles.tsx
index be380107..0f216167 100644
--- a/src/pages/PostUpload/ImageSwiper/styles.tsx
+++ b/src/pages/Post/PostUpload/ImageSwiper/styles.tsx
@@ -1,4 +1,4 @@
-import styled from 'styled-components';
+import { styled } from 'styled-components';
export const SwiperContainer = styled.div`
display: flex;
diff --git a/src/pages/PostUpload/SearchBottomSheetContent/index.tsx b/src/pages/Post/PostUpload/SearchBottomSheetContent/index.tsx
similarity index 96%
rename from src/pages/PostUpload/SearchBottomSheetContent/index.tsx
rename to src/pages/Post/PostUpload/SearchBottomSheetContent/index.tsx
index b3c1b0bc..66cb32da 100644
--- a/src/pages/PostUpload/SearchBottomSheetContent/index.tsx
+++ b/src/pages/Post/PostUpload/SearchBottomSheetContent/index.tsx
@@ -1,12 +1,14 @@
-import React, { useState, useEffect, useRef } from 'react';
+import { useState, useEffect, useRef } from 'react';
+
import axios from 'axios';
-import { Content, Input, SearchResultList, SearchResultItem, Loader } from './styles';
-import theme from '../../../styles/theme';
+import theme from '@styles/theme';
-import { StyledText } from '../../../components/Text/StyledText';
+import { StyledText } from '@components/Text/StyledText';
-import { SearchBottomSheetProps } from '../dto';
+import type { SearchBottomSheetProps } from '../dto';
+
+import { Content, Input, SearchResultList, SearchResultItem, Loader } from './styles';
const SearchBottomSheetContent: React.FC = ({ onClose, onSelectClothingInfo }) => {
const [searchQuery, setSearchQuery] = useState('');
@@ -93,6 +95,7 @@ const SearchBottomSheetContent: React.FC = ({ onClose, o
// 타겟 요소가 뷰포트에 들어오고 로딩 중이 아닐 때 결과 로드 함수 호출
if (entry.isIntersecting && !isLoading) {
loadResults();
+ console.log('감지');
}
};
diff --git a/src/pages/PostUpload/SearchBottomSheetContent/styles.tsx b/src/pages/Post/PostUpload/SearchBottomSheetContent/styles.tsx
similarity index 90%
rename from src/pages/PostUpload/SearchBottomSheetContent/styles.tsx
rename to src/pages/Post/PostUpload/SearchBottomSheetContent/styles.tsx
index c2639a89..fdee8c66 100644
--- a/src/pages/PostUpload/SearchBottomSheetContent/styles.tsx
+++ b/src/pages/Post/PostUpload/SearchBottomSheetContent/styles.tsx
@@ -1,5 +1,5 @@
// src/pages/ProfilePage/BottomSheet/styles.tsx
-import styled from 'styled-components';
+import { styled } from 'styled-components';
export const Content = styled.div`
height: calc(100vh - 44px);
@@ -132,6 +132,13 @@ export const SearchResultItem = styled.div`
.detail {
margin-right: auto;
color: ${({ theme }) => theme.colors.black};
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: 2; /* 최대 2줄로 제한 */
+ -webkit-box-orient: vertical;
+ word-break: keep-all; /* 단어 단위로 줄바꿈 */
+ overflow-wrap: break-word;
}
&:last-child {
diff --git a/src/pages/PostUpload/ToggleSwitch/index.tsx b/src/pages/Post/PostUpload/ToggleSwitch/index.tsx
similarity index 78%
rename from src/pages/PostUpload/ToggleSwitch/index.tsx
rename to src/pages/Post/PostUpload/ToggleSwitch/index.tsx
index aaf748bb..6b1d99c1 100644
--- a/src/pages/PostUpload/ToggleSwitch/index.tsx
+++ b/src/pages/Post/PostUpload/ToggleSwitch/index.tsx
@@ -1,6 +1,6 @@
-import React from 'react';
+import type { ToggleSwitchProps } from '../dto';
+
import { HiddenCheckbox } from './styles';
-import { ToggleSwitchProps } from '../dto';
const ToggleSwitch: React.FC = ({ checked, onChange, disabled }) => {
return ;
diff --git a/src/pages/PostUpload/ToggleSwitch/styles.tsx b/src/pages/Post/PostUpload/ToggleSwitch/styles.tsx
similarity index 94%
rename from src/pages/PostUpload/ToggleSwitch/styles.tsx
rename to src/pages/Post/PostUpload/ToggleSwitch/styles.tsx
index 294066bd..1672e375 100644
--- a/src/pages/PostUpload/ToggleSwitch/styles.tsx
+++ b/src/pages/Post/PostUpload/ToggleSwitch/styles.tsx
@@ -1,4 +1,4 @@
-import styled from 'styled-components';
+import { styled } from 'styled-components';
export const HiddenCheckbox = styled.input.attrs({ type: 'checkbox' })`
appearance: none;
diff --git a/src/pages/PostUpload/dto.ts b/src/pages/Post/PostUpload/dto.ts
similarity index 76%
rename from src/pages/PostUpload/dto.ts
rename to src/pages/Post/PostUpload/dto.ts
index 743ad97f..6e8025bc 100644
--- a/src/pages/PostUpload/dto.ts
+++ b/src/pages/Post/PostUpload/dto.ts
@@ -1,5 +1,5 @@
-import { ClothingInfo } from '../../components/ClothingInfoItem/dto';
-import { PostImage } from '../../apis/post/dto';
+import type { PostImage } from '@apis/post/dto';
+import type { ClothingInfo } from '@components/ClothingInfoItem/dto';
export interface PostUploadModalProps {
postId?: number | null;
diff --git a/src/pages/PostUpload/index.tsx b/src/pages/Post/PostUpload/index.tsx
similarity index 87%
rename from src/pages/PostUpload/index.tsx
rename to src/pages/Post/PostUpload/index.tsx
index 1b4f7b86..988b0289 100644
--- a/src/pages/PostUpload/index.tsx
+++ b/src/pages/Post/PostUpload/index.tsx
@@ -1,8 +1,14 @@
//PostUploadModal/index.tsx
-import React, { useState, useEffect } from 'react';
+import { useState, useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
+import { ref, uploadBytes, getDownloadURL } from 'firebase/storage';
import { useRecoilState } from 'recoil';
+
+import { getPostDetailApi, createPostApi, modifyPostApi } from '@apis/post';
+import { PostBase } from '@apis/post/dto';
+import { handleError } from '@apis/util/handleError';
+import { storage } from '@config/firebaseConfig';
import {
postImagesAtom,
postContentAtom,
@@ -10,7 +16,32 @@ import {
postStyletagAtom,
postIsRepresentativeAtom,
modeAtom,
-} from '../../recoil/PostUpload/PostUploadAtom';
+} from '@recoil/PostUpload/PostUploadAtom';
+import { getCurrentUserId } from '@utils/getCurrentUserId';
+
+import Left from '@assets/arrow/left.svg';
+import Right from '@assets/arrow/right.svg';
+import Up from '@assets/arrow/up.svg';
+import ClothingTag from '@assets/default/clothes-tag.svg';
+import Pin from '@assets/default/pin.svg';
+import StyleTag from '@assets/default/style-tag.svg';
+
+import BottomButton from '@components/BottomButton';
+import BottomSheet from '@components/BottomSheet';
+import ClothingInfoItem from '@components/ClothingInfoItem';
+import { OODDFrame } from '@components/Frame/Frame';
+import Modal from '@components/Modal';
+import { StyledText } from '@components/Text/StyledText';
+import TopBar from '@components/TopBar';
+
+import type { BottomSheetProps } from '@components/BottomSheet/dto';
+import type { ClothingInfo } from '@components/ClothingInfoItem/dto';
+import type { ModalProps } from '@components/Modal/dto';
+
+import type { PostUploadModalProps } from './dto';
+
+import ImageSwiper from './ImageSwiper/index';
+import SearchBottomSheetContent from './SearchBottomSheetContent/index';
import {
UploadContainer,
@@ -22,35 +53,7 @@ import {
StyletagItem,
PinnedPostToggleContainer,
} from './styles';
-
-import { OODDFrame } from '../../components/Frame/Frame';
-import { StyledText } from '../../components/Text/StyledText';
-import TopBar from '../../components/TopBar';
-import BottomSheet from '../../components/BottomSheet';
-import { BottomSheetProps } from '../../components/BottomSheet/dto';
-import BottomButton from '../../components/BottomButton';
-import ClothingInfoItem from '../../components/ClothingInfoItem';
-import ImageSwiper from './ImageSwiper';
-import SearchBottomSheetContent from './SearchBottomSheetContent';
import ToggleSwitch from './ToggleSwitch';
-import Modal from '../../components/Modal';
-
-import Left from '../../assets/arrow/left.svg';
-import Right from '../../assets/arrow/right.svg';
-import Up from '../../assets/arrow/up.svg';
-import ClothingTag from '../../assets/default/clothes-tag.svg';
-import StyleTag from '../../assets/default/style-tag.svg';
-import Pin from '../../assets/default/pin.svg';
-
-import { ClothingInfo } from '../../components/ClothingInfoItem/dto';
-import { ModalProps } from '../../components/Modal/dto';
-import { PostUploadModalProps } from './dto';
-import { PostBase } from '../../apis/post/dto';
-
-import { ref, uploadBytes, getDownloadURL } from 'firebase/storage';
-import { storage } from '../../config/firebaseConfig';
-import { getPostDetailApi, createPostApi, modifyPostApi } from '../../apis/post';
-import { handleError } from '../../apis/util/handleError';
const PostUpload: React.FC = () => {
const [selectedImages, setSelectedImages] = useRecoilState(postImagesAtom);
@@ -66,6 +69,7 @@ const PostUpload: React.FC = () => {
const [modalContent, setModalContent] = useState('알 수 없는 오류입니다.\n관리자에게 문의해 주세요.');
const location = useLocation();
const navigate = useNavigate();
+ const userId = getCurrentUserId();
const styletags = [
'classic',
@@ -100,7 +104,7 @@ const PostUpload: React.FC = () => {
if (mode === 'edit') {
setMode('edit2');
}
- navigate('/image-select', { state: { mode: mode, postId: state.postId } });
+ navigate('/post/upload/photo/select', { state: { mode: mode, postId: state.postId } });
};
const getPost = async (postId: number) => {
@@ -286,7 +290,7 @@ const PostUpload: React.FC = () => {
setSelectedStyletag([]);
setMode('');
- navigate('/mypage');
+ navigate(`/profile/${userId}`);
} catch (error) {
const errorMessage = handleError(error, 'post');
setModalContent(errorMessage);
@@ -307,7 +311,7 @@ const PostUpload: React.FC = () => {
return (
-
+
= () => {

-
+
옷 정보 태그
{clothingInfos.length > 0 && (
-
+
{clothingInfos.length}
)}
@@ -385,7 +389,7 @@ const PostUpload: React.FC = () => {
diff --git a/src/pages/PostUpload/styles.tsx b/src/pages/Post/PostUpload/styles.tsx
similarity index 91%
rename from src/pages/PostUpload/styles.tsx
rename to src/pages/Post/PostUpload/styles.tsx
index 39f909a1..bd8ef4fe 100644
--- a/src/pages/PostUpload/styles.tsx
+++ b/src/pages/Post/PostUpload/styles.tsx
@@ -1,4 +1,4 @@
-import styled from 'styled-components';
+import { styled } from 'styled-components';
export const UploadContainer = styled.div`
flex-grow: 1;
@@ -187,17 +187,3 @@ export const PinnedPostToggleContainer = styled.label`
margin-left: auto;
}
`;
-
-export const ButtonWrapper = styled.div`
- display: flex;
- position: absolute;
- bottom: 0;
- left: 50%;
- transform: translateX(-50%);
- width: 100%;
- height: 6.25rem;
- background-color: ${({ theme }) => theme.colors.white};
- justify-content: flex-end;
- z-index: 1;
- border: none;
-`;
diff --git a/src/pages/Post/index.tsx b/src/pages/Post/index.tsx
index 69986ff1..b3b8b0d7 100644
--- a/src/pages/Post/index.tsx
+++ b/src/pages/Post/index.tsx
@@ -1,22 +1,133 @@
-import React, { useState } from 'react';
+import { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
-import { useRecoilValue } from 'recoil';
-import { postIdAtom, userAtom } from '../../recoil/Post/PostAtom.ts';
+import { useRecoilValue, useRecoilState } from 'recoil';
-import PostBase from '../../components/PostBase/index.tsx';
-import OptionsBottomSheet from '../../components/BottomSheet/OptionsBottomSheet/index.tsx';
-import { OptionsBottomSheetProps } from '../../components/BottomSheet/OptionsBottomSheet/dto.ts';
+import { modifyPostRepresentativeStatusApi, deletePostApi } from '@apis/post';
+import { isPostRepresentativeAtom, postIdAtom, userAtom } from '@recoil/Post/PostAtom';
+import { getCurrentUserId } from '@utils/getCurrentUserId';
+
+import Delete from '@assets/default/delete.svg';
+import Edit from '@assets/default/edit.svg';
+import Pin from '@assets/default/pin.svg';
+
+import BottomSheet from '@components/BottomSheet';
+import BottomSheetMenu from '@components/BottomSheet/BottomSheetMenu';
+import OptionsBottomSheet from '@components/BottomSheet/OptionsBottomSheet';
+import Modal from '@components/Modal';
+
+import type { BottomSheetMenuProps } from '@components/BottomSheet/BottomSheetMenu/dto';
+import type { BottomSheetProps } from '@components/BottomSheet/dto';
+import type { OptionsBottomSheetProps } from '@components/BottomSheet/OptionsBottomSheet/dto';
+import type { ModalProps } from '@components/Modal/dto';
+
+import PostBase from './PostBase/index';
const Post: React.FC = () => {
- const [isOptionsBottomSheetOpen, setIsOptionsBottomSheetOpen] = useState(false);
- const postId = useRecoilValue(postIdAtom);
const user = useRecoilValue(userAtom);
+ const postId = useRecoilValue(postIdAtom);
+ const [isMyPost, setIsMyPost] = useState(false);
+ const [isPostRepresentative, setIsPostRepresentative] = useRecoilState(isPostRepresentativeAtom);
+
+ const [isMyPostMenuBottomSheetOpen, setIsMyPostMenuBottomSheetOpen] = useState(false);
+ const [isOptionsBottomSheetOpen, setIsOptionsBottomSheetOpen] = useState(false);
+
+ const [isDeleteConfirmationModalOpen, setIsDeleteConfirmationModalOpen] = useState(false);
+ const [isApiResponseModalOpen, setIsApiResponseModalOpen] = useState(false);
+ const [modalContent, setModalContent] = useState('');
+ const [postPinStatus, setPostPinStatus] = useState<'지정' | '해제'>('지정');
+
+ const userId = getCurrentUserId();
+ const navigate = useNavigate();
const handleMenuOpen = () => {
- setIsOptionsBottomSheetOpen(true);
+ if (isMyPost) {
+ setIsMyPostMenuBottomSheetOpen(true);
+ } else {
+ setIsOptionsBottomSheetOpen(true);
+ }
+ };
+
+ const modifyPostRepresentativeStatus = async () => {
+ try {
+ const response = await modifyPostRepresentativeStatusApi(Number(postId));
+
+ if (response.isSuccess) {
+ setModalContent(`대표 OOTD ${postPinStatus}에 성공했어요`);
+ setIsPostRepresentative((prev) => !prev);
+ } else {
+ setModalContent(`대표 OOTD ${postPinStatus}에 실패했어요\n잠시 뒤 다시 시도해 보세요`);
+ }
+ } catch (error) {
+ console.error('Error pinning post:', error);
+ } finally {
+ setIsApiResponseModalOpen(true);
+ }
+ };
+
+ const deletePost = async () => {
+ try {
+ const response = await deletePostApi(Number(postId));
+
+ if (response.isSuccess) {
+ setModalContent('OOTD 삭제에 성공했어요');
+
+ setTimeout(() => {
+ navigate(`/profile/${userId}`);
+ }, 1000);
+ } else {
+ setModalContent(`OOTD 삭제에 실패했어요\n잠시 뒤 다시 시도해 보세요`);
+ }
+ } catch (error) {
+ console.error('Error deleting post:', error);
+ } finally {
+ setIsApiResponseModalOpen(true);
+ setIsDeleteConfirmationModalOpen(false); // 확인 모달을 닫음
+ }
+ };
+
+ useEffect(() => {
+ // 현재 게시글이 내 게시글인지 확인
+ if (user?.id && postId) {
+ setIsMyPost(Number(userId) === user.id);
+ }
+ }, [user, postId]);
+
+ useEffect(() => {
+ setPostPinStatus(isPostRepresentative ? '해제' : '지정');
+ }, [isPostRepresentative]);
+
+ // MyPost 전용 메뉴 구성
+ const myPostMenuProps: BottomSheetMenuProps = {
+ items: [
+ {
+ text: `대표 OOTD ${postPinStatus}하기`,
+ action: () => {
+ setIsMyPostMenuBottomSheetOpen(false);
+ modifyPostRepresentativeStatus();
+ },
+ icon: Pin,
+ },
+ {
+ text: 'OOTD 수정하기',
+ action: () => {
+ setIsMyPostMenuBottomSheetOpen(false);
+ navigate('/post/upload/content', { state: { mode: 'edit', postId: postId } });
+ },
+ icon: Edit,
+ },
+ {
+ text: 'OOTD 삭제하기',
+ action: () => {
+ setIsMyPostMenuBottomSheetOpen(false);
+ setIsDeleteConfirmationModalOpen(true);
+ },
+ icon: Delete,
+ },
+ ],
};
- // 게시글 옵션(더보기) 바텀시트
+ // 일반 Post 메뉴 구성
const optionsBottomSheetProps: OptionsBottomSheetProps = {
domain: 'post',
targetId: {
@@ -30,11 +141,37 @@ const Post: React.FC = () => {
},
};
+ const menuBottomSheetProps: BottomSheetProps = {
+ isOpenBottomSheet: isMyPostMenuBottomSheetOpen,
+ Component: BottomSheetMenu,
+ componentProps: isMyPost ? myPostMenuProps : optionsBottomSheetProps,
+ onCloseBottomSheet: () => setIsMyPostMenuBottomSheetOpen(false),
+ };
+
+ const deleteConfirmationModalProps: ModalProps = {
+ isCloseButtonVisible: true,
+ onClose: () => setIsDeleteConfirmationModalOpen(false),
+ content: '해당 OOTD를 삭제하시겠습니까?',
+ button: {
+ content: '삭제하기',
+ onClick: deletePost,
+ },
+ };
+
+ const apiResponseModalProps: ModalProps = {
+ onClose: () => setIsApiResponseModalOpen(false),
+ content: modalContent,
+ };
+
return (
<>
+
+
+ {isDeleteConfirmationModalOpen && }
+ {isApiResponseModalOpen && }
>
);
};
diff --git a/src/pages/Post/styles.tsx b/src/pages/Post/styles.tsx
index e7119c4a..437fc956 100644
--- a/src/pages/Post/styles.tsx
+++ b/src/pages/Post/styles.tsx
@@ -1,4 +1,4 @@
-import styled from 'styled-components';
+import { styled } from 'styled-components';
export const InputLayout = styled.div`
display: flex;
diff --git a/src/pages/PostInstaFeedSelect/dto.ts b/src/pages/PostInstaFeedSelect/dto.ts
deleted file mode 100644
index 82dee836..00000000
--- a/src/pages/PostInstaFeedSelect/dto.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export interface InstaFeedSelectModalProps {}
-
-export interface Post {
- imgs: string[];
-}
diff --git a/src/pages/MyPage/ButtonSecondary/index.tsx b/src/pages/Profile/ButtonSecondary/index.tsx
similarity index 54%
rename from src/pages/MyPage/ButtonSecondary/index.tsx
rename to src/pages/Profile/ButtonSecondary/index.tsx
index c9201b58..72539c8a 100644
--- a/src/pages/MyPage/ButtonSecondary/index.tsx
+++ b/src/pages/Profile/ButtonSecondary/index.tsx
@@ -1,19 +1,21 @@
-import React from 'react';
import { useNavigate } from 'react-router-dom';
+
+import theme from '@styles/theme';
+
+import { StyledText } from '@components/Text/StyledText';
+
import { Button } from './styles';
-import { StyledText } from '../../../components/Text/StyledText';
-import theme from '../../../styles/theme';
const ButtonSecondary: React.FC = () => {
const navigate = useNavigate();
const handleClick = () => {
- navigate('/profile/edit');
+ navigate('/profile/edit');
};
return (
diff --git a/src/pages/MyPage/ButtonSecondary/styles.tsx b/src/pages/Profile/ButtonSecondary/styles.tsx
similarity index 56%
rename from src/pages/MyPage/ButtonSecondary/styles.tsx
rename to src/pages/Profile/ButtonSecondary/styles.tsx
index 715b35c9..af4a1820 100644
--- a/src/pages/MyPage/ButtonSecondary/styles.tsx
+++ b/src/pages/Profile/ButtonSecondary/styles.tsx
@@ -1,15 +1,14 @@
-import styled from 'styled-components';
+import { styled } from 'styled-components';
export const Button = styled.button`
width: 100%;
- margin: 1.25rem auto;
- height: 3.1rem;
+ margin: 16px auto;
+ height: 3.1rem;
text-align: center;
- color: #ff2389;
+ color: ${({ theme }) => theme.colors.brand.primary};
cursor: pointer;
box-sizing: border-box;
border: 1px solid;
border-radius: 10px;
- border-color: #ff2389;
padding: 10px;
`;
diff --git a/src/pages/Profile/NavbarProfile/index.tsx b/src/pages/Profile/NavbarProfile/index.tsx
new file mode 100644
index 00000000..811eca61
--- /dev/null
+++ b/src/pages/Profile/NavbarProfile/index.tsx
@@ -0,0 +1,26 @@
+import { Link } from 'react-router-dom';
+
+import theme from '@styles/theme';
+
+import settingIcon from '@assets/default/setting.svg';
+
+import { StyledText } from '@components/Text/StyledText';
+
+import { Nav, IconContainer } from './styles';
+
+const NavbarProfile: React.FC = () => {
+ return (
+
+ );
+};
+
+export default NavbarProfile;
diff --git a/src/pages/Profile/NavbarProfile/styles.tsx b/src/pages/Profile/NavbarProfile/styles.tsx
new file mode 100644
index 00000000..8845a0c1
--- /dev/null
+++ b/src/pages/Profile/NavbarProfile/styles.tsx
@@ -0,0 +1,31 @@
+import { styled } from 'styled-components';
+
+export const Nav = styled.nav`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 8px 20px;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ background-color: ${({ theme }) => theme.colors.contrast};
+ z-index: 1000;
+ position: sticky;
+`;
+
+export const IconContainer = styled.div`
+ display: flex;
+ align-items: center;
+ margin-right: 18px;
+
+ a {
+ display: flex;
+ align-items: center;
+ }
+
+ img {
+ width: 1.5rem;
+ height: 1.5rem;
+ }
+`;
diff --git a/src/pages/ProfileEdit/index.tsx b/src/pages/Profile/ProfileEdit/index.tsx
similarity index 77%
rename from src/pages/ProfileEdit/index.tsx
rename to src/pages/Profile/ProfileEdit/index.tsx
index f7405b45..9cf565b6 100644
--- a/src/pages/ProfileEdit/index.tsx
+++ b/src/pages/Profile/ProfileEdit/index.tsx
@@ -1,4 +1,26 @@
-import React, { useRef, useEffect, useState } from 'react';
+import { useRef, useEffect, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+
+import { ref, uploadBytes, getDownloadURL } from 'firebase/storage';
+
+import theme from '@styles/theme';
+
+import { getUserInfoApi, patchUserInfoApi } from '@apis/user';
+import { storage } from '@config/firebaseConfig';
+
+import back from '@assets/arrow/left.svg';
+import camera from '@assets/default/camera.svg';
+import imageBasic from '@assets/default/defaultProfile.svg';
+
+import BottomButton from '@components/BottomButton/index';
+import { OODDFrame } from '@components/Frame/Frame';
+import Loading from '@components/Loading/index';
+import Modal from '@components/Modal/index';
+import { StyledText } from '@components/Text/StyledText';
+import TopBar from '@components/TopBar/index';
+
+import type { UserInfoData, PatchUserInfoRequest } from '@apis/user/dto'; // type 명시
+
import {
ProfileEditContainer,
ProfilePic,
@@ -10,24 +32,8 @@ import {
CameraIcon,
UserInfo,
Username,
- EmailInput
+ EmailInput,
} from './styles';
-import { StyledText } from '../../components/Text/StyledText';
-import theme from '../../styles/theme';
-import { OODDFrame } from '../../components/Frame/Frame';
-import { useNavigate } from 'react-router-dom';
-import { ref, uploadBytes, getDownloadURL } from 'firebase/storage';
-import { storage } from '../../config/firebaseConfig';
-import Modal from '../../components/Modal';
-
-import TopBar from '../../components/TopBar';
-import back from '../../assets/arrow/left.svg';
-import BottomButton from '../../components/BottomButton';
-import imageBasic from '../../assets/default/defaultProfile.svg';
-import Loading from '../../components/Loading';
-import camera from '../../assets/default/camera.svg';
-import { getUserInfoApi, patchUserInfoApi } from '../../apis/user'; // API import
-import { UserInfoData, PatchUserInfoRequest } from '../../apis/user/dto'; // DTO import
type ExtendedUserInfoData = UserInfoData & {
birthDate?: string; // 확장된 속성
@@ -47,6 +53,7 @@ const ProfileEdit: React.FC = () => {
const [modalContent, setModalContent] = useState(null);
const [isModalVisible, setIsModalVisible] = useState(false);
const [uploading, setUploading] = useState(false); // 업로드 상태 관리
+ const userId = localStorage.getItem('my_id');
// 사용자 정보 불러오기
useEffect(() => {
@@ -127,8 +134,6 @@ const ProfileEdit: React.FC = () => {
bio: bio || '',
};
- console.log('Payload being sent:', payload);
-
const response = await patchUserInfoApi(payload, storedUserId);
if (response.isSuccess) {
@@ -138,16 +143,16 @@ const ProfileEdit: React.FC = () => {
// 모달 닫힌 후 마이페이지로 이동
setTimeout(() => {
handleModalClose();
- navigate('/mypage');
+ navigate(`/profile/${userId}`);
}, 2000);
} else {
setModalContent('프로필 수정에 실패했습니다.');
setIsModalVisible(true);
}
- } catch (error: any) {
+ } catch (error) {
setModalContent('프로필 수정 중 오류가 발생했습니다.');
setIsModalVisible(true);
- console.error('Error updating user info:', error.response?.data || error.message);
+ console.error('Error updating user info:', error);
}
};
@@ -158,7 +163,7 @@ const ProfileEdit: React.FC = () => {
return (
- navigate(-1)} />
+ navigate(-1)} />
{isModalVisible && (
{
-
+
{nickname || '알수없음'}
-
+
이름
setName(e.target.value)} />
-
+
닉네임
setNickname(e.target.value)} />
-
+
소개글
setBio(e.target.value)} />
-
+
전화번호
setPhoneNumber(e.target.value)} />
-
+
생년월일
setBirthDate(e.target.value)} />
-
+
이메일
setEmail(e.target.value)} />
diff --git a/src/pages/ProfileEdit/styles.tsx b/src/pages/Profile/ProfileEdit/styles.tsx
similarity index 57%
rename from src/pages/ProfileEdit/styles.tsx
rename to src/pages/Profile/ProfileEdit/styles.tsx
index 685b8cbb..56a5170e 100644
--- a/src/pages/ProfileEdit/styles.tsx
+++ b/src/pages/Profile/ProfileEdit/styles.tsx
@@ -1,7 +1,7 @@
-import styled from 'styled-components';
+import { styled } from 'styled-components';
export const ProfileEditContainer = styled.div`
- flex-grow: 1;
+ flex-grow: 1;
width: 100%;
margin: 0 auto;
display: flex;
@@ -14,23 +14,23 @@ export const ProfilePicWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
- margin-bottom: 10px; /* 20px */
+ margin-bottom: 10px;
position: relative;
`;
export const Label = styled.label`
- font-size: 0.875rem; /* 14px */
- color: #333;
+ font-size: 0.875rem;
+ color: ${({ theme }) => theme.colors.gray[700]};
`;
export const Input = styled.input`
- width: 100%; /* Row의 padding에 맞춰 꽉 채우기 */
- padding: 25px; /* 10px padding */
- margin: 10px 0; /* 위아래 간격 조정 */
+ width: 100%;
+ padding: 25px;
+ margin: 10px 0;
border: 0px;
box-sizing: border-box;
border-radius: 10px;
- background-color: #f0f0f0; /* 박스 내부 회색 배경 */
+ background-color: ${({ theme }) => theme.colors.background.secondary};
text-align: left;
`;
@@ -43,20 +43,20 @@ export const Button = styled.button`
height: 1.7rem;
padding: 0.3rem;
border-radius: 50%;
- background-color: white;
- box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.2); /* 그림자 효과 */
- border: 1px solid #ddd; /* 아이콘 테두리 */
+ background-color: ${({ theme }) => theme.colors.background.primary};
+ box-shadow: 0px 2px 4px ${({ theme }) => `${theme.colors.black}33`};
+ border: 1px solid ${({ theme }) => theme.colors.gray[300]};
cursor: pointer;
`;
export const ProfilePic = styled.div`
- width: 7.25rem; /* 116px */
- height: 7.25rem; /* 116px */
+ width: 7.25rem;
+ height: 7.25rem;
flex-shrink: 0;
border-radius: 50%;
overflow: hidden;
- margin-top: 2.125rem; /* 34px */
- margin-bottom: 15px; /* 20px */
+ margin-top: 2.125rem;
+ margin-bottom: 15px;
img {
width: 100%;
@@ -69,25 +69,25 @@ export const CameraIcon = styled.img``;
export const Row = styled.div`
display: flex;
- flex-direction: column; /* 세로 배치 */
+ flex-direction: column;
align-items: stretch;
width: 100%;
- margin-top: 0px; /* Row 간격 10px */
- padding: 0px 20px; /* 좌우 여백 20px */
+ margin-top: 0px;
+ padding: 0px 20px;
${Label} {
- width: 6.25rem; /* 100px */
+ width: 6.25rem;
}
`;
export const EmailInput = styled.input`
- margin-bottom: 120px;
- width: 100%; /* Row의 padding에 맞춰 꽉 채우기 */
- padding: 25px; /* 10px padding */
+ margin-bottom: 120px;
+ width: 100%;
+ padding: 25px;
border: 0px;
box-sizing: border-box;
border-radius: 10px;
- background-color: #f0f0f0; /* 박스 내부 회색 배경 */
+ background-color: ${({ theme }) => theme.colors.background.secondary};
text-align: left;
`;
@@ -98,12 +98,11 @@ export const FileInput = styled.input`
export const UserInfo = styled.div``;
export const Username = styled.button`
- color: #000;
+ color: ${({ theme }) => theme.colors.black};
font-family: Pretendard;
font-size: 22px;
font-style: normal;
font-weight: 700;
- line-height: 136.4%; /* 30.008px */
+ line-height: 136.4%;
letter-spacing: -0.427px;
`;
-
diff --git a/src/pages/Profile/index.tsx b/src/pages/Profile/index.tsx
new file mode 100644
index 00000000..76151556
--- /dev/null
+++ b/src/pages/Profile/index.tsx
@@ -0,0 +1,187 @@
+import { useState, useEffect } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+
+import theme from '@styles/theme';
+
+import { createMatchingApi } from '@apis/matching';
+import { getUserPostListApi } from '@apis/post';
+import { getUserInfoApi } from '@apis/user';
+
+import BackSvg from '@assets/arrow/left.svg';
+import imageBasic from '@assets/default/defaultProfile.svg';
+import MoreSvg from '@assets/default/more.svg';
+import button_plus from '@assets/default/plus.svg';
+
+import CommentBottomSheet from '@components/BottomSheet/CommentBottomSheet';
+import OptionsBottomSheet from '@components/BottomSheet/OptionsBottomSheet';
+import { OODDFrame } from '@components/Frame/Frame';
+import Loading from '@components/Loading';
+import Modal from '@components/Modal';
+import NavBar from '@components/NavBar';
+import PostItem from '@components/PostItem';
+import { StyledText } from '@components/Text/StyledText';
+import TopBar from '@components/TopBar';
+import UserProfile from '@components/UserProfile';
+
+import type { UserPostSummary } from '@apis/post/dto'; // type 명시
+import type { UserInfoData } from '@apis/user/dto'; // type 명시
+
+import ButtonSecondary from './ButtonSecondary/index'; // 상대 경로 index 명시
+import NavbarProfile from './NavbarProfile/index'; // 상대 경로 index 명시
+
+import {
+ ProfileContainer,
+ Header,
+ StatsContainer,
+ Stat,
+ StatNumber,
+ StatLabel,
+ PostsContainer,
+ AddButton,
+ NoPostWrapper,
+ Button,
+} from './styles';
+
+const Profile: React.FC = () => {
+ const { userId } = useParams<{ userId: string }>();
+ const profileUserId = Number(userId);
+ const loggedInUserId = Number(localStorage.getItem('current_user_id'));
+
+ const [isLoading, setIsLoading] = useState(true);
+ const [posts, setPosts] = useState([]);
+ const [totalPosts, setTotalPosts] = useState